Quellcode durchsuchen

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

logicpowerhouse vor 4 Jahren
Ursprung
Commit
5b88f8cd68
40 geänderte Dateien mit 3244 neuen und 251 gelöschten Zeilen
  1. 2 18
      app/Http/Controllers/HomeController.php
  2. 40 2
      app/Http/Controllers/PatientController.php
  3. 12 1
      app/Http/Controllers/PracticeManagementController.php
  4. 6 2
      app/Models/Client.php
  5. 14 0
      app/Models/IncomingReport.php
  6. 250 0
      database/seeds/MeasurementSeeder.php
  7. 0 0
      public/c3/c3.min.css
  8. 1 0
      public/c3/c3.min.js
  9. 1 0
      public/c3/d3.v5.min.js
  10. 169 1
      public/css/style.css
  11. 32 13
      public/js/pro-suggest.js
  12. 24 10
      public/js/stag-popup.js
  13. 35 86
      resources/views/app/patient/appointment-calendar.blade.php
  14. 0 1
      resources/views/app/patient/flowsheets.blade.php
  15. 234 0
      resources/views/app/patient/incoming-reports.blade.php
  16. 28 8
      resources/views/app/patient/partials/mcp-queue.blade.php
  17. 762 0
      resources/views/app/patient/tickets.blade.php
  18. 63 0
      resources/views/app/patient/tickets/attachments.blade.php
  19. 32 0
      resources/views/app/patient/tickets/comments.blade.php
  20. 89 0
      resources/views/app/patient/tickets/common-fields.blade.php
  21. 41 0
      resources/views/app/patient/tickets/equipment-data.blade.php
  22. 124 0
      resources/views/app/patient/tickets/equipment.blade.php
  23. 59 0
      resources/views/app/patient/tickets/erx-data.blade.php
  24. 218 0
      resources/views/app/patient/tickets/erx.blade.php
  25. 13 0
      resources/views/app/patient/tickets/header-end.blade.php
  26. 37 0
      resources/views/app/patient/tickets/imaging-data.blade.php
  27. 138 0
      resources/views/app/patient/tickets/imaging.blade.php
  28. 37 0
      resources/views/app/patient/tickets/lab-data.blade.php
  29. 143 0
      resources/views/app/patient/tickets/lab.blade.php
  30. 68 0
      resources/views/app/patient/tickets/methods.blade.php
  31. 98 0
      resources/views/app/patient/tickets/pharmacy-suggest.blade.php
  32. 3 0
      resources/views/app/patient/tickets/ticket_action_links.blade.php
  33. 22 0
      resources/views/app/patient/tickets/ticket_update_pro_form.blade.php
  34. 84 0
      resources/views/app/patient/tickets/ticket_vue_collab_card.blade.php
  35. 82 0
      resources/views/app/patient/tickets/ticket_vue_methods.blade.php
  36. 208 0
      resources/views/app/patient/vitals-graph.blade.php
  37. 35 86
      resources/views/app/practice-management/calendar.blade.php
  38. 15 9
      resources/views/layouts/patient.blade.php
  39. 14 12
      resources/views/layouts/template.blade.php
  40. 11 2
      routes/web.php

+ 2 - 18
app/Http/Controllers/HomeController.php

@@ -422,7 +422,7 @@ class HomeController extends Controller
         return view('app/pharmacy-suggest', compact('pharmacies'));
     }
 
-    public function proSuggest(Request $request, $format = 'html') {
+    public function proSuggest(Request $request) {
         $term = $request->input('term') ? trim($request->input('term')) : '';
         if (empty($term)) return '';
         $term = strtolower($term);
@@ -441,23 +441,7 @@ class HomeController extends Controller
             $pros->whereIn('id', $accessibleProIds);
         }
         $suggestedPros = $pros->orderBy('name_last')->orderBy('name_first')->get();
-        $response = '';
-        if($format === 'html') {
-            $response = view('app/pro-suggest', compact('suggestedPros'));
-        }
-        else if($format === 'json') {
-            $output = [];
-            foreach ($suggestedPros as $sPro) {
-                $output[] = [
-                    "id" => $sPro->id,
-                    "uid" => $sPro->uid,
-                    "text" => $sPro->displayName(),
-                    "initials" => $sPro->initials(),
-                ];
-            }
-            $response = json_encode($output);
-        }
-        return $response;
+        return view('app/pro-suggest', compact('suggestedPros'));
     }
 
     public function proDisplayName(Request $request, Pro $pro) {

+ 40 - 2
app/Http/Controllers/PatientController.php

@@ -12,7 +12,7 @@ use App\Models\Facility;
 use App\Models\Handout;
 use App\Models\NoteTemplate;
 use App\Models\Pro;
-use App\Models\Program;
+use App\Models\ProProAccess;
 use App\Models\SectionTemplate;
 use App\Models\Ticket;
 use Illuminate\Http\Request;
@@ -269,6 +269,11 @@ class PatientController extends Controller
         return view('app.patient.documents', compact('patient'));
     }
 
+    public function incomingReports(Request $request, Client $patient )
+    {
+        return view('app.patient.incoming-reports', compact('patient'));
+    }
+
     public function education(Request $request, Client $patient )
     {
         return view('app.patient.education', compact('patient'));
@@ -302,7 +307,19 @@ class PatientController extends Controller
     }
 
     public function calendar(Request $request, Client $patient, Appointment $currentAppointment) {
-        return view('app.patient.appointment-calendar', compact('patient', 'currentAppointment'));
+
+        $pros =  Pro::all();
+        if($this->pro && $this->pro->pro_type != 'ADMIN'){
+            $accessiblePros = ProProAccess::where('owner_pro_id', $this->pro->id);
+            $accessibleProIds = [];
+            foreach($accessiblePros as $accessiblePro){
+                $accessibleProIds[] = $accessiblePro->id;
+            }
+            $accessibleProIds[] = $this->pro->id;
+            $pros = Pro::whereIn('id', $accessibleProIds)->get();
+        }
+
+        return view('app.patient.appointment-calendar', compact('pros', 'patient', 'currentAppointment'));
     }
 
     public function programs(Request $request, Client $patient, $filter = '') {
@@ -314,4 +331,25 @@ class PatientController extends Controller
         $pros = $this->pros;
         return view('app.patient.flowsheets', compact('patient', 'pros', 'filter'));
     }
+
+    public function vitalsGraph(Request $request, Client $patient, $filter = '') {
+        $pros = $this->pros;
+        return view('app.patient.vitals-graph', compact('patient', 'pros', 'filter'));
+    }
+
+    public function tickets(Request $request, Client $patient) {
+        $pros = $this->pros;
+        $allPros = Pro::all();
+        return view('app.patient.tickets', compact('patient', 'pros', 'allPros'));
+    }
+
+    public function getTicket(Request $request, Ticket $ticket) {
+        $ticket->data = json_decode($ticket->data);
+//        $ticket->created_at = friendly_date_time($ticket->created_at);
+        $ticket->assignedPro;
+        $ticket->managerPro;
+        $ticket->orderingPro;
+        $ticket->initiatingPro;
+        return json_encode($ticket);
+    }
 }

+ 12 - 1
app/Http/Controllers/PracticeManagementController.php

@@ -11,6 +11,7 @@ use App\Models\Note;
 use App\Models\Pro;
 use App\Models\ProFavorite;
 use App\Models\ProGeneralAvailability;
+use App\Models\ProProAccess;
 use App\Models\ProRate;
 use App\Models\ProSpecificAvailability;
 use App\Models\ProSpecificUnavailability;
@@ -370,7 +371,17 @@ class PracticeManagementController extends Controller
 
     public function calendar(Request $request, $proUid = null)
     {
-        return view('app.practice-management.calendar');
+        $pros =  Pro::all();
+        if($this->pro && $this->pro->pro_type != 'ADMIN'){
+            $accessiblePros = ProProAccess::where('owner_pro_id', $this->pro->id);
+            $accessibleProIds = [];
+            foreach($accessiblePros as $accessiblePro){
+                $accessibleProIds[] = $accessiblePro->id;
+            }
+            $accessibleProIds[] = $this->pro->id;
+            $pros = Pro::whereIn('id', $accessibleProIds)->get();
+        }
+        return view('app.practice-management.calendar', compact('pros'));
     }
 
     public function cellularDeviceManager(Request $request, $proUid = null)

+ 6 - 2
app/Models/Client.php

@@ -169,8 +169,12 @@ class Client extends Model
             ->orderBy('created_at', 'desc');
     }
 
-    public function smsNumbers()
-    {
+    public function incomingReports() {
+        return $this->hasMany(IncomingReport::class, 'client_id', 'id')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function smsNumbers() {
         return $this->hasMany(ClientSMSNumber::class, 'client_id', 'id')
             ->orderBy('created_at', 'desc');
     }

+ 14 - 0
app/Models/IncomingReport.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class IncomingReport extends Model
+{
+    protected $table = 'incoming_report';
+
+    public function hcp() {
+        return $this->hasOne(Pro::class, 'id', 'hcp_pro_id');
+    }
+}

+ 250 - 0
database/seeds/MeasurementSeeder.php

@@ -0,0 +1,250 @@
+<?php
+
+use Illuminate\Database\Seeder;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Str;
+
+class MeasurementSeeder extends Seeder
+{
+    /**
+     * Run the database seeds.
+     *
+     * @return void
+     */
+    public function run()
+    {
+        // Device IDs and IMEIs
+        // BP - 1 - 987987987983
+        // WEIGHT - 2 - 987987987984
+
+        $clientID = 1;
+        $bpDeviceID = 1;
+        $bpDeviceIMEI = '987987987983';
+        $weightDeviceID = 2;
+        $weightDeviceIMEI = '987987987984';
+
+        // delete all
+        DB::table('bdt_measurement')->truncate();
+        DB::table('client_bdt_measurement')->truncate();
+        DB::table('measurement')->truncate();
+
+        // create dates for the last 90 days
+        $dates = [];
+        for ($i=90; $i>=0; $i--) {
+            $d = date_sub(date_create(), date_interval_create_from_date_string($i . " day" . ($i === 1 ? '' : 's')));
+            $dates[] = date_format($d, "Y-m-d");
+        }
+
+        $bdt_measurement_next_id = 1;
+        $client_bdt_measurement_next_id = 1;
+        $measurement_next_id = 1;
+
+        // for each date, create measurements
+        for ($i=0; $i<count($dates); $i++) {
+
+            $date = $dates[$i];
+
+            // -- BP -----------------------------------------------------------
+
+            $sbp = rand(130, 145);
+            $dbp = rand(75, 95);
+            $pulse = rand(60, 70);
+
+            // create bdt_measurement
+            $bdt_measurement_id = DB::table('bdt_measurement')->insertGetId([
+                'id' => $bdt_measurement_next_id++,
+                'uid' => Str::uuid(),
+                'imei' => $bpDeviceIMEI,
+                'ts' => strtotime($date),
+                'battery_voltage' => '12',
+                'signal_strength' => '100',
+                'value_tare' => null,
+                'value_weight' => null,
+                'weight_in_pounds' => null,
+                'value_systolic' => round($sbp / 0.0075006),
+                'value_diastolic' => round($dbp / 0.0075006),
+                'value_pulse' => $pulse,
+                'value_unit' => null,
+                'value_irregular' => null,
+                'rssi' => null,
+                'device_id' => null,
+                'device_category' => 'BP',
+                'created_at' => null,
+                'created_by_session_id' => null,
+                'type' => 'BDTMeasurement',
+                'diastolic_bp_in_mm_hg' => $sbp,
+                'systolic_bp_in_mm_hg' => $dbp,
+            ]);
+
+            // create client_bdt_measurement
+            $client_bdt_measurement_id = DB::table('client_bdt_measurement')->insertGetId([
+                'id' => $client_bdt_measurement_next_id++,
+                'uid' => Str::uuid(),
+                'client_id' => $clientID,
+                'bdt_measurement_id' => $bdt_measurement_id,
+                'status' => null,
+                'status_memo' => null,
+                'created_at' => null,
+                'created_by_session_id' => null,
+                'type' => 'ClientBDTMeasurement',
+                'measurement_id' => null, // to update at the end
+            ]);
+
+            // create measurement (1 parent and 2 children for BP)
+            // parent BP measurement
+            $parent_measurement_id = DB::table('measurement')->insertGetId([
+                'id' => $measurement_next_id++,
+                'created_at' => date('Y-m-d'),
+                'type' => 'Measurement',
+                'uid' => Str::uuid(),
+                'is_removed' => false,
+                'removal_memo' => null,
+                'removed_at' => null,
+                'effective_date' => $date,
+                'label' => 'BP',
+                'memo' => null,
+                'value' => "$sbp/$dbp mmHg",
+                'created_by_session_id' => null,
+                'removed_by_session_id' => null,
+                'client_id' => 1,
+                'status' => null,
+                'status_memo' => null,
+                'numeric_value' => null,
+                'client_bdt_measurement_id' => $client_bdt_measurement_id,
+                'parent_measurement_id' => null,
+                'source' => 'Device',
+            ]);
+            // child SBP measurement
+            $sbp_measurement_id = DB::table('measurement')->insertGetId([
+                'id' => $measurement_next_id++,
+                'created_at' => date('Y-m-d'),
+                'type' => 'Measurement',
+                'uid' => Str::uuid(),
+                'is_removed' => false,
+                'removal_memo' => null,
+                'removed_at' => null,
+                'effective_date' => $date,
+                'label' => 'SBP',
+                'memo' => null,
+                'value' => $sbp,
+                'created_by_session_id' => null,
+                'removed_by_session_id' => null,
+                'client_id' => 1,
+                'status' => null,
+                'status_memo' => null,
+                'numeric_value' => $sbp,
+                'client_bdt_measurement_id' => $client_bdt_measurement_id,
+                'parent_measurement_id' => $parent_measurement_id,
+                'source' => 'Device',
+            ]);
+            // child DBP measurement
+            $dbp_measurement_id = DB::table('measurement')->insertGetId([
+                'id' => $measurement_next_id++,
+                'created_at' => date('Y-m-d'),
+                'type' => 'Measurement',
+                'uid' => Str::uuid(),
+                'is_removed' => false,
+                'removal_memo' => null,
+                'removed_at' => null,
+                'effective_date' => $date,
+                'label' => 'DBP',
+                'memo' => null,
+                'value' => $dbp,
+                'created_by_session_id' => null,
+                'removed_by_session_id' => null,
+                'client_id' => 1,
+                'status' => null,
+                'status_memo' => null,
+                'numeric_value' => $dbp,
+                'client_bdt_measurement_id' => $client_bdt_measurement_id,
+                'parent_measurement_id' => $parent_measurement_id,
+                'source' => 'Device',
+            ]);
+
+            // set measurement_id on client_bdt_measurement
+            DB::table('client_bdt_measurement')
+                ->where('id', $client_bdt_measurement_id)
+                ->update([
+                    'measurement_id' => $parent_measurement_id
+                ]);
+
+            // -- WEIGHT -----------------------------------------------------------
+
+            $weight = rand(120, 145);
+
+            // create bdt_measurement
+            $bdt_measurement_id = DB::table('bdt_measurement')->insertGetId([
+                'id' => $bdt_measurement_next_id++,
+                'uid' => Str::uuid(),
+                'imei' => $weightDeviceIMEI,
+                'ts' => strtotime($date),
+                'battery_voltage' => '12',
+                'signal_strength' => '100',
+                'value_tare' => null,
+                'value_weight' => round($weight / 0.00220462),
+                'weight_in_pounds' => $weight,
+                'value_systolic' => null,
+                'value_diastolic' => null,
+                'value_pulse' => null,
+                'value_unit' => null,
+                'value_irregular' => null,
+                'rssi' => null,
+                'device_id' => null,
+                'device_category' => 'WEIGHT',
+                'created_at' => null,
+                'created_by_session_id' => null,
+                'type' => 'BDTMeasurement',
+                'diastolic_bp_in_mm_hg' => null,
+                'systolic_bp_in_mm_hg' => null,
+            ]);
+
+            // create client_bdt_measurement
+            $client_bdt_measurement_id = DB::table('client_bdt_measurement')->insertGetId([
+                'id' => $client_bdt_measurement_next_id++,
+                'uid' => Str::uuid(),
+                'client_id' => $clientID,
+                'bdt_measurement_id' => $bdt_measurement_id,
+                'status' => null,
+                'status_memo' => null,
+                'created_at' => null,
+                'created_by_session_id' => null,
+                'type' => 'ClientBDTMeasurement',
+                'measurement_id' => null, // to update at the end
+            ]);
+
+            // create measurement (1 parent only)
+            $parent_measurement_id = DB::table('measurement')->insertGetId([
+                'id' => $measurement_next_id++,
+                'created_at' => date('Y-m-d'),
+                'type' => 'Measurement',
+                'uid' => Str::uuid(),
+                'is_removed' => false,
+                'removal_memo' => null,
+                'removed_at' => null,
+                'effective_date' => $date,
+                'label' => 'WEIGHT',
+                'memo' => null,
+                'value' => "$weight lbs",
+                'created_by_session_id' => null,
+                'removed_by_session_id' => null,
+                'client_id' => 1,
+                'status' => null,
+                'status_memo' => null,
+                'numeric_value' => $weight,
+                'client_bdt_measurement_id' => $client_bdt_measurement_id,
+                'parent_measurement_id' => null,
+                'source' => 'Device',
+            ]);
+
+            // set measurement_id on client_bdt_measurement
+            DB::table('client_bdt_measurement')
+                ->where('id', $client_bdt_measurement_id)
+                ->update([
+                    'measurement_id' => $parent_measurement_id
+                ]);
+
+        }
+
+    }
+}

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
public/c3/c3.min.css


Datei-Diff unterdrückt, da er zu groß ist
+ 1 - 0
public/c3/c3.min.js


Datei-Diff unterdrückt, da er zu groß ist
+ 1 - 0
public/c3/d3.v5.min.js


+ 169 - 1
public/css/style.css

@@ -967,8 +967,164 @@ body .node input[type="number"] {
     margin-right: 1.5rem;
     margin-left: auto;
 }
+
+/* slide-in stag-popups */
+.stag-popup.stag-slide {
+    display: block;
+    background: center center no-repeat scroll rgba(0, 0, 0, 0);
+    pointer-events: none;
+    overflow: hidden;
+}
+.stag-popup.stag-slide.show {
+    pointer-events: all;
+}
+.stag-popup.stag-slide>form {
+    position: absolute;
+    top: 0;
+    height: 100% !important;
+    overflow-y: auto;
+    border-radius: 0;
+    border-top: 0;
+    border-bottom: 0;
+    border-right: 0;
+    transition: right 0.3s ease;
+    width: 0;
+}
+.stag-popup.stag-slide.stag-popup-sm>form {
+    width: 500px;
+    right: -500px;
+}
+.stag-popup.stag-slide.stag-popup-md>form {
+    width: 632pt;
+    right: -632pt;
+}
+.stag-popup.stag-slide.show>form {
+    right: 0;
+}
+
+/* asana style ticket management */
+.pro-initials {
+    border-radius: 100%;
+    height: 24px;
+    width: 24px;
+    min-height: 24px;
+    min-width: 24px;
+    max-height: 24px;
+    max-width: 24px;
+    text-align: center;
+    font-weight: 400;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    background-color: #6457f9;
+    color: #fff;
+    font-size: 10px !important;
+    opacity: 0.85;
+}
+.pro-initials.pro-initials-sm {
+    height: 18px;
+    width: 18px;
+    min-height: 18px;
+    min-width: 18px;
+    max-height: 18px;
+    max-width: 18px;
+    font-size: 9px !important;
+}
+.pro-initials:hover {
+    opacity: 1;
+}
+.ticket-section {
+
+}
+.ticket-section.ticket-section-collapsed {
+    max-height: 0;
+    overflow: hidden;
+}
+.tickets-table tbody tr {
+    transition: background-color 0.2s ease;
+}
+.tickets-table tbody tr:hover {
+    background-color: rgba(0,0,0,.04);
+}
+.tickets-table tbody tr.current {
+    background-color: aliceblue;
+}
+.stag-slide input.form-control:not(:focus) {
+    border-color: transparent;
+    background-color: transparent;
+    cursor: pointer;
+    font-weight: bold;
+    padding-left: 0;
+}
+.stag-slide input.form-control {
+    box-shadow: none !important;
+}
+.stag-slide input.form-control:not(:focus):not([readonly]):hover {
+    text-decoration: underline;
+}
+.stag-slide input.form-control[readonly] {
+    opacity: 0.75;
+}
+.stag-slide ::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
+    color: #bbb !important;
+    font-weight: normal !important;
+    opacity: 1; /* Firefox */
+}
+.stag-slide :-ms-input-placeholder { /* Internet Explorer 10-11 */
+    color: #bbb !important;
+    font-weight: normal !important;
+}
+.stag-slide ::-ms-input-placeholder { /* Microsoft Edge */
+    color: #bbb !important;
+    font-weight: normal !important;
+}
+.stag-slide .text-success {
+    color: #00bf9c !important;
+}
+.stag-slide .btn-success,
+.stag-slide .btn-success:hover {
+    background: #00bf9c;
+    border-color: #00bf9c;
+}
+.stag-slide .comment-input-outer {
+    position: sticky;
+    bottom: 0;
+}
+.stag-slide .txt-comment {
+    padding-bottom: calc(23px + 1rem);
+    height: calc(23px + 1rem);
+    transition: height 0.3s ease;
+    overflow: hidden;
+}
+.stag-slide .txt-comment:focus {
+    height: 120px;
+    overflow: auto;
+}
+.stag-slide .txt-comment:focus~.btn-save-comment {
+    opacity: 1;
+}
+.stag-slide .btn-save-comment {
+    position: absolute;
+    bottom: 0.5rem;
+    right: 0.5rem;
+    height: 28px;
+    line-height: 28px;
+    padding: 0 1rem;
+    transition: opacity 0.3s ease;
+    opacity: 0.5;
+}
+.stag-slide input[type="text"]:invalid {
+    background-color: #f8ecec;
+    padding-left: 0.5rem;
+}
+.stag-slide input[type="text"]:focus:invalid {
+    background: #fff;
+    border-color: #e24848;
+    box-shadow: 0 0 2px #c10707 !important;
+}
+
 .no-scroll {
-    /*overflow: hidden;*/
+    overflow: hidden;
 }
 .client-rs-contents p {
     margin-bottom: 0.25rem;
@@ -1238,3 +1394,15 @@ button.note-templates-trigger-assessment {
     width: 80px;
     text-align: left;
 }
+
+/* vitals graph */
+.stag-chart {
+    min-height: 300px;
+}
+.stag-chart .safe-region>rect {
+    fill: green;
+}
+.stag-chart .safe-region>text {
+    fill: #888;
+    transform: translateY(-18px);
+}

+ 32 - 13
public/js/pro-suggest.js

@@ -21,11 +21,11 @@
         var term = elem.val();
         if (!!term && lastTerm !== term) {
             $.get('/pro-suggest?term=' + $.trim(term), function (_data) {
-                $('.suggestions-outer.pro-suggestions').html(_data).removeClass('d-none');
+                suggestionsOuter.html(_data).removeClass('d-none');
             });
             lastTerm = term;
         } else {
-            $('.suggestions-outer.pro-suggestions').addClass('d-none');
+            suggestionsOuter.addClass('d-none');
         }
     }, 250);
 
@@ -88,40 +88,59 @@
         }
     }
 
-    function init() {
+    window.initProSuggest = function() {
 
         // make select[provider-search] hidden & insert a textbox with pro-suggest
-        $('select[provider-search]:not([pro-suggest-initialized])').each(function() {
+        $('select[provider-search]').each(function() {
             let elem = $(this);
+            elem.next('.pro-suggest-input').remove();
+            elem.next('.pro-suggestions-container').remove();
             let input = $('<input type="text" placeholder="Pro">').addClass('pro-suggest-input form-control form-control-sm').insertAfter(elem);
-            $('<div class="position-relative"><div class="suggestions-outer pro-suggestions position-absolute d-none"></div></div>').insertAfter(input);
+            $('<div class="pro-suggestions-container position-relative">' +
+                '<div class="suggestions-outer pro-suggestions position-absolute d-none"></div>' +
+                '</div>').insertAfter(input);
             elem.hide();
 
             if(!!elem.attr('data-pro-uid')) {
                 $.get('/pro-display-name/' + elem.attr('data-pro-uid'), function(_data) {
-                    input.val(_data);
+                    input.val(_data).data('original', _data);
                     elem.empty().append($('<option value="' + elem.attr('data-pro-uid') + '" selected/>').text(_data));
                 });
             }
+            else {
+                input.val(elem.attr('data-pro-name')).data('original', elem.attr('data-pro-name'));
+            }
+
+            if(elem.is('[required]')) {
+                input.attr('required', 'required');
+            }
 
             input
                 .off('keydown.pro-suggest')
                 .on('keydown.pro-suggest', function (e) {
-                    suggestionsOuter = $(this).next('.suggestions-outer');
+                    suggestionsOuter = $(this).next('.pro-suggestions-container').find('>.suggestions-outer');
                     return handleKeydown($(this), e);
                 })
                 .off('keypress.pro-suggest')
                 .on('keypress.pro-suggest', function (e) {
-                    suggestionsOuter = $(this).next('.suggestions-outer');
+                    suggestionsOuter = $(this).next('.pro-suggestions-container').find('>.suggestions-outer');
                     return handleKeypress($(this), e);
                 });
-
-            $(elem).attr('pro-suggest-initialized', 1);
+                // .off('blur.pro-suggest')
+                // .on('blur.pro-suggest', function (e) {
+                //     window.setTimeout(() => {
+                //         $(this).next('.pro-suggestions-container').find('>.suggestions-outer').addClass('d-none');
+                //         $(this).val($(this).data('original'));
+                //     }, 50);
+                // });
 
         });
 
+        $(document).off('click', '.suggest-item.pro-suggest[data-target-uid]');
         $(document).on('click', '.suggest-item.pro-suggest[data-target-uid]', function () {
 
+            $('.suggestions-outer.pro-suggestions').addClass('d-none');
+
             let uid = $(this).attr('data-target-uid'),
                 label = $.trim($(this).text());
 
@@ -131,20 +150,20 @@
                 .prev('select[provider-search]');
             select.empty().append($('<option value="' + uid + '" selected/>').text(label));
             select.val(uid).trigger('change');
+            select.trigger('pro-changed');
 
             // set input value
             $(this).closest('.position-relative')
                 .prev('.pro-suggest-input')
                 .val(label)
+                .data('original', label)
                 .trigger('change');
 
-            $('.suggestions-outer.pro-suggestions').addClass('d-none');
-
             return false;
         });
 
     }
-    addMCInitializer('pro-suggest', init);
+    addMCInitializer('pro-suggest', initProSuggest);
 })();
 
 

+ 24 - 10
public/js/stag-popup.js

@@ -1,5 +1,7 @@
+window.stagPopupsQueue = [];
 function showStagPopup(_key, _noAutoFocus) {
-    $('html, body').addClass('no-scroll');
+    /*$('html, body').addClass('no-scroll');
+    $(window.top.document.body).find('#stag_mcp_lhs').addClass('no-scroll');*/
     let stagPopup = $('[stag-popup-key="' + _key + '"]');
     stagPopup.addClass('show');
     stagPopup.find('[moe][initialized]').removeAttr('initialized');
@@ -9,6 +11,7 @@ function showStagPopup(_key, _noAutoFocus) {
             stagPopup.find('input[type="text"]:not([readonly]):visible,select:visible').first().focus();
         }, 150);
     }
+    stagPopupsQueue.push(stagPopup);
     return false;
 }
 function submitStagPopup(_form) {
@@ -18,29 +21,40 @@ function submitStagPopup(_form) {
     }
     showMask();
     $.post(_form.attr('action'), _form.serialize(), function(_data) {
+        stagPopupsQueue = [];
         fastReload();
     });
     return false;
 }
 function closeStagPopup() {
-    $('.stag-popup').removeClass('show');
-    $('html, body').removeClass('no-scroll');
+    if(!stagPopupsQueue.length) return false;
+    let popup = stagPopupsQueue[stagPopupsQueue.length - 1];
+    popup.removeClass('show');
+    stagPopupsQueue.splice(stagPopupsQueue.length - 1, 1);
+    // if all closed
+    if(!stagPopupsQueue.length) {
+        $('html, body').removeClass('no-scroll');
+        $(window.top.document.body).find('#stag_mcp_lhs').removeClass('no-scroll');
+        $(document).trigger('stag-popup-closed');
+    }
     return false;
 }
 (function() {
     window.initStagPopupEvents = function () {
-        $(document).on('click', '.stag-popup', function(_e) {
-            if($(_e.target).is('.stag-popup')) {
-                closeStagPopup();
-            }
-        });
+        $(document)
+            .off('click.stag-popup-discard', '.stag-popup')
+            .on('click.stag-popup-discard', '.stag-popup', function(_e) {
+                if($(_e.target).is('.stag-popup')) {
+                    closeStagPopup();
+                    return false;
+                }
+            });
         // catch ESC and discard any visible popups
         $(document)
             .off('keydown.stag-popup-escape')
             .on('keydown.stag-popup-escape', function (e) {
                 if(e.which === 27) {
-                    let visiblePopups = $('.stag-popup.show');
-                    if (visiblePopups.length) {
+                    if(stagPopupsQueue.length) {
                         closeStagPopup();
                         return false;
                     }

+ 35 - 86
resources/views/app/patient/appointment-calendar.blade.php

@@ -106,6 +106,28 @@
                 <select id="eventPros" name="proUid" xprovider-search
                         class="form-control form-control-sm flex-grow-1" multiple
                         v-model="proIds">
+                    <?php
+                    $proIndex = 0;
+                    $proMeta = [];
+                    ?>
+                    @foreach($pros as $iPro)
+                        <option value="{{$iPro->id}}"
+                                data-bc="{{$palette[$proIndex]["bc"]}}"
+                                data-fc="{{$palette[$proIndex]["fc"]}}"
+                                data-initials="{{$iPro->initials()}}">
+                            {{$iPro->displayName()}}
+                        </option>
+                        <?php
+                        $proMeta[$iPro->uid] = [
+                            "bc" => $palette[$proIndex]["bc"],
+                            "fc" => $palette[$proIndex]["fc"],
+                            "ac" => $palette[$proIndex]["ac"],
+                            "initials" => $iPro->initials()
+                        ];
+                        $proIndex++;
+                        if($proIndex >= count($palette)) $proIndex = 0;
+                        ?>
+                    @endforeach
                 </select>
             </form>
             <hr class="my-2">
@@ -345,9 +367,6 @@
     <script>
         (function() {
 
-            var palette = {!! json_encode($palette) !!};
-            var lastUsedColor = -1;
-
             <?php
             $patient->nameStr = $patient->displayName();
             $clientObject = json_encode($patient);
@@ -360,7 +379,7 @@
                         client: {!! json_encode($patient) !!},
                         eventTypes: '{{ $currentAppointment ? 'BOTH_ALL' : 'BOTH' }}',
                         calendar: null,
-                        proMeta: {},
+                        proMeta: {!! json_encode($proMeta) !!},
                         proIds: ['{{ $currentAppointment ? $currentAppointment->pro_id : $pro->id }}'],
                         timezone: '{{ $currentAppointment ? $currentAppointment->timezone : 'EASTERN' }}',
                         today: new Date('{{ date('Y-m-d 00:00:00') }}'),
@@ -425,62 +444,32 @@
                                 .select2({
                                     closeOnSelect: false,
 
-                                    ajax: {
-                                        url: "/pro-suggest/json",
-                                        type: "get",
-                                        dataType: 'json',
-                                        delay: 250,
-                                        data: function (params) {
-                                            return {
-                                                term: params.term // search term
-                                            };
-                                        },
-                                        processResults: function (response) {
-                                            return {
-                                                results: response
-                                            };
-                                        },
-                                        cache: true
-                                    },
-
                                     // dropdown options
                                     templateResult: function(_state) {
-                                        if(!_state.id) {
+                                        let element = _state.element;
+                                        if(!element || !element.value) {
                                             return $('<span class="mcp-theme-1"><span>' + _state.text + '</span></span>');
                                         }
-
-                                        if(!self.proMeta[_state.uid]) {
-                                            lastUsedColor++;
-                                            if(lastUsedColor > 23) lastUsedColor = 0;
-                                            _state.ag = palette[lastUsedColor].ac;
-                                            _state.bg = palette[lastUsedColor].bc;
-                                            _state.fg = palette[lastUsedColor].fc;
-                                            self.proMeta[_state.uid] = {
-                                                ac: _state.ag,
-                                                bc: _state.bg,
-                                                fc: _state.fg,
-                                                initials: _state.initials
-                                            };
-                                        }
-
+                                        element = $(element);
                                         return $('<span class="mcp-theme-1 pro-option" ' +
-                                            'data-initials="' + _state.initials + '" ' +
-                                            'data-bc="' + _state.bg + '" ' +
-                                            'data-fc="' + _state.fg + '"><span>' +
+                                            'data-initials="' + element.attr('data-initials') + '" ' +
+                                            'data-bc="' + element.attr('data-bc') + '" ' +
+                                            'data-fc="' + element.attr('data-fc') + '"><span>' +
                                             '<span class="pro-option-initials" ' +
-                                            'style="background: ' + _state.bg + '; color: ' + _state.fg + '">' +
-                                            _state.initials + '</span>' +
+                                            'style="background: ' + element.attr('data-bc') + '; color: ' + element.attr('data-fc') + '">' +
+                                            element.attr('data-initials') + '</span>' +
                                             _state.text +
                                             '</span></span>');
                                     },
 
                                     // selected items
                                     templateSelection: function(_state) {
-                                        if(!_state.id) {
+                                        let element = _state.element;
+                                        if(!element || !element.value) {
                                             return $('<span class="mcp-theme-1"><span>' + _state.text + '</span></span>');
                                         }
-                                        return $('<span class="pro-selection" ' +
-                                            'style="background: ' + _state.bg + '; color: ' + _state.fg + '">' +
+                                        element = $(element);
+                                        return $('<span class="pro-selection" style="background: ' + element.attr('data-bc') + '; color: ' + element.attr('data-fc') + '">' +
                                             _state.text + '</span>');
                                     }
                                 })
@@ -492,26 +481,6 @@
                             $('#addApptPro')
                                 .select2({
                                     width: '100%',
-                                    ajax: {
-                                        url: "/pro-suggest/json",
-                                        type: "get",
-                                        dataType: 'json',
-                                        delay: 250,
-                                        data: function (params) {
-                                            return {
-                                                term: params.term // search term
-                                            };
-                                        },
-                                        processResults: function (response) {
-                                            for (let i = 0; i < response.length; i++) {
-                                                response[i].id = response[i].uid;
-                                            }
-                                            return {
-                                                results: response
-                                            };
-                                        },
-                                        cache: true
-                                    },
                                     templateResult: function(_state) {
                                         return $('<span class="mcp-theme-1"><span>' + _state.text + '</span></span>');
                                     },
@@ -523,26 +492,6 @@
                             $('#editApptPro')
                                 .select2({
                                     width: '100%',
-                                    ajax: {
-                                        url: "/pro-suggest/json",
-                                        type: "get",
-                                        dataType: 'json',
-                                        delay: 250,
-                                        data: function (params) {
-                                            return {
-                                                term: params.term // search term
-                                            };
-                                        },
-                                        processResults: function (response) {
-                                            for (let i = 0; i < response.length; i++) {
-                                                response[i].id = response[i].uid;
-                                            }
-                                            return {
-                                                results: response
-                                            };
-                                        },
-                                        cache: true
-                                    },
                                     templateResult: function(_state) {
                                         return $('<span class="mcp-theme-1"><span>' + _state.text + '</span></span>');
                                     },

+ 0 - 1
resources/views/app/patient/flowsheets.blade.php

@@ -1,6 +1,5 @@
 @extends ('layouts.patient')
 @section('inner-content')
-    <?php $pro->pro_type = 'ADMIN'; ?>
     <link href="/select2/select2.min.css" rel="stylesheet" />
     <script src="/select2/select2.min.js"></script>
     <div id="flowsheetsComponent">

+ 234 - 0
resources/views/app/patient/incoming-reports.blade.php

@@ -0,0 +1,234 @@
+@extends ('layouts.patient')
+@section('inner-content')
+    <div>
+        <div class="d-flex align-items-center pb-2">
+            <h4 class="font-weight-bold m-0">Incoming Reports</h4>
+            <span class="mx-2 text-secondary">|</span>
+            <div moe>
+                <a start show class="">Upload</a>
+                <form url="dummy" action="/api/incomingReport/create"
+                      method="post"
+                      onsubmit="return submitIncomingReport(this)"
+                      enctype="multipart/form-data">
+                    <input type="hidden" name="clientUid" value="{{ $patient->uid }}">
+                    <div class="mb-2">
+                        <label for="" class="text-sm text-secondary mb-1">Category</label>
+                        <input type="text" class="form-control form-control-sm" name="category"
+                               value="" placeholder="Category">
+                    </div>
+                    <div class="mb-2">
+                        <label for="" class="text-sm text-secondary mb-1">Sub-category</label>
+                        <input type="text" class="form-control form-control-sm" name="subcategory"
+                               value="" placeholder="Sub-category">
+                    </div>
+                    <div class="mb-2">
+                        <label for="" class="text-sm text-secondary mb-1">Report Date</label>
+                        <input type="date" class="form-control form-control-sm" name="reportDate"
+                               value="" placeholder="Report Date">
+                    </div>
+                    <div class="mb-2">
+                        <label for="" class="text-sm text-secondary mb-1">Title *</label>
+                        <input type="text" class="form-control form-control-sm" name="title"
+                               value="" placeholder="Title" required>
+                    </div>
+                    <div class="mb-2">
+                        <label for="" class="text-sm text-secondary mb-1">File *</label>
+                        <input type="file" class="form-control form-control-sm" name="pdf"
+                               value="" placeholder="File" required>
+                    </div>
+                    <div class="mb-2">
+                        <label for="" class="text-sm text-secondary mb-1">HCP Pro</label>
+                        <select name="hcpProUid" provider-search data-pro-uid="{{$pro->uid}}" data-pro-name="{{$pro->displayName()}}"
+                                class="form-control form-control-sm">
+                        </select>
+                    </div>
+                    <div class="mb-2">
+                        <label for="" class="text-sm text-secondary mb-1">Memo</label>
+                        <input type="text" class="form-control form-control-sm" name="memo"
+                               value="" placeholder="Memo">
+                    </div>
+                    <div class="d-flex align-items-center">
+                        <button class="btn btn-sm btn-primary mr-2" type="submit">Save</button>
+                        <button class="btn btn-sm btn-default mr-2 border" cancel>Cancel</button>
+                    </div>
+                </form>
+            </div>
+        </div>
+        <table class="table table-striped table-sm table-bordered mb-0">
+            <thead>
+            <tr>
+                <th class="px-2 text-secondary">Created</th>
+                <th class="px-2 text-secondary">Report</th>
+                <th class="px-2 text-secondary">Sign</th>
+                <th class="px-2 text-secondary">Title</th>
+                <th class="px-2 text-secondary">Category</th>
+                <th class="px-2 text-secondary">Memo</th>
+                <th class="px-2"></th>
+            </tr>
+            </thead>
+            <tbody>
+            @foreach($patient->incomingReports as $document)
+                <tr class="{{$document->is_entry_error ? 'entry-error' : ''}}">
+                    <td class="px-2">{{ friendly_date_time($document->created_at) }}</td>
+                    <td class="px-2">
+                        <div class="d-flex align-items-center">
+                            <a class="pdf-viewer-trigger" native target="_blank"
+                               href="/api/incomingReport/download/{{ $document->uid }}"
+                               title="View">View</a>
+                            @if($document->is_entry_error)
+                                <span class="ml-auto text-danger on-hover-opaque" title="Entry Error">
+                                    <i class="fa fa-exclamation-triangle"></i>
+                                </span>
+                            @endif
+                        </div>
+                    </td>
+                    <td class="px-2">
+                        @if(!$document->has_hcp_pro_signed)
+                            @if($document->hcp_pro_id === $pro->id)
+                                <div moe relative class="mr-2">
+                                    <a start show class="">Sign</a>
+                                    <form url="/api/incomingReport/signAsHcp" right>
+                                        <input type="hidden" name="uid" value="{{ $document->uid }}">
+                                        <p class="small">Are you sure you want to sign this report as HCP?</p>
+                                        <div class="d-flex align-items-center">
+                                            <button class="btn btn-sm btn-success mr-2" submit>Sign</button>
+                                            <button class="btn btn-sm btn-default mr-2 border" cancel>Cancel</button>
+                                        </div>
+                                    </form>
+                                </div>
+                            @else
+                                -
+                            @endif
+                        @else
+                            <span class="text-secondary">
+                                <i class="fa fa-check"></i>
+                                Signed
+                            </span>
+                        @endif
+                    </td>
+                    <td class="px-2">{{ $document->title }}</td>
+                    <td class="px-2">{{ $document->category }}{{ $document->subcategory ? ' / ' . $document->subcategory : '' }}</td>
+                    <td class="px-2">{{ $document->memo }}</td>
+                    <td class="px-2">
+                        <div class="d-flex align-items-center">
+                            <a class="on-hover-opaque mr-3" native target="_blank"
+                               href="/api/incomingReport/download/{{ $document->uid }}"
+                               title="Download"><i class="fa fa-download"></i></a>
+                            @if(!$document->has_hcp_pro_signed)
+                                <div moe relative class="mr-3">
+                                    <a href="#" start show class="on-hover-opaque" title="Edit"><i class="fa fa-edit text-primary"></i></a>
+                                    <form url="dummy" action="/api/incomingReport/updateBasic"
+                                          right
+                                          method="post"
+                                          onsubmit="return submitIncomingReport(this)"
+                                          enctype="multipart/form-data">
+                                        <input type="hidden" name="uid" value="{{ $document->uid }}">
+
+                                        <div class="mb-2">
+                                            <label for="" class="text-sm text-secondary mb-1">Category</label>
+                                            <input type="text" class="form-control form-control-sm" name="category"
+                                                   value="{{$document->category}}" placeholder="Category">
+                                        </div>
+                                        <div class="mb-2">
+                                            <label for="" class="text-sm text-secondary mb-1">Sub-category</label>
+                                            <input type="text" class="form-control form-control-sm" name="subcategory"
+                                                   value="{{$document->subcategory}}" placeholder="Sub-category">
+                                        </div>
+                                        <div class="mb-2">
+                                            <label for="" class="text-sm text-secondary mb-1">Report Date</label>
+                                            <input type="date" class="form-control form-control-sm" name="reportDate"
+                                                   value="{{$document->reportDate}}" placeholder="Report Date">
+                                        </div>
+                                        <div class="mb-2">
+                                            <label for="" class="text-sm text-secondary mb-1">Title *</label>
+                                            <input type="text" class="form-control form-control-sm" name="title"
+                                                   value="{{$document->title}}" placeholder="Title" required>
+                                        </div>
+                                        <div class="mb-2">
+                                            <label for="" class="text-sm text-secondary mb-1">File *</label>
+                                            <input type="file" class="form-control form-control-sm" name="pdf"
+                                                   value="" placeholder="File">
+                                            <span class="text-sm text-secondary">Leave blank if not changing</span>
+                                        </div>
+                                        <div class="mb-2">
+                                            <label for="" class="text-sm text-secondary mb-1">HCP Pro</label>
+                                            <select name="hcpProUid" provider-search
+                                                    data-pro-name="{{$document->hcp ? $document->hcp->displayName() : ''}}"
+                                                    class="form-control form-control-sm">
+                                            </select>
+                                        </div>
+                                        <div class="mb-2">
+                                            <label for="" class="text-sm text-secondary mb-1">Memo</label>
+                                            <input type="text" class="form-control form-control-sm" name="memo"
+                                                   value="{{$document->memo}}" placeholder="Memo">
+                                        </div>
+
+                                        <div class="d-flex align-items-center">
+                                            <button class="btn btn-sm btn-primary mr-2" type="submit">Save</button>
+                                            <button class="btn btn-sm btn-default mr-2 border" cancel>Cancel</button>
+                                        </div>
+                                    </form>
+                                </div>
+                                @if(!$document->is_entry_error)
+                                    <div moe relative class="mr-3">
+                                        <a start show class="on-hover-opaque" title="Mark as entry-error"><i class="fa fa-ban text-danger"></i></a>
+                                        <form url="/api/incomingReport/setIsEntryErrorToTrue" right>
+                                            <input type="hidden" name="uid" value="{{ $document->uid }}">
+                                            <p class="small">Are you sure you want to set this report as entry error?</p>
+                                            <div class="d-flex align-items-center">
+                                                <button class="btn btn-sm btn-primary mr-2" submit>Save</button>
+                                                <button class="btn btn-sm btn-default mr-2 border" cancel>Cancel</button>
+                                            </div>
+                                        </form>
+                                    </div>
+                                @else
+                                    <div moe relative class="mr-3">
+                                        <a start show class="on-hover-opaque" title="Undo mark as entry-error"><i class="fa fa-undo"></i></a>
+                                        <form url="/api/incomingReport/setIsEntryErrorToFalse" right>
+                                            <input type="hidden" name="uid" value="{{ $document->uid }}">
+                                            <p class="small">Are you sure you want to set unset this report as entry error?</p>
+                                            <div class="d-flex align-items-center">
+                                                <button class="btn btn-sm btn-primary mr-2" submit>Save</button>
+                                                <button class="btn btn-sm btn-default mr-2 border" cancel>Cancel</button>
+                                            </div>
+                                        </form>
+                                    </div>
+                                @endif
+                            @endif
+                        </div>
+                    </td>
+                </tr>
+            @endforeach
+            </tbody>
+        </table>
+    </div>
+    <script>
+        function submitIncomingReport(_form) {
+            showMask();
+            $.ajax({
+                type: "POST",
+                enctype: 'multipart/form-data',
+                url: _form.action,
+                data: new FormData(_form),
+                processData: false,
+                contentType: false,
+                cache: false,
+                timeout: 600000,
+                success: function (data) {
+                    hideMask();
+                    if(data.success) {
+                        fastReload();
+                    }
+                    else {
+                        toastr.error(data.message);
+                    }
+                },
+                error: function (e) {
+                    hideMask();
+                    toastr.error('Unable to upload document!');
+                }
+            });
+            return false;
+        }
+    </script>
+@endsection

+ 28 - 8
resources/views/app/patient/partials/mcp-queue.blade.php

@@ -21,17 +21,12 @@
         window.queueComponent = new Vue({
             el: '#queueComponent',
             data: {
-                items: []
-            },
-            mounted: function() {
-                let self = this;
-                this.refresh();
-                window.setInterval(function() {
-                    self.refresh();
-                }, 15000); // once in 15 seconds
+                items: [],
+                socketClient: null,
             },
             methods: {
                 refresh: function() {
+                    console.log("Refreshing");
                     let self = this;
                     $.get('/patients-in-queue', function(_data) {
                         self.items = _data;
@@ -54,6 +49,31 @@
                         }
                     }, 'json');
                 }
+            },
+            mounted:function(){
+                let self = this;
+                self.refresh();
+                let socket = new SockJS("{{ config('app.backend_ws_url') }}");
+                self.socketClient = Stomp.over(socket);
+                self.socketClient.connect({}, (frame) => {
+
+                    self.socketClient.send("/app/register", {},     // register self
+                        JSON.stringify({
+                            sessionKey: '{{$performer->session_key}}'
+                        })
+                    );
+
+                    self.socketClient.subscribe("/user/topic/newMcpRequest", function(message) {
+                        console.log("Received new mcp request: ", message);
+                        self.refresh();
+                    }); 
+
+                    self.socketClient.subscribe("/user/topic/removeMcpRequest", function(message) {
+                        console.log("An mcp request has been removed: ", message);
+                        self.refresh();
+                    });
+                    
+                });
             }
         });
     })();

+ 762 - 0
resources/views/app/patient/tickets.blade.php

@@ -0,0 +1,762 @@
+@extends ('layouts.patient')
+@section('inner-content')
+
+    <?php
+
+    $categories = ['erx', 'lab', 'imaging', 'equipment'];
+
+    function adjustBrightness($hex, $steps) {
+        $steps = max(-255, min(255, $steps));
+        $hex = str_replace('#', '', $hex);
+        if (strlen($hex) == 3) {
+            $hex = str_repeat(substr($hex,0,1), 2).str_repeat(substr($hex,1,1), 2).str_repeat(substr($hex,2,1), 2);
+        }
+        $color_parts = str_split($hex, 2);
+        $return = '#';
+        foreach ($color_parts as $color) {
+            $color   = hexdec($color); // Convert to decimal
+            $color   = max(0,min(255,$color + $steps)); // Adjust color
+            $return .= str_pad(dechex($color), 2, '0', STR_PAD_LEFT); // Make two char hex code
+        }
+        return $return;
+    }
+
+    $palette = [
+        ["bc" => '#522e92', "fc" => "#ffffff", "ac" => adjustBrightness('#522e92', 180) . 'aa'],
+        ["bc" => '#003152', "fc" => "#ffffff", "ac" => adjustBrightness('#003152', 180) . 'aa'],
+        ["bc" => '#111e6c', "fc" => "#ffffff", "ac" => adjustBrightness('#111e6c', 180) . 'aa'],
+        ["bc" => '#1034a6', "fc" => "#ffffff", "ac" => adjustBrightness('#1034a6', 180) . 'aa'],
+        ["bc" => '#0f52ba', "fc" => "#ffffff", "ac" => adjustBrightness('#0f52ba', 180) . 'aa'],
+        ["bc" => '#447684', "fc" => "#ffffff", "ac" => adjustBrightness('#447684', 180) . 'aa'],
+        ["bc" => '#d86700', "fc" => "#ffffff", "ac" => adjustBrightness('#d86700', 180) . 'aa'],
+        ["bc" => '#643c07', "fc" => "#ffffff", "ac" => adjustBrightness('#643c07', 180) . 'aa'],
+        ["bc" => '#ff3f3f', "fc" => "#ffffff", "ac" => adjustBrightness('#ff3f3f', 180) . 'aa'],
+        ["bc" => '#ffa395', "fc" => "#222222", "ac" => adjustBrightness('#ffa395', 180) . 'aa'],
+        ["bc" => '#6450ff', "fc" => "#ffffff", "ac" => adjustBrightness('#6450ff', 180) . 'aa'],
+        ["bc" => '#8ec7f4', "fc" => "#222222", "ac" => adjustBrightness('#8ec7f4', 180) . 'aa'],
+        ["bc" => '#522e92', "fc" => "#ffffff", "ac" => adjustBrightness('#522e92', 180) . 'aa'],
+        ["bc" => '#111e6c', "fc" => "#ffffff", "ac" => adjustBrightness('#111e6c', 180) . 'aa'],
+        ["bc" => '#003152', "fc" => "#ffffff", "ac" => adjustBrightness('#003152', 180) . 'aa'],
+        ["bc" => '#1034a6', "fc" => "#ffffff", "ac" => adjustBrightness('#1034a6', 180) . 'aa'],
+        ["bc" => '#0f52ba', "fc" => "#ffffff", "ac" => adjustBrightness('#0f52ba', 180) . 'aa'],
+        ["bc" => '#447684', "fc" => "#ffffff", "ac" => adjustBrightness('#447684', 180) . 'aa'],
+        ["bc" => '#d86700', "fc" => "#ffffff", "ac" => adjustBrightness('#d86700', 180) . 'aa'],
+        ["bc" => '#643c07', "fc" => "#ffffff", "ac" => adjustBrightness('#643c07', 180) . 'aa'],
+        ["bc" => '#ff3f3f', "fc" => "#ffffff", "ac" => adjustBrightness('#ff3f3f', 180) . 'aa'],
+        ["bc" => '#ffa395', "fc" => "#222222", "ac" => adjustBrightness('#ffa395', 180) . 'aa'],
+        ["bc" => '#6450ff', "fc" => "#ffffff", "ac" => adjustBrightness('#6450ff', 180) . 'aa'],
+        ["bc" => '#8ec7f4', "fc" => "#222222", "ac" => adjustBrightness('#8ec7f4', 180) . 'aa'],
+    ];
+    ?>
+
+    <link href="/select2/select2.min.css" rel="stylesheet" />
+    <script src="/select2/select2.min.js"></script>
+    <div id="ticketsApp" v-cloak>
+        <div class="d-flex align-items-end pb-0">
+            <h4 class="font-weight-bold m-0 font-size-14">Tickets</h4>
+            <select class="ml-auto max-width-300px form-control form-control-sm pr-2"
+                    v-model="statusFilter">
+                <option value="open">Open tickets (@{{ numOpen }})</option>
+                <option value="closed">Closed tickets (@{{ numClosed }})</option>
+                <option value="all">All tickets (@{{ numAll }})</option>
+            </select>
+        </div>
+
+        @include('app.patient.tickets.erx')
+        @include('app.patient.tickets.lab')
+        @include('app.patient.tickets.imaging')
+        @include('app.patient.tickets.equipment')
+
+    </div>
+
+    <script>
+        (function() {
+            const debounce = (func, delay) => {
+                let debounceTimer
+                return function() {
+                    const context = this
+                    const args = arguments
+                    clearTimeout(debounceTimer)
+                    debounceTimer
+                        = setTimeout(() => func.apply(context, args), delay)
+                }
+            }
+            <?php
+            $tickets = $patient->tickets;
+
+            $ticketsByType = [
+                "erx" => [],
+                "lab" => [],
+                "imaging" => [],
+                "equipment" => [],
+            ];
+
+            $ticketsArray = [];
+            foreach ($tickets as $ticket) {
+                $ticket->data = json_decode($ticket->data);
+                $ticket->created_at = friendly_date_time($ticket->created_at);
+                $ticket->assignedPro;
+                $ticket->managerPro;
+                $ticket->orderingPro;
+                $ticket->initiatingPro;
+                $ticketsByType[$ticket->category][] = $ticket;
+                $ticketsArray[] = $ticket;
+            }
+
+            $reportsArray = [];
+            foreach ($patient->incomingReports as $report) {
+                $report->created_at = friendly_date_time($report->created_at);
+                $report->hcp;
+                $reportsArray[] = $report;
+            }
+
+            $allProsFlat = [];
+            $paletteIndex = 0;
+            foreach ($allPros as $allPro) {
+                $allPro->displayedName = $allPro->displayName();
+                $allPro->displayedInitials = $allPro->initials();
+                $allProsFlat["pro_" . $allPro->id] = $allPro;
+                $allProsFlat["pro_" . $allPro->id]['colors'] = $palette[$paletteIndex++];
+                if($paletteIndex >= count($palette)) $paletteIndex = 0;
+            }
+            ?>
+            function init() {
+                window.ticketsApp = new Vue({
+                    el: '#ticketsApp',
+                    delimiters: ['@{{', '}}'],
+                    data: {
+
+                        own_pro_id: {{ $pro->id }},
+
+                        tickets: {!! json_encode($ticketsArray) !!},
+                        ticketsByType: {!! json_encode($ticketsByType) !!},
+
+                        statusFilter: 'open',
+
+                        // category specific fields
+                        @include('app.patient.tickets.erx-data')
+                        @include('app.patient.tickets.lab-data')
+                        @include('app.patient.tickets.imaging-data')
+                        @include('app.patient.tickets.equipment-data')
+
+                        // common
+                        currentCategory: '',
+                        currentItemUid: '',
+                        allPros: {!! json_encode($allPros) !!},
+                        allProsFlat: {!! json_encode($allProsFlat) !!},
+                        proToUpdate: '',
+                        proTypes: ['Assigned', 'Manager', 'Initiating', 'Ordering'],
+                        newProUid: '',
+                        reloading: false,
+                        comment: '',
+                        preparing: false,
+                        reports: {!! json_encode($reportsArray) !!},
+                        addingReport: false,
+
+                        // debounced auto-saver
+                        <?php for ($i=0; $i<count($categories); $i++) { ?>
+                            {{ $categories[$i] }}AutoSave: null,
+                        <?php } ?>
+                    },
+                    computed: {
+                        numAll: function() {
+                            return this.tickets.length;
+                        },
+                        numOpen: function() {
+                            return this.tickets.filter(_x => _x.is_open).length;
+                        },
+                        numClosed: function() {
+                            return this.tickets.filter(_x => !_x.is_open).length;
+                        },
+                        <?php for ($i=0; $i<count($categories); $i++) { ?>
+                            {{ $categories[$i] }}NumAll: function() {
+                                return this.ticketsByType.{{ $categories[$i] }}.length;
+                            },
+                            {{ $categories[$i] }}NumOpen: function() {
+                                return this.ticketsByType.{{ $categories[$i] }}.filter(_x => _x.is_open).length;
+                            },
+                            {{ $categories[$i] }}NumClosed: function() {
+                                return this.ticketsByType.{{ $categories[$i] }}.filter(_x => !_x.is_open).length;
+                            },
+                            {{ $categories[$i] }}CurrentItemData: function() {
+                                return this.{{ $categories[$i] }}PopupItem.data;
+                            },
+                        <?php } ?>
+                    },
+                    watch: {
+                        <?php for ($i=0; $i<count($categories); $i++) { ?>
+                            {{ $categories[$i] }}CurrentItemData: { // ex: erxCurrentItemData, labCurrentItemData, etc.
+                                handler: function(val, oldVal) {
+                                    if (this.{{ $categories[$i] }}PopupMode !== 'edit' || !this.currentItemUid || this.reloading) return;
+                                    this.{{ $categories[$i] }}AutoSave();
+                                },
+                                deep: true
+                            },
+                        <?php } ?>
+                    },
+                    methods: {
+
+                        // common show popup
+                        preparePopup: function(_type, _item) {
+                            this.preparing = true;
+                            this[_type + 'PopupMode'] = _item ? 'edit' : 'add';
+                            this[_type + 'PopupItem'] = _item ? JSON.parse(JSON.stringify(_item)) : JSON.parse(JSON.stringify(this[_type + 'Model']));
+                            this[_type + 'PopupItem'].assigned_pro_uid = '';
+                            this[_type + 'PopupItem'].ordering_pro_uid = '';
+                            if(!this[_type + 'PopupItem'].data.comments) {
+                                this[_type + 'PopupItem'].data.comments = [];
+                            }
+                            else if(this[_type + 'PopupItem'].data.comments.length === 1 && !this[_type + 'PopupItem'].data.comments[0].pro_id) {
+                                this[_type + 'PopupItem'].data.comments = [];
+                            }
+                            this.comment = '';
+                            this.preparing = false;
+                        },
+                        reinitProSuggest: function() {
+                            $('#ticketsApp [pro-suggest-initialized]').removeAttr('pro-suggest-initialized');
+                            initProSuggest();
+                        },
+                        postShowPopup: function(_type, _item) {
+                            if (_item) {
+                                this.currentCategory = _type;
+                                this.currentItemUid = _item.uid;
+                            }
+                        },
+
+                        // category specific show popup
+                        erxShowPopup: function(_item) {
+                            this.preparePopup('erx', _item);
+                            Vue.nextTick(() => {
+                                showStagPopup('erx-popup', true);
+                                this.reinitProSuggest();
+                                this.postShowPopup('erx', _item);
+                            });
+                        },
+                        labShowPopup: function(_item) {
+                            this.preparePopup('lab', _item);
+                            Vue.nextTick(() => {
+                                showStagPopup('lab-popup', true);
+                                this.reinitProSuggest();
+                                this.labInitICDAutoSuggest();
+                                this.postShowPopup('lab', _item);
+                            });
+                        },
+                        imagingShowPopup: function(_item) {
+                            this.preparePopup('imaging', _item);
+                            Vue.nextTick(() => {
+                                showStagPopup('imaging-popup', true);
+                                this.reinitProSuggest();
+                                this.imagingInitICDAutoSuggest();
+                                this.postShowPopup('imaging', _item);
+                            });
+                        },
+                        equipmentShowPopup: function(_item) {
+                            this.preparePopup('equipment', _item);
+                            Vue.nextTick(() => {
+                                showStagPopup('equipment-popup', true);
+                                this.reinitProSuggest();
+                                this.postShowPopup('equipment', _item);
+                            });
+                        },
+
+                        erxSavePopupItem: function(_autoSave = false) {
+                            let form = $('#ticketsApp [stag-popup-key="erx-popup"] form').first();
+                            if(!_autoSave) {
+                                if(!form[0].checkValidity()) {
+                                    form[0].reportValidity();
+                                    return false;
+                                }
+                            }
+                            if(!_autoSave) showMask();
+                            let payload = {};
+                            if(this.erxPopupMode === 'add') {
+                                payload.clientUid = '{{ $patient->uid  }}';
+                                payload.category = 'erx';
+                                payload.assignedProUid = '{{ $pro->uid  }}';
+                                payload.managerProUid = '{{ $pro->uid  }}';
+                                payload.orderingProUid = '{{ $pro->uid  }}';
+                                payload.initiatingProUid = '{{ $pro->uid  }}';
+                                payload.data = JSON.stringify(this.erxPopupItem.data);
+                            }
+                            else {
+                                payload.uid = this.erxPopupItem.uid;
+                                payload.newData = JSON.stringify(this.erxPopupItem.data);
+                            }
+                            $.post(
+                                '/api/ticket/' + (this.erxPopupMode === 'add' ? 'create' : 'updateData'),
+                                payload,
+                                (_data) => {
+                                    console.log(_data);
+                                    if(!_autoSave) {
+                                        fastReload();
+                                    }
+                                    else {
+                                        this.reloadPopupItem('erx');
+                                    }
+                                },
+                                'json');
+                            return false;
+                        },
+                        erxSaveComment: function() {
+                            if(!this.comment) return;
+                            let comment = {
+                                pro_id: this.own_pro_id,
+                                created_at: new Date().toLocaleString(),
+                                message: this.comment
+                            };
+                            if(!this.erxPopupItem.data.comments) {
+                                this.erxPopupItem.data.comments = [];
+                            }
+                            this.erxPopupItem.data.comments.push(comment);
+                            this.erxSavePopupItem(true);
+                        },
+
+                        labSavePopupItem: function(_autoSave = false) {
+                            let form = $('#ticketsApp [stag-popup-key="lab-popup"] form').first();
+                            if(!form[0].checkValidity()) {
+                                if(!_autoSave) form[0].reportValidity();
+                                return false;
+                            }
+                            if(!_autoSave) showMask();
+                            let payload = {};
+                            if(this.labPopupMode === 'add') {
+                                payload.clientUid = '{{ $patient->uid  }}';
+                                payload.category = 'lab';
+                                payload.assignedProUid = '{{ $pro->uid  }}';
+                                payload.managerProUid = '{{ $pro->uid  }}';
+                                payload.orderingProUid = '{{ $pro->uid  }}';
+                                payload.initiatingProUid = '{{ $pro->uid  }}';
+                                payload.data = JSON.stringify(this.labPopupItem.data);
+                            }
+                            else {
+                                payload.uid = this.labPopupItem.uid;
+                                payload.newData = JSON.stringify(this.labPopupItem.data);
+                            }
+                            $.post(
+                                '/api/ticket/' + (this.labPopupMode === 'add' ? 'create' : 'updateData'),
+                                payload,
+                                (_data) => {
+                                    console.log(_data);
+                                    if(!_autoSave) {
+                                        fastReload();
+                                    }
+                                    else {
+                                        this.reloadPopupItem('lab');
+                                    }
+                                },
+                                'json');
+                            return false;
+                        },
+                        labSaveComment: function() {
+                            if(!this.comment) return;
+                            let comment = {
+                                pro_id: this.own_pro_id,
+                                created_at: new Date().toLocaleString(),
+                                message: this.comment
+                            };
+                            if(!this.labPopupItem.data.comments) {
+                                this.labPopupItem.data.comments = [];
+                            }
+                            this.labPopupItem.data.comments.push(comment);
+                            this.labSavePopupItem(true);
+                        },
+
+                        imagingSavePopupItem: function(_autoSave = false) {
+                            let form = $('#ticketsApp [stag-popup-key="imaging-popup"] form').first();
+                            if(!_autoSave) {
+                                if(!form[0].checkValidity()) {
+                                    form[0].reportValidity();
+                                    return false;
+                                }
+                            }
+                            if(!_autoSave) showMask();
+                            let payload = {};
+                            if(this.imagingPopupMode === 'add') {
+                                payload.clientUid = '{{ $patient->uid  }}';
+                                payload.category = 'imaging';
+                                payload.assignedProUid = '{{ $pro->uid  }}';
+                                payload.managerProUid = '{{ $pro->uid  }}';
+                                payload.orderingProUid = '{{ $pro->uid  }}';
+                                payload.initiatingProUid = '{{ $pro->uid  }}';
+                                payload.data = JSON.stringify(this.imagingPopupItem.data);
+                            }
+                            else {
+                                payload.uid = this.imagingPopupItem.uid;
+                                payload.newData = JSON.stringify(this.imagingPopupItem.data);
+                            }
+                            $.post(
+                                '/api/ticket/' + (this.imagingPopupMode === 'add' ? 'create' : 'updateData'),
+                                payload,
+                                (_data) => {
+                                    console.log(_data);
+                                    if(!_autoSave) {
+                                        fastReload();
+                                    }
+                                    else {
+                                        this.reloadPopupItem('imaging');
+                                    }
+                                },
+                                'json');
+                            return false;
+                        },
+                        imagingSaveComment: function() {
+                            if(!this.comment) return;
+                            let comment = {
+                                pro_id: this.own_pro_id,
+                                created_at: new Date().toLocaleString(),
+                                message: this.comment
+                            };
+                            if(!this.imagingPopupItem.data.comments) {
+                                this.imagingPopupItem.data.comments = [];
+                            }
+                            this.imagingPopupItem.data.comments.push(comment);
+                            this.imagingSavePopupItem(true);
+                        },
+
+                        equipmentSavePopupItem: function(_autoSave = false) {
+                            let form = $('#ticketsApp [stag-popup-key="equipment-popup"] form').first();
+                            if(!_autoSave) {
+                                if(!form[0].checkValidity()) {
+                                    form[0].reportValidity();
+                                    return false;
+                                }
+                            }
+                            if(!_autoSave) showMask();
+                            let payload = {};
+                            if(this.equipmentPopupMode === 'add') {
+                                payload.clientUid = '{{ $patient->uid  }}';
+                                payload.category = 'equipment';
+                                payload.assignedProUid = '{{ $pro->uid  }}';
+                                payload.managerProUid = '{{ $pro->uid  }}';
+                                payload.orderingProUid = '{{ $pro->uid  }}';
+                                payload.initiatingProUid = '{{ $pro->uid  }}';
+                                payload.data = JSON.stringify(this.equipmentPopupItem.data);
+                            }
+                            else {
+                                payload.uid = this.equipmentPopupItem.uid;
+                                payload.newData = JSON.stringify(this.equipmentPopupItem.data);
+                            }
+                            $.post(
+                                '/api/ticket/' + (this.equipmentPopupMode === 'add' ? 'create' : 'updateData'),
+                                payload,
+                                (_data) => {
+                                    console.log(_data);
+                                    if(!_autoSave) {
+                                        fastReload();
+                                    }
+                                    else {
+                                        this.reloadPopupItem('equipment');
+                                    }
+                                },
+                                'json');
+                            return false;
+                        },
+                        equipmentSaveComment: function() {
+                            if(!this.comment) return;
+                            let comment = {
+                                pro_id: this.own_pro_id,
+                                created_at: new Date().toLocaleString(),
+                                message: this.comment
+                            };
+                            if(!this.equipmentPopupItem.data.comments) {
+                                this.equipmentPopupItem.data.comments = [];
+                            }
+                            this.equipmentPopupItem.data.comments.push(comment);
+                            this.equipmentSavePopupItem(true);
+                        },
+
+                        // common
+                        reloadPopupItem: function(_type, _close = false) {
+                            this.reloading = true;
+                            $.get('/get-ticket/' + this[_type + 'PopupItem'].uid, (_data) => {
+                                this[_type + 'PopupItem'] = _data;
+
+                                // also update in main list
+                                for (let i=0; i < this.ticketsByType[_type].length; i++) {
+                                    if (this.ticketsByType[_type][i].uid === this[_type + 'PopupItem'].uid) {
+                                        this.ticketsByType[_type].splice(i, 1, _data);
+                                        break;
+                                    }
+                                }
+
+                                Vue.nextTick(() => {
+                                    this.reloading = false;
+                                    if(_close) {
+                                        closeStagPopup();
+                                    }
+                                });
+
+                            }, 'json');
+                        },
+                        closeItem: function(_item) {
+                            $.post('/api/ticket/close', {
+                                uid: _item.uid
+                            }, (_data) => {
+                                this.reloadPopupItem(this.currentCategory, true);
+                            }, 'json');
+                        },
+                        openItem: function(_item) {
+                            $.post('/api/ticket/open', {
+                                uid: _item.uid
+                            }, (_data) => {
+                                this.reloadPopupItem(this.currentCategory);
+                            }, 'json');
+                        },
+                        signAsOrderingPro: function(_item) {
+                            $.post('/api/ticket/signAsOrderingPro', {
+                                uid: _item.uid
+                            }, (_data) => {
+                                this.reloadPopupItem(this.currentCategory);
+                            }, 'json');
+                        },
+                        undoSignAsOrderingPro: function(_item) {
+                            $.post('/api/ticket/undoSignAsOrderingPro', {
+                                uid: _item.uid
+                            }, (_data) => {
+                                this.reloadPopupItem(this.currentCategory);
+                            }, 'json');
+                        },
+                        toggleAddingReport: function() {
+                            this.addingReport = !this.addingReport;
+                            Vue.nextTick(() => {
+                                if(this.addingReport) {
+                                    $('[stag-popup-key="'+ this.currentCategory +'-popup"] .adding-report-section').first()[0].scrollIntoView();
+                                }
+                            });
+                        },
+                        addReportToItem: function(_item, _report) {
+                            if(!this[this.currentCategory + 'PopupItem'].data.attachments) {
+                                this[this.currentCategory + 'PopupItem'].data.attachments = [];
+                            }
+                            this[this.currentCategory + 'PopupItem'].data.attachments.push(_report);
+                            this[this.currentCategory + 'SavePopupItem'](true);
+                            this.addingReport = false;
+                            Vue.nextTick(() => {
+                                $('[stag-popup-key="'+ this.currentCategory +'-popup"] .listing-attachments-section').first()[0].scrollIntoView();
+                            });
+                        },
+                        removeAttachmentFromItem: function(_item, _attachment) {
+                            if(!this[this.currentCategory + 'PopupItem'].data.attachments) {
+                                this[this.currentCategory + 'PopupItem'].data.attachments = [];
+                            }
+                            this[this.currentCategory + 'PopupItem'].data.attachments =
+                                this[this.currentCategory + 'PopupItem'].data.attachments.filter((_x) => {
+                                    return _x.uid !== _attachment.uid;
+                                });
+                            this[this.currentCategory + 'SavePopupItem'](true);
+                        },
+                        initRxAutoSuggest: function() {
+                            let self = this;
+                            $('#ticketsApp input[type="text"][data-field="medication"]:not([ac-initialized])').each(function() {
+                                let elem = this,
+                                    randPart = Math.ceil(Math.random() * 1000000),
+                                    dynID = 'rx-' + randPart;
+                                $(elem).attr('id', dynID);
+                                var strengthElem = $(elem).closest('.stag-popup').find('[data-field="strength"]')[0],
+                                    dynStrengthsID = 'rx-' + randPart + '-strengths';
+                                $(strengthElem).attr('id', dynStrengthsID);
+                                $(strengthElem).attr('rx-id', dynID);
+                                new window.Def.Autocompleter.Prefetch(dynStrengthsID, []);
+                                new window.Def.Autocompleter.Search(dynID,
+                                    'https://clinicaltables.nlm.nih.gov/api/rxterms/v3/search?ef=STRENGTHS_AND_FORMS');
+                                window.Def.Autocompleter.Event.observeListSelections(dynID, function() {
+                                    var autocomp = elem.autocomp, acData = autocomp.getSelectedItemData();
+                                    var strengths = acData[0].data['STRENGTHS_AND_FORMS'];
+                                    if (strengths) {
+                                        strengthElem.autocomp.setListAndField(strengths, '');
+                                    }
+                                    self.erxPopupItem.data.medication = $(elem).val();
+                                });
+                                window.Def.Autocompleter.Event.observeListSelections(dynStrengthsID, function() {
+                                    var autocomp = elem.autocomp, acData = autocomp.getSelectedItemData();
+                                    self.erxPopupItem.data.strength = $(strengthElem).val();
+                                });
+                                $(elem).attr('ac-initialized', 1);
+                                $(strengthElem).attr('ac-initialized', 1);
+                            });
+                        },
+                        labAddICD: function() {
+                            this.labPopupItem.data.icds.push('');
+                            Vue.nextTick(() => {
+                                this.labInitICDAutoSuggest();
+                            });
+                        },
+                        imagingAddICD: function() {
+                            this.imagingPopupItem.data.icds.push('');
+                            Vue.nextTick(() => {
+                                this.imagingInitICDAutoSuggest();
+                            });
+                        },
+                        erxInitICDAutoSuggest: function() {
+                            let self = this;
+                            $('#ticketsApp [stag-popup-key="erx-popup"] input[type="text"][data-field="icd"]:not([ac-initialized])').each(function() {
+                                var elem = this,
+                                    dynID = 'icd-' + Math.ceil(Math.random() * 1000000),
+                                    vueIndex = $(this).attr('data-index');
+                                $(elem).attr('id', dynID);
+                                new window.Def.Autocompleter.Search(dynID,
+                                    'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?sf=code,name&ef=name', {
+                                        tableFormat: true,
+                                        valueCols: [0],
+                                        colHeaders: ['Code', 'Name'],
+                                    }
+                                );
+                                window.Def.Autocompleter.Event.observeListSelections(dynID, function() {
+                                    let autocomp = elem.autocomp, acData = autocomp.getSelectedItemData();
+                                    self.erxPopupItem.data.purpose = acData[0].code + ' - ' + acData[0].data.name;
+                                    return false;
+                                });
+                                $(elem).attr('ac-initialized', 1);
+                            });
+                        },
+                        labInitICDAutoSuggest: function() {
+                            let self = this;
+                            $('#ticketsApp [stag-popup-key="lab-popup"] input[type="text"][data-field="icd"]:not([ac-initialized])').each(function() {
+                                var elem = this,
+                                    dynID = 'icd-' + Math.ceil(Math.random() * 1000000),
+                                    vueIndex = $(this).attr('data-index');
+                                $(elem).attr('id', dynID);
+                                new window.Def.Autocompleter.Search(dynID,
+                                    'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?sf=code,name&ef=name', {
+                                        tableFormat: true,
+                                        valueCols: [0],
+                                        colHeaders: ['Code', 'Name'],
+                                    }
+                                );
+                                window.Def.Autocompleter.Event.observeListSelections(dynID, function() {
+                                    let autocomp = elem.autocomp, acData = autocomp.getSelectedItemData();
+                                    self.labPopupItem.data.icds[vueIndex] = acData[0].code;
+                                    return false;
+                                });
+                                $(elem).attr('ac-initialized', 1);
+                            });
+                        },
+                        imagingInitICDAutoSuggest: function() {
+                            let self = this;
+                            $('#ticketsApp [stag-popup-key="imaging-popup"] input[type="text"][data-field="icd"]:not([ac-initialized])').each(function() {
+                                var elem = this,
+                                    dynID = 'icd-' + Math.ceil(Math.random() * 1000000),
+                                    vueIndex = $(this).attr('data-index');
+                                $(elem).attr('id', dynID);
+                                new window.Def.Autocompleter.Search(dynID,
+                                    'https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?sf=code,name&ef=name', {
+                                        tableFormat: true,
+                                        valueCols: [0],
+                                        colHeaders: ['Code', 'Name'],
+                                    }
+                                );
+                                window.Def.Autocompleter.Event.observeListSelections(dynID, function() {
+                                    let autocomp = elem.autocomp, acData = autocomp.getSelectedItemData();
+                                    self.imagingPopupItem.data.icds[vueIndex] = acData[0].code;
+                                    return false;
+                                });
+                                $(elem).attr('ac-initialized', 1);
+                            });
+                        },
+                        initDomElementEvents: function() {
+                            let self = this;
+                            $(document).off('pro-changed', '#ticketsApp select[provider-search]')
+                            $(document).on('pro-changed', '#ticketsApp select[provider-search]', function() {
+                                console.log('ALIX pro-changed', $(this).attr('data-field'), $(this).val());
+                                if(!!$(this).val()) {
+                                    self[self.currentCategory + 'PopupItem'][$(this).attr('data-field')] = $(this).val();
+
+                                    // save
+                                    let proType = '';
+                                    switch($(this).attr('data-field')) {
+                                        case 'assigned_pro_uid':
+                                            proType = 'Assigned';
+                                            break;
+                                        case 'ordering_pro_uid':
+                                            proType = 'Ordering';
+                                            break;
+                                    }
+                                    if (proType) {
+                                        $.post('/api/ticket/update' + proType + 'Pro', {
+                                            uid: self[self.currentCategory + 'PopupItem'].uid,
+                                            newProUid: $(this).val()
+                                        }, function(_data) {
+                                            if (!self.hasError(_data)) {
+                                                // toastr.success(proType + ' pro updated');
+                                                self.reloadPopupItem(self.currentCategory);
+                                            }
+                                        });
+                                    }
+                                }
+                            });
+
+                            $(document).off('stag-popup-closed')
+                            $(document).on('stag-popup-closed', function() {
+                                self.currentItemUid = '';
+                            });
+
+                            $('input.date-input')
+                                .datepicker({
+                                    autoclose: true,
+                                    todayHighlight: true,
+                                    format: 'yyyy-mm-dd'
+                                })
+                                .off('changeDate')
+                                .on('changeDate', function() {
+                                    self[self.currentCategory + 'PopupItem'].data.due_date = $(this).val();
+                                    self[self.currentCategory + 'SavePopupItem'].call(self, true);
+                                });
+
+                            $(document).off('focus', '#ticketsApp input.form-control');
+                            $(document).on('focus', '#ticketsApp input.form-control', function() {
+                                $(this).select();
+                            });
+                        },
+                        inWords: function (num) {
+                            try {
+                                num = +num;
+                                var a = ['','one ','two ','three ','four ', 'five ','six ','seven ','eight ','nine ','ten ','eleven ','twelve ','thirteen ','fourteen ','fifteen ','sixteen ','seventeen ','eighteen ','nineteen '];
+                                var b = ['', '', 'twenty','thirty','forty','fifty', 'sixty','seventy','eighty','ninety'];
+                                if ((num = num.toString()).length > 3) return 'overflow';
+                                let n = ('000000000' + num).substr(-9).match(/^(\d{2})(\d{2})(\d{2})(\d{1})(\d{2})$/);
+                                if (!n) return; var str = '';
+                                str += (n[1] != 0) ? (a[Number(n[1])] || b[n[1][0]] + ' ' + a[n[1][1]]) + 'crore ' : '';
+                                str += (n[2] != 0) ? (a[Number(n[2])] || b[n[2][0]] + ' ' + a[n[2][1]]) + 'lakh ' : '';
+                                str += (n[3] != 0) ? (a[Number(n[3])] || b[n[3][0]] + ' ' + a[n[3][1]]) + 'thousand ' : '';
+                                str += (n[4] != 0) ? (a[Number(n[4])] || b[n[4][0]] + ' ' + a[n[4][1]]) + 'hundred ' : '';
+                                str += (n[5] != 0) ? ((str != '') ? 'and ' : '') + (a[Number(n[5])] || b[n[5][0]] + ' ' + a[n[5][1]]) : '';
+                                return str ? '(' + $.trim(str) + ')' : '';
+                            }
+                            catch (e) {
+                                return '';
+                            }
+                        },
+                        pharmacy: function(_item) {
+                            return [
+                                _item.pharmacyName,
+                                _item.pharmacyCity,
+                                /*_item.pharmacyState,
+                                _item.pharmacyAddressMemo,
+                                _item.pharmacyPhone,
+                                _item.pharmacyFax,*/
+                            ].filter(Boolean).join(', ');
+                        },
+                        @include('app.patient.tickets.pharmacy-suggest')
+                        @include('app.patient.tickets.ticket_vue_methods')
+                    },
+                    mounted: function () {
+                        this.initRxAutoSuggest();
+                        this.erxInitICDAutoSuggest();
+                        this.labInitICDAutoSuggest();
+                        this.initPharmacySearch();
+                        this.initDomElementEvents();
+
+                        <?php for ($i=0; $i<count($categories); $i++) { ?>
+                            this.{{ $categories[$i] }}AutoSave = debounce(() => {
+                                this.{{ $categories[$i] }}SavePopupItem(true);
+                            }, 1000);
+                        <?php } ?>
+
+                    }
+                })
+            }
+            addMCInitializer('patient-tickets', init, '#ticketsApp');
+        })();
+    </script>
+
+@endsection

+ 63 - 0
resources/views/app/patient/tickets/attachments.blade.php

@@ -0,0 +1,63 @@
+<div class="px-3 pt-3 pb-2 bg-light">
+    <div class="row mb-2">
+        <div class="col-12">
+            <label class="text-secondary mb-0 font-weight-normal font-size-16 d-flex align-items-center">
+                Attachments
+                (<span
+                    v-html="<?=$category?>PopupItem.data.attachments && <?=$category?>PopupItem.data.attachments.length ? <?=$category?>PopupItem.data.attachments.length : '0'"></span>)
+                <a href="#" class="ml-auto" v-on:click.prevent="toggleAddingReport()">Add from Reports</a>
+            </label>
+        </div>
+    </div>
+    <div class="mb-3" v-if="addingReport">
+        <div v-for="(report) in reports" class="d-flex align-items-start py-1 border-bottom">
+            <div class="pro-initials pro-initials-sm text-uppercase mr-2"
+                 :title="allProsFlat['pro_' + report.hcp_pro_id].displayedName"
+                 :style="'background-color: ' + allProsFlat['pro_' + report.hcp_pro_id].colors.bc + '; color: ' + allProsFlat['pro_' + report.hcp_pro_id].colors.fc + ';'"
+                 v-html="allProsFlat['pro_' + report.hcp_pro_id].displayedInitials">
+            </div>
+            <div class="flex-grow-1">
+                <div class="d-flex align-items-center">
+                    <a class="pdf-viewer-trigger" native target="_blank"
+                       :href="'/api/incomingReport/download/' + report.uid"
+                       title="View">
+                        <span v-if="report.title" v-html="report.title"></span>
+                    </a>
+                    <a class="ml-auto" native target="_blank" href="#"
+                       v-if="!<?=$category?>PopupItem.data.attachments || <?=$category?>PopupItem.data.attachments.filter(_x => _x.uid === report.uid).length === 0"
+                       v-on:click.prevent="addReportToItem(<?=$category?>PopupItem, report)">Add to ticket</a>
+                    <span class="ml-auto text-secondary"
+                       v-if="<?=$category?>PopupItem.data.attachments && <?=$category?>PopupItem.data.attachments.filter(_x => _x.uid === report.uid).length !== 0"
+                       title="View">
+                        <i class="fa fa-check"></i>
+                        Added
+                    </span>
+                </div>
+            </div>
+        </div>
+        <div class="adding-report-section"></div>
+    </div>
+    <div
+        v-if="!addingReport && <?=$category?>PopupMode === 'edit' && <?=$category?>PopupItem.data.attachments && <?=$category?>PopupItem.data.attachments.length"
+        class="py-2 bg-light">
+        <div v-for="(attachment) in <?=$category?>PopupItem.data.attachments" class="d-flex align-items-start pb-2">
+            <div class="pro-initials text-uppercase mr-2"
+                 :title="allProsFlat['pro_' + attachment.hcp_pro_id].displayedName"
+                 :style="'background-color: ' + allProsFlat['pro_' + attachment.hcp_pro_id].colors.bc + '; color: ' + allProsFlat['pro_' + attachment.hcp_pro_id].colors.fc + ';'"
+                 v-html="allProsFlat['pro_' + attachment.hcp_pro_id].displayedInitials">
+            </div>
+            <div class="flex-grow-1">
+                <div class="d-flex align-items-center">
+                    <a class="pdf-viewer-trigger" native target="_blank"
+                       :href="'/api/incomingReport/download/' + attachment.uid"
+                       title="View">
+                        <span v-if="attachment.title" v-html="attachment.title"></span>
+                    </a>
+                    <a class="text-warning-mellow ml-auto" native target="_blank" href="#"
+                       v-on:click.prevent="removeAttachmentFromItem(<?=$category?>PopupItem, attachment)">Remove from ticket</a>
+                </div>
+            </div>
+        </div>
+        <div class="listing-attachments-section"></div>
+    </div>
+</div>

+ 32 - 0
resources/views/app/patient/tickets/comments.blade.php

@@ -0,0 +1,32 @@
+<div v-if="<?=$category?>PopupMode === 'edit' && <?=$category?>PopupItem.data.comments && <?=$category?>PopupItem.data.comments.length" class="px-3 pt-3 pb-0 bg-light">
+    <div class="row mb-3">
+        <div class="col-12">
+            <label class="text-secondary mb-0 font-weight-normal font-size-16">Comments (<span v-html="<?=$category?>PopupItem.data.comments.length"></span>)</label>
+        </div>
+    </div>
+    <div v-for="(message) in <?=$category?>PopupItem.data.comments" class="d-flex align-items-start pb-3">
+        <div class="pro-initials text-uppercase mr-2"
+             :title="allProsFlat['pro_' + message.pro_id].displayedName"
+             :style="'background-color: ' + allProsFlat['pro_' + message.pro_id].colors.bc + '; color: ' + allProsFlat['pro_' + message.pro_id].colors.fc + ';'"
+             v-html="allProsFlat['pro_' + message.pro_id].displayedInitials">
+        </div>
+        <div class="flex-grow-1">
+            <div class="">
+                @{{ allProsFlat['pro_' + message.pro_id].displayedName }}
+                <span class="text-secondary ml-3">@{{ message.created_at }}</span>
+            </div>
+            <div>@{{ message.message }}</div>
+        </div>
+    </div>
+</div>
+<div v-if="<?=$category?>PopupMode === 'edit'" class="comment-input-outer p-3 bg-light d-flex align-items-start border-top">
+    <div class="pro-initials text-uppercase mr-2"
+         :title="allProsFlat['pro_' + own_pro_id].displayedName"
+         :style="'background-color: ' + allProsFlat['pro_' + own_pro_id].colors.bc + '; color: ' + allProsFlat['pro_' + own_pro_id].colors.fc + ';'"
+         v-html="allProsFlat['pro_' + own_pro_id].displayedInitials">
+    </div>
+    <div class="flex-grow-1 position-relative">
+        <textarea class="form-control form-control-sm txt-comment shadow-none" v-model="comment"></textarea>
+        <a href="#" v-on:click.prevent="<?=$category?>SaveComment()" class="btn btn-sm btn-primary text-white btn-save-comment">Comment</a>
+    </div>
+</div>

+ 89 - 0
resources/views/app/patient/tickets/common-fields.blade.php

@@ -0,0 +1,89 @@
+<div class="p-3 border-bottom">
+
+    <div v-if="<?=$category?>PopupMode === 'edit'">
+        <div class="row mb-2">
+            <div class="col-3 d-inline-flex align-items-center">
+                <label class="text-secondary m-0">Assigned Pro</label>
+            </div>
+            <div class="col-9">
+                <div class="d-flex align-items-center">
+                    <div class="pro-initials text-uppercase"
+                         :title="allProsFlat['pro_' + <?=$category?>PopupItem.assigned_pro_id].displayedName"
+                         :style="'background-color: ' + allProsFlat['pro_' + <?=$category?>PopupItem.assigned_pro_id].colors.bc + '; color: ' + allProsFlat['pro_' + <?=$category?>PopupItem.assigned_pro_id].colors.fc + ';'"
+                         v-html="allProsFlat['pro_' + <?=$category?>PopupItem.assigned_pro_id].displayedInitials">
+                    </div>
+                    <div class="flex-grow-1 ml-2">
+                        <select provider-search
+                                data-field="assigned_pro_uid"
+                                v-model="<?=$category?>PopupItem.assigned_pro_uid"
+                                :data-pro-name="[<?=$category?>PopupItem.assigned_pro.name_last, <?=$category?>PopupItem.assigned_pro.name_first].filter(Boolean).join(', ')"
+                                class="form-control form-control-sm">
+                            <option value=""> --select--</option>
+                        </select>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <div class="col-3 d-inline-flex align-items-center">
+                <label class="text-secondary m-0">Ordering Pro</label>
+            </div>
+            <div class="col-9">
+                <div class="d-flex align-items-center">
+                    <div class="pro-initials text-uppercase"
+                         :title="allProsFlat['pro_' + <?=$category?>PopupItem.ordering_pro_id].displayedName"
+                         :style="'background-color: ' + allProsFlat['pro_' + <?=$category?>PopupItem.ordering_pro_id].colors.bc + '; color: ' + allProsFlat['pro_' + <?=$category?>PopupItem.ordering_pro_id].colors.fc + ';'"
+                         v-html="allProsFlat['pro_' + <?=$category?>PopupItem.ordering_pro_id].displayedInitials">
+                    </div>
+                    <div class="flex-grow-1 mx-2">
+                        <select :provider-search="!<?=$category?>PopupItem.has_ordering_pro_signed"
+                                data-field="ordering_pro_uid"
+                                v-model="<?=$category?>PopupItem.ordering_pro_uid"
+                                :data-pro-name="[<?=$category?>PopupItem.ordering_pro.name_last, <?=$category?>PopupItem.ordering_pro.name_first].filter(Boolean).join(', ')"
+                                class="form-control form-control-sm"
+                                :class="<?=$category?>PopupItem.has_ordering_pro_signed ? 'd-none' : ''">
+                            <option value=""> --select--</option>
+                        </select>
+                        <input v-if="<?=$category?>PopupItem.has_ordering_pro_signed" type="text" readonly
+                               class="form-control form-control-sm mr-2"
+                               :value="[<?=$category?>PopupItem.ordering_pro.name_last, <?=$category?>PopupItem.ordering_pro.name_first].filter(Boolean).join(', ')">
+                    </div>
+                    <div v-if="<?=$category?>PopupItem.has_ordering_pro_signed" class="text-success text-nowrap">
+                        <i class="fa fa-check mr-1"></i>Signed
+                    </div>
+                    <div v-if="<?=$category?>PopupItem.ordering_pro_id === <?=$pro->id?>" class="ml-2 text-nowrap">
+                        <span v-if="!<?=$category?>PopupItem.has_ordering_pro_signed"
+                           class="btn btn-sm btn-primary text-white c-pointer"
+                           v-on:click.prevent="signAsOrderingPro(<?=$category?>PopupItem)">
+                            <i class="fa fa-signature mr-1"></i>Sign
+                        </span>
+                        {{--<span v-if="<?=$category?>PopupItem.has_ordering_pro_signed"
+                           class="text-warning-mellow c-pointer"
+                           v-on:click.prevent="undoSignAsOrderingPro(<?=$category?>PopupItem)">
+                            <i class="fa fa-ban mr-1"></i>Undo
+                        </span>--}}
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div class="row">
+        <div class="col-3 d-inline-flex align-items-center">
+            <label class="text-secondary m-0">Due Date</label>
+        </div>
+        <div class="col-9">
+            <div class="d-flex align-items-center">
+                <div class="pro-initials text-uppercase text-white bg-info"
+                     title="Due Date">
+                    <i class="fa fa-calendar-day"></i>
+                </div>
+                <div class="flex-grow-1 ml-2">
+                    <input type="text" data-field="due_date" placeholder="Due Date"
+                           v-model="<?=$category?>PopupItem.data.due_date" class="form-control form-control-sm date-input">
+                </div>
+            </div>
+        </div>
+    </div>
+
+</div>

+ 41 - 0
resources/views/app/patient/tickets/equipment-data.blade.php

@@ -0,0 +1,41 @@
+// equipment
+equipmentCollapsed: false,
+equipmentPopupMode: 'add',
+equipmentModel: {
+    uid: '',
+    is_open: true,
+    assigned_pro_uid: '',
+    ordering_pro_uid: '',
+    data: {
+        due_date: '',
+        items: [''],
+        purpose: '',
+        memo: '',
+        shipping_status: '',
+        tracking_number: '',
+        comments: [{
+            pro_id: '',
+            message: '',
+            created_at: '',
+        }],
+    }
+},
+equipmentPopupItem: {
+    uid: '',
+    is_open: true,
+    assigned_pro_uid: '',
+    ordering_pro_uid: '',
+    data: {
+        due_date: '',
+        items: [''],
+        purpose: '',
+        memo: '',
+        shipping_status: '',
+        tracking_number: '',
+        comments: [{
+            pro_id: '',
+            message: '',
+            created_at: '',
+        }],
+    }
+},

+ 124 - 0
resources/views/app/patient/tickets/equipment.blade.php

@@ -0,0 +1,124 @@
+<div class="my-4">
+    <div class="d-flex align-items-end mb-3">
+        <h4 class="font-weight-bold m-0 text-secondary font-size-16">Equipment
+            <span v-if="statusFilter === 'open'" class="text-secondary font-weight-normal">(@{{ equipmentNumOpen }} open)</span>
+            <span v-if="statusFilter === 'closed'" class="text-secondary font-weight-normal">(@{{ equipmentNumClosed }} closed)</span>
+            <span v-if="statusFilter === 'all'" class="text-secondary font-weight-normal">(@{{ equipmentNumOpen }} open, @{{ equipmentNumClosed }} closed)</span>
+        </h4>
+        <a class="px-3 c-pointer font-weight-bold"
+           v-on:click.prevent="equipmentShowPopup()">+ New</a>
+        <a class="py-0 font-weight-normal c-pointer flex-grow-1 text-right pr-2"
+           v-if="!equipmentCollapsed"
+           v-on:click.prevent="equipmentCollapsed = true">
+            <i class="text-secondary fa fa-chevron-up"></i>
+        </a>
+        <a class="py-0 font-weight-normal c-pointer flex-grow-1 text-right pr-2"
+           v-if="equipmentCollapsed"
+           v-on:click.prevent="equipmentCollapsed = false">
+            <i class="text-secondary fa fa-chevron-down"></i>
+        </a>
+    </div>
+    <div class="ticket-section" :class="equipmentCollapsed ? 'ticket-section-collapsed' : ''">
+        <table class="table table-sm tickets-table mb-0 border-bottom">
+            <tbody>
+            <tr v-for="(item, index) in ticketsByType.equipment"
+                v-if="(statusFilter === 'all' || (statusFilter === 'open' && item.is_open) || (statusFilter === 'closed' && !item.is_open)) && (item.data.items && Array.isArray(item.data.items) && item.data.items.length)"
+                :class="(item.is_open ? ' ' : 'opacity-60 bg-light ') + (currentItemUid === item.uid ? 'current' : '')">
+                <td class="px-2 py-2 c-pointer" v-on:click.prevent="equipmentShowPopup(item)">
+                    <div class="d-flex align-items-center">
+                        <div class="pro-initials text-uppercase"
+                             :title="allProsFlat['pro_' + item.assigned_pro_id].displayedName"
+                             :style="'background-color: ' + allProsFlat['pro_' + item.assigned_pro_id].colors.bc + '; color: ' + allProsFlat['pro_' + item.assigned_pro_id].colors.fc + ';'">
+                            @{{allProsFlat['pro_' + item.assigned_pro_id].displayedInitials}}
+                        </div>
+                        <div class="flex-grow-1 d-inline-flex ml-2 flex-wrap align-items-center">
+                            <span class="font-weight-bold text-dark font-size-13"
+                                  v-html="item.data.items && Array.isArray(item.data.items) ? item.data.items.join(', ') : '-'"></span>
+                            <span class="ml-3 font-weight-bold" v-if="item.data.attachments && item.data.attachments.length">
+                                <i class="fa fa-paperclip"></i>
+                            </span>
+                            <span class="text-nowrap ml-auto text-sm" v-if="item.data.due_date">
+                                <div title="Due Date" class="pro-initials text-uppercase text-white bg-info"><i class="fa fa-calendar-day"></i></div>
+                                <span class="text-secondary ml-1">@{{ item.data.due_date }}</span>
+                            </span>
+                        </div>
+                    </div>
+                    <!--
+                    <div>
+                        <span class="text-secondary text-sm mt-1">Created:</span>
+                        @{{ item.created_at }}
+                    </div>
+                    -->
+                </td>
+
+            </tr>
+            </tbody>
+        </table>
+    </div>
+    <div v-if="!preparing" class="stag-popup stag-popup-sm stag-slide mcp-theme-1" stag-popup-key="equipment-popup">
+        <form method="POST" action="" class="p-0">
+            <h3 class="stag-popup-title mb-0 p-3 bg-light sticky-top">
+                <span v-if="equipmentPopupMode === 'add'" class="flex-grow-1 text-nowrap overflow-hidden text-ellipsis mr-3">Add Equipment Ticket</span>
+                <span v-if="equipmentPopupMode === 'edit'" class="flex-grow-1 text-nowrap overflow-hidden text-ellipsis mr-3"
+                      v-html="equipmentPopupItem.data.items && Array.isArray(equipmentPopupItem.data.items) ? equipmentPopupItem.data.items.join(', ') : 'Edit Lab Ticket'"></span>
+                @include('app.patient.tickets.header-end', ['category' => 'equipment'])
+            </h3>
+
+            <!-- common - only applicable for edit -->
+            @include('app.patient.tickets.common-fields', ['category' => 'equipment'])
+
+            <!-- equipment specific -->
+            <div class="p-3 border-bottom">
+
+                <div class="row mb-2">
+                    <div class="col-12">
+                        <label class="text-secondary mb-0 font-weight-normal font-size-16">Equipment</label>
+                    </div>
+                </div>
+
+
+                <div class="mb-2">
+                    <div class="d-flex align-items-center mb-1">
+                        <label class="text-sm text-secondary mb-0">Items</label>
+                        <span class="text-sm mx-2 text-secondary">|</span>
+                        <a href="#" class="text-sm" v-on:click.prevent="equipmentPopupItem.data.items.push('')">Add</a>
+                    </div>
+                    <div class="d-flex align-items-center mb-0" v-for="(item, itemIndex) in equipmentPopupItem.data.items">
+                        <div class="position-relative flex-grow-1">
+                            <input required type="text" data-option-list v-model="equipmentPopupItem.data.items[itemIndex]" class="form-control form-control-sm" placeholder="Item">
+                            <div class="data-option-list">
+                                <div>Weight Scale</div>
+                                <div>Pulse Ox</div>
+                                <div>Temperature Gun</div>
+                            </div>
+                        </div>
+                        <a v-if="equipmentPopupItem.data.items.length > 1" class="ml-2 text-danger" href="#" v-on:click.prevent="equipmentPopupItem.data.items.splice(itemIndex, 1)">
+                            <i class="fa fa-trash-alt"></i>
+                        </a>
+                    </div>
+                </div>
+                <div class="mb-2">
+                    <label class="text-sm text-secondary mb-1">Purpose</label>
+                    <input type="text" v-model="equipmentPopupItem.data.purpose" class="form-control form-control-sm" placeholder="Purpose">
+                </div>
+                <div class="mb-2">
+                    <label class="text-sm text-secondary mb-1">Memo</label>
+                    <input type="text" v-model="equipmentPopupItem.data.memo" class="form-control form-control-sm" placeholder="Memo">
+                </div>
+
+
+                <div v-if="equipmentPopupMode === 'add'" class="d-flex align-items-center justify-content-start mt-3">
+                    <button type="button" class="btn btn-sm btn-primary mr-2" v-on:click.prevent="equipmentSavePopupItem()">Submit</button>
+                    <button type="button" class="btn btn-sm btn-default border" onclick="return closeStagPopup()">Cancel</button>
+                </div>
+            </div>
+
+            <!-- attachments -->
+            @include('app.patient.tickets.attachments', ['category' => 'equipment'])
+
+            <!-- comments -->
+            @include('app.patient.tickets.comments', ['category' => 'equipment'])
+
+        </form>
+    </div>
+</div>

+ 59 - 0
resources/views/app/patient/tickets/erx-data.blade.php

@@ -0,0 +1,59 @@
+// erx
+erxCollapsed: false,
+erxPopupMode: 'add',
+erxModel: {
+    uid: '',
+    is_open: true,
+    assigned_pro_uid: '',
+    ordering_pro_uid: '',
+    data: {
+        due_date: '',
+        medication: '',
+        strength: '',
+        amount: '',
+        route: '',
+        frequency: '',
+        dispense: '',
+        refills: '',
+        purpose: '',
+        pharmacyName: '',
+        pharmacyCity: '',
+        pharmacyState: '',
+        pharmacyAddressMemo: '',
+        pharmacyPhone: '',
+        pharmacyFax: '',
+        comments: [{
+            pro_id: '',
+            message: '',
+            created_at: '',
+        }],
+    }
+},
+erxPopupItem: {
+    uid: '',
+    is_open: true,
+    assigned_pro_uid: '',
+    ordering_pro_uid: '',
+    data: {
+        due_date: '',
+        medication: '',
+        strength: '',
+        amount: '',
+        route: '',
+        frequency: '',
+        dispense: '',
+        refills: '',
+        purpose: '',
+        pharmacyName: '',
+        pharmacyCity: '',
+        pharmacyState: '',
+        pharmacyAddressMemo: '',
+        pharmacyPhone: '',
+        pharmacyFax: '',
+        comments: [{
+            pro_id: '',
+            message: '',
+            created_at: '',
+        }],
+    }
+},

+ 218 - 0
resources/views/app/patient/tickets/erx.blade.php

@@ -0,0 +1,218 @@
+<div class="my-4">
+    <div class="d-flex align-items-end mb-3">
+        <h4 class="font-weight-bold m-0 text-secondary font-size-16">ERx
+            <span v-if="statusFilter === 'open'" class="text-secondary font-weight-normal">(@{{ erxNumOpen }} open)</span>
+            <span v-if="statusFilter === 'closed'" class="text-secondary font-weight-normal">(@{{ erxNumClosed }} closed)</span>
+            <span v-if="statusFilter === 'all'" class="text-secondary font-weight-normal">(@{{ erxNumOpen }} open, @{{ erxNumClosed }} closed)</span>
+        </h4>
+        <a class="px-3 c-pointer font-weight-bold"
+           v-on:click.prevent="erxShowPopup()">+ New</a>
+        <a class="py-0 font-weight-normal c-pointer flex-grow-1 text-right pr-2"
+           v-if="!erxCollapsed"
+           v-on:click.prevent="erxCollapsed = true">
+            <i class="text-secondary fa fa-chevron-up"></i>
+        </a>
+        <a class="py-0 font-weight-normal c-pointer flex-grow-1 text-right pr-2"
+           v-if="erxCollapsed"
+           v-on:click.prevent="erxCollapsed = false">
+            <i class="text-secondary fa fa-chevron-down"></i>
+        </a>
+    </div>
+    <div class="ticket-section" :class="erxCollapsed ? 'ticket-section-collapsed' : ''">
+        <table class="table table-sm tickets-table mb-0 border-bottom">
+            <tbody>
+            <tr v-for="(item, index) in ticketsByType.erx"
+                v-if="statusFilter === 'all' || (statusFilter === 'open' && item.is_open) || (statusFilter === 'closed' && !item.is_open)"
+                :class="(item.is_open ? ' ' : 'opacity-60 bg-light ') + (currentItemUid === item.uid ? 'current' : '')">
+                <td class="px-2 py-2 c-pointer" v-on:click.prevent="erxShowPopup(item)">
+                    <div class="d-flex align-items-center">
+                        <div class="pro-initials text-uppercase"
+                             :title="allProsFlat['pro_' + item.assigned_pro_id].displayedName"
+                             :style="'background-color: ' + allProsFlat['pro_' + item.assigned_pro_id].colors.bc + '; color: ' + allProsFlat['pro_' + item.assigned_pro_id].colors.fc + ';'">
+                            @{{allProsFlat['pro_' + item.assigned_pro_id].displayedInitials}}
+                        </div>
+                        <div class="flex-grow-1 d-inline-flex ml-2 flex-wrap align-items-center">
+                            <span class="font-weight-bold text-dark font-size-13">@{{item.data.medication}}</span>
+                            <span class="d-inline-flex align-items-center" v-if="item.data.strength">
+                                    <span class="mx-2 text-secondary">•</span>
+                                    <span>@{{item.data.strength}}</span>
+                                </span>
+                            <!--                                <span class="d-inline-flex align-items-center" v-if="item.data.route">
+                                                                <span class="mx-2 text-secondary">•</span>
+                                                                <span>@{{item.data.route}}</span>
+                                                            </span>
+                                                            <span class="d-inline-flex align-items-center" v-if="item.data.frequency">
+                                                                <span class="mx-2 text-secondary">•</span>
+                                                                <span>@{{item.data.frequency}}</span>
+                                                            </span>-->
+                            <!--
+                            <span class="d-inline-flex align-items-center" v-if="item.data.dispense">
+                                <span class="mx-2 text-secondary">•</span>
+                                <span>Dispense:</span> @{{item.data.dispense}}
+                                <span class="text-secondary ml-1" v-html="inWords(item.data.dispense)"></span>
+                            </span>
+                            <span class="d-inline-flex align-items-center" v-if="item.data.refills">
+                                <span class="mx-2 text-secondary">•</span>
+                                <span><span>Refills:</span> @{{item.data.refills}}</span>
+                            </span>
+                            <span class="d-inline-flex align-items-center" v-if="item.data.purpose">
+                                <span class="mx-2 text-secondary">•</span>
+                                <span><span>Purpose:</span> @{{item.data.purpose}}</span>
+                            </span>
+                            -->
+                            <span class="text-nowrap ml-4 text-secondary" v-html="pharmacy(item.data)"></span>
+                            <span class="ml-3 font-weight-bold" v-if="item.data.attachments && item.data.attachments.length">
+                                <i class="fa fa-paperclip"></i>
+                            </span>
+                            <span class="text-nowrap ml-auto text-sm" v-if="item.data.due_date">
+                                <div title="Due Date" class="pro-initials text-uppercase text-white bg-info"><i class="fa fa-calendar-day"></i></div>
+                                <span class="text-secondary ml-1">@{{ item.data.due_date }}</span>
+                            </span>
+                        </div>
+                    </div>
+                    <!--
+                    <div>
+                        <span class="text-secondary text-sm mt-1">Created:</span>
+                        @{{ item.created_at }}
+                    </div>
+                    -->
+                </td>
+
+            </tr>
+            </tbody>
+        </table>
+    </div>
+    <div v-if="!preparing" class="stag-popup stag-popup-sm stag-slide mcp-theme-1" stag-popup-key="erx-popup">
+        <form method="POST" action="" class="p-0">
+            <h3 class="stag-popup-title mb-0 p-3 bg-light sticky-top">
+                <span class="flex-grow-1 text-nowrap overflow-hidden text-ellipsis mr-3">@{{ erxPopupMode === 'add' ? 'Add ERx Ticket' : erxPopupItem.data.medication + '  •  ' + erxPopupItem.data.strength }}</span>
+                @include('app.patient.tickets.header-end', ['category' => 'erx'])
+            </h3>
+
+            <!-- common - only applicable for edit -->
+            @include('app.patient.tickets.common-fields', ['category' => 'erx'])
+
+            <div class="p-3 border-bottom">
+
+                <div class="row mb-2">
+                    <div class="col-12">
+                        <label class="text-secondary mb-0 font-weight-normal font-size-16">ERx</label>
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-9">
+                        <label class="text-sm text-secondary mb-1">Medication</label>
+                        <input type="hidden" v-model="erxPopupItem.data.medication">
+                        <input required type="text" data-field="medication" placeholder="Medication"
+                               :value="erxPopupItem.data.medication"
+                               class="form-control form-control-sm">
+                    </div>
+                    <div class="col-3 pl-0">
+                        <label class="text-sm text-secondary mb-1">Strength</label>
+                        <input type="hidden" v-model="erxPopupItem.data.strength">
+                        <input type="text" data-field="strength" placeholder="Strength"
+                               :value="erxPopupItem.data.strength"
+                               class="form-control form-control-sm min-width-unset">
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-3">
+                        <label class="text-sm text-secondary mb-1">Frequency</label>
+                        <input type="text" placeholder="Frequency"
+                               v-model="erxPopupItem.data.frequency" class="form-control form-control-sm min-width-unset"
+                               data-option-list="frequency-options">
+                        <div id="frequency-options" class="data-option-list">
+                            <div>Once a day</div>
+                            <div>Twice a day</div>
+                        </div>
+                    </div>
+                    <div class="col-3 pl-0">
+                        <label class="text-sm text-secondary mb-1">Route</label>
+                        <input required type="text" placeholder="Route"
+                               v-model="erxPopupItem.data.route" class="form-control form-control-sm min-width-unset"
+                               data-option-list="route-options">
+                        <div id="route-options" class="data-option-list">
+                            <div>PO (by mouth)</div>
+                            <div>PR (per rectum)</div>
+                            <div>IM (intramuscular)</div>
+                            <div>IV (intravenous)</div>
+                            <div>ID (intradermal)</div>
+                            <div>IN (intranasal)</div>
+                            <div>TP (topical)</div>
+                            <div>SL (sublingual)</div>
+                            <div>BUCC (buccal)</div>
+                            <div>IP (intraperitoneal)</div>
+                        </div>
+                    </div>
+                    <div class="col-3 pl-0">
+                        <label class="text-sm text-secondary mb-1">Dispense Amount</label>
+                        <input required type="number" placeholder="Dispense Amount"
+                               v-model="erxPopupItem.data.dispense" class="form-control form-control-sm min-width-unset">
+                    </div>
+                    <div class="col-3 pl-0">
+                        <label class="text-sm text-secondary mb-1">Refills</label>
+                        <input type="number" placeholder="Refills"
+                               v-model="erxPopupItem.data.refills" class="form-control form-control-sm min-width-unset">
+                    </div>
+                </div>
+                <div class="row">
+                    <div class="col-12">
+                        <label class="text-sm text-secondary mb-1">Purpose</label>
+                        <input required type="text"  placeholder="Purpose"
+                               data-field="icd" v-model="erxPopupItem.data.purpose" class="form-control form-control-sm">
+                    </div>
+                </div>
+
+            </div>
+            <div class="p-3 border-bottom">
+                <div class="row mb-2">
+                    <div class="col-12">
+                        <label class="text-secondary mb-0 font-weight-normal font-size-16">Preferred Pharmacy</label>
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-12">
+                        <label class="text-sm text-secondary mb-1">Business Name</label>
+                        <input type="text" autocomplete="donotdoit" placeholder="Business Name" id="pharmacy-search" v-model="erxPopupItem.data.pharmacyName" class="form-control form-control-sm">
+                        <div class="suggestions-outer pharmacy-suggestions position-absolute d-none"></div>
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-4">
+                        <label class="text-sm text-secondary mb-1">City</label>
+                        <input type="text" placeholder="City" v-model="erxPopupItem.data.pharmacyCity" class="form-control form-control-sm min-width-unset">
+                    </div>
+                    <div class="col-2 pl-0">
+                        <label class="text-sm text-secondary mb-1">State</label>
+                        <input type="text" placeholder="State" v-model="erxPopupItem.data.pharmacyState" class="form-control form-control-sm min-width-unset">
+                    </div>
+                    <div class="col-3 pl-0">
+                        <label class="text-sm text-secondary mb-1">Phone</label>
+                        <input type="text" placeholder="Phone" v-model="erxPopupItem.data.pharmacyPhone" class="form-control form-control-sm min-width-unset">
+                    </div>
+                    <div class="col-3 pl-0">
+                        <label class="text-sm text-secondary mb-1">Fax</label>
+                        <input type="text" placeholder="Fax" v-model="erxPopupItem.data.pharmacyFax" class="form-control form-control-sm min-width-unset">
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-12">
+                        <label class="text-sm text-secondary mb-1">Address Memo</label>
+                        <input type="text" placeholder="Address Memo" autocomplete="donotdoit" v-model="erxPopupItem.data.pharmacyAddressMemo" class="form-control form-control-sm">
+                    </div>
+                </div>
+                <div v-if="erxPopupMode === 'add'" class="d-flex align-items-center justify-content-start mt-3">
+                    <button type="button" class="btn btn-sm btn-primary mr-2" v-on:click.prevent="erxSavePopupItem()">Submit</button>
+                    <button type="button" class="btn btn-sm btn-default border" onclick="return closeStagPopup()">Cancel</button>
+                </div>
+            </div>
+
+            <!-- attachments -->
+            @include('app.patient.tickets.attachments', ['category' => 'erx'])
+
+            <!-- comments -->
+            @include('app.patient.tickets.comments', ['category' => 'erx'])
+
+        </form>
+    </div>
+</div>

+ 13 - 0
resources/views/app/patient/tickets/header-end.blade.php

@@ -0,0 +1,13 @@
+<div v-if="<?=$category?>PopupMode === 'edit'" class="ml-auto mr-3">
+    <a v-if="<?=$category?>PopupItem.is_open" href="#"
+       class="btn btn-sm btn-default border bg-white text-dark text-nowrap shadow-none"
+       v-on:click.prevent="closeItem(<?=$category?>PopupItem)">
+        <i class="fa fa-check text-success mr-2"></i>Mark Complete
+    </a>
+    <a v-if="!<?=$category?>PopupItem.is_open" href="#"
+       class="btn btn-sm btn-success text-nowrap text-white shadow-none"
+       v-on:click.prevent="openItem(<?=$category?>PopupItem)">
+        <i class="fa fa-check mr-2"></i>Complete
+    </a>
+</div>
+<a href="#" class="text-secondary" onclick="return closeStagPopup()"><i class="fa fa-chevron-right"></i></a>

+ 37 - 0
resources/views/app/patient/tickets/imaging-data.blade.php

@@ -0,0 +1,37 @@
+// imaging
+imagingCollapsed: false,
+imagingPopupMode: 'add',
+imagingModel: {
+    uid: '',
+    is_open: true,
+    assigned_pro_uid: '',
+    ordering_pro_uid: '',
+    data: {
+        due_date: '',
+        tests: [''],
+        icds: [''],
+        memo: '',
+        comments: [{
+            pro_id: '',
+            message: '',
+            created_at: '',
+        }],
+    }
+},
+imagingPopupItem: {
+    uid: '',
+    is_open: true,
+    assigned_pro_uid: '',
+    ordering_pro_uid: '',
+    data: {
+        due_date: '',
+        tests: [''],
+        icds: [''],
+        memo: '',
+        comments: [{
+            pro_id: '',
+            message: '',
+            created_at: '',
+        }],
+    }
+},

+ 138 - 0
resources/views/app/patient/tickets/imaging.blade.php

@@ -0,0 +1,138 @@
+<div class="my-4">
+    <div class="d-flex align-items-end mb-3">
+        <h4 class="font-weight-bold m-0 text-secondary font-size-16">Imaging
+            <span v-if="statusFilter === 'open'" class="text-secondary font-weight-normal">(@{{ imagingNumOpen }} open)</span>
+            <span v-if="statusFilter === 'closed'" class="text-secondary font-weight-normal">(@{{ imagingNumClosed }} closed)</span>
+            <span v-if="statusFilter === 'all'" class="text-secondary font-weight-normal">(@{{ imagingNumOpen }} open, @{{ imagingNumClosed }} closed)</span>
+        </h4>
+        <a class="px-3 c-pointer font-weight-bold"
+           v-on:click.prevent="imagingShowPopup()">+ New</a>
+        <a class="py-0 font-weight-normal c-pointer flex-grow-1 text-right pr-2"
+           v-if="!imagingCollapsed"
+           v-on:click.prevent="imagingCollapsed = true">
+            <i class="text-secondary fa fa-chevron-up"></i>
+        </a>
+        <a class="py-0 font-weight-normal c-pointer flex-grow-1 text-right pr-2"
+           v-if="imagingCollapsed"
+           v-on:click.prevent="imagingCollapsed = false">
+            <i class="text-secondary fa fa-chevron-down"></i>
+        </a>
+    </div>
+    <div class="ticket-section" :class="imagingCollapsed ? 'ticket-section-collapsed' : ''">
+        <table class="table table-sm tickets-table mb-0 border-bottom">
+            <tbody>
+            <tr v-for="(item, index) in ticketsByType.imaging"
+                v-if="(statusFilter === 'all' || (statusFilter === 'open' && item.is_open) || (statusFilter === 'closed' && !item.is_open)) && (item.data.tests && Array.isArray(item.data.tests) && item.data.tests.length)"
+                :class="(item.is_open ? ' ' : 'opacity-60 bg-light ') + (currentItemUid === item.uid ? 'current' : '')">
+                <td class="px-2 py-2 c-pointer" v-on:click.prevent="imagingShowPopup(item)">
+                    <div class="d-flex align-items-center">
+                        <div class="pro-initials text-uppercase"
+                             :title="allProsFlat['pro_' + item.assigned_pro_id].displayedName"
+                             :style="'background-color: ' + allProsFlat['pro_' + item.assigned_pro_id].colors.bc + '; color: ' + allProsFlat['pro_' + item.assigned_pro_id].colors.fc + ';'">
+                            @{{allProsFlat['pro_' + item.assigned_pro_id].displayedInitials}}
+                        </div>
+                        <div class="flex-grow-1 d-inline-flex ml-2 flex-wrap align-items-center">
+                            <span class="font-weight-bold text-dark font-size-13"
+                                  v-html="item.data.tests && Array.isArray(item.data.tests) ? item.data.tests.join(', ') : '-'"></span>
+                            <span class="ml-3 font-weight-bold" v-if="item.data.attachments && item.data.attachments.length">
+                                <i class="fa fa-paperclip"></i>
+                            </span>
+                            <span class="text-nowrap ml-auto text-sm" v-if="item.data.due_date">
+                                <div title="Due Date" class="pro-initials text-uppercase text-white bg-info"><i class="fa fa-calendar-day"></i></div>
+                                <span class="text-secondary ml-1">@{{ item.data.due_date }}</span>
+                            </span>
+                        </div>
+                    </div>
+                    <!--
+                    <div>
+                        <span class="text-secondary text-sm mt-1">Created:</span>
+                        @{{ item.created_at }}
+                    </div>
+                    -->
+                </td>
+
+            </tr>
+            </tbody>
+        </table>
+    </div>
+    <div v-if="!preparing" class="stag-popup stag-popup-sm stag-slide mcp-theme-1" stag-popup-key="imaging-popup">
+        <form method="POST" action="" class="p-0">
+            <h3 class="stag-popup-title mb-0 p-3 bg-light sticky-top">
+                <span v-if="imagingPopupMode === 'add'" class="flex-grow-1 text-nowrap overflow-hidden text-ellipsis mr-3">Add Imaging Ticket</span>
+                <span v-if="imagingPopupMode === 'edit'" class="flex-grow-1 text-nowrap overflow-hidden text-ellipsis mr-3"
+                      v-html="imagingPopupItem.data.tests && Array.isArray(imagingPopupItem.data.tests) ? imagingPopupItem.data.tests.join(', ') : 'Edit Imaging Ticket'"></span>
+                @include('app.patient.tickets.header-end', ['category' => 'imaging'])
+            </h3>
+
+            <!-- common - only applicable for edit -->
+            @include('app.patient.tickets.common-fields', ['category' => 'imaging'])
+
+            <!-- imaging specific -->
+            <div class="p-3 border-bottom">
+
+                <div class="row mb-2">
+                    <div class="col-12">
+                        <label class="text-secondary mb-0 font-weight-normal font-size-16">Imaging</label>
+                    </div>
+                </div>
+
+                <div class="mb-2">
+                    <div class="d-flex align-items-center mb-1">
+                        <label class="text-sm text-secondary mb-0">Tests</label>
+                        <span class="text-sm mx-2 text-secondary">|</span>
+                        <a href="#" class="text-sm" v-on:click.prevent="imagingPopupItem.data.tests.push('')">Add</a>
+                    </div>
+                    <div class="d-flex align-items-center mb-2" v-for="(test, testIndex) in imagingPopupItem.data.tests">
+                        <div class="position-relative flex-grow-1">
+                            <input required type="text" data-option-list v-model="imagingPopupItem.data.tests[testIndex]" class="form-control form-control-sm" placeholder="Test">
+                            <div class="data-option-list">
+                                <div>CAT Scan</div>
+                                <div>Fluoroscopy</div>
+                                <div>MRI Scan</div>
+                                <div>Mammography</div>
+                                <div>X-Ray</div>
+                                <div>PET</div>
+                                <div>PET-CT</div>
+                            </div>
+                        </div>
+                        <a v-if="imagingPopupItem.data.tests.length > 1" class="ml-2 text-danger" href="#" v-on:click.prevent="imagingPopupItem.data.tests.splice(testIndex, 1)">
+                            <i class="fa fa-trash-alt"></i>
+                        </a>
+                    </div>
+                </div>
+                <div class="mb-2">
+                    <div class="d-flex align-items-center mb-1">
+                        <label class="text-sm text-secondary mb-0">ICDs</label>
+                        <span class="text-sm mx-2 text-secondary">|</span>
+                        <a href="#" class="text-sm" v-on:click.prevent="imagingAddICD()">Add</a>
+                    </div>
+                    <div class="d-flex align-items-center mb-2" v-for="(icd, icdIndex) in imagingPopupItem.data.icds">
+                        <div class="position-relative flex-grow-1">
+                            <input required type="text" data-field="icd" :data-index="icdIndex" placeholder="ICD"
+                                   v-model="imagingPopupItem.data.icds[icdIndex]" class="form-control form-control-sm">
+                        </div>
+                        <a v-if="imagingPopupItem.data.icds.length > 1" class="ml-2 text-danger" href="#" v-on:click.prevent="imagingPopupItem.data.icds.splice(icdIndex, 1)">
+                            <i class="fa fa-trash-alt"></i>
+                        </a>
+                    </div>
+                </div>
+                <div class="mb-2">
+                    <label class="text-sm text-secondary mb-1">Memo</label>
+                    <input type="text" v-model="imagingPopupItem.data.memo" class="form-control form-control-sm">
+                </div>
+
+
+                <div v-if="imagingPopupMode === 'add'" class="d-flex align-items-center justify-content-start mt-3">
+                    <button type="button" class="btn btn-sm btn-primary mr-2" v-on:click.prevent="saveimagingPopupItem()">Submit</button>
+                    <button type="button" class="btn btn-sm btn-default border" onclick="return closeStagPopup()">Cancel</button>
+                </div>
+            </div>
+
+            <!-- attachments -->
+            @include('app.patient.tickets.attachments', ['category' => 'imaging'])
+
+            <!-- comments -->
+            @include('app.patient.tickets.comments', ['category' => 'imaging'])
+        </form>
+    </div>
+</div>

+ 37 - 0
resources/views/app/patient/tickets/lab-data.blade.php

@@ -0,0 +1,37 @@
+// lab
+labCollapsed: false,
+labPopupMode: 'add',
+labModel: {
+    uid: '',
+    is_open: true,
+    assigned_pro_uid: '',
+    ordering_pro_uid: '',
+    data: {
+        due_date: '',
+        tests: [''],
+        icds: [''],
+        memo: '',
+        comments: [{
+            pro_id: '',
+            message: '',
+            created_at: '',
+        }],
+    }
+},
+labPopupItem: {
+    uid: '',
+    is_open: true,
+    assigned_pro_uid: '',
+    ordering_pro_uid: '',
+    data: {
+        due_date: '',
+        tests: [''],
+        icds: [''],
+        memo: '',
+        comments: [{
+            pro_id: '',
+            message: '',
+            created_at: '',
+        }],
+    }
+},

+ 143 - 0
resources/views/app/patient/tickets/lab.blade.php

@@ -0,0 +1,143 @@
+<div class="my-4">
+    <div class="d-flex align-items-end mb-3">
+        <h4 class="font-weight-bold m-0 text-secondary font-size-16">Lab
+            <span v-if="statusFilter === 'open'" class="text-secondary font-weight-normal">(@{{ labNumOpen }} open)</span>
+            <span v-if="statusFilter === 'closed'" class="text-secondary font-weight-normal">(@{{ labNumClosed }} closed)</span>
+            <span v-if="statusFilter === 'all'" class="text-secondary font-weight-normal">(@{{ labNumOpen }} open, @{{ labNumClosed }} closed)</span>
+        </h4>
+        <a class="px-3 c-pointer font-weight-bold"
+           v-on:click.prevent="labShowPopup()">+ New</a>
+        <a class="py-0 font-weight-normal c-pointer flex-grow-1 text-right pr-2"
+           v-if="!labCollapsed"
+           v-on:click.prevent="labCollapsed = true">
+            <i class="text-secondary fa fa-chevron-up"></i>
+        </a>
+        <a class="py-0 font-weight-normal c-pointer flex-grow-1 text-right pr-2"
+           v-if="labCollapsed"
+           v-on:click.prevent="labCollapsed = false">
+            <i class="text-secondary fa fa-chevron-down"></i>
+        </a>
+    </div>
+    <div class="ticket-section" :class="labCollapsed ? 'ticket-section-collapsed' : ''">
+        <table class="table table-sm tickets-table mb-0 border-bottom">
+            <tbody>
+            <tr v-for="(item, index) in ticketsByType.lab"
+                v-if="(statusFilter === 'all' || (statusFilter === 'open' && item.is_open) || (statusFilter === 'closed' && !item.is_open)) && (item.data.tests && Array.isArray(item.data.tests) && item.data.tests.length)"
+                :class="(item.is_open ? ' ' : 'opacity-60 bg-light ') + (currentItemUid === item.uid ? 'current' : '')">
+                <td class="px-2 py-2 c-pointer" v-on:click.prevent="labShowPopup(item)">
+                    <div class="d-flex align-items-center">
+                        <div class="pro-initials text-uppercase"
+                             :title="allProsFlat['pro_' + item.assigned_pro_id].displayedName"
+                             :style="'background-color: ' + allProsFlat['pro_' + item.assigned_pro_id].colors.bc + '; color: ' + allProsFlat['pro_' + item.assigned_pro_id].colors.fc + ';'">
+                            @{{allProsFlat['pro_' + item.assigned_pro_id].displayedInitials}}
+                        </div>
+                        <div class="flex-grow-1 d-inline-flex ml-2 flex-wrap align-items-center">
+                            <span class="font-weight-bold text-dark font-size-13"
+                                  v-html="item.data.tests && Array.isArray(item.data.tests) ? item.data.tests.join(', ') : '-'"></span>
+                            <span class="ml-3 font-weight-bold" v-if="item.data.attachments && item.data.attachments.length">
+                                <i class="fa fa-paperclip"></i>
+                            </span>
+                            <span class="text-nowrap ml-auto text-sm" v-if="item.data.due_date">
+                                <div title="Due Date" class="pro-initials text-uppercase text-white bg-info"><i class="fa fa-calendar-day"></i></div>
+                                <span class="text-secondary ml-1">@{{ item.data.due_date }}</span>
+                            </span>
+                        </div>
+                    </div>
+                    <!--
+                    <div>
+                        <span class="text-secondary text-sm mt-1">Created:</span>
+                        @{{ item.created_at }}
+                    </div>
+                    -->
+                </td>
+
+            </tr>
+            </tbody>
+        </table>
+    </div>
+    <div v-if="!preparing" class="stag-popup stag-popup-sm stag-slide mcp-theme-1" stag-popup-key="lab-popup">
+        <form method="POST" action="" class="p-0">
+            <h3 class="stag-popup-title mb-0 p-3 bg-light sticky-top">
+                <span v-if="labPopupMode === 'add'" class="flex-grow-1 text-nowrap overflow-hidden text-ellipsis mr-3">Add Lab Ticket</span>
+                <span v-if="labPopupMode === 'edit'" class="flex-grow-1 text-nowrap overflow-hidden text-ellipsis mr-3"
+                      v-html="labPopupItem.data.tests && Array.isArray(labPopupItem.data.tests) ? labPopupItem.data.tests.join(', ') : 'Edit Lab Ticket'"></span>
+                @include('app.patient.tickets.header-end', ['category' => 'lab'])
+            </h3>
+
+            <!-- common - only applicable for edit -->
+            @include('app.patient.tickets.common-fields', ['category' => 'lab'])
+
+            <!-- lab specific -->
+            <div class="p-3 border-bottom">
+
+                <div class="row mb-2">
+                    <div class="col-12">
+                        <label class="text-secondary mb-0 font-weight-normal font-size-16">Lab</label>
+                    </div>
+                </div>
+
+
+                <div class="mb-2">
+                    <div class="d-flex align-items-center mb-1">
+                        <label class="text-sm text-secondary mb-0">Tests</label>
+                        <span class="text-sm mx-2 text-secondary">|</span>
+                        <a href="#" class="text-sm" v-on:click.prevent="labPopupItem.data.tests.push('')">Add</a>
+                    </div>
+                    <div class="d-flex align-items-center mb-0" v-for="(test, testIndex) in labPopupItem.data.tests">
+                        <div class="position-relative flex-grow-1">
+                            <input required type="text" data-option-list v-model="labPopupItem.data.tests[testIndex]" class="form-control form-control-sm" placeholder="Test">
+                            <div class="data-option-list">
+                                <div>Complete Blood Count</div>
+                                <div>Prothrombin Time</div>
+                                <div>Basic Metabolic Panel</div>
+                                <div>Comprehensive Metabolic Panel</div>
+                                <div>Lipid Panel</div>
+                                <div>Liver Panel</div>
+                                <div>Thyroid Stimulating Hormone</div>
+                                <div>Hemoglobin A1C</div>
+                                <div>Urinalysis</div>
+                                <div>Cultures</div>
+                            </div>
+                        </div>
+                        <a v-if="labPopupItem.data.tests.length > 1" class="ml-2 text-danger" href="#" v-on:click.prevent="labPopupItem.data.tests.splice(testIndex, 1)">
+                            <i class="fa fa-trash-alt"></i>
+                        </a>
+                    </div>
+                </div>
+                <div class="mb-2">
+                    <div class="d-flex align-items-center mb-1">
+                        <label class="text-sm text-secondary mb-0">ICDs</label>
+                        <span class="text-sm mx-2 text-secondary">|</span>
+                        <a href="#" class="text-sm" v-on:click.prevent="labAddICD()">Add</a>
+                    </div>
+                    <div class="d-flex align-items-center mb-0" v-for="(icd, icdIndex) in labPopupItem.data.icds">
+                        <div class="position-relative flex-grow-1">
+                            <input required type="text" data-field="icd" :data-index="icdIndex" placeholder="ICD"
+                                   v-model="labPopupItem.data.icds[icdIndex]" class="form-control form-control-sm">
+                        </div>
+                        <a v-if="labPopupItem.data.icds.length > 1" class="ml-2 text-danger" href="#" v-on:click.prevent="labPopupItem.data.icds.splice(icdIndex, 1)">
+                            <i class="fa fa-trash-alt"></i>
+                        </a>
+                    </div>
+                </div>
+                <div class="mb-2">
+                    <label class="text-sm text-secondary mb-1">Memo</label>
+                    <input type="text" v-model="labPopupItem.data.memo" class="form-control form-control-sm">
+                </div>
+
+
+                <div v-if="labPopupMode === 'add'" class="d-flex align-items-center justify-content-start mt-3">
+                    <button type="button" class="btn btn-sm btn-primary mr-2" v-on:click.prevent="labSavePopupItem()">Submit</button>
+                    <button type="button" class="btn btn-sm btn-default border" onclick="return closeStagPopup()">Cancel</button>
+                </div>
+            </div>
+
+            <!-- attachments -->
+            @include('app.patient.tickets.attachments', ['category' => 'lab'])
+
+            <!-- comments -->
+            @include('app.patient.tickets.comments', ['category' => 'lab'])
+
+        </form>
+    </div>
+</div>

+ 68 - 0
resources/views/app/patient/tickets/methods.blade.php

@@ -0,0 +1,68 @@
+{{ $category }}ShowPopup: function(_item) {
+    this.{{ $category }}PopupMode = _item ? 'edit' : 'add';
+    this.{{ $category }}PopupItem = _item ? JSON.parse(JSON.stringify(_item)) : JSON.parse(JSON.stringify(this.{{ $category }}Model));
+    this.{{ $category }}PopupItem.assigned_pro_uid = '';
+    this.{{ $category }}PopupItem.ordering_pro_uid = '';
+    if(!this.{{ $category }}PopupItem.comments) this.{{ $category }}PopupItem.comments = [];
+    this.comment = '';
+    showStagPopup('{{ $category }}-popup', true);
+    Vue.nextTick(() => {
+        $('#ticketsApp [pro-suggest-initialized]').removeAttr('pro-suggest-initialized');
+        initProSuggest();
+        if (_item) {
+            this.currentCategory = '{{ $category }}';
+            this.currentItemUid = _item.uid;
+        }
+    });
+},
+{{ $category }}SavePopupItem: function(_autoSave = false) {
+    let form = $('#ticketsApp form').first();
+    if(!_autoSave) {
+        if(!form[0].checkValidity()) {
+            form[0].reportValidity();
+            return false;
+        }
+    }
+    if(!_autoSave) showMask();
+    let payload = {};
+    if(this.{{ $category }}PopupMode === 'add') {
+        payload.clientUid = '{{ $patient->uid }}';
+        payload.category = '{{ $category }}';
+        payload.assignedProUid = '{{ $pro->uid  }}';
+        payload.managerProUid = '{{ $pro->uid  }}';
+        payload.orderingProUid = '{{ $pro->uid  }}';
+        payload.initiatingProUid = '{{ $pro->uid  }}';
+        payload.data = JSON.stringify(this.{{ $category }}PopupItem.data);
+    }
+    else {
+        payload.uid = this.{{ $category }}PopupItem.uid;
+        payload.newData = JSON.stringify(this.{{ $category }}PopupItem.data);
+    }
+    $.post(
+        '/api/ticket/' + (this.{{ $category }}PopupMode === 'add' ? 'create' : 'updateData'),
+        payload,
+        (_data) => {
+            console.log(_data);
+            if(!_autoSave) {
+                fastReload();
+            }
+            else {
+                this.reloadPopupItem('{{ $category }}');
+            }
+        },
+        'json');
+    return false;
+},
+{{ $category }}SaveComment: function() {
+    if(!this.comment) return;
+    let comment = {
+        pro_id: this.own_pro_id,
+        created_at: new Date().toLocaleString(),
+        message: this.comment
+    };
+    if(!this.{{ $category }}PopupItem.data.comments) {
+        this.{{ $category }}PopupItem.data.comments = [];
+    }
+    this.{{ $category }}PopupItem.data.comments.push(comment);
+    this.{{ $category }}AutoSave();
+},

+ 98 - 0
resources/views/app/patient/tickets/pharmacy-suggest.blade.php

@@ -0,0 +1,98 @@
+initPharmacySearch: function () {
+    let self = this;
+    const debounce = (func, wait) => {
+        let timeout;
+        return function executedFunction(...args) {
+            const later = () => {
+                clearTimeout(timeout);
+                func(...args);
+            };
+            clearTimeout(timeout);
+            timeout = setTimeout(later, wait);
+        };
+    };
+    var lastTerm = '';
+    var returnedFunction = debounce(function () {
+        var term = $('#pharmacy-search').val();
+        if (!!term && lastTerm !== term) {
+            $.get('/pharmacy-suggest?term=' + $.trim(term), function (_data) {
+                $('.suggestions-outer.pharmacy-suggestions').html(_data).removeClass('d-none');
+            });
+            lastTerm = term;
+        } else {
+            $('.suggestions-outer.pharmacy-suggestions').addClass('d-none');
+        }
+    }, 250);
+    $('#pharmacy-search')
+        .on('keydown', function (e) {
+            var term = $.trim($('#pharmacy-search').val());
+            var activeItem = $('.suggestions-outer.pharmacy-suggestions .suggest-item.active');
+            switch (e.which) {
+                case 27:
+                    $('.suggestions-outer.pharmacy-suggestions').addClass('d-none');
+                    return false;
+                case 38:
+                    if (activeItem.prev().length) {
+                        activeItem.prev()
+                            .addClass('active')
+                            .siblings().removeClass('active');
+                        activeItem = $('.suggestions-outer.pharmacy-suggestions .suggest-item.active');
+                        if (activeItem.length) {
+                            activeItem[0].scrollIntoView();
+                        }
+                    }
+                    return false;
+                case 40:
+                    if (activeItem.next().length) {
+                        activeItem.next()
+                            .addClass('active')
+                            .siblings().removeClass('active');
+                        activeItem = $('.suggestions-outer.pharmacy-suggestions .suggest-item.active');
+                        if (activeItem.length) {
+                            activeItem[0].scrollIntoView();
+                        }
+                    }
+                    return false;
+                case 13:
+                    if (activeItem.length) {
+                        activeItem.first().click();
+                    }
+                    return false;
+                default:
+                    if (!!term) {
+                        $('.suggestions-outer.pharmacy-suggestions')
+                            .html('<span class="d-block no-suggest-items">Searching...</span>')
+                            .removeClass('d-none');
+                        returnedFunction();
+                    } else {
+                        $('.suggestions-outer.pharmacy-suggestions').addClass('d-none');
+                    }
+                    break;
+            }
+        })
+        .on('keypress', function (e) {
+            var term = $.trim($('#pharmacy-search').val());
+            if (!!term) {
+                $('.suggestions-outer.pharmacy-suggestions')
+                    .html('<span class="d-block no-suggest-items">Searching...</span>')
+                    .removeClass('d-none');
+                returnedFunction();
+            } else {
+                $('.suggestions-outer.pharmacy-suggestions').addClass('d-none');
+            }
+        });
+    $(document).on('click', '.suggest-item.pharmacy-suggest[data-target-uid]', function () {
+        $('#pharmacy-search').val('');
+        $('.suggestions-outer.pharmacy-suggestions').addClass('d-none');
+        self.applySuggestion(this);
+        return false;
+    });
+},
+applySuggestion: function(_elem) {
+    this.erxPopupItem.data.pharmacyName = $(_elem).attr('data-pharmacyName');
+    this.erxPopupItem.data.pharmacyCity = $(_elem).attr('data-pharmacyCity');
+    this.erxPopupItem.data.pharmacyState = $(_elem).attr('data-pharmacyState');
+    this.erxPopupItem.data.pharmacyAddressMemo = $(_elem).attr('data-pharmacyAddressMemo');
+    this.erxPopupItem.data.pharmacyPhone = $(_elem).attr('data-pharmacyPhone');
+    this.erxPopupItem.data.pharmacyFax = $(_elem).attr('data-pharmacyFax');
+},

+ 3 - 0
resources/views/app/patient/tickets/ticket_action_links.blade.php

@@ -0,0 +1,3 @@
+<a class="btn btn-sm btn-primary text-white font-weight-bold mr-2 c-pointer" v-if="!item.is_entry_error" v-on:click.prevent="setIsEntryErrorToTrue(item)">Mark As Entry Error</a>
+<a class="btn btn-sm btn-primary text-white font-weight-bold mr-2 c-pointer" v-if="item.is_entry_error" v-on:click.prevent="setIsEntryErrorToFalse(item)">Undo Mark As Entry Error</a>
+

+ 22 - 0
resources/views/app/patient/tickets/ticket_update_pro_form.blade.php

@@ -0,0 +1,22 @@
+<div class="stag-popup stag-popup-sm mcp-theme-1" stag-popup-key="{{$ticketType}}-pro-update-popup">
+    <form method="POST" class="overflow-visible">
+        <h3 class="stag-popup-title mb-2">
+            <span>Update @{{ proToUpdate}} Pro</span>
+            <a href="#" class="ml-auto text-secondary"
+                onclick="return closeStagPopup()"><i class="fa fa-times-circle"></i></a>
+        </h3>
+        <div class="form-group mb-2">
+            <label class="text-sm text-secondary mb-1">@{{proToUpdate}} Pro</label>
+            <select v-model="newProUid" class="form-control" provider-search>
+                <option value="">-- select ---</option>
+                @foreach($pros as $pro)
+                <option value="{{$pro->uid}}">{{$pro->name_first}} {{$pro->name_last}}</option>
+                @endforeach
+            </select>
+        </div>
+        <div class="d-flex align-items-center justify-content-center mt-3">
+            <button type="button" class="btn btn-sm btn-primary mr-2" v-on:click.prevent="updateTicketPro()">Submit</button>
+            <button type="button" class="btn btn-sm btn-default border" onclick="return closeStagPopup()">Cancel</button>
+        </div>
+    </form>
+</div>

+ 84 - 0
resources/views/app/patient/tickets/ticket_vue_collab_card.blade.php

@@ -0,0 +1,84 @@
+<div class="card bg-light">
+    <div class="card-body">
+        <span class="d-flex align-items-center mb-2" v-if="item.assigned_pro_id">
+            <span class="text-secondary text-sm width-70px">Assigned:</span>
+            <b>@{{proNameFromId(item.assigned_pro_id)}}</b>
+            <span class="d-inline-flex" v-if="item.manager_pro_id === {{$pro->id}}">
+                <span class="mx-2 opacity-60 text-secondary">•</span>
+                <a class="on-hover-opaque c-pointer" v-on:click.prevent="showProUpdatePopup('other-popup', 'Assigned', item)">
+                    <i class="fa fa-edit"></i>
+                </a>
+            </span>
+            <span class="d-inline-flex" v-if="item.assigned_pro_id === {{$pro->id}}">
+                <span class="mx-2 opacity-60 text-secondary">•</span>
+                <span v-if="item.has_assigned_pro_signed" class="text-success">
+                    <i class="fa fa-check"></i>
+                    Signed
+                    <a class="ml-2 c-pointer" v-on:click.prevent="undoSignAsAssignedPro(item)">Undo</a>
+                </span>
+                <a v-if="!item.has_assigned_pro_signed" class="c-pointer"
+                   v-on:click.prevent="signAsAssignedPro(item)">Sign</a>
+            </span>
+        </span>
+        <span class="d-flex align-items-center mb-2" v-if="item.manager_pro_id">
+            <span class="text-secondary text-sm width-70px">Manager:</span>
+            <b>@{{proNameFromId(item.manager_pro_id)}}</b>
+            <span class="d-inline-flex" v-if="item.manager_pro_id === {{$pro->id}}">
+                <span class="mx-2 opacity-60 text-secondary">•</span>
+                <a class="on-hover-opaque c-pointer" v-on:click.prevent="showProUpdatePopup('other-popup', 'Manager', item)">
+                    <i class="fa fa-edit"></i>
+                </a>
+            </span>
+            <span class="d-inline-flex" v-if="item.manager_pro_id === {{$pro->id}}">
+                <span class="mx-2 opacity-60 text-secondary">•</span>
+                <span v-if="item.has_manager_pro_signed" class="text-success">
+                    <i class="fa fa-check"></i>
+                    Signed
+                    <a class="ml-2 c-pointer" v-on:click.prevent="undoSignAsManagerPro(item)">Undo</a>
+                </span>
+                <a v-if="!item.has_manager_pro_signed" class="c-pointer"
+                   v-on:click.prevent="signAsManagerPro(item)">Sign</a>
+            </span>
+        </span>
+        <span class="d-flex align-items-center mb-2" v-if="item.ordering_pro_id">
+            <span class="text-secondary text-sm width-70px">Ordering:</span>
+            <b>@{{proNameFromId(item.ordering_pro_id)}}</b>
+            <span class="d-inline-flex" v-if="item.manager_pro_id === {{$pro->id}}">
+                <span class="mx-2 opacity-60 text-secondary">•</span>
+                <a class="on-hover-opaque c-pointer" v-on:click.prevent="showProUpdatePopup('other-popup', 'Ordering', item)">
+                    <i class="fa fa-edit"></i>
+                </a>
+            </span>
+            <span class="d-inline-flex" v-if="item.ordering_pro_id === {{$pro->id}}">
+                <span class="mx-2 opacity-60 text-secondary">•</span>
+                <span v-if="item.has_ordering_pro_signed" class="text-success">
+                    <i class="fa fa-check"></i>
+                    Signed
+                    <a class="ml-2 c-pointer" v-on:click.prevent="undoSignAsOrderingPro(item)">Undo</a>
+                </span>
+                <a v-if="!item.has_ordering_pro_signed" class="c-pointer"
+                   v-on:click.prevent="signAsOrderingPro(item)">Sign</a>
+            </span>
+        </span>
+        <span class="d-flex align-items-center" v-if="item.initiating_pro_id">
+            <span class="text-secondary text-sm width-70px">Initiating:</span>
+            <b>@{{proNameFromId(item.initiating_pro_id)}}</b>
+            <span class="d-inline-flex" v-if="item.manager_pro_id === {{$pro->id}}">
+                <span class="mx-2 opacity-60 text-secondary">•</span>
+                <a class="on-hover-opaque c-pointer" v-on:click.prevent="showProUpdatePopup('other-popup', 'Initiating', item)">
+                    <i class="fa fa-edit"></i>
+                </a>
+            </span>
+            <span class="d-inline-flex" v-if="item.initiating_pro_id === {{$pro->id}}">
+                <span class="mx-2 opacity-60 text-secondary">•</span>
+                <span v-if="item.has_initiating_pro_signed" class="text-success">
+                    <i class="fa fa-check"></i>
+                    Signed
+                    <a class="ml-2 c-pointer" v-on:click.prevent="undoSignAsInitiatingPro(item)">Undo</a>
+                </span>
+                <a v-if="!item.has_initiating_pro_signed" class="c-pointer"
+                   v-on:click.prevent="signAsInitiatingPro(item)">Sign</a>
+            </span>
+        </span>
+    </div>
+</div>

+ 82 - 0
resources/views/app/patient/tickets/ticket_vue_methods.blade.php

@@ -0,0 +1,82 @@
+
+setIsEntryErrorToTrue: function(_item){
+    showMask();
+    $.post('/api/ticket/setIsEntryErrorToTrue', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();
+    });
+},
+
+setIsEntryErrorToFalse: function(_item){
+    showMask();
+    $.post('/api/ticket/setIsEntryErrorToFalse', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();
+    })
+},
+
+signAsAssignedPro: function(_item){
+    showMask();
+    $.post('/api/ticket/signAsAssignedPro', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();
+    })
+},
+
+undoSignAsAssignedPro: function(_item){
+    showMask();
+    $.post('/api/ticket/undoSignAsAssignedPro', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();
+    })
+},
+
+signAsManagerPro: function(_item){
+    showMask();
+    $.post('/api/ticket/signAsManagerPro', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();
+    })
+},
+
+undoSignAsManagerPro: function(_item){
+    showMask();
+    $.post('/api/ticket/undoSignAsManagerPro', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();
+    })
+},
+
+signAsInitiatingPro: function(_item){
+    showMask();
+    $.post('/api/ticket/signAsInitiatingPro', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();
+    })
+},
+
+undoSignAsInitiatingPro: function(_item){
+    showMask();
+    $.post('/api/ticket/undoSignAsInitiatingPro', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();
+    })
+},
+
+hasError: function(_data) {     // check and report error if exists via toastr
+    let msg = 'Unknown error!';
+    if(_data) {
+        if(_data.success) return false;
+        else if(_data.message) msg = _data.message;
+    }
+    toastr.error(msg);
+    return true;
+}

+ 208 - 0
resources/views/app/patient/vitals-graph.blade.php

@@ -0,0 +1,208 @@
+@extends ('layouts.patient')
+@section('inner-content')
+
+    <link href="/c3/c3.min.css" rel="stylesheet">
+    <script src="/c3/d3.v5.min.js" charset="utf-8"></script>
+    <script src="/c3/c3.min.js"></script>
+
+    <div class="d-flex align-items-center">
+        <h4 class="font-weight-bold mb-0 text-secondary font-size-16">Vitals Graph</h4>
+        <?php
+        $months = [];
+        $monthNames = [];
+        for ($i=6; $i>=0; $i--) {
+            $m = date_sub(date_create(), date_interval_create_from_date_string($i . " month" . ($i === 1 ? '' : 's')));
+            $monthNames[] = date_format($m, "M, Y");
+            $months[] = date_format($m, "Y-m-01");
+        }
+        $current = $filter ? $filter : $months[count($months) - 1];
+        ?>
+        <select class="ml-auto max-width-200px form-control form-control-sm"
+                onchange="fastLoad('/patients/view/{{$patient->uid}}/vitals-graph/' + this.value, true, false, false)">
+            <?php for ($i=count($months) - 1; $i>=0; $i--): ?>
+            <option value="{{$months[$i]}}" {{ $current === $months[$i] ? 'selected' : '' }}>{{$monthNames[$i]}}</option>
+            <?php endfor; ?>
+        </select>
+    </div>
+
+    <hr class="m-neg-4">
+
+    <div id="vitalsGraphComponent" class="stag-chart">
+        <h4 class="font-weight-bold mb-3 text-secondary font-size-14 text-center">Blood Pressure</h4>
+        <div id="bp-chart" class="mb-4">BP Graph</div>
+        <hr class="m-neg-4">
+        <h4 class="font-weight-bold mb-3 text-secondary font-size-14 text-center">Weight</h4>
+        <div id="weight-chart">Weight Graph</div>
+    </div>
+
+    <?php
+    $dates = [];
+    $startDate = $current;
+    $nextMonthFirstDay = date_format(date_add(date_create($startDate), date_interval_create_from_date_string("1 month")), 'Y-m-d');
+
+    $nextDay = $startDate;
+    while ($nextDay !== $nextMonthFirstDay) {
+        $dates[] = $nextDay;
+        $nextDay = date_format(date_add(date_create($nextDay), date_interval_create_from_date_string('1 day')), 'Y-m-d');
+    }
+
+    $allMeasurements = $patient->measurements->toArray();
+
+    $bpData = [];
+    $weightData = [];
+
+    for ($i=0; $i<count($dates); $i++) {
+
+        $date = $dates[$i];
+
+        // sbp
+        $sbp = array_filter($allMeasurements, function($_measurement) use ($date) {
+            return $_measurement['label'] === 'SBP' && $_measurement['effective_date'] === $date;
+        });
+        if(count($sbp)) {
+            $sbp = array_values($sbp);
+            $sbp = $sbp[count($sbp) - 1];
+        }
+        else {
+            $sbp = null;
+        }
+        // dbp
+        $dbp = null;
+        if($sbp) {
+            $dbp = array_filter($allMeasurements, function($_measurement) use ($date) {
+                $measurementDate = date('Y-m-d', strtotime($_measurement['effective_date']));
+                return $_measurement['label'] === 'DBP' && $measurementDate === $date;
+            });
+            if(count($dbp)) {
+                $dbp = array_values($dbp);
+                $dbp = $dbp[count($dbp) - 1];
+            }
+            else {
+                $dbp = null;
+            }
+        }
+        if ($sbp && $dbp) {
+            $bpData[] = [
+                "date" => $date,
+                "sbp" => $sbp["numeric_value"],
+                "dbp" => $dbp["numeric_value"]
+            ];
+        }
+
+        // weight
+        $weight = array_filter($allMeasurements, function($_measurement) use ($date) {
+            return $_measurement['label'] === 'WEIGHT' && $_measurement['effective_date'] === $date;
+        });
+        if(count($weight)) {
+            $weight = array_values($weight);
+            $weight = $weight[count($weight) - 1];
+            $weightData[] = [
+                "date" => $date,
+                "weight" => $weight["numeric_value"]
+            ];
+        }
+
+    }
+
+    $bpDates = [];
+    $sbpValues = [];
+    $dbpValues = [];
+    for ($i = 0; $i < count($bpData); $i++) {
+        $bpDates[] = $bpData[$i]['date'];
+        $sbpValues[] = $bpData[$i]['sbp'];
+        $dbpValues[] = $bpData[$i]['dbp'];
+    }
+
+    $weightDates = [];
+    $weightValues = [];
+    for ($i = 0; $i < count($weightData); $i++) {
+        $weightDates[] = $weightData[$i]['date'];
+        $weightValues[] = $weightData[$i]['weight'];
+    }
+
+    ?>
+
+    <script>
+        (function() {
+            function init() {
+                bpChart();
+                weightChart();
+            }
+            function bpChart() {
+                var chart = c3.generate({
+                    bindto: '#bp-chart',
+                    data: {
+                        x: 'x',
+                        // xFormat: '%Y%m%d', // 'xFormat' can be used as custom format of 'x'
+                        columns: [
+                            ['x', <?= implode(", ", array_map(function($_x) { return "'" . $_x . "'"; }, $bpDates)) ?>],
+                            ['Systolic BP', <?= implode(", ", array_map(function($_x) { return "'" . $_x . "'"; }, $sbpValues)) ?>],
+                            ['Diastolic BP', <?= implode(", ", array_map(function($_x) { return "'" . $_x . "'"; }, $dbpValues)) ?>]
+                        ]
+                    },
+                    axis: {
+                        x: {
+                            type: 'timeseries',
+                            tick: {
+                                format: '%Y-%m-%d',
+                                multiline: true,
+                                fit: true,
+                                rotate: -45
+                            },
+                        },
+                        y: {
+                            show: true,
+                            label: {
+                                text: 'Blood Pressure (mmHg)',
+                                position: 'outer-middle'
+                            },
+                            min: 60,
+                            max: 220
+                        },
+                    },
+                    regions: [
+                        {axis: 'y', start: 100, end: 145, class: 'safe-region', label: 'Safe Systolic BP: 100 to 145 mmHg'},
+                        {axis: 'y', start: 70, end: 90, class: 'safe-region', label: 'Safe Diastolic BP: 70 to 90 mmHg'}
+                    ]
+                });
+            }
+            function weightChart() {
+                var chart = c3.generate({
+                    bindto: '#weight-chart',
+                    data: {
+                        x: 'x',
+                        columns: [
+                            ['x', <?= implode(", ", array_map(function($_x) { return "'" . $_x . "'"; }, $weightDates)) ?>],
+                            ['Weight', <?= implode(", ", array_map(function($_x) { return "'" . $_x . "'"; }, $weightValues)) ?>]
+                        ]
+                    },
+                    axis: {
+                        x: {
+                            type: 'timeseries',
+                            tick: {
+                                format: '%Y-%m-%d',
+                                multiline: true,
+                                fit: true,
+                                rotate: -45
+                            },
+                        },
+                        y: {
+                            show: true,
+                            label: {
+                                text: 'Weight (lbs)',
+                                position: 'outer-middle'
+                            },
+                            min: 70,
+                            max: 250
+                        },
+                    },
+                    regions: [
+                        {axis: 'y', start: 100, end: 140, class: 'safe-region', label: 'Safe Weight: 100 to 140 lbs'},
+                    ]
+                });
+            }
+            addMCInitializer('vitalsGraph', init, '#vitalsGraphComponent');
+        }).call(window);
+    </script>
+
+@endsection

+ 35 - 86
resources/views/app/practice-management/calendar.blade.php

@@ -100,6 +100,28 @@
                 <select id="eventPros" name="proUid" xprovider-search
                         class="form-control form-control-sm flex-grow-1" multiple
                         v-model="proIds">
+                    <?php
+                    $proIndex = 0;
+                    $proMeta = [];
+                    ?>
+                    @foreach($pros as $iPro)
+                        <option value="{{$iPro->id}}"
+                                data-bc="{{$palette[$proIndex]["bc"]}}"
+                                data-fc="{{$palette[$proIndex]["fc"]}}"
+                                data-initials="{{$iPro->initials()}}">
+                            {{$iPro->displayName()}}
+                        </option>
+                        <?php
+                        $proMeta[$iPro->uid] = [
+                            "bc" => $palette[$proIndex]["bc"],
+                            "fc" => $palette[$proIndex]["fc"],
+                            "ac" => $palette[$proIndex]["ac"],
+                            "initials" => $iPro->initials()
+                        ];
+                        $proIndex++;
+                        if($proIndex >= count($palette)) $proIndex = 0;
+                        ?>
+                    @endforeach
                 </select>
             </form>
             @else
@@ -316,16 +338,13 @@
     <script>
         (function() {
 
-            var palette = {!! json_encode($palette) !!};
-            var lastUsedColor = -1;
-
             function init() {
                 window.proCalendarApp = new Vue({
                     el: '#proCalendarApp',
                     data: {
                         eventTypes: 'BOTH',
                         calendar: null,
-                        proMeta: {},
+                        proMeta: {!! json_encode($proMeta) !!},
                         proIds: ['{{ $pro->id }}'],
                         timezone: 'EASTERN',
                         today: new Date('{{ date('Y-m-d 00:00:00') }}'),
@@ -392,62 +411,32 @@
                                 .select2({
                                     closeOnSelect: false,
 
-                                    ajax: {
-                                        url: "/pro-suggest/json",
-                                        type: "get",
-                                        dataType: 'json',
-                                        delay: 250,
-                                        data: function (params) {
-                                            return {
-                                                term: params.term // search term
-                                            };
-                                        },
-                                        processResults: function (response) {
-                                            return {
-                                                results: response
-                                            };
-                                        },
-                                        cache: true
-                                    },
-
                                     // dropdown options
                                     templateResult: function(_state) {
-                                        if(!_state.id) {
+                                        let element = _state.element;
+                                        if(!element || !element.value) {
                                             return $('<span class="mcp-theme-1"><span>' + _state.text + '</span></span>');
                                         }
-
-                                        if(!self.proMeta[_state.uid]) {
-                                            lastUsedColor++;
-                                            if(lastUsedColor > 23) lastUsedColor = 0;
-                                            _state.ag = palette[lastUsedColor].ac;
-                                            _state.bg = palette[lastUsedColor].bc;
-                                            _state.fg = palette[lastUsedColor].fc;
-                                            self.proMeta[_state.uid] = {
-                                                ac: _state.ag,
-                                                bc: _state.bg,
-                                                fc: _state.fg,
-                                                initials: _state.initials
-                                            };
-                                        }
-
+                                        element = $(element);
                                         return $('<span class="mcp-theme-1 pro-option" ' +
-                                            'data-initials="' + _state.initials + '" ' +
-                                            'data-bc="' + _state.bg + '" ' +
-                                            'data-fc="' + _state.fg + '"><span>' +
+                                            'data-initials="' + element.attr('data-initials') + '" ' +
+                                            'data-bc="' + element.attr('data-bc') + '" ' +
+                                            'data-fc="' + element.attr('data-fc') + '"><span>' +
                                             '<span class="pro-option-initials" ' +
-                                            'style="background: ' + _state.bg + '; color: ' + _state.fg + '">' +
-                                            _state.initials + '</span>' +
+                                            'style="background: ' + element.attr('data-bc') + '; color: ' + element.attr('data-fc') + '">' +
+                                            element.attr('data-initials') + '</span>' +
                                             _state.text +
                                             '</span></span>');
                                     },
 
                                     // selected items
                                     templateSelection: function(_state) {
-                                        if(!_state.id) {
+                                        let element = _state.element;
+                                        if(!element || !element.value) {
                                             return $('<span class="mcp-theme-1"><span>' + _state.text + '</span></span>');
                                         }
-                                        return $('<span class="pro-selection" ' +
-                                            'style="background: ' + _state.bg + '; color: ' + _state.fg + '">' +
+                                        element = $(element);
+                                        return $('<span class="pro-selection" style="background: ' + element.attr('data-bc') + '; color: ' + element.attr('data-fc') + '">' +
                                             _state.text + '</span>');
                                     }
                                 })
@@ -461,26 +450,6 @@
                             $('#addApptPro')
                                 .select2({
                                     width: '100%',
-                                    ajax: {
-                                        url: "/pro-suggest/json",
-                                        type: "get",
-                                        dataType: 'json',
-                                        delay: 250,
-                                        data: function (params) {
-                                            return {
-                                                term: params.term // search term
-                                            };
-                                        },
-                                        processResults: function (response) {
-                                            for (let i = 0; i < response.length; i++) {
-                                                response[i].id = response[i].uid;
-                                            }
-                                            return {
-                                                results: response
-                                            };
-                                        },
-                                        cache: true
-                                    },
                                     templateResult: function(_state) {
                                         return $('<span class="mcp-theme-1"><span>' + _state.text + '</span></span>');
                                     }
@@ -491,26 +460,6 @@
                             $('#editApptPro')
                                 .select2({
                                     width: '100%',
-                                    ajax: {
-                                        url: "/pro-suggest/json",
-                                        type: "get",
-                                        dataType: 'json',
-                                        delay: 250,
-                                        data: function (params) {
-                                            return {
-                                                term: params.term // search term
-                                            };
-                                        },
-                                        processResults: function (response) {
-                                            for (let i = 0; i < response.length; i++) {
-                                                response[i].id = response[i].uid;
-                                            }
-                                            return {
-                                                results: response
-                                            };
-                                        },
-                                        cache: true
-                                    },
                                     templateResult: function(_state) {
                                         return $('<span class="mcp-theme-1"><span>' + _state.text + '</span></span>');
                                     }

+ 15 - 9
resources/views/layouts/patient.blade.php

@@ -25,6 +25,10 @@
                             <a class="nav-link {{ strpos($routeName, 'patients.view.flowsheets') === 0 ? 'active' : '' }}"
                                href="{{ route('patients.view.flowsheets', ['patient' => $patient]) }}">Flowsheets</a>
                         </li>
+                        <li class="nav-item">
+                            <a class="nav-link {{ strpos($routeName, 'patients.view.vitals-graph') === 0 ? 'active' : '' }}"
+                               href="{{ route('patients.view.vitals-graph', ['patient' => $patient]) }}">Vitals Graph</a>
+                        </li>
                         <li class="nav-item">
                             <a class="nav-link {{ strpos($routeName, 'patients.view.care-months') === 0 ? 'active' : '' }}"
                                href="{{ route('patients.view.care-months', ['patient' => $patient]) }}">Care Months</a>
@@ -49,12 +53,10 @@
                             <a class="nav-link {{ strpos($routeName, 'patients.view.handouts') === 0 ? 'active' : '' }}"
                                href="{{ route('patients.view.handouts', ['patient' => $patient]) }}">Handouts</a>
                         </li>
-                        <li class="nav-item">
+                        <?php /* <li class="nav-item">
                             <a class="nav-link d-flex align-items-center {{ strpos($routeName, 'patients.view.action-items') === 0 ? 'active' : '' }}"
                                native onclick="return false">
                                 <span class="text-dark">ERx/Orders</span>
-                                {{--<i class="fa if-collapsed fa-chevron-down text-secondary ml-auto mr-1"></i>
-                                <i class="fa if-not-collapsed fa-chevron-up text-secondary ml-auto mr-1"></i>--}}
                             </a>
                             <ul class="m-0 p-0 nav-child-list">
                                 <li class="nav-item">
@@ -73,11 +75,15 @@
                                     <a class="nav-link {{ strpos($routeName, 'patients.view.action-items-equipment') === 0 ? 'active' : '' }}"
                                        href="{{ route('patients.view.action-items-equipment', ['patient' => $patient]) }}">Equipment</a>
                                 </li>
-                                {{--<li class="nav-item">
-                                    <a class="nav-link {{ strpos($routeName, 'patients.view.action-items-other') === 0 ? 'active' : '' }}"
-                                       href="{{ route('patients.view.action-items-other', ['patient' => $patient]) }}">Other</a>
-                                </li>--}}
                             </ul>
+                        </li> */ ?>
+                        <li class="nav-item">
+                            <a class="nav-link {{ strpos($routeName, 'patients.view.tickets') === 0 ? 'active' : '' }}"
+                               href="{{ route('patients.view.patient-tickets', ['patient' => $patient]) }}">Tickets</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link {{ strpos($routeName, 'patients.view.incoming-reports') === 0 ? 'active' : '' }}"
+                               href="{{ route('patients.view.incoming-reports', ['patient' => $patient]) }}">Incoming Reports</a>
                         </li>
                         <li class="nav-item">
                             <a class="nav-link {{ strpos($routeName, 'patients.view.allergies') === 0 ? 'active' : '' }}"
@@ -290,7 +296,7 @@
                                                                 <input type="hidden" name="uid" value="{{$patient->uid}}">
                                                                 <div class="mb-2">
                                                                     <label class="text-secondary text-sm">Remove MCP Pro</label>
-                                                                    
+
                                                                 </div>
                                                                 <div>
                                                                     <button submit class="btn btn-sm btn-primary mr-1">Submit
@@ -404,7 +410,7 @@
                                                         <input type="hidden" name="uid" value="{{$patient->uid}}">
                                                         <div class="mb-2">
                                                             <label class="text-secondary text-sm">Remove Physician Pro</label>
-                                                            
+
                                                         </div>
                                                         <div>
                                                             <button submit class="btn btn-sm btn-primary mr-1">Submit

+ 14 - 12
resources/views/layouts/template.blade.php

@@ -55,7 +55,7 @@
     <script src='/js/autocomplete-lhc.js'></script>
 
     {{-- inline bootstrap datepicker --}}
-    <link href='/bootstrap-datepicker/css/bootstrap-datepicker.min.css' rel="stylesheet">
+    <link href='/bootstrap-datepicker/css/bootstrap-datepicker.standalone.min.css' rel="stylesheet">
     <script src='/bootstrap-datepicker/js/bootstrap-datepicker.min.js'></script>
 
     {{-- pdfjs --}}
@@ -312,24 +312,26 @@
                 })
             );
 
-            window.socketClient.subscribe("/user/topic/newMcpRequest", function(message) {
-                console.log("Received new mcp request: ", message);
-                //TODO: Update the mcp queue on the DOM
-            }); // once in 15 seconds
-
-            window.socketClient.subscribe("/user/topic/removeMcpRequest", function(message) {
-                console.log("An mcp request has been removed: ", message);
-                //TODO: Update the mcp queue on the DOM
-            });
-
             window.socketClient.subscribe("/user/topic/myCurrentProClientWork", function(message) {
                 console.log("You have a client pro work: ", message);
                 $.get('/current-work', function(_data) {
                     $('.current-work').html(_data);
-                    initFastLoad($('.current-work'));
+                    //initFastLoad($('.current-work'));
+                    fastReload();
                 });
             });
 
+            window.socketClient.subscribe("/user/topic/killMyCurrentProClientWork", function(message) {
+                console.log("You have killed your current pro client work: ", message);
+                $.get('/current-work', function(_data) {
+                    $('.current-work').html(_data);
+                    //initFastLoad($('.current-work'));
+                    fastReload();
+                });
+            });
+
+            
+
         });
     </script>
 </body>

+ 11 - 2
routes/web.php

@@ -113,7 +113,6 @@ Route::middleware('pro.auth')->group(function () {
         Route::get('memos', 'PatientController@memos')->name('memos');
         Route::get('sms', 'PatientController@sms')->name('sms');
         Route::get('sms-numbers', 'PatientController@smsNumbers')->name('sms-numbers');
-        Route::get('documents', 'PatientController@documents')->name('documents');
         Route::get('immunizations', 'PatientController@immunizations')->name('immunizations');
         Route::get('allergies', 'PatientController@allergies')->name('allergies');
         Route::get('action-items', 'PatientController@actionItems')->name('action-items');
@@ -138,6 +137,7 @@ Route::middleware('pro.auth')->group(function () {
         Route::get('account', 'PatientController@account')->name('account');
         Route::get('care-checklist', 'PatientController@careChecklist')->name('care-checklist');
         Route::get('documents', 'PatientController@documents')->name('documents');
+        Route::get('incoming-reports', 'PatientController@incomingReports')->name('incoming-reports');
         Route::get('education', 'PatientController@education')->name('education');
         Route::get('messaging', 'PatientController@messaging')->name('messaging');
         Route::get('duplicate', 'PatientController@duplicate')->name('duplicate');
@@ -154,6 +154,12 @@ Route::middleware('pro.auth')->group(function () {
 
         // flowsheets
         Route::get('flowsheets/{filter?}', 'PatientController@flowsheets')->name('flowsheets');
+
+        // vitals-graph
+        Route::get('vitals-graph/{filter?}', 'PatientController@vitalsGraph')->name('vitals-graph');
+
+        // tickets
+        Route::get('tickets', 'PatientController@tickets')->name('patient-tickets');
     });
 
     // pro dashboard events (ajax)
@@ -180,7 +186,7 @@ Route::middleware('pro.auth')->group(function () {
     Route::get('/pharmacy-suggest', 'HomeController@pharmacySuggest');
 
     // Pro suggest
-    Route::get('/pro-suggest/{format?}', 'HomeController@proSuggest');
+    Route::get('/pro-suggest', 'HomeController@proSuggest');
     Route::get('/pro-display-name/{pro}', 'HomeController@proDisplayName');
 
     // embeddable sections
@@ -189,6 +195,9 @@ Route::middleware('pro.auth')->group(function () {
     // AJAX presence poll
     Route::get('/patients/{patient}/presence', 'PatientController@presence');
 
+    // refresh single ticket
+    Route::get('/get-ticket/{ticket}', 'PatientController@getTicket');
+
     // 2-pane outer page housing lhs (practice management) and rhs (video call)
     Route::get('/mc/{fragment?}', 'HomeController@mc')
         ->where('fragment', '.*')

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.