浏览代码

resolved conflicts

Josh 4 年之前
父节点
当前提交
67dc986d7c
共有 31 个文件被更改,包括 1981 次插入286 次删除
  1. 14 0
      app/Helpers/helpers.php
  2. 2 2
      app/Http/Controllers/LoginController.php
  3. 5 0
      app/Http/Controllers/PatientController.php
  4. 1 1
      app/Http/Controllers/PracticeManagementController.php
  5. 9 1
      app/Models/Client.php
  6. 7 0
      app/Models/ClientProgram.php
  7. 11 0
      app/Models/ClientProgramMonth.php
  8. 25 0
      app/Models/Measurement.php
  9. 2 0
      config/app.php
  10. 73 7
      public/css/meeting.css
  11. 12 1
      public/css/style.css
  12. 1 1
      resources/views/app/current-work.blade.php
  13. 1 1
      resources/views/app/patient/memos.blade.php
  14. 14 4
      resources/views/app/patient/partials/equipment.blade.php
  15. 15 5
      resources/views/app/patient/partials/erx.blade.php
  16. 13 3
      resources/views/app/patient/partials/imaging.blade.php
  17. 14 3
      resources/views/app/patient/partials/lab.blade.php
  18. 60 0
      resources/views/app/patient/partials/mcp-queue.blade.php
  19. 111 0
      resources/views/app/patient/partials/measurement.blade.php
  20. 2 49
      resources/views/app/patient/partials/measurements.blade.php
  21. 21 5
      resources/views/app/patient/partials/other.blade.php
  22. 10 179
      resources/views/app/patient/partials/programs.blade.php
  23. 13 0
      resources/views/app/patient/partials/ticket_action_links.blade.php
  24. 22 0
      resources/views/app/patient/partials/ticket_update_pro_form.blade.php
  25. 3 0
      resources/views/app/patient/partials/ticket_vue_data.blade.php
  26. 125 0
      resources/views/app/patient/partials/ticket_vue_methods.blade.php
  27. 559 0
      resources/views/app/patient/programs.blade.php
  28. 803 0
      resources/views/app/video/call-agora-v2.blade.php
  29. 4 1
      resources/views/layouts/patient.blade.php
  30. 27 23
      resources/views/layouts/template.blade.php
  31. 2 0
      routes/web.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');
         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;
+    }
+}

+ 2 - 2
app/Http/Controllers/LoginController.php

@@ -183,7 +183,7 @@ class LoginController extends Controller
         return redirect()->to(config('stag.authUrl').'/logout');
         return redirect()->to(config('stag.authUrl').'/logout');
     }
     }
 
 
-    public function loginWithSessionKey($sessionKey, $appAccessUID, Request $request){
+    public function loginWithSessionKey($sessionKey, $appAccessUID=null, Request $request){
         $url = "/session/pro_log_in_with_session_key/${sessionKey}";
         $url = "/session/pro_log_in_with_session_key/${sessionKey}";
         if(!!$appAccessUID) {
         if(!!$appAccessUID) {
             $url .= "/$appAccessUID";
             $url .= "/$appAccessUID";
@@ -196,7 +196,7 @@ class LoginController extends Controller
             if (!property_exists($data, 'success') || !$data->success) {
             if (!property_exists($data, 'success') || !$data->success) {
                 return redirect('/mc');
                 return redirect('/mc');
             }
             }
-
+            
             Cookie::queue('sessionKey', $data->data->sessionKey);
             Cookie::queue('sessionKey', $data->data->sessionKey);
 
 
             return redirect('/mc');
             return redirect('/mc');

+ 5 - 0
app/Http/Controllers/PatientController.php

@@ -236,4 +236,9 @@ class PatientController extends Controller
     public function calendar(Request $request, Client $patient, Appointment $currentAppointment) {
     public function calendar(Request $request, Client $patient, Appointment $currentAppointment) {
         return view('app.patient.appointment-calendar', compact('patient', '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'));
+    }
 }
 }

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

@@ -254,7 +254,7 @@ class PracticeManagementController extends Controller
     public function meet(Request $request, $uid = false) {
     public function meet(Request $request, $uid = false) {
         $session = AppSession::where('session_key', $request->cookie('sessionKey'))->first();
         $session = AppSession::where('session_key', $request->cookie('sessionKey'))->first();
         $client = !empty($uid) ? Client::where('uid', $uid)->first() : null;
         $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) {
     public function getParticipantInfo(Request $request) {

+ 9 - 1
app/Models/Client.php

@@ -91,6 +91,13 @@ class Client extends Model
             ->orderBy('label', 'asc')
             ->orderBy('label', 'asc')
             ->orderBy('effective_date', 'desc');
             ->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() {
     public function smses() {
         return $this->hasMany(ClientSMS::class, 'client_id', 'id')
         return $this->hasMany(ClientSMS::class, 'client_id', 'id')
@@ -161,7 +168,8 @@ class Client extends Model
 
 
     public function clientPrograms() {
     public function clientPrograms() {
         return $this->hasMany(ClientProgram::class, 'client_id', 'id')
         return $this->hasMany(ClientProgram::class, 'client_id', 'id')
-            ->where('is_active', true);
+            ->where('is_active', true)
+            ->orderBy('title', 'desc');
     }
     }
 
 
     public function tickets() {
     public function tickets() {

+ 7 - 0
app/Models/ClientProgram.php

@@ -16,4 +16,11 @@ class ClientProgram extends Model
         return $this->hasOne(Pro::class, 'id', 'manager_pro_id');
         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";
+
+}

+ 25 - 0
app/Models/Measurement.php

@@ -16,4 +16,29 @@ class Measurement extends Model
     public function numCPMEntries() {
     public function numCPMEntries() {
         return ClientProgramMonthEntry::where('measurement_id', $this->id)->where('is_cancelled', false)->count();
         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();
+    }
 }
 }

+ 2 - 0
config/app.php

@@ -59,6 +59,8 @@ return [
     'agora_appid' => env('AGORA_APPID'),
     'agora_appid' => env('AGORA_APPID'),
     'agora_mode' => env('AGORA_MODE'),
     'agora_mode' => env('AGORA_MODE'),
 
 
+    'backend_ws_url' => env('BACKEND_WS_URL'),
+
     /*
     /*
     |--------------------------------------------------------------------------
     |--------------------------------------------------------------------------
     | Application Timezone
     | Application Timezone

+ 73 - 7
public/css/meeting.css

@@ -112,7 +112,7 @@ h1 {
     color: #fff;
     color: #fff;
     font-size: 13px;
     font-size: 13px;
     white-space: nowrap;
     white-space: nowrap;
-    padding: 0.5rem;
+    padding: 0.6rem 0.5rem 0.4rem 34px;
     overflow: hidden;
     overflow: hidden;
     text-overflow: ellipsis;
     text-overflow: ellipsis;
     pointer-events: none;
     pointer-events: none;
@@ -122,7 +122,7 @@ h1 {
 .main-view .thumbs {
 .main-view .thumbs {
     position: absolute;
     position: absolute;
     z-index: 2;
     z-index: 2;
-    bottom: 1.5rem;
+    bottom: 1rem;
     right: 1rem;
     right: 1rem;
     width: 180px;
     width: 180px;
     height: 100%;
     height: 100%;
@@ -151,13 +151,72 @@ h1 {
     left: 0;
     left: 0;
     width: 100%;
     width: 100%;
     bottom: 0;
     bottom: 0;
-    background: rgba(0, 0, 0, 0.5);
+    background: rgba(0, 0, 0, 0.75);
     color: #fff;
     color: #fff;
     font-size: 11px;
     font-size: 11px;
     white-space: nowrap;
     white-space: nowrap;
     padding: 0 5px;
     padding: 0 5px;
     overflow: hidden;
     overflow: hidden;
     text-overflow: ellipsis;
     text-overflow: ellipsis;
+    font-weight: bold;
+    text-align: center;
+}
+.media-status-indicator {
+    position: absolute;
+    z-index: 1;
+    right: 3px;
+    top: 3px;
+    display: flex;
+    justify-content: flex-end;
+    width: calc(100% - 6px);
+}
+.full-view .media-status-indicator {
+    right: 8px;
+    top: 8px;
+}
+.full-view .user-type-indicator {
+    left: 8px;
+    top: 8px;
+}
+.media-status-indicator i.muted {
+    font-size: 12px;
+    color: #fff;
+    background: #333;
+    width: 20px;
+    height: 20px;
+    text-align: center;
+    line-height: 20px;
+    border-radius: 3px;
+}
+.user-type-indicator {
+    position: absolute;
+    z-index: 1;
+    left: 3px;
+    top: 3px;
+    display: flex;
+    justify-content: end;
+}
+.user-type-indicator i {
+    font-size: 12px;
+    color: #fff;
+    background: #333;
+    width: 20px;
+    height: 20px;
+    text-align: center;
+    line-height: 20px;
+    border-radius: 3px;
+}
+.thumb-view .media-status-indicator i.muted,
+.thumb-view .user-type-indicator i {
+    font-size: 9px;
+    width: 18px;
+    height: 18px;
+    line-height: 18px;
+    opacity: 0.7;
+}
+.thumb-view:hover .media-status-indicator i.muted,
+.thumb-view:hover .user-type-indicator i {
+    opacity: 1;
 }
 }
 .main-view .thumbs .disconnected-view {
 .main-view .thumbs .disconnected-view {
     opacity: 0;
     opacity: 0;
@@ -212,14 +271,21 @@ h1 {
 body .OT_fit-mode-cover .OT_video-element {
 body .OT_fit-mode-cover .OT_video-element {
     object-fit: contain;
     object-fit: contain;
 }
 }
-.hang-up {
+.call-actions {
     position: absolute;
     position: absolute;
     z-index: 2;
     z-index: 2;
     bottom: 1rem;
     bottom: 1rem;
     left: 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 {
 .call-mcp {
     position: absolute;
     position: absolute;

+ 12 - 1
public/css/style.css

@@ -263,6 +263,9 @@ body>nav.navbar {
     width: 100% !important;
     width: 100% !important;
     min-width: unset !important;
     min-width: unset !important;
 }
 }
+.mcp-theme-1 .max-width-200px {
+    max-width: 200px;
+}
 .mcp-theme-1 .max-width-300px {
 .mcp-theme-1 .max-width-300px {
     max-width: 300px;
     max-width: 300px;
 }
 }
@@ -850,6 +853,7 @@ body .node input[type="number"] {
     left: 0;
     left: 0;
     width: 100%;
     width: 100%;
     bottom: 0;
     bottom: 0;
+    z-index: 4;
 }
 }
 .queue-item {
 .queue-item {
     width: 100px;
     width: 100px;
@@ -966,7 +970,11 @@ span.pro-selection {
     border-top-right-radius: 3px;
     border-top-right-radius: 3px;
     border-bottom-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;
     padding: 0 !important;
     overflow: hidden !important;
     overflow: hidden !important;
 }
 }
@@ -1116,3 +1124,6 @@ input[data-option-list]:focus+.data-option-list {
 .data-option-list>div:hover {
 .data-option-list>div:hover {
     background: aliceblue;
     background: aliceblue;
 }
 }
+.measurement-item:not(:last-child) {
+    border-bottom: 1px solid #e7e7e7;
+}

+ 1 - 1
resources/views/app/current-work.blade.php

@@ -3,7 +3,7 @@
     <div class="current-work-indicator" title="Started: {{ friendly_time($currentWork->start_time) }}">
     <div class="current-work-indicator" title="Started: {{ friendly_time($currentWork->start_time) }}">
         <i class="fa fa-folder-open mr-1"></i>
         <i class="fa fa-folder-open mr-1"></i>
         <a class="text-white font-weight-bold text-sm" href="/patients/view/{{ $currentWork->client->uid }}">
         <a class="text-white font-weight-bold text-sm" href="/patients/view/{{ $currentWork->client->uid }}">
-            {{ $currentWork->client->displayName() }}
+           {{ $currentWork->client->displayName() }}
         </a>
         </a>
     </div>
     </div>
 @endif
 @endif

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

@@ -39,7 +39,7 @@
                 @foreach($patient->memos as $memo)
                 @foreach($patient->memos as $memo)
                     <tr>
                     <tr>
                         <td class="px-2">{{ $memo->category }}</td>
                         <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">{{ friendly_date_time($memo->created_at) }}</td>
                         <td class="px-2 text-center delete-column">
                         <td class="px-2 text-center delete-column">
                             <div moe wide relative class="mr-2">
                             <div moe wide relative class="mr-2">

+ 14 - 4
resources/views/app/patient/partials/equipment.blade.php

@@ -6,7 +6,7 @@
     </div>
     </div>
     <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
     <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
         <thead>
         <thead>
-        <tr>
+        <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 width-30px">#</th>
             <th class="px-2 text-secondary border-bottom-0">Items</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">Purpose</th>
@@ -16,14 +16,16 @@
         </thead>
         </thead>
         <tbody>
         <tbody>
             <tr v-for="(item, index) in items" :class="item.is_open ? '' : 'opacity-60'">
             <tr v-for="(item, index) in items" :class="item.is_open ? '' : 'opacity-60'">
-                <td class="px-2">#</td>
+                <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" 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.purpose}}</td>
                 <td class="px-2">@{{item.memo}}</td>
                 <td class="px-2">@{{item.memo}}</td>
-                <td class="px-2 text-nowrap">
+                <td class="px-2">
                     <a class="mr-2 c-pointer" v-on:click.prevent="showPopup('equipment-popup', item)">Edit</a>
                     <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="closeItem(item)">Close</a>
                     <a class="mr-2 c-pointer" v-if="!item.is_open" v-on:click.prevent="openItem(item)">Open</a>
                     <a class="mr-2 c-pointer" v-if="!item.is_open" v-on:click.prevent="openItem(item)">Open</a>
+
+                    @include('app.patient.partials.ticket_action_links')
                 </td>
                 </td>
             </tr>
             </tr>
         </tbody>
         </tbody>
@@ -69,6 +71,7 @@
             </div>
             </div>
         </form>
         </form>
     </div>
     </div>
+    @include('app.patient.partials.ticket_update_pro_form',['ticketType'=>'equipment'])
 </div>
 </div>
 <script>
 <script>
     (function() {
     (function() {
@@ -81,6 +84,11 @@
             $item = json_decode($ticket->data);
             $item = json_decode($ticket->data);
             $item->uid = $ticket->uid;
             $item->uid = $ticket->uid;
             $item->is_open = $ticket->is_open;
             $item->is_open = $ticket->is_open;
+            $item->has_assigned_pro_signed = $ticket->has_assigned_pro_signed;
+            $item->has_initiating_pro_signed = $ticket->has_initiating_pro_signed;
+            $item->has_manager_pro_signed = $ticket->has_manager_pro_signed;
+            $item->has_ordering_pro_signed = $ticket->has_ordering_pro_signed;
+            $item->is_entry_error = $ticket->is_entry_error;
             $items[] = $item;
             $items[] = $item;
         }
         }
         ?>
         ?>
@@ -99,6 +107,7 @@
                         purpose: '',
                         purpose: '',
                         memo: '',
                         memo: '',
                     },
                     },
+                    @include('app.patient.partials.ticket_vue_data')
                 },
                 },
                 methods: {
                 methods: {
                     showPopup: function(_name, _item) {
                     showPopup: function(_name, _item) {
@@ -162,7 +171,8 @@
                         }, function(_data) {
                         }, function(_data) {
                             fastReload();
                             fastReload();
                         });
                         });
-                    }
+                    },
+                    @include('app.patient.partials.ticket_vue_methods',['ticketType'=>'equipment'])
                 },
                 },
                 mounted: function () {
                 mounted: function () {
 
 

+ 15 - 5
resources/views/app/patient/partials/erx.blade.php

@@ -6,7 +6,7 @@
     </div>
     </div>
     <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
     <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
         <thead>
         <thead>
-        <tr>
+        <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 width-30px">#</th>
             <th class="px-2 text-secondary border-bottom-0">Medication</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">Strength</th>
@@ -22,7 +22,7 @@
         </thead>
         </thead>
         <tbody>
         <tbody>
             <tr v-for="(item, index) in items" :class="item.is_open ? '' : 'opacity-60'">
             <tr v-for="(item, index) in items" :class="item.is_open ? '' : 'opacity-60'">
-                <td class="px-2">#</td>
+                <td class="px-2">@{{ index + 1 }}</td>
                 <td class="px-2">@{{item.medication}}</td>
                 <td class="px-2">@{{item.medication}}</td>
                 <td class="px-2">@{{item.strength}}</td>
                 <td class="px-2">@{{item.strength}}</td>
                 <td class="px-2">@{{item.amount}}</td>
                 <td class="px-2">@{{item.amount}}</td>
@@ -32,10 +32,12 @@
                 <td class="px-2">@{{item.refills}}</td>
                 <td class="px-2">@{{item.refills}}</td>
                 <td class="px-2">@{{item.purpose}}</td>
                 <td class="px-2">@{{item.purpose}}</td>
                 <td class="px-2">@{{item.pharmacy}}</td>
                 <td class="px-2">@{{item.pharmacy}}</td>
-                <td class="px-2 text-nowrap">
+                <td class="px-2">
                     <a class="mr-2 c-pointer" v-on:click.prevent="showPopup('erx-popup', item)">Edit</a>
                     <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="closeItem(item)">Close</a>
                     <a class="mr-2 c-pointer" v-if="!item.is_open" v-on:click.prevent="openItem(item)">Open</a>
                     <a class="mr-2 c-pointer" v-if="!item.is_open" v-on:click.prevent="openItem(item)">Open</a>
+                    
+                    @include('app.patient.partials.ticket_action_links')
                 </td>
                 </td>
             </tr>
             </tr>
         </tbody>
         </tbody>
@@ -155,6 +157,7 @@
             </div>
             </div>
         </form>
         </form>
     </div>
     </div>
+    @include('app.patient.partials.ticket_update_pro_form',['ticketType'=>'erx'])
 </div>
 </div>
 <script>
 <script>
     (function() {
     (function() {
@@ -167,6 +170,11 @@
             $item = json_decode($ticket->data);
             $item = json_decode($ticket->data);
             $item->uid = $ticket->uid;
             $item->uid = $ticket->uid;
             $item->is_open = $ticket->is_open;
             $item->is_open = $ticket->is_open;
+            $item->has_assigned_pro_signed = $ticket->has_assigned_pro_signed;
+            $item->has_initiating_pro_signed = $ticket->has_initiating_pro_signed;
+            $item->has_manager_pro_signed = $ticket->has_manager_pro_signed;
+            $item->has_ordering_pro_signed = $ticket->has_ordering_pro_signed;
+            $item->is_entry_error = $ticket->is_entry_error;
             $items[] = $item;
             $items[] = $item;
         }
         }
         ?>
         ?>
@@ -195,7 +203,8 @@
                         pharmacyAddressMemo: '',
                         pharmacyAddressMemo: '',
                         pharmacyPhone: '',
                         pharmacyPhone: '',
                         pharmacyFax: '',
                         pharmacyFax: '',
-                    }
+                    },
+                    @include('app.patient.partials.ticket_vue_data')
                 },
                 },
                 methods: {
                 methods: {
                     showPopup: function(_name, _item) {
                     showPopup: function(_name, _item) {
@@ -320,7 +329,8 @@
                         catch (e) {
                         catch (e) {
                             return '';
                             return '';
                         }
                         }
-                    }
+                    },
+                    @include('app.patient.partials.ticket_vue_methods',['ticketType'=>'erx'])
                 },
                 },
                 mounted: function () {
                 mounted: function () {
                     this.initRxAutoSuggest();
                     this.initRxAutoSuggest();

+ 13 - 3
resources/views/app/patient/partials/imaging.blade.php

@@ -6,7 +6,7 @@
     </div>
     </div>
     <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
     <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
         <thead>
         <thead>
-        <tr>
+        <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 width-30px">#</th>
             <th class="px-2 text-secondary border-bottom-0">Tests</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">ICDs</th>
@@ -16,14 +16,16 @@
         </thead>
         </thead>
         <tbody>
         <tbody>
         <tr v-for="(item, index) in items" :class="item.is_open ? '' : 'opacity-60'">
         <tr v-for="(item, index) in items" :class="item.is_open ? '' : 'opacity-60'">
-            <td class="px-2">#</td>
+            <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.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" 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">@{{item.memo}}</td>
-            <td class="px-2 text-nowrap">
+            <td class="px-2">
                 <a class="mr-2 c-pointer" v-on:click.prevent="showPopup('imaging-popup', item)">Edit</a>
                 <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="closeItem(item)">Close</a>
                 <a class="mr-2 c-pointer" v-if="!item.is_open" v-on:click.prevent="openItem(item)">Open</a>
                 <a class="mr-2 c-pointer" v-if="!item.is_open" v-on:click.prevent="openItem(item)">Open</a>
+
+                @include('app.patient.partials.ticket_action_links')        
             </td>
             </td>
         </tr>
         </tr>
         </tbody>
         </tbody>
@@ -85,6 +87,7 @@
             </div>
             </div>
         </form>
         </form>
     </div>
     </div>
+    @include('app.patient.partials.ticket_update_pro_form',['ticketType'=>'imaging'])
 </div>
 </div>
 <script>
 <script>
     (function() {
     (function() {
@@ -97,6 +100,11 @@
             $item = json_decode($ticket->data);
             $item = json_decode($ticket->data);
             $item->uid = $ticket->uid;
             $item->uid = $ticket->uid;
             $item->is_open = $ticket->is_open;
             $item->is_open = $ticket->is_open;
+            $item->has_assigned_pro_signed = $ticket->has_assigned_pro_signed;
+            $item->has_initiating_pro_signed = $ticket->has_initiating_pro_signed;
+            $item->has_manager_pro_signed = $ticket->has_manager_pro_signed;
+            $item->has_ordering_pro_signed = $ticket->has_ordering_pro_signed;
+            $item->is_entry_error = $ticket->is_entry_error;
             $items[] = $item;
             $items[] = $item;
         }
         }
         ?>
         ?>
@@ -115,6 +123,7 @@
                         icds: [''],
                         icds: [''],
                         memo: '',
                         memo: '',
                     },
                     },
+                    @include('app.patient.partials.ticket_vue_data')
                 },
                 },
                 methods: {
                 methods: {
                     showPopup: function(_name, _item) {
                     showPopup: function(_name, _item) {
@@ -205,6 +214,7 @@
                             $(elem).attr('ac-initialized', 1);
                             $(elem).attr('ac-initialized', 1);
                         });
                         });
                     },
                     },
+                    @include('app.patient.partials.ticket_vue_methods',['ticketType'=>'imaging'])
                 },
                 },
                 mounted: function () {
                 mounted: function () {
                     this.initICDAutoSuggest();
                     this.initICDAutoSuggest();

+ 14 - 3
resources/views/app/patient/partials/lab.blade.php

@@ -6,7 +6,7 @@
     </div>
     </div>
     <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
     <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
         <thead>
         <thead>
-        <tr>
+        <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 width-30px">#</th>
             <th class="px-2 text-secondary border-bottom-0">Tests</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">ICDs</th>
@@ -16,14 +16,17 @@
         </thead>
         </thead>
         <tbody>
         <tbody>
             <tr v-for="(item, index) in items" :class="item.is_open ? '' : 'opacity-60'">
             <tr v-for="(item, index) in items" :class="item.is_open ? '' : 'opacity-60'">
-                <td class="px-2">#</td>
+                <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.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" 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">@{{item.memo}}</td>
-                <td class="px-2 text-nowrap">
+                <td class="px-2">
                     <a class="mr-2 c-pointer" v-on:click.prevent="showPopup('lab-popup', item)">Edit</a>
                     <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="closeItem(item)">Close</a>
                     <a class="mr-2 c-pointer" v-if="!item.is_open" v-on:click.prevent="openItem(item)">Open</a>
                     <a class="mr-2 c-pointer" v-if="!item.is_open" v-on:click.prevent="openItem(item)">Open</a>
+
+                    @include('app.patient.partials.ticket_action_links')
+
                 </td>
                 </td>
             </tr>
             </tr>
         </tbody>
         </tbody>
@@ -88,6 +91,7 @@
             </div>
             </div>
         </form>
         </form>
     </div>
     </div>
+    @include('app.patient.partials.ticket_update_pro_form',['ticketType'=>'lab'])
 </div>
 </div>
 <script>
 <script>
     (function() {
     (function() {
@@ -100,6 +104,11 @@
             $item = json_decode($ticket->data);
             $item = json_decode($ticket->data);
             $item->uid = $ticket->uid;
             $item->uid = $ticket->uid;
             $item->is_open = $ticket->is_open;
             $item->is_open = $ticket->is_open;
+            $item->has_assigned_pro_signed = $ticket->has_assigned_pro_signed;
+            $item->has_initiating_pro_signed = $ticket->has_initiating_pro_signed;
+            $item->has_manager_pro_signed = $ticket->has_manager_pro_signed;
+            $item->has_ordering_pro_signed = $ticket->has_ordering_pro_signed;
+            $item->is_entry_error = $ticket->is_entry_error;
             $items[] = $item;
             $items[] = $item;
         }
         }
         ?>
         ?>
@@ -118,6 +127,7 @@
                         icds: [''],
                         icds: [''],
                         memo: '',
                         memo: '',
                     },
                     },
+                    @include('app.patient.partials.ticket_vue_data')
                 },
                 },
                 methods: {
                 methods: {
                     showPopup: function(_name, _item) {
                     showPopup: function(_name, _item) {
@@ -208,6 +218,7 @@
                             $(elem).attr('ac-initialized', 1);
                             $(elem).attr('ac-initialized', 1);
                         });
                         });
                     },
                     },
+                    @include('app.patient.partials.ticket_vue_methods',['ticketType'=>'lab'])
                 },
                 },
                 mounted: function () {
                 mounted: function () {
                     this.initICDAutoSuggest();
                     this.initICDAutoSuggest();

+ 60 - 0
resources/views/app/patient/partials/mcp-queue.blade.php

@@ -0,0 +1,60 @@
+<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.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>

+ 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>

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

@@ -51,8 +51,8 @@
                                 <a class="on-hover-opaque" start show title="Update">
                                 <a class="on-hover-opaque" start show title="Update">
                                     <i class="font-size-11 fa fa-edit text-primary"></i>
                                     <i class="font-size-11 fa fa-edit text-primary"></i>
                                 </a>
                                 </a>
-                                <form url="/api/measurement/create">
-                                    <input type="hidden" name="clientUid" value="{{ $patient->uid }}">
+                                <form url="/api/measurement/updateBasic">
+                                    <input type="hidden" name="uid" value="{{ $measurement->uid }}">
                                     <div class="mb-2">
                                     <div class="mb-2">
                                         <input required type="text" class="form-control form-control-sm" name="label" value="{{ $measurement->label }}" placeholder="Type">
                                         <input required type="text" class="form-control form-control-sm" name="label" value="{{ $measurement->label }}" placeholder="Type">
                                     </div>
                                     </div>
@@ -71,53 +71,6 @@
                             <span>{{ $measurement->label }}:</span>
                             <span>{{ $measurement->label }}:</span>
                             <span class="font-weight-bold ml-1">{{ $measurement->value }}</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>
                             <span class="font-weight-normal text-secondary ml-2 text-sm">(as on {{ friendly_date_time($measurement->effective_date, false) }})</span>
-                            @php $matchingClientPrograms = $pro->canAddCPMEntryForMeasurement($measurement, $pro); @endphp
-                            @if($matchingClientPrograms && count($matchingClientPrograms))
-                                <div moe>
-                                    <a href="#" class="ml-2 font-weight-normal" start show><i class="fa fa-clock"></i> <b>Entry</b> ({{ $measurement->numCPMEntries() }})</a>
-                                    <form url="/api/clientProgramMonthEntry/create">
-                                        <input type="hidden" name="measurementUid" value="{{ $measurement->uid }}">
-                                        <input type="hidden" name="proUid" value="{{ $pro->uid }}">
-                                        @if(count($matchingClientPrograms) === 1)
-                                            <input type="hidden" name="clientProgramUid" value="{{ $matchingClientPrograms->first()->uid }}">
-                                        @else
-                                            <div class="mb-2">
-                                                <label class="text-sm mb-1 text-secondary">Program</label>
-                                                <select autofocus class="form-control form-control-sm" name="clientProgramUid" required>
-                                                    <option value="">-- select --</option>
-                                                    @foreach($matchingClientPrograms as $clientProgram)
-                                                        <option value="{{$clientProgram->uid}}">{{$clientProgram->title}}</option>
-                                                    @endforeach
-                                                </select>
-                                            </div>
-                                        @endif
-                                        <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">Quick Text</label>
-                                            <input autofocus required 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" name="measurementDetermination" value="">
-                                        </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="mb-2">
-                                            <label class="text-sm mb-1 text-secondary">Time (minutes)</label>
-                                            <input required type="number" class="form-control form-control-sm" name="timeInMinutes" 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>
-                            @endif
                         </div>
                         </div>
                     </td>
                     </td>
                 </tr>
                 </tr>

+ 21 - 5
resources/views/app/patient/partials/other.blade.php

@@ -6,7 +6,7 @@
     </div>
     </div>
     <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
     <table class="table table-sm table-bordered mb-0" style="table-layout: fixed">
         <thead>
         <thead>
-        <tr>
+        <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 width-30px">#</th>
             <th class="px-2 text-secondary border-bottom-0">Category</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">Title</th>
@@ -16,14 +16,17 @@
         </thead>
         </thead>
         <tbody>
         <tbody>
             <tr v-for="(item, index) in items" :class="item.is_open ? '' : 'opacity-60'">
             <tr v-for="(item, index) in items" :class="item.is_open ? '' : 'opacity-60'">
-                <td class="px-2">#</td>
+                <td class="px-2">@{{ index + 1 }}</td>
                 <td class="px-2" v-html="item.category"></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.title"></td>
                 <td class="px-2" v-html="item.description"></td>
                 <td class="px-2" v-html="item.description"></td>
-                <td class="px-2 text-nowrap">
+                <td class="px-2">
                     <a class="mr-2 c-pointer" v-on:click.prevent="showPopup('other-popup', item)">Edit</a>
                     <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="closeItem(item)">Close</a>
                     <a class="mr-2 c-pointer" v-if="!item.is_open" v-on:click.prevent="openItem(item)">Open</a>
                     <a class="mr-2 c-pointer" v-if="!item.is_open" v-on:click.prevent="openItem(item)">Open</a>
+                    
+                    @include('app.patient.partials.ticket_action_links')
+
                 </td>
                 </td>
             </tr>
             </tr>
         </tbody>
         </tbody>
@@ -53,18 +56,26 @@
             </div>
             </div>
         </form>
         </form>
     </div>
     </div>
+    @include('app.patient.partials.ticket_update_pro_form',['ticketType'=>'other'])
 </div>
 </div>
+
 <script>
 <script>
     (function() {
     (function() {
         <?php
         <?php
         $tickets = $patient->tickets->filter(function($_item) {
         $tickets = $patient->tickets->filter(function($_item) {
             return $_item->category === 'other';
             return $_item->category === 'other';
         });
         });
+      
         $items = [];
         $items = [];
         foreach ($tickets as $ticket) {
         foreach ($tickets as $ticket) {
             $item = json_decode($ticket->data);
             $item = json_decode($ticket->data);
             $item->uid = $ticket->uid;
             $item->uid = $ticket->uid;
             $item->is_open = $ticket->is_open;
             $item->is_open = $ticket->is_open;
+            $item->has_assigned_pro_signed = $ticket->has_assigned_pro_signed;
+            $item->has_initiating_pro_signed = $ticket->has_initiating_pro_signed;
+            $item->has_manager_pro_signed = $ticket->has_manager_pro_signed;
+            $item->has_ordering_pro_signed = $ticket->has_ordering_pro_signed;
+            $item->is_entry_error = $ticket->is_entry_error;
             $items[] = $item;
             $items[] = $item;
         }
         }
         ?>
         ?>
@@ -81,8 +92,9 @@
                         is_open: true,
                         is_open: true,
                         category: '',
                         category: '',
                         title: '',
                         title: '',
-                        description: '',
+                        description: ''
                     },
                     },
+                    @include('app.patient.partials.ticket_vue_data')
                 },
                 },
                 methods: {
                 methods: {
                     showPopup: function(_name, _item) {
                     showPopup: function(_name, _item) {
@@ -134,6 +146,7 @@
 
 
                         return false;
                         return false;
                     },
                     },
+
                     closeItem: function(_item) {
                     closeItem: function(_item) {
                         showMask();
                         showMask();
                         $.post('/api/ticket/close', {
                         $.post('/api/ticket/close', {
@@ -142,6 +155,7 @@
                             fastReload();
                             fastReload();
                         });
                         });
                     },
                     },
+
                     openItem: function(_item) {
                     openItem: function(_item) {
                         showMask();
                         showMask();
                         $.post('/api/ticket/open', {
                         $.post('/api/ticket/open', {
@@ -149,7 +163,9 @@
                         }, function(_data) {
                         }, function(_data) {
                             fastReload();
                             fastReload();
                         });
                         });
-                    }
+                    },
+
+                    @include('app.patient.partials.ticket_vue_methods',['ticketType'=>'other'])
                 },
                 },
                 mounted: function () {
                 mounted: function () {
                 }
                 }

+ 10 - 179
resources/views/app/patient/partials/programs.blade.php

@@ -1,25 +1,6 @@
 <div class="mt-0 pb-1">
 <div class="mt-0 pb-1">
     <div class="d-flex align-items-center mb-1 pt-2 pb-1 border-top">
     <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>
         <h6 class="my-0 font-weight-bold text-secondary">Programs</h6>
-        {{--<span class="mx-2 text-secondary on-hover-opaque">|</span>
-        <div moe>
-            <a start show class="py-0 font-weight-normal">Add</a>
-            <form url="/api/clientProgram/create">
-                <input type="hidden" name="clientUid" value="{{ $patient->uid }}">
-                <div class="mb-2">
-                    <select class="form-control form-control-sm" name="programUid" required>
-                        <option value="">-- select --</option>
-                        @foreach($programs as $program)
-                            <option value="{{$program->uid}}">{{$program->title}}</option>
-                        @endforeach
-                    </select>
-                </div>
-                <div class="d-flex align-items-center">
-                    <button class="btn btn-sm btn-primary mr-2" submit>Add</button>
-                    <button class="btn btn-sm btn-default mr-2 border" cancel>Cancel</button>
-                </div>
-            </form>
-        </div>--}}
     </div>
     </div>
     <table class="table table-bordered table-sm table-striped m-0">
     <table class="table table-bordered table-sm table-striped m-0">
         <tbody>
         <tbody>
@@ -27,175 +8,25 @@
         @foreach($patient->clientPrograms as $clientProgram)
         @foreach($patient->clientPrograms as $clientProgram)
             <?php $program = $clientProgram; ?>
             <?php $program = $clientProgram; ?>
             <tr>
             <tr>
-                <td class="text-black p-2">
-                    <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/clientProgram/deactivate">
-                                <input type="hidden" name="uid" value="{{ $clientProgram->uid }}">
-                                <p class="small min-width-200px">Are you sure you want to deactivate this client program?</p>
-                                <div class="d-flex align-items-center">
-                                    <button class="btn btn-sm btn-danger mr-2" submit>Deactivate</button>
-                                    <button class="btn btn-sm btn-default mr-2 border" cancel>Cancel</button>
-                                </div>
-                            </form>
-                        </div>
-                        <span class="font-weight-bold">{{ $program->title }}</span>
+                <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>
                     </div>
                     <?php $programNumber++; ?>
                     <?php $programNumber++; ?>
                     <div class="mt-1 pl-3 d-flex align-items-center flex-wrap">
                     <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="pr-1">MCP: <b class="text-secondary">{{ $clientProgram->mcp ? $clientProgram->mcp->displayName() : '' }}</b></span>
-                        <span moe>
-                            <a start show class="py-0 font-weight-normal"><i class="fa fa-pencil-alt"></i></a>
-                            <form url="/api/clientProgram/changeMcp">
-                                <input type="hidden" name="uid" value="{{ $clientProgram->uid }}">
-                                <div class="mb-2">
-                                    <label class="text-sm mb-1 text-secondary">MCP</label>
-                                    <select class="form-control form-control-sm bg-light" name="newMcpProUid" required>
-                                        <option value="">-- select --</option>
-                                        @foreach($pros as $iPro)
-                                            <option value="{{$iPro->uid}}" {{ $clientProgram->mcp_pro_id === $iPro->id ? 'selected' : '' }}>{{$iPro->displayName()}}</option>
-                                        @endforeach
-                                    </select>
-                                </div>
-                                <div class="mb-2">
-                                    <label class="mb-1 text-secondary d-flex align-items-center">
-                                        <span class="mr-2">Is Intention To Remove MCP</span>
-                                        <input type="checkbox" name="isIntentionToRemoveMcp">
-                                    </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>
-                        </span>
                         <span class="mx-2 text-secondary on-hover-opaque">|</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>
                         <span class="pr-1">Manager: <b class="text-secondary">{{ $clientProgram->manager ? $clientProgram->manager->displayName() : '' }}</b></span>
-                        <span moe>
-                            <a start show class="py-0 font-weight-normal"><i class="fa fa-pencil-alt"></i></a>
-                            <form url="/api/clientProgram/changeManager">
-                                <input type="hidden" name="uid" value="{{ $clientProgram->uid }}">
-                                <div class="mb-2">
-                                    <label class="text-sm mb-1 text-secondary">Manager</label>
-                                    <select class="form-control form-control-sm bg-light" name="newManagerProUid" required>
-                                        <option value="">-- select --</option>
-                                        @foreach($pros as $iPro)
-                                            <option value="{{$iPro->uid}}" {{ $clientProgram->manager_pro_id === $iPro->id ? 'selected' : '' }}>{{$iPro->displayName()}}</option>
-                                        @endforeach
-                                    </select>
-                                </div>
-                                <div class="mb-2">
-                                    <label class="mb-1 text-secondary d-flex align-items-center">
-                                        <span class="mr-2">Is Intention To Remove Manager</span>
-                                        <input type="checkbox" name="isIntentionToRemoveManager">
-                                    </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>
-                        </span>
-                        <span class="mx-2 text-secondary on-hover-opaque">|</span>
-                        <span class="pr-1">OB Visit: <b class="text-secondary">{{ $clientProgram->has_mcp_done_onboarding_visit }}</b></span>
-                        <span moe>
-                            <a start show class="py-0 font-weight-normal"><i class="fa fa-pencil-alt"></i></a>
-                            <form url="/api/clientProgram/editMcpOnboardingVisitInfo">
-                                <input type="hidden" name="uid" value="{{ $clientProgram->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" {{ $clientProgram->has_mcp_done_onboarding_visit === 'YES' ? 'selected' : '' }}>Yes</option>
-                                        <option value="NO" {{ $clientProgram->has_mcp_done_onboarding_visit === 'NO' ? 'selected' : '' }}>No</option>
-                                        <option value="UNKNOWN" {{ $clientProgram->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="{{ $clientProgram->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>
-                        </span>
                     </div>
                     </div>
                     <div class="mt-1 pl-3 d-flex align-items-center flex-wrap">
                     <div class="mt-1 pl-3 d-flex align-items-center flex-wrap">
-                        <span class="pr-1">Setup: <b class="text-secondary">{{ $clientProgram->is_setup_complete }}</b></span>
-                        <span moe>
-                            <a start show class="py-0 font-weight-normal"><i class="fa fa-pencil-alt"></i></a>
-                            <form url="/api/clientProgram/editSetupInfo">
-                                <input type="hidden" name="uid" value="{{ $clientProgram->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" {{ $clientProgram->is_setup_complete === 'YES' ? 'selected' : '' }}>Yes</option>
-                                        <option value="NO" {{ $clientProgram->is_setup_complete === 'NO' ? 'selected' : '' }}>No</option>
-                                        <option value="UNKNOWN" {{ $clientProgram->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="{{ $clientProgram->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="{{ $clientProgram->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="{{ $clientProgram->device_identifier }}" placeholder="History">
-                                </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>
-                        </span>
+                        <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="mx-2 text-secondary on-hover-opaque">|</span>
-                        <span class="pr-1">Work Spec: <b class="text-secondary">{{ $clientProgram->min_monthly_time_in_minutes }}m - {{ $clientProgram->max_monthly_time_in_minutes }}m</b></span>
-                        <span moe>
-                            <a start show class="py-0 font-weight-normal"><i class="fa fa-pencil-alt"></i></a>
-                            <form url="/api/clientProgram/editWorkSpec">
-                                <input type="hidden" name="uid" value="{{ $clientProgram->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="{{ $clientProgram->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="{{ $clientProgram->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="{{ $clientProgram->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="{{ $clientProgram->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="{{ $clientProgram->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>
-                        </span>
+                        <span class="pr-1">Setup: <b class="text-secondary">{{ $clientProgram->is_setup_complete }}</b></span>
                     </div>
                     </div>
                 </td>
                 </td>
             </tr>
             </tr>

+ 13 - 0
resources/views/app/patient/partials/ticket_action_links.blade.php

@@ -0,0 +1,13 @@
+<a class="mr-2 c-pointer" v-if="!item.is_entry_error" v-on:click.prevent="setIsEntryErrorToTrue(item)">Mark As Entry Error</a>
+<a class="mr-2 c-pointer" v-if="item.is_entry_error" v-on:click.prevent="setIsEntryErrorToFalse(item)">Undo Mark As Entry Error</a>
+<a class="mr-2 c-pointer" v-if="!item.has_assigned_pro_signed" v-on:click.prevent="signAsAssignedPro(item)">Sign As Assigned Pro</a>
+<a class="mr-2 c-pointer" v-if="item.has_assigned_pro_signed" v-on:click.prevent="undoSignAsAssignedPro(item)">Undo Sign As Assigned Pro</a>
+<a class="mr-2 c-pointer" v-if="!item.has_manager_pro_signed" v-on:click.prevent="signAsManagerPro(item)">Sign As Manager Pro</a>
+<a class="mr-2 c-pointer" v-if="item.has_manager_pro_signed" v-on:click.prevent="undoSignAsManagerPro(item)">Undo Sign As Manager Pro</a>
+<a class="mr-2 c-pointer" v-if="!item.has_ordering_pro_signed" v-on:click.prevent="signAsOrderingPro(item)">Sign As Ordering Pro</a>
+<a class="mr-2 c-pointer" v-if="item.has_ordering_pro_signed" v-on:click.prevent="undoSignAsOrderingPro(item)">Undo Sign As Ordering Pro</a>
+<a class="mr-2 c-pointer" v-if="!item.has_initiating_pro_signed" v-on:click.prevent="signAsInitiatingPro(item)">Sign As Initiating Pro</a>
+<a class="mr-2 c-pointer" v-if="item.has_initiating_pro_signed" v-on:click.prevent="undoSignAsInitiatingPro(item)">Undo Sign As Initiating Pro</a>
+<template v-for="proType in proTypes">
+    <a class="mr-2 c-pointer" v-on:click.prevent="showProUpdatePopup('other-popup', proType, item)">Update @{{proType}} Pro</a>
+</template>

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

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

+ 3 - 0
resources/views/app/patient/partials/ticket_vue_data.blade.php

@@ -0,0 +1,3 @@
+proToUpdate: '',
+proTypes: ['Assigned', 'Manager', 'Initiating', 'Ordering'],
+newProUid:''

+ 125 - 0
resources/views/app/patient/partials/ticket_vue_methods.blade.php

@@ -0,0 +1,125 @@
+showProUpdatePopup: function(_name, _proToUpdate, _item) {
+    closeStagPopup();
+    this.proToUpdate = _proToUpdate
+    this.popupItem =JSON.parse(JSON.stringify(_item))
+    let self = this;
+    Vue.nextTick(function() {
+        showStagPopup('{{$ticketType}}-pro-update-popup', true);
+    });
+},
+
+updateTicketPro: function() {
+    let self  = this;
+    let form = $('#{{$ticketType}}App form').first();
+    if(!form[0].checkValidity()) {
+        form[0].reportValidity();
+        return false;
+    }
+
+    showMask();
+    let payload = this.popupItem;
+    payload.newProUid = self.newProUid;
+    
+    $.post(
+        '/api/ticket/update' + self.proToUpdate +'Pro',
+        payload,
+        function(_data) {
+            console.log(_data);
+            fastReload();
+        },
+        'json');
+
+    return false;
+},
+
+
+
+setIsEntryErrorToTrue: function(_item){
+    showMask();
+    $.post('/api/ticket/setIsEntryErrorToTrue', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();     
+    });
+},
+
+setIsEntryErrorToFalse: function(_item){
+    showMask();
+    $.post('/api/ticket/setIsEntryErrorToFalse', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();     
+    })
+},
+
+signAsAssignedPro: function(_item){
+    showMask();
+    $.post('/api/ticket/signAsAssignedPro', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();     
+    })
+},
+
+undoSignAsAssignedPro: function(_item){
+    showMask();
+    $.post('/api/ticket/undoSignAsAssignedPro', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();     
+    })
+},
+
+signAsManagerPro: function(_item){
+    showMask();
+    $.post('/api/ticket/signAsManagerPro', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();     
+    })
+},
+
+undoSignAsManagerPro: function(_item){
+    showMask();
+    $.post('/api/ticket/undoSignAsManagerPro', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();     
+    })
+},
+
+signAsOrderingPro: function(_item){
+    showMask();
+    $.post('/api/ticket/signAsOrderingPro', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();     
+    })
+},
+
+undoSignAsOrderingPro: function(_item){
+    showMask();
+    $.post('/api/ticket/undoSignAsOrderingPro', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();     
+    })
+},
+
+signAsInitiatingPro: function(_item){
+    showMask();
+    $.post('/api/ticket/signAsInitiatingPro', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();     
+    })
+},
+
+undoSignAsInitiatingPro: function(_item){
+    showMask();
+    $.post('/api/ticket/undoSignAsInitiatingPro', {
+        uid:_item.uid
+    }, function(_data){
+        fastReload();     
+    })
+}

+ 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

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

@@ -0,0 +1,803 @@
+<!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>
+    <link rel="stylesheet" href="{{ asset('/css/toastr.min.css') }}">
+    <script src="{{ asset('js/toastr.min.js') }}" type="application/javascript"></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">{{ 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 v-if="appMode === 'name'" class="d-flex">
+            <button v-on:click.prevent="enterClientRoomAsPro()"
+                    class="btn btn-primary px-4 font-weight-bold mx-auto my-3">Start Video</button>
+        </div>
+        <div v-show="appMode === 'video'">
+            <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 class="user-type-indicator">
+                        <i v-if="mainViewParticipant.type === 'CLIENT_GUEST'" class="fa fa-user text-white"></i>
+                        <i v-if="mainViewParticipant.type === 'PRO'" class="fa fa-stethoscope text-white"></i>
+                    </div>
+                    <div class="media-status-indicator">
+                        <i v-show="!getMediaByMediaServiceId(mainViewParticipant.uid).isCameraOn"
+                           class="fa fa-video-slash muted ml-1"
+                           :class="!getMediaByMediaServiceId(mainViewParticipant.uid).isCameraAcquired ? 'text-danger' : 'text-white'"></i>
+                        <i v-show="!getMediaByMediaServiceId(mainViewParticipant.uid).isMicrophoneOn"
+                           class="fa fa-microphone-slash muted ml-1"
+                           :class="!getMediaByMediaServiceId(mainViewParticipant.uid).isMicrophoneAcquired ? 'text-danger' : 'text-white'"></i>
+                    </div>
+                </div>
+                <div class="thumbs">
+                    <div v-if="mainViewParticipant.uid !== myMediaServiceIdentifier"
+                         :id="'remote-view-' + myMediaServiceIdentifier"
+                         :data-self="true"
+                         :data-uid="myMediaServiceIdentifier"
+                         :data-name="'You (' + myName + ')'"
+                         :data-type="'PRO'"   {{-- TODO: change in FE4 --}}
+                         :data-audio="myMedia && myMedia.isMicrophoneOn ? 'on' : 'off'"
+                         v-on:click.prevent="showInCenterView(true, myMediaServiceIdentifier, 'You (' + myName + ')', 'PRO')"
+                         class="remote-view thumb-view c-pointer">
+                        <div class="user-type-indicator">
+                            <i class="fa fa-stethoscope text-white"></i>
+                        </div>
+                        <div class="media-status-indicator">
+                            <i v-show="!myMedia || !myMedia.isCameraOn"
+                               class="fa fa-video-slash muted ml-1"
+                               :class="!myMedia || !myMedia.isCameraAcquired ? 'text-danger' : 'text-white'"></i>
+                            <i v-show="!myMedia || !myMedia.isMicrophoneOn"
+                               class="fa fa-microphone-slash muted ml-1"
+                               :class="!myMedia || !myMedia.isMicrophoneAcquired ? 'text-danger' : 'text-white'"></i>
+                        </div>
+                    </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">
+                        <div class="user-type-indicator">
+                            <i v-if="participant.participantType === 'CLIENT_GUEST'" class="fa fa-user text-white"></i>
+                            <i v-if="participant.participantType === 'PRO'" class="fa fa-stethoscope text-white"></i>
+                        </div>
+                        <div class="media-status-indicator">
+                            <i v-show="!participant.media || !participant.media.isCameraOn"
+                               class="fa fa-video-slash muted ml-1"
+                               :class="!participant.media || !participant.media.isCameraAcquired ? 'text-danger' : 'text-white'"></i>
+                            <i v-show="!participant.media || !participant.media.isMicrophoneOn"
+                               class="fa fa-microphone-slash muted ml-1"
+                               :class="!participant.media || !participant.media.isMicrophoneAcquired ? 'text-danger' : 'text-white'"></i>
+                        </div>
+                    </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>
+    <script>
+        (function () {
+            window.proCallComponent = new Vue({
+                el: '#proCallComponent',
+                delimiters: ['@{{', '}}'],
+                data: {
+
+                    // main 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: '',
+                    myName: '{{ $performer->pro->displayName() }}',
+
+                    // agora
+                    mediaServiceClient: null, // set on agora init
+                    appId: '{{ config('app.agora_appid') }}',
+                    channel: '', // set on mount
+                    myMicrophone: null,
+                    myCamera: null,
+                    appMode: 'name', // Agora needs a user-gesture to init correctly
+
+                    // 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 participant
+                    mainViewParticipant: {
+                        self: true,
+                        uid: '',
+                        type: 'PRO',
+                        name: 'You ({{ $performer->pro->displayName() }})',
+                    },
+                },
+                methods: {
+
+                    // start: main flow
+                    enterClientRoomAsPro: function() {
+                        @if($client)
+                            this.socketClient.send("/app/leaveClientRoom", {},
+                                JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                            );
+                            window.setTimeout(() => {
+                                $.post('/api/meeting/enterClientRoomAsPro', {clientUid: '{{ $client->uid }}'}, (_data) => {
+                                    if(!this.hasError(_data)) {
+                                        this.appMode = 'video';
+                                        this.getMeetingInfo(true);
+                                    }
+                                });
+                            }, 250);
+                        @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.initMediaService();
+                                        }
+                                    }, '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}}'
+                                })
+                            );
+                            window.setInterval(() => {
+                                this.socketClient.send("/app/heartbeat", {},
+                                    JSON.stringify({sessionKey: '{{ request()->cookie('sessionKey') }}'})
+                                );
+                            }, 5000);
+                        });
+                    },
+                    initSocketListeners: function() {
+
+                        function _isSelf(_eventData) {
+                            return _eventData.performer === '{{ $session->uid  }}';
+                        }
+
+                        function _setParticipantProperty(_message, _propertyName, _valueKeyName) {
+                            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][_propertyName] = eventData.data[_valueKeyName];
+                                            break;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+
+                        function _setParticipantMediaProperty(_message, _propertyName, _value) {
+                            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[_propertyName] = _value;
+                                            break;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+
+                        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);
+                                    Vue.nextTick(() => {
+                                        this.refreshVideos();
+                                    });
+                                }
+                            }
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myMicrophoneIsAcquired", (message) => {
+                            console.log("myMicrophoneIsAcquired received:", message.body);
+                            _setParticipantMediaProperty.call(this, message, 'isMicrophoneAcquired', true);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myMicrophoneIsNotAcquired", (message) => {
+                            console.log("myMicrophoneIsNotAcquired received:", message.body);
+                            _setParticipantMediaProperty.call(this, message, 'isMicrophoneAcquired', false);
+                            _setParticipantMediaProperty.call(this, message, 'isMicrophoneOn', false);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myMicrophoneIsOn", (message) => {
+                            console.log("myMicrophoneIsOn received:", message.body);
+                            _setParticipantMediaProperty.call(this, message, 'isMicrophoneOn', true);
+                            _setParticipantMediaProperty.call(this, message, 'isMicrophoneAcquired', true);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myMicrophoneIsOff", (message) => {
+                            console.log("ALIX myMicrophoneIsOff received:", message.body);
+                            _setParticipantMediaProperty.call(this, message, 'isMicrophoneOn', false);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myCameraIsAcquired", (message) => {
+                            console.log("myCameraIsAcquired received:", message.body);
+                            _setParticipantMediaProperty.call(this, message, 'isCameraAcquired', true);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myCameraIsNotAcquired", (message) => {
+                            console.log("myCameraIsNotAcquired received:", message.body);
+                            _setParticipantMediaProperty.call(this, message, 'isCameraAcquired', false);
+                            _setParticipantMediaProperty.call(this, message, 'isCameraOn', false);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myCameraIsOn", (message) => {
+                            console.log("myCameraIsOn received:", message.body);
+                            _setParticipantMediaProperty.call(this, message, 'isCameraOn', true);
+                            _setParticipantMediaProperty.call(this, message, 'isCameraAcquired', true);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/myCameraIsOff", (message) => {
+                            console.log("myCameraIsOff received:", message.body);
+                            _setParticipantMediaProperty.call(this, message, 'isCameraOn', false);
+                        });
+
+                        this.socketClient.subscribe("/user/topic/editMyName", (message) => {
+                            console.log("editMyName received:", message.body);
+                            _setParticipantProperty.call(this, message, 'displayName', 'myName');
+                        });
+
+                        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 (' + this.myName + ')', 'PRO');
+                                                break;
+                                            }
+                                        }
+                                    }
+
+                                    this.otherParticipants = this.otherParticipants.filter(_participant => {
+                                         return _participant.uid !== eventData.performer;
+                                    });
+
+                                    Vue.nextTick(() => {
+                                        this.refreshVideos();
+                                    });
+                                }
+                            }
+                        });
+
+                    },
+                    initMediaService: function() {
+
+                        this.mediaServiceClient = AgoraRTC.createClient({mode:'rtc', codec:'vp8'});
+
+                        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({
+                                        optimizationMode: "motion"
+                                    });
+                                @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) => {
+                                console.log('ALIX user-published', user);
+                                await this.mediaServiceClient.subscribe(user, mediaType)
+                                this.attemptToPlayParticipantMedia(user, mediaType);
+                                window.setTimeout(() => {
+                                    this.refreshVideos();
+                                }, 250);
+                            });
+                        }
+                        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; // allow to proceed without any device!
+                            }
+
+                            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
+                            );
+
+                            if(this.myMicrophone || this.myCamera) {
+                                await this.mediaServiceClient.publish(      // publish audio/video
+                                    [this.myMicrophone, this.myCamera].filter(Boolean)
+                                );
+                            }
+
+                            // notify others about my camera status
+                            if(this.myCamera && this.myMedia.isCameraAcquired) {
+                                this.myCameraIsAcquired();
+                                this.myCameraIsOn();
+                            }
+                            else {
+                                this.myCameraIsNotAcquired();
+                                this.myCameraIsOff();
+                            }
+
+                            // notify others about my microphone status
+                            if(this.myMicrophone && this.myMedia.isMicrophoneAcquired) {
+                                this.myMicrophoneIsAcquired();
+                                this.myMicrophoneIsOn();
+                            }
+                            else {
+                                this.myMicrophoneIsNotAcquired();
+                                this.myMicrophoneIsOff();
+                            }
+
+                        }
+
+                        _initMediaService.call(this);
+                    },
+                    attemptToPlayParticipantMedia: function(user, mediaType) {
+                        //  LOGIC
+                        //  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
+                        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' && user.hasAudio && user.audioTrack) {
+                                    participant.media.isMicrophoneAcquired = true;
+                                    participant.media.isMicrophoneOn = true;
+                                    user.audioTrack.play();
+                                }
+                                else if(mediaType === 'video' && user.hasVideo && user.videoTrack) {
+                                    participant.media.isCameraAcquired = true;
+                                    participant.media.isCameraOn = true;
+                                    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);
+                        }
+                    },
+                    // end: main flow
+
+                    // 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.myMicrophone && this.myMedia.isMicrophoneAcquired) {
+                            this.myMedia.isMicrophoneOn = true;
+                            this.myMicrophone.setEnabled(true);
+                            this.socketClient.send("/app/myMicrophoneIsOn", {},
+                                JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                            );
+                        }
+                    },
+                    myMicrophoneIsOff: function () {
+                        if(this.myMicrophone) {
+                            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.myCamera && this.myMedia.isCameraAcquired) {
+                            this.myCamera.setEnabled(true);
+                            this.myMedia.isCameraOn = true;
+                            this.socketClient.send("/app/myCameraIsOn", {},
+                                JSON.stringify({sessionKey: '{{$performer->session_key}}'})
+                            );
+                        }
+                    },
+                    myCameraIsOff: function () {
+                        if(this.myCamera) {
+                            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') {
+                                    let videoContainer = $('[data-uid="' + this.myMediaServiceIdentifier + '"]');
+                                    if(videoContainer.length) {
+                                        track.play(videoContainer[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) {
+                                    let videoContainer = $('[data-uid="' + remoteParticipant.uid + '"]');
+                                    if(videoContainer.length) {
+                                        remoteParticipant.videoTrack.play(videoContainer[0], {fit: 'contain'});
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    // end: main view / thumb views
+
+                    // start: other/misc
+                    getMediaByMediaServiceId: function(_msid) {
+                        if((+this.myMediaServiceIdentifier) === _msid) {               // is it self?
+                            return this.myMedia;
+                        }
+                        for (let i = 0; i < this.otherParticipants.length; i++) {   // or a remote participant
+                            if((+this.otherParticipants[i].mediaServiceIdentifier) === _msid) {
+                                return this.otherParticipants[i].media;
+                            }
+                        }
+                        return {    // return falsy object if nothing found
+                            isCameraAcquired: false,
+                            isCameraOn: false,
+                            isMicrophoneAcquired: false,
+                            isMicrophoneOn: false,
+                        };
+                    },
+                    toggleRinger: function () {
+                        $.post('/api/pro/' + (this.ringer ? 'turnOffRing' : 'turnOnRing'), (_data) => {
+                            if(!this.hasError(_data)) {
+                                this.ringer = !this.ringer;
+                            }
+                        }, 'json');
+                    },
+                    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: other/misc
+                },
+                mounted: function () {
+                    this.registerSocket();
+                }
+            });
+        })();
+    </script>
+
+    @include('app/patient/partials/mcp-queue')
+
+</body>
+</html>

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

@@ -17,6 +17,10 @@
                             <a class="nav-link {{ strpos($routeName, 'patients.view.calendar') === 0 ? 'active' : '' }}"
                             <a class="nav-link {{ strpos($routeName, 'patients.view.calendar') === 0 ? 'active' : '' }}"
                                href="{{ route('patients.view.calendar', ['patient' => $patient]) }}">Calendar</a>
                                href="{{ route('patients.view.calendar', ['patient' => $patient]) }}">Calendar</a>
                         </li>
                         </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">
                         <li class="nav-item">
                             <a class="nav-link {{ strpos($routeName, 'patients.view.devices') === 0 ? 'active' : '' }}"
                             <a class="nav-link {{ strpos($routeName, 'patients.view.devices') === 0 ? 'active' : '' }}"
                                href="{{ route('patients.view.devices', ['patient' => $patient]) }}">Devices</a>
                                href="{{ route('patients.view.devices', ['patient' => $patient]) }}">Devices</a>
@@ -589,7 +593,6 @@
                     </div>
                     </div>
                     {{--<div class="text-container border-bottom d-flex align-items-center mcp-theme-1 px-3">
                     {{--<div class="text-container border-bottom d-flex align-items-center mcp-theme-1 px-3">
 
 
-
                         @if($patient->mcp_pro_id !== $pro->id && $patient->active_mcp_request_id)
                         @if($patient->mcp_pro_id !== $pro->id && $patient->active_mcp_request_id)
                             <div moe relative class="ml-2">
                             <div moe relative class="ml-2">
                                 <a href="" start show class="btn btn-sm btn-success text-white font-weight-bold small">Claim as MCP</a>
                                 <a href="" start show class="btn btn-sm btn-success text-white font-weight-bold small">Claim as MCP</a>

+ 27 - 23
resources/views/layouts/template.blade.php

@@ -61,8 +61,13 @@
     {{-- pdfjs --}}
     {{-- pdfjs --}}
     <script src="https://mozilla.github.io/pdf.js/build/pdf.js"></script>
     <script src="https://mozilla.github.io/pdf.js/build/pdf.js"></script>
 
 
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.0/sockjs.js"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
+    {{-- WebSockets --}}
+    <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>
 
 
     @yield('head')
     @yield('head')
 </head>
 </head>
@@ -268,39 +273,38 @@
                 fastLoad('/patients/view/' + $(this).attr('data-target-uid'), true, false, false);
                 fastLoad('/patients/view/' + $(this).attr('data-target-uid'), true, false, false);
                 return false;
                 return false;
             });
             });
-
-            window.setInterval(function() {
-                $.get('/current-work', function(_data) {
-                    $('.current-work').html(_data);
-                    initFastLoad($('.current-work'));
-                });
-            }, 15000); // once in 15 seconds
         });
         });
     </script>
     </script>
     <script src="/js/stag-popup.js"></script>
     <script src="/js/stag-popup.js"></script>
     <script src="/js/option-list.js"></script>
     <script src="/js/option-list.js"></script>
     @include('app/pdf/viewer')
     @include('app/pdf/viewer')
-
     <script>
     <script>
-        // connect to WS
-        self.socket = new SockJS('http://localhost:8080/ws');
-        self.socketClient = Stomp.over(self.socket);
-        self.socketClient.connect({}, function(frame) {
-            console.log('Connected: ' + frame);
-
-            self.socketClient.subscribe("/user/topic/registration", function(message) {
-                console.log("Receiving message")
-                console.log("registration result:", message.body);
-            });
+        let socket = new SockJS("{{ config('app.backend_ws_url') }}");
+        window.socketClient = Stomp.over(socket);
+        window.socketClient.connect({}, (frame) => {
 
 
-            // join self
-            console.log("Sending message")
-            self.socketClient.send("/app/register", {},
+            window.socketClient.send("/app/register", {},     // register self
                 JSON.stringify({
                 JSON.stringify({
                     sessionKey: '{{$performer->session_key}}'
                     sessionKey: '{{$performer->session_key}}'
                 })
                 })
             );
             );
 
 
+            window.socketClient.subscribe("/user/topic/newMcpRequest", function(message) {
+                console.log("Received new mcp request: ", message);
+            }); // once in 15 seconds
+
+            window.socketClient.subscribe("/user/topic/removeMcpRequest", function(message) {
+                console.log("An mcp request has been removed: ", message);
+            }); 
+
+            window.socketClient.subscribe("/user/topic/myCurrentProClientWork", function(message) {
+                console.log("You have a client pro work: ", message);
+                $.get('/current-work', function(_data) {
+                    $('.current-work').html(_data);
+                    initFastLoad($('.current-work'));
+                });
+            });   
+
         });
         });
     </script>
     </script>
 </body>
 </body>

+ 2 - 0
routes/web.php

@@ -123,6 +123,8 @@ Route::middleware('pro.auth')->group(function () {
         // appointment calendar
         // appointment calendar
         Route::get('calendar/{currentAppointment?}', 'PatientController@calendar')->name('calendar');
         Route::get('calendar/{currentAppointment?}', 'PatientController@calendar')->name('calendar');
 
 
+        // programs
+        Route::get('programs/{filter?}', 'PatientController@programs')->name('programs');
     });
     });
 
 
     // pro dashboard events (ajax)
     // pro dashboard events (ajax)