Jelajahi Sumber

Availability calendar [very much wip]

Vijayakrishnan 4 tahun lalu
induk
melakukan
4bc65db777

+ 117 - 7
app/Http/Controllers/AppointmentController.php

@@ -10,6 +10,9 @@ use App\Models\ClientInfoLine;
 use App\Models\Facility;
 use App\Models\NoteTemplate;
 use App\Models\Pro;
+use App\Models\ProGeneralAvailability;
+use App\Models\ProSpecificAvailability;
+use App\Models\ProSpecificUnavailability;
 use App\Models\SectionTemplate;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\File;
@@ -23,6 +26,8 @@ class AppointmentController extends Controller
         $end = $request->get('end');
         $clientId = $request->get('clientId');
         $timeZone = $request->get('timeZone');
+
+        // get appointments
         $appointments = Appointment
             ::where('status', '!=', 'COMPLETED')
             ->where('status', '!=', 'CANCELLED')
@@ -37,6 +42,7 @@ class AppointmentController extends Controller
         $events = [];
         foreach ($appointments as $appointment) {
             $events[] = [
+                "type" => "appointment",
                 "title" => ($appointment->client->id != $clientId ? '* ' : '') . $appointment->pro->displayName(),
                 "_title" => $appointment->title,
                 "description" => $appointment->description,
@@ -52,13 +58,120 @@ class AppointmentController extends Controller
                 "editable" => true
             ];
         }
+
+        // get availability
+        $genAvail = ProGeneralAvailability
+            ::where('is_cancelled', false)
+            ->whereIn('pro_id', $proIds)
+            ->get();
+        $specAvail = ProSpecificAvailability
+            ::where('is_cancelled', false)
+            ->whereIn('pro_id', $proIds)
+            ->where(function ($query) use ($start, $end) {
+                $query
+                    ->where(function ($query2) use ($start, $end) {
+                        $query2
+                            ->where('start_time', '>=', $start)
+                            ->where('start_time', '<=', $end);
+                    })
+                    ->orWhere(function ($query2) use ($start, $end) {
+                        $query2
+                            ->where('end_time', '>=', $start)
+                            ->where('end_time', '<=', $end);
+                    });
+            })
+            ->get();
+        $specUnavail = ProSpecificUnavailability
+            ::where('is_cancelled', false)
+            ->whereIn('pro_id', $proIds)
+            ->where(function ($query) use ($start, $end) {
+                $query
+                    ->where(function ($query2) use ($start, $end) {
+                        $query2
+                            ->where('start_time', '>=', $start)
+                            ->where('start_time', '<=', $end);
+                    })
+                    ->orWhere(function ($query2) use ($start, $end) {
+                        $query2
+                            ->where('end_time', '>=', $start)
+                            ->where('end_time', '<=', $end);
+                    });
+            })
+            ->get();
+
+        // logic
+        // 1. enumerate days between start and end (inclusive)
+            // 2. for each pro
+                // 3. calculate pairs of start-end of availability
+
+        // 1. enumerate days between start and end (inclusive)
+        $phpTZ = $this->appTZtoPHPTZ($timeZone);
+        $startDate = new \DateTime($start, new \DateTimeZone($phpTZ));
+        $endDate = new \DateTime($end, new \DateTimeZone($phpTZ));
+        $period = new \DatePeriod($startDate, \DateInterval::createFromDateString('1 day'), $endDate);
+        $days = [];
+        foreach ($period as $day) {
+            $days[] = [
+                "day" => strtoupper($day->format("l")), // SUNDAY, etc.
+                "date" => $day->format("Y-m-d"), // 2020-10-04, etc.
+            ];
+        }
+
+        foreach ($proIds as $proId) {
+
+            $pro = Pro::where('id', $proId)->first();
+
+            $proGenAvail = $genAvail->filter(function ($record) use ($proId) {
+                return $record->pro_id == $proId;
+            });
+
+            foreach ($days as $day) {
+
+                $proGenAvailForTheDay = $proGenAvail->filter(function ($record) use ($day) {
+                    return $record->day_of_week === $day["day"];
+                });
+
+                if(count($proGenAvailForTheDay)) {
+                    foreach ($proGenAvailForTheDay as $ga) {
+
+                        $gaStart = new \DateTime($day["date"], new \DateTimeZone($phpTZ));
+                        $parts = explode(":", $ga->start_time);
+                        $gaStart->setTime(intval($parts[0]), intval($parts[1]), intval($parts[2]));
+
+                        $gaEnd = new \DateTime($day["date"], new \DateTimeZone($phpTZ));
+                        $parts = explode(":", $ga->end_time);
+                        $gaEnd->setTime(intval($parts[0]), intval($parts[1]), intval($parts[2]));
+
+                        $events[] = [
+                            "type" => "availability",
+                            "title" => $pro->displayName(),
+                            "proId" => $pro->id,
+                            "proUid" => $pro->uid,
+                            "start" => $gaStart->format('Y-m-d H:i:s'),
+                            "end" => $gaEnd->format('Y-m-d H:i:s'),
+                            "editable" => false
+                        ];
+                    }
+                }
+
+                // TODO: add spec avail
+                // TODO: subtract spec unavail
+
+            }
+        }
+
         return json_encode($events);
     }
 
-    private function convertToTimezone($_dateTime, $_targetTimezone) {
+    private function convertToTimezone($_dateTime, $_targetTimezone, $_sourceTimezone = 'UTC', $_returnRaw = false) {
         if(!$_dateTime) return $_dateTime;
-        $timezone = 'US/Eastern';
-        switch($_targetTimezone) {
+        $date = new \DateTime($_dateTime, new \DateTimeZone($_sourceTimezone));
+        $date->setTimezone(new \DateTimeZone($this->appTZtoPHPTZ($_targetTimezone)));
+        return $_returnRaw ? $date : $date->format('Y-m-d H:i:s');
+    }
+
+    private function appTZtoPHPTZ($_timezone) {
+        switch($_timezone) {
             case 'ALASKA':
                 $timezone = "US/Alaska";
                 break;
@@ -81,9 +194,6 @@ class AppointmentController extends Controller
                 $timezone = "US/Eastern";
                 break;
         }
-
-        $date = new \DateTime($_dateTime, new \DateTimeZone("UTC"));
-        $date->setTimezone(new \DateTimeZone($timezone));
-        return $date->format('Y-m-d H:i:s');
+        return $timezone;
     }
 }

+ 4 - 0
public/css/style.css

@@ -945,3 +945,7 @@ span.select2-container.select2-container--default.select2-container--open {
 .fc .other-client:hover {
     opacity: 0.8;
 }
+.w-150 {
+    width: 150px !important;
+    min-width: 150px !important;
+}

+ 88 - 36
resources/views/app/patient/appointment-calendar.blade.php

@@ -2,35 +2,56 @@
 
 @section('inner-content')
 
+
+
     <?php
+
+    function adjustBrightness($hex, $steps) {
+        $steps = max(-255, min(255, $steps));
+        $hex = str_replace('#', '', $hex);
+        if (strlen($hex) == 3) {
+            $hex = str_repeat(substr($hex,0,1), 2).str_repeat(substr($hex,1,1), 2).str_repeat(substr($hex,2,1), 2);
+        }
+        $color_parts = str_split($hex, 2);
+        $return = '#';
+        foreach ($color_parts as $color) {
+            $color   = hexdec($color); // Convert to decimal
+            $color   = max(0,min(255,$color + $steps)); // Adjust color
+            $return .= str_pad(dechex($color), 2, '0', STR_PAD_LEFT); // Make two char hex code
+        }
+        return $return;
+    }
+
     $palette = [
-        ["bc" => '#522e92', "fc" => "#fff"],
-        ["bc" => '#003152', "fc" => "#fff"],
-        ["bc" => '#111e6c', "fc" => "#fff"],
-        ["bc" => '#1034a6', "fc" => "#fff"],
-        ["bc" => '#0f52ba', "fc" => "#fff"],
-        ["bc" => '#447684', "fc" => "#fff"],
-        ["bc" => '#d86700', "fc" => "#fff"],
-        ["bc" => '#643c07', "fc" => "#fff"],
-        ["bc" => '#ff3f3f', "fc" => "#fff"],
-        ["bc" => '#ffa395', "fc" => "#222"],
-        ["bc" => '#6450ff', "fc" => "#fff"],
-        ["bc" => '#8ec7f4', "fc" => "#222"],
-        ["bc" => '#522e92', "fc" => "#fff"],
-        ["bc" => '#111e6c', "fc" => "#fff"],
-        ["bc" => '#003152', "fc" => "#fff"],
-        ["bc" => '#1034a6', "fc" => "#fff"],
-        ["bc" => '#0f52ba', "fc" => "#fff"],
-        ["bc" => '#447684', "fc" => "#fff"],
-        ["bc" => '#d86700', "fc" => "#fff"],
-        ["bc" => '#643c07', "fc" => "#fff"],
-        ["bc" => '#ff3f3f', "fc" => "#fff"],
-        ["bc" => '#ffa395', "fc" => "#222"],
-        ["bc" => '#6450ff', "fc" => "#fff"],
-        ["bc" => '#8ec7f4', "fc" => "#222"],
+        ["bc" => '#522e92', "fc" => "#ffffff", "ac" => adjustBrightness('#522e92', 180) . 'cc'],
+        ["bc" => '#003152', "fc" => "#ffffff", "ac" => adjustBrightness('#003152', 180) . 'cc'],
+        ["bc" => '#111e6c', "fc" => "#ffffff", "ac" => adjustBrightness('#111e6c', 180) . 'cc'],
+        ["bc" => '#1034a6', "fc" => "#ffffff", "ac" => adjustBrightness('#1034a6', 180) . 'cc'],
+        ["bc" => '#0f52ba', "fc" => "#ffffff", "ac" => adjustBrightness('#0f52ba', 180) . 'cc'],
+        ["bc" => '#447684', "fc" => "#ffffff", "ac" => adjustBrightness('#447684', 180) . 'cc'],
+        ["bc" => '#d86700', "fc" => "#ffffff", "ac" => adjustBrightness('#d86700', 180) . 'cc'],
+        ["bc" => '#643c07', "fc" => "#ffffff", "ac" => adjustBrightness('#643c07', 180) . 'cc'],
+        ["bc" => '#ff3f3f', "fc" => "#ffffff", "ac" => adjustBrightness('#ff3f3f', 180) . 'cc'],
+        ["bc" => '#ffa395', "fc" => "#222222", "ac" => adjustBrightness('#ffa395', 180) . 'cc'],
+        ["bc" => '#6450ff', "fc" => "#ffffff", "ac" => adjustBrightness('#6450ff', 180) . 'cc'],
+        ["bc" => '#8ec7f4', "fc" => "#222222", "ac" => adjustBrightness('#8ec7f4', 180) . 'cc'],
+        ["bc" => '#522e92', "fc" => "#ffffff", "ac" => adjustBrightness('#522e92', 180) . 'cc'],
+        ["bc" => '#111e6c', "fc" => "#ffffff", "ac" => adjustBrightness('#111e6c', 180) . 'cc'],
+        ["bc" => '#003152', "fc" => "#ffffff", "ac" => adjustBrightness('#003152', 180) . 'cc'],
+        ["bc" => '#1034a6', "fc" => "#ffffff", "ac" => adjustBrightness('#1034a6', 180) . 'cc'],
+        ["bc" => '#0f52ba', "fc" => "#ffffff", "ac" => adjustBrightness('#0f52ba', 180) . 'cc'],
+        ["bc" => '#447684', "fc" => "#ffffff", "ac" => adjustBrightness('#447684', 180) . 'cc'],
+        ["bc" => '#d86700', "fc" => "#ffffff", "ac" => adjustBrightness('#d86700', 180) . 'cc'],
+        ["bc" => '#643c07', "fc" => "#ffffff", "ac" => adjustBrightness('#643c07', 180) . 'cc'],
+        ["bc" => '#ff3f3f', "fc" => "#ffffff", "ac" => adjustBrightness('#ff3f3f', 180) . 'cc'],
+        ["bc" => '#ffa395', "fc" => "#222222", "ac" => adjustBrightness('#ffa395', 180) . 'cc'],
+        ["bc" => '#6450ff', "fc" => "#ffffff", "ac" => adjustBrightness('#6450ff', 180) . 'cc'],
+        ["bc" => '#8ec7f4', "fc" => "#222222", "ac" => adjustBrightness('#8ec7f4', 180) . 'cc'],
     ];
     ?>
 
+
+
     <link href="/select2/select2.min.css" rel="stylesheet" />
     <script src="/select2/select2.min.js"></script>
     <link href='/fullcalendar-5.3.2/lib/main.css' rel='stylesheet' />
@@ -41,10 +62,21 @@
             <h4 class="font-weight-bold m-0 font-size-16">
                 {{ $patient->displayName() }}'s Calendar
             </h4>
-            <div class="ml-auto">
+            <div class="ml-auto d-inline-flex align-items-center">
+                <label class="mr-2 my-0 text-secondary">Show</label>
+                <select id="eventTypes"
+                        class="form-control form-control-sm w-150"
+                        v-model="eventTypes"
+                        v-on:change="refreshEvents()">
+                    <option value="APPOINTMENTS" selected>Appointments</option>
+                    <option value="PRO_AVAILABILITY">Pro Availability</option>
+                    <option value="BOTH">Both</option>
+                </select>
+            </div>
+            <div class="ml-3 d-inline-flex align-items-center">
                 <label class="mr-2 my-0 text-secondary">Timezone</label>
                 <select id="eventTz" name="timeZone"
-                        class="form-control form-control-sm"
+                        class="form-control form-control-sm w-150"
                         v-model="timezone">
                     <option value="EASTERN" selected>Eastern</option>
                     <option value="CENTRAL">Central</option>
@@ -77,6 +109,7 @@
                         $proMeta[$iPro->uid] = [
                             "bc" => $palette[$proIndex]["bc"],
                             "fc" => $palette[$proIndex]["fc"],
+                            "ac" => $palette[$proIndex]["ac"],
                             "initials" => $iPro->initials()
                         ];
                         $proIndex++;
@@ -313,7 +346,7 @@
                     el: '#calendarApp',
                     data: {
                         client: {!! json_encode($patient) !!},
-
+                        eventTypes: 'APPOINTMENTS',
                         calendar: null,
                         proMeta: {!! json_encode($proMeta) !!},
                         proIds: ['{{ $pro->id }}'],
@@ -348,6 +381,9 @@
                             description: '',
                         },
 
+                        // availability
+                        availability: {},
+
                         inProgress: false
                     },
                     methods: {
@@ -458,23 +494,39 @@
                                         '&start=' + info.startStr.substr(0, 10) +
                                         '&end=' + info.endStr.substr(0, 10) +
                                         '&timeZone=' + self.timezone, function(_data) {
-                                        // $.get('/api/appointment/getAllAppointmentsForPro?start=1990-01-01&end=2025-01-01&timeZone=CENTRAL', function(_data) {
                                         if(_data && Array.isArray(_data)) {
-                                            let events = _data;
+                                            let events = _data, displayEvents = [];
                                             for(let e in events) {
                                                 if(events.hasOwnProperty(e) && self.proMeta[events[e].proUid]) {
                                                     let ev = events[e], meta = self.proMeta[ev.proUid];
-                                                    ev.backgroundColor =  meta.bc;
-                                                    ev.borderColor = meta.bc;
-                                                    ev.textColor = meta.fc;
-                                                    ev.initials = meta.initials;
-                                                    ev.display = 'block';
+                                                    if(ev.type === 'appointment') {
+                                                        if(self.eventTypes === 'APPOINTMENTS' || self.eventTypes === 'BOTH') {
+                                                            ev.backgroundColor =  meta.bc;
+                                                            ev.borderColor = meta.bc;
+                                                            ev.textColor = meta.fc;
+                                                            ev.initials = meta.initials;
+                                                            ev.display = 'block';
+                                                            ev.editable = true;
+                                                            displayEvents.push(ev);
+                                                        }
+                                                    }
+                                                    else {
+                                                        if(self.eventTypes === 'PRO_AVAILABILITY' || self.eventTypes === 'BOTH') {
+                                                            ev.backgroundColor = meta.ac;
+                                                            ev.borderColor = meta.bc;
+                                                            ev.textColor = meta.bc;
+                                                            ev.initials = meta.initials;
+                                                            ev.display = 'block';
+                                                            ev.editable = false;
+                                                            displayEvents.push(ev);
+                                                        }
+                                                    }
                                                 }
                                             }
-                                            successCallback(events);
+                                            successCallback(displayEvents);
                                         }
                                         else {
-                                            failureCallback(_data);
+                                            failureCallback('Unable to refresh appointments!');
                                         }
                                     }, 'json');
                                 },