Переглянути джерело

Merge branch 'dev-vj-agora-v2' of rav.triplestart.com:jmudaka/stagfe2 into dev-josh

Josh 4 роки тому
батько
коміт
ab62ead2d2
62 змінених файлів з 5115 додано та 763 видалено
  1. 14 0
      app/Helpers/helpers.php
  2. 1 0
      app/Http/Controllers/AppointmentController.php
  3. 36 22
      app/Http/Controllers/HomeController.php
  4. 6 1
      app/Http/Controllers/NoteController.php
  5. 8 1
      app/Http/Controllers/PatientController.php
  6. 40 1
      app/Http/Controllers/PracticeManagementController.php
  7. 22 0
      app/Models/Client.php
  8. 26 0
      app/Models/ClientProgram.php
  9. 11 0
      app/Models/ClientProgramMonth.php
  10. 10 0
      app/Models/ClientProgramMonthEntry.php
  11. 34 0
      app/Models/Measurement.php
  12. 56 3
      app/Models/Pro.php
  13. 16 0
      app/Models/ProFavorite.php
  14. 11 0
      app/Models/Ticket.php
  15. 5 0
      config/app.php
  16. 29 6
      public/css/meeting.css
  17. 53 1
      public/css/style.css
  18. 1 1
      public/js/mc.js
  19. 13 0
      public/js/option-list.js
  20. 51 0
      public/js/stag-popup.js
  21. 4 4
      resources/views/app/dashboard.blade.php
  22. 17 341
      resources/views/app/patient/action-items.blade.php
  23. 7 3
      resources/views/app/patient/appointment-calendar.blade.php
  24. 164 3
      resources/views/app/patient/canvas-sections/allergies/form.blade.php
  25. 9 2
      resources/views/app/patient/canvas-sections/allergies/summary.php
  26. 175 5
      resources/views/app/patient/canvas-sections/dx/form.blade.php
  27. 24 23
      resources/views/app/patient/canvas-sections/dx/summary.php
  28. 5 2
      resources/views/app/patient/canvas-sections/fhx/summary.php
  29. 171 10
      resources/views/app/patient/canvas-sections/rx/form.blade.php
  30. 9 2
      resources/views/app/patient/canvas-sections/rx/summary.php
  31. 91 21
      resources/views/app/patient/canvas-sections/vitals/form.blade.php
  32. 42 14
      resources/views/app/patient/canvas-sections/vitals/summary.php
  33. 7 155
      resources/views/app/patient/dashboard.blade.php
  34. 1 1
      resources/views/app/patient/memos.blade.php
  35. 22 21
      resources/views/app/patient/note/_cancel-signed-note.blade.php
  36. 4 2
      resources/views/app/patient/note/dashboard.blade.php
  37. 1 0
      resources/views/app/patient/note/dashboard_script.blade.php
  38. 1 1
      resources/views/app/patient/notes.blade.php
  39. 77 0
      resources/views/app/patient/partials/device-measurements.blade.php
  40. 174 0
      resources/views/app/patient/partials/equipment.blade.php
  41. 332 0
      resources/views/app/patient/partials/erx.blade.php
  42. 216 0
      resources/views/app/patient/partials/imaging.blade.php
  43. 219 0
      resources/views/app/patient/partials/lab.blade.php
  44. 111 0
      resources/views/app/patient/partials/measurement.blade.php
  45. 88 0
      resources/views/app/patient/partials/measurements.blade.php
  46. 160 0
      resources/views/app/patient/partials/other.blade.php
  47. 44 0
      resources/views/app/patient/partials/programs.blade.php
  48. 2 2
      resources/views/app/patient/partials/vitals.blade.php
  49. 559 0
      resources/views/app/patient/programs.blade.php
  50. 24 0
      resources/views/app/patient/settings.blade.php
  51. 51 9
      resources/views/app/patients.blade.php
  52. 21 47
      resources/views/app/practice-management/calendar.blade.php
  53. 68 0
      resources/views/app/practice-management/my-favorites.blade.php
  54. 762 0
      resources/views/app/video/call-agora-v2.blade.php
  55. 386 0
      resources/views/app/video/call-agora.blade.php
  56. 585 0
      resources/views/app/video/call-ot.blade.php
  57. 28 1
      resources/views/layouts/patient.blade.php
  58. 4 55
      resources/views/layouts/template.blade.php
  59. 4 0
      routes/web.php
  60. 0 0
      storage/sections/allergies/deleted_actions.blade.php
  61. 2 2
      storage/sections/fhx/summary.php
  62. 1 1
      storage/sections/vitals/default.php

+ 14 - 0
app/Helpers/helpers.php

@@ -323,3 +323,17 @@ if(!function_exists('convertToTimezone')) {
         return $_returnRaw ? $date : $date->format('Y-m-d H:i:s');
     }
 }
+
+if(!function_exists('minutes_to_hhmm')) {
+    function minutes_to_hhmm($_minutes)
+    {
+        $h = intval(floor($_minutes / 60));
+        $m = $_minutes;
+        if($h > 0) {
+            $m = $_minutes - $h * 60;
+        }
+        $h = ($h < 10 ? '0' : '') . $h;
+        $m = ($m < 10 ? '0' : '') . $m;
+        return $h . ':' . $m;
+    }
+}

+ 1 - 0
app/Http/Controllers/AppointmentController.php

@@ -100,6 +100,7 @@ class AppointmentController extends Controller
                 "clientUid" => $appointment->client->uid,
                 "proId" => $appointment->pro->id,
                 "proUid" => $appointment->pro->uid,
+                "proName" => $appointment->pro->displayName(),
                 "start" => convertToTimezone($appointment->start_time, $timeZone),
                 "end" => convertToTimezone($appointment->end_time, $timeZone),
                 "clientOnly" => !in_array($appointment->pro->id, $proIds),

+ 36 - 22
app/Http/Controllers/HomeController.php

@@ -188,13 +188,23 @@ class HomeController extends Controller
 
         $keyNumbers  = [];
 
-        $totalPatients = Client::where('mcp_pro_id', $performer->pro->id)->count();
-        $keyNumbers['totalPatients'] = $totalPatients;
-
-        $patientNotSeenYet = Client::where('mcp_pro_id', $performer->pro->id)
-            ->where(function ($query) {
-                $query->where('has_mcp_done_onboarding_visit', 'UNKNOWN')
-                    ->orWhere('has_mcp_done_onboarding_visit', 'NO');
+        $queryClients = $this->performer()->pro->getAccessibleClientsQuery();
+
+        $keyNumbers['totalPatients'] = $queryClients->count();
+
+        // patientNotSeenYet
+        $patientNotSeenYet = $queryClients
+            ->where(function ($query) use ($performer) {     // own patient and primary OB visit pending
+                $query->where('mcp_pro_id', $performer->pro->id)
+                    ->where('has_mcp_done_onboarding_visit', '!=', 'YES');
+            })
+            ->orWhere(function ($query) {   // mcp of any client program and program OB pending
+                $query->where(function ($_query) {
+                    $_query->select(DB::raw('COUNT(id)'))
+                        ->from('client_program')
+                        ->whereColumn('client_id', 'client.id')
+                        ->where('has_mcp_done_onboarding_visit', '!=', 'YES');
+                }, '>=', 1);
             })->count();
         $keyNumbers['patientsNotSeenYet'] = $patientNotSeenYet;
 
@@ -314,22 +324,26 @@ class HomeController extends Controller
 
     public function patients(Request $request, $filter = '')
     {
-        $proID = $this->performer()->pro->id;
-        if ($this->performer()->pro->pro_type === 'ADMIN') {
-            $query = Client::where('id', '>', 0);
-        } else {
-            $query = Client::where(function ($q) use ($proID) {
-                $q->where('mcp_pro_id', $proID)
-                    ->orWhere('cm_pro_id', $proID)
-                    ->orWhere('rmm_pro_id', $proID)
-                    ->orWhere('rme_pro_id', $proID)
-                    ->orWhereRaw('id IN (SELECT client_id FROM client_pro_access WHERE is_active AND pro_id = ?)', [$proID])
-                    ->orWhereRaw('id IN (SELECT client_id FROM appointment WHERE pro_id = ?)', [$proID]);
-            });
-        }
+        $performer = $this->performer();
+        $query = $performer->pro->getAccessibleClientsQuery();
+
         switch ($filter) {
             case 'not-yet-seen':
-                $query = $query->where('has_mcp_done_onboarding_visit', '<>', 'YES');
+                $query = $query
+                    ->where(function ($query) use ($performer) {
+                        $query
+                            ->where(function ($query) use ($performer) {     // own patient and primary OB visit pending
+                                $query->where('mcp_pro_id', $performer->pro->id)
+                                    ->where('has_mcp_done_onboarding_visit', '<>', 'YES');
+                            })
+                            ->orWhere(function ($query) use ($performer) {   // mcp of any client program and program OB pending
+                                $query->select(DB::raw('COUNT(id)'))
+                                    ->from('client_program')
+                                    ->whereColumn('client_id', 'client.id')
+                                    ->where('mcp_pro_id', $performer->pro->id)
+                                    ->where('has_mcp_done_onboarding_visit', '<>', 'YES');
+                            }, '>=', 1);
+                    });
                 break;
 
                 // more cases can be added as needed
@@ -350,7 +364,7 @@ class HomeController extends Controller
             $q->where('name_first', 'ILIKE', '%' . $term . '%')
                 ->orWhere('name_last', 'ILIKE', '%' . $term . '%');
         });
-        
+
         if($pro->pro_type != 'ADMIN'){
             $clientQuery->whereIn('id', $pro->getMyClientIds());
         }

+ 6 - 1
app/Http/Controllers/NoteController.php

@@ -117,7 +117,12 @@ class NoteController extends Controller
             $response = $this->calljava($request, '/client/updateCanvasData', $data, $guestAccessCode);
             //TODO: handle $response->success == false
 
-            $client = Client::where('id', $note->client_id)->first();
+            if($note){
+                $client = Client::where('id', $note->client_id)->first();
+            }else{
+                $client = Client::where('id', $section->client_id)->first();
+            }
+
             $patient = $client;
             if (file_exists(resource_path("views/app/patient/canvas-sections/{$sectionInternalName}/processor.php"))) {
                 include(resource_path("views/app/patient/canvas-sections/{$sectionInternalName}/processor.php"));

+ 8 - 1
app/Http/Controllers/PatientController.php

@@ -12,6 +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\SectionTemplate;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\File;
@@ -32,7 +33,8 @@ class PatientController extends Controller
             ->where('is_removed', false)
             ->orderBy('content_text', 'asc')
             ->get();
-        return view('app.patient.dashboard', compact('patient', 'facilities', 'devices', 'dxInfoLines'));
+        return view('app.patient.dashboard',
+            compact('patient', 'facilities', 'devices', 'dxInfoLines'));
     }
 
     public function actionItems(Request $request, Client $patient )
@@ -234,4 +236,9 @@ class PatientController extends Controller
     public function calendar(Request $request, Client $patient, Appointment $currentAppointment) {
         return view('app.patient.appointment-calendar', compact('patient', 'currentAppointment'));
     }
+
+    public function programs(Request $request, Client $patient, $filter = '') {
+        $pros = $this->pros;
+        return view('app.patient.programs', compact('patient', 'pros', 'filter'));
+    }
 }

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

@@ -8,6 +8,7 @@ use App\Models\Client;
 use App\Models\McpRequest;
 use App\Models\Note;
 use App\Models\Pro;
+use App\Models\ProFavorite;
 use App\Models\ProGeneralAvailability;
 use App\Models\ProRate;
 use App\Models\ProSpecificAvailability;
@@ -147,6 +148,21 @@ class PracticeManagementController extends Controller
         return view('app.practice-management.my-text-shortcuts', compact('myTextShortcuts'));
     }
 
+    public function myFavorites(Request $request, $filter = 'all')
+    {
+        $performer = $this->performer();
+        $myFavorites = ProFavorite::where('pro_id', $performer->pro_id)
+            ->where('is_removed', false);
+        if($filter !== 'all') {
+            $myFavorites = $myFavorites->where('category', $filter);
+        }
+        $myFavorites = $myFavorites
+            ->orderBy('category', 'asc')
+            ->orderBy('position_index', 'asc')
+            ->get();
+        return view('app.practice-management.my-favorites', compact('myFavorites', 'filter'));
+    }
+
     public function proAvailability(Request $request, $proUid = null)
     {
         $performer = $this->performer();
@@ -238,7 +254,30 @@ class PracticeManagementController extends Controller
     public function meet(Request $request, $uid = false) {
         $session = AppSession::where('session_key', $request->cookie('sessionKey'))->first();
         $client = !empty($uid) ? Client::where('uid', $uid)->first() : null;
-        return view('app.video.call', compact('session', 'client'));
+        return view('app.video.call-agora-v2', compact('session', 'client'));
+    }
+
+    public function getParticipantInfo(Request $request) {
+        $sid = intval($request->get('uid')) - 1000000;
+        $session = AppSession::where('id', $sid)->first();
+        $result = [
+            "type" => '',
+            "name" => ''
+        ];
+        if($session) {
+            $result["type"] = $session->session_type;
+            switch($session->session_type) {
+                case 'PRO':
+                    $pro = Pro::where('id', $session->pro_id)->first();
+                    $result["name"] = $pro->displayName();
+                    break;
+                case 'CLIENT':
+                    $client = Client::where('id', $session->client_id)->first();
+                    $result["name"] = $client->displayName();
+                    break;
+            }
+        }
+        return json_encode($result);
     }
 
     // ajax ep used by the video page

+ 22 - 0
app/Models/Client.php

@@ -19,6 +19,10 @@ class Client extends Model
         return $this->hasOne(Pro::class, 'id', 'mcp_pro_id');
     }
 
+    public function pcp() {
+        return $this->hasOne(Pro::class, 'id', 'physician_pro_id');
+    }
+
     public function cm() {
         return $this->hasOne(Pro::class, 'id', 'cm_pro_id');
     }
@@ -87,6 +91,13 @@ class Client extends Model
             ->orderBy('label', 'asc')
             ->orderBy('effective_date', 'desc');
     }
+    public function allMeasurements() {
+        return $this->hasMany(Measurement::class, 'client_id', 'id')
+            ->where('is_removed', false)
+            ->whereNull('parent_measurement_id')
+            ->orderBy('label', 'asc')
+            ->orderBy('effective_date', 'desc');
+    }
 
     public function smses() {
         return $this->hasMany(ClientSMS::class, 'client_id', 'id')
@@ -155,4 +166,15 @@ class Client extends Model
         return $this->hasOne(McpRequest::class, 'id', 'active_mcp_request_id');
     }
 
+    public function clientPrograms() {
+        return $this->hasMany(ClientProgram::class, 'client_id', 'id')
+            ->where('is_active', true)
+            ->orderBy('title', 'desc');
+    }
+
+    public function tickets() {
+        return $this->hasMany(Ticket::class, 'client_id', 'id')
+            ->orderBy('created_at', 'desc');
+    }
+
 }

+ 26 - 0
app/Models/ClientProgram.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ClientProgram extends Model
+{
+    protected $table = "client_program";
+
+    public function mcp() {
+        return $this->hasOne(Pro::class, 'id', 'mcp_pro_id');
+    }
+
+    public function manager() {
+        return $this->hasOne(Pro::class, 'id', 'manager_pro_id');
+    }
+
+    public function getProgramMonth($m, $y)
+    {
+        return ClientProgramMonth::where('client_program_id', $this->id)
+            ->where('month', $m)
+            ->where('year', $y)
+            ->first();
+    }
+}

+ 11 - 0
app/Models/ClientProgramMonth.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ClientProgramMonth extends Model
+{
+    protected $table = "client_program_month";
+
+}

+ 10 - 0
app/Models/ClientProgramMonthEntry.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ClientProgramMonthEntry extends Model
+{
+    protected $table = "client_program_month_entry";
+}

+ 34 - 0
app/Models/Measurement.php

@@ -7,4 +7,38 @@ namespace App\Models;
 class Measurement extends Model
 {
     protected $table = 'measurement';
+
+    public function client()
+    {
+        return $this->hasOne(Client::class, 'id', 'client_id');
+    }
+
+    public function numCPMEntries() {
+        return ClientProgramMonthEntry::where('measurement_id', $this->id)->where('is_cancelled', false)->count();
+    }
+
+    public function minutesEntered($pro) {
+        $entries = ClientProgramMonthEntry::where('measurement_id', $this->id)
+            ->where('pro_id', $pro->id)
+            ->where('is_cancelled', false)
+            ->get();
+        $minutes = 0;
+        foreach ($entries as $entry) {
+            $minutes += $entry->time_in_minutes;
+        }
+        return $minutes;
+    }
+
+    public function entriesByPro($pro) {
+        return ClientProgramMonthEntry::where('measurement_id', $this->id)
+            ->where('pro_id', $pro->id)
+            ->where('is_cancelled', false)
+            ->get();
+    }
+
+    public function childMeasurements() {
+        return Measurement::where('parent_measurement_id', $this->id)
+            ->where('is_removed', false)
+            ->get();
+    }
 }

+ 56 - 3
app/Models/Pro.php

@@ -6,6 +6,7 @@ namespace App\Models;
 
 use App\Helpers\TimeLine;
 use Exception;
+use Illuminate\Support\Facades\DB;
 
 class Pro extends Model
 {
@@ -262,14 +263,14 @@ class Pro extends Model
     public function getMyClientIds(){
 
         $accessibleClientIds = [];
-        
+
         $clientProAccesses = ClientProAccess::where('pro_id', $this->id)->get();
         foreach($clientProAccesses as $cpa){
             $accessibleClientIds[] = $cpa->client_id;
         }
 
         $appointmentClientIds = [];
-    
+
         $appointments = Appointment::where('pro_id', $this->id)->get();
         foreach($appointments as $appts){
             $appointmentClientIds[] = $appts->client_id;
@@ -288,8 +289,60 @@ class Pro extends Model
         foreach($clients as $client){
             $clientIds[] = $client->id;
         }
-        
+
         return $clientIds;
     }
 
+    public function favoritesByCategory($_category) {
+        return ProFavorite::where('pro_id', $this->id)
+            ->where('is_removed', false)
+            ->where('category', $_category)
+            ->orderBy('category', 'asc')
+            ->orderBy('position_index', 'asc')
+            ->get();
+    }
+
+    public function getAccessibleClientsQuery() {
+        $proID = $this->id;
+        if ($this->pro_type === 'ADMIN') {
+            $query = Client::where('id', '>', 0);
+        } else {
+            $query = Client::where(function ($q) use ($proID) {
+                $q->where('mcp_pro_id', $proID + 1)
+                    ->orWhere('cm_pro_id', $proID)
+                    ->orWhere('rmm_pro_id', $proID)
+                    ->orWhere('rme_pro_id', $proID)
+                    ->orWhereRaw('id IN (SELECT client_id FROM client_pro_access WHERE is_active AND pro_id = ?)', [$proID])
+                    ->orWhereRaw('id IN (SELECT client_id FROM appointment WHERE pro_id = ?)', [$proID])
+                    ->orWhereRaw('id IN (SELECT mcp_pro_id FROM client_program WHERE client_id = client.id AND is_active = TRUE)')
+                    ->orWhereRaw('id IN (SELECT manager_pro_id FROM client_program WHERE client_id = client.id AND is_active = TRUE)');
+            });
+        }
+        return $query;
+    }
+
+    public function canAddCPMEntryForMeasurement(Measurement $measurement, Pro $pro)
+    {
+        // check if client has any programs where this measurement type is allowed
+        $allowed = false;
+        $client = $measurement->client;
+        $clientPrograms = $client->clientPrograms;
+
+        if($pro->pro_type !== 'ADMIN') {
+            $clientPrograms = $clientPrograms->filter(function($_clientProgram) use ($pro) {
+                return  $_clientProgram->manager_pro_id === $pro->id;
+            });
+        }
+
+        if(count($clientPrograms)) {
+            foreach ($clientPrograms as $clientProgram) {
+                if(strpos(strtolower($clientProgram->measurement_labels), '|' . strtolower($measurement->label) . '|') !== FALSE) {
+                    $allowed = true;
+                    break;
+                }
+            }
+        }
+
+        return $allowed ? $clientPrograms : FALSE;
+    }
 }

+ 16 - 0
app/Models/ProFavorite.php

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

+ 11 - 0
app/Models/Ticket.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class Ticket extends Model
+{
+    protected $table = "ticket";
+
+}

+ 5 - 0
config/app.php

@@ -56,6 +56,11 @@ return [
 
     'asset_url' => env('ASSET_URL', null),
 
+    'agora_appid' => env('AGORA_APPID'),
+    'agora_mode' => env('AGORA_MODE'),
+
+    'backend_ws_url' => env('BACKEND_WS_URL'),
+
     /*
     |--------------------------------------------------------------------------
     | Application Timezone

+ 29 - 6
public/css/meeting.css

@@ -116,12 +116,13 @@ h1 {
     overflow: hidden;
     text-overflow: ellipsis;
     pointer-events: none;
+    font-weight: bold;
 }
 
 .main-view .thumbs {
     position: absolute;
     z-index: 2;
-    bottom: 1.5rem;
+    bottom: 1rem;
     right: 1rem;
     width: 180px;
     height: 100%;
@@ -150,13 +151,28 @@ h1 {
     left: 0;
     width: 100%;
     bottom: 0;
-    background: rgba(0, 0, 0, 0.5);
+    background: rgba(0, 0, 0, 0.75);
     color: #fff;
     font-size: 11px;
     white-space: nowrap;
     padding: 0 5px;
     overflow: hidden;
     text-overflow: ellipsis;
+    font-weight: bold;
+    text-align: center;
+}
+.main-view .thumbs .thumb-view>i.muted {
+    position: absolute;
+    z-index: 1;
+    right: 3px;
+    top: 3px;
+    font-size: 12px;
+    color: #fff;
+    background: #333;
+    width: 18px;
+    height: 18px;
+    text-align: center;
+    line-height: 18px;
 }
 .main-view .thumbs .disconnected-view {
     opacity: 0;
@@ -211,14 +227,21 @@ h1 {
 body .OT_fit-mode-cover .OT_video-element {
     object-fit: contain;
 }
-.hang-up {
+.call-actions {
     position: absolute;
     z-index: 2;
     bottom: 1rem;
     left: 1rem;
-    width: 50px;
-    height: 50px;
-    font-size: 120%;
+    width: 200px;
+    height: 40px;
+}
+.call-actions>button {
+    width: 40px;
+    height: 40px;
+    font-size: 90%;
+    margin-right: 0.75rem;
+    padding-left: 0;
+    padding-right: 0;
 }
 .call-mcp {
     position: absolute;

+ 53 - 1
public/css/style.css

@@ -128,6 +128,15 @@ body.stag_rhs_collapsed .app-right-panel {
 .mcp-theme-1 .on-hover-opaque:hover {
     opacity: 1;
 }
+.mcp-theme-1 .opacity-60 {
+    opacity: .6;
+}
+.mcp-theme-1 .overflow-visible {
+    overflow: visible;
+}
+.mcp-theme-1 .text-secondary-light {
+    color: #c9ddef !important;
+}
 .mcp-theme-1 a, .mcp-theme-1 a:link {
     color: rgb(13, 89, 175);
 }
@@ -254,6 +263,9 @@ body>nav.navbar {
     width: 100% !important;
     min-width: unset !important;
 }
+.mcp-theme-1 .max-width-200px {
+    max-width: 200px;
+}
 .mcp-theme-1 .max-width-300px {
     max-width: 300px;
 }
@@ -332,6 +344,10 @@ body>nav.navbar {
     background: #f6f9fc;
     cursor: pointer;
 }
+.note-signed-by-hcp .note-section:not(.edit):hover {
+    background: #f6f9fc;
+    cursor: auto;
+}
 .note-section.edit .if-edit {
     display: block !important;
 }
@@ -837,6 +853,7 @@ body .node input[type="number"] {
     left: 0;
     width: 100%;
     bottom: 0;
+    z-index: 4;
 }
 .queue-item {
     width: 100px;
@@ -953,7 +970,11 @@ span.pro-selection {
     border-top-right-radius: 3px;
     border-bottom-right-radius: 3px;
 }
-.select2-selection__choice__display {
+.select2-results__option--selectable {
+    font-size: 13px;
+}
+#calendarApp .select2-selection__choice__display,
+#proCalendarApp .select2-selection__choice__display {
     padding: 0 !important;
     overflow: hidden !important;
 }
@@ -1066,12 +1087,43 @@ table.table-edit-sheet .ql-toolbar {
 table.table-edit-sheet .ql-container {
     border-bottom: 0 !important;
 }
+table.table-edit-sheet .ql-editor[contenteditable] {
+    min-height: 60px;
+}
 .w-35 {
     width: 35%;
 }
 .client-single-dashboard .hide-if-dashboard {
     display: none;
 }
+.notes-list .hide-if-note,
 .note-section .hide-if-note {
     display: none;
 }
+.data-option-list {
+    position: absolute;
+    background: #fff;
+    border: 1px solid #ddd;
+    margin-top: -1px;
+    width: 100%;
+    z-index: 1;
+    display: none;
+}
+input[data-option-list]:focus+.data-option-list {
+    display: block;
+}
+.data-option-list>div {
+    cursor: pointer;
+    padding: 0.3rem 0.5rem;
+    border-bottom: 1px solid #ddd;
+    color: #666;
+}
+.data-option-list>div:last-child {
+    border-bottom: 0;
+}
+.data-option-list>div:hover {
+    background: aliceblue;
+}
+.measurement-item:not(:last-child) {
+    border-bottom: 1px solid #e7e7e7;
+}

+ 1 - 1
public/js/mc.js

@@ -250,7 +250,7 @@ function initCreateNote() {
             createNewNote($(this).attr('data-patient-uid'), $(this).attr('data-hcp-uid'), $(this).attr('data-effective-date'));
         });
     if ($('select[name="hasMcpDoneOnboardingVisit"]').length) {
-        $('select[name="hasMcpDoneOnboardingVisit"]')[0].onchange();
+        $('select[name="hasMcpDoneOnboardingVisit"]').trigger('change');
     }
 }
 

+ 13 - 0
public/js/option-list.js

@@ -0,0 +1,13 @@
+(function() {
+    function init() {
+        $(document)
+            .off('mousedown.option-list', '.data-option-list>div')
+            .on('mousedown.option-list', '.data-option-list>div', function() {
+                console.log(12);
+                $(this).parent().prev('input[data-option-list]').val('').focus();
+                document.execCommand('insertText', false, $(this).text());
+                return false;
+            });
+    }
+    addMCInitializer('option-list', init);
+})();

+ 51 - 0
public/js/stag-popup.js

@@ -0,0 +1,51 @@
+function showStagPopup(_key, _noAutoFocus) {
+    $('html, body').addClass('no-scroll');
+    let stagPopup = $('[stag-popup-key="' + _key + '"]');
+    stagPopup.addClass('show');
+    stagPopup.find('[moe][initialized]').removeAttr('initialized');
+    initMoes();
+    if(!_noAutoFocus) {
+        window.setTimeout(function() {
+            stagPopup.find('input[type="text"]:not([readonly]):visible,select:visible').first().focus();
+        }, 150);
+    }
+    return false;
+}
+function submitStagPopup(_form) {
+    if(!_form[0].checkValidity()) {
+        _form[0].reportValidity();
+        return false;
+    }
+    showMask();
+    $.post(_form.attr('action'), _form.serialize(), function(_data) {
+        fastReload();
+    });
+    return false;
+}
+function closeStagPopup() {
+    $('.stag-popup').removeClass('show');
+    $('html, body').removeClass('no-scroll');
+    return false;
+}
+(function() {
+    window.initStagPopupEvents = function () {
+        $(document).on('click', '.stag-popup', function(_e) {
+            if($(_e.target).is('.stag-popup')) {
+                closeStagPopup();
+            }
+        });
+        // 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) {
+                        closeStagPopup();
+                        return false;
+                    }
+                }
+            });
+    }
+    addMCInitializer('stag-popups', window.initStagPopupEvents);
+})();

+ 4 - 4
resources/views/app/dashboard.blade.php

@@ -20,19 +20,19 @@
                             <tbody>
                                 <tr>
                                     <th class="px-2 text-center">{{$keyNumbers['totalPatients']}}</th>
-                                    <th class="pl-2"><a target="_top" href="/mc/patients">Total patients</a></th>
+                                    <th class="pl-2"><a native target="_top" href="/patients">Total patients</a></th>
                                 </tr>
                                 <tr>
                                     <th class="px-2 text-center">{{$keyNumbers['patientsNotSeenYet']}}</th>
-                                    <th class="pl-2"><a target="_top" href="/mc/patients/not-yet-seen">Patients I have not seen yet</a></th>
+                                    <th class="pl-2"><a native target="_top" href="/patients/not-yet-seen">Patients I have not seen yet</a></th>
                                 </tr>
                                 <tr>
                                     <th class="px-2 text-center">{{$keyNumbers['pendingBillsToSign']}}</th>
-                                    <th class="pl-2"><a target="_top" href="/mc/practice-management/bills/not-yet-signed">Pending bills to sign</a></th>
+                                    <th class="pl-2"><a native target="_top" href="/practice-management/bills/not-yet-signed">Pending bills to sign</a></th>
                                 </tr>
                                 <tr>
                                     <th class="px-2 text-center">{{$keyNumbers['pendingNotesToSign']}}</th>
-                                    <th class="pl-2"><a target="_top" href="/mc/practice-management/notes/not-yet-signed">Pending notes to sign</a></th>
+                                    <th class="pl-2"><a native target="_top" href="/practice-management/notes/not-yet-signed">Pending notes to sign</a></th>
                                 </tr>
                             </tbody>
                         </table>

+ 17 - 341
resources/views/app/patient/action-items.blade.php

@@ -3,347 +3,23 @@
 /** @var \App\Models\Client $patient */
 ?>
 @section('inner-content')
-    <div class="">
-        <div class="d-flex align-items-center pb-2">
-            <h4 class="font-weight-bold m-0">ERx</h4>
-            <span class="mx-2 text-secondary">|</span>
-            <div moe>
-                <a start show class="py-0 font-weight-normal">Add</a>
-                <form url="/api/actionItem/create" wide>
-                    <input type="hidden" name="clientUid" value="{{ $patient->uid }}">
-                    <input type="hidden" name="prescriberProUid" value="{{ $pro->uid }}">
-                    <input type="hidden" name="category" value="DRUG">
-                    <div class="mb-2">
-                        <label for="" class="control-label text-sm text-secondary mb-1">Pharmacy</label>
-                        <select name="toFacilityUid"
-                                class="form-control form-control-sm">
-                            <option value="">-- Pharmacy --</option>
-                            @foreach ($facilities as $facility)
-                                <option value="{{$facility->uid}}">{{$facility->name}}</option>
-                            @endforeach
-                        </select>
-                    </div>
-                    <div class="mb-2">
-                        <input type="text" class="form-control form-control-sm" name="contentText" value="" placeholder="Title *" required>
-                    </div>
-                    <div class="mb-2">
-                        <input type="text" class="form-control form-control-sm" name="contentDetail" value="" placeholder="Directions">
-                    </div>
-                    <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>
-        </div>
-        <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
-            <thead>
-            <tr>
-                <th class="px-2 text-secondary border-bottom-0">Pharmacy</th>
-                <th class="px-2 text-secondary border-bottom-0 w-25">Action</th>
-                <th class="px-2 text-secondary border-bottom-0">Created</th>
-                <th class="px-2 text-secondary border-bottom-0">Status</th>
-                <th class="px-2 text-secondary border-bottom-0">&nbsp;</th>
-            </tr>
-            </thead>
-            <tbody>
-            <?php $prevItemType = false; ?>
-            @foreach($patient->actionItems as $item)
-                @if($item->action_item_category === 'DRUG')
-                <tr>
-                    <td class="px-2">
-                        {{$item->facility ? $item->facility->name : ''}}
-                        <span moe>
-                            <a start show class="on-hover-opaque"><i class="fa fa-edit"></i></a>
-                            <form url="/api/actionItem/updateToFacility">
-                                <input type="hidden" name="uid" value="{{ $item->uid }}">
-                                <div class="mb-2">
-                                    <label for="" class="control-label text-sm text-secondary mb-1">Pharmacy *</label>
-                                    <select name="toFacilityUid" class="form-control form-control-sm" required>
-                                        <option value="">-- Pharmacy --</option>
-                                        @foreach ($facilities as $facility)
-                                            <option {{ $item->to_facility_id === $facility->id ? 'selected' : '' }} value="{{$facility->uid}}">{{$facility->name}}</option>
-                                        @endforeach
-                                    </select>
-                                </div>
-                                <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>
-                        </span>
-                    </td>
-                    <td class="px-2">
-                        {{$item->content_text}}
-                        <span moe>
-                            <a start show class="on-hover-opaque"><i class="fa fa-edit"></i></a>
-                            <form url="/api/actionItem/updateContent" wide>
-                                <input type="hidden" name="uid" value="{{ $item->uid }}">
-                                <div class="mb-2">
-                                    <input type="text" class="form-control form-control-sm" name="contentText" value="{{ $item->content_text }}" placeholder="Title *" required>
-                                </div>
-                                <div class="mb-2">
-                                    <input type="text" class="form-control form-control-sm" name="contentDetail" value="{{ $item->content_detail }}" placeholder="Details">
-                                </div>
-                                <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>
-                        </span>
-                        <div class="text-sm text-secondary">{{$item->content_detail}}</div>
-                    </td>
-                    <td class="px-2">{{friendly_date_time($item->created_at, false)}}</td>
-                    <td class="px-2">
-                        {{$item->status_category}}
-                        <span moe>
-                            <a start show class="on-hover-opaque"><i class="fa fa-edit"></i></a>
-                            <form url="/api/actionItem/updateStatus">
-                                <input type="hidden" name="uid" value="{{ $item->uid }}">
-                                <div class="mb-2">
-                                    <label for="" class="control-label text-sm text-secondary mb-1">Status *</label>
-                                    <select name="statusCategory" class="form-control form-control-sm" required>
-                                        <option {{ $item->status_category === 'OPEN' ? 'selected' : '' }} value="OPEN">Open</option>
-                                        <option {{ $item->status_category === 'CLOSED' ? 'selected' : '' }} value="CLOSED">Closed</option>
-                                    </select>
-                                </div>
-                                <div class="mb-2">
-                                    <input type="text" class="form-control form-control-sm" name="statusMemo" value="" placeholder="Memo">
-                                </div>
-                                <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>
-                        </span>
-                    </td>
-                    <td class="px-2 text-center">
-                        <div class="d-flex align-items-center justify-content-start">
-                        @if($item->is_signed_by_prescriber)
-                            <span class="text-secondary">
-                                <i class="fa fa-check"></i>
-                                Signed
-                            </span>
-                            <span class="mx-2 text-secondary">|</span>
-                        @else
-                            @if($pro->id === $item->prescriber_pro_id)
-                                <span moe relative>
-                                    <a start show>Sign</a>
-                                    <form url="/api/actionItem/signAsPrescriber" right>
-                                        <input type="hidden" name="uid" value="{{ $item->uid }}">
-                                        <p class="small min-width-200px text-left">Sign this action items as the prescriber?</p>
-                                        <div class="d-flex align-items-center">
-                                            <button class="btn btn-sm btn-success mr-2" submit>Yes</button>
-                                            <button class="btn btn-sm btn-default mr-2 border" cancel>No</button>
-                                        </div>
-                                    </form>
-                                </span>
-                                <span class="mx-2 text-secondary">|</span>
-                            @endif
-                        @endif
-                        <span moe relative>
-                            <a start show>eFax</a>
-                            <form url="/api/actionItem/efax" right>
-                                <input type="hidden" name="uid" value="{{ $item->uid }}">
-                                <div class="mb-2">
-                                    <input type="text" class="form-control form-control-sm" name="toFaxNumber" value="" placeholder="To Number *" required>
-                                </div>
-                                <div class="d-flex align-items-center">
-                                    <button class="btn btn-sm btn-primary mr-2" submit>Send</button>
-                                    <button class="btn btn-sm btn-default mr-2 border" cancel>Cancel</button>
-                                </div>
-                            </form>
-                        </span>
-                        </div>
-                    </td>
-                </tr>
-                @endif
-            @endforeach
-            </tbody>
-        </table>
-    </div>
 
-    <div class="mt-5">
-        <div class="d-flex align-items-center pb-2">
-            <h4 class="font-weight-bold m-0">Orders</h4>
-            <span class="mx-2 text-secondary">|</span>
-            <div moe>
-                <a start show class="py-0 font-weight-normal">Add</a>
-                <form url="/api/actionItem/create" wide>
-                    <input type="hidden" name="clientUid" value="{{ $patient->uid }}">
-                    <input type="hidden" name="prescriberProUid" value="{{ $pro->uid }}">
-                    <div class="mb-2">
-                        <label for="" class="control-label text-sm text-secondary mb-1">Facility</label>
-                        <select name="toFacilityUid"
-                                class="form-control form-control-sm">
-                            <option value="">-- Facility --</option>
-                            @foreach ($facilities as $facility)
-                                <option value="{{$facility->uid}}">{{$facility->name}}</option>
-                            @endforeach
-                        </select>
-                    </div>
-                    <div class="mb-2">
-                        <label for="" class="control-label text-sm text-secondary mb-1">Category *</label>
-                        <select name="category"
-                                class="form-control form-control-sm" required>
-                            <option value="">-- Category --</option>
-                            <option value="DIAGNOSTIC">Diagnostic</option>
-                            <option value="APPOINTMENT">Appointment</option>
-                            <option value="EXERCISE">Exercise</option>
-                            <option value="DIETETITC">Dietetitc</option>
-                            <option value="EQUIPMENT">Equipment</option>
-                            <option value="OTHER">Other</option>
-                        </select>
-                    </div>
-                    <div class="mb-2">
-                        <input type="text" class="form-control form-control-sm" name="contentText" value="" placeholder="Title *" required>
-                    </div>
-                    <div class="mb-2">
-                        <input type="text" class="form-control form-control-sm" name="contentDetail" value="" placeholder="Details">
-                    </div>
-                    <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>
-        </div>
-        <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
-            <thead>
-            <tr>
-                <th class="px-2 text-secondary border-bottom-0">Category</th>
-                <th class="px-2 text-secondary border-bottom-0">Facility</th>
-                <th class="px-2 text-secondary border-bottom-0 w-25">Action</th>
-                <th class="px-2 text-secondary border-bottom-0">Created</th>
-                <th class="px-2 text-secondary border-bottom-0">Status</th>
-                <th class="px-2 text-secondary border-bottom-0">&nbsp;</th>
-            </tr>
-            </thead>
-            <tbody>
-            <?php $prevItemType = false; ?>
-            @foreach($patient->actionItems as $item)
-                @if($item->action_item_category !== 'DRUG')
-                @if(!$prevItemType || $prevItemType !== $item->action_item_category)
-                    <tr class="bg-light">
-                        <td colspan="6" class="font-weight-bold px-2">
-                            {{ucwords(str_replace("_", " ", strtolower($item->action_item_category)))}}
-                        </td>
-                    </tr>
-                @endif
-                <tr>
-                    <td class="px-2">
-                        &nbsp;
-                    </td>
-                    <td class="px-2">
-                        {{$item->facility ? $item->facility->name : ''}}
-                        <span moe>
-                            <a start show class="on-hover-opaque"><i class="fa fa-edit"></i></a>
-                            <form url="/api/actionItem/updateToFacility">
-                                <input type="hidden" name="uid" value="{{ $item->uid }}">
-                                <div class="mb-2">
-                                    <label for="" class="control-label text-sm text-secondary mb-1">Pharmacy *</label>
-                                    <select name="toFacilityUid" class="form-control form-control-sm" required>
-                                        <option value="">-- Pharmacy --</option>
-                                        @foreach ($facilities as $facility)
-                                            <option {{ $item->to_facility_id === $facility->id ? 'selected' : '' }} value="{{$facility->uid}}">{{$facility->name}}</option>
-                                        @endforeach
-                                    </select>
-                                </div>
-                                <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>
-                        </span>
-                    </td>
-                    <td class="px-2">
-                        {{$item->content_text}}
-                        <span moe>
-                            <a start show class="on-hover-opaque"><i class="fa fa-edit"></i></a>
-                            <form url="/api/actionItem/updateContent" wide>
-                                <input type="hidden" name="uid" value="{{ $item->uid }}">
-                                <div class="mb-2">
-                                    <input type="text" class="form-control form-control-sm" name="contentText" value="{{ $item->content_text }}" placeholder="Title *" required>
-                                </div>
-                                <div class="mb-2">
-                                    <input type="text" class="form-control form-control-sm" name="contentDetail" value="{{ $item->content_detail }}" placeholder="Details">
-                                </div>
-                                <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>
-                        </span>
-                        <div class="text-sm text-secondary">{{$item->content_detail}}</div>
-                    </td>
-                    <td class="px-2">{{friendly_date_time($item->created_at, false)}}</td>
-                    <td class="px-2">
-                        {{$item->status_category}}
-                        <span moe>
-                            <a start show class="on-hover-opaque"><i class="fa fa-edit"></i></a>
-                            <form url="/api/actionItem/updateStatus">
-                                <input type="hidden" name="uid" value="{{ $item->uid }}">
-                                <div class="mb-2">
-                                    <label for="" class="control-label text-sm text-secondary mb-1">Status *</label>
-                                    <select name="statusCategory" class="form-control form-control-sm" required>
-                                        <option {{ $item->status_category === 'OPEN' ? 'selected' : '' }} value="OPEN">Open</option>
-                                        <option {{ $item->status_category === 'CLOSED' ? 'selected' : '' }} value="CLOSED">Closed</option>
-                                    </select>
-                                </div>
-                                <div class="mb-2">
-                                    <input type="text" class="form-control form-control-sm" name="statusMemo" value="" placeholder="Memo">
-                                </div>
-                                <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>
-                        </span>
-                    </td>
-                    <td class="px-2 text-center">
-                        <div class="d-flex align-items-center justify-content-start">
-                            @if($item->is_signed_by_prescriber)
-                                <span class="text-secondary">
-                                <i class="fa fa-check"></i>
-                                Signed
-                            </span>
-                                <span class="mx-2 text-secondary">|</span>
-                            @else
-                                @if($pro->id === $item->prescriber_pro_id)
-                                    <span moe relative>
-                                    <a start show>Sign</a>
-                                    <form url="/api/actionItem/signAsPrescriber" right>
-                                        <input type="hidden" name="uid" value="{{ $item->uid }}">
-                                        <p class="small min-width-200px text-left">Sign this action items as the prescriber?</p>
-                                        <div class="d-flex align-items-center">
-                                            <button class="btn btn-sm btn-success mr-2" submit>Yes</button>
-                                            <button class="btn btn-sm btn-default mr-2 border" cancel>No</button>
-                                        </div>
-                                    </form>
-                                </span>
-                                    <span class="mx-2 text-secondary">|</span>
-                                @endif
-                            @endif
-                            <span moe relative>
-                            <a start show>eFax</a>
-                            <form url="/api/actionItem/efax" right>
-                                <input type="hidden" name="uid" value="{{ $item->uid }}">
-                                <div class="mb-2">
-                                    <input type="text" class="form-control form-control-sm" name="toFaxNumber" value="" placeholder="To Number *" required>
-                                </div>
-                                <div class="d-flex align-items-center">
-                                    <button class="btn btn-sm btn-primary mr-2" submit>Send</button>
-                                    <button class="btn btn-sm btn-default mr-2 border" cancel>Cancel</button>
-                                </div>
-                            </form>
-                        </span>
-                        </div>
-                    </td>
-                </tr>
-                <?php $prevItemType = $item->action_item_category; ?>
-                @endif
-            @endforeach
-            </tbody>
-        </table>
+    <div class="mb-5">
+
+    @include('app/patient/partials/erx')
+
+    <hr class="m-neg-4 my-4">
+    @include('app/patient/partials/lab')
+
+    <hr class="m-neg-4 my-4">
+    @include('app/patient/partials/imaging')
+
+    <hr class="m-neg-4 my-4">
+    @include('app/patient/partials/equipment')
+
+    <hr class="m-neg-4 my-4">
+    @include('app/patient/partials/other')
+
     </div>
+
 @endsection

+ 7 - 3
resources/views/app/patient/appointment-calendar.blade.php

@@ -515,8 +515,9 @@
                                 selectable: true,
                                 navLinks: true,
                                 dayMaxEvents: false,
-                                slotMinTime: '06:00',
-                                slotMaxTime: '20:00',
+                                slotMinTime: '00:00',
+                                slotMaxTime: '23:59',
+                                scrollTime: '06:00:00',
                                 slotDuration: '00:15:00',
                                 events: function(info, successCallback, failureCallback) {
                                     // if(!self.proIds || !self.proIds.length) {
@@ -578,8 +579,11 @@
                                         }
                                     }, 'json');
                                 },
-                                eventDidMount: function(view) {
+                                eventDidMount: function(arg) {
                                     self.afterRenderingEvents();
+                                    if(arg.event.extendedProps.type === 'appointment') {
+                                        console.log(arg.event)
+                                    }
                                 },
                                 eventClassNames: function(arg) {
                                     let classes = [];

+ 164 - 3
resources/views/app/patient/canvas-sections/allergies/form.blade.php

@@ -15,6 +15,29 @@ $formID = rand(0, 100000);
 <div id="allergySection">
     <h3 class="stag-popup-title mb-2 border-bottom-0 pb-1 hide-if-note">
         <span>Allergies</span>
+        <div v-if="favorites && favorites.length" class="d-inline-flex ml-2">
+            <span class="mr-2 text-secondary">|</span>
+            <div moe relative>
+                <a start show href="#">
+                    <i class="fa fa-star"></i>
+                    Favorites
+                </a>
+                <div url="#" class="mt-1">
+                    <a href="#"
+                       v-for="(favorite, favIndex) in favorites"
+                       class="d-flex align-items-center text-nowrap text-decoration-none"
+                       :class="favIndex < favorites.length - 1 ? 'mb-2' : ''"
+                       v-on:click.prevent="addFromFavorite(favorite)">
+                        <i class="fa fa-check font-weight-bold mr-2" :class="isFavoriteAdded(favorite) ? '' : 'text-secondary-light'"></i>
+                        <span :class="isFavoriteAdded(favorite) ? 'font-weight-bold' : ''">@{{ favorite.data.title }}</span>
+                        <span v-if="favorite.data.strength" class="text-secondary d-inline-flex">
+                            <span class="mx-1">/</span>
+                            <span>@{{ favorite.data.strength }}</span>
+                        </span>
+                    </a>
+                </div>
+            </div>
+        </div>
         <a href="#" onclick="return closeStagPopup()"
            class="ml-auto text-secondary">
             <i class="fa fa-times-circle"></i>
@@ -27,7 +50,36 @@ $formID = rand(0, 100000);
         <thead>
         <tr class="bg-light">
             <th class="px-2 text-secondary border-bottom-0 width-30px text-center">#</th>
-            <th class="px-2 text-secondary border-bottom-0">Allergy</th>
+            <th class="px-2 text-secondary border-bottom-0">
+                <div class="d-flex align-items-center font-weight-normal">
+                    <span>Title</span>
+                    <div class="hide-if-dashboard ml-auto">
+                        <div v-if="favorites && favorites.length" class="d-inline-flex2">
+                            <div moe relative>
+                                <a start show href="#">
+                                    <i class="fa fa-star"></i>
+                                    Favorites
+                                </a>
+                                <div url="#" class="mt-1">
+                                    <a href="#"
+                                       v-for="(favorite, favIndex) in favorites"
+                                       class="d-flex align-items-center text-nowrap text-decoration-none"
+                                       :class="favIndex < favorites.length - 1 ? 'mb-2' : ''"
+                                       v-on:click.prevent="addFromFavorite(favorite)">
+                                        <i class="fa fa-check font-weight-bold mr-2"
+                                           :class="isFavoriteAdded(favorite) ? '' : 'text-secondary-light'"></i>
+                                        <span :class="isFavoriteAdded(favorite) ? 'font-weight-bold' : ''">@{{ favorite.data.title }}</span>
+                                        <span v-if="favorite.data.strength" class="text-secondary d-inline-flex">
+                                            <span class="mx-1">/</span>
+                                            <span>@{{ favorite.data.strength }}</span>
+                                        </span>
+                                    </a>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </th>
             <th class="px-2 text-secondary border-bottom-0 w-50">Detail</th>
             <th class="px-2 text-secondary border-bottom-0"></th>
         </tr>
@@ -45,7 +97,19 @@ $formID = rand(0, 100000);
                           allergy-rte :data-index="index" data-field="detail"
                           v-model="item.detail"></textarea>
             </td>
-            <td class="px-2">
+            <td class="px-2 text-nowrap">
+                <a href="#"
+                   v-if="!isFavorite(item)" v-on:click.prevent="addToFavorites(item)"
+                   class="mt-1 d-inline-block mr-1 text-secondary on-hover-opaque"
+                   title="Add to favorites">
+                    <i class="fa fa-star"></i>
+                </a>
+                <a href="#"
+                   v-if="isFavorite(item)" v-on:click.prevent="removeFromFavorites(item)"
+                   class="mt-1 d-inline-block mr-1 text-warning"
+                   title="Remove from favorites">
+                    <i class="fa fa-star"></i>
+                </a>
                 <a href="#" v-on:click.prevent="removeItem(index)"
                    class="on-hover-opaque text-danger mt-1 d-inline-block">
                     <i class="fa fa-trash-alt"></i>
@@ -64,14 +128,21 @@ $formID = rand(0, 100000);
 </div>
 <script>
     (function() {
+        let favorites = <?= json_encode($pro->favoritesByCategory('allergies')) ?>;
+        for (let i = 0; i < favorites.length; i++) {
+            favorites[i].data = JSON.parse(favorites[i].data);
+        }
         function init() {
             window.clientAllergyApp = new Vue({
                 el: '#allergySection',
                 data: {
-                    items: <?= json_encode($contentData['items']) ?>
+                    items: <?= json_encode($contentData['items']) ?>,
+                    favorites: favorites,
                 },
                 mounted: function() {
                     this.initRTE();
+                    $('#allergySection [moe][initialized]').removeAttr('initialized');
+                    initMoes();
                 },
                 watch: {
                     $data: {
@@ -155,6 +226,96 @@ $formID = rand(0, 100000);
 
                         });
                     },
+                    isFavorite: function(_item) {
+                        let matches = this.favorites.filter(function(_x) {
+                            return _x.data.title === _item.title;
+                        });
+                        return matches.length ? matches[0].uid : false;
+                    },
+                    addToFavorites: function(_item) {
+                        if(!_item.title || !$.trim(_item.title)) return;
+                        if(this.isFavorite(_item)) return;
+                        let self = this;
+                        $.post('/api/proFavorite/create', {
+                            proUid: '{{$pro->uid}}',
+                            category: 'allergies',
+                            data: JSON.stringify(_item)
+                        }, function(_data) {
+                            if(_data && _data.success) {
+                                self.favorites.push({
+                                    uid: _data.data,
+                                    category: 'allergies',
+                                    data: _item
+                                });
+                                toastr.success('Added to favorites');
+                            }
+                            else {
+                                if(_data && _data.message) {
+                                    toastr.error(_data.message);
+                                }
+                                else {
+                                    toastr.error('Unable to add item to favorites');
+                                }
+                            }
+                        }, 'json');
+                    },
+                    removeFromFavorites: function(_item) {
+                        let uid = this.isFavorite(_item);
+                        if(!uid) return;
+                        let self = this;
+                        $.post('/api/proFavorite/remove', {
+                            uid: uid
+                        }, function(_data) {
+                            if(_data && _data.success) {
+                                self.favorites = self.favorites.filter(function (_x) {
+                                    return _x.uid !== uid;
+                                });
+                                toastr.success('Removed from favorites');
+                            }
+                            else {
+                                if(_data && _data.message) {
+                                    toastr.error(_data.message);
+                                }
+                                else {
+                                    toastr.error('Unable to remove item from favorites');
+                                }
+                            }
+                        }, 'json');
+                    },
+                    addFromFavorite: function(_favorite) {
+                        if(this.isFavoriteAdded(_favorite)) {
+                            for (let i = 0; i < this.items.length; i++) {
+                                if(this.items[i].title === _favorite.data.title) {
+                                    this.items.splice(i, 1);
+                                    return;
+                                }
+                            }
+                            return;
+                        }
+                        let item = _favorite.data;
+                        if(!this.items.length) {
+                            this.items.push(item);
+                        }
+                        else {
+                            let lastItem = this.items[this.items.length - 1];
+                            if(!lastItem.title) {
+                                this.items.splice(this.items.length - 1, 1, item);
+                            }
+                            else {
+                                this.items.push(item);
+                            }
+                        }
+                        let self = this;
+                        Vue.nextTick(function() {
+                            self.initRTE();
+                        });
+                    },
+                    isFavoriteAdded: function(_favorite) {
+                        let matches = this.items.filter(function(_x) {
+                            return _x.title === _favorite.data.title;
+                        });
+                        return matches.length > 0;
+                    }
                 }
             });
         }

+ 9 - 2
resources/views/app/patient/canvas-sections/allergies/summary.php

@@ -7,6 +7,9 @@ if($patient->canvas_data) {
     $canvasData = json_decode($patient->canvas_data, true);
     if(isset($canvasData["allergies"])) {
         $contentData = $canvasData["allergies"];
+        if(!isset($contentData['items'])){
+            $contentData['items'] = [];
+        }
     }
 }
 
@@ -19,8 +22,12 @@ if(count($contentData['items'])) {
                 <b><?= $item["title"] ?></b>
             </div>
 
-            <?php if(isset($item["detail"]) && !empty($item["detail"])): ?>
-                <div class="text-secondary"><?= $item["detail"] ?></div>
+            <?php
+            $detailPlain = isset($item["detail"]) ? $item["detail"] : '';
+            $detailPlain = trim(strip_tags($detailPlain));
+            if(!empty($detailPlain)):
+            ?>
+                <div class="text-secondary"><?= $detailPlain ?></div>
             <?php endif; ?>
         </div>
 <?php

+ 175 - 5
resources/views/app/patient/canvas-sections/dx/form.blade.php

@@ -1,11 +1,17 @@
 <?php
+if(!isset($contentData['items'])){
+    $contentData['items'] = [];
+}
+if(!is_array($contentData['items'])){
+    $contentData['items'] = [];
+}
 if(!$contentData || !isset($contentData['items']) || !count($contentData['items'])) {
     $contentData = [
         "items" => [
             [
                 "title" => "",
                 "icd" => "",
-                "coa" => "",
+                "coa" => "Chronic",
                 "detail" => "",
                 "plan" => "",
             ]
@@ -18,6 +24,29 @@ $formID = rand(0, 100000);
 <div id="dxSection">
     <h3 class="stag-popup-title mb-2 border-bottom-0 pb-1 hide-if-note">
         <span>Current Problems / Focus Areas</span>
+        <div v-if="favorites && favorites.length" class="d-inline-flex ml-2">
+            <span class="mr-2 text-secondary">|</span>
+            <div moe relative>
+                <a start show href="#">
+                    <i class="fa fa-star"></i>
+                    Favorites
+                </a>
+                <div url="#" class="mt-1">
+                    <a href="#"
+                       v-for="(favorite, favIndex) in favorites"
+                       class="d-flex align-items-center text-nowrap text-decoration-none"
+                       :class="favIndex < favorites.length - 1 ? 'mb-2' : ''"
+                       v-on:click.prevent="addFromFavorite(favorite)">
+                        <i class="fa fa-check font-weight-bold mr-2" :class="isFavoriteAdded(favorite) ? '' : 'text-secondary-light'"></i>
+                        <span :class="isFavoriteAdded(favorite) ? 'font-weight-bold' : ''">@{{ favorite.data.title }}</span>
+                        <span v-if="favorite.data.strength" class="text-secondary d-inline-flex">
+                            <span class="mx-1">/</span>
+                            <span>@{{ favorite.data.strength }}</span>
+                        </span>
+                    </a>
+                </div>
+            </div>
+        </div>
         <a href="#" onclick="return closeStagPopup()"
            class="ml-auto text-secondary">
             <i class="fa fa-times-circle"></i>
@@ -37,7 +66,36 @@ $formID = rand(0, 100000);
                     <span>All</span>
                 </label>
             </th>
-            <th class="px-2 text-secondary border-bottom-0 w-35">Title</th>
+            <th class="px-2 text-secondary border-bottom-0 w-35">
+                <div class="d-flex align-items-center font-weight-normal">
+                    <span>Title</span>
+                    <div class="hide-if-dashboard ml-auto">
+                        <div v-if="favorites && favorites.length" class="d-inline-flex2">
+                            <div moe relative>
+                                <a start show href="#">
+                                    <i class="fa fa-star"></i>
+                                    Favorites
+                                </a>
+                                <div url="#" class="mt-1">
+                                    <a href="#"
+                                       v-for="(favorite, favIndex) in favorites"
+                                       class="d-flex align-items-center text-nowrap text-decoration-none"
+                                       :class="favIndex < favorites.length - 1 ? 'mb-2' : ''"
+                                       v-on:click.prevent="addFromFavorite(favorite)">
+                                        <i class="fa fa-check font-weight-bold mr-2"
+                                           :class="isFavoriteAdded(favorite) ? '' : 'text-secondary-light'"></i>
+                                        <span :class="isFavoriteAdded(favorite) ? 'font-weight-bold' : ''">@{{ favorite.data.title }}</span>
+                                        <span v-if="favorite.data.strength" class="text-secondary d-inline-flex">
+                                            <span class="mx-1">/</span>
+                                            <span>@{{ favorite.data.strength }}</span>
+                                        </span>
+                                    </a>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </th>
             <th class="px-2 text-secondary border-bottom-0 min-width-140px">ICD</th>
             <th class="px-2 text-secondary border-bottom-0 w-25">Detail</th>
             <th class="px-2 text-secondary border-bottom-0 w-25">Plan</th>
@@ -77,7 +135,19 @@ $formID = rand(0, 100000);
                           dx-rte :data-index="index" data-field="plan"
                           v-model="item.plan"></textarea>
             </td>
-            <td class="px-2">
+            <td class="px-2 text-nowrap">
+                <a href="#"
+                   v-if="!isFavorite(item)" v-on:click.prevent="addToFavorites(item)"
+                   class="mt-1 d-inline-block mr-1 text-secondary on-hover-opaque"
+                   title="Add to favorites">
+                    <i class="fa fa-star"></i>
+                </a>
+                <a href="#"
+                   v-if="isFavorite(item)" v-on:click.prevent="removeFromFavorites(item)"
+                   class="mt-1 d-inline-block mr-1 text-warning"
+                   title="Remove from favorites">
+                    <i class="fa fa-star"></i>
+                </a>
                 <a href="#" v-on:click.prevent="removeItem(index)"
                    class="on-hover-opaque text-danger mt-1 d-inline-block">
                     <i class="fa fa-trash-alt"></i>
@@ -96,13 +166,18 @@ $formID = rand(0, 100000);
 </div>
 <script>
     (function() {
+        let favorites = <?= json_encode($pro->favoritesByCategory('dx')) ?>;
+        for (let i = 0; i < favorites.length; i++) {
+            favorites[i].data = JSON.parse(favorites[i].data);
+        }
         function init() {
             window.clientDXApp = new Vue({
                 el: '#dxSection',
                 data: {
-                    includeAll: true,
+                    includeAll: false,
                     items: <?= json_encode($contentData['items']) ?>,
-                    inclusion: []
+                    inclusion: [],
+                    favorites: favorites,
                 },
                 mounted: function() {
                     this.inclusion = [];
@@ -111,6 +186,9 @@ $formID = rand(0, 100000);
                     }
                     this.initRTE();
                     this.initTitleAutoSuggest();
+                    $('#dxSection [moe][initialized]').removeAttr('initialized');
+                    initMoes();
+                    this.includeChanged();
                 },
                 watch: {
                     items: {
@@ -270,6 +348,98 @@ $formID = rand(0, 100000);
                             });
                             $(elem).attr('ac-initialized', 1);
                         });
+                    },
+                    isFavorite: function(_item) {
+                        let matches = this.favorites.filter(function(_x) {
+                            return _x.data.title === _item.title;
+                        });
+                        return matches.length ? matches[0].uid : false;
+                    },
+                    addToFavorites: function(_item) {
+                        if(!_item.title || !$.trim(_item.title)) return;
+                        if(this.isFavorite(_item)) return;
+                        let self = this;
+                        $.post('/api/proFavorite/create', {
+                            proUid: '{{$pro->uid}}',
+                            category: 'dx',
+                            data: JSON.stringify(_item)
+                        }, function(_data) {
+                            if(_data && _data.success) {
+                                self.favorites.push({
+                                    uid: _data.data,
+                                    category: 'dx',
+                                    data: _item
+                                });
+                                toastr.success('Added to favorites');
+                            }
+                            else {
+                                if(_data && _data.message) {
+                                    toastr.error(_data.message);
+                                }
+                                else {
+                                    toastr.error('Unable to add item to favorites');
+                                }
+                            }
+                        }, 'json');
+                    },
+                    removeFromFavorites: function(_item) {
+                        let uid = this.isFavorite(_item);
+                        if(!uid) return;
+                        let self = this;
+                        $.post('/api/proFavorite/remove', {
+                            uid: uid
+                        }, function(_data) {
+                            if(_data && _data.success) {
+                                self.favorites = self.favorites.filter(function (_x) {
+                                    return _x.uid !== uid;
+                                });
+                                toastr.success('Removed from favorites');
+                            }
+                            else {
+                                if(_data && _data.message) {
+                                    toastr.error(_data.message);
+                                }
+                                else {
+                                    toastr.error('Unable to remove item from favorites');
+                                }
+                            }
+                        }, 'json');
+                    },
+                    addFromFavorite: function(_favorite) {
+                        if(this.isFavoriteAdded(_favorite)) {
+                            for (let i = 0; i < this.items.length; i++) {
+                                if(this.items[i].title === _favorite.data.title) {
+                                    this.items.splice(i, 1);
+                                    return;
+                                }
+                            }
+                            return;
+                        }
+                        let item = _favorite.data;
+                        item.included = true; // default to included
+                        if(!this.items.length) {
+                            this.items.push(item);
+                        }
+                        else {
+                            let lastItem = this.items[this.items.length - 1];
+                            if(!lastItem.title) {
+                                this.items.splice(this.items.length - 1, 1, item);
+                            }
+                            else {
+                                this.items.push(item);
+                            }
+                        }
+                        let self = this;
+                        Vue.nextTick(function() {
+                            self.initRTE();
+                            self.initTitleAutoSuggest();
+                        });
+                    },
+                    isFavoriteAdded: function(_favorite) {
+                        let matches = this.items.filter(function(_x) {
+                            return _x.title === _favorite.data.title;
+                        });
+                        return matches.length > 0;
                     }
                 }
             });

+ 24 - 23
resources/views/app/patient/canvas-sections/dx/summary.php

@@ -1,29 +1,22 @@
 <?php
 
-if(isset($contentData) && !!$contentData) {
-    $dxContentData = $contentData;
-}
-else {
-    $dxContentData = false;
-}
-
-if(!$dxContentData && $patient->canvas_data) {
+$contentData = [
+    "items" => []
+];
+if($patient->canvas_data) {
     $canvasData = json_decode($patient->canvas_data, true);
     if(isset($canvasData["dx"])) {
-        $dxContentData = $canvasData["dx"];
-    }
-}
+        $contentData = $canvasData["dx"];
 
-if(!$dxContentData) {
-    $dxContentData = [
-        "items" => []
-    ];
+        if(!isset($contentData['items'])){
+            $contentData['items'] = [];
+        }
+    }
 }
 
-if(count($dxContentData['items'])) {
-    for ($i = 0; $i < count($dxContentData['items']); $i++) {
-        $item = $dxContentData['items'][$i];
-
+if(count($contentData['items'])) {
+    for ($i = 0; $i < count($contentData['items']); $i++) {
+        $item = $contentData['items'][$i];
 ?>
         <div class="mb-2 <?= @$item["included"] ? '' : 'hide-if-note' ?>">
             <div class="">
@@ -31,13 +24,21 @@ if(count($dxContentData['items'])) {
                 <?= isset($item["icd"]) ? '/&nbsp;' . $item["icd"] : '' ?>
                 <?= isset($item["coa"]) ? '/&nbsp;' . $item["coa"] : '' ?>
             </div>
-            <?php if(isset($item["detail"]) && !empty($item["detail"])): ?>
+            <?php
+            $detailPlain = isset($item["detail"]) ? $item["detail"] : '';
+            $detailPlain = trim(strip_tags($detailPlain));
+            if(!empty($detailPlain)):
+            ?>
                 <div class="text-secondary font-weight-bold">Detail</div>
-                <div class="ml-2"><?= $item["detail"] ?></div>
+                <div class="ml-2"><?= $detailPlain ?></div>
             <?php endif; ?>
-            <?php if(isset($item["plan"]) && !empty($item["plan"])): ?>
+            <?php
+            $planPlain = isset($item["plan"]) ? $item["plan"] : '';
+            $planPlain = trim(strip_tags($planPlain));
+            if(!empty($planPlain)):
+            ?>
                 <div class="text-secondary font-weight-bold">Plan</div>
-                <div class="ml-2"><?= $item["plan"] ?></div>
+                <div class="ml-2"><?= $planPlain ?></div>
             <?php endif; ?>
         </div>
 <?php

+ 5 - 2
resources/views/app/patient/canvas-sections/fhx/summary.php

@@ -9,6 +9,9 @@ if($patient->canvas_data) {
     $canvasData = json_decode($patient->canvas_data, true);
     if(isset($canvasData["fhx"])) {
         $contentData = $canvasData["fhx"];
+        if(!isset($contentData['items'])){
+            $contentData['items'] = [];
+        }
     }
 }
 
@@ -17,8 +20,8 @@ $labels = [
     'general_arthritis' => 'Arthritis',
     'general_asthma' => 'Asthma',
     'general_bleeding_disorder' => 'Bleeding disorder',
-    'general_cad_lt_age_55' => 'Cad &gt; age 55',
-    'general_copd' => 'Copd',
+    'general_cad_lt_age_55' => 'CAD &gt; age 55',
+    'general_copd' => 'COPD',
     'general_diabetes' => 'Diabetes',
     'general_heart_attack' => 'Heart attack',
     'general_heart_disease' => 'Heart disease',

+ 171 - 10
resources/views/app/patient/canvas-sections/rx/form.blade.php

@@ -17,6 +17,29 @@ $formID = rand(0, 100000);
 <div id="rxSection">
     <h3 class="stag-popup-title mb-2 border-bottom-0 pb-1 hide-if-note">
         <span>Current Medications</span>
+        <div v-if="favorites && favorites.length" class="d-inline-flex ml-2">
+            <span class="mr-2 text-secondary">|</span>
+            <div moe relative>
+                <a start show href="#">
+                    <i class="fa fa-star"></i>
+                    Favorites
+                </a>
+                <div url="#" class="mt-1">
+                    <a href="#"
+                       v-for="(favorite, favIndex) in favorites"
+                       class="d-flex align-items-center text-nowrap text-decoration-none"
+                       :class="favIndex < favorites.length - 1 ? 'mb-2' : ''"
+                       v-on:click.prevent="addFromFavorite(favorite)">
+                        <i class="fa fa-check font-weight-bold mr-2" :class="isFavoriteAdded(favorite) ? '' : 'text-secondary-light'"></i>
+                        <span :class="isFavoriteAdded(favorite) ? 'font-weight-bold' : ''">@{{ favorite.data.title }}</span>
+                        <span v-if="favorite.data.strength" class="text-secondary d-inline-flex">
+                            <span class="mx-1">/</span>
+                            <span>@{{ favorite.data.strength }}</span>
+                        </span>
+                    </a>
+                </div>
+            </div>
+        </div>
         <a href="#" onclick="return closeStagPopup()"
            class="ml-auto text-secondary">
             <i class="fa fa-times-circle"></i>
@@ -29,7 +52,36 @@ $formID = rand(0, 100000);
         <thead>
         <tr class="bg-light">
             <th class="px-2 text-secondary border-bottom-0 width-30px text-center">#</th>
-            <th class="px-2 text-secondary border-bottom-0 w-35">Medication</th>
+            <th class="px-2 text-secondary border-bottom-0 w-35">
+                <div class="d-flex align-items-center font-weight-normal">
+                    <span>Title</span>
+                    <div class="hide-if-dashboard ml-auto">
+                        <div v-if="favorites && favorites.length" class="d-inline-flex2">
+                            <div moe relative>
+                                <a start show href="#">
+                                    <i class="fa fa-star"></i>
+                                    Favorites
+                                </a>
+                                <div url="#" class="mt-1">
+                                    <a href="#"
+                                       v-for="(favorite, favIndex) in favorites"
+                                       class="d-flex align-items-center text-nowrap text-decoration-none"
+                                       :class="favIndex < favorites.length - 1 ? 'mb-2' : ''"
+                                       v-on:click.prevent="addFromFavorite(favorite)">
+                                        <i class="fa fa-check font-weight-bold mr-2"
+                                           :class="isFavoriteAdded(favorite) ? '' : 'text-secondary-light'"></i>
+                                        <span :class="isFavoriteAdded(favorite) ? 'font-weight-bold' : ''">@{{ favorite.data.title }}</span>
+                                        <span v-if="favorite.data.strength" class="text-secondary d-inline-flex">
+                                            <span class="mx-1">/</span>
+                                            <span>@{{ favorite.data.strength }}</span>
+                                        </span>
+                                    </a>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </th>
             <th class="px-2 text-secondary border-bottom-0">Strength/Form/Freq.</th>
             <th class="px-2 text-secondary border-bottom-0 w-35">Detail</th>
             <th class="px-2 text-secondary border-bottom-0"></th>
@@ -43,20 +95,36 @@ $formID = rand(0, 100000);
                        class="form-control form-control-sm canvas-rx-title"
                        data-field="title" v-model="item.title" autofocus required>
             </td>
-            <td>
+            <td class="position-relative">
                 <input type="text" :data-index="index"
                        class="form-control form-control-sm"
                        data-field="strength" v-model="item.strength">
                 <input type="text" :data-index="index"
                        class="form-control form-control-sm"
-                       list="frequency-options"
+                       data-option-list="frequency-options"
                        data-field="frequency" v-model="item.frequency">
+                <div id="frequency-options" class="data-option-list">
+                    <div>Once a day</div>
+                    <div>Twice a day</div>
+                </div>
             </td>
             <td><textarea type="text" class="form-control form-control-sm"
                           rx-rte :data-index="index" data-field="detail"
                           v-model="item.detail"></textarea>
             </td>
-            <td class="px-2">
+            <td class="px-2 text-nowrap">
+                <a href="#"
+                   v-if="!isFavorite(item)" v-on:click.prevent="addToFavorites(item)"
+                   class="mt-1 d-inline-block mr-1 text-secondary on-hover-opaque"
+                   title="Add to favorites">
+                    <i class="fa fa-star"></i>
+                </a>
+                <a href="#"
+                   v-if="isFavorite(item)" v-on:click.prevent="removeFromFavorites(item)"
+                   class="mt-1 d-inline-block mr-1 text-warning"
+                   title="Remove from favorites">
+                    <i class="fa fa-star"></i>
+                </a>
                 <a href="#" v-on:click.prevent="removeItem(index)"
                    class="on-hover-opaque text-danger mt-1 d-inline-block">
                     <i class="fa fa-trash-alt"></i>
@@ -72,23 +140,25 @@ $formID = rand(0, 100000);
     >+ New Entry</button>
     </div>
 
-    <datalist id="frequency-options">
-        <option value="Once a day">
-        <option value="Twice a day">
-    </datalist>
-
 </div>
 <script>
     (function() {
+        let favorites = <?= json_encode($pro->favoritesByCategory('rx')) ?>;
+        for (let i = 0; i < favorites.length; i++) {
+            favorites[i].data = JSON.parse(favorites[i].data);
+        }
         function init() {
             window.clientRXApp = new Vue({
                 el: '#rxSection',
                 data: {
-                    items: <?= json_encode($contentData['items']) ?>
+                    items: <?= json_encode($contentData['items']) ?>,
+                    favorites: favorites,
                 },
                 mounted: function() {
                     this.initRTE();
                     this.initRxAutoSuggest();
+                    $('#rxSection [moe][initialized]').removeAttr('initialized');
+                    initMoes();
                 },
                 watch: {
                     $data: {
@@ -205,6 +275,97 @@ $formID = rand(0, 100000);
                             $(elem).attr('ac-initialized', 1);
                             $(strengthElem).attr('ac-initialized', 1);
                         });
+                    },
+                    isFavorite: function(_item) {
+                        let matches = this.favorites.filter(function(_x) {
+                            return _x.data.title === _item.title;
+                        });
+                        return matches.length ? matches[0].uid : false;
+                    },
+                    addToFavorites: function(_item) {
+                        if(!_item.title || !$.trim(_item.title)) return;
+                        if(this.isFavorite(_item)) return;
+                        let self = this;
+                        $.post('/api/proFavorite/create', {
+                            proUid: '{{$pro->uid}}',
+                            category: 'rx',
+                            data: JSON.stringify(_item)
+                        }, function(_data) {
+                            if(_data && _data.success) {
+                                self.favorites.push({
+                                    uid: _data.data,
+                                    category: 'rx',
+                                    data: _item
+                                });
+                                toastr.success('Added to favorites');
+                            }
+                            else {
+                                if(_data && _data.message) {
+                                    toastr.error(_data.message);
+                                }
+                                else {
+                                    toastr.error('Unable to add item to favorites');
+                                }
+                            }
+                        }, 'json');
+                    },
+                    removeFromFavorites: function(_item) {
+                        let uid = this.isFavorite(_item);
+                        if(!uid) return;
+                        let self = this;
+                        $.post('/api/proFavorite/remove', {
+                            uid: uid
+                        }, function(_data) {
+                            if(_data && _data.success) {
+                                self.favorites = self.favorites.filter(function (_x) {
+                                    return _x.uid !== uid;
+                                });
+                                toastr.success('Removed from favorites');
+                            }
+                            else {
+                                if(_data && _data.message) {
+                                    toastr.error(_data.message);
+                                }
+                                else {
+                                    toastr.error('Unable to remove item from favorites');
+                                }
+                            }
+                        }, 'json');
+                    },
+                    addFromFavorite: function(_favorite) {
+                        if(this.isFavoriteAdded(_favorite)) {
+                            for (let i = 0; i < this.items.length; i++) {
+                                if(this.items[i].title === _favorite.data.title) {
+                                    this.items.splice(i, 1);
+                                    return;
+                                }
+                            }
+                            return;
+                        }
+                        let item = _favorite.data;
+                        if(!this.items.length) {
+                            this.items.push(item);
+                        }
+                        else {
+                            let lastItem = this.items[this.items.length - 1];
+                            if(!lastItem.title) {
+                                this.items.splice(this.items.length - 1, 1, item);
+                            }
+                            else {
+                                this.items.push(item);
+                            }
+                        }
+                        let self = this;
+                        Vue.nextTick(function() {
+                            self.initRTE();
+                            self.initRxAutoSuggest();
+                        });
+                    },
+                    isFavoriteAdded: function(_favorite) {
+                        let matches = this.items.filter(function(_x) {
+                            return _x.title === _favorite.data.title;
+                        });
+                        return matches.length > 0;
                     }
                 }
             });

+ 9 - 2
resources/views/app/patient/canvas-sections/rx/summary.php

@@ -7,6 +7,9 @@ if($patient->canvas_data) {
     $canvasData = json_decode($patient->canvas_data, true);
     if(isset($canvasData["rx"])) {
         $contentData = $canvasData["rx"];
+        if(!isset($contentData['items'])){
+            $contentData['items'] = [];
+        }
     }
 }
 
@@ -22,8 +25,12 @@ if(count($contentData['items'])) {
                 <?= !!$item["strength"] ? '/&nbsp;' . $item["strength"] : '' ?>
                 <?= !!$item["frequency"] ? '/&nbsp;' . $item["frequency"] : '' ?>
             </div>
-            <?php if(isset($item["detail"]) && !empty($item["detail"])): ?>
-                <div class="text-secondary"><?= $item["detail"] ?></div>
+            <?php
+            $detailPlain = isset($item["detail"]) ? $item["detail"] : '';
+            $detailPlain = trim(strip_tags($detailPlain));
+            if(!empty($detailPlain)):
+            ?>
+                <div class="text-secondary"><?= $detailPlain ?></div>
             <?php endif; ?>
         </div>
 <?php

+ 91 - 21
resources/views/app/patient/canvas-sections/vitals/form.blade.php

@@ -1,7 +1,19 @@
 <?php
+$vitalLabels = [
+    "heightInInches" => "Ht. (in.)",
+    "weightPounds" => "Wt. (lbs.)",
+    "temperatureF" => "Temp. (F)",
+    "systolicBP" => "SBP",
+    "diastolicBP" => "DBP",
+    "pulseRatePerMinute" => "Pulse",
+    "respirationRatePerMinute" => "Resp.",
+    "pulseOx" => "Pulse Ox.",
+    "smokingStatus" => "Smoking Status",
+    "bmi" => "BMI (kg/m²)",
+];
 if(!$contentData) {
     $contentData = [
-        "heightInches" => [
+        "heightInInches" => [
             "label" => "Ht. (in.)",
             "value" => "",
             "date" => "",
@@ -16,28 +28,28 @@ if(!$contentData) {
             "value" => "",
             "date" => "",
         ],
-        "pulseRatePerMinute" => [
-            "label" => "Pulse",
+        "systolicBP" => [
+            "label" => "SBP",
             "value" => "",
             "date" => "",
         ],
-        "respirationRatePerMinute" => [
-            "label" => "Resp.",
+        "diastolicBP" => [
+            "label" => "DBP",
             "value" => "",
             "date" => "",
         ],
-        "pulseOx" => [
-            "label" => "Pulse Ox.",
+        "pulseRatePerMinute" => [
+            "label" => "Pulse",
             "value" => "",
             "date" => "",
         ],
-        "systolicBP" => [
-            "label" => "SBP",
+        "respirationRatePerMinute" => [
+            "label" => "Resp.",
             "value" => "",
             "date" => "",
         ],
-        "diastolicBP" => [
-            "label" => "DBP",
+        "pulseOx" => [
+            "label" => "Pulse Ox.",
             "value" => "",
             "date" => "",
         ],
@@ -53,6 +65,18 @@ if(!$contentData) {
         ],
     ];
 }
+else {
+    // ensure $contentData has all the expected vitals and correct labels!
+    foreach ($vitalLabels as $k => $v) {
+        if(!isset($contentData[$k])) {
+            $contentData[$k] = [
+                "label" => $v,
+                "value" => "",
+                "date" => "",
+            ];
+        }
+    }
+}
 
 $formID = rand(0, 100000);
 ?>
@@ -76,18 +100,67 @@ $formID = rand(0, 100000);
         </tr>
         </thead>
         <tbody>
-        <tr v-for="(item, index) in items">
+        @foreach($vitalLabels as $k => $v)
+            <tr>
+                <td>
+                    <input type="text" tabindex="-1"
+                           class="form-control form-control-sm events-none"
+                           data-field="title" value="{{ $v }}" readonly>
+                </td>
+                <td class="position-relative">
+                    @if($k === "bmi")
+                        <input type="text" readonly
+                               class="form-control form-control-sm vitals-title"
+                               data-field="value" v-model="bmi">
+                        <p class="py-1 m-0 px-2 font-weight-bold bg-white" v-if="!!bmi">
+                            <span class="text-sm text-warning-mellow" v-if="+bmi < 18.5">Underweight</span>
+                            <span class="text-sm text-success" v-if="+bmi >= 18.5 && +bmi < 25">Healthy Weight</span>
+                            <span class="text-sm text-warning-mellow" v-if="+bmi >= 25 && +bmi < 30">Overweight</span>
+                            <span class="text-sm text-warning-mellow" v-if="+bmi >= 30">Obese</span>
+                        </p>
+                    @elseif($k === "smokingStatus")
+                        <input type="text"
+                               class="form-control form-control-sm"
+                               data-field="value" v-model="items['smokingStatus'].value"
+                               data-option-list="smokingStatus"
+                               v-on:change="autoDate(items['smokingStatus'], 'smokingStatus')" v-on:keyup="autoDate(items['smokingStatus'], 'smokingStatus')">
+                        <div id="smoking-status-options" class="data-option-list">
+                            <div>Current</div>
+                            <div>Former</div>
+                            <div>Never</div>
+                        </div>
+                    @else
+                        <input type="text"
+                               class="form-control form-control-sm"
+                               data-field="value" v-model="items['{{ $k }}'].value"
+                               v-on:change="autoDate(items['{{ $k }}'], '{{ $k }}')" v-on:keyup="autoDate(items['{{ $k }}'], '{{ $k }}')">
+                    @endif
+                </td>
+                <td>
+                    <input type="date" {{ $k === 'bmi' ? 'readonly' : '' }}
+                           class="form-control form-control-sm vitals-title"
+                           data-field="date" v-model="items['{{ $k }}'].date">
+                </td>
+            </tr>
+        @endforeach
+
+        {{--<tr v-for="(item, index) in items">
             <td>
                 <input type="text" :data-index="index" tabindex="-1"
                        class="form-control form-control-sm events-none"
                        data-field="title" v-model="item.label" readonly>
             </td>
-            <td>
+            <td class="position-relative">
                 <input type="text" :data-index="index" v-if="index !== 'bmi'"
                        class="form-control form-control-sm"
                        data-field="value" v-model="item.value"
-                       :list="index === 'smokingStatus' ? 'smoking-status-options' : ''"
+                       :data-option-list="index === 'smokingStatus'"
                        v-on:change="autoDate(item, index)" v-on:keyup="autoDate(item, index)">
+                <div id="smoking-status-options" class="data-option-list">
+                    <div>Current</div>
+                    <div>Former</div>
+                    <div>Never</div>
+                </div>
                 <input type="text" :data-index="index" v-if="index === 'bmi'" readonly
                        class="form-control form-control-sm vitals-title"
                        data-field="value" v-model="bmi">
@@ -103,14 +176,9 @@ $formID = rand(0, 100000);
                        class="form-control form-control-sm vitals-title"
                        data-field="date" v-model="item.date">
             </td>
-        </tr>
+        </tr>--}}
         </tbody>
     </table>
-    <datalist id="smoking-status-options">
-        <option value="Current">
-        <option value="Former">
-        <option value="Never">
-    </datalist>
 </div>
 <script>
     (function() {
@@ -140,10 +208,12 @@ $formID = rand(0, 100000);
                 computed: {
                     bmi: function () {
                         let result = '';
-                        let h = this.items.heightInches.value, w = this.items.weightPounds.value;
+                        let h = this.items.heightInInches, w = this.items.weightPounds;
                         if(!h || !w) {
                             return result;
                         }
+                        h = h.value;
+                        w = w.value;
                         try {
                             h = parseFloat(h);
                             w = parseFloat(w);

+ 42 - 14
resources/views/app/patient/canvas-sections/vitals/summary.php

@@ -1,7 +1,18 @@
 <?php
-
+$vitalLabels = [
+    "heightInInches" => "Ht. (in.)",
+    "weightPounds" => "Wt. (lbs.)",
+    "temperatureF" => "Temp. (F)",
+    "systolicBP" => "SBP",
+    "diastolicBP" => "DBP",
+    "pulseRatePerMinute" => "Pulse",
+    "respirationRatePerMinute" => "Resp.",
+    "pulseOx" => "Pulse Ox.",
+    "smokingStatus" => "Smoking Status",
+    "bmi" => "BMI (kg/m²)",
+];
 $contentData = [
-    "heightInches" => [
+    "heightInInches" => [
         "label" => "Ht. (in.)",
         "value" => "",
         "date" => "",
@@ -16,28 +27,28 @@ $contentData = [
         "value" => "",
         "date" => "",
     ],
-    "pulseRatePerMinute" => [
-        "label" => "Pulse",
+    "systolicBP" => [
+        "label" => "SBP",
         "value" => "",
         "date" => "",
     ],
-    "respirationRatePerMinute" => [
-        "label" => "Resp.",
+    "diastolicBP" => [
+        "label" => "DBP",
         "value" => "",
         "date" => "",
     ],
-    "pulseOx" => [
-        "label" => "Pulse Ox.",
+    "pulseRatePerMinute" => [
+        "label" => "Pulse",
         "value" => "",
         "date" => "",
     ],
-    "systolicBP" => [
-        "label" => "SBP",
+    "respirationRatePerMinute" => [
+        "label" => "Resp.",
         "value" => "",
         "date" => "",
     ],
-    "diastolicBP" => [
-        "label" => "DBP",
+    "pulseOx" => [
+        "label" => "Pulse Ox.",
         "value" => "",
         "date" => "",
     ],
@@ -57,14 +68,31 @@ if($patient->canvas_data) {
     $canvasData = json_decode($patient->canvas_data, true);
     if(isset($canvasData["vitals"])) {
         $contentData = $canvasData["vitals"];
+        // ensure $contentData has all the expected vitals and correct labels!
+        foreach ($vitalLabels as $k => $v) {
+            if(!isset($contentData[$k])) {
+                $contentData[$k] = [
+                    "label" => $v,
+                    "value" => "",
+                    "date" => "",
+                ];
+            }
+        }
+
     }
 }
 
-foreach ($contentData as $k => $vital) {
+foreach ($vitalLabels as $k => $v) {
 ?>
     <div class="d-flex vital-item align-items-center">
         <span class="content-html text-nowrap">
-            <span><?= $vital["label"] ?>:</span>
+            <span><?= $v ?>:</span>
+            <?php
+            $vital = [];
+            if(isset($contentData[$k])) {
+                $vital = $contentData[$k];
+            }
+            ?>
             <b><?= isset($vital["value"]) && !empty($vital["value"]) ? $vital["value"] : '-' ?></b>
             <?php if($k === 'bmi' && isset($vital["value"]) && !empty($vital["value"])):
                 $bmi = floatval($vital["value"]);

+ 7 - 155
resources/views/app/patient/dashboard.blade.php

@@ -30,9 +30,6 @@
                 {{-- appointments --}}
                 @include('app/patient/partials/appointments')
 
-                {{-- allergies --}}
-                {{--@include('app/patient/partials/allergies')--}}
-
                 {{-- canvas based allergies --}}
                 <div class="pt-2 mt-2 border-top">
                     <div class="d-flex align-items-center pb-2">
@@ -45,9 +42,6 @@
                     </div>
                 </div>
 
-                {{-- rx --}}
-                {{--@include('app/patient/partials/rx')--}}
-
                 {{-- canvas based rx --}}
                 <div class="pt-2 mt-2 border-top">
                     <div class="d-flex align-items-center pb-2">
@@ -60,6 +54,11 @@
                     </div>
                 </div>
 
+                {{-- programs --}}
+                @if($pro->pro_type == 'ADMIN')
+                    @include('app/patient/partials/programs')
+                @endif
+
                 {{-- devices --}}
                 <?php $availableDevices = 0; ?>
                 @foreach($devices as $device)
@@ -172,157 +171,10 @@
                 </div>
 
                 {{-- device measurements --}}
-                <div class="mt-2 pb-1">
-                    <div class="d-flex align-items-center mb-2 py-2 border-top border-bottom">
-                        <h6 class="my-0 font-weight-bold text-secondary">Cellular Measurements</h6>
-                        <span class="mx-2 text-secondary">|</span>
-                        <a start show class="py-0 font-weight-normal"
-                           href="/patients/view/{{ $patient->uid }}/measurements">
-                            View All
-                        </a>
-                    </div>
-                    <table class="table table-sm border-0 my-0 mx-2">
-                        <tbody>
-                        @foreach($patient->deviceMeasurements as $measurement)
-                            <tr>
-                                <td class="text-black p-0 border-0">
-                                    <div class="pb-1 d-flex align-items-center">
-                                        <span class="text-secondary mr-2">{{ $measurement->measurement->device_category }}:</span>
-                                        @if($measurement->measurement->device_category === 'WEIGHT')
-                                            <b>{{ round(($measurement->measurement->value_weight / 1000)*2.20462, 1) }} lb</b>
-                                        @elseif($measurement->measurement->device_category === 'BP')
-                                            <b>{{ $measurement->measurement->systolic_bp_in_mm_hg }}/{{ $measurement->measurement->diastolic_bp_in_mm_hg }} mmHg</b>
-                                        @endif
-                                        <span class="font-weight-normal text-secondary ml-2 text-sm">({{ friendly_date_time($measurement->measurement->created_at, true) }})</span>
-                                        <span class="ml-2 text-sm {{$measurement->status === 'NEW' ? 'text-info' : ($measurement->status === 'ACCEPTED' ? 'text-success' : 'text-sacondary')}}">{{$measurement->status}}</span>
-                                        <span>
-                                            <span moe class="ml-2" relative>
-                                                <a href="#" start show><i class="on-hover-opaque font-size-11 fa fa-edit text-primary"></i></a>
-                                                <form url="/api/clientBdtMeasurement/updateStatus" right>
-                                                    <input type="hidden" name="uid" value="{{ $measurement->uid }}">
-                                                    <div class="mb-2">
-                                                        <select name="status" class="form-control form-control-sm" required>
-                                                            <option value="">-- select --</option>
-                                                            <option {{$measurement->status === 'NEW' ? 'selected' : ''  }} value="NEW">NEW</option>
-                                                            <option {{$measurement->status === 'ACCEPTED' ? 'selected' : ''  }} value="ACCEPTED">ACCEPTED</option>
-                                                            <option {{$measurement->status === 'REJECTED' ? 'selected' : ''  }} value="REJECTED">REJECTED</option>
-                                                        </select>
-                                                    </div>
-                                                    <div class="mb-2">
-                                                        <input type="text" name="statusMemo" placeholder="Status memo" class="form-control form-control-sm">
-                                                    </div>
-                                                    <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>
-                                            </span>
-                                        </span>
-                                    </div>
-                                </td>
-                            </tr>
-                        @endforeach
-                        @if(!$patient->deviceMeasurements || count($patient->deviceMeasurements) === 0)
-                            <tr>
-                                <td class="text-secondary p-0 border-0">
-                                    No measurements
-                                </td>
-                            </tr>
-                        @endif
-                        </tbody>
-                    </table>
-                </div>
-
+                @include('app/patient/partials/device-measurements')
 
                 {{-- measurements --}}
-                <div class="mt-2 pb-1">
-                    <div class="d-flex align-items-center mb-2 py-2 border-top border-bottom">
-                        <h6 class="my-0 font-weight-bold text-secondary">Measurements</h6>
-                        <span class="mx-2 text-secondary">|</span>
-                        <div moe>
-                            <a start show class="py-0 font-weight-normal">Add</a>
-                            <form url="/api/measurement/create">
-                                <input type="hidden" name="clientUid" value="{{ $patient->uid }}">
-                                <div class="mb-2">
-                                    <input required autofocus type="text" class="form-control form-control-sm" name="label" value="" placeholder="Type">
-                                </div>
-                                <div class="mb-2">
-                                    <input required type="text" class="form-control form-control-sm" name="value" value="" placeholder="Value">
-                                </div>
-                                <div class="mb-2">
-                                    <input required type="date" class="form-control form-control-sm" name="effectiveDate" max="{{ date('Y-m-d') }}" value="{{ date('Y-m-d') }}">
-                                </div>
-                                <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>
-                        <span class="mx-2 text-secondary">|</span>
-                        <a start show class="py-0 font-weight-normal"
-                           href="/patients/view/{{ $patient->uid }}/measurements">
-                            View All
-                        </a>
-                    </div>
-                    <table class="table table-sm border-0 m-0">
-                        <tbody>
-                        @foreach($patient->measurements as $measurement)
-                            @if(!empty($measurement->label) && !in_array($measurement->label, $vitalLabels))
-                                <tr>
-                                    <td class="text-black p-0 border-0">
-                                        <div class="d-flex align-items-center">
-                                            <div moe relative class="mr-2">
-                                                <a class="on-hover-opaque" start show title="Delete">
-                                                    <i class="font-size-11 fa fa-trash-alt text-danger"></i>
-                                                </a>
-                                                <form url="/api/measurement/remove">
-                                                    <input type="hidden" name="uid" value="{{ $measurement->uid }}">
-                                                    <p class="small min-width-200px">Are you sure you want to delete this entry?</p>
-                                                    <div class="d-flex align-items-center">
-                                                        <button class="btn btn-sm btn-danger mr-2" submit>Delete</button>
-                                                        <button class="btn btn-sm btn-default mr-2 border" cancel>Cancel</button>
-                                                    </div>
-                                                </form>
-                                            </div>
-                                            <div moe class="mr-2">
-                                                <a class="on-hover-opaque" start show title="Update">
-                                                    <i class="font-size-11 fa fa-edit text-primary"></i>
-                                                </a>
-                                                <form url="/api/measurement/create">
-                                                    <input type="hidden" name="clientUid" value="{{ $patient->uid }}">
-                                                    <div class="mb-2">
-                                                        <input required type="text" class="form-control form-control-sm" name="label" value="{{ $measurement->label }}" placeholder="Type">
-                                                    </div>
-                                                    <div class="mb-2">
-                                                        <input required autofocus type="text" class="form-control form-control-sm" name="value" value="{{ $measurement->value }}" placeholder="Value">
-                                                    </div>
-                                                    <div class="mb-2">
-                                                        <input required type="date" class="form-control form-control-sm" name="effectiveDate" max="{{ date('Y-m-d') }}" value="{{ date('Y-m-d') }}">
-                                                    </div>
-                                                    <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>
-                                            <span>{{ $measurement->label }}:</span>
-                                            <span class="font-weight-bold ml-1">{{ $measurement->value }}</span>
-                                            <span class="font-weight-normal text-secondary ml-2 text-sm">(as on {{ friendly_date_time($measurement->effective_date, false) }})</span>
-                                        </div>
-                                    </td>
-                                </tr>
-                            @endif
-                        @endforeach
-                        @if(!$patient->measurements || !count($patient->measurements) === 0)
-                            <tr>
-                                <td class="text-secondary p-0 border-0">
-                                    No items to show
-                                </td>
-                            </tr>
-                        @endif
-                        </tbody>
-                    </table>
-                </div>
+                @include('app/patient/partials/measurements')
 
                 {{-- vitals --}}
                 {{--@include('app/patient/partials/vitals')--}}

+ 1 - 1
resources/views/app/patient/memos.blade.php

@@ -39,7 +39,7 @@
                 @foreach($patient->memos as $memo)
                     <tr>
                         <td class="px-2">{{ $memo->category }}</td>
-                        <td class="px-2"><pre class="m-0">{{ $memo->content }}</pre></td>
+                        <td class="px-2"><pre class="m-0 break-spaces">{{ $memo->content }}</pre></td>
                         <td class="px-2">{{ friendly_date_time($memo->created_at) }}</td>
                         <td class="px-2 text-center delete-column">
                             <div moe wide relative class="mr-2">

+ 22 - 21
resources/views/app/patient/note/_cancel-signed-note.blade.php

@@ -1,23 +1,24 @@
 @if(!$note->is_cancelled && $note->is_signed_by_hcp)
-    <span class="mx-2 text-secondary">|</span>
-    <span moe relatve
-         class="{{ $hasBills ? 'moe-disabled' : '' }}"
-         title="{{ $hasBills ? 'Cannot cancel note since it has un-cancelled bills in it' : '' }}">
-        <a class="text-danger font-weight-bold" href="" show start>
-            <i class="fa fa-exclamation-triangle"></i>
-            Cancel Signed Note
-        </a>
-        <form url="/api/note/cancel">
-            <input type="hidden" name="uid" value="{{$note->uid}}">
-            <p class="small mb-1 font-weight-bold">This note has been already signed by the HCP.</p>
-            <p class="mb-2">Do you still want to cancel this note?</p>
-            <div class="mb-2">
-                <textarea name="memo" id="" cols="30" rows="5" placeholder="Memo (required to cancel signed note)" class="memo-textarea form-control form-control-sm" required></textarea>
-            </div>
-            <div class="d-flex align-items-center">
-                <button class="btn btn-sm btn-danger mr-2" submit>Yes</button>
-                <button class="btn btn-sm btn-default mr-2 border" cancel>No</button>
-            </div>
-        </form>
-    </span>
+    <div class="border-top p-3">
+        <div moe relatve
+             class="{{ $hasBills ? 'moe-disabled' : '' }}"
+             title="{{ $hasBills ? 'Cannot cancel note since it has un-cancelled bills in it' : '' }}">
+            <a class="text-danger font-weight-normal on-hover-opaque" href="" show start>
+                <i class="fa fa-exclamation-triangle"></i>
+                Cancel Signed Note
+            </a>
+            <form url="/api/note/cancel">
+                <input type="hidden" name="uid" value="{{$note->uid}}">
+                <p class="small mb-1 font-weight-bold">This note has been already signed by the HCP.</p>
+                <p class="mb-2">Do you still want to cancel this note?</p>
+                <div class="mb-2">
+                    <textarea name="memo" id="" cols="30" rows="5" placeholder="Memo (required to cancel signed note)" class="memo-textarea form-control form-control-sm" required></textarea>
+                </div>
+                <div class="d-flex align-items-center">
+                    <button class="btn btn-sm btn-danger mr-2" submit>Yes</button>
+                    <button class="btn btn-sm btn-default mr-2 border" cancel>No</button>
+                </div>
+            </form>
+        </div>
+    </div>
 @endif

+ 4 - 2
resources/views/app/patient/note/dashboard.blade.php

@@ -274,7 +274,9 @@
                         $shortcuts = "";
                         $latestSectionTS = 0;
                         ?>
+                        <div class="{{ $note->is_signed_by_hcp ? 'note-signed-by-hcp' : '' }}">
                         @include('app.patient.note.note-section-list')
+                        </div>
                     </div>
                 </div>
 
@@ -285,7 +287,6 @@
                         <div class="d-flex align-items-center mb-2">
                             <p class="font-weight-bold text-secondary m-0">Bills</p>
                             @include('app/patient/note/_create-bill')
-                            @include('app/patient/note/_cancel-signed-note')
                         </div>
                         <table class="table table-sm tabe-striped mb-3 border-left border-right border-bottom">
                             <thead class="bg-light">
@@ -391,7 +392,6 @@
                     <div class="my-3 px-3 d-flex">
                         <p class="font-weight-bold mb-0 text-secondary">No bills in this note</p>
                         @include('app/patient/note/_create-bill')
-                        @include('app/patient/note/_cancel-signed-note')
                     </div>
                 @endif
 
@@ -457,6 +457,8 @@
                     @endif
                 </div>
 
+                @include('app/patient/note/_cancel-signed-note')
+
             </div>
         </div>
     </div>

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

@@ -227,6 +227,7 @@
                 $(document)
                     .off('mousedown.enable-edit', '.note-section:not(.edit)')
                     .on('mousedown.enable-edit', '.note-section:not(.edit)', function(e) {
+                        if($(this).closest('.note-signed-by-hcp').length) return;
                         e.stopPropagation();
                         e.preventDefault();
                         $(this).find('.edit-trigger').first().click();

+ 1 - 1
resources/views/app/patient/notes.blade.php

@@ -35,7 +35,7 @@
         </h6>
     </div>
 
-    <table class="table table-sm table-striped table-bordered mb-0 mt-2">
+    <table class="table table-sm table-striped table-bordered mb-0 mt-2 notes-list">
         <thead>
         <tr>
             <th class="px-2 text-secondary border-bottom-0">Effective Date</th>

+ 77 - 0
resources/views/app/patient/partials/device-measurements.blade.php

@@ -0,0 +1,77 @@
+<div class="mt-2 pb-1">
+    <div class="d-flex align-items-center mb-2 py-2 border-top border-bottom">
+        <h6 class="my-0 font-weight-bold text-secondary">Cellular Measurements</h6>
+        <span class="mx-2 text-secondary">|</span>
+        <a start show class="py-0 font-weight-normal"
+           href="/patients/view/{{ $patient->uid }}/measurements">
+            View All
+        </a>
+    </div>
+    <table class="table table-sm border-0 my-0 mx-2">
+        <tbody>
+        <?php
+        $validCount = 0;
+        foreach($patient->deviceMeasurements as $measurement) {
+            $displayMeasurement = true;
+            if($measurement->measurement->device_category === 'WEIGHT') {
+                if(round(($measurement->measurement->value_weight / 1000)*2.20462, 1) <= 0) {
+                    $displayMeasurement = false;
+                }
+            }
+            else if($measurement->measurement->device_category === 'BP') {
+                if(floatval($measurement->measurement->systolic_bp_in_mm_hg) <= 0 || floatval($measurement->measurement->diastolic_bp_in_mm_hg) <= 0) {
+                    $displayMeasurement = false;
+                }
+            }
+            if($displayMeasurement) {
+                $validCount++;
+            ?>
+            <tr>
+                <td class="text-black p-0 border-0">
+                    <div class="pb-1 d-flex align-items-center">
+                        <span class="text-secondary mr-2">{{ $measurement->measurement->device_category }}:</span>
+                        @if($measurement->measurement->device_category === 'WEIGHT')
+                            <b>{{ round(($measurement->measurement->value_weight / 1000)*2.20462, 1) }} lb</b>
+                        @elseif($measurement->measurement->device_category === 'BP')
+                            <b>{{ $measurement->measurement->systolic_bp_in_mm_hg }}/{{ $measurement->measurement->diastolic_bp_in_mm_hg }} mmHg</b>
+                        @endif
+                        <span class="font-weight-normal text-secondary ml-2 text-sm">({{ friendly_date_time($measurement->measurement->created_at, true) }})</span>
+                        <span class="ml-2 text-sm {{$measurement->status === 'NEW' ? 'text-info' : ($measurement->status === 'ACCEPTED' ? 'text-success' : 'text-sacondary')}}">{{$measurement->status}}</span>
+                        <span>
+                                        <span moe class="ml-2" relative>
+                                            <a href="#" start show><i class="on-hover-opaque font-size-11 fa fa-edit text-primary"></i></a>
+                                            <form url="/api/clientBdtMeasurement/updateStatus" right>
+                                                <input type="hidden" name="uid" value="{{ $measurement->uid }}">
+                                                <div class="mb-2">
+                                                    <select name="status" class="form-control form-control-sm" required>
+                                                        <option value="">-- select --</option>
+                                                        <option {{$measurement->status === 'NEW' ? 'selected' : ''  }} value="NEW">NEW</option>
+                                                        <option {{$measurement->status === 'ACCEPTED' ? 'selected' : ''  }} value="ACCEPTED">ACCEPTED</option>
+                                                        <option {{$measurement->status === 'REJECTED' ? 'selected' : ''  }} value="REJECTED">REJECTED</option>
+                                                    </select>
+                                                </div>
+                                                <div class="mb-2">
+                                                    <input type="text" name="statusMemo" placeholder="Status memo" class="form-control form-control-sm">
+                                                </div>
+                                                <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>
+                                        </span>
+                                    </span>
+                    </div>
+                </td>
+            </tr>
+        <?php }
+        }
+        if(!$validCount) { ?>
+            <tr>
+                <td class="text-secondary p-0 border-0">
+                    No measurements
+                </td>
+            </tr>
+        <?php } ?>
+        </tbody>
+    </table>
+</div>

+ 174 - 0
resources/views/app/patient/partials/equipment.blade.php

@@ -0,0 +1,174 @@
+<div id="equipmentApp" v-cloak>
+    <div class="d-flex align-items-center pb-2">
+        <h4 class="font-weight-bold m-0 font-size-14"><i class="fa fa-car-battery mr-2"></i>Equipment/Device</h4>
+        <span class="mx-2 text-secondary">|</span>
+        <a class="py-0 font-weight-normal c-pointer" v-on:click.prevent="showPopup('equipment-popup')">Add</a>
+    </div>
+    <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
+        <thead>
+        <tr class="bg-light">
+            <th class="px-2 text-secondary border-bottom-0 width-30px">#</th>
+            <th class="px-2 text-secondary border-bottom-0">Items</th>
+            <th class="px-2 text-secondary border-bottom-0">Purpose</th>
+            <th class="px-2 text-secondary border-bottom-0">Memo</th>
+            <th class="px-2 text-secondary border-bottom-0">&nbsp;</th>
+        </tr>
+        </thead>
+        <tbody>
+            <tr v-for="(item, index) in items" :class="item.is_open ? '' : 'opacity-60'">
+                <td class="px-2">@{{ index + 1 }}</td>
+                <td class="px-2" v-html="item.items && Array.isArray(item.items) ? item.items.join('<br>') : item.items"></td>
+                <td class="px-2">@{{item.purpose}}</td>
+                <td class="px-2">@{{item.memo}}</td>
+                <td class="px-2 text-nowrap">
+                    <a class="mr-2 c-pointer" v-on:click.prevent="showPopup('equipment-popup', item)">Edit</a>
+                    <a class="mr-2 c-pointer" v-if="item.is_open" v-on:click.prevent="closeItem(item)">Close</a>
+                    <a class="mr-2 c-pointer" v-if="!item.is_open" v-on:click.prevent="openItem(item)">Open</a>
+                </td>
+            </tr>
+        </tbody>
+    </table>
+    <div class="stag-popup stag-popup-sm mcp-theme-1" stag-popup-key="equipment-popup">
+        <form method="POST" action="/api/appointment/create">
+            <h3 class="stag-popup-title mb-2">
+                <span>@{{ popupMode === 'add' ? 'Add Equipment' : 'Edit Equipment' }}</span>
+                <a href="#" class="ml-auto text-secondary"
+                   onclick="return closeStagPopup()"><i class="fa fa-times-circle"></i></a>
+            </h3>
+            <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="popupItem.items.push('')">Add</a>
+                </div>
+                <div class="d-flex align-items-center mb-2" v-for="(item, itemIndex) in popupItem.items">
+                    <div class="position-relative flex-grow-1">
+                        <input required type="text" data-option-list v-model="popupItem.items[itemIndex]" class="form-control form-control-sm">
+                        <div class="data-option-list">
+                            <div>Weight Scale</div>
+                            <div>Pulse Ox</div>
+                            <div>Temperature Gun</div>
+                        </div>
+                    </div>
+                    <a v-if="popupItem.items.length > 1" class="ml-2 text-danger" href="#" v-on:click.prevent="popupItem.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="popupItem.purpose" class="form-control form-control-sm">
+            </div>
+            <div class="mb-2">
+                <label class="text-sm text-secondary mb-1">Memo</label>
+                <input type="text" v-model="popupItem.memo" class="form-control form-control-sm">
+            </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="savePopupItem()">Submit</button>
+                <button type="button" class="btn btn-sm btn-default border" onclick="return closeStagPopup()">Cancel</button>
+            </div>
+        </form>
+    </div>
+</div>
+<script>
+    (function() {
+        <?php
+        $tickets = $patient->tickets->filter(function($_item) {
+            return $_item->category === 'equipment';
+        });
+        $items = [];
+        foreach ($tickets as $ticket) {
+            $item = json_decode($ticket->data);
+            $item->uid = $ticket->uid;
+            $item->is_open = $ticket->is_open;
+            $items[] = $item;
+        }
+        ?>
+        function init() {
+            let items = [];
+            let equipmentApp = new Vue({
+                el: '#equipmentApp',
+                delimiters: ['@{{', '}}'],
+                data: {
+                    popupMode: 'add',
+                    items: {!! json_encode($items) !!},
+                    popupItem: {
+                        uid: '',
+                        is_open: true,
+                        items: [''],
+                        purpose: '',
+                        memo: '',
+                    },
+                },
+                methods: {
+                    showPopup: function(_name, _item) {
+                        closeStagPopup();
+                        this.popupMode = _item ? 'edit' : 'add';
+                        this.popupItem = _item ? JSON.parse(JSON.stringify(_item)) : {
+                            uid: '',
+                            is_open: true,
+                            items: [''],
+                            purpose: '',
+                            memo: '',
+                        };
+                        showStagPopup('equipment-popup', true);
+                    },
+                    savePopupItem: function() {
+                        let form = $('#equipmentApp form').first();
+                        if(!form[0].checkValidity()) {
+                            form[0].reportValidity();
+                            return false;
+                        }
+
+                        showMask();
+                        let payload = {};
+                        if(this.popupMode === '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.popupItem);
+                        }
+                        else {
+                            payload.uid = this.popupItem.uid;
+                            payload.newData = JSON.stringify(this.popupItem);
+                        }
+
+                        $.post(
+                            '/api/ticket/' + (this.popupMode === 'add' ? 'create' : 'updateData'),
+                            payload,
+                            function(_data) {
+                                console.log(_data);
+                                fastReload();
+                            },
+                            'json');
+
+                        return false;
+                    },
+                    closeItem: function(_item) {
+                        showMask();
+                        $.post('/api/ticket/close', {
+                            uid: _item.uid
+                        }, function(_data) {
+                            fastReload();
+                        });
+                    },
+                    openItem: function(_item) {
+                        showMask();
+                        $.post('/api/ticket/open', {
+                            uid: _item.uid
+                        }, function(_data) {
+                            fastReload();
+                        });
+                    }
+                },
+                mounted: function () {
+
+                }
+            })
+        }
+        addMCInitializer('equipment', init, '#equipmentApp');
+    })();
+</script>

+ 332 - 0
resources/views/app/patient/partials/erx.blade.php

@@ -0,0 +1,332 @@
+<div id="erxApp" v-cloak>
+    <div class="d-flex align-items-center pb-2">
+        <h4 class="font-weight-bold m-0 font-size-14"><i class="fa fa-prescription mr-2"></i>ERx</h4>
+        <span class="mx-2 text-secondary">|</span>
+        <a class="py-0 font-weight-normal c-pointer" v-on:click.prevent="showPopup('erx-popup')">Add</a>
+    </div>
+    <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
+        <thead>
+        <tr class="bg-light">
+            <th class="px-2 text-secondary border-bottom-0 width-30px">#</th>
+            <th class="px-2 text-secondary border-bottom-0">Medication</th>
+            <th class="px-2 text-secondary border-bottom-0">Strength</th>
+            <th class="px-2 text-secondary border-bottom-0">Amount</th>
+            <th class="px-2 text-secondary border-bottom-0">Route</th>
+            <th class="px-2 text-secondary border-bottom-0">Frequency</th>
+            <th class="px-2 text-secondary border-bottom-0">Dispense</th>
+            <th class="px-2 text-secondary border-bottom-0">Refills</th>
+            <th class="px-2 text-secondary border-bottom-0">Purpose</th>
+            <th class="px-2 text-secondary border-bottom-0">Pharmacy</th>
+            <th class="px-2 text-secondary border-bottom-0">&nbsp;</th>
+        </tr>
+        </thead>
+        <tbody>
+            <tr v-for="(item, index) in items" :class="item.is_open ? '' : 'opacity-60'">
+                <td class="px-2">@{{ index + 1 }}</td>
+                <td class="px-2">@{{item.medication}}</td>
+                <td class="px-2">@{{item.strength}}</td>
+                <td class="px-2">@{{item.amount}}</td>
+                <td class="px-2">@{{item.route}}</td>
+                <td class="px-2">@{{item.frequency}}</td>
+                <td class="px-2">@{{item.dispense}} <span class="text-secondary" v-html="inWords(item.dispense)"></span></td>
+                <td class="px-2">@{{item.refills}}</td>
+                <td class="px-2">@{{item.purpose}}</td>
+                <td class="px-2">@{{item.pharmacy}}</td>
+                <td class="px-2 text-nowrap">
+                    <a class="mr-2 c-pointer" v-on:click.prevent="showPopup('erx-popup', item)">Edit</a>
+                    <a class="mr-2 c-pointer" v-if="item.is_open" v-on:click.prevent="closeItem(item)">Close</a>
+                    <a class="mr-2 c-pointer" v-if="!item.is_open" v-on:click.prevent="openItem(item)">Open</a>
+                </td>
+            </tr>
+        </tbody>
+    </table>
+    <div class="stag-popup stag-popup-sm mcp-theme-1" stag-popup-key="erx-popup">
+        <form method="POST" action="/api/appointment/create">
+            <h3 class="stag-popup-title mb-2">
+                <span>@{{ popupMode === 'add' ? 'Add ERx Item' : 'Edit ERx Item' }}</span>
+                <a href="#" class="ml-auto text-secondary"
+                   onclick="return closeStagPopup()"><i class="fa fa-times-circle"></i></a>
+            </h3>
+            <div class="row mb-2">
+                <div class="col-6">
+                    <label class="text-sm text-secondary mb-1">Medication</label>
+                    <input required type="text" data-field="medication"
+                           v-model="popupItem.medication" class="form-control form-control-sm">
+                </div>
+                <div class="col-6">
+                    <label class="text-sm text-secondary mb-1">Strength</label>
+                    <input type="text" data-field="strength"
+                           v-model="popupItem.strength" class="form-control form-control-sm">
+                </div>
+            </div>
+            <div class="row mb-2">
+                <div class="col-6">
+                    <label class="text-sm text-secondary mb-1">Amount</label>
+                    <input type="text" v-model="popupItem.amount" class="form-control form-control-sm">
+                </div>
+                <div class="col-6">
+                    <label class="text-sm text-secondary mb-1">Route</label>
+                    <input required type="text" v-model="popupItem.route" class="form-control form-control-sm"
+                           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>
+            <div class="row mb-2">
+                <div class="col-6">
+                    <label class="text-sm text-secondary mb-1">Frequency</label>
+                    <input type="text" v-model="popupItem.frequency" class="form-control form-control-sm"
+                           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>
+            <div class="row mb-2">
+                <div class="col-6">
+                    <label class="text-sm text-secondary mb-1">Dispense</label>
+                    <input required type="number" v-model="popupItem.dispense" class="form-control form-control-sm">
+                </div>
+                <div class="col-6">
+                    <label class="text-sm text-secondary mb-1">Refills</label>
+                    <input type="number" v-model="popupItem.refills" class="form-control form-control-sm">
+                </div>
+            </div>
+            <div class="row mb-2">
+                <div class="col-12">
+                    <label class="text-sm text-secondary mb-1">Purpose</label>
+                    <input required type="text" v-model="popupItem.purpose" class="form-control form-control-sm">
+                </div>
+            </div>
+            <hr class="mt-3 mb-2">
+
+            <div class="row mb-2">
+                <div class="col-12">
+                    <label class="text-sm text-secondary mb-1 font-weight-bold">Preferred Pharmacy</label>
+                </div>
+            </div>
+            <div class="row mb-2">
+                <div class="col-8">
+                    <label class="text-sm text-secondary mb-1">Business Name</label>
+                    <input type="text" v-model="popupItem.pharmacyName" class="form-control form-control-sm">
+                </div>
+            </div>
+            <div class="row mb-2">
+                <div class="col-6">
+                    <label class="text-sm text-secondary mb-1">City</label>
+                    <input type="text" v-model="popupItem.pharmacyCity" class="form-control form-control-sm">
+                </div>
+                <div class="col-6">
+                    <label class="text-sm text-secondary mb-1">State</label>
+                    <input type="text" v-model="popupItem.pharmacyState" class="form-control form-control-sm">
+                </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" v-model="popupItem.pharmacyAddressMemo" class="form-control form-control-sm">
+                </div>
+            </div>
+            <div class="row mb-2">
+                <div class="col-6">
+                    <label class="text-sm text-secondary mb-1">Phone</label>
+                    <input type="text" v-model="popupItem.pharmacyPhone" class="form-control form-control-sm">
+                </div>
+                <div class="col-6">
+                    <label class="text-sm text-secondary mb-1">Fax</label>
+                    <input type="text" v-model="popupItem.pharmacyFax" class="form-control form-control-sm">
+                </div>
+            </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="savePopupItem()">Submit</button>
+                <button type="button" class="btn btn-sm btn-default border" onclick="return closeStagPopup()">Cancel</button>
+            </div>
+        </form>
+    </div>
+</div>
+<script>
+    (function() {
+        <?php
+        $tickets = $patient->tickets->filter(function($_item) {
+            return $_item->category === 'erx';
+        });
+        $items = [];
+        foreach ($tickets as $ticket) {
+            $item = json_decode($ticket->data);
+            $item->uid = $ticket->uid;
+            $item->is_open = $ticket->is_open;
+            $items[] = $item;
+        }
+        ?>
+        function init() {
+            let items = [];
+            let erxApp = new Vue({
+                el: '#erxApp',
+                delimiters: ['@{{', '}}'],
+                data: {
+                    popupMode: 'add',
+                    items: {!! json_encode($items) !!},
+                    popupItem: {
+                        uid: '',
+                        is_open: true,
+                        medication: '',
+                        strength: '',
+                        amount: '',
+                        route: '',
+                        frequency: '',
+                        dispense: '',
+                        refills: '',
+                        purpose: '',
+                        pharmacyName: '',
+                        pharmacyCity: '',
+                        pharmacyState: '',
+                        pharmacyAddressMemo: '',
+                        pharmacyPhone: '',
+                        pharmacyFax: '',
+                    }
+                },
+                methods: {
+                    showPopup: function(_name, _item) {
+                        closeStagPopup();
+                        this.popupMode = _item ? 'edit' : 'add';
+                        this.popupItem = _item ? JSON.parse(JSON.stringify(_item)) : {
+                            uid: '',
+                            is_open: true,
+                            medication: '',
+                            strength: '',
+                            amount: '',
+                            route: '',
+                            frequency: '',
+                            dispense: '',
+                            refills: '',
+                            purpose: '',
+                            pharmacy: '',
+                            pharmacyName: '',
+                            pharmacyCity: '',
+                            pharmacyState: '',
+                            pharmacyAddressMemo: '',
+                            pharmacyPhone: '',
+                            pharmacyFax: '',
+                        };
+                        showStagPopup('erx-popup');
+                    },
+                    savePopupItem: function() {
+                        let form = $('#erxApp form').first();
+                        if(!form[0].checkValidity()) {
+                            form[0].reportValidity();
+                            return false;
+                        }
+
+                        showMask();
+                        let payload = {};
+                        if(this.popupMode === '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.popupItem);
+                        }
+                        else {
+                            payload.uid = this.popupItem.uid;
+                            payload.newData = JSON.stringify(this.popupItem);
+                        }
+
+                        $.post(
+                            '/api/ticket/' + (this.popupMode === 'add' ? 'create' : 'updateData'),
+                            payload,
+                            function(_data) {
+                                console.log(_data);
+                                fastReload();
+                            },
+                            'json');
+
+                        return false;
+                    },
+                    closeItem: function(_item) {
+                        showMask();
+                        $.post('/api/ticket/close', {
+                            uid: _item.uid
+                        }, function(_data) {
+                            fastReload();
+                        });
+                    },
+                    openItem: function(_item) {
+                        showMask();
+                        $.post('/api/ticket/open', {
+                            uid: _item.uid
+                        }, function(_data) {
+                            fastReload();
+                        });
+                    },
+                    initRxAutoSuggest: function() {
+                        let self = this;
+                        $('#erxApp 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.popupItem.medication = $(elem).val();
+                            });
+                            window.Def.Autocompleter.Event.observeListSelections(dynStrengthsID, function() {
+                                var autocomp = elem.autocomp, acData = autocomp.getSelectedItemData();
+                                self.popupItem.strength = $(strengthElem).val();
+                            });
+                            $(elem).attr('ac-initialized', 1);
+                            $(strengthElem).attr('ac-initialized', 1);
+                        });
+                    },
+                    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 '';
+                        }
+                    }
+                },
+                mounted: function () {
+                    this.initRxAutoSuggest();
+                }
+            })
+        }
+        addMCInitializer('erx', init, '#erxApp');
+    })();
+</script>

+ 216 - 0
resources/views/app/patient/partials/imaging.blade.php

@@ -0,0 +1,216 @@
+<div id="imagingApp" v-cloak>
+    <div class="d-flex align-items-center pb-2">
+        <h4 class="font-weight-bold m-0 font-size-14"><i class="fa fa-image mr-2"></i>Imaging</h4>
+        <span class="mx-2 text-secondary">|</span>
+        <a class="py-0 font-weight-normal c-pointer" v-on:click.prevent="showPopup('imaging-popup')">Add</a>
+    </div>
+    <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
+        <thead>
+        <tr class="bg-light">
+            <th class="px-2 text-secondary border-bottom-0 width-30px">#</th>
+            <th class="px-2 text-secondary border-bottom-0">Tests</th>
+            <th class="px-2 text-secondary border-bottom-0">ICDs</th>
+            <th class="px-2 text-secondary border-bottom-0">Memo</th>
+            <th class="px-2 text-secondary border-bottom-0">&nbsp;</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr v-for="(item, index) in items" :class="item.is_open ? '' : 'opacity-60'">
+            <td class="px-2">@{{ index + 1 }}</td>
+            <td class="px-2" v-html="item.tests && Array.isArray(item.tests) ? item.tests.join('<br>') : item.tests"></td>
+            <td class="px-2" v-html="item.icds && Array.isArray(item.icds) ? item.icds.join('<br>') : item.icds"></td>
+            <td class="px-2">@{{item.memo}}</td>
+            <td class="px-2 text-nowrap">
+                <a class="mr-2 c-pointer" v-on:click.prevent="showPopup('imaging-popup', item)">Edit</a>
+                <a class="mr-2 c-pointer" v-if="item.is_open" v-on:click.prevent="closeItem(item)">Close</a>
+                <a class="mr-2 c-pointer" v-if="!item.is_open" v-on:click.prevent="openItem(item)">Open</a>
+            </td>
+        </tr>
+        </tbody>
+    </table>
+    <div class="stag-popup stag-popup-sm mcp-theme-1" stag-popup-key="imaging-popup">
+        <form method="POST" action="/api/appointment/create" class="overflow-visible">
+            <h3 class="stag-popup-title mb-2">
+                <span>@{{ popupMode === 'add' ? 'Add Imaging Order Item' : 'Edit Imaging Order Item' }}</span>
+                <a href="#" class="ml-auto text-secondary"
+                   onclick="return closeStagPopup()"><i class="fa fa-times-circle"></i></a>
+            </h3>
+            <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="popupItem.tests.push('')">Add</a>
+                </div>
+                <div class="d-flex align-items-center mb-2" v-for="(test, testIndex) in popupItem.tests">
+                    <div class="position-relative flex-grow-1">
+                        <input required type="text" data-option-list v-model="popupItem.tests[testIndex]" class="form-control form-control-sm">
+                        <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="popupItem.tests.length > 1" class="ml-2 text-danger" href="#" v-on:click.prevent="popupItem.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="popupItem.icds.push('')">Add</a>
+                </div>
+                <div class="d-flex align-items-center mb-2" v-for="(icd, icdIndex) in popupItem.icds">
+                    <div class="position-relative flex-grow-1">
+                        <input required type="text" data-field="icd" :data-index="icdIndex"
+                               v-model="popupItem.icds[icdIndex]" class="form-control form-control-sm">
+                    </div>
+                    <a v-if="popupItem.icds.length > 1" class="ml-2 text-danger" href="#" v-on:click.prevent="popupItem.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="popupItem.memo" class="form-control form-control-sm">
+            </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="savePopupItem()">Submit</button>
+                <button type="button" class="btn btn-sm btn-default border" onclick="return closeStagPopup()">Cancel</button>
+            </div>
+        </form>
+    </div>
+</div>
+<script>
+    (function() {
+        <?php
+        $tickets = $patient->tickets->filter(function($_item) {
+            return $_item->category === 'imaging';
+        });
+        $items = [];
+        foreach ($tickets as $ticket) {
+            $item = json_decode($ticket->data);
+            $item->uid = $ticket->uid;
+            $item->is_open = $ticket->is_open;
+            $items[] = $item;
+        }
+        ?>
+        function init() {
+            let items = [];
+            let imagingApp = new Vue({
+                el: '#imagingApp',
+                delimiters: ['@{{', '}}'],
+                data: {
+                    popupMode: 'add',
+                    items: {!! json_encode($items) !!},
+                    popupItem: {
+                        uid: '',
+                        is_open: true,
+                        tests: [''],
+                        icds: [''],
+                        memo: '',
+                    },
+                },
+                methods: {
+                    showPopup: function(_name, _item) {
+                        closeStagPopup();
+                        this.popupMode = _item ? 'edit' : 'add';
+                        this.popupItem = _item ? JSON.parse(JSON.stringify(_item)) : {
+                            uid: '',
+                            is_open: true,
+                            tests: [''],
+                            icds: [''],
+                            memo: '',
+                        };
+                        let self = this;
+                        Vue.nextTick(function() {
+                            self.initICDAutoSuggest();
+                            showStagPopup('imaging-popup', true);
+                        });
+                    },
+                    savePopupItem: function() {
+                        let form = $('#imagingApp form').first();
+                        if(!form[0].checkValidity()) {
+                            form[0].reportValidity();
+                            return false;
+                        }
+
+                        showMask();
+                        let payload = {};
+                        if(this.popupMode === '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.popupItem);
+                        }
+                        else {
+                            payload.uid = this.popupItem.uid;
+                            payload.newData = JSON.stringify(this.popupItem);
+                        }
+
+                        $.post(
+                            '/api/ticket/' + (this.popupMode === 'add' ? 'create' : 'updateData'),
+                            payload,
+                            function(_data) {
+                                console.log(_data);
+                                fastReload();
+                            },
+                            'json');
+
+                        return false;
+                    },
+                    closeItem: function(_item) {
+                        showMask();
+                        $.post('/api/ticket/close', {
+                            uid: _item.uid
+                        }, function(_data) {
+                            fastReload();
+                        });
+                    },
+                    openItem: function(_item) {
+                        showMask();
+                        $.post('/api/ticket/open', {
+                            uid: _item.uid
+                        }, function(_data) {
+                            fastReload();
+                        });
+                    },
+                    initICDAutoSuggest: function() {
+                        let self = this;
+                        $('#imagingApp 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.popupItem.icds[vueIndex] = acData[0].code;
+                                return false;
+                            });
+                            $(elem).attr('ac-initialized', 1);
+                        });
+                    },
+                },
+                mounted: function () {
+                    this.initICDAutoSuggest();
+                }
+            })
+        }
+        addMCInitializer('imaging', init, '#imagingApp');
+    })();
+</script>

+ 219 - 0
resources/views/app/patient/partials/lab.blade.php

@@ -0,0 +1,219 @@
+<div id="labApp" v-cloak>
+    <div class="d-flex align-items-center pb-2">
+        <h4 class="font-weight-bold m-0 font-size-14"><i class="fa fa-vials mr-2"></i>Lab</h4>
+        <span class="mx-2 text-secondary">|</span>
+        <a class="py-0 font-weight-normal c-pointer" v-on:click.prevent="showPopup('lab-popup')">Add</a>
+    </div>
+    <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
+        <thead>
+        <tr class="bg-light">
+            <th class="px-2 text-secondary border-bottom-0 width-30px">#</th>
+            <th class="px-2 text-secondary border-bottom-0">Tests</th>
+            <th class="px-2 text-secondary border-bottom-0">ICDs</th>
+            <th class="px-2 text-secondary border-bottom-0">Memo</th>
+            <th class="px-2 text-secondary border-bottom-0">&nbsp;</th>
+        </tr>
+        </thead>
+        <tbody>
+            <tr v-for="(item, index) in items" :class="item.is_open ? '' : 'opacity-60'">
+                <td class="px-2">@{{ index + 1 }}</td>
+                <td class="px-2" v-html="item.tests && Array.isArray(item.tests) ? item.tests.join('<br>') : item.tests"></td>
+                <td class="px-2" v-html="item.icds && Array.isArray(item.icds) ? item.icds.join('<br>') : item.icds"></td>
+                <td class="px-2">@{{item.memo}}</td>
+                <td class="px-2 text-nowrap">
+                    <a class="mr-2 c-pointer" v-on:click.prevent="showPopup('lab-popup', item)">Edit</a>
+                    <a class="mr-2 c-pointer" v-if="item.is_open" v-on:click.prevent="closeItem(item)">Close</a>
+                    <a class="mr-2 c-pointer" v-if="!item.is_open" v-on:click.prevent="openItem(item)">Open</a>
+                </td>
+            </tr>
+        </tbody>
+    </table>
+    <div class="stag-popup stag-popup-sm mcp-theme-1" stag-popup-key="lab-popup">
+        <form method="POST" action="/api/appointment/create" class="overflow-visible">
+            <h3 class="stag-popup-title mb-2">
+                <span>@{{ popupMode === 'add' ? 'Add Lab Order Item' : 'Edit Lab Order Item' }}</span>
+                <a href="#" class="ml-auto text-secondary"
+                   onclick="return closeStagPopup()"><i class="fa fa-times-circle"></i></a>
+            </h3>
+            <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="popupItem.tests.push('')">Add</a>
+                </div>
+                <div class="d-flex align-items-center mb-2" v-for="(test, testIndex) in popupItem.tests">
+                    <div class="position-relative flex-grow-1">
+                        <input required type="text" data-option-list v-model="popupItem.tests[testIndex]" class="form-control form-control-sm">
+                        <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="popupItem.tests.length > 1" class="ml-2 text-danger" href="#" v-on:click.prevent="popupItem.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="popupItem.icds.push('')">Add</a>
+                </div>
+                <div class="d-flex align-items-center mb-2" v-for="(icd, icdIndex) in popupItem.icds">
+                    <div class="position-relative flex-grow-1">
+                        <input required type="text" data-field="icd" :data-index="icdIndex"
+                               v-model="popupItem.icds[icdIndex]" class="form-control form-control-sm">
+                    </div>
+                    <a v-if="popupItem.icds.length > 1" class="ml-2 text-danger" href="#" v-on:click.prevent="popupItem.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="popupItem.memo" class="form-control form-control-sm">
+            </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="savePopupItem()">Submit</button>
+                <button type="button" class="btn btn-sm btn-default border" onclick="return closeStagPopup()">Cancel</button>
+            </div>
+        </form>
+    </div>
+</div>
+<script>
+    (function() {
+        <?php
+        $tickets = $patient->tickets->filter(function($_item) {
+            return $_item->category === 'lab';
+        });
+        $items = [];
+        foreach ($tickets as $ticket) {
+            $item = json_decode($ticket->data);
+            $item->uid = $ticket->uid;
+            $item->is_open = $ticket->is_open;
+            $items[] = $item;
+        }
+        ?>
+        function init() {
+            let items = [];
+            let labApp = new Vue({
+                el: '#labApp',
+                delimiters: ['@{{', '}}'],
+                data: {
+                    popupMode: 'add',
+                    items: {!! json_encode($items) !!},
+                    popupItem: {
+                        uid: '',
+                        is_open: true,
+                        tests: [''],
+                        icds: [''],
+                        memo: '',
+                    },
+                },
+                methods: {
+                    showPopup: function(_name, _item) {
+                        closeStagPopup();
+                        this.popupMode = _item ? 'edit' : 'add';
+                        this.popupItem = _item ? JSON.parse(JSON.stringify(_item)) : {
+                            uid: '',
+                            is_open: true,
+                            tests: [''],
+                            icds: [''],
+                            memo: '',
+                        };
+                        let self = this;
+                        Vue.nextTick(function() {
+                            self.initICDAutoSuggest();
+                            showStagPopup('lab-popup', true);
+                        });
+                    },
+                    savePopupItem: function() {
+                        let form = $('#labApp form').first();
+                        if(!form[0].checkValidity()) {
+                            form[0].reportValidity();
+                            return false;
+                        }
+
+                        showMask();
+                        let payload = {};
+                        if(this.popupMode === '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.popupItem);
+                        }
+                        else {
+                            payload.uid = this.popupItem.uid;
+                            payload.newData = JSON.stringify(this.popupItem);
+                        }
+
+                        $.post(
+                            '/api/ticket/' + (this.popupMode === 'add' ? 'create' : 'updateData'),
+                            payload,
+                            function(_data) {
+                                console.log(_data);
+                                fastReload();
+                            },
+                            'json');
+
+                        return false;
+                    },
+                    closeItem: function(_item) {
+                        showMask();
+                        $.post('/api/ticket/close', {
+                            uid: _item.uid
+                        }, function(_data) {
+                            fastReload();
+                        });
+                    },
+                    openItem: function(_item) {
+                        showMask();
+                        $.post('/api/ticket/open', {
+                            uid: _item.uid
+                        }, function(_data) {
+                            fastReload();
+                        });
+                    },
+                    initICDAutoSuggest: function() {
+                        let self = this;
+                        $('#labApp 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.popupItem.icds[vueIndex] = acData[0].code;
+                                return false;
+                            });
+                            $(elem).attr('ac-initialized', 1);
+                        });
+                    },
+                },
+                mounted: function () {
+                    this.initICDAutoSuggest();
+                }
+            })
+        }
+        addMCInitializer('lab', init, '#labApp');
+    })();
+</script>

+ 111 - 0
resources/views/app/patient/partials/measurement.blade.php

@@ -0,0 +1,111 @@
+<div class="d-flex align-items-start py-1 pr-2 measurement-item {{ $child ? 'pl-4' : 'pl-2' }}">
+    {{--<div moe relative class="mr-2">
+        <a class="on-hover-opaque" start show title="Delete">
+            <i class="font-size-11 fa fa-trash-alt text-danger"></i>
+        </a>
+        <form url="/api/measurement/remove">
+            <input type="hidden" name="uid" value="{{ $measurement->uid }}">
+            <p class="small min-width-200px">Are you sure you want to delete this entry?</p>
+            <div class="d-flex align-items-center">
+                <button class="btn btn-sm btn-danger mr-2" submit>Delete</button>
+                <button class="btn btn-sm btn-default mr-2 border" cancel>Cancel</button>
+            </div>
+        </form>
+    </div>
+    <div moe class="mr-2">
+        <a class="on-hover-opaque" start show title="Update">
+            <i class="font-size-11 fa fa-edit text-primary"></i>
+        </a>
+        <form url="/api/measurement/updateBasic">
+            <input type="hidden" name="uid" value="{{ $measurement->uid }}">
+            <div class="mb-2">
+                <input required type="text" class="form-control form-control-sm" name="label" value="{{ $measurement->label }}" placeholder="Type">
+            </div>
+            <div class="mb-2">
+                <input required autofocus type="text" class="form-control form-control-sm" name="value" value="{{ $measurement->value }}" placeholder="Value">
+            </div>
+            <div class="mb-2">
+                <input required type="date" class="form-control form-control-sm" name="effectiveDate" max="{{ date('Y-m-d') }}" value="{{ date('Y-m-d') }}">
+            </div>
+            <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>--}}
+    <div>
+        <div class="d-flex">
+            <span>{{ $measurement->label }}</span>
+            <span class="font-weight-bold ml-2">{{ $measurement->value }}</span>
+            <div moe>
+                <a href="#" class="ml-2 font-weight-normal text-nowrap" start show>+ Entry</a>
+                <form url="/api/clientProgramMonthEntry/create">
+                    <input type="hidden" name="measurementUid" value="{{ $measurement->uid }}">
+                    <input type="hidden" name="proUid" value="{{ $pro->uid }}">
+                    <div class="mb-2">
+                        <label class="text-sm mb-1 text-secondary">Effective Date</label>
+                        <input required type="date" class="form-control form-control-sm"
+                               name="effectiveDate" value="{{ date('Y-m-d') }}">
+                    </div>
+                    <div class="mb-2">
+                        <label class="text-sm mb-1 text-secondary">Time (minutes)</label>
+                        <input autofocus required type="number" class="form-control form-control-sm" name="timeInMinutes"
+                               value="">
+                    </div>
+                    <div class="mb-2">
+                        <label class="text-sm mb-1 text-secondary">Quick Text</label>
+                        <input type="text" class="form-control form-control-sm" name="quickText" value="">
+                    </div>
+                    <div class="mb-2">
+                        <label class="text-sm mb-1 text-secondary">Measurement Determination</label>
+                        <input type="text" class="form-control form-control-sm" data-option-list autocomplete="off"
+                               name="measurementDetermination" value="">
+                        <div class="data-option-list">
+                            <div>Normal</div>
+                            <div>High</div>
+                            <div>Low</div>
+                            <div>Abnormal</div>
+                        </div>
+                    </div>
+                    <div class="mb-2">
+                        <label class="text-sm mb-1 text-secondary">Measurement Memo</label>
+                        <input type="text" class="form-control form-control-sm" name="measurementMemo" value="">
+                    </div>
+                    <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>
+        </div>
+        <div>
+            <span class="font-weight-normal text-secondary text-sm">
+                {{ friendly_date_time($measurement->effective_date, false) }}
+            </span>
+        </div>
+    </div>
+    @if(!!$programMonth)
+        @if($minutes)
+            <?php $entries = $measurement->entriesByPro($pro); ?>
+            <div class="ml-auto">
+                @foreach($entries as $entry)
+                    <div class="d-flex align-items-start mb-1 pr-2">
+                        <span class="width-200px text-sm text-right pr-2 text-secondary">{{ $entry->quick_text }}</span>
+                        <span class="pr-2 text-sm">
+                            <i class="fa fa-clock text-secondary on-hover-opaque"></i>
+                            {{ minutes_to_hhmm($entry->time_in_minutes) }}
+                        </span>
+                        <span class="text-sm font-weight-bold">
+                            ${{ round($pro->hourly_rate * ($entry->time_in_minutes / 60), 1) }}
+                        </span>
+                    </div>
+                @endforeach
+            </div>
+        @else
+            <span class="ml-auto pr-2 text-secondary"><i
+                    class="fa fa-exclamation-triangle"></i>&nbsp;Entry pending</span>
+        @endif
+    @elseif($filter === '')
+        <span class="ml-auto pr-2 text-secondary">Entry pending</span>
+    @endif
+</div>

+ 88 - 0
resources/views/app/patient/partials/measurements.blade.php

@@ -0,0 +1,88 @@
+<div class="mt-2 pb-1">
+    <div class="d-flex align-items-center mb-2 py-2 border-top border-bottom">
+        <h6 class="my-0 font-weight-bold text-secondary">Measurements</h6>
+        <span class="mx-2 text-secondary">|</span>
+        <div moe>
+            <a start show class="py-0 font-weight-normal">Add</a>
+            <form url="/api/measurement/create">
+                <input type="hidden" name="clientUid" value="{{ $patient->uid }}">
+                <div class="mb-2">
+                    <input required autofocus type="text" class="form-control form-control-sm" name="label" value="" placeholder="Type">
+                </div>
+                <div class="mb-2">
+                    <input required type="text" class="form-control form-control-sm" name="value" value="" placeholder="Value">
+                </div>
+                <div class="mb-2">
+                    <input required type="date" class="form-control form-control-sm" name="effectiveDate" max="{{ date('Y-m-d') }}" value="{{ date('Y-m-d') }}">
+                </div>
+                <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>
+        <span class="mx-2 text-secondary">|</span>
+        <a start show class="py-0 font-weight-normal"
+           href="/patients/view/{{ $patient->uid }}/measurements">
+            View All
+        </a>
+    </div>
+    <table class="table table-sm border-0 m-0">
+        <tbody>
+        @foreach($patient->measurements as $measurement)
+            @if(!empty($measurement->label) && !in_array($measurement->label, $vitalLabels))
+                <tr>
+                    <td class="text-black p-0 border-0">
+                        <div class="d-flex align-items-center">
+                            <div moe relative class="mr-2">
+                                <a class="on-hover-opaque" start show title="Delete">
+                                    <i class="font-size-11 fa fa-trash-alt text-danger"></i>
+                                </a>
+                                <form url="/api/measurement/remove">
+                                    <input type="hidden" name="uid" value="{{ $measurement->uid }}">
+                                    <p class="small min-width-200px">Are you sure you want to delete this entry?</p>
+                                    <div class="d-flex align-items-center">
+                                        <button class="btn btn-sm btn-danger mr-2" submit>Delete</button>
+                                        <button class="btn btn-sm btn-default mr-2 border" cancel>Cancel</button>
+                                    </div>
+                                </form>
+                            </div>
+                            <div moe class="mr-2">
+                                <a class="on-hover-opaque" start show title="Update">
+                                    <i class="font-size-11 fa fa-edit text-primary"></i>
+                                </a>
+                                <form url="/api/measurement/updateBasic">
+                                    <input type="hidden" name="uid" value="{{ $measurement->uid }}">
+                                    <div class="mb-2">
+                                        <input required type="text" class="form-control form-control-sm" name="label" value="{{ $measurement->label }}" placeholder="Type">
+                                    </div>
+                                    <div class="mb-2">
+                                        <input required autofocus type="text" class="form-control form-control-sm" name="value" value="{{ $measurement->value }}" placeholder="Value">
+                                    </div>
+                                    <div class="mb-2">
+                                        <input required type="date" class="form-control form-control-sm" name="effectiveDate" max="{{ date('Y-m-d') }}" value="{{ date('Y-m-d') }}">
+                                    </div>
+                                    <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>
+                            <span>{{ $measurement->label }}:</span>
+                            <span class="font-weight-bold ml-1">{{ $measurement->value }}</span>
+                            <span class="font-weight-normal text-secondary ml-2 text-sm">(as on {{ friendly_date_time($measurement->effective_date, false) }})</span>
+                        </div>
+                    </td>
+                </tr>
+            @endif
+        @endforeach
+        @if(!$patient->measurements || !count($patient->measurements) === 0)
+            <tr>
+                <td class="text-secondary p-0 border-0">
+                    No items to show
+                </td>
+            </tr>
+        @endif
+        </tbody>
+    </table>
+</div>

+ 160 - 0
resources/views/app/patient/partials/other.blade.php

@@ -0,0 +1,160 @@
+<div id="otherApp" v-cloak>
+    <div class="d-flex align-items-center pb-2">
+        <h4 class="font-weight-bold m-0 font-size-14"><i class="fa fa-file-alt mr-2"></i>Other</h4>
+        <span class="mx-2 text-secondary">|</span>
+        <a class="py-0 font-weight-normal c-pointer" v-on:click.prevent="showPopup('other-popup')">Add</a>
+    </div>
+    <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
+        <thead>
+        <tr class="bg-light">
+            <th class="px-2 text-secondary border-bottom-0 width-30px">#</th>
+            <th class="px-2 text-secondary border-bottom-0">Category</th>
+            <th class="px-2 text-secondary border-bottom-0">Title</th>
+            <th class="px-2 text-secondary border-bottom-0">Description</th>
+            <th class="px-2 text-secondary border-bottom-0">&nbsp;</th>
+        </tr>
+        </thead>
+        <tbody>
+            <tr v-for="(item, index) in items" :class="item.is_open ? '' : 'opacity-60'">
+                <td class="px-2">@{{ index + 1 }}</td>
+                <td class="px-2" v-html="item.category"></td>
+                <td class="px-2" v-html="item.title"></td>
+                <td class="px-2" v-html="item.description"></td>
+                <td class="px-2 text-nowrap">
+                    <a class="mr-2 c-pointer" v-on:click.prevent="showPopup('other-popup', item)">Edit</a>
+                    <a class="mr-2 c-pointer" v-if="item.is_open" v-on:click.prevent="closeItem(item)">Close</a>
+                    <a class="mr-2 c-pointer" v-if="!item.is_open" v-on:click.prevent="openItem(item)">Open</a>
+                </td>
+            </tr>
+        </tbody>
+    </table>
+    <div class="stag-popup stag-popup-sm mcp-theme-1" stag-popup-key="other-popup">
+        <form method="POST" action="/api/appointment/create" class="overflow-visible">
+            <h3 class="stag-popup-title mb-2">
+                <span>@{{ popupMode === 'add' ? 'Add Item' : 'Edit Item' }}</span>
+                <a href="#" class="ml-auto text-secondary"
+                   onclick="return closeStagPopup()"><i class="fa fa-times-circle"></i></a>
+            </h3>
+            <div class="mb-2">
+                <label class="text-sm text-secondary mb-1">Category</label>
+                <input required type="text" v-model="popupItem.category" class="form-control form-control-sm">
+            </div>
+            <div class="mb-2">
+                <label class="text-sm text-secondary mb-1">Title</label>
+                <input required type="text" v-model="popupItem.title" class="form-control form-control-sm">
+            </div>
+            <div class="mb-2">
+                <label class="text-sm text-secondary mb-1">Description</label>
+                <textarea type="text" rows="3" v-model="popupItem.description" class="form-control form-control-sm"></textarea>
+            </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="savePopupItem()">Submit</button>
+                <button type="button" class="btn btn-sm btn-default border" onclick="return closeStagPopup()">Cancel</button>
+            </div>
+        </form>
+    </div>
+</div>
+<script>
+    (function() {
+        <?php
+        $tickets = $patient->tickets->filter(function($_item) {
+            return $_item->category === 'other';
+        });
+        $items = [];
+        foreach ($tickets as $ticket) {
+            $item = json_decode($ticket->data);
+            $item->uid = $ticket->uid;
+            $item->is_open = $ticket->is_open;
+            $items[] = $item;
+        }
+        ?>
+        function init() {
+            let items = [];
+            let otherApp = new Vue({
+                el: '#otherApp',
+                delimiters: ['@{{', '}}'],
+                data: {
+                    popupMode: 'add',
+                    items: {!! json_encode($items) !!},
+                    popupItem: {
+                        uid: '',
+                        is_open: true,
+                        category: '',
+                        title: '',
+                        description: '',
+                    },
+                },
+                methods: {
+                    showPopup: function(_name, _item) {
+                        closeStagPopup();
+                        this.popupMode = _item ? 'edit' : 'add';
+                        this.popupItem = _item ? JSON.parse(JSON.stringify(_item)) : {
+                            uid: '',
+                            is_open: true,
+                            category: '',
+                            title: '',
+                            description: '',
+                        };
+                        let self = this;
+                        Vue.nextTick(function() {
+                            showStagPopup('other-popup', true);
+                        });
+                    },
+                    savePopupItem: function() {
+                        let form = $('#otherApp form').first();
+                        if(!form[0].checkValidity()) {
+                            form[0].reportValidity();
+                            return false;
+                        }
+
+                        showMask();
+                        let payload = {};
+                        if(this.popupMode === 'add') {
+                            payload.clientUid = '{{ $patient->uid }}';
+                            payload.category = 'other';
+                            payload.assignedProUid = '{{ $pro->uid  }}';
+                            payload.managerProUid = '{{ $pro->uid  }}';
+                            payload.orderingProUid = '{{ $pro->uid  }}';
+                            payload.initiatingProUid = '{{ $pro->uid  }}';
+                            payload.data = JSON.stringify(this.popupItem);
+                        }
+                        else {
+                            payload.uid = this.popupItem.uid;
+                            payload.newData = JSON.stringify(this.popupItem);
+                        }
+
+                        $.post(
+                            '/api/ticket/' + (this.popupMode === 'add' ? 'create' : 'updateData'),
+                            payload,
+                            function(_data) {
+                                console.log(_data);
+                                fastReload();
+                            },
+                            'json');
+
+                        return false;
+                    },
+                    closeItem: function(_item) {
+                        showMask();
+                        $.post('/api/ticket/close', {
+                            uid: _item.uid
+                        }, function(_data) {
+                            fastReload();
+                        });
+                    },
+                    openItem: function(_item) {
+                        showMask();
+                        $.post('/api/ticket/open', {
+                            uid: _item.uid
+                        }, function(_data) {
+                            fastReload();
+                        });
+                    }
+                },
+                mounted: function () {
+                }
+            })
+        }
+        addMCInitializer('other', init, '#otherApp');
+    })();
+</script>

+ 44 - 0
resources/views/app/patient/partials/programs.blade.php

@@ -0,0 +1,44 @@
+<div class="mt-0 pb-1">
+    <div class="d-flex align-items-center mb-1 pt-2 pb-1 border-top">
+        <h6 class="my-0 font-weight-bold text-secondary">Programs</h6>
+    </div>
+    <table class="table table-bordered table-sm table-striped m-0">
+        <tbody>
+        <?php $programNumber = 1; ?>
+        @foreach($patient->clientPrograms as $clientProgram)
+            <?php $program = $clientProgram; ?>
+            <tr>
+                <td class="text-black px-2 py-3">
+                    <div class="d-flex align-items-center pb-1">
+                        <span class="font-weight-bold">{{ $programNumber }}. {{ $program->title }}</span>
+                        <span class="mx-2 text-secondary ml-auto"><i class="fa fa-clock"></i></span>
+                        <?php $programMonth = $program->getProgramMonth(strtoupper(date('F')), date('Y')); ?>
+                        <b class="text-success">{{ $programMonth ? $programMonth->time_in_minutes : 0 }}</b>&nbsp;mins billed
+                        <span class="mx-2 text-secondary">|</span>
+                        <b>{{ $program->max_monthly_time_in_minutes - ($programMonth ? $programMonth->time_in_minutes : 0) }}</b>&nbsp;mins remaining
+                    </div>
+                    <?php $programNumber++; ?>
+                    <div class="mt-1 pl-3 d-flex align-items-center flex-wrap">
+                        <span class="pr-1">MCP: <b class="text-secondary">{{ $clientProgram->mcp ? $clientProgram->mcp->displayName() : '' }}</b></span>
+                        <span class="mx-2 text-secondary on-hover-opaque">|</span>
+                        <span class="pr-1">Manager: <b class="text-secondary">{{ $clientProgram->manager ? $clientProgram->manager->displayName() : '' }}</b></span>
+                    </div>
+                    <div class="mt-1 pl-3 d-flex align-items-center flex-wrap">
+                        <span class="pr-1">OB Visit: <b class="text-secondary">{{ $clientProgram->has_mcp_done_onboarding_visit }}</b></span>
+                        <span class="mx-2 text-secondary on-hover-opaque">|</span>
+                        <span class="pr-1">Setup: <b class="text-secondary">{{ $clientProgram->is_setup_complete }}</b></span>
+                    </div>
+                </td>
+            </tr>
+        @endforeach
+        </tbody>
+    </table>
+    @if(!$patient->clientPrograms || count($patient->clientPrograms) === 0)
+        <div class="p-2 border-top">
+            <span class="text-secondary">
+                No programs
+            </span>
+        </div>
+    @endif
+</div>
+

+ 2 - 2
resources/views/app/patient/partials/vitals.blade.php

@@ -1,7 +1,7 @@
 <?php
 
 $vitalMap = [
-    'heightInches' => 'Ht. (in.)',
+    'heightInInches' => 'Ht. (in.)',
     'weightPounds' => 'Wt. (lbs.)',
     'temperatureF' => 'Temp. (F)',
     'pulseRatePerMinute' => 'Pulse',
@@ -44,7 +44,7 @@ foreach($vitalLabels as $vitalLabel){
                 <input type="hidden" name="clientUid" value="{{ $patient->uid }}">
                 <label class="text-secondary text-sm mb-1">Ht. (in.)</label>
                 <div class="mb-2">
-                    <input type="text" class="form-control form-control-sm" name="heightInches" value="{{$vitalValues['Ht. (in.)']['value']}}" placeholder="">
+                    <input type="text" class="form-control form-control-sm" name="heightInInches" value="{{$vitalValues['Ht. (in.)']['value']}}" placeholder="">
                 </div>
                 <label class="text-secondary text-sm mb-1">Wt. (lbs.)</label>
                 <div class="mb-2">

+ 559 - 0
resources/views/app/patient/programs.blade.php

@@ -0,0 +1,559 @@
+@extends ('layouts.patient')
+@section('inner-content')
+    <?php // $pro->pro_type = 'INDIVIDUAL'; ?>
+    <link href="/select2/select2.min.css" rel="stylesheet" />
+    <script src="/select2/select2.min.js"></script>
+    <div id="programsComponent">
+        <div class="d-flex align-items-center pb-3">
+            <h4 class="font-weight-bold m-0">Programs</h4>
+            {{-- add program --}}
+            @if($pro->pro_type === 'ADMIN')
+            <span class="mx-2 text-secondary">|</span>
+            <div moe large>
+                <a start show href="#">Add</a>
+                <form url="/api/clientProgram/create">
+                    <input type="hidden" name="clientUid" value="{{$patient->uid}}">
+                    <div class="mb-2">
+                        <label class="mb-1 text-sm text-secondary">Title</label>
+                        <input type="text" name="title" value=""
+                               class="form-control form-control-sm"
+                               placeholder="Title" required>
+                    </div>
+                    <div class="mb-2">
+                        <label class="mb-1 text-sm text-secondary">Measurement Types</label>
+                        <input type="hidden" name="measurementLabels" v-model="newMeasurementLabels">
+                        <select multiple add class="form-control form-control-sm">
+                            <option value=""></option>
+                            <option value="BP">Blood Pressure</option>
+                            <option value="BS">Blood Sugar</option>
+                            <option value="WEIGHT">Weight</option>
+                        </select>
+                    </div>
+                    <div class="form-group mb-0">
+                        <button class="btn btn-primary btn-sm mr-1" submit>Submit</button>
+                        <button class="btn btn-default border btn-sm" cancel>Cancel</button>
+                    </div>
+                </form>
+            </div>
+            @endif
+            <select class="ml-auto max-width-200px form-control form-control-sm"
+                    onchange="fastLoad('/patients/view/{{$patient->uid}}/programs/' + this.value, true, false, false)">
+                <option value="" {{ $filter === '' ? 'selected' : '' }}>Current Month</option>
+                <option value="all" {{ $filter === 'all' ? 'selected' : '' }}>All Time</option>
+            </select>
+        </div>
+
+        @foreach($patient->clientPrograms as $program)
+        <?php
+            $programCategories = explode('|', $program->measurement_labels);
+            $programCategories = array_filter($programCategories, function($_item) {
+                return !empty($_item);
+            });
+            $programICDs = explode('|', $program->icds);
+            $programICDs = array_filter($programICDs, function($_item) {
+                return !empty($_item);
+            });
+            $programMonth = null;
+            if($filter === '') {
+                $programMonth = $program->getProgramMonth(strtoupper(date('F')), date('Y'));
+            }
+        ?>
+        <div class="card mb-4">
+
+            <div class="card-header d-flex align-items-start px-3 py-2 border-bottom">
+                <div class="pr-2">
+                    <span class="mr-1 font-weight-bold">{{ $program->title }}</span>
+                    @if($pro->pro_type === 'ADMIN')
+                    <div moe>
+                        <a href="#" show start class="on-hover-opaque"><i class="fa fa-edit"></i></a>
+                        <form url="/api/clientProgram/updateTitle">
+                            <input type="hidden" name="uid" value="{{ $program->uid }}">
+                            <div class="mb-2">
+                                <label class="mb-1 text-sm text-secondary">Program Title</label>
+                                <input type="text" name="title"
+                                       class="form-control form-control-sm"
+                                       placeholder="Title" value="{{ $program->title }}" required>
+                            </div>
+                            <div class="form-group mb-0">
+                                <button class="btn btn-primary btn-sm" submit>Submit</button>
+                                <button class="btn btn-default border btn-sm" cancel>Cancel</button>
+                            </div>
+                        </form>
+                    </div>
+                    @endif
+                </div>
+
+                <div class="px-2 ml-auto border-right">
+                    <div class="d-flex">
+                        <span class="mr-2"><span class="text-secondary">MCP:</span>
+                            {{ $program->mcp ? $program->mcp->displayName() : '-' }}
+                        </span>
+                        @if($pro->pro_type === 'ADMIN')
+                        <div moe relative class="ml-auto">
+                            <a href="#" show start class="on-hover-opaque"><i class="fa fa-edit"></i></a>
+                            <form url="/api/clientProgram/changeMcp" right>
+                                <input type="hidden" name="uid" value="{{ $program->uid }}">
+                                <div class="mb-2">
+                                    <label class="mb-1 text-sm text-secondary">Program MCP</label>
+                                    <select name="newMcpProUid" class="form-control form-control-sm">
+                                        <option value="">-- Select MCP --</option>
+                                        @foreach($pros as $iPro)
+                                            <option value="{{ $iPro->uid }}" {{ $iPro->id === $program->mcp_pro_id ? 'selected' : ''  }}>
+                                                {{ $iPro->displayName() }}
+                                            </option>
+                                        @endforeach
+                                    </select>
+                                </div>
+                                <div class="mb-0">
+                                    <button class="btn btn-primary btn-sm" submit>Submit</button>
+                                    <button class="btn btn-default border btn-sm" cancel>Cancel</button>
+                                </div>
+                            </form>
+                        </div>
+                        @endif
+                    </div>
+                </div>
+
+                <div class="pl-2">
+                    <div class="d-flex">
+                        <span class="mr-2"><span class="text-secondary">Manager:</span>
+                            {{ $program->manager ? $program->manager->displayName() : '-' }}
+                        </span>
+                        @if($pro->pro_type === 'ADMIN')
+                        <div moe relative class="ml-auto">
+                            <a href="#" show start class="on-hover-opaque"><i class="fa fa-edit"></i></a>
+                            <form url="/api/clientProgram/changeManager" right>
+                                <input type="hidden" name="uid" value="{{ $program->uid }}">
+                                <div class="mb-2">
+                                    <label class="mb-1 text-sm text-secondary">Program Manager</label>
+                                    <select name="newManagerProUid" class="form-control form-control-sm">
+                                        <option value="">-- Select Manager --</option>
+                                        @foreach($pros as $iPro)
+                                            <option value="{{ $iPro->uid }}" {{ $iPro->id === $program->manager_pro_id ? 'selected' : ''  }}>
+                                                {{ $iPro->displayName() }}
+                                            </option>
+                                        @endforeach
+                                    </select>
+                                </div>
+                                <div class="mb-0">
+                                    <button class="btn btn-primary btn-sm" submit>Submit</button>
+                                    <button class="btn btn-default border btn-sm" cancel>Cancel</button>
+                                </div>
+                            </form>
+                        </div>
+                        @endif
+                    </div>
+                </div>
+            </div>
+
+            <div class="card-body p-0">
+
+                <div class="row">
+                    <div class="col-4 pr-0 border-right">
+                        <div class="">
+                            {{-- setup --}}
+                            <div class="border-bottom py-1 px-3">
+                                <span class="mr-1 text-secondary">Setup</span>
+                                @if($program->is_setup_complete === 'YES')
+                                    <i class="fa fa-check text-success"></i>
+                                @else
+                                    <i class="fa fa-exclamation-triangle text-warning"></i>
+                                @endif
+                                @if($pro->pro_type === 'ADMIN')
+                                <div moe relative class="ml-1">
+                                    <a start show class="py-0 font-weight-normal on-hover-opaque"><i class="fa fa-edit"></i></a>
+                                    <form url="/api/clientProgram/editSetupInfo">
+                                        <input type="hidden" name="uid" value="{{ $program->uid }}">
+                                        <div class="mb-2">
+                                            <label class="text-sm mb-1 text-secondary">Setup Status</label>
+                                            <select class="form-control form-control-sm bg-light" name="isSetupComplete" required>
+                                                <option value="">-- select --</option>
+                                                <option value="YES" {{ $program->is_setup_complete === 'YES' ? 'selected' : '' }}>Yes</option>
+                                                <option value="NO" {{ $program->is_setup_complete === 'NO' ? 'selected' : '' }}>No</option>
+                                                <option value="UNKNOWN" {{ $program->is_setup_complete === 'UNKNOWN' ? 'selected' : '' }}>Unknown</option>
+                                            </select>
+                                        </div>
+                                        <div class="mb-2">
+                                            <label class="text-sm mb-1 text-secondary">Setup Status Memo</label>
+                                            <textarea class="form-control form-control-sm" rows="2" name="setupStatusMemo" value="{{ $program->setup_status_memo }}" placeholder="Memo"></textarea>
+                                        </div>
+                                        <div class="mb-2">
+                                            <label class="text-sm mb-1 text-secondary">Setup Detail</label>
+                                            <textarea class="form-control form-control-sm" rows="2" name="setupDetail" value="{{ $program->setup_detail }}" placeholder="Detail"></textarea>
+                                        </div>
+                                        <div class="mb-2">
+                                            <label class="text-sm mb-1 text-secondary">Device Identifier</label>
+                                            <input type="text" class="form-control form-control-sm" name="deviceIdentifier" value="{{ $program->device_identifier }}" placeholder="Device ID">
+                                        </div>
+                                        <div class="d-flex align-items-center">
+                                            <button class="btn btn-sm btn-primary mr-2" submit>Ok</button>
+                                            <button class="btn btn-sm btn-default mr-2 border" cancel>Cancel</button>
+                                        </div>
+                                    </form>
+                                </div>
+                                @endif
+                            </div>
+
+                            {{-- onboarding --}}
+                            <div class="border-bottom py-1 px-3">
+                                <span class="mr-1 text-secondary">Onboarding</span>
+                                @if($program->has_mcp_done_onboarding_visit === 'YES')
+                                    <i class="fa fa-check text-success"></i>
+                                @else
+                                    <i class="fa fa-exclamation-triangle text-warning"></i>
+                                @endif
+                                @if($pro->pro_type === 'ADMIN')
+                                <div moe relative class="ml-1">
+                                    <a start show class="py-0 font-weight-normal on-hover-opaque"><i class="fa fa-edit"></i></a>
+                                    <form url="/api/clientProgram/editMcpOnboardingVisitInfo">
+                                        <input type="hidden" name="uid" value="{{ $program->uid }}">
+                                        <div class="mb-2">
+                                            <label class="text-sm mb-1 text-secondary">OB Visit Done?</label>
+                                            <select class="form-control form-control-sm bg-light" name="hasMcpDoneOnboardingVisit" required>
+                                                <option value="">-- select --</option>
+                                                <option value="YES" {{ $program->has_mcp_done_onboarding_visit === 'YES' ? 'selected' : '' }}>Yes</option>
+                                                <option value="NO" {{ $program->has_mcp_done_onboarding_visit === 'NO' ? 'selected' : '' }}>No</option>
+                                                <option value="UNKNOWN" {{ $program->has_mcp_done_onboarding_visit === 'UNKNOWN' ? 'selected' : '' }}>Unknown</option>
+                                            </select>
+                                        </div>
+                                        <div class="mb-2">
+                                            <label class="text-sm mb-1 text-secondary">Date</label>
+                                            <input type="date" class="form-control form-control-sm" name="mcpOnboardingVisitDate" value="{{ $program->mcp_onboarding_visit_date }}" placeholder="">
+                                        </div>
+                                        <div class="d-flex align-items-center">
+                                            <button class="btn btn-sm btn-primary mr-2" submit>Ok</button>
+                                            <button class="btn btn-sm btn-default mr-2 border" cancel>Cancel</button>
+                                        </div>
+                                    </form>
+                                </div>
+                                @endif
+                            </div>
+
+                            {{-- measurement labels --}}
+                            <div class="border-bottom py-1 px-3">
+                                <div class="d-flex">
+                                    <span class="mr-2 text-secondary">Measurement Types: </span>
+                                    <?php
+                                    $labels = '-';
+                                    if ($programCategories && count($programCategories)) {
+                                        $labels = implode(", ", $programCategories);
+                                    }
+                                    ?>
+                                    <span class="mr-2">{{ $labels }}</span>
+                                    @if($pro->pro_type === 'ADMIN')
+                                    <div moe large>
+                                        <a href="#" show start class="on-hover-opaque"><i class="fa fa-edit"></i></a>
+                                        <form url="/api/clientProgram/updateCategories">
+                                            <input type="hidden" name="uid" value="{{ $program->uid }}">
+                                            <div class="mb-2">
+                                                <label class="mb-1 text-sm text-secondary">Measurement Types</label>
+                                                <input type="hidden" name="measurementLabels" v-model="existingMeasurementLabels['{{ $program->uid }}']">
+                                                <select multiple edit class="form-control form-control-sm" data-uid="{{ $program->uid }}">
+                                                    <option value=""></option>
+                                                    <option value="BP" {{ strpos($program->measurement_labels, "|BP|") !== FALSE ? 'selected' : '' }}>Blood Pressure</option>
+                                                    <option value="BS" {{ strpos($program->measurement_labels, "|BS|") !== FALSE ? 'selected' : '' }}>Blood Sugar</option>
+                                                    <option value="WEIGHT" {{ strpos($program->measurement_labels, "|WEIGHT|") !== FALSE ? 'selected' : '' }}>Weight</option>
+                                                </select>
+                                            </div>
+                                            <div class="mb-0">
+                                                <button class="btn btn-primary btn-sm" submit="">Submit</button>
+                                                <button class="btn btn-default border btn-sm" cancel="">Cancel</button>
+                                            </div>
+                                        </form>
+                                    </div>
+                                    @endif
+                                </div>
+                            </div>
+
+                            {{-- icds --}}
+                            <div class="border-bottom py-1 px-3">
+                                <div class="d-flex">
+                                    <span class="mr-2 text-secondary">ICDs: </span>
+                                    <?php
+                                    $labels = '-';
+                                    if ($programICDs && count($programICDs)) {
+                                        $labels = implode(", ", $programICDs);
+                                    }
+                                    ?>
+                                    <span class="mr-2">{{ $labels }}</span>
+                                    @if($pro->pro_type === 'ADMIN')
+                                    <div moe large>
+                                        <a href="#" show start class="on-hover-opaque"><i class="fa fa-edit"></i></a>
+                                        <form url="/api/clientProgram/updateIcds">
+                                            <input type="hidden" name="uid" value="{{ $program->uid }}">
+                                            <input type="hidden" name="icds" v-model="existingICDsFlattened['{{ $program->uid }}']">
+                                            <div class="mb-2">
+                                                <label class="mb-1 text-sm text-secondary">ICDs
+                                                    <a href="#" class="ml-3"
+                                                       v-on:click.prevent="addICDItem('{{ $program->uid }}')">+ Add</a>
+                                                </label>
+                                                <div v-for="(icd, index) in existingICDs['{{ $program->uid }}']" class="d-flex align-items-center mb-2">
+                                                    <input required type="text"
+                                                           data-field="icd" data-program="{{ $program->uid }}" :data-index="index"
+                                                           v-model="existingICDs['{{ $program->uid }}'][index]"
+                                                           class="form-control form-control-sm flex-grow-1">
+                                                    <a v-if="existingICDs['{{ $program->uid }}'].length > 1"
+                                                       class="on-hover-opaque text-danger ml-2"
+                                                       v-on:click.prevent="existingICDs['{{ $program->uid }}'].splice(index, 1)">
+                                                        <i class="fa fa-trash-alt"></i>
+                                                    </a>
+                                                </div>
+                                            </div>
+                                            <div class="mb-0">
+                                                <button class="btn btn-primary btn-sm" submit="">Submit</button>
+                                                <button class="btn btn-default border btn-sm" cancel="">Cancel</button>
+                                            </div>
+                                        </form>
+                                    </div>
+                                    @endif
+                                </div>
+                            </div>
+
+                            {{-- work spec --}}
+                            <div class="py-1 px-3">
+                                <div class="d-flex">
+                                    <span class="mr-2 text-secondary">Work Spec: </span>
+                                    <span class="mr-2 font-weight-bold">{{ $program->min_monthly_time_in_minutes }}m - {{ $program->max_monthly_time_in_minutes }}m</span>
+                                    @if($pro->pro_type === 'ADMIN')
+                                    <div moe>
+                                        <a start show class="py-0 font-weight-normal on-hover-opaque"><i class="fa fa-edit"></i></a>
+                                        <form url="/api/clientProgram/editWorkSpec">
+                                            <input type="hidden" name="uid" value="{{ $program->uid }}">
+                                            <div class="mb-2">
+                                                <label class="text-sm mb-1 text-secondary">Min Monthly Time (minutes)</label>
+                                                <input type="number" class="form-control form-control-sm" name="minMonthlyTimeInMinutes" value="{{ $program->min_monthly_time_in_minutes }}" placeholder="">
+                                            </div>
+                                            <div class="mb-2">
+                                                <label class="text-sm mb-1 text-secondary">Max Monthly Time (minutes)</label>
+                                                <input type="number" class="form-control form-control-sm" name="maxMonthlyTimeInMinutes" value="{{ $program->max_monthly_time_in_minutes }}" placeholder="">
+                                            </div>
+                                            <div class="mb-2">
+                                                <label class="text-sm mb-1 text-secondary">Time In Minutes Memo</label>
+                                                <textarea class="form-control form-control-sm" rows="2" name="timeInMinutesMemo" value="{{ $program->time_in_minutes_memo }}" placeholder=""></textarea>
+                                            </div>
+                                            <div class="mb-2">
+                                                <label class="text-sm mb-1 text-secondary">Goal</label>
+                                                <textarea class="form-control form-control-sm" rows="2" name="goal" value="{{ $program->goal }}" placeholder=""></textarea>
+                                            </div>
+                                            <div class="mb-2">
+                                                <label class="text-sm mb-1 text-secondary">Sticky Note</label>
+                                                <textarea class="form-control form-control-sm" rows="2" name="stickyNote" value="{{ $program->sticky_note }}" placeholder=""></textarea>
+                                            </div>
+                                            <div class="mb-2">
+                                                <label class="mb-1 text-secondary d-flex align-items-center">
+                                                    <span class="mr-2">Change Current Month</span>
+                                                    <input type="checkbox" name="changeCurrentMonth">
+                                                </label>
+                                            </div>
+                                            <div class="d-flex align-items-center">
+                                                <button class="btn btn-sm btn-primary mr-2" submit>Ok</button>
+                                                <button class="btn btn-sm btn-default mr-2 border" cancel>Cancel</button>
+                                            </div>
+                                        </form>
+                                    </div>
+                                    @endif
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-8 pl-0">
+                        <div class="border-bottom py-1 px-2 bg-light d-flex">
+                            <span class="font-weight-bold">Measurements</span>
+                            @if($programCategories && count($programCategories))
+                                <div class="d-inline-flex">
+                                @foreach($programCategories as $category)
+                                    <span class="mx-2 text-secondary">|</span>
+                                    <div moe relative>
+                                        <a href="#" start show>+ {{ $category }}</a>
+                                        <form url="/api/measurement/create">
+                                            <input type="hidden" name="clientUid" value="{{ $patient->uid }}">
+                                            <input type="hidden" name="label" value="{{ $category }}">
+                                            <div class="mb-2">
+                                                <label class="text-sm text-secondary mb-1 font-weight-bold">{{ $category }}</label>
+                                                <input required autofocus type="text" class="form-control form-control-sm"
+                                                       name="value" placeholder="Value">
+                                            </div>
+                                            <div class="mb-2">
+                                                <input required type="date" class="form-control form-control-sm"
+                                                       name="effectiveDate" max="{{ date('Y-m-d') }}" value="{{ date('Y-m-d') }}">
+                                            </div>
+                                            <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>
+                                @endforeach
+                                </div>
+                            @endif
+                            @if(!!$programMonth)
+                                <div class="ml-auto pr-2">
+                                    <b>{{ minutes_to_hhmm($programMonth->time_in_minutes) }}</b> billed,
+                                    <b>{{ minutes_to_hhmm($program->max_monthly_time_in_minutes - $programMonth->time_in_minutes) }}</b> remaining
+                                </div>
+                            @endif
+                        </div>
+                        <?php
+                        $programMeasurements = [];
+                        foreach($patient->allMeasurements as $measurement) {
+                            $measurementED = strtotime($measurement->effective_date);
+                            if(in_array($measurement->label, $programCategories) !== FALSE) {
+                                if($filter === 'all' ||
+                                    (date('Y') === date('Y', $measurementED) && date('m') === date('m', $measurementED))) {
+                                    $programMeasurements[] = $measurement;
+                                }
+                            }
+                        }
+                        ?>
+                        @foreach($programMeasurements as $measurement)
+                            <?php $minutes = $measurement->minutesEntered($pro); ?>
+                            @include('app/patient/partials/measurement', ['measurement' => $measurement, 'child' => false])
+                            @foreach($measurement->childMeasurements() as $childMeasurement)
+                                @include('app/patient/partials/measurement', ['measurement' => $childMeasurement, 'child' => true])
+                            @endforeach
+                        @endforeach
+                        @if(!count($programMeasurements))
+                            <div class="text-secondary py-1 px-2 border-0">
+                                No measurements to show
+                            </div>
+                        @endif
+                    </div>
+                </div>
+
+            </div>
+
+        </div>
+        @endforeach
+
+    </div>
+
+    <script>
+        <?php
+            $measurementLabels = [];
+            foreach($patient->clientPrograms as $program) {
+                $measurementLabels[$program->uid] = $program->measurement_labels;
+            }
+            $icds = [];
+            $icdsFlattened = [];
+            foreach($patient->clientPrograms as $program) {
+                $icds[$program->uid] = array_values(array_filter(explode("|", $program->icds), function($_item) {
+                    return !empty($_item);
+                }));
+                $icdsFlattened[$program->uid] = $program->icds;
+            }
+        ?>
+        (function() {
+            function init() {
+                window.programsComponent = new Vue({
+                    el: '#programsComponent',
+                    delimiters: ['@{{', '}}'],
+                    data: {
+                        newMeasurementLabels: '',
+                        existingMeasurementLabels: <?= json_encode($measurementLabels) ?>,
+                        existingICDs: <?= json_encode($icds) ?>,
+                        existingICDsFlattened: <?= json_encode($icdsFlattened) ?>,
+                    },
+                    methods: {
+                        combine: function(_array) {
+                            if(!_array || !_array.length) return '';
+                            let valid = _array.filter(function(_x) {
+                                return !!_x;
+                            });
+                            if(!valid || !valid.length) return '';
+                            return '|' + valid.join('|') + '|';
+                        },
+                        addICDItem: function(_programUid) {
+                            this.existingICDs[_programUid].push('');
+                            let self = this;
+                            Vue.nextTick(function() {
+                                self.initICDAutoSuggest();
+                            });
+                        },
+                        initICDAutoSuggest: function() {
+                            let self = this;
+                            $('#programsComponent 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.existingICDs[$(elem).attr('data-program')][+$(elem).attr('data-index')] = acData[0].code;
+                                    self.existingICDsFlattened[$(elem).attr('data-program')] = self.combine(self.existingICDs[$(elem).attr('data-program')]);
+                                    return false;
+                                });
+                                $(elem).attr('ac-initialized', 1);
+                            });
+                        },
+                    },
+                    watch: {
+                        existingICDs: {
+                            handler: function(val, oldVal) {
+                                let self = this;
+                                for(let x in this.existingICDs) {
+                                    if(this.existingICDs.hasOwnProperty(x)) {
+                                        this.existingICDsFlattened[x] = this.combine(this.existingICDs[x]);
+                                    }
+                                }
+                            },
+                            deep: true
+                        }
+                    },
+                    mounted: function() {
+                        let self = this;
+                        $('#programsComponent [moe][initialized]').removeAttr('initialized');
+                        initMoes();
+                        $('select[multiple][add]')
+                            .select2({
+                                width: '100%',
+                                placeholder: '-- select --'
+                            })
+                            .on('change', function() {
+                                if($(this).val() && $(this).val().length) {
+                                    self.newMeasurementLabels = '|' + $(this).val().join('|') + '|';
+                                }
+                                else {
+                                    self.newMeasurementLabels = '';
+                                }
+                            });
+                        $('select[multiple][edit]')
+                            .select2({
+                                width: '100%',
+                                placeholder: '-- select --'
+                            })
+                            .on('change', function() {
+                                if($(this).val() && $(this).val().length) {
+                                    self.existingMeasurementLabels[$(this).attr('data-uid')] = '|' + $(this).val().join('|') + '|';
+                                }
+                                else {
+                                    self.existingMeasurementLabels[$(this).attr('data-uid')] = '';
+                                }
+                            });
+
+                        // give 1 row min for editing
+                        for(let x in this.existingICDs) {
+                            if(this.existingICDs.hasOwnProperty(x)) {
+                                if(!this.existingICDs[x] || !this.existingICDs[x].length || !Array.isArray(this.existingICDs[x])) {
+                                    this.existingICDs[x] = [''];
+                                }
+                            }
+                        }
+
+                        Vue.nextTick(function() {
+                            self.initICDAutoSuggest();
+                        });
+
+                    }
+                });
+            }
+            addMCInitializer('programs', init, '#programsComponent');
+        })();
+    </script>
+@endsection

+ 24 - 0
resources/views/app/patient/settings.blade.php

@@ -168,6 +168,30 @@
     </div>
     @endif
 
+    <hr class="m-negator my-3">
+    <span>Physician: <b>{{ $patient->pcp ? $patient->pcp->displayName() : '-' }}</b></span>
+    @if($pro->pro_type == 'ADMIN')
+        <div moe class="ml-2">
+            <a start show><i class="fa fa-edit"></i></a>
+            <form url="/api/client/putPhysicianPro" class="mcp-theme-1">
+                <input type="hidden" name="uid" value="{{$patient->uid}}">
+                <div class="mb-2">
+                    <label class="text-secondary text-sm">Physician Pro</label>
+                    <select name="physicianProUid" class="form-control form-control-sm">
+                        <option value=""> --select-- </option>
+                        @foreach($pros as $iPro)
+                            <option value="{{$iPro->uid}}" {{ $patient->pcp && $iPro->uid === $patient->pcp->uid ? 'selected' : '' }}>{{$iPro->displayName()}}</option>
+                        @endforeach
+                    </select>
+                </div>
+                <div>
+                    <button submit class="btn btn-sm btn-primary mr-1">Submit</button>
+                    <button cancel class="btn btn-sm btn-default border">Cancel</button>
+                </div>
+            </form>
+        </div>
+    @endif
+
     <hr class="m-negator my-3">
     <span>MCP Onboarding Visit: <b>{{ $patient->has_mcp_done_onboarding_visit }}</b></span>
     <span moe class="ml-2">

+ 51 - 9
resources/views/app/patients.blade.php

@@ -2,6 +2,27 @@
 
 @section('content')
 
+    <?php
+    $showProgramsColumn = false;
+    foreach($patients as $patient) {
+        if(count($patient->clientPrograms)) {
+            if($pro->pro_type === 'ADMIN') {
+                $showProgramsColumn = true;
+                break;
+            }
+            else {
+                foreach($patient->clientPrograms as $clientProgram) {
+                    if(in_array($pro->id, [$clientProgram->mcp_pro_id, $clientProgram->manager_pro_id])) {
+                        $showProgramsColumn = true;
+                        break;
+                    }
+                }
+                if($showProgramsColumn) break;
+            }
+        }
+    }
+    ?>
+
     <div class="p-3 mcp-theme-1">
     <div class="card">
 
@@ -19,15 +40,14 @@
             <table class="table table-condensed p-0 m-0">
                 <thead class="bg-light">
                 <tr>
-                    <th></th>
+                    <th class="border-0"></th>
                     <th class="px-3 border-0">#</th>
-                    <th class="border-0">Name</th>
+                    <th class="border-0">Patient</th>
                     <th class="border-0">Created At</th>
-                    <th class="border-0">DOB</th>
-                    <th class="border-0">Sex</th>
+                    @if($showProgramsColumn)<th class="border-0">Program(s)</th>@endif
                     <th class="border-0">MCN</th>
                     <th class="border-0">PCP</th>
-                    <th class="border-0">RMM</th>
+                    {{--<th class="border-0">RMM</th>--}}
                     <th class="border-0">Upcoming Appointments</th>
                 </tr>
                 </thead>
@@ -45,10 +65,32 @@
                             @if($patient->has_mcp_done_onboarding_visit !== 'YES')
                             <span title="MCP Onboarding Visit Pending"><i class="fa fa-exclamation-triangle"></i></span>
                             @endif
+                            <div>{{ friendly_date_time($patient->dob, false) }}{{ $patient->sex === 'M' ? ', Male' : ($patient->sex === ', F' ? 'Female' : '') }}</div>
                         </td>
                         <td>{{friendly_date_time_short_with_tz($patient->created_at, true, 'EASTERN')}}</td>
-                        <td>{{ friendly_date_time($patient->dob, false) }}</td>
-                        <td>{{ $patient->sex === 'M' ? 'Male' : ($patient->sex === 'F' ? 'Female' : '-') }}</td>
+                        @if($showProgramsColumn)
+                        <td>
+                            <?php $programNumber = 0; ?>
+                            @foreach($patient->clientPrograms as $clientProgram)
+                                <?php
+                                    if($pro->pro_type === 'ADMIN' ||
+                                        in_array($pro->id, [$clientProgram->mcp_pro_id, $clientProgram->manager_pro_id])
+                                    ) {
+                                        // $program = $clientProgram->program;
+                                        $programNumber++;
+                                ?>
+                                <div class="mb-1 text-nowrap">
+                                    {{ $programNumber }}. {{ $clientProgram->title }}
+                                    @if($clientProgram->has_mcp_done_onboarding_visit !== 'YES')
+                                        <span title="Onboarding Pending" class="ml-1"><i class="fa fa-exclamation-triangle"></i></span>
+                                    @else
+                                        <span title="Onboarding Complete" class="ml-1 text-secondary"><i class="fa fa-check"></i></span>
+                                    @endif
+                                </div>
+                                <?php } ?>
+                            @endforeach
+                        </td>
+                        @endif
                         <td>
                             @if($patient->was_medicare_validation_successful && $patient->is_part_b_primary == 'YES')
                             Covered <span style="color:green"><i class="fa fa-check-circle"></i></span>
@@ -69,9 +111,9 @@
                         <td>
                             {{ $patient->mcp ? $patient->mcp->displayName() : '-' }}
                         </td>
-                        <td>
+                        {{--<td>
                             {{ $patient->rmm ? $patient->rmm->displayName() : '-' }}
-                        </td>
+                        </td>--}}
                         <td>
                             <table class="table table-sm border-0 my-0">
                                 <tbody>

+ 21 - 47
resources/views/app/practice-management/calendar.blade.php

@@ -247,7 +247,7 @@
         <div class="stag-popup stag-popup-sm mcp-theme-1" stag-popup-key="client-edit-appointment">
             <form method="POST" action="/api/appointment/update" id="editApptForm">
                 <h3 class="stag-popup-title">
-                    <span>Edit Appointment</span>
+                    <span>Appointment Details</span>
                     <a href="#" class="ml-auto text-secondary"
                        onclick="return closeStagPopup()"><i class="fa fa-times-circle"></i></a>
                 </h3>
@@ -257,9 +257,10 @@
                         Patient
                     </div>
                     <div class="col-9 font-weight-bold">
-                        <input type="text"
-                               class="form-control form-control-sm"
-                               :value="editAppointment.clientName" readonly>
+                        @{{ editAppointment.clientName }}
+                        <a :href="'/patients/view/' + editAppointment.clientUid + '/calendar/' + editAppointment.uid" class="ml-2">
+                            <i class="fa fa-calendar"></i>
+                        </a>
                     </div>
                 </div>
                 <div class="row mb-2">
@@ -267,15 +268,7 @@
                         Pro
                     </div>
                     <div class="col-9 font-weight-bold">
-                        <select id="editApptPro" name="proUid" required
-                                v-model="editAppointment.proUid"
-                                class="form-control form-control-sm">
-                            @foreach($pros as $iPro)
-                                <option value="{{$iPro->uid}}">
-                                    {{$iPro->displayName()}}
-                                </option>
-                            @endforeach
-                        </select>
+                        @{{ editAppointment.proName }}
                     </div>
                 </div>
                 <div class="row mb-2">
@@ -283,9 +276,7 @@
                         Date
                     </div>
                     <div class="col-9 font-weight-bold">
-                        <input type="date" name="date" required
-                               class="form-control form-control-sm"
-                               v-model="editAppointment.date">
+                        @{{ editAppointment.date }}
                     </div>
                 </div>
                 <div class="row mb-2">
@@ -293,9 +284,7 @@
                         Start Time
                     </div>
                     <div class="col-9 font-weight-bold">
-                        <input type="time" name="startTime" required
-                               class="form-control form-control-sm"
-                               v-model="editAppointment.startTime">
+                        @{{ editAppointment.startTime ? editAppointment.startTime : '-' }}
                     </div>
                 </div>
                 <div class="row mb-2">
@@ -303,9 +292,7 @@
                         End Time
                     </div>
                     <div class="col-9 font-weight-bold">
-                        <input type="time" name="endTime"
-                               class="form-control form-control-sm"
-                               v-model="editAppointment.endTime">
+                        @{{ editAppointment.endTime ? editAppointment.endTime : '-' }}
                     </div>
                 </div>
                 <input type="hidden" name="timeZone" :value="timezone">
@@ -314,9 +301,7 @@
                         Timezone
                     </div>
                     <div class="col-9 font-weight-bold">
-                        <input type="text"
-                               class="form-control form-control-sm"
-                               :value="timezone" readonly>
+                        @{{ timezone }}
                     </div>
                 </div>
                 <div class="row mb-2">
@@ -324,15 +309,7 @@
                         Status
                     </div>
                     <div class="col-9 font-weight-bold">
-                        <select id="editApptStatus" name="status" required
-                                v-model="editAppointment.status"
-                                class="form-control form-control-sm font-weight-bold px-1">
-                            <option value="CREATED">CREATED</option>
-                            <option value="CONFIRMED">CONFIRMED</option>
-                            <option value="CANCELLED">CANCELLED</option>
-                            <option value="COMPLETED">COMPLETED</option>
-                            <option value="ABANDONED">ABANDONED</option>
-                        </select>
+                        @{{ editAppointment.status ? editAppointment.status : '-' }}
                     </div>
                 </div>
                 <div class="row mb-2">
@@ -340,9 +317,7 @@
                         Title
                     </div>
                     <div class="col-9 font-weight-bold">
-                        <input type="text" name="title"
-                               class="form-control form-control-sm"
-                               v-model="editAppointment.title">
+                        @{{ editAppointment.title ? editAppointment.title : '-' }}
                     </div>
                 </div>
                 <div class="row mb-2">
@@ -350,17 +325,12 @@
                         Description
                     </div>
                     <div class="col-9 font-weight-bold">
-                        <textarea name="description"
-                                  class="form-control form-control-sm"
-                                  v-model="editAppointment.description"></textarea>
+                        @{{ editAppointment.description ? editAppointment.description : '-' }}
                     </div>
                 </div>
                 <div class="d-flex align-items-center justify-content-center">
-                    <button class="btn btn-sm btn-primary mr-2"
-                            :disabled="inProgress"
-                            v-on:click.prevent="updateAppointment()">Submit</button>
                     <button class="btn btn-sm btn-default border"
-                            onclick="return closeStagPopup()">Cancel</button>
+                            onclick="return closeStagPopup()">Close</button>
                 </div>
             </form>
         </div>
@@ -398,7 +368,9 @@
                         // edit appt.
                         editAppointment: {
                             proUid: '',
+                            clientUid: '',
                             clientName: '',
+                            proName: '',
                             date: '',
                             startTime: '',
                             endTime: '',
@@ -733,9 +705,11 @@
                         },
                         showEditAppointmentModal: function() {
                             // setup model data
-                            /*this.inProgress = false;
+                            this.inProgress = false;
                             this.editAppointment.uid = this.selectedEvent.extendedProps.appointmentUid;
+                            this.editAppointment.clientUid = this.selectedEvent.extendedProps.clientUid;
                             this.editAppointment.clientName = this.selectedEvent.extendedProps.clientName;
+                            this.editAppointment.proName = this.selectedEvent.extendedProps.proName;
                             this.editAppointment.proUid = this.selectedEvent.extendedProps.proUid;
                             this.editAppointment.date = this.dateStr(this.selectedEvent.start);
                             this.editAppointment.startTime = this.timeStr(this.selectedEvent.start);
@@ -745,9 +719,9 @@
                             this.editAppointment.description = this.selectedEvent.extendedProps.description;
                             this.editAppointment.status = this.selectedEvent.extendedProps.status;
                             Vue.nextTick(function() {
-                                $('#editApptPro').trigger('change');
                                 showStagPopup('client-edit-appointment');
-                            });*/
+                                initFastLoad($('[stag-popup-key="client-edit-appointment"]'));
+                            });
                         },
                         updateAppointment: function() {
                             let form = $('#editApptForm');

+ 68 - 0
resources/views/app/practice-management/my-favorites.blade.php

@@ -0,0 +1,68 @@
+@extends ('layouts/template')
+
+@section('content')
+
+    <div class="p-3 mcp-theme-1">
+    <div class="card">
+
+        <div class="card-header px-3 py-2 d-flex align-items-center">
+            <strong class="">
+                <i class="fas fa-user-injured"></i>
+                Favorites
+            </strong>
+            <span class="ml-3 text-secondary">
+                You can add Allergy, Rx and Dx items to favorites from the respective sections
+            </span>
+            <select class="ml-auto max-width-300px form-control form-control-sm"
+                    onchange="fastLoad('/practice-management/my-favorites/' + this.value, true, false, false)">
+                <option value="allergies" {{ $filter === 'allergies' ? 'selected' : '' }}>Allergies</option>
+                <option value="rx" {{ $filter === 'rx' ? 'selected' : '' }}>Rx</option>
+                <option value="dx" {{ $filter === 'dx' ? 'selected' : '' }}>Dx</option>
+                <option value="all" {{ $filter === 'all' ? 'selected' : '' }}>All favorites</option>
+            </select>
+        </div>
+        <div class="card-body p-0">
+            <table class="table table-sm table-condensed p-0 m-0">
+                <thead class="bg-light">
+                <tr>
+                    <th class="px-3 border-0 width-100px">Category</th>
+                    <th class="border-0">Content</th>
+                    <th class="border-0 w-25">&nbsp;</th>
+                </tr>
+                </thead>
+                <tbody>
+                @foreach($myFavorites as $favorite)
+                    <tr>
+                        <td class="px-3">{{ ucwords($favorite->category) }}</td>
+                        <td>
+                            <?php $parsed = json_decode($favorite->data); ?>
+                            {{ $parsed->title }}
+                        </td>
+                        <td>
+                            <div class="d-flex align-items-center">
+                                <div moe relative wide class="mr-2">
+                                    <a start show class="text-danger">
+                                        <i class="fa fa-trash-alt"></i>
+                                    </a>
+                                    <form url="/api/proFavorite/remove" right>
+                                        <input type="hidden" name="uid" value="{{$favorite->uid}}">
+                                        <p>
+                                            Are you sure you want to remove this from your favorites?
+                                        </p>
+                                        <div class="form-group m-0">
+                                            <button submit class="btn btn-danger btn-sm mr-2">Yes</button>
+                                            <button cancel class="btn btn-default border btn-sm mr-2">No</button>
+                                        </div>
+                                    </form>
+                                </div>
+                            </div>
+                        </td>
+                    </tr>
+                @endforeach
+                </tbody>
+            </table>
+        </div>
+    </div>
+    </div>
+
+@endsection

+ 762 - 0
resources/views/app/video/call-agora-v2.blade.php

@@ -0,0 +1,762 @@
+<!DOCTYPE html>
+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <link href="https://fonts.googleapis.com/css?family=Nunito:200,600,700" rel="stylesheet">
+    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
+    <link href="/css/app.css" rel="stylesheet">
+    <link rel="stylesheet" href="/fontawesome-free/css/all.min.css">
+    <link href="/css/meeting.css" rel="stylesheet">
+    <link href="/css/style.css" rel="stylesheet">
+    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.0/sockjs.min.js"
+            integrity="sha512-5yJ548VSnLflcRxWNqVWYeQZnby8D8fJTmYRLyvs445j1XmzR8cnWi85lcHx3CUEeAX+GrK3TqTfzOO6LKDpdw=="
+            crossorigin="anonymous"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"
+            integrity="sha512-iKDtgDyTHjAitUDdLljGhenhPwrbBfqTKWO1mkhSFH3A7blITC9MhYon6SjnMhp4o0rADGw9yAC6EW4t5a4K3g=="
+            crossorigin="anonymous"></script>
+    <script defer src=//download.agora.io/sdk/web/AgoraRTC_N-4.1.0.js></script>
+    <title>Scholar: Meet</title>
+</head>
+
+<body class="p-0 m-0">
+
+    <div id="proCallComponent" v-cloak>
+
+        <div class="d-flex px-2 border-bottom mb-2 {{ $client ? '' : 'justify-content-center' }}">
+            @if($client)
+            <div class="py-2 font-weight-normal mcp-theme-1 d-inline-flex align-items-center flex-grow-1">
+                <i class="fa fa-user-injured small mr-2"></i>
+                <a href="#" onclick="return window.top.openInLHS('/patients/view/{{$client->uid}}')">
+                    <span class="font-weight-bold">{{ $client->displayName() }}</span>
+                </a>
+                <span class="text-secondary ml-3">{{ $pro->displayName() }}</span>
+                <span class="text-secondary ml-3">{{ $session->uid }}</span>
+                <span class="text-secondary ml-3">{{ date('H:i:s') }}</span>
+            </div>
+            @endif
+
+            <div class="py-2 d-flex align-items-center">
+                <a href="#" v-if="ringer" v-on:click.prevent="toggleRinger()"
+                   class="font-weight-bold btn btn-sm btn-success">
+                    Ringer
+                    <i class="ml-1 fa fa-volume-up"></i>
+                </a>
+                <a href="#" v-if="!ringer" v-on:click.prevent="toggleRinger()"
+                   class="font-weight-bold btn btn-sm btn-warning">
+                    Ringer
+                    <i class="ml-1 fa fa-volume-mute"></i>
+                </a>
+            </div>
+        </div>
+
+        @if($client)
+        <div class="">
+            <div class="main-view mx-auto">
+                <div id="self-view" class="full-view"
+                     :data-self="mainViewParticipant.self"
+                     :data-uid="mainViewParticipant.uid"
+                     :data-name="mainViewParticipant.name"
+                     :data-type="mainViewParticipant.type"></div>
+                <div class="thumbs">
+                    <div v-if="mainViewParticipant.uid !== myMediaServiceIdentifier"
+                         :id="'remote-view-' + myMediaServiceIdentifier"
+                         :data-self="true"
+                         :data-uid="myMediaServiceIdentifier"
+                         :data-name="'You'"
+                         :data-type="'PRO'"   {{-- TODO: change in FE4 --}}
+                         :data-audio="myMedia && myMedia.isMicrophoneOn ? 'on' : 'off'"
+                         v-on:click.prevent="showInCenterView(true, myMediaServiceIdentifier, 'You', 'PRO')"
+                         class="remote-view thumb-view c-pointer">
+                        <i v-show="!myMedia || !myMedia.isMicrophoneOn" class="fa fa-microphone-slash muted"></i>
+                    </div>
+                    <div v-for="participant in otherParticipants"
+                         v-if="mainViewParticipant.uid !== (+participant.mediaServiceIdentifier)"
+                         :id="'remote-view-' + participant.mediaServiceIdentifier"
+                         :data-self="false"
+                         :data-uid="participant.mediaServiceIdentifier"
+                         :data-name="participant.displayName"
+                         :data-type="participant.participantType"
+                         :data-audio="participant.media && participant.media.isMicrophoneOn ? 'on' : 'off'"
+                         v-on:click.prevent="showInCenterView(false, participant.mediaServiceIdentifier, participant.displayName, participant.participantType)"
+                         class="remote-view thumb-view c-pointer">
+                        <i v-show="!participant.media || !participant.media.isMicrophoneOn" class="fa fa-microphone-slash muted"></i>
+                    </div>
+                </div>
+                <div class="call-actions d-flex align-items-center">
+                    <button class="btn btn-danger rounded-circle"
+                            title="Leave Call"
+                            v-on:click.prevent="leaveClientRoom()">
+                        <i class="fa fa-phone"></i>
+                    </button>
+                    <button v-if="myMedia.isCameraOn" class="btn btn-default bg-light border rounded-circle"
+                            title="Stop Camera"
+                            v-on:click.prevent="myCameraIsOff()">
+                        <i class="fa fa-video"></i>
+                    </button>
+                    <button v-if="!myMedia.isCameraOn" class="btn btn-secondary rounded-circle"
+                            title="Start Camera"
+                            v-on:click.prevent="myCameraIsOn()">
+                        <i class="fa fa-video-slash"></i>
+                    </button>
+                    <button v-if="myMedia.isMicrophoneOn" class="btn btn-default bg-light border rounded-circle"
+                            title="Stop Microphone"
+                            v-on:click.prevent="myMicrophoneIsOff()">
+                        <i class="fa fa-microphone"></i>
+                    </button>
+                    <button v-if="!myMedia.isMicrophoneOn" class="btn btn-secondary rounded-circle"
+                            title="Start Microphone"
+                            v-on:click.prevent="myMicrophoneIsOn()">
+                        <i class="fa fa-microphone-slash"></i>
+                    </button>
+                </div>
+            </div>
+        </div>
+        @endif
+
+    </div>
+
+    <div class="border-top patient-queue mcp-theme-1" id="queueComponent" v-cloak>
+        <div class="bg-secondary text-white font-weight-bold text-center py-1" v-if="items.length > 0">
+            @{{ items.length }} patient@{{ items.length > 1 ? 's' : '' }} in the queue
+        </div>
+        <div class="bg-secondary text-white font-weight-bold text-center py-1" v-if="items.length === 0">
+            No patients in the queue
+        </div>
+        <div v-if="items && items.length" class="d-flex align-items-center my-1">
+            <div v-for="item in items">
+                <div class="queue-item border border-primary rounded mx-1" :title="item.name">
+                    <div class="patient-avatar mb-1 text-dark">@{{ item.initials }}</div>
+                    <div class="font-weight-bold small text-nowrap text-ellipsis">@{{ item.name }}</div>
+                </div>
+                <button class="btn btn-sm btn-primary mt-1 text-white font-weight-bold py-0 mx-auto d-block"
+                        v-on:click.prevent="claim(item.clientUid)">Claim</button>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        (function () {
+            window.proCallComponent = new Vue({
+                el: '#proCallComponent',
+                delimiters: ['@{{', '}}'],
+                data: {
+
+                    // model - declare up-front to make reactive - override with server data on mount
+                    amIInAMeeting: false,
+                    meetingType: '', // PRO/CLIENT,
+                    inMeetingForClientUid: '',
+                    inMeetingForClient: {
+                        clientMediaServiceRoomIdentifier: '',
+                        uid: '',
+                        displayName: '',
+                        dob: '',
+                    },
+                    myMediaServiceToken: '',
+                    myMediaServiceIdentifier: '',
+                    myMedia: {
+                        isCameraAcquired: false,
+                        isCameraOn: false,
+                        isMicrophoneAcquired: false,
+                        isMicrophoneOn: false,
+                    },
+                    otherParticipants: [
+                        {
+                            participantType: '', // PRO/CLIENT_GUEST,
+                            uid: '',
+                            mediaServiceIdentifier: '',
+                            displayName: '',
+                            media: {
+                                isCameraAcquired: false,
+                                isCameraOn: false,
+                                isMicrophoneAcquired: false,
+                                isMicrophoneOn: false,
+                            },
+                            awayMessage: '',
+                            deviceType: '',
+                            isMeetingAccessGranted: '',
+                            isSocketConnected: '',
+                        }
+                    ],
+                    awayMessage: '',
+
+                    // agora
+                    mediaServiceClient: null, // set on agora init
+                    appId: '{{ config('app.agora_appid') }}',
+                    channel: '', // set on mount
+                    myMicrophone: null,
+                    myCamera: null,
+
+                    // sockets
+                    backendWsURL: '{{ config('app.backend_ws_url') }}',
+                    socketClient: null,
+
+                    // other
+                    ringer: {{ $pro->is_ring_on ? 'true' : 'false' }},
+
+                    // agora <-> WS sync
+                    unrenderedParticipants: [],     // exists in otherParticipants, but not yet in DOM
+                    unresolvedParticipants: [],     // does not exist in otherParticipants, but came in via Agora
+                    unrenderedParticipantsTimer: false,
+                    unresolvedParticipantsTimer: false,
+
+                    // main-view & thumb views
+                    mainViewParticipant: {
+                        self: true,
+                        uid: '',
+                        type: 'PRO',
+                        name: 'You',
+                    },
+                },
+                methods: {
+
+                    toggleRinger: function () {
+                        let self = this, endPoint = this.ringer ? 'turnOffRing' : 'turnOnRing';
+                        $.post('/api/pro/' + endPoint, function (_data) {
+                            if (_data && _data.success) {
+                                self.ringer = !self.ringer;
+                            } else {
+                                if (_data.message) {
+                                    toastr.error(_data.message);
+                                } else {
+                                    toastr.error('Unable to change ringer status');
+                                }
+                            }
+                        }, 'json');
+                    },
+                    activateParty: function (_stream = 'self') {
+                        var current = $('.full-view');
+                        if (current.attr('data-stream') === _stream) return;
+                        current.removeClass('full-view').addClass('thumb-view');
+                        if (current.attr('data-type') === 'CLIENT') {
+                            current.prependTo('.thumbs');
+                        } else {
+                            current.appendTo('.thumbs');
+                        }
+                        if (_stream === 'self') {
+                            $('#self-view')
+                                .removeClass('thumb-view')
+                                .removeClass('disconnected-view')
+                                .addClass('full-view')
+                                .prependTo('.main-view');
+                        } else {
+                            $('div[data-stream="' + _stream + '"]')
+                                .removeClass('thumb-view')
+                                .removeClass('disconnected-view')
+                                .addClass('full-view')
+                                .prependTo('.main-view');
+                        }
+                    },
+
+                    enterClientRoomAsPro: function() {
+                        @if($client)
+                        $.post('/api/meeting/enterClientRoomAsPro', {clientUid: '{{ $client->uid }}'}, (_data) => {
+                            // TODO: check success
+                            console.log(_data);
+                            this.getMeetingInfo(true);
+                        });
+                        @endif
+                    },
+                    getMeetingInfo: function(_firstRun = false) {
+                        $.post('/api/meeting/getMyMeeting', (_data) => {
+                            if(_data && _data.success) {
+                                let state = _data.data;
+                                console.log(state);
+
+                                // overwrite model data
+                                this.amIInAMeeting = state.amIInAMeeting;
+                                this.inMeetingForClientUid = state.inMeetingForClientUid;
+                                this.inMeetingForClient.clientMediaServiceRoomIdentifier =
+                                    state.inMeetingForClient.clientMediaServiceRoomIdentifier;
+                                this.inMeetingForClient.uid = state.inMeetingForClient.uid;
+                                this.inMeetingForClient.displayName = state.inMeetingForClient.displayName;
+                                this.inMeetingForClient.dob = state.inMeetingForClient.dob;
+                                this.meetingType = state.meetingType;
+                                // NOTE: this now comes from its own end-point (see below)
+                                // this.myMediaServiceToken = state.myMediaServiceToken;
+                                this.myMediaServiceIdentifier = +state.myMediaServiceIdentifier;
+                                this.otherParticipants = state.otherParticipants;
+
+                                if(_firstRun) {
+                                    this.mainViewParticipant.uid = +state.myMediaServiceIdentifier;
+                                    $.post('/api/meeting/refreshMyMediaServiceToken', (_data) => {  // get new agora token
+                                        if(!this.hasError(_data)) {
+                                            this.myMediaServiceToken = _data.data;
+                                            this.channel = this.inMeetingForClient.clientMediaServiceRoomIdentifier;
+                                            this.registerSocket();
+                                        }
+                                    }, 'json');
+                                }
+
+                                console.log(this.$data);
+                            }
+                        }, 'json');
+                    },
+                    registerSocket: function(_done) {
+                        let socket = new SockJS(this.backendWsURL);
+                        this.socketClient = Stomp.over(socket);
+                        this.socketClient.connect({}, (frame) => {
+                            console.log('Connected: ' + frame);
+                            this.initSocketListeners();                     // init listeners
+                            this.socketClient.send("/app/register", {},     // register self
+                                JSON.stringify({
+                                    sessionKey: '{{$performer->session_key}}'
+                                })
+                            );
+                            this.initMediaService();
+                        });
+                    },
+                    initSocketListeners: function() {
+
+                        function _isSelf(_eventData) {
+                            return _eventData.performer === '{{ $session->uid  }}';
+                        }
+
+                        this.socketClient.subscribe("/user/topic/newParticipant", (message) => {
+                            console.log("newParticipant received:", message.body);
+                            if(message && message.body) {
+                                let eventData = JSON.parse(message.body);
+                                if(!_isSelf(eventData) && eventData.data) {
+                                    let existing = this.otherParticipants.filter(_participant => {
+                                        return _participant.uid === eventData.performer;
+                                    });
+                                    if(!existing.length) this.otherParticipants.push(eventData.data);
+                                }
+                            }
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myMicrophoneIsAcquired", (message) => {
+                            console.log("myMicrophoneIsAcquired received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myMicrophoneIsNotAcquired", (message) => {
+                            console.log("myMicrophoneIsNotAcquired received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myMicrophoneIsOn", (message) => {
+                            console.log("myMicrophoneIsOn received:", message.body);
+                            if(message && message.body) {
+                                let eventData = JSON.parse(message.body);
+                                if(!_isSelf(eventData) && eventData.data) {
+                                    for (let i = 0; i < this.otherParticipants.length; i++) {
+                                        if(this.otherParticipants[i].uid === eventData.performer) {
+                                            this.otherParticipants[i].media.isMicrophoneOn = true;
+                                            break;
+                                        }
+                                    }
+                                }
+                            }
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myMicrophoneIsOff", (message) => {
+                            console.log("ALIX myMicrophoneIsOff received:", message.body);
+                            if(message && message.body) {
+                                let eventData = JSON.parse(message.body);
+                                if(!_isSelf(eventData) && eventData.data) {
+                                    for (let i = 0; i < this.otherParticipants.length; i++) {
+                                        if(this.otherParticipants[i].uid === eventData.performer) {
+                                            this.otherParticipants[i].media.isMicrophoneOn = false;
+                                            break;
+                                        }
+                                    }
+                                }
+                            }
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myCameraIsAcquired", (message) => {
+                            console.log("myCameraIsAcquired received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myCameraIsNotAcquired", (message) => {
+                            console.log("myCameraIsNotAcquired received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myCameraIsOn", (message) => {
+                            console.log("myCameraIsOn received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myCameraIsOff", (message) => {
+                            console.log("myCameraIsOff received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/editMyName", (message) => {
+                            console.log("editMyName received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/setMyAwayMessage", (message) => {
+                            console.log("setMyAwayMessage received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/removeMyAwayMessage", (message) => {
+                            console.log("removeMyAwayMessage received:", message.body);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/leaveClientRoom", (message) => {
+                            console.log("leaveClientRoom received:", message.body);
+                            if(message && message.body) {
+                                let eventData = JSON.parse(message.body);
+                                if(!_isSelf(eventData) && eventData.data) {
+
+                                    // if the participant who left is in center view, switch center view to self
+                                    for (let i = 0; i < this.otherParticipants.length; i++) {
+                                        if (this.otherParticipants[i].uid === eventData.performer) {
+                                            if(this.mainViewParticipant.uid === (+this.otherParticipants[i].mediaServiceIdentifier)) {
+                                                this.showInCenterView(true, this.myMediaServiceIdentifier, 'You', 'PRO');
+                                            }
+                                        }
+                                    }
+
+                                    this.otherParticipants = this.otherParticipants.filter(_participant => {
+                                         return _participant.uid !== eventData.performer;
+                                    });
+                                }
+                            }
+                        });
+
+                    },
+                    initMediaService: function() {
+
+                        this.mediaServiceClient = AgoraRTC.createClient({mode:'rtc', codec:'h264'})
+
+                        async function _acquireMicrophone() {
+                            this.myMedia.isMicrophoneAcquired = false;
+                            this.myMedia.isMicrophoneOn = false;
+                            try {
+                                this.myMicrophone = await AgoraRTC.createMicrophoneAudioTrack();
+                                this.myMedia.isMicrophoneAcquired = true;
+                            }
+                            catch (e) {
+                                console.log('ALIX: error in getting mic');
+                            }
+                        }
+                        async function _acquireCamera() {
+                            this.myMedia.isCameraAcquired = false;
+                            this.myMedia.isCameraOn = false;
+                            try {
+                                @if(config('app.agora_mode') === 'screen') // testing
+                                    this.myCamera = await AgoraRTC.createScreenVideoTrack();
+                                @else
+                                    this.myCamera = await AgoraRTC.createCameraVideoTrack();
+                                @endif
+                                this.myMedia.isCameraAcquired = true;
+                            }
+                            catch (e) {
+                                console.log('ALIX: error in getting camera');
+                            }
+                        }
+                        async function _initMediaServiceEvents() {
+                            this.mediaServiceClient.on('user-joined', user => {
+                                // do nothing, newParticipant logic handled via WS
+                            });
+                            this.mediaServiceClient.on('user-left', user => {
+                                // do nothing, leaveClientRoom logic handled via WS
+                            });
+                            this.mediaServiceClient.on('user-published', async (user, mediaType) => {
+                                await this.mediaServiceClient.subscribe(user, mediaType)
+                                this.attemptToPlayParticipantMedia(user, mediaType);
+                            });
+                        }
+                        async function _initMediaService() {
+
+                            await _acquireMicrophone.call(this);  // get mic
+                            await _acquireCamera.call(this);      // get cam (or screen for testing)
+
+                            if (!this.myMicrophone && !this.myCamera) {
+                                alert('Do you have camera/mic? Unable to hear or see you.');
+                                return;
+                            }
+
+                            await _initMediaServiceEvents.call(this);
+
+                            // Show own feed
+                            if(this.myCamera && this.myMedia.isCameraAcquired) {
+                                this.myCamera.play($('#self-view')[0], {fit: 'contain'});
+                            }
+
+                            // init unrenderedParticipantsTimer and unresolvedParticipantsTimer
+                            this.initUnrenderedParticipantsTimer();
+                            this.initUnresolvedParticipantsTimer();
+
+                            await this.mediaServiceClient.join(         // join agora channel
+                                this.appId,
+                                this.channel,
+                                this.myMediaServiceToken,
+                                +this.myMediaServiceIdentifier
+                            );
+                            await this.mediaServiceClient.publish(      // publish audio/video
+                                [this.myMicrophone, this.myCamera].filter(Boolean)
+                            );
+
+                            // notify others that I have arrived
+                            if(this.myCamera && this.myMedia.isCameraAcquired) {
+                                this.myCameraIsOn();
+                            }
+                            if(this.myMicrophone && this.myMedia.isMicrophoneAcquired) {
+                                this.myMicrophoneIsOn();
+                            }
+
+                        }
+
+                        _initMediaService.call(this);
+                    },
+
+                    //  attemptToPlayParticipantMedia
+                    //      if user already in otherParticipants
+                    //          if user's thumb already rendered
+                    //              if yes, check participant's isCameraOn is true
+                    //                  if yes, play participant's video in his thumb
+                    //          else store "user" in unrenderedParticipants
+                    //              and keep retrying after 500mx (max 2 times) - i.e. give vue a cycle or 2 to render thumb
+                    //      else store "user" in unresolvedParticipants
+                    //          and keep retrying after 500ms (max 10 times) till resolved - i.e. give WS time to receive the newParticipant event
+                    attemptToPlayParticipantMedia: function(user, mediaType) {
+                        let participant = this.otherParticipants.filter(function(_participant) {
+                            return (+_participant.mediaServiceIdentifier) === user.uid;
+                        });
+                        if(participant && participant.length) {
+                            participant = participant[0];
+                            if($('[data-uid="' + participant.mediaServiceIdentifier + '"]').length) {
+                                if(mediaType === 'audio' && participant.media.isMicrophoneOn) {
+                                    user.audioTrack.play();
+                                }
+                                else if(mediaType === 'video' && participant.media.isCameraOn) {
+                                    user.videoTrack.play($('[data-uid="' + user.uid + '"]')[0], {fit: 'contain'});
+                                }
+                                this.markUserAsRendered(user);
+                            }
+                            else {
+                                console.warn('Thumb not yet in DOM for participant!', user.uid);
+                                this.markUserAsUnrendered(user, mediaType);
+                            }
+                            this.markUserAsResolved(user);
+                        }
+                        else {
+                            console.warn('Participant not found in otherParticipants!', user.uid);
+                            this.markUserAsUnresolved(user, mediaType);
+                        }
+                    },
+
+                    // start: agora <-> WS sync helpers
+                    initUnrenderedParticipantsTimer: function() {
+                        this.unrenderedParticipantsTimer = window.setInterval(() => {
+                            this.unrenderedParticipants.forEach((_user) => {
+                                this.attemptToPlayParticipantMedia(_user, _user.mediaType);
+                            });
+                        }, 500);
+                    },
+                    initUnresolvedParticipantsTimer: function() {
+                        this.unresolvedParticipantsTimer = window.setInterval(() => {
+                            this.unresolvedParticipants.forEach((_user) => {
+                                this.attemptToPlayParticipantMedia(_user, _user.mediaType);
+                            });
+                        }, 1000);
+                    },
+                    markUserAsUnrendered: function(_user, _mediaType) {
+                        let existing = !!this.unrenderedParticipants.filter((_item) => _item.uid === _user.uid).length;
+                        if(!existing) {
+                            _user.mediaType = _mediaType;
+                            this.unrenderedParticipants.push(_user);
+                        }
+                    },
+                    markUserAsRendered: function(_user) {
+                        this.unrenderedParticipants = this.unrenderedParticipants.filter((_item) => _item.uid !== _user.uid);
+                    },
+                    markUserAsUnresolved: function(_user, _mediaType) {
+                        let existing = !!this.unresolvedParticipants.filter((_item) => _item.uid === _user.uid).length;
+                        if(!existing) {
+                            _user.mediaType = _mediaType;
+                            this.unresolvedParticipants.push(_user);
+                        }
+                    },
+                    markUserAsResolved: function(_user) {
+                        this.unresolvedParticipants = this.unresolvedParticipants.filter((_item) => _item.uid !== _user.uid);
+                    },
+                    // end: agora <-> WS sync helpers
+
+                    // start: actions that notify participants via socket
+                    myMicrophoneIsAcquired: function () {
+                        this.socketClient.send("/app/myMicrophoneIsAcquired", {},
+                            JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                        );
+                    },
+                    myMicrophoneIsNotAcquired: function () {
+                        this.socketClient.send("/app/myMicrophoneIsNotAcquired", {},
+                            JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                        );
+                    },
+                    myMicrophoneIsOn: function () {
+                        if(!this.myMedia.isMicrophoneAcquired) return;
+                        this.myMicrophone.setEnabled(true);
+                        this.myMedia.isMicrophoneOn = true;
+                        this.socketClient.send("/app/myMicrophoneIsOn", {},
+                            JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                        );
+                    },
+                    myMicrophoneIsOff: function () {
+                        if(!this.myMedia.isMicrophoneAcquired) return;
+                        this.myMicrophone.setEnabled(false);
+                        this.myMedia.isMicrophoneOn = false;
+                        this.socketClient.send("/app/myMicrophoneIsOff", {},
+                            JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                        );
+                    },
+                    myCameraIsAcquired: function () {
+                        this.socketClient.send("/app/myCameraIsAcquired", {},
+                            JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                        );
+                    },
+                    myCameraIsNotAcquired: function () {
+                        this.socketClient.send("/app/myCameraIsNotAcquired", {},
+                            JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                        );
+                    },
+                    myCameraIsOn: function () {
+                        if(!this.myMedia.isCameraAcquired) return;
+                        this.myCamera.setEnabled(true);
+                        this.myMedia.isCameraOn = true;
+                        this.socketClient.send("/app/myCameraIsOn", {},
+                            JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                        );
+                    },
+                    myCameraIsOff: function () {
+                        if(!this.myMedia.isCameraAcquired) return;
+                        this.myCamera.setEnabled(false);
+                        this.myMedia.isCameraOn = false;
+                        this.socketClient.send("/app/myCameraIsOff", {},
+                            JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                        );
+                    },
+                    editMyName: function (_myNewName) {
+                        this.socketClient.send("/app/editMyName", {},
+                            JSON.stringify({
+                                sessionKey: '{{$performer->session_key}}',
+                                myNewName: _myNewName
+                            })
+                        );
+                    },
+                    setMyAwayMessage: function (_message) {
+                        this.socketClient.send("/app/setMyAwayMessage", {},
+                            JSON.stringify({
+                                sessionKey: '{{$performer->session_key}}',
+                                message: _message
+                            })
+                        );
+                    },
+                    removeMyAwayMessage: function () {
+                        this.socketClient.send("/app/removeMyAwayMessage", {},
+                            JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                        );
+                    },
+                    leaveClientRoom: function () {
+                        this.socketClient.send("/app/leaveClientRoom", {},
+                            JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                        );
+                        window.setTimeout(() => {   // a little timeout for the WS message sending op to complete
+                            window.location.href = '/pro/meet';
+                        }, 250);
+                    },
+                    // end: actions that notify participants via socket
+
+                    // start: main view / thumb views
+                    showInCenterView: function(_self, _uid, _name, _type) {
+                        this.mainViewParticipant = {
+                            self: _self,
+                            uid: +_uid,
+                            type: _type,
+                            name: _name,
+                        };
+                        Vue.nextTick(() => {
+                            this.refreshVideos();
+                        });
+                    },
+                    refreshVideos: function() {
+                        // play self (only video)
+                        // no need to check camera/mic acquired/on etc. as only published tracks will appear here
+                        for(let track in this.mediaServiceClient.localTracks) {
+                            if(this.mediaServiceClient.localTracks.hasOwnProperty(track)) {
+                                track = this.mediaServiceClient.localTracks[track];
+                                if(track.trackMediaType === 'video') {
+                                    track.play($('[data-uid="' + this.myMediaServiceIdentifier + '"]')[0], {fit: 'contain'});
+                                }
+                            }
+                        }
+
+                        // play others
+                        for(let remoteParticipant in this.mediaServiceClient.remoteUsers) {
+                            if(this.mediaServiceClient.remoteUsers.hasOwnProperty(remoteParticipant)) {
+                                remoteParticipant = this.mediaServiceClient.remoteUsers[remoteParticipant];
+                                if(remoteParticipant.hasAudio && remoteParticipant.audioTrack) {
+                                    remoteParticipant.audioTrack.play();
+                                }
+                                if(remoteParticipant.hasVideo && remoteParticipant.videoTrack) {
+                                    remoteParticipant.videoTrack.play($('[data-uid="' + remoteParticipant.uid + '"]')[0], {fit: 'contain'});
+                                }
+                            }
+                        }
+                    },
+                    // end: main view / thumb views
+
+                    // start: utils
+                    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;
+                    }
+                    // end: utils
+                },
+                mounted: function () {
+                    this.enterClientRoomAsPro();
+                }
+            });
+        })();
+    </script>
+    <script>
+        (function() {
+            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
+                },
+                methods: {
+                    refresh: function() {
+                        let self = this;
+                        $.get('/patients-in-queue', function(_data) {
+                            self.items = _data;
+                        }, 'json');
+                    },
+                    claim: function(_uid) {
+                        $.post('/api/mcpRequest/claim', {clientUid: _uid}, function(_data) {
+                            if(_data && _data.success) {
+                                // open patient in LHS
+                                window.top.openInLHS('/patients/view/' + _uid);
+                                // open patient video in RHS
+                                window.top.openInRHS('/pro/meet/' + _uid);
+                            }
+                            else {
+                                if (_data.message) {
+                                    window.top.toastr.error(_data.message);
+                                } else {
+                                    window.top.toastr.error('Unable to claim the patient');
+                                }
+                            }
+                        }, 'json');
+                    }
+                }
+            });
+        })();
+    </script>
+</body>
+</html>

+ 386 - 0
resources/views/app/video/call-agora.blade.php

@@ -0,0 +1,386 @@
+<!DOCTYPE html>
+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <link href="https://fonts.googleapis.com/css?family=Nunito:200,600,700" rel="stylesheet">
+    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
+    <link href="/css/app.css" rel="stylesheet">
+    <link rel="stylesheet" href="/fontawesome-free/css/all.min.css">
+    <link href="/css/meeting.css" rel="stylesheet">
+    <link href="/css/style.css" rel="stylesheet">
+    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
+    <script defer src=//download.agora.io/sdk/web/AgoraRTC_N-4.1.0.js></script>
+</head>
+
+<body class="p-0 m-0">
+
+    <div id="proCallComponent">
+
+        @if($client)
+        <div v-show="videoActive" class="text-center py-2 border-bottom font-weight-normal mcp-theme-1">
+            <i class="fa fa-user-injured small mr-2"></i><a href="#" onclick="return window.top.openInLHS('/patients/view/{{$client->uid}}')">
+                <span class="font-weight-bold">{{ $client->displayName() }}</span>
+            </a>
+        </div>
+        @endif
+
+        <div class="py-2 d-flex align-items-center justify-content-center border-bottom">
+            <a href="#" v-if="ringer" v-on:click.prevent="toggleRinger()"
+               class="font-weight-bold btn btn-sm btn-success">
+                Ringer
+                <i class="ml-1 fa fa-volume-up"></i>
+            </a>
+            <a href="#" v-if="!ringer" v-on:click.prevent="toggleRinger()"
+               class="font-weight-bold btn btn-sm btn-warning">
+                Ringer
+                <i class="ml-1 fa fa-volume-mute"></i>
+            </a>
+        </div>
+
+        <div class="" v-show="videoActive">
+            <div class="py-3 text-center" v-if="started">
+                <h6 class="text-black font-weight-bold m-0">Call in progress: @{{ timeDisplay() }}</h6>
+            </div>
+            <div class="py-3 text-center" v-if="noOneElseInCall">
+                <h6 class="text-black font-weight-bold m-0">No other participants in the call.
+                    <a href="#" class="text-danger font-weight-bold" v-on:click.prevent="hangUp()">Hang up</a>
+                </h6>
+            </div>
+            <div class="main-view mx-auto">
+                <div id="self-view" class="full-view" data-stream="self" data-name="You" data-type="PRO"></div>
+                <div class="thumbs">
+
+                </div>
+                <button class="btn btn-danger rounded-circle hang-up"
+                        v-if="started"
+                        title="Leave Call"
+                        v-on:click.prevent="hangUp()">
+                    <i class="fa fa-phone"></i>
+                </button>
+            </div>
+        </div>
+
+        @if($client)
+        <div class="" v-show="!videoActive && client">
+            <button class="btn btn-sm btn-primary font-weight-bold mx-auto mt-4 d-block"
+                    v-on:click.prevent="connect()">
+                Start video call with {{ $client->displayName() }}
+            </button>
+        </div>
+        @endif
+
+    </div>
+
+    <div class="border-top patient-queue mcp-theme-1" id="queueComponent">
+        <div class="bg-secondary text-white font-weight-bold text-center py-1" v-if="items.length > 0">
+            @{{ items.length }} patient@{{ items.length > 1 ? 's' : '' }} in the queue
+        </div>
+        <div class="bg-secondary text-white font-weight-bold text-center py-1" v-if="items.length === 0">
+            No patients in the queue
+        </div>
+        <div v-if="items && items.length" class="d-flex align-items-center my-1">
+            <div v-for="item in items">
+                <div class="queue-item border border-primary rounded mx-1" :title="item.name">
+                    <div class="patient-avatar mb-1 text-dark">@{{ item.initials }}</div>
+                    <div class="font-weight-bold small text-nowrap text-ellipsis">@{{ item.name }}</div>
+                </div>
+                <button class="btn btn-sm btn-primary mt-1 text-white font-weight-bold py-0 mx-auto d-block"
+                        v-on:click.prevent="claim(item.clientUid)">Claim</button>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        (function () {
+            new Vue({
+                el: '#proCallComponent',
+                delimiters: ['@{{', '}}'],
+                data: {
+
+                    agoraClient: null,
+                    appId: '{{ config('app.agora_appid') }}',
+                    channel: '{{ $client ? $client->uid : '' }}',
+                    uid: '{{ $session->id + 1000000 }}',
+
+                    time: 0,
+                    startTime: 0,
+                    started: false,
+                    client: false,
+                    pro: false,
+
+                    selfName: '',
+                    selfToken: '',
+
+                    clientUid: '',
+
+                    selfUserType: 'PRO',
+                    noOneElseInCall: true,
+                    patientInQueue: false,
+
+                    videoActive: false,
+
+                    heartbeatTimer: false,
+
+                    ringer: {{ $pro->is_ring_on ? 'true' : 'false' }},
+                },
+                methods: {
+                    resolveParticipantNames: function() {
+                        $('[data-stream]:not([data-name])').each(function() {
+                            let elem = this;
+                            $.post('/pro/meet/get-participant-info', {
+                                _token: '{{ csrf_token() }}',
+                                uid: $(elem).attr('data-stream')
+                            }, function(_data) {
+                                $(elem).attr('data-type', _data.type);
+                                $(elem).attr('data-name', _data.name);
+                            }, 'json');
+                        });
+                    },
+                    toggleRinger: function () {
+                        let self = this, endPoint = this.ringer ? 'turnOffRing' : 'turnOnRing';
+                        $.post('/api/pro/' + endPoint, function (_data) {
+                            if (_data && _data.success) {
+                                self.ringer = !self.ringer;
+                            } else {
+                                if (_data.message) {
+                                    toastr.error(_data.message);
+                                } else {
+                                    toastr.error('Unable to change ringer status');
+                                }
+                            }
+                        }, 'json');
+                    },
+                    connect: function () {
+                        var self = this;
+                        self.selfName = '{{ $pro->name_display  }}';
+                        $.get('/api/agora/getClientToken', {
+                            clientUid: self.clientUid,
+                        }, function (_data) {
+                            console.log(_data);
+                            self.selfToken = _data.data;
+                            self.initAgora();
+                        });
+                    },
+                    timeDisplay: function () {
+                        var seconds = this.time / 1000,
+                            minutes = parseInt(seconds / 60, 10);
+                        seconds = parseInt(seconds % 60, 10);
+                        return minutes + " min, " + seconds + " sec";
+                    },
+                    hangUp: function () {
+                        var self = this;
+                        async function _leave() {
+                            if(self.agoraClient) {
+                                await self.agoraClient.leave();
+                                window.top.hideRHS();
+                                window.location.reload();
+                            }
+                        }
+                        _leave();
+                    },
+                    initAgora: function () {
+
+                        let self = this;
+
+                        async function _initAgora(){
+
+                            self.agoraClient = AgoraRTC.createClient({mode:'rtc', codec:'h264'})
+                            let camera, mic
+                            try { mic = await AgoraRTC.createMicrophoneAudioTrack() } catch {
+                                console.log('ALIX: error in getting mic');
+                            }
+                            try { camera = await AgoraRTC.createCameraVideoTrack() } catch {
+                                console.log('ALIX: error in getting camera');
+                            }
+
+                            // testing
+                            @if(config('app.agora_mode') === 'screen')
+                                try { camera = await AgoraRTC.createScreenVideoTrack() } catch { }
+                            @endif
+
+                            if (!mic && !camera){
+                                alert('Do you have camera/mic? Unable to hear or see you.')
+                                return
+                            }
+
+                            // Add myself to the page.
+                            if(camera) {
+                                camera.play($('#self-view')[0]);
+                            }
+
+                            // events
+                            self.agoraClient.on('user-joined', user => {
+
+                                // add a div for remove view
+                                $('[data-stream="' + user.uid + '"]').remove();
+                                var remoteViewID = 'remote-view-' + user.uid;
+                                var remoteElem = $('<div id="' + remoteViewID + '" class="remote-view thumb-view" data-stream="' + user.uid + '"></div>');
+                                remoteElem.appendTo('.thumbs');
+
+                                if (!self.startTime) {
+                                    self.startTime = new Date().getTime();
+                                    window.setInterval(function () {
+                                        self.time = new Date().getTime() - self.startTime;
+                                    }, 1000);
+                                    self.started = true;
+                                }
+                                self.activateParty(user.uid);
+                                self.noOneElseInCall = false;
+                                self.resolveParticipantNames();
+                            })
+                            self.agoraClient.on('user-left', user => {
+
+                                if ($('.full-view[data-stream="' + user.uid + '"]').length) {
+                                    var allThumbs = $('.thumbs [data-stream]:not([data-stream=""]):not(.disconnected-view):visible');
+                                    if (allThumbs.length) {
+                                        $('.thumbs [data-stream]:not([data-stream=""])').each(function () {
+                                            if ($(this).attr('data-stream') !== user.uid) {
+                                                self.activateParty($(this).attr('data-stream'));
+                                                return false;
+                                            }
+                                        });
+                                    } else {
+                                        self.noOneElseInCall = true;
+                                    }
+                                }
+
+                                $('[data-stream="' + user.uid + '"]').remove();
+
+                                // if no other parties in call, hang up
+                                if (!$('[data-stream]:not([data-stream="' + {{ $session->id }} + '"])').length) {
+                                    console.warn('No other parties in the call!');
+                                    self.startTime = 0;
+                                    self.started = false;
+                                    self.noOneElseInCall = true;
+                                }
+                            })
+                            self.agoraClient.on('user-published', async function(user, mediaType){
+                                await self.agoraClient.subscribe(user, mediaType)
+                                mediaType === 'audio'
+                                    ? user.audioTrack.play()
+                                    : user.videoTrack.play($('[data-stream="' + user.uid + '"]')[0]);
+                            })
+
+                            await self.agoraClient.join(self.appId, self.channel, self.selfToken, self.uid)
+                            await self.agoraClient.publish([mic, camera].filter(Boolean))
+
+                            // assume connected by this point, notify backend & show self video
+                            if (mic || camera) {
+                                self.joinMeetingAsPro(self.selfUserType);
+                                $('#self-view').attr('data-type', 'PRO').show();
+                                self.activateParty('self');
+                                self.videoActive = true;
+                            }
+                        }
+
+                        _initAgora();
+
+                    },
+                    joinMeetingAsPro: function (_type) {
+                        var self = this;
+                        $.ajax({
+                            type: 'post',
+                            url: '/api/clientVideoVisit/joinVideoVisitAsPro',
+                            headers: {
+                                'sessionKey': '{{ request()->cookie('sessionKey') }}'
+                            },
+                            data: {uid: self.clientUid},
+                            dataType: 'json'
+                        })
+                            .done(function (_data) {
+                                console.log(_data);
+
+                                // navigate to this patient on LHS
+                                window.top.openInLHS('/patients/view/' + self.clientUid, true, false);
+
+                            })
+                            .fail(function (_data) {
+                                console.warn(_data);
+                                alert(_data.message);
+                            });
+                    },
+                    activateParty: function (_stream = 'self') {
+                        var current = $('.full-view');
+                        if (current.attr('data-stream') === _stream) return;
+                        current.removeClass('full-view').addClass('thumb-view');
+                        if (current.attr('data-type') === 'CLIENT') {
+                            current.prependTo('.thumbs');
+                        } else {
+                            current.appendTo('.thumbs');
+                        }
+                        if (_stream === 'self') {
+                            $('#self-view')
+                                .removeClass('thumb-view')
+                                .removeClass('disconnected-view')
+                                .addClass('full-view')
+                                .prependTo('.main-view');
+                        } else {
+                            $('div[data-stream="' + _stream + '"]')
+                                .removeClass('thumb-view')
+                                .removeClass('disconnected-view')
+                                .addClass('full-view')
+                                .prependTo('.main-view');
+                        }
+                    }
+                },
+                mounted: function () {
+
+                    var self = this;
+
+                    $(document).on('click', '.thumbs>div[data-stream]', function () {
+                        self.activateParty($(this).attr('data-stream'));
+                        return false;
+                    });
+
+                    @if(isset($client))
+                        self.client = true;
+                        self.clientUid = '{{ $client->uid }}';
+                        self.videoActive = false;
+                    @endif
+                }
+            });
+            new Vue({
+                el: '#queueComponent',
+                data: {
+                    items: []
+                },
+                mounted: function() {
+                    let self = this;
+                    this.refresh();
+                    window.setInterval(function() {
+                        self.refresh();
+                    }, 15000); // once in 15 seconds
+                },
+                methods: {
+                    refresh: function() {
+                        let self = this;
+                        $.get('/patients-in-queue', function(_data) {
+                            self.items = _data;
+                        }, 'json');
+                    },
+                    claim: function(_uid) {
+                        $.post('/api/mcpRequest/claim', {clientUid: _uid}, function(_data) {
+                            if(_data && _data.success) {
+                                // open patient in LHS
+                                window.top.openInLHS('/patients/view/' + _uid);
+                                // open patient video in RHS
+                                window.top.openInRHS('/pro/meet/' + _uid);
+                            }
+                            else {
+                                if (_data.message) {
+                                    window.top.toastr.error(_data.message);
+                                } else {
+                                    window.top.toastr.error('Unable to claim the patient');
+                                }
+                            }
+                        }, 'json');
+                    }
+                }
+            })
+        })();
+    </script>
+
+</body>
+</html>

+ 585 - 0
resources/views/app/video/call-ot.blade.php

@@ -0,0 +1,585 @@
+<!DOCTYPE html>
+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <link href="https://fonts.googleapis.com/css?family=Nunito:200,600,700" rel="stylesheet">
+    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
+    <link href="/css/app.css" rel="stylesheet">
+    <link rel="stylesheet" href="/fontawesome-free/css/all.min.css">
+    <link href="/css/meeting.css" rel="stylesheet">
+    <link href="/css/style.css" rel="stylesheet">
+    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
+    <script src="https://static.opentok.com/v2/js/opentok.js"></script>
+</head>
+
+<body class="p-0 m-0">
+
+    <div id="proCallComponent">
+
+        @if($client)
+        <div v-show="videoActive" class="text-center py-2 border-bottom font-weight-normal mcp-theme-1">
+            <i class="fa fa-user-injured small mr-2"></i><a href="#" onclick="return window.top.openInLHS('/patients/view/{{$client->uid}}')">
+                <span class="font-weight-bold">{{ $client->displayName() }}</span>
+            </a>
+        </div>
+        @endif
+
+        {{--
+        <div v-show="!videoActive" class="align-items-center justify-content-center py-3 border-bottom" style="display: flex;">
+            <button class="btn btn-sm btn-primary px-4 font-weight-bold stag-primary-bg stag-primary-border"
+                    v-on:click.prevent="nextPatient()"
+                    :disabled="client || checkingForNextPatient || started || !(patientInQueue && !started)">Next Patient</button>
+            <span v-if="patientInQueue && !started" class="text-success text-sm ml-3 small">
+                <i class="fa fa-circle"></i>
+            </span>
+            <span v-if="!patientInQueue && !started" class="text-secondary text-sm ml-3 small cancelled-item">
+                <i class="fa fa-circle"></i>
+            </span>
+        </div>
+        --}}
+
+        <div class="py-2 d-flex align-items-center justify-content-center border-bottom">
+            <a href="#" v-if="ringer" v-on:click.prevent="toggleRinger()"
+               class="font-weight-bold btn btn-sm btn-success">
+                Ringer
+                <i class="ml-1 fa fa-volume-up"></i>
+            </a>
+            <a href="#" v-if="!ringer" v-on:click.prevent="toggleRinger()"
+               class="font-weight-bold btn btn-sm btn-warning">
+                Ringer
+                <i class="ml-1 fa fa-volume-mute"></i>
+            </a>
+        </div>
+
+        <div v-if="!started && noNextPatient" class="bg-light rounded text-center py-1 font-weight-bold text-sm my-3 mx-3">@{{ noNextPatient }}</div>
+
+        <div class="" v-show="videoActive">
+            <div class="py-3 text-center" v-if="started">
+                <h6 class="text-black font-weight-bold m-0">Call in progress: @{{ timeDisplay() }}</h6>
+            </div>
+            <div class="py-3 text-center" v-if="noOneElseInCall">
+                <h6 class="text-black font-weight-bold m-0">No other participants in the call.
+                    <a href="#" class="text-danger font-weight-bold" v-on:click.prevent="hangUp()">Hang up</a>
+                </h6>
+            </div>
+            <div class="main-view mx-auto">
+                <div id="self-view" class="full-view" data-name="{{ $pro->name_display }}" data-type="PRO"></div>
+                <div class="thumbs">
+
+                </div>
+                <button class="btn btn-danger rounded-circle hang-up"
+                        v-if="started"
+                        title="Leave Call"
+                        v-on:click.prevent="hangUp()">
+                    <i class="fa fa-phone"></i>
+                </button>
+            </div>
+        </div>
+
+    </div>
+
+    <div class="border-top patient-queue mcp-theme-1" id="queueComponent">
+        <div class="bg-secondary text-white font-weight-bold text-center py-1" v-if="items.length > 0">
+            @{{ items.length }} patient@{{ items.length > 1 ? 's' : '' }} in the queue
+        </div>
+        <div class="bg-secondary text-white font-weight-bold text-center py-1" v-if="items.length === 0">
+            No patients in the queue
+        </div>
+        <div v-if="items && items.length" class="d-flex align-items-center my-1">
+            <div v-for="item in items">
+                <div class="queue-item border border-primary rounded mx-1" :title="item.name">
+                    <div class="patient-avatar mb-1 text-dark">@{{ item.initials }}</div>
+                    <div class="font-weight-bold small text-nowrap text-ellipsis">@{{ item.name }}</div>
+                </div>
+                <button class="btn btn-sm btn-primary mt-1 text-white font-weight-bold py-0 mx-auto d-block"
+                        v-on:click.prevent="claim(item.clientUid)">Claim</button>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        (function () {
+            new Vue({
+                el: '#proCallComponent',
+                delimiters: ['@{{', '}}'],
+                data: {
+                    time: 0,
+                    startTime: 0,
+                    started: false,
+                    client: false,
+                    pro: false,
+
+                    selfName: '',
+                    selfToken: '',
+
+                    clientUid: '',
+
+                    otSessionId: '',
+
+                    publisher: false,
+
+                    checkingForNextPatient: false,
+                    noNextPatient: false,
+
+                    otSession: false,
+
+                    selfUserType: 'PRO',
+                    selfStreamId: '',
+                    noOneElseInCall: true,
+                    patientInQueue: false,
+
+                    videoActive: false,
+
+                    heartbeatTimer: false,
+
+                    ringer: {{ $pro->is_ring_on ? 'true' : 'false' }},
+                },
+                methods: {
+                    toggleRinger: function () {
+                        let self = this, endPoint = this.ringer ? 'turnOffRing' : 'turnOnRing';
+                        $.post('/api/pro/' + endPoint, function (_data) {
+                            if (_data && _data.success) {
+                                self.ringer = !self.ringer;
+                            } else {
+                                if (_data.message) {
+                                    toastr.error(_data.message);
+                                } else {
+                                    toastr.error('Unable to change ringer status');
+                                }
+                            }
+                        }, 'json');
+                    },
+                    pollForNextPatient: function () {
+                        if (!this.started) {
+                            this.nextPatient(true);
+                        }
+                    },
+                    nextPatient: function (_pollOnly = false) {
+                        var self = this;
+                        if (!_pollOnly) this.checkingForNextPatient = true;
+                        $.post('/api/client/getNextClientForVideoVisit', {}, function (_data) {
+                            if (_pollOnly) {
+                                self.patientInQueue = _data.success;
+                            } else {
+                                self.checkingForNextPatient = false;
+                                if (!_data.success) {
+                                    self.noNextPatient = _data.message;
+                                    window.setTimeout(function () {
+                                        self.noNextPatient = false;
+                                    }, 2000);
+                                } else {
+                                    // get ot session key from client record
+                                    self.client = true;
+                                    self.clientUid = _data.data;
+                                    self.videoActive = true;
+                                    self.startOpenTokSession();
+                                }
+                            }
+                        }, 'json');
+                    },
+                    startOpenTokSession: function () {
+                        var self = this;
+                        self.getOpenTokSessionId(function () {
+                            self.selfName = '{{ $pro->name_display  }}';
+                            $.post('/api/openTok/getClientToken', {
+                                opentokSessionId: self.otSessionId,
+                                data: JSON.stringify({
+                                    uid: '{{ $pro->uid  }}',
+                                    name: self.selfName,
+                                    type: 'PRO'
+                                })
+                            }, function (_data) {
+                                console.log(_data);
+                                self.selfToken = _data.data;
+                                self.initOpenTok();
+                            });
+                        });
+                    },
+                    timeDisplay: function () {
+                        var seconds = this.time / 1000,
+                            minutes = parseInt(seconds / 60, 10);
+                        seconds = parseInt(seconds % 60, 10);
+                        return minutes + " min, " + seconds + " sec";
+                    },
+                    hangUp: function () {
+                        var self = this;
+                        if (this.otSession) {
+                            try {
+                                this.otSession.unpublish(this.publisher);
+                                this.otSession.disconnect();
+                            } catch (e) {
+                                console.log('Was already disconnected.');
+                            }
+                            this.otSession = false;
+                            this.otSessionId = '';
+                            this.started = false;
+                            this.startTime = false;
+                            this.videoActive = false;
+                             if(self.publisher){
+                                self.publisher.destroy();
+                             }
+                             window.top.hideRHS();
+                            // this.client = false;
+                        }
+                    },
+                    initOpenTok: function () {
+
+                        /* fake video feed (temp) */
+                        const randomColour = () => {
+                            return Math.round(Math.random() * 255);
+                        };
+
+                        const canvas = document.createElement('canvas');
+                        canvas.width = 640;
+                        canvas.height = 480;
+                        const ctx = canvas.getContext('2d');
+                        var pos = 100;
+                        window.setInterval(function () {
+                            ctx.clearRect(0, 0, canvas.width, canvas.height);
+                            ctx.font = "20px Georgia";
+                            ctx.fillStyle = `rgb(220, 220, 220)`;
+                            ctx.fillText("Video feed from {{ $pro->name_display }}", 20, pos);
+                            pos += 5;
+                            if (pos > canvas.height) pos = 100;
+                        }, 1000);
+
+                        var self = this;
+
+                        var apiKey = '<?= env('TOKBOX_API_KEY', '46678902') ?>';
+                        var sessionId = this.otSessionId;
+                        var token = this.selfToken;
+
+                        // destroy if existing
+                        // self.hangUp();
+
+                        self.otSession = OT.initSession(apiKey, sessionId);
+
+                        // peer connected
+                        self.otSession.on('streamCreated', function streamCreated(event) {
+                            console.log('streamCreated', arguments);
+                            var subscriberOptions = {
+                                insertMode: 'append',
+                                width: '100%',
+                                height: '100%'
+                            };
+
+                            var connectionData = JSON.parse(event.stream.connection.data);
+
+                            // add a div for remove view
+                            var remoteViewID = 'remote-view-' + event.stream.id;
+                            var remoteElem = $('<div id="' + remoteViewID + '" class="remote-view thumb-view" ' +
+                                'data-stream="' + event.stream.id + '" ' +
+                                'data-connection-data="' + event.stream.connection.data + '" ' +
+                                'data-name="' + connectionData.name + '" ' +
+                                'data-type="' + connectionData.type + '"></div>');
+                            remoteElem.appendTo('.thumbs');
+
+                            self.otSession.subscribe(event.stream, remoteViewID, subscriberOptions, self.handleOpenTokError);
+
+                            if (connectionData.type === 'CLIENT') {
+                                self.client = true;
+                            }
+
+                            if (!self.startTime) {
+                                self.startTime = new Date().getTime();
+                                window.setInterval(function () {
+                                    self.time = new Date().getTime() - self.startTime;
+                                }, 1000);
+                                self.started = true;
+                            }
+
+                            self.activateParty(event.stream.id);
+
+                            self.noOneElseInCall = false;
+                        });
+
+                        // peer disconnected
+                        self.otSession.on("streamDestroyed", function (event) {
+                            onPeerDisconnection(event, event.stream.connection.data);
+                        });
+                        // self.otSession.on("connectionDestroyed", function(event) {
+                        //     debugger;
+                        //     console.log('connectionDestroyed from ' + event.connection.data);
+                        //     onPeerDisconnection(event, event.connection.data);
+                        // });
+
+                        self.otSession.on("connectionCreated", function (event) {
+                            console.log('connectionCreated');
+                            console.log(event);
+                        });
+
+                        function onPeerDisconnection(event, data) {
+
+                            if (event.stream && $('.full-view[data-stream="' + event.stream.id + '"]').length) {
+                                var allThumbs = $('.thumbs [data-stream]:not([data-stream=""]):not(.disconnected-view):visible');
+                                if (allThumbs.length) {
+                                    $('.thumbs [data-stream]:not([data-stream=""])').each(function () {
+                                        if ($(this).attr('data-stream') !== event.stream.id) {
+                                            self.activateParty($(this).attr('data-stream'));
+                                            return false;
+                                        }
+                                    });
+                                } else {
+                                    self.noOneElseInCall = true;
+                                }
+                            }
+
+                            if (event.stream) {
+                                var remoteViewElem = $('[data-stream="' + event.stream.id + '"]');
+                                remoteViewElem.remove();
+                                // if(remoteViewElem.length) {
+                                //     remoteViewElem.attr('data-stream', '');
+                                //     remoteViewElem.attr('data-connection-data', '');
+                                //     remoteViewElem.attr('data-type', '');
+                                //     remoteViewElem.attr('data-name', '');
+                                // }
+                                // remoteViewElem.addClass('disconnected-view')
+                            }
+
+                            var connectionData = JSON.parse(data);
+                            if (connectionData.type === 'CLIENT') {
+                                self.client = false;
+                            }
+
+                            // if no other parties in call, hang up
+                            if (!$('[data-stream]:not([data-stream="' + self.selfStreamId + '"])').length) {
+                                console.warn('No other parties in the call!');
+                                self.startTime = 0;
+                                self.started = false;
+                                self.noOneElseInCall = true;
+                            }
+                        }
+
+                        // self connected
+                        self.otSession.on("sessionConnected", function (event) {
+                            self.joinMeetingAsPro(self.selfUserType);
+                        });
+
+                        // self disconnected
+                        self.otSession.on('sessionDisconnected', function sessionDisconnected(event) {
+                            console.log('You were disconnected from the session.', event.reason);
+
+                            // turn pro video off
+                            $.post('/api/clientVideoVisit/turnProVideoOff', {}, function (_data) {
+                                console.log(_data);
+
+                                // stop heart beat
+                                if (self.heartbeatTimer) {
+                                    window.clearInterval(self.heartbeatTimer);
+                                    self.heartbeatTimer = false;
+                                }
+                            });
+                        });
+
+                        // initialize the publisher
+                        var publisherOptions = {
+                            // videoSource: canvas.captureStream(1).getVideoTracks()[0], // TODO: Comment this line to use webcam
+                            insertMode: 'append',
+                            width: '100%',
+                            height: '100%',
+                        };
+                        var publisher = OT.initPublisher('self-view', publisherOptions, self.handleOpenTokError);
+
+                        publisher.on('streamCreated', function (event) {
+                            var selfView = $('#self-view');
+                            selfView.attr('data-stream', event.stream.id);
+                            selfView.attr('data-connection-data', event.stream.connection.data);
+                            self.selfStreamId = event.stream.id;
+                        });
+
+                        publisher.on('streamCreated', function (event) {
+                            console.log('publisher->streamCreated');
+                            var selfView = $('#self-view');
+                            selfView.attr('data-stream', event.stream.id);
+                            selfView.attr('data-connection-data', event.stream.connection.data);
+                            selfView.attr('data-type', 'PRO');
+                            self.activateParty('self');
+                            $('#self-view').show();
+
+                            // turn pro video on
+                            $.post('/api/clientVideoVisit/turnProVideoOn', {}, function (_data) {
+                                console.log(_data);
+
+                                // start heart beat
+                                self.heartbeatTimer = window.setInterval(function () {
+                                    $.post('/api/clientVideoVisit/registerProMeetingHeartbeat', {}, function (_data) {
+                                        console.log(_data);
+                                    });
+                                }, 5000);
+                            });
+                        });
+
+                        publisher.on('streamDestroyed', function (event) {
+                            event.preventDefault();
+                            console.log('publisher->streamDestroyed');
+                            $('#self-view').hide();
+                            var allThumbs = $('.thumbs [data-stream]:not([data-stream=""]):not(.disconnected-view):visible');
+                            if (allThumbs.length) {
+                                $('.thumbs [data-stream]:not([data-stream=""])').each(function () {
+                                    if ($(this).attr('data-stream') !== $('#self-view').attr('data-stream')) {
+                                        self.activateParty($(this).attr('data-stream'));
+                                        return false;
+                                    }
+                                });
+                            } else {
+                                self.hangUp();
+                            }
+
+                            // turn pro video off
+                            $.post('/api/clientVideoVisit/turnProVideoOff', {}, function (_data) {
+                                console.log(_data);
+
+                                // stop heart beat
+                                if (self.heartbeatTimer) {
+                                    window.clearInterval(self.heartbeatTimer);
+                                    self.heartbeatTimer = false;
+                                }
+                            });
+                        });
+                        self.publisher = publisher;
+
+                        // Connect to the session
+                        self.otSession.connect(token, function callback(error) {
+                            if (error) {
+                                self.handleOpenTokError(error);
+                            } else {
+                                // If the connection is successful, publish the publisher to the session
+                                self.otSession.publish(publisher, self.handleOpenTokError);
+                            }
+                        });
+                    },
+                    handleOpenTokError: function (e) {
+
+                    },
+
+                    getOpenTokSessionId: function (_done) {
+                        var self = this;
+                        $.get('/pro/get-opentok-session-key/' + self.clientUid, function (_data) {
+                            self.otSessionId = _data.data;
+                            console.log(_data);
+                            _done();
+                        }, 'json');
+                    },
+
+                    joinMeetingAsPro: function (_type) {
+                        var self = this;
+                        $.ajax({
+                            type: 'post',
+                            url: '/api/clientVideoVisit/joinVideoVisitAsPro',
+                            headers: {
+                                'sessionKey': '{{ request()->cookie('sessionKey') }}'
+                            },
+                            data: {uid: self.clientUid},
+                            dataType: 'json'
+                        })
+                            .done(function (_data) {
+                                console.log(_data);
+
+                                // navigate to this patient on LHS
+                                window.top.openInLHS('/patients/view/' + self.clientUid, true, false);
+
+                            })
+                            .fail(function (_data) {
+                                console.warn(_data);
+                                alert(_data.message);
+                            });
+                    },
+
+                    activateParty: function (_stream = 'self') {
+                        var current = $('.full-view');
+                        if (current.attr('data-stream') === _stream) return;
+                        current.removeClass('full-view').addClass('thumb-view');
+                        if (current.attr('data-type') === 'CLIENT') {
+                            current.prependTo('.thumbs');
+                        } else {
+                            current.appendTo('.thumbs');
+                        }
+                        if (_stream === 'self') {
+                            $('#self-view')
+                                .removeClass('thumb-view')
+                                .removeClass('disconnected-view')
+                                .addClass('full-view')
+                                .prependTo('.main-view');
+                        } else {
+                            $('div[data-stream="' + _stream + '"]')
+                                .removeClass('thumb-view')
+                                .removeClass('disconnected-view')
+                                .addClass('full-view')
+                                .prependTo('.main-view');
+                        }
+                    }
+
+                },
+                mounted: function () {
+
+                    var self = this;
+
+                    $(document).on('click', '.thumbs>div[data-stream]', function () {
+                        self.activateParty($(this).attr('data-stream'));
+                        return false;
+                    });
+
+                    // poll for new patients and alert
+                    // window.setInterval(function () {
+                    //     self.pollForNextPatient();
+                    // }, 5000);
+
+                    window.onbeforeunload = function () {
+                        if (self.started) {
+                            return "A call is in progress";
+                        }
+                    };
+
+                    @if(isset($client))
+                        self.client = true;
+                    self.clientUid = '{{ $client->uid }}';
+                    self.videoActive = true;
+                    self.startOpenTokSession();
+                    @endif
+                }
+            });
+            new Vue({
+                el: '#queueComponent',
+                data: {
+                    items: []
+                },
+                mounted: function() {
+                    let self = this;
+                    this.refresh();
+                    window.setInterval(function() {
+                        self.refresh();
+                    }, 15000); // once in 15 seconds
+                },
+                methods: {
+                    refresh: function() {
+                        let self = this;
+                        $.get('/patients-in-queue', function(_data) {
+                            self.items = _data;
+                        }, 'json');
+                    },
+                    claim: function(_uid) {
+                        $.post('/api/mcpRequest/claim', {clientUid: _uid}, function(_data) {
+                            if(_data && _data.success) {
+                                // open patient in LHS
+                                window.top.openInLHS('/patients/view/' + _uid);
+                                // open patient video in RHS
+                                window.top.openInRHS('/pro/meet/' + _uid);
+                            }
+                            else {
+                                if (_data.message) {
+                                    window.top.toastr.error(_data.message);
+                                } else {
+                                    window.top.toastr.error('Unable to claim the patient');
+                                }
+                            }
+                        }, 'json');
+                    }
+                }
+            })
+        })();
+    </script>
+
+</body>
+</html>

+ 28 - 1
resources/views/layouts/patient.blade.php

@@ -17,6 +17,10 @@
                             <a class="nav-link {{ strpos($routeName, 'patients.view.calendar') === 0 ? 'active' : '' }}"
                                href="{{ route('patients.view.calendar', ['patient' => $patient]) }}">Calendar</a>
                         </li>
+                            <li class="nav-item">
+                                <a class="nav-link {{ strpos($routeName, 'patients.view.programs') === 0 ? 'active' : '' }}"
+                                   href="{{ route('patients.view.programs', ['patient' => $patient]) }}">Programs</a>
+                            </li>
                         <li class="nav-item">
                             <a class="nav-link {{ strpos($routeName, 'patients.view.devices') === 0 ? 'active' : '' }}"
                                href="{{ route('patients.view.devices', ['patient' => $patient]) }}">Devices</a>
@@ -269,7 +273,6 @@
                                                 </div>
                                             @endif
                                         </div>
-                                        {{--@if(!empty($cmName))<div><label>MA:</label> {{$cmName}}</div>@endif--}}
                                     </div>
                                     <div>
                                         @if($patient->has_mcp_done_onboarding_visit !== 'YES')
@@ -330,6 +333,30 @@
                                         </span>
                                         @endif
                                     </div>
+                                    <div>
+                                        <label>Physician:</label> {{$patient->pcp ? $patient->pcp->displayName() : '-' }}
+                                        @if($pro->pro_type == 'ADMIN')
+                                        <div moe class="ml-2">
+                                            <a start show><i class="fa fa-edit"></i></a>
+                                            <form url="/api/client/putPhysicianPro" class="mcp-theme-1">
+                                                <input type="hidden" name="uid" value="{{$patient->uid}}">
+                                                <div class="mb-2">
+                                                    <label class="text-secondary text-sm">Physician Pro</label>
+                                                    <select name="physicianProUid" class="form-control form-control-sm">
+                                                        <option value=""> --select-- </option>
+                                                        @foreach($pros as $iPro)
+                                                            <option value="{{$iPro->uid}}" {{ $patient->pcp && $iPro->uid === $patient->pcp->uid ? 'selected' : '' }}>{{$iPro->displayName()}}</option>
+                                                        @endforeach
+                                                    </select>
+                                                </div>
+                                                <div>
+                                                    <button submit class="btn btn-sm btn-primary mr-1">Submit</button>
+                                                    <button cancel class="btn btn-sm btn-default border">Cancel</button>
+                                                </div>
+                                            </form>
+                                        </div>
+                                        @endif
+                                    </div>
                                 </section>
                                 {{--<section>
                                     <div>

+ 4 - 55
resources/views/layouts/template.blade.php

@@ -85,7 +85,7 @@
                 @endif
                 <li class="nav-item dropdown">
                     <a class="nav-link dropdown-toggle" href="#" id="practice-management" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="mr-1 fas fa-tasks"></i> Practice</a>
-                    <div class="dropdown-menu" aria-labelledby="practice-management">
+                    <div class="dropdown-menu mcp-theme-1" aria-labelledby="practice-management">
                         {{--<a class="dropdown-item" href="{{ route('practice-management.dashboard') }}">Dashboard</a>--}}
                         @if($pro && $pro->pro_type == 'ADMIN')
                             <a class="dropdown-item" href="/practice-management/rates/all">Payment Rates</a>
@@ -95,8 +95,8 @@
                         <a class="dropdown-item" href="/practice-management/bills/not-yet-signed">Pending Bills to Sign</a>
                         <a class="dropdown-item" href="/practice-management/notes/not-yet-signed">Pending Notes to Sign</a>
                         <a class="dropdown-item" href="{{ route('unmapped-sms') }}">Unmapped SMS</a>
-                        {{--<a class="dropdown-item" href="/practice-management/hr">HR</a>--}}
                         <a class="dropdown-item" href="{{ route('practice-management.myTextShortcuts') }}">My Text Shortcuts</a>
+                        <a class="dropdown-item" href="{{ route('practice-management.myFavorites') }}">My Favorites</a>
                         <a class="dropdown-item" href="{{ route('practice-management.proAvailability') }}">Pro Availability</a>
                         <a class="dropdown-item" href="{{ route('practice-management.proCalendar') }}">Pro Calendar</a>
                     </div>
@@ -274,59 +274,8 @@
             }, 15000); // once in 15 seconds
         });
     </script>
-    <script>
-        function showStagPopup(_key, _noAutoFocus) {
-            $('html, body').addClass('no-scroll');
-            let stagPopup = $('[stag-popup-key="' + _key + '"]');
-            stagPopup.addClass('show');
-            stagPopup.find('[moe][initialized]').removeAttr('initialized');
-            initMoes();
-            if(!_noAutoFocus) {
-                window.setTimeout(function() {
-                    stagPopup.find('input[type="text"]:not([readonly]):visible,select:visible').first().focus();
-                }, 150);
-            }
-            return false;
-        }
-        function submitStagPopup(_form) {
-            if(!_form[0].checkValidity()) {
-                _form[0].reportValidity();
-                return false;
-            }
-            showMask();
-            $.post(_form.attr('action'), _form.serialize(), function(_data) {
-                fastReload();
-            });
-            return false;
-        }
-        function closeStagPopup() {
-            $('.stag-popup').removeClass('show');
-            $('html, body').removeClass('no-scroll');
-            return false;
-        }
-        (function() {
-            window.initStagPopupEvents = function () {
-                $(document).on('click', '.stag-popup', function(_e) {
-                    if($(_e.target).is('.stag-popup')) {
-                        closeStagPopup();
-                    }
-                });
-                // 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) {
-                                closeStagPopup();
-                                return false;
-                            }
-                        }
-                    });
-            }
-            addMCInitializer('stag-popups', window.initStagPopupEvents);
-        })();
-    </script>
+    <script src="/js/stag-popup.js"></script>
+    <script src="/js/option-list.js"></script>
     @include('app/pdf/viewer')
     <script>
         // connect to WS

+ 4 - 0
routes/web.php

@@ -77,6 +77,7 @@ Route::middleware('pro.auth')->group(function () {
         Route::get('notes/{filter?}', 'PracticeManagementController@notes')->name('notes');
         Route::get('bills/{filter?}', 'PracticeManagementController@bills')->name('bills');
         Route::get('my-text-shortcuts', 'PracticeManagementController@myTextShortcuts')->name('myTextShortcuts');
+        Route::get('my-favorites/{filter?}', 'PracticeManagementController@myFavorites')->name('myFavorites');
         Route::get('pro-availability/{proUid?}', 'PracticeManagementController@proAvailability')->name('proAvailability');
         Route::get('calendar/{proUid?}', 'PracticeManagementController@calendar')->name('proCalendar');
 
@@ -122,6 +123,8 @@ Route::middleware('pro.auth')->group(function () {
         // appointment calendar
         Route::get('calendar/{currentAppointment?}', 'PatientController@calendar')->name('calendar');
 
+        // programs
+        Route::get('programs/{filter?}', 'PatientController@programs')->name('programs');
     });
 
     // pro dashboard events (ajax)
@@ -154,6 +157,7 @@ Route::middleware('pro.auth')->group(function () {
 
     // pro meeting
     Route::get('/pro/meet/{uid?}', 'PracticeManagementController@meet');
+    Route::post('/pro/meet/get-participant-info', 'PracticeManagementController@getParticipantInfo');
     Route::get('/pro/get-opentok-session-key/{uid}', 'PracticeManagementController@getOpentokSessionKey');
     Route::get('/patients-in-queue', 'PracticeManagementController@getPatientsInQueue');
     Route::get('/current-work', 'PracticeManagementController@currentWork');

+ 0 - 0
storage/sections/allergies/actions.blade.php → storage/sections/allergies/deleted_actions.blade.php


+ 2 - 2
storage/sections/fhx/summary.php

@@ -17,8 +17,8 @@ for ($i = 0; $i < $count; $i++) { ?>
             <?= isset($newContentData['item_' . $i . '_general_arthritis']) ? '<div>Arthritis: <b>' . $newContentData['item_' . $i . '_general_arthritis'] . '</b></div>' : '' ?>
             <?= isset($newContentData['item_' . $i . '_general_asthma']) ? '<div>Asthma: <b>' . $newContentData['item_' . $i . '_general_asthma'] . '</b></div>' : '' ?>
             <?= isset($newContentData['item_' . $i . '_general_bleeding_disorder']) ? '<div>Bleeding disorder: <b>' . $newContentData['item_' . $i . '_general_bleeding_disorder'] . '</b></div>' : '' ?>
-            <?= isset($newContentData['item_' . $i . '_general_cad_lt_age_55']) ? '<div>Cad &gt; age 55: <b>' . $newContentData['item_' . $i . '_general_cad_lt_age_55'] . '</b></div>' : '' ?>
-            <?= isset($newContentData['item_' . $i . '_general_copd']) ? '<div>Copd: <b>' . $newContentData['item_' . $i . '_general_copd'] . '</b></div>' : '' ?>
+            <?= isset($newContentData['item_' . $i . '_general_cad_lt_age_55']) ? '<div>CAD &gt; age 55: <b>' . $newContentData['item_' . $i . '_general_cad_lt_age_55'] . '</b></div>' : '' ?>
+            <?= isset($newContentData['item_' . $i . '_general_copd']) ? '<div>COPD: <b>' . $newContentData['item_' . $i . '_general_copd'] . '</b></div>' : '' ?>
             <?= isset($newContentData['item_' . $i . '_general_diabetes']) ? '<div>Diabetes: <b>' . $newContentData['item_' . $i . '_general_diabetes'] . '</b></div>' : '' ?>
             <?= isset($newContentData['item_' . $i . '_general_heart_attack']) ? '<div>Heart attack: <b>' . $newContentData['item_' . $i . '_general_heart_attack'] . '</b></div>' : '' ?>
             <?= isset($newContentData['item_' . $i . '_general_heart_disease']) ? '<div>Heart disease: <b>' . $newContentData['item_' . $i . '_general_heart_disease'] . '</b></div>' : '' ?>

+ 1 - 1
storage/sections/vitals/default.php

@@ -1,7 +1,7 @@
 <?php
 
 $vitalMap = [
-    'heightInches' => 'Ht. (in.)',
+    'heightInInches' => 'Ht. (in.)',
     'weightPounds' => 'Wt. (lbs.)',
     'temperatureF' => 'Temp. (F)',
     'pulseRatePerMinute' => 'Pulse',