Explorar o código

Practice > calendar

Vijayakrishnan %!s(int64=4) %!d(string=hai) anos
pai
achega
15529da99a

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

@@ -52,10 +52,26 @@ class AppointmentController extends Controller
 
         $events = [];
         foreach ($appointments as $appointment) {
+
+            // default - as used from patient calendar
+            $title = ($appointment->client->id != $clientId ? '* ' : '') . $appointment->pro->displayName() .
+                " (" . strtolower($appointment->status) . ")";
+
+            if($clientId == "-1") {
+                if(count($proIds) === 1) {
+                    $title = $appointment->client->displayName() .
+                    " (" . strtolower($appointment->status) . ")";
+                }
+                else {
+                    $title = '[' . $appointment->pro->initials()  . '] ' .
+                        $appointment->client->displayName() .
+                        " (" . strtolower($appointment->status) . ")";
+                }
+            }
+
             $events[] = [
                 "type" => "appointment",
-                "title" => ($appointment->client->id != $clientId ? '* ' : '') . $appointment->pro->displayName() .
-                    " (" . strtolower($appointment->status) . ")",
+                "title" => $title,
                 "_title" => $appointment->title,
                 "description" => $appointment->description,
                 "clientName" => $appointment->client->displayName(),

+ 4 - 0
app/Http/Controllers/PracticeManagementController.php

@@ -270,4 +270,8 @@ class PracticeManagementController extends Controller
     public function currentWork(Request $request) {
         return view('app/current-work');
     }
+
+    public function calendar(Request $request, $proUid = null) {
+        return view('app.practice-management.calendar');
+    }
 }

+ 813 - 0
resources/views/app/practice-management/calendar.blade.php

@@ -0,0 +1,813 @@
+@extends ('layouts.template')
+
+@section('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" => "#ffffff", "ac" => adjustBrightness('#522e92', 180) . 'aa'],
+        ["bc" => '#003152', "fc" => "#ffffff", "ac" => adjustBrightness('#003152', 180) . 'aa'],
+        ["bc" => '#111e6c', "fc" => "#ffffff", "ac" => adjustBrightness('#111e6c', 180) . 'aa'],
+        ["bc" => '#1034a6', "fc" => "#ffffff", "ac" => adjustBrightness('#1034a6', 180) . 'aa'],
+        ["bc" => '#0f52ba', "fc" => "#ffffff", "ac" => adjustBrightness('#0f52ba', 180) . 'aa'],
+        ["bc" => '#447684', "fc" => "#ffffff", "ac" => adjustBrightness('#447684', 180) . 'aa'],
+        ["bc" => '#d86700', "fc" => "#ffffff", "ac" => adjustBrightness('#d86700', 180) . 'aa'],
+        ["bc" => '#643c07', "fc" => "#ffffff", "ac" => adjustBrightness('#643c07', 180) . 'aa'],
+        ["bc" => '#ff3f3f', "fc" => "#ffffff", "ac" => adjustBrightness('#ff3f3f', 180) . 'aa'],
+        ["bc" => '#ffa395', "fc" => "#222222", "ac" => adjustBrightness('#ffa395', 180) . 'aa'],
+        ["bc" => '#6450ff', "fc" => "#ffffff", "ac" => adjustBrightness('#6450ff', 180) . 'aa'],
+        ["bc" => '#8ec7f4', "fc" => "#222222", "ac" => adjustBrightness('#8ec7f4', 180) . 'aa'],
+        ["bc" => '#522e92', "fc" => "#ffffff", "ac" => adjustBrightness('#522e92', 180) . 'aa'],
+        ["bc" => '#111e6c', "fc" => "#ffffff", "ac" => adjustBrightness('#111e6c', 180) . 'aa'],
+        ["bc" => '#003152', "fc" => "#ffffff", "ac" => adjustBrightness('#003152', 180) . 'aa'],
+        ["bc" => '#1034a6', "fc" => "#ffffff", "ac" => adjustBrightness('#1034a6', 180) . 'aa'],
+        ["bc" => '#0f52ba', "fc" => "#ffffff", "ac" => adjustBrightness('#0f52ba', 180) . 'aa'],
+        ["bc" => '#447684', "fc" => "#ffffff", "ac" => adjustBrightness('#447684', 180) . 'aa'],
+        ["bc" => '#d86700', "fc" => "#ffffff", "ac" => adjustBrightness('#d86700', 180) . 'aa'],
+        ["bc" => '#643c07', "fc" => "#ffffff", "ac" => adjustBrightness('#643c07', 180) . 'aa'],
+        ["bc" => '#ff3f3f', "fc" => "#ffffff", "ac" => adjustBrightness('#ff3f3f', 180) . 'aa'],
+        ["bc" => '#ffa395', "fc" => "#222222", "ac" => adjustBrightness('#ffa395', 180) . 'aa'],
+        ["bc" => '#6450ff', "fc" => "#ffffff", "ac" => adjustBrightness('#6450ff', 180) . 'aa'],
+        ["bc" => '#8ec7f4', "fc" => "#222222", "ac" => adjustBrightness('#8ec7f4', 180) . 'aa'],
+    ];
+    ?>
+
+
+
+    <link href="/select2/select2.min.css" rel="stylesheet" />
+    <script src="/select2/select2.min.js"></script>
+    <link href='/fullcalendar-5.3.2/lib/main.css' rel='stylesheet' />
+    <script src='/fullcalendar-5.3.2/lib/main.js'></script>
+
+    <div id="proCalendarApp" class="mcp-theme-1 p-3">
+
+        <div class="d-flex align-items-center mb-2">
+            <h4 class="font-weight-bold m-0 font-size-16">
+                @if($pro->pro_type == 'ADMIN')
+                    Admin Calendar
+                @else
+                    {{ $pro->displayName() }}'s Calendar
+                @endif
+            </h4>
+            <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-180"
+                        v-model="eventTypes"
+                        v-on:change="refreshEvents()">
+                    <option value="APPOINTMENTS">Active Appointments</option>
+                    <option value="APPOINTMENTS_ALL">All Appointments</option>
+                    <option value="PRO_AVAILABILITY">Availability</option>
+                    <option value="BOTH">Availability &amp; Active Appointments</option>
+                    <option value="BOTH_ALL">Availability &amp; All Appointments</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 w-180"
+                        v-model="timezone">
+                    <option value="EASTERN" selected>Eastern</option>
+                    <option value="CENTRAL">Central</option>
+                    <option value="MOUNTAIN">Mountain</option>
+                    <option value="PACIFIC">Pacific</option>
+                    <option value="ALASKA">Alaska</option>
+                    <option value="HAWAII">Hawaii</option>
+                    <option value="PUERTO_RICO">Puerto Rico</option>
+                </select>
+            </div>
+        </div>
+        <div class="d-block appt-form">
+            @if($pro->pro_type == 'ADMIN')
+            <form class="appt-form-col w-100 d-flex align-items-center">
+                <label class="mr-2 my-0 text-secondary text-nowrap">Show all appointments for</label>
+                <select id="eventPros" name="proUid"
+                        class="form-control form-control-sm flex-grow-1" multiple
+                        v-model="proIds">
+                    <?php
+                    $proIndex = 0;
+                    $proMeta = [];
+                    ?>
+                    @foreach($pros as $iPro)
+                        <option value="{{$iPro->id}}"
+                                data-bc="{{$palette[$proIndex]["bc"]}}"
+                                data-fc="{{$palette[$proIndex]["fc"]}}"
+                                data-initials="{{$iPro->initials()}}">
+                            {{$iPro->displayName()}}
+                        </option>
+                        <?php
+                        $proMeta[$iPro->uid] = [
+                            "bc" => $palette[$proIndex]["bc"],
+                            "fc" => $palette[$proIndex]["fc"],
+                            "ac" => $palette[$proIndex]["ac"],
+                            "initials" => $iPro->initials()
+                        ];
+                        $proIndex++;
+                        if($proIndex >= count($palette)) $proIndex = 0;
+                        ?>
+                    @endforeach
+                </select>
+            </form>
+            @else
+                <?php
+                $proMeta[$pro->uid] = [
+                    "bc" => $palette[0]["bc"],
+                    "fc" => $palette[0]["fc"],
+                    "ac" => $palette[0]["ac"],
+                    "initials" => $pro->initials()
+                ];
+                ?>
+            @endif
+            <hr class="my-2">
+            <div class="appt-calendar-col mt-2">
+                <div class="stag-fc-container"></div>
+            </div>
+        </div>
+        <div class="stag-popup stag-popup-sm mcp-theme-1" stag-popup-key="client-add-appointment">
+            <form method="POST" action="/api/appointment/create" id="newApptForm">
+                <h3 class="stag-popup-title">
+                    <span>Book New Appointment</span>
+                    <a href="#" class="ml-auto text-secondary"
+                       onclick="return closeStagPopup()"><i class="fa fa-times-circle"></i></a>
+                </h3>
+                <input type="hidden" name="clientUid" :value="newAppointment.clientUid">
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        Patient
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <input type="text"
+                               class="form-control form-control-sm"
+                               value="- TODO -" readonly>
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        Pro
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <select id="addApptPro" name="proUid" required
+                                v-model="newAppointment.proUid"
+                                class="form-control form-control-sm">
+                            @foreach($pros as $iPro)
+                                <option value="{{$iPro->uid}}">
+                                    {{$iPro->displayName()}}
+                                </option>
+                            @endforeach
+                        </select>
+                    </div>
+                </div>
+                <input type="hidden" name="referringProUid" value="{{ $pro->uid }}">
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        Date
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <input type="date" name="date" required
+                               class="form-control form-control-sm"
+                               v-model="newAppointment.date">
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        Start Time
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <input type="time" name="startTime" required
+                               class="form-control form-control-sm"
+                               v-model="newAppointment.startTime">
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        End Time
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <input type="time" name="endTime"
+                               class="form-control form-control-sm"
+                               v-model="newAppointment.endTime">
+                    </div>
+                </div>
+                <input type="hidden" name="timeZone" :value="timezone">
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        Timezone
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <input type="text"
+                               class="form-control form-control-sm"
+                               :value="timezone" readonly>
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        Title
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <input type="text" name="title"
+                               class="form-control form-control-sm"
+                               v-model="newAppointment.title">
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        Description
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <textarea name="description"
+                                  class="form-control form-control-sm"
+                                  v-model="newAppointment.description"></textarea>
+                    </div>
+                </div>
+                <div class="d-flex align-items-center justify-content-center">
+                    <button class="btn btn-sm btn-primary mr-2"
+                            :disabled="inProgress"
+                            v-on:click.prevent="addAppointment()">Submit</button>
+                    <button class="btn btn-sm btn-default border"
+                            onclick="return closeStagPopup()">Cancel</button>
+                </div>
+            </form>
+        </div>
+        <div class="stag-popup stag-popup-sm mcp-theme-1" stag-popup-key="client-edit-appointment">
+            <form method="POST" action="/api/appointment/update" id="editApptForm">
+                <h3 class="stag-popup-title">
+                    <span>Edit Appointment</span>
+                    <a href="#" class="ml-auto text-secondary"
+                       onclick="return closeStagPopup()"><i class="fa fa-times-circle"></i></a>
+                </h3>
+                <input type="hidden" name="uid" :value="editAppointment.uid">
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        Patient
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <input type="text"
+                               class="form-control form-control-sm"
+                               :value="editAppointment.clientName" readonly>
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        Pro
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <select id="editApptPro" name="proUid" required
+                                v-model="editAppointment.proUid"
+                                class="form-control form-control-sm">
+                            @foreach($pros as $iPro)
+                                <option value="{{$iPro->uid}}">
+                                    {{$iPro->displayName()}}
+                                </option>
+                            @endforeach
+                        </select>
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        Date
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <input type="date" name="date" required
+                               class="form-control form-control-sm"
+                               v-model="editAppointment.date">
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        Start Time
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <input type="time" name="startTime" required
+                               class="form-control form-control-sm"
+                               v-model="editAppointment.startTime">
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        End Time
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <input type="time" name="endTime"
+                               class="form-control form-control-sm"
+                               v-model="editAppointment.endTime">
+                    </div>
+                </div>
+                <input type="hidden" name="timeZone" :value="timezone">
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        Timezone
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <input type="text"
+                               class="form-control form-control-sm"
+                               :value="timezone" readonly>
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        Status
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <select id="editApptStatus" name="status" required
+                                v-model="editAppointment.status"
+                                class="form-control form-control-sm font-weight-bold px-1">
+                            <option value="CREATED">CREATED</option>
+                            <option value="CONFIRMED">CONFIRMED</option>
+                            <option value="CANCELLED">CANCELLED</option>
+                            <option value="COMPLETED">COMPLETED</option>
+                            <option value="ABANDONED">ABANDONED</option>
+                        </select>
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        Title
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <input type="text" name="title"
+                               class="form-control form-control-sm"
+                               v-model="editAppointment.title">
+                    </div>
+                </div>
+                <div class="row mb-2">
+                    <div class="col-3 text-secondary">
+                        Description
+                    </div>
+                    <div class="col-9 font-weight-bold">
+                        <textarea name="description"
+                                  class="form-control form-control-sm"
+                                  v-model="editAppointment.description"></textarea>
+                    </div>
+                </div>
+                <div class="d-flex align-items-center justify-content-center">
+                    <button class="btn btn-sm btn-primary mr-2"
+                            :disabled="inProgress"
+                            v-on:click.prevent="updateAppointment()">Submit</button>
+                    <button class="btn btn-sm btn-default border"
+                            onclick="return closeStagPopup()">Cancel</button>
+                </div>
+            </form>
+        </div>
+    </div>
+    <script>
+        (function() {
+
+            function init() {
+                window.proCalendarApp = new Vue({
+                    el: '#proCalendarApp',
+                    data: {
+                        eventTypes: 'BOTH',
+                        calendar: null,
+                        proMeta: {!! json_encode($proMeta) !!},
+                        proIds: ['{{ $pro->id }}'],
+                        timezone: 'EASTERN',
+                        today: new Date('{{ date('Y-m-d 00:00:00') }}'),
+
+                        // user clicks/selection
+                        selectedSlot: null,
+                        selectedEvent: null,
+
+                        // new appt.
+                        newAppointment: {
+                            proUid: '{{ $pro->uid }}',
+                            referringProUid: '{{ $pro->uid }}',
+                            date: '',
+                            startTime: '',
+                            endTime: '',
+                            timeZone: '',
+                            title: '',
+                            description: '',
+                        },
+
+                        // edit appt.
+                        editAppointment: {
+                            proUid: '',
+                            clientName: '',
+                            date: '',
+                            startTime: '',
+                            endTime: '',
+                            timeZone: '',
+                            title: '',
+                            description: '',
+                            status: '',
+                        },
+
+                        // availability
+                        availability: {},
+
+                        inProgress: false,
+                        editHonored: false,
+                    },
+                    methods: {
+                        // init
+                        init: function() {
+                            this.initSelect2();
+                            this.initCalendar();
+                        },
+                        initSelect2: function () {
+                            let self = this;
+                            $('#eventTz')
+                                .select2({
+                                    templateResult: function(_state) {
+                                        return $('<span class="mcp-theme-1"><span>' + _state.text + '</span></span>');
+                                    }
+                                })
+                                .on('change', function() {
+                                    self.timezone = $(this).val();
+                                    @if($pro->pro_type == 'ADMIN')
+                                    localStorage.stagProCalendarTZ = self.timezone;
+                                    @endif
+                                    self.refreshEvents();
+                                });
+                            $('#eventPros')
+                                .select2({
+                                    closeOnSelect: false,
+
+                                    // dropdown options
+                                    templateResult: function(_state) {
+                                        let element = _state.element;
+                                        if(!element || !element.value) {
+                                            return $('<span class="mcp-theme-1"><span>' + _state.text + '</span></span>');
+                                        }
+                                        element = $(element);
+                                        return $('<span class="mcp-theme-1 pro-option" ' +
+                                            'data-initials="' + element.attr('data-initials') + '" ' +
+                                            'data-bc="' + element.attr('data-bc') + '" ' +
+                                            'data-fc="' + element.attr('data-fc') + '"><span>' +
+                                            '<span class="pro-option-initials" ' +
+                                            'style="background: ' + element.attr('data-bc') + '; color: ' + element.attr('data-fc') + '">' +
+                                            element.attr('data-initials') + '</span>' +
+                                            _state.text +
+                                            '</span></span>');
+                                    },
+
+                                    // selected items
+                                    templateSelection: function(_state) {
+                                        let element = _state.element;
+                                        if(!element || !element.value) {
+                                            return $('<span class="mcp-theme-1"><span>' + _state.text + '</span></span>');
+                                        }
+                                        element = $(element);
+                                        return $('<span class="pro-selection" style="background: ' + element.attr('data-bc') + '; color: ' + element.attr('data-fc') + '">' +
+                                            _state.text + '</span>');
+                                    }
+                                })
+                                .on('change', function() {
+                                    self.proIds = $(this).val();
+                                    @if($pro->pro_type == 'ADMIN')
+                                    localStorage.stagProCalendarProIds = JSON.stringify(self.proIds);
+                                    @endif
+                                    self.refreshEvents();
+                                });
+                            $('#addApptPro')
+                                .select2({
+                                    width: '100%',
+                                    templateResult: function(_state) {
+                                        return $('<span class="mcp-theme-1"><span>' + _state.text + '</span></span>');
+                                    }
+                                })
+                                .on('change', function() {
+                                    self.newAppointment.proUid = $(this).val();
+                                });
+                            $('#editApptPro')
+                                .select2({
+                                    width: '100%',
+                                    templateResult: function(_state) {
+                                        return $('<span class="mcp-theme-1"><span>' + _state.text + '</span></span>');
+                                    }
+                                })
+                                .on('change', function() {
+                                    self.editAppointment.proUid = $(this).val();
+                                });
+                        },
+                        initCalendar: function () {
+                            let self = this;
+                            <?php
+                            $initialDate = date('Y-m-d');
+                            ?>
+                            this.calendar = new FullCalendar.Calendar($('.stag-fc-container')[0], {
+                                headerToolbar: {
+                                    left: 'prev,next today',
+                                    center: 'title',
+                                    right: 'dayGridMonth,timeGridWeek,timeGridDay'
+                                },
+                                initialView: 'timeGridWeek',
+                                initialDate: '{{ $initialDate }}',
+                                editable: true,
+                                selectable: true,
+                                navLinks: true,
+                                dayMaxEvents: false,
+                                slotMinTime: '06:00',
+                                slotMaxTime: '20:00',
+                                slotDuration: '00:15:00',
+                                events: function(info, successCallback, failureCallback) {
+                                    if(!self.proIds || !self.proIds.length) {
+                                        successCallback([]); // no events if no pro selected
+                                        return;
+                                    }
+                                    if(!self.timezone) {
+                                        successCallback([]); // no events if no tz selected
+                                        return;
+                                    }
+                                    $.get('/appointment/getAllAppointmentsForPros' +
+                                        '?proIds=' + self.proIds +
+                                        '&clientId=-1' +
+                                        '&start=' + info.startStr.substr(0, 10) +
+                                        '&end=' + info.endStr.substr(0, 10) +
+                                        '&timeZone=' + self.timezone, function(_data) {
+                                        if(_data && Array.isArray(_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];
+                                                    if(ev.type === 'appointment') {
+                                                        if(self.eventTypes.indexOf('APPOINTMENTS') === 0 || self.eventTypes.indexOf('BOTH') === 0) {
+                                                            ev.backgroundColor =  meta.bc;
+                                                            ev.borderColor = meta.bc;
+                                                            ev.textColor = meta.fc;
+                                                            ev.initials = meta.initials;
+                                                            ev.display = 'block';
+                                                            ev.editable = true;
+
+                                                            // active/all
+                                                            if(['CANCELLED', 'COMPLETED', 'ABANDONED'].indexOf(ev.status) !== -1) {
+                                                                if(self.eventTypes === 'APPOINTMENTS_ALL' || self.eventTypes === 'BOTH_ALL') {
+                                                                    displayEvents.push(ev);
+                                                                }
+                                                            }
+                                                            else {
+                                                                displayEvents.push(ev);
+                                                            }
+                                                        }
+                                                    }
+                                                    else {
+                                                        if(self.eventTypes === 'PRO_AVAILABILITY' || self.eventTypes.indexOf('BOTH') === 0) {
+                                                            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(displayEvents);
+                                        }
+                                        else {
+                                            failureCallback('Unable to refresh appointments!');
+                                        }
+                                    }, 'json');
+                                },
+                                eventDidMount: function(view) {
+                                    self.afterRenderingEvents();
+                                },
+                                eventClassNames: function(arg) {
+                                    let classes = [];
+                                    /*if (arg.event.extendedProps.clientOnly) {
+                                        classes.push('client-only');
+                                    }
+                                    if (arg.event.extendedProps.otherClient) {
+                                        classes.push('other-client');
+                                    }*/
+                                    if (arg.event.extendedProps.type === 'availability') {
+                                        classes.push('availability');
+                                    }
+                                    if (['CANCELLED', 'COMPLETED', 'ABANDONED'].indexOf(arg.event.extendedProps.status) !== -1) {
+                                        classes.push('inactive-appointment');
+                                    }
+                                    if (arg.event.extendedProps.type === 'appointment') {
+                                        classes.push('appointment-' + arg.event.extendedProps.appointmentUid);
+                                    }
+                                    return classes;
+                                },
+                                loading: function(bool) {
+
+                                },
+                                eventClick: function(info) {
+                                    self.selectedEvent = info.event;
+                                    self.showEditAppointmentModal();
+                                },
+                                selectAllow: function(info) { // allow only single selections
+                                    let seconds = info.end.getTime() - info.start.getTime(),
+                                        maxDelta = 86400000;
+                                    console.log(seconds);
+                                    if(!info.allDay) {
+                                        maxDelta = 1800000;
+                                    }
+                                    return seconds <= maxDelta;
+                                },
+                                select: function(info) {
+                                    self.selectedSlot = info;
+                                    /*if(self.today.getTime() <= self.selectedSlot.start.getTime()) {
+                                        $('<a href="#" class="add-overlay ' +
+                                            (info.allDay ? 'add-overlay-day-grid' : 'add-overlay-time-grid') + '">Add</a>')
+                                            .on('mousedown', function() {
+                                                self.showAddAppointmentModal();
+                                                return false;
+                                            })
+                                            .appendTo('.fc-highlight');
+                                    }*/
+                                },
+                                eventDrop: self.eventMovedOrResized,
+                                eventResize: self.eventMovedOrResized
+                            });
+                            $(document).on('mousedown', '.fc-highlight', function() {
+                                 if(self.selectedSlot && self.today.getTime() <= self.selectedSlot.start.getTime()) {
+                                     self.showAddAppointmentModal();
+                                 }
+                            });
+                            this.calendar.render();
+                        },
+                        afterRenderingEvents: function() {
+                            let self = this;
+                            $('.fc .availability').each(function () {
+                                $(this).parent('.fc-timegrid-event-harness').css('pointer-events', 'none');
+                            });
+                        },
+                        dateStr: function(_dateTime) {
+                            return _dateTime.getFullYear() + "-" +
+                                ("0"+(_dateTime.getMonth()+1)).slice(-2) + "-" +
+                                ("0" + _dateTime.getDate()).slice(-2);
+                        },
+                        timeStr: function(_dateTime) {
+                            return ("0" + _dateTime.getHours()).slice(-2) + ":" +
+                                ("0" + _dateTime.getMinutes()).slice(-2);
+                        },
+                        eventMovedOrResized: function (info) {
+                            let self = this;
+                            if(!window.confirm('Are you sure?')) {
+                                this.refreshEvents();
+                                return;
+                            }
+                            let date = this.dateStr(info.event.start);
+                            let startTime = this.timeStr(info.event.start);
+                            let endTime = null;
+                            if(info.event.end) {
+                                endTime = this.timeStr(info.event.end);
+                            }
+                            $.post('/api/appointment/updateDateAndTime', {
+                                uid: info.event.extendedProps.appointmentUid,
+                                date: date,
+                                startTime: startTime,
+                                endTime: endTime,
+                                timeZone: this.timezone,
+                            }, function(_data) {
+                                self.refreshEvents();
+                                if(_data) {
+                                    if(!_data.success) {
+                                        toastr.error(_data.message);
+                                    }
+                                }
+                                else {
+                                    toastr.error('Unable to update the appointment!');
+                                }
+                            }, 'json');
+                        },
+                        refreshEvents: function() {
+                            this.calendar.refetchEvents();
+                        },
+                        showAddAppointmentModal: function() {
+                            // setup model data
+                            /*this.inProgress = false;
+                            let startTime = this.timeStr(this.selectedSlot.start);
+                            let endTime = this.timeStr(this.selectedSlot.end);
+                            this.newAppointment.clientUid = -1; // TODO
+                            this.newAppointment.proUid = '';
+                            this.newAppointment.referringProUid = '';
+                            this.newAppointment.date = this.dateStr(this.selectedSlot.start);
+                            this.newAppointment.startTime = startTime === '00:00' ? '' : startTime;
+                            this.newAppointment.endTime = (this.selectedSlot.allDay || endTime === '00:00') ? '' : endTime;
+                            this.newAppointment.timeZone = this.timezone;
+                            this.newAppointment.title = '';
+                            this.newAppointment.description = '';
+                            Vue.nextTick(function() {
+                                $('#addApptPro').find('option').prop('selected', false);
+                                $('#addApptPro').trigger('change');
+                                showStagPopup('client-add-appointment');
+                                $('#addApptPro').select2('open');
+                            });*/
+                        },
+                        addAppointment: function() {
+                            let form = $('#newApptForm');
+                            if(!form[0].checkValidity()) {
+                                form[0].reportValidity();
+                                return false;
+                            }
+                            this.inProgress = true;
+                            let self = this;
+                            showMask();
+                            $.post(form.attr('action'), form.serialize(), function(_data) {
+                                if(_data) {
+                                    if(_data.success) {
+                                        self.refreshEvents();
+                                        closeStagPopup();
+                                    }
+                                    else {
+                                        toastr.error(_data.message);
+                                    }
+                                }
+                                else {
+                                    toastr.error('Unable to book appointment!');
+                                }
+                                self.inProgress = false;
+                                hideMask();
+                            }, 'json');
+                        },
+                        showEditAppointmentModal: function() {
+                            // setup model data
+                            /*this.inProgress = false;
+                            this.editAppointment.uid = this.selectedEvent.extendedProps.appointmentUid;
+                            this.editAppointment.clientName = this.selectedEvent.extendedProps.clientName;
+                            this.editAppointment.proUid = this.selectedEvent.extendedProps.proUid;
+                            this.editAppointment.date = this.dateStr(this.selectedEvent.start);
+                            this.editAppointment.startTime = this.timeStr(this.selectedEvent.start);
+                            this.editAppointment.endTime = this.selectedEvent.end ? this.timeStr(this.selectedEvent.end) : '';
+                            this.editAppointment.timeZone = this.timezone;
+                            this.editAppointment.title = this.selectedEvent.extendedProps._title;
+                            this.editAppointment.description = this.selectedEvent.extendedProps.description;
+                            this.editAppointment.status = this.selectedEvent.extendedProps.status;
+                            Vue.nextTick(function() {
+                                $('#editApptPro').trigger('change');
+                                showStagPopup('client-edit-appointment');
+                            });*/
+                        },
+                        updateAppointment: function() {
+                            let form = $('#editApptForm');
+                            if(!form[0].checkValidity()) {
+                                form[0].reportValidity();
+                                return false;
+                            }
+
+                            // confirming if changing to cancelled, completed or abandoned
+                            if(this.selectedEvent.extendedProps.status !== this.editAppointment.status &&
+                                ['CANCELLED', 'COMPLETED', 'ABANDONED'].indexOf(this.editAppointment.status) !== -1) {
+                                if(!window.confirm('CANCELLED, COMPLETED and ABANDONED appointments will ' +
+                                    'not be displayed in the calendar by default.\n\n' +
+                                    'Are you sure you want to update the status of this appointment to ' + this.editAppointment.status + '?')) {
+                                    return false;
+                                }
+                            }
+
+                            this.inProgress = true;
+                            let self = this;
+                            showMask();
+                            $.post(form.attr('action'), form.serialize(), function(_data) {
+                                if(_data) {
+                                    if(_data.success) {
+                                        self.refreshEvents();
+                                        closeStagPopup();
+                                    }
+                                    else {
+                                        toastr.error(_data.message);
+                                    }
+                                }
+                                else {
+                                    toastr.error('Unable to update appointment!');
+                                }
+                                self.inProgress = false;
+                                hideMask();
+                            }, 'json');
+                        },
+                    },
+                    mounted: function() {
+                        this.init();
+                        @if($pro->pro_type == 'ADMIN')
+                            if(localStorage.stagProCalendarTZ) {
+                                this.timezone = localStorage.stagProCalendarTZ;
+                                Vue.nextTick(function() {
+                                    $('#eventTz').trigger('change');
+                                });
+                            }
+                            if(localStorage.stagProCalendarProIds) {
+                                this.proIds = JSON.parse(localStorage.stagProCalendarProIds);
+                                Vue.nextTick(function() {
+                                    $('#eventPros').trigger('change');
+                                });
+                            }
+                        @endif
+                    }
+                });
+            }
+
+            addMCInitializer('patient-calendar', init);
+        })();
+    </script>
+@endsection

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

@@ -98,6 +98,7 @@
                         {{--<a class="dropdown-item" href="/practice-management/hr">HR</a>--}}
                         <a class="dropdown-item" href="{{ route('practice-management.myTextShortcuts') }}">My Text Shortcuts</a>
                         <a class="dropdown-item" href="{{ route('practice-management.proAvailability') }}">Pro Availability</a>
+                        <a class="dropdown-item" href="{{ route('practice-management.proCalendar') }}">Pro Calendar</a>
                     </div>
                 </li>
             </ul>

+ 1 - 0
routes/web.php

@@ -78,6 +78,7 @@ Route::middleware('pro.auth')->group(function () {
         Route::get('bills/{filter?}', 'PracticeManagementController@bills')->name('bills');
         Route::get('my-text-shortcuts', 'PracticeManagementController@myTextShortcuts')->name('myTextShortcuts');
         Route::get('pro-availability/{proUid?}', 'PracticeManagementController@proAvailability')->name('proAvailability');
+        Route::get('calendar/{proUid?}', 'PracticeManagementController@calendar')->name('proCalendar');
 
     });