Browse Source

Merge branch 'master' of rav.triplestart.com:jmudaka/stagfe2

Josh 3 years ago
parent
commit
2e3d8fbdd2
30 changed files with 785 additions and 66 deletions
  1. 9 0
      app/Http/Controllers/NoteController.php
  2. 40 0
      app/Models/Point.php
  3. 142 0
      public/js/dq.js
  4. 1 1
      public/js/shortcut.js
  5. 5 1
      public/js/stag-popup.js
  6. 18 0
      resources/views/app/dq-engine/edit.blade.php
  7. 1 0
      resources/views/app/dq-engine/field/date.blade.php
  8. 1 0
      resources/views/app/dq-engine/field/numeric.blade.php
  9. 8 0
      resources/views/app/dq-engine/field/radios.blade.php
  10. 6 0
      resources/views/app/dq-engine/field/select.blade.php
  11. 1 0
      resources/views/app/dq-engine/field/text.blade.php
  12. 1 0
      resources/views/app/dq-engine/field/time.blade.php
  13. 14 0
      resources/views/app/dq-engine/line.blade.php
  14. 25 0
      resources/views/app/dq-engine/read.blade.php
  15. 84 0
      resources/views/app/dq-templates/hpi/core.json
  16. 129 0
      resources/views/app/dq-templates/tech.json
  17. 20 0
      resources/views/app/dq-templates/test.json
  18. 1 1
      resources/views/app/patient/allergies-center.blade.php
  19. 1 1
      resources/views/app/patient/careteam-center.blade.php
  20. 1 1
      resources/views/app/patient/goals-center.blade.php
  21. 1 1
      resources/views/app/patient/medications-center.blade.php
  22. 114 0
      resources/views/app/patient/note/edit-hpi.blade.php
  23. 37 0
      resources/views/app/patient/note/hpi-log.blade.php
  24. 28 0
      resources/views/app/patient/note/last-hpi.blade.php
  25. 30 13
      resources/views/app/patient/problems-center.blade.php
  26. 45 43
      resources/views/app/patient/wizard-partials/common-fields.blade.php
  27. 16 4
      resources/views/app/patient/wizard-partials/common-script.blade.php
  28. 3 0
      resources/views/layouts/patient.blade.php
  29. 1 0
      resources/views/layouts/template.blade.php
  30. 2 0
      routes/web.php

+ 9 - 0
app/Http/Controllers/NoteController.php

@@ -282,6 +282,15 @@ class NoteController extends Controller
         ];
     }
 
+    // edit hpi (structured)
+    public function editHPI(Note $note, Point $point) {
+        return view('app.patient.note.edit-hpi', compact('note', 'point'));
+    }
+
+    public function hpiLog(Note $note, Point $point) {
+        return view('app.patient.note.hpi-log', compact('note', 'point'));
+    }
+
     // review log
     public function reviewLog(Point $point) {
         return view('app.patient.note.review-log', compact('point'));

+ 40 - 0
app/Models/Point.php

@@ -219,4 +219,44 @@ class Point extends Model
             ]
         ];
     }
+
+    public static function fillPointStateAndBadge(Point $point, Note $note)
+    {
+
+        // state
+        if (!$point->is_removed) {
+            $point->state = "ACTIVE";
+        } elseif ($point->is_removed) {
+            if (!$point->is_removed_due_to_entry_error) {
+                $point->state = "HISTORIC";
+            } else {
+                $point->state = "ENTRY_ERROR";
+            }
+        }
+
+        // added/removed info
+        if ($point->state === 'ACTIVE') {
+            if ($point->added_in_note_id === $note->id && $point->addition_reason_category === 'DURING_VISIT') {
+                $point->badge = 'Added During Visit';
+            } elseif ($point->added_in_note_id === $note->id && $point->addition_reason_category === 'ON_INTAKE') {
+                $point->badge = 'New Record - Pre-existing';
+            } elseif ($point->added_in_note_id !== $note->id) {
+                $point->badge = 'Record Present Before Visit';
+            }
+        } elseif ($point->state === 'HISTORIC') {
+            if ($point->removed_in_note_id === $note->id && $point->removal_reason_category === 'DURING_VISIT') {
+                $point->badge = 'Removed During Visit';
+            } elseif ($point->removed_in_note_id === $note->id && $point->removal_reason_category === 'ON_INTAKE') {
+                $point->badge = 'Marked Removed on Intake';
+            } elseif ($point->removed_in_note_id !== $note->id) {
+                $point->badge = 'Historic Record Removed In A Previous Visit';
+            }
+        } elseif ($point->state === 'ENTRY_ERROR') {
+            if ($point->removed_in_note_id === $note->id) {
+                $point->badge = 'Marked as Entry Error During Visit';
+            }
+        }
+
+        return $point;
+    }
 }

+ 142 - 0
public/js/dq.js

@@ -0,0 +1,142 @@
+(function() {
+    window.initDQ = function() {
+        $(document)
+            .off('change input paste', '.dq-edit-container input, .dq-edit-container textarea, .dq-edit-container select')
+            .on('change input paste', '.dq-edit-container input, .dq-edit-container textarea, .dq-edit-container select', function() {
+                let key = $(this).closest('.dq-line').attr('dq-key'),
+                    dataElem = $(this).closest('.dq-edit-container').find('>.dq-data-map').first(),
+                    current = JSON.parse(dataElem.text());
+                current[key] = $(this).val();
+                let serialized = JSON.stringify(current);
+                dataElem.text(serialized);
+                $(this).closest('form').find('input[name="data"]').val(serialized);
+                let fullData = {
+                    lines: JSON.parse($(this).closest('.dq-edit-container').find('>.dq-definition').first().text()),
+                    dataMap: current
+                };
+                $(this).closest('form').find('input[name="data"]').val(JSON.stringify(fullData));
+
+                runDQConditions($(this).closest('.dq-edit-container'));
+            });
+        $(document)
+            .off('input.auto-grow', '.dq-edit-container textarea')
+            .on('input.auto-grow', '.dq-edit-container textarea', function() {
+                this.style.minHeight = "calc(1.5em + .5rem + 2px)";
+                this.style.height = "calc(1.5em + .5rem + 2px)";
+                this.style.height = (this.scrollHeight)+"px";
+            });
+    };
+
+    function resolveAtomicCondition(_condition, _dataMap) {
+        if(!_condition.hasOwnProperty('key') || !_condition.hasOwnProperty('value')) {
+            _condition.resolution = false;
+            return;
+        }
+        let key = _condition.key, op = _condition.hasOwnProperty('op') ? _condition.op : 'eq';
+        let lhs = _dataMap[key], rhs = _condition.value;
+        switch(op) {
+            case 'eq':
+                _condition.resolution = (lhs == rhs); // NOTE: using == instead of === on purpose
+                break;
+            case 'lt':
+                _condition.resolution = (+lhs < +rhs);
+                break;
+            case 'lte':
+                _condition.resolution = (+lhs <= +rhs);
+                break;
+            case 'gt':
+                _condition.resolution = (+lhs > +rhs);
+                break;
+            case 'gte':
+                _condition.resolution = (+lhs >= +rhs);
+                break;
+            default:
+                _condition.resolution = false;
+        }
+    }
+
+    function resolveAllAtomicConditions(_conditions, _dataMap) {
+        if(Array.isArray(_conditions)) {
+            for (let i = 0; i < _conditions.length; i++) {
+                resolveAllAtomicConditions(_conditions[i], _dataMap);
+            }
+        }
+        else if(typeof _conditions === 'object') {
+            resolveAtomicCondition(_conditions, _dataMap);
+        }
+    }
+
+    function reduceConditionsIntoResolutions(_conditions) {
+        for (let i = 0; i < _conditions.length; i++) {
+            // if simple object, resolve
+            if(!Array.isArray(_conditions[i]) && typeof _conditions[i] === 'object') {
+                _conditions.splice(i, 1, _conditions[i].resolution);
+            }
+            else if(Array.isArray(_conditions[i])) {
+                reduceConditionsIntoResolutions(_conditions[i]);
+            }
+        }
+    }
+
+    function combineResolutionListsIntoSingleResolutions(_conditions) {
+        console.log('ALIX 1', _conditions);
+        for (let i = 0; i < _conditions.length; i++) {
+            if(Array.isArray(_conditions[i])) {
+                _conditions[i] = combineResolutionListsIntoSingleResolutions(_conditions[i]);
+            }
+        }
+        console.log('ALIX 2', _conditions);
+
+        // at this point, the array will have only booleans and "AND", "OR" combinators
+        let resolution = _conditions[0];
+        for (let i = 1; i < _conditions.length; i+=2) {
+            if(_conditions[i] === 'AND') {
+                resolution = resolution && _conditions[i + 1];
+            }
+            else if(_conditions[i] === 'OR') {
+                resolution = resolution || _conditions[i + 1];
+            }
+        }
+
+        return resolution;
+    }
+
+    window.runDQConditions = function(_parent) {
+
+        _parent.find('.dq-line.has-pre-condition').each(function() {
+            let conditions = JSON.parse($(this).find('>.dq-pre-condition').first().text()),
+                dataMap = JSON.parse($(this).closest('.dq-edit-container').find('>.dq-data-map').first().text());
+
+            resolveAllAtomicConditions(conditions, dataMap);
+
+            // if object, single condition with key, op and value
+            if(!Array.isArray(conditions) && typeof conditions === 'object') {
+                if(conditions.resolution) {
+                    $(this).removeClass('d-none');
+                }
+                else {
+                    $(this).addClass('d-none');
+                }
+            }
+            // else if array - means list of conditions with 'and' or 'or' separators - array size MUST be an odd number
+            else if(Array.isArray(conditions)) {
+
+                // goal is to reduce each item in the array into a single boolean - recursively
+                reduceConditionsIntoResolutions(conditions);
+
+                conditions = combineResolutionListsIntoSingleResolutions(conditions);
+
+                if(conditions) {
+                    $(this).removeClass('d-none');
+                }
+                else {
+                    $(this).addClass('d-none');
+                }
+
+            }
+
+        });
+
+    }
+    addMCInitializer('dq-edit', initDQ, '.dq-edit-container');
+}).call(window);

+ 1 - 1
public/js/shortcut.js

@@ -165,7 +165,7 @@
                     case 27:
                         if(!isVisible()) return;
                         consumed = !discard();
-                        if(consumed) markEventAsConsumed(e);
+                        if(consumed) markEventAsConsumed(_e);
                         break;
                     case 38:
                         if(!isVisible()) return;

+ 5 - 1
public/js/stag-popup.js

@@ -80,7 +80,8 @@ function convertContentLinksForInPopupNavigation(popup) {
         }
     });
 }
-function openDynamicStagPopup(url, initer, title, updateParent, style = '', replace = false) {
+function openDynamicStagPopup(_url, initer, title, updateParent, style = '', replace = false) {
+    let url = _url;
     if(url.indexOf('popupmode') === -1) {
         url += (url.indexOf('?') !== -1 ? '&' : '?') + 'popupmode=1';
     }
@@ -138,6 +139,9 @@ function openDynamicStagPopup(url, initer, title, updateParent, style = '', repl
         runMCInitializer('pro-suggest'); // not the place for this! Move to better place.
         initMoes();
         hideMask();
+    }).fail(() => {
+        toastr.error('Unable to open ' + _url);
+        hideMask();
     });
 }
 function isDynamicStagPopupPresent() {

+ 18 - 0
resources/views/app/dq-engine/edit.blade.php

@@ -0,0 +1,18 @@
+<?php
+$template = json_decode(file_get_contents(resource_path('views/app/dq-templates/' . $template)));
+$loadedData = $data;
+if(!$loadedData || !@$loadedData->lines || !@$loadedData->dataMap) {
+    $loadedData = $template;
+}
+?>
+<div class="dq-edit-container">
+    <div class="dq-definition d-none"><?= htmlentities(json_encode($loadedData->lines)) ?></div>
+    <div class="dq-data-map d-none"><?= json_encode($loadedData->dataMap) ?></div>
+    @foreach ($loadedData->lines as $line)
+        @include('app.dq-engine.line', compact('line'))
+    @endforeach
+</div>
+
+
+
+

+ 1 - 0
resources/views/app/dq-engine/field/date.blade.php

@@ -0,0 +1 @@
+<input type="date" class="form-control form-control-sm" id="" value="{{ @$loadedData->dataMap->{$line->key} ?: '' }}">

+ 1 - 0
resources/views/app/dq-engine/field/numeric.blade.php

@@ -0,0 +1 @@
+<input type="number" class="form-control form-control-sm" id="" value="{{ @$loadedData->dataMap->{$line->key} ?: '' }}">

+ 8 - 0
resources/views/app/dq-engine/field/radios.blade.php

@@ -0,0 +1,8 @@
+<div class="d-flex align-items-center">
+    @foreach(@$line->options as $option)
+        <label class="d-inline-flex align-items-center my-0 mr-3">
+            <input type="radio" name="{{$line->key}}" value="{{$option}}" class="my-0 mr-1" {{ @$loadedData->dataMap->{$line->key} == $option ? 'checked' : '' }}>
+            <span>{{$option}}</span>
+        </label>
+    @endforeach
+</div>

+ 6 - 0
resources/views/app/dq-engine/field/select.blade.php

@@ -0,0 +1,6 @@
+<select class="form-control form-control-sm" id="">
+    <option value="">-- select --</option>
+    @foreach(@$line->options as $option)
+        <option value="{{$option}}" {{ @$loadedData->dataMap->{$line->key} == $option ? 'selected' : '' }}>{{$option}}</option>
+    @endforeach
+</select>

+ 1 - 0
resources/views/app/dq-engine/field/text.blade.php

@@ -0,0 +1 @@
+<textarea class="form-control form-control-sm auto-grow" id="" rows="1">{{ @$loadedData->dataMap->{$line->key} ?: '' }}</textarea>

+ 1 - 0
resources/views/app/dq-engine/field/time.blade.php

@@ -0,0 +1 @@
+<input type="time" class="form-control form-control-sm" id="" value="{{ @$loadedData->dataMap->{$line->key} ?: '' }}">

+ 14 - 0
resources/views/app/dq-engine/line.blade.php

@@ -0,0 +1,14 @@
+<div class="mb-2 dq-line {{@$line->preConditions ? 'has-pre-condition d-none' : ''}}" dq-key="{{$line->key}}">
+    @if(@$line->preConditions)
+    <div class="dq-pre-condition d-none">{!! json_encode($line->preConditions) !!}</div>
+    @endif
+    <label for="" class="mb-1 text-secondary">{{$line->helpText}}</label>
+    @include('app.dq-engine.field.' . $line->fieldType)
+    @if(@($line->lines) && count($line->lines))
+        <div class="pt-2 pl-4">
+            @foreach ($line->lines as $line)
+                @include('app.dq-engine.line', compact('line'))
+            @endforeach
+        </div>
+    @endif
+</div>

+ 25 - 0
resources/views/app/dq-engine/read.blade.php

@@ -0,0 +1,25 @@
+<?php
+$template = json_decode(file_get_contents(resource_path('views/app/dq-templates/' . $template)));
+$loadedData = $data;
+if(!$loadedData || !@$loadedData->lines || !@$loadedData->dataMap) {
+    $loadedData = $template;
+}
+if(!function_exists('_renderDQLine')) {
+    function _renderDQLine($_line, $loadedData) {
+        if (!!@$loadedData->dataMap->{$_line->key}) {
+            $result = str_replace('{value}', $loadedData->dataMap->{$_line->key}, $_line->resultSummary);
+            echo '<span class="mr-2 text-nowrap">• ' . $result . '</span>';
+            if (@$_line->lines) {
+                foreach ($_line->lines as $line) {
+                    _renderDQLine($line, $loadedData);
+                }
+            }
+        }
+    }
+}
+?>
+<div class="dq-read-container">
+    @foreach ($loadedData->lines as $line)
+        <?php _renderDQLine($line, $loadedData); ?>
+    @endforeach
+</div>

+ 84 - 0
resources/views/app/dq-templates/hpi/core.json

@@ -0,0 +1,84 @@
+{
+  "lines": [
+    {
+      "key": "onset",
+      "helpText": "Onset: When did the problem begin? (date)",
+      "resultSummary": "<b>Onset</b>: {value}",
+      "fieldType": "text"
+    },
+    {
+      "key": "location",
+      "helpText": "Location: Where is the problem located?",
+      "resultSummary": "<b>Location</b>: {value}",
+      "fieldType": "text"
+    },
+    {
+      "key": "duration",
+      "helpText": "Duration: How long has the problem been going on for?",
+      "resultSummary": "<b>Duration</b>: {value}",
+      "fieldType": "text"
+    },
+    {
+      "key": "characterization",
+      "helpText": "Characterization: How does the patient describe the problem?",
+      "resultSummary": "<b>Characterization</b>: {value}",
+      "fieldType": "text"
+    },
+    {
+      "key": "alleviating_factors",
+      "helpText": "Alleviating Factors: What makes it better?",
+      "resultSummary": "<b>Alleviating factors</b>: {value}",
+      "fieldType": "text"
+    },
+    {
+      "key": "aggravating_factors",
+      "helpText": "Aggravating Factors: What makes it worse?",
+      "resultSummary": "<b>Aggravating factors</b>: {value}",
+      "fieldType": "text"
+    },
+    {
+      "key": "radiation",
+      "helpText": "Radiation: Does the problem move or stay in one location?",
+      "resultSummary": "<b>Radiation</b>: {value}",
+      "fieldType": "text"
+    },
+    {
+      "key": "temporal_factor",
+      "helpText": "Temporal factor: Is the problem worse (or better) at a certain time of the day?",
+      "resultSummary": "<b>Temporal factor</b>: {value}",
+      "fieldType": "text"
+    },
+    {
+      "key": "severity",
+      "helpText": "Severity: Using a scale of 1 to 10, 1 being the least, 10 being the worst, how does the patient rate the problem?",
+      "resultSummary": "<b>Severity</b>: {value}",
+      "fieldType": "radios",
+      "options": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+    },
+    {
+      "key": "coping",
+      "helpText": "Coping: How do you cope?",
+      "resultSummary": "<b>Coping</b>: {value}",
+      "fieldType": "text"
+    },
+    {
+      "key": "emotional_impact",
+      "helpText": "Emotional Impact: How does this affect you emotionally?",
+      "resultSummary": "<b>Emotional Impact</b>: {value}",
+      "fieldType": "text"
+    }
+  ],
+  "dataMap": {
+    "onset" : null,
+    "location" : null,
+    "duration" :  null,
+    "characterization" : null,
+    "alleviating_factors" :  null,
+    "aggravating_factors" : null,
+    "radiation" :  null,
+    "temporal_factor" : null,
+    "severity" :  null,
+    "coping" : null,
+    "emotional_impact" :  null
+  }
+}

+ 129 - 0
resources/views/app/dq-templates/tech.json

@@ -0,0 +1,129 @@
+{
+  "lines": [
+    {
+      "key": "fe_or_be",
+      "helpText": "Front end or Back end?",
+      "resultSummary": "<b>Tech</b>: {value}",
+      "fieldType": "radios",
+      "options": ["Front End", "Back End"]
+    },
+    {
+      "key": "fav_fe_tech",
+      "helpText": "Favorite Front End Framework?",
+      "resultSummary": "<b>Framework</b>: {value}",
+      "fieldType": "radios",
+      "options": ["Angular", "React", "Ember", "NextJS"],
+      "preConditions": {
+        "key": "fe_or_be",
+        "value": "Front End"
+      }
+    },
+    {
+      "key": "react_version",
+      "helpText": "React Version:",
+      "resultSummary": "<b>React v</b>: {value}",
+      "fieldType": "numeric",
+      "preConditions": [
+        {
+          "key": "fe_or_be",
+          "value": "Front End"
+        },
+        "AND",
+        {
+          "key": "fav_fe_tech",
+          "value": "React"
+        }
+      ]
+    },
+    {
+      "key": "angular_version",
+      "helpText": "Angular Version:",
+      "resultSummary": "<b>Angular v</b>: {value}",
+      "fieldType": "numeric",
+      "preConditions": [
+        {
+          "key": "fe_or_be",
+          "value": "Front End"
+        },
+        "AND",
+        {
+          "key": "fav_fe_tech",
+          "value": "Angular"
+        }
+      ]
+    },
+    {
+      "key": "fav_color",
+      "helpText": "Favorite Color?",
+      "resultSummary": "<b>Color</b>: {value}",
+      "fieldType": "select",
+      "options": ["Red", "Blue", "Yellow", "Orange"]
+    },
+    {
+      "key": "condition_test_1",
+      "helpText": "So you're either a front-end angular dev - OR - you like yellow!",
+      "resultSummary": "<b>Ex: </b>: {value}",
+      "fieldType": "text",
+      "preConditions": [
+        [
+          {
+            "key": "fe_or_be",
+            "value": "Front End"
+          },
+          "AND",
+          {
+            "key": "fav_fe_tech",
+            "value": "Angular"
+          }
+        ],
+        "OR",
+        {
+          "key": "fav_color",
+          "value": "Yellow"
+        }
+      ]
+    },
+    {
+      "key": "condition_test_2",
+      "helpText": "So you're a (front-end AND (react or angular) dev) AND (you like red OR blue)!",
+      "resultSummary": "<b>Ex: </b>: {value}",
+      "fieldType": "text",
+      "preConditions": [
+        [
+          {
+            "key": "fe_or_be",
+            "value": "Front End"
+          },
+          "AND",
+          [
+            {
+              "key": "fav_fe_tech",
+              "value": "Angular"
+            },
+            "OR",
+            {
+              "key": "fav_fe_tech",
+              "value": "React"
+            }
+          ]
+        ],
+        "AND",
+        [
+          {
+            "key": "fav_color",
+            "value": "Red"
+          },
+          "OR",
+          {
+            "key": "fav_color",
+            "value": "Blue"
+          }
+        ]
+      ]
+    }
+  ],
+  "dataMap": {
+    "fe_or_be" : null,
+    "location" : null
+  }
+}

+ 20 - 0
resources/views/app/dq-templates/test.json

@@ -0,0 +1,20 @@
+{
+  "lines": [
+    {
+      "key": "onset",
+      "helpText": "When did the problem begin?",
+      "resultSummary": "<b>Onset</b>: {value}",
+      "fieldType": "date"
+    },
+    {
+      "key": "location",
+      "helpText": "Where is the CC located?",
+      "resultSummary": "<b>Location</b>: {value}",
+      "fieldType": "time"
+    }
+  ],
+  "dataMap": {
+    "onset" : null,
+    "location" : null
+  }
+}

+ 1 - 1
resources/views/app/patient/allergies-center.blade.php

@@ -161,7 +161,7 @@ list($allergies, $counts) = Point::getPointsOfCategoryExtended($patient, 'ALLERG
 
         <div class="d-flex align-items-center">
             <div class="mt-1 w-100 border p-3 bg-aliceblue border-info rounded">
-                <form action="/api/visitPoint/addTopLevel" class="mcp-theme-1 w-100" id="frm-add-allergy">
+                <form action="/api/visitPoint/addTopLevel" class="mcp-theme-1 w-100" id="frm-add-allergy" novalidate>
                     <input type="hidden" name="noteUid" value="<?= $note->uid ?>">
                     <input type="hidden" name="category" value="ALLERGY">
                     <input type="hidden" name="data">

+ 1 - 1
resources/views/app/patient/careteam-center.blade.php

@@ -196,7 +196,7 @@ list($careTeamMembers, $counts) = Point::getPointsOfCategoryExtended($patient, '
 
         <div class="d-flex align-items-center">
             <div class="mt-1 w-100 border p-3 bg-aliceblue border-info rounded">
-                <form action="/api/visitPoint/addTopLevel" class="mcp-theme-1 w-100" id="frm-add-care-team-member">
+                <form action="/api/visitPoint/addTopLevel" class="mcp-theme-1 w-100" id="frm-add-care-team-member" novalidate>
                     <input type="hidden" name="noteUid" value="<?= $note->uid ?>">
                     <input type="hidden" name="category" value="CARE_TEAM_MEMBER">
                     <input type="hidden" name="data">

+ 1 - 1
resources/views/app/patient/goals-center.blade.php

@@ -157,7 +157,7 @@ list($goals, $counts) = Point::getPointsOfCategoryExtended($patient, 'GOAL', $no
 
         <div class="d-flex align-items-center">
             <div class="mt-1 w-100 border p-3 bg-aliceblue border-info rounded">
-                <form action="/api/visitPoint/addTopLevel" class="mcp-theme-1 w-100" id="frm-add-goal">
+                <form action="/api/visitPoint/addTopLevel" class="mcp-theme-1 w-100" id="frm-add-goal" novalidate>
                     <input type="hidden" name="noteUid" value="<?= $note->uid ?>">
                     <input type="hidden" name="category" value="GOAL">
                     <input type="hidden" name="data">

+ 1 - 1
resources/views/app/patient/medications-center.blade.php

@@ -172,7 +172,7 @@ list($medications, $counts) = Point::getPointsOfCategoryExtended($patient, 'MEDI
     <div class="d-flex align-items-center">
         <div class="mt-1 w-100 border p-3 bg-aliceblue border-info rounded">
             <!--<a href="#" start show class="btn btn-sm btn btn-outline-primary">+ Add new medication, prescribed during this visit</a>-->
-            <form action="/api/visitPoint/addTopLevel" class="mcp-theme-1 w-100" id="frm-add-medication">
+            <form action="/api/visitPoint/addTopLevel" class="mcp-theme-1 w-100" id="frm-add-medication" novalidate>
                 <input type="hidden" name="noteUid" value="<?= $note->uid ?>">
                 <input type="hidden" name="category" value="MEDICATION">
                 <input type="hidden" name="data">

+ 114 - 0
resources/views/app/patient/note/edit-hpi.blade.php

@@ -0,0 +1,114 @@
+<div class="p-3">
+
+    <!-- basic info regarding problem for context -->
+    <?php
+    if ($point->data) {
+        $point->data = json_decode($point->data);
+    }
+    $point = \App\Models\Point::fillPointStateAndBadge($point, $note);
+    ?>
+    <div class="border bg-aliceblue d-flex align-items-baseline p-2 mb-3">
+        <b>{{$point->state}}</b>
+        @if($point->badge)
+            <span class="mx-2 text-secondary">|</span>
+            <span class="">{{$point->badge}}</span>
+        @endif
+        @if(@($point->data->start_date))
+            <span class="mx-2 text-secondary">|</span>
+            <span class="">Start: {{friendly_date($point->data->start_date)}}</span>
+        @endif
+        @if(@($point->removal_effective_date))
+            <span class="mx-2 text-secondary">|</span>
+            <span class="">End: {{friendly_date($point->removal_effective_date)}}</span>
+        @endif
+    </div>
+
+    <?php
+    $currentReview = '';
+    $currentValue = '';
+    $previousValue = '';
+    $previousChildReview = null;
+    if ($point->lastChildReview && $point->last_child_review_point_scoped_note_id === $note->id) {
+        $parsedReview = json_decode($point->lastChildReview->data);
+        $currentReview = $parsedReview;
+        if(@$parsedReview->value) {
+            $currentValue = $parsedReview->value;
+            $previousChildReview = \App\Models\Point::where('id', '<', $point->lastChildReview->id)
+                ->where('category', 'REVIEW')
+                ->where('parent_point_id', $point->id)
+                ->orderBy('id', 'DESC')
+                ->first();
+            if($previousChildReview && $previousChildReview->data) {
+                $parsedReview = json_decode($previousChildReview->data);
+                $previousValue = $parsedReview->value;
+            }
+        }
+    }
+    else {
+        $previousChildReview = \App\Models\Point::where('parent_point_id', $point->id)
+            ->where('category', 'REVIEW')
+            ->orderBy('id', 'DESC')
+            ->first();
+        if($previousChildReview && $previousChildReview->data) {
+            $parsedReview = json_decode($previousChildReview->data);
+            if(@$parsedReview->value) {
+                $previousValue = $parsedReview->value;
+            }
+        }
+    }
+    ?>
+
+    <?php if($previousValue): ?>
+    <div class="mb-2">
+        <div class="d-flex align-items-baseline mb-1">
+            <span class="text-secondary">Previous HPI - <?= friendlier_date($previousChildReview->created_at) ?></span>
+        </div>
+        <div class="p-2 bg-light border inline-html-container"><?= $previousValue ?></div>
+    </div>
+    <?php endif; ?>
+
+    <?php
+    if(!@$template) {
+        $template = 'hpi/core.json';
+    }
+    $patient = $note->client;
+    $data = $currentReview ? $currentReview : null;
+    $segment = null;
+    if($note->id !== $patient->core_note_id) {
+        $segment = $note->getSegmentByInternalName('intake_problems');
+    }
+    ?>
+    <div id="edit-hpi-{{$point->id}}">
+
+        <div moe relative class="d-block">
+            <form show url="/api/visitPoint/upsertChildReview" class="mcp-theme-1" right>
+                <input type="hidden" name="uid" value="<?= $point->uid ?>">
+                <input type="hidden" name="noteUid" value="<?= $note->uid ?>">
+                @if(!!$segment)
+                    <input type="hidden" name="segmentUid" value="<?= $segment->uid ?>">
+                @endif
+
+                <input type="hidden" name="data">
+
+                @include('app.dq-engine.edit', compact('template', 'note', 'patient', 'point', 'data'))
+
+                <div>
+                    <button submit class="btn btn-sm btn-primary mr-2">Save</button>
+                    <button cancel class="btn btn-sm btn-default border" onclick="closeStagPopup()">Close</button>
+                </div>
+            </form>
+        </div>
+
+    </div>
+
+    <script src="/js/dq.js?v={{config('app.asset_version')}}"></script>
+    <script>
+        (function() {
+            function init() {
+                initDQ();
+            }
+            addMCInitializer('edit-hpi-{{$point->id}}', init, '#edit-hpi-{{$point->id}}');
+        }).call(window);
+    </script>
+
+</div>

+ 37 - 0
resources/views/app/patient/note/hpi-log.blade.php

@@ -0,0 +1,37 @@
+<div class="p-3">
+    <?php $numReviews = 0; ?>
+    @foreach($point->childReviews as $record)
+        @if(@$record->data)
+            <?php $numReviews++; ?>
+            <div class="border mb-3">
+                <div class="border-bottom p-2">
+                    <span class="text-secondary text-sm">Reviewed on </span>
+                    @if($record->client->core_note_id !== $record->note->id)
+                        <a native target="_blank" class="text-sm"
+                           href="<?= route('patients.view.notes.view.dashboard', ['patient' => $record->client, 'note' => $record->note]) ?>">
+                            <?= friendlier_date_time($record->created_at) ?>
+                        </a>
+                    @else
+                        <span class="text-sm"><?= friendlier_date_time($record->created_at) ?> from the patient's chart</span>
+                    @endif
+                </div>
+                <div class="p-2">
+                    <?php
+                    if(!@$template) $template = 'hpi/core.json';
+                    $review = json_decode($record->data);
+                    $data = $review ? $review : null;
+                    $patient = $note->client;
+                    ?>
+                    @include('app.dq-engine.read', compact('template', 'note', 'patient', 'point', 'data'))
+                </div>
+            </div>
+        @endif
+    @endforeach
+    @if(!$numReviews)
+        <div class="border mb-3">
+            <div class="p-2">
+                No HPI present
+            </div>
+        </div>
+    @endif
+</div>

+ 28 - 0
resources/views/app/patient/note/last-hpi.blade.php

@@ -0,0 +1,28 @@
+<?php if ($point->lastChildReview): ?>
+<?php $parsedReview = json_decode($point->lastChildReview->data); ?>
+<div class="<?= $point->last_child_review_point_scoped_note_id === $note->id ? 'bg-warning-mellow p-2 rounded' : '' ?>">
+    <div>
+        <?php
+        if(!@$template) $template = 'hpi/core.json';
+        $data = $parsedReview ? $parsedReview : null;
+        ?>
+        @include('app.dq-engine.read', compact('template', 'note', 'patient', 'point', 'data'))
+    </div>
+
+    <?php if ($point->last_child_review_point_scoped_note_id === $patient->core_note_id): ?>
+    <span class="text-sm">(reviewed on the patient's chart)</span>
+    <?php else: ?>
+    <?php if ($point->last_child_review_point_scoped_note_id === $note->id): ?>
+    <span class="text-sm">(reviewed on this note)</span>
+    <?php else: ?>
+    <a native target="_blank"
+       href="<?= route('patients.view.notes.view.dashboard', ['patient' => $patient, 'note' => $point->lastChildReviewNote]) ?>">
+        <?= friendlier_date_time($point->last_child_review_effective_date) ?>
+    </a>
+    <?php endif; ?>
+    <?php endif; ?>
+
+</div>
+<?php else: ?>
+<span class="text-secondary text-sm">None</span>
+<?php endif; ?>

+ 30 - 13
resources/views/app/patient/problems-center.blade.php

@@ -68,21 +68,38 @@ $ccSegment = $note->getSegmentByInternalName('chief_complaint');
                 </td>
                 <td>
                     <div class="d-flex align-items-start">
-                        <div class="flex-grow-1">
+                        {{--<div class="flex-grow-1">
                             <?php
                             $point = $problem;
                             include resource_path('views/app/patient/segment-templates/_child_review/last-review.php');
                             ?>
+                        </div>--}}
+                        <div class="flex-grow-1">
+                            @include('app.patient.note.last-hpi')
                         </div>
-                        <div class="d-inline-flex flex-nowrap">
-                            <a class="pl-2 view-review-log"
-                               native target="_blank"
-                               open-in-stag-popup
-                               popup-style="stag-popup-md"
-                               title="Review log<?= !!@($problem->data->name) ? ' for ' . @($problem->data->name) : '' ?>"
-                               href="/point/review-log/<?= $problem->uid ?>?popupmode=1">
-                                <i class="fa fa-history"></i>
-                            </a>
+                        <div>
+                            <div class="">
+                                <a class="pl-2 view-review-log"
+                                   native target="_blank"
+                                   open-in-stag-popup
+                                   popup-style="stag-popup-md"
+                                   title="HPI log<?= !!@($problem->data->name) ? ' for ' . @($problem->data->name) : '' ?>"
+                                   href="/point/hpi-log/<?= $problem->uid ?>?popupmode=1">
+                                    <i class="fa fa-history"></i>
+                                </a>
+                            </div>
+                            <div class="">
+                                <a class="pl-2 view-review-log"
+                                   native target="_blank"
+                                   open-in-stag-popup
+                                   update-parent
+                                   mc-initer="edit-hpi-{{$problem->id}}"
+                                   popup-style="stag-popup-md"
+                                   title="HPI<?= !!@($problem->data->name) ? ' for ' . @($problem->data->name) : '' ?> / <?= $patient->displayName() ?>"
+                                   href="/point/edit-hpi/<?= $note->uid ?>/<?= $problem->uid ?>?popupmode=1">
+                                    <i class="fa fa-plus-square"></i>
+                                </a>
+                            </div>
                         </div>
                     </div>
                 </td>
@@ -174,7 +191,7 @@ $ccSegment = $note->getSegmentByInternalName('chief_complaint');
 
                             @endif
 
-                            @include('app.patient.wizard-partials.common-fields', ['label' => 'problem', 'point' => $problem, 'reviewLabel' => 'HPI', 'addVerbPT' => 'Diagnosed'])
+                            @include('app.patient.wizard-partials.common-fields', ['label' => 'problem', 'point' => $problem, 'reviewLabel' => 'HPI', 'addVerbPT' => 'Diagnosed', 'noReview' => true])
 
                             <div class="mt-3 pt-2 d-flex align-items-center border-top">
                                 <button type="submit" class="btn-save-problem btn btn-sm btn-primary mr-2">Save</button>
@@ -189,7 +206,7 @@ $ccSegment = $note->getSegmentByInternalName('chief_complaint');
 
         <div class="d-flex align-items-center">
             <div class="mt-1 w-100 border p-3 bg-aliceblue border-info rounded">
-                <form action="/api/visitPoint/addTopLevel" class="mcp-theme-1 w-100" id="frm-add-problem">
+                <form action="/api/visitPoint/addTopLevel" class="mcp-theme-1 w-100" id="frm-add-problem" novalidate>
                     <input type="hidden" name="noteUid" value="<?= $note->uid ?>">
                     <input type="hidden" name="category" value="PROBLEM">
                     <input type="hidden" name="data">
@@ -225,7 +242,7 @@ $ccSegment = $note->getSegmentByInternalName('chief_complaint');
                             </div>
 
                             <?php $point = null; ?>
-                            @include('app.patient.wizard-partials.common-fields', ['label' => 'problem', 'reviewLabel' => 'HPI', 'addVerbPT' => 'Diagnosed'])
+                            @include('app.patient.wizard-partials.common-fields', ['label' => 'problem', 'reviewLabel' => 'HPI', 'addVerbPT' => 'Diagnosed', 'noReview' => true])
 
                         </div>
                         <div class="col-4 border-left">

+ 45 - 43
resources/views/app/patient/wizard-partials/common-fields.blade.php

@@ -2,11 +2,11 @@
     <span class="font-weight-bold">Is this {{$label}} active?</span>
     <div class="d-flex align-items-baseline mt-1">
         <label class="my-0 d-inline-flex align-items-center">
-            <input type="radio" name="isRemoved" value="0" {{!$point || !$point->is_removed ? 'checked' : ''}}>
+            <input type="radio" name="isRemoved" required value="0" {{!$point || !$point->is_removed ? 'checked' : ''}}>
             <span class="ml-1">Yes</span>
         </label>
         <label class="ml-3 my-0 d-inline-flex align-items-center">
-            <input type="radio" name="isRemoved" value="1" {{$point && $point->is_removed ? 'checked' : ''}}>
+            <input type="radio" name="isRemoved" required value="1" {{$point && $point->is_removed ? 'checked' : ''}}>
             <span class="ml-1">No</span>
         </label>
     </div>
@@ -45,11 +45,11 @@
             <span class="font-weight-bold">Is this {{$label}} being removed due to an entry error?</span>
             <div class="d-flex align-items-baseline mt-1">
                 <label class="my-0 d-inline-flex align-items-center">
-                    <input type="radio" name="isRemovedDueToEntryError" value="1" {{$point && $point->is_removed_due_to_entry_error === '1' ? 'checked' : ''}}>
+                    <input type="radio" name="isRemovedDueToEntryError" value="1" {{$point && $point->is_removed && $point->is_removed_due_to_entry_error ? 'checked' : ''}}>
                     <span class="ml-1">Yes</span>
                 </label>
                 <label class="ml-3 my-0 d-inline-flex align-items-center">
-                    <input type="radio" name="isRemovedDueToEntryError" value="0" {{$point && $point->is_removed_due_to_entry_error === '0' ? 'checked' : ''}}>
+                    <input type="radio" name="isRemovedDueToEntryError" value="0" {{$point && $point->is_removed && !$point->is_removed_due_to_entry_error ? 'checked' : ''}}>
                     <span class="ml-1">No</span>
                 </label>
             </div>
@@ -122,55 +122,57 @@
 
     @if($patient->core_note_id !== $note->id)
         <div class="row mb-2">
-            <div class="col-6 pr-0">
-                <?php
-                $currentValue = '';
-                $previousValue = '';
-                $previousChildReview = null;
-                if (@$point) {
-                    if ($point->lastChildReview && $point->last_child_review_point_scoped_note_id === $note->id) {
-                        $parsedReview = json_decode($point->lastChildReview->data);
-                        if (@$parsedReview->value) {
-                            $currentValue = $parsedReview->value;
-                            $previousChildReview = \App\Models\Point::where('id', '<', $point->lastChildReview->id)
+            @if(!@$noReview)
+                <div class="col-6 pr-0">
+                    <?php
+                    $currentValue = '';
+                    $previousValue = '';
+                    $previousChildReview = null;
+                    if (@$point) {
+                        if ($point->lastChildReview && $point->last_child_review_point_scoped_note_id === $note->id) {
+                            $parsedReview = json_decode($point->lastChildReview->data);
+                            if (@$parsedReview->value) {
+                                $currentValue = $parsedReview->value;
+                                $previousChildReview = \App\Models\Point::where('id', '<', $point->lastChildReview->id)
+                                    ->where('category', 'REVIEW')
+                                    ->where('parent_point_id', $point->id)
+                                    ->orderBy('id', 'DESC')
+                                    ->first();
+                                if ($previousChildReview && $previousChildReview->data) {
+                                    $parsedReview = json_decode($previousChildReview->data);
+                                    $previousValue = $parsedReview->value;
+                                }
+                            }
+                        } else {
+                            $previousChildReview = \App\Models\Point::where('parent_point_id', $point->id)
                                 ->where('category', 'REVIEW')
-                                ->where('parent_point_id', $point->id)
                                 ->orderBy('id', 'DESC')
                                 ->first();
                             if ($previousChildReview && $previousChildReview->data) {
                                 $parsedReview = json_decode($previousChildReview->data);
-                                $previousValue = $parsedReview->value;
-                            }
-                        }
-                    } else {
-                        $previousChildReview = \App\Models\Point::where('parent_point_id', $point->id)
-                            ->where('category', 'REVIEW')
-                            ->orderBy('id', 'DESC')
-                            ->first();
-                        if ($previousChildReview && $previousChildReview->data) {
-                            $parsedReview = json_decode($previousChildReview->data);
-                            if (@$parsedReview->value) {
-                                $previousValue = $parsedReview->value;
+                                if (@$parsedReview->value) {
+                                    $previousValue = $parsedReview->value;
+                                }
                             }
                         }
                     }
-                }
-                ?>
-                <label class="text-sm mb-1 font-weight-bold">{{@$reviewLabel ? $reviewLabel : 'Review'}}</label>
-                <div note-rte
-                     class="form-group mb-2 border-left border-right rte-holder bg-white"
-                     data-field-name="reviewValue"><?= $currentValue ?></div>
-                <?php if($previousValue): ?>
-                <div class="mb-2">
-                    <div class="d-flex align-items-baseline mb-1">
-                        <span class="text-sm text-secondary">Previous {{@$reviewLabel ? $reviewLabel : 'Review'}} / <?= friendlier_date($previousChildReview->created_at) ?> (click to copy)</span>
+                    ?>
+                    <label class="text-sm mb-1 font-weight-bold">{{@$reviewLabel ? $reviewLabel : 'Review'}}</label>
+                    <div note-rte
+                         class="form-group mb-2 border-left border-right rte-holder bg-white"
+                         data-field-name="reviewValue"><?= $currentValue ?></div>
+                    <?php if($previousValue): ?>
+                    <div class="mb-2">
+                        <div class="d-flex align-items-baseline mb-1">
+                            <span class="text-sm text-secondary">Previous {{@$reviewLabel ? $reviewLabel : 'Review'}} / <?= friendlier_date($previousChildReview->created_at) ?> (click to copy)</span>
+                        </div>
+                        <div class="p-2 bg-light border inline-html-container click-to-copy"><?= $previousValue ?></div>
                     </div>
-                    <div class="p-2 bg-light border inline-html-container click-to-copy"><?= $previousValue ?></div>
+                    <div class="d-none disallow-if-review-same-as" compare-width="reviewValue"><?= str_compact($previousValue) ?></div>
+                    <?php endif; ?>
                 </div>
-                <div class="d-none disallow-if-review-same-as" compare-width="reviewValue"><?= str_compact($previousValue) ?></div>
-                <?php endif; ?>
-            </div>
-            <div class="col-6">
+            @endif
+            <div class="col-{{!@$noReview ? '6' : '12'}}">
                 <?php
                 $currentValue = '';
                 $previousValue = '';

+ 16 - 4
resources/views/app/patient/wizard-partials/common-script.blade.php

@@ -153,15 +153,27 @@ parentSegment.find('#frm-add-{{$label}}')
 
         let form = $(this);
 
+        // require additionReasonCategory if active
+        form.find('[name="additionReasonCategory"]').removeAttr('required');
+        if(!form.find('[name="additionReasonCategory"]:checked').length) {
+            if(form.find('[name="isRemoved"][value="0"]').is(':checked')) {
+                form.find('[name="additionReasonCategory"]').attr('required', 'required');
+            }
+        }
+
+        // require removalReasonCategory if not active
+        form.find('[name="removalReasonCategory"]').removeAttr('required');
+        if(!form.find('[name="removalReasonCategory"]:checked').length) {
+            if(form.find('[name="isRemoved"][value="1"]').is(':checked')) {
+                form.find('[name="removalReasonCategory"]').attr('required', 'required');
+            }
+        }
+
         if (!form[0].checkValidity()) {
             form[0].reportValidity();
             return false;
         }
 
-        if(!form.find('[name="additionReasonCategory"]:checked').length) {
-            form.find('[name="additionReasonCategory"][value="ON_INTAKE"]').prop('checked', true);
-        }
-
         // add [data-name] values to payload
         let dataField = form.find('[name="data"]').first();
         let parsed = null;

+ 3 - 0
resources/views/layouts/patient.blade.php

@@ -732,6 +732,9 @@ $isVisitNote = ($routeName === 'patients.view.notes.view.dashboard' && @$note &&
                                             <button class="col-2-button" onclick="return openInRHS('/pro/check-video/{{ $patient->uid }}')">Check Video</button>
                                         </div>
                                         @endif
+										<div>
+											<button class="col-2-button" onclick="return openInRHS('/pro/meet/{{ $patient->uid }}')">Join Video</button>
+										</div>
                                     </section>
 																		<section class="hide-inside-popup screen-only vbox align-self-start mt-2 mx-2">
 																			@include('app.patient.coverage-status')

+ 1 - 0
resources/views/layouts/template.blade.php

@@ -404,6 +404,7 @@
     <script src="/js/stag-suggest.js?v={{config('app.asset_version')}}"></script>
     <script src="/js/option-list.js?v={{config('app.asset_version')}}"></script>
     <script src="/js/show-on-click.js?v={{config('app.asset_version')}}"></script>
+    <script src="/js/dq.js?v={{config('app.asset_version')}}"></script>
     @include('app/pdf/viewer')
     @if(config('app.enableSockets'))
     <script>

+ 2 - 0
routes/web.php

@@ -377,6 +377,8 @@ Route::middleware('pro.auth')->group(function () {
 
     });
 
+    Route::get('/point/edit-hpi/{note}/{point}', 'NoteController@editHPI')->name('point-edit-hpi');
+    Route::get('/point/hpi-log/{point}', 'NoteController@hpiLog')->name('point-hpi-log');
     Route::get('/point/review-log/{point}', 'NoteController@reviewLog')->name('point-review-log');
     Route::get('/point/plan-log/{point}', 'NoteController@planLog')->name('point-plan-log');
     Route::get('/note/pdf/{note}', 'NoteController@downloadAsPdf')->name('note-pdf');