diff --git a/src/application/views/backend/calendar.php b/src/application/views/backend/calendar.php index 8ac14f51..f82c7a7b 100644 --- a/src/application/views/backend/calendar.php +++ b/src/application/views/backend/calendar.php @@ -37,7 +37,7 @@ }; $(document).ready(function() { - BackendCalendar.initialize(true); + BackendCalendar.initialize(GlobalVariables.calendarView); }); diff --git a/src/assets/js/backend_calendar.js b/src/assets/js/backend_calendar.js index add07e8f..902c3035 100644 --- a/src/assets/js/backend_calendar.js +++ b/src/assets/js/backend_calendar.js @@ -12,9 +12,9 @@ /** * Backend Calendar * - * This namespace contains functions that are used by the backend calendar page. + * This module contains functions that are used by the backend calendar page. * - * @namespace BackendCalendar + * @module BackendCalendar */ window.BackendCalendar = window.BackendCalendar || {}; @@ -22,10 +22,6 @@ window.BackendCalendar = window.BackendCalendar || {}; 'use strict'; - // Constants - var FILTER_TYPE_PROVIDER = 'provider'; - var FILTER_TYPE_SERVICE = 'service'; - // Variables var lastFocusedEventData; // Contains event data for later use. @@ -36,38 +32,9 @@ window.BackendCalendar = window.BackendCalendar || {}; * default handlers then initialize the page by setting the "defaultEventHandlers" argument to "false". */ function _bindEventHandlers() { - /** - * Event: Calendar Filter Item "Change" - * - * Load the appointments that correspond to the select filter item and display them on the calendar. - */ - $('#select-filter-item').change(function() { - _refreshCalendarAppointments( - $('#calendar'), - $('#select-filter-item').val(), - $('#select-filter-item option:selected').attr('type'), - $('#calendar').fullCalendar('getView').visStart, - $('#calendar').fullCalendar('getView').visEnd); - - // If current value is service, then the sync buttons must be disabled. - if ($('#select-filter-item option:selected').attr('type') === FILTER_TYPE_SERVICE) { - $('#google-sync, #enable-sync, #insert-appointment, #insert-unavailable').prop('disabled', true); - } else { - $('#google-sync, #enable-sync, #insert-appointment, #insert-unavailable').prop('disabled', false); - - // If the user has already the sync enabled then apply the proper style changes. - if ($('#select-filter-item option:selected').attr('google-sync') === 'true') { - $('#enable-sync').addClass('btn-danger enabled'); - $('#enable-sync span:eq(1)').text(EALang['disable_sync']); - $('#google-sync').prop('disabled', false); - } else { - $('#enable-sync').removeClass('btn-danger enabled'); - $('#enable-sync span:eq(1)').text(EALang['enable_sync']); - $('#google-sync').prop('disabled', true); - } - } - }); + var $calendarPage = $('#calendar-page'); + /** * Event: Google Sync Button "Click" * @@ -119,7 +86,7 @@ window.BackendCalendar = window.BackendCalendar || {}; * * Hides the open popover element. */ - $(document).on('click', '.close-popover', function() { + $calendarPage.on('click', '.close-popover', function() { $(this).parents().eq(2).remove(); }); @@ -128,7 +95,7 @@ window.BackendCalendar = window.BackendCalendar || {}; * * Enables the edit dialog of the selected calendar event. */ - $(document).on('click', '.edit-popover', function() { + $calendarPage.on('click', '.edit-popover', function() { $(this).parents().eq(2).remove(); // Hide the popover var $dialog; @@ -193,7 +160,7 @@ window.BackendCalendar = window.BackendCalendar || {}; * Displays a prompt on whether the user wants the appoinmtent to be deleted. If he confirms the * deletion then an ajax call is made to the server and deletes the appointment from the database. */ - $(document).on('click', '.delete-popover', function() { + $calendarPage.on('click', '.delete-popover', function() { $(this).parents().eq(2).remove(); // Hide the popover if (lastFocusedEventData.data.is_unavailable == false) { @@ -643,7 +610,7 @@ window.BackendCalendar = window.BackendCalendar || {}; /** * Event: Select Existing Customer From List "Click" */ - $(document).on('click', '#existing-customers-list div', function() { + $calendarPage.on('click', '#existing-customers-list div', function() { var id = $(this).attr('data-id'); $.each(GlobalVariables.customers, function(index, c) { @@ -788,282 +755,6 @@ window.BackendCalendar = window.BackendCalendar || {}; }); } - /** - * Get Calendar Component Height - * - * This method calculates the proper calendar height, in order to be displayed correctly, even when the - * browser window is resizing. - * - * @return {Number} Returns the calendar element height in pixels. - */ - function _getCalendarHeight() { - var result = window.innerHeight - $('#footer').outerHeight() - $('#header').outerHeight() - - $('#calendar-toolbar').outerHeight() - 60; // 60 for fine tuning - return (result > 500) ? result : 500; // Minimum height is 500px - } - - /** - * Refresh Calendar Appointments - * - * This method reloads the registered appointments for the selected date period and filter type. - * - * @param {Object} $calendar The calendar jQuery object. - * @param {Number} recordId The selected record id. - * @param {String} filterType The filter type, could be either FILTER_TYPE_PROVIDER or FILTER_TYPE_SERVICE. - * @param {Date} startDate Visible start date of the calendar. - * @param {Date} endDate Visible end date of the calendar. - */ - function _refreshCalendarAppointments($calendar, recordId, filterType, startDate, endDate) { - var postUrl = GlobalVariables.baseUrl + '/index.php/backend_api/ajax_get_calendar_appointments'; - var postData = { - csrfToken: GlobalVariables.csrfToken, - record_id: recordId, - start_date: startDate.toString('yyyy-MM-dd'), - end_date: endDate.toString('yyyy-MM-dd'), - filter_type: filterType - }; - - $.post(postUrl, postData, function(response) { - if (!GeneralFunctions.handleAjaxExceptions(response)) { - return; - } - - // Add appointments to calendar. - var calendarEvents = []; - var $calendar = $('#calendar'); - - $.each(response.appointments, function(index, appointment) { - var event = { - id: appointment['id'], - title: appointment['service']['name'] + ' - ' - + appointment['customer']['first_name'] + ' ' - + appointment['customer']['last_name'], - start: appointment['start_datetime'], - end: appointment['end_datetime'], - allDay: false, - data: appointment // Store appointment data for later use. - }; - - calendarEvents.push(event); - }); - - $calendar.fullCalendar('removeEvents'); - $calendar.fullCalendar('addEventSource', calendarEvents); - - // :: ADD PROVIDER'S UNAVAILABLE TIME PERIODS - var calendarView = $calendar.fullCalendar('getView').name; - - if (filterType === FILTER_TYPE_PROVIDER && calendarView !== 'month') { - $.each(GlobalVariables.availableProviders, function(index, provider) { - if (provider['id'] == recordId) { - var workingPlan = jQuery.parseJSON(provider.settings.working_plan); - var unavailablePeriod; - - switch(calendarView) { - case 'agendaDay': - var selDayName = $calendar.fullCalendar('getView') - .start.toString('dddd').toLowerCase(); - - // Add custom unavailable periods. - $.each(response.unavailables, function(index, unavailable) { - var unavailablePeriod = { - title: EALang['unavailable'] + '
' + ((unavailable.notes.length > 30) - ? unavailable.notes.substring(0, 30) + '...' - : unavailable.notes) + '', - start: Date.parse(unavailable.start_datetime), - end: Date.parse(unavailable.end_datetime), - allDay: false, - color: '#879DB4', - editable: true, - className: 'fc-unavailable fc-custom', - data: unavailable - }; - $calendar.fullCalendar('renderEvent', unavailablePeriod, false); - }); - - // Non-working day - if (workingPlan[selDayName] == null) { - unavailablePeriod = { - title: EALang['not_working'], - start: GeneralFunctions.clone($calendar.fullCalendar('getView').start), - end: GeneralFunctions.clone($calendar.fullCalendar('getView').end), - allDay: false, - color: '#BEBEBE', - editable: false, - className: 'fc-unavailable' - }; - $calendar.fullCalendar('renderEvent', unavailablePeriod, true); - return; // Go to next loop - } - - // Add unavailable period before work starts. - var calendarDateStart = $calendar.fullCalendar('getView').start, - workDateStart = Date.parseExact( - calendarDateStart.toString('dd/MM/yyyy') + ' ' - + workingPlan[selDayName].start, - 'dd/MM/yyyy HH:mm'); - - if (calendarDateStart < workDateStart) { - unavailablePeriod = { - title: EALang['not_working'], - start: calendarDateStart, - end: workDateStart, - allDay: false, - color: '#BEBEBE', - editable: false, - className: 'fc-unavailable' - }; - $calendar.fullCalendar('renderEvent', unavailablePeriod, false); - } - - // Add unavailable period after work ends. - var calendarDateEnd = $calendar.fullCalendar('getView').end; - var workDateEnd = Date.parseExact( - calendarDateStart.toString('dd/MM/yyyy') + ' ' - + workingPlan[selDayName].end, - 'dd/MM/yyyy HH:mm'); // Use calendarDateStart *** - if (calendarDateEnd > workDateEnd) { - var unavailablePeriod = { - title: EALang['not_working'], - start: workDateEnd, - end: calendarDateEnd, - allDay: false, - color: '#BEBEBE', - editable: false, - className: 'fc-unavailable' - }; - $calendar.fullCalendar('renderEvent', unavailablePeriod, false); - } - - // Add unavailable periods for breaks. - var breakStart; - var breakEnd; - $.each(workingPlan[selDayName].breaks, function(index, currBreak) { - breakStart = Date.parseExact(calendarDateStart.toString('dd/MM/yyyy') - + ' ' + currBreak.start, 'dd/MM/yyyy HH:mm'); - breakEnd = Date.parseExact(calendarDateStart.toString('dd/MM/yyyy') - + ' ' + currBreak.end, 'dd/MM/yyyy HH:mm'); - var unavailablePeriod = { - title: EALang['break'], - start: breakStart, - end: breakEnd, - allDay: false, - color: '#BEBEBE', - editable: false, - className: 'fc-unavailable fc-break' - }; - $calendar.fullCalendar('renderEvent', unavailablePeriod, false); - }); - - break; - - case 'agendaWeek': - var currDateStart = GeneralFunctions.clone($calendar.fullCalendar('getView').start); - var currDateEnd = GeneralFunctions.clone(currDateStart).addDays(1); - - // Add custom unavailable periods (they are always displayed on the calendar, even if - // the provider won't work on that day). - $.each(response.unavailables, function(index, unavailable) { - unavailablePeriod = { - title: EALang['unavailable'] + '
' + ((unavailable.notes.length > 30) - ? unavailable.notes.substring(0, 30) + '...' - : unavailable.notes) + '', - start: Date.parse(unavailable.start_datetime), - end: Date.parse(unavailable.end_datetime), - allDay: false, - color: '#879DB4', - editable: true, - className: 'fc-unavailable fc-custom', - data: unavailable - }; - $calendar.fullCalendar('renderEvent', unavailablePeriod, false); - }); - - $.each(workingPlan, function(index, workingDay) { - - if (workingDay == null) { - // Add a full day unavailable event. - unavailablePeriod = { - title: EALang['not_working'], - start: GeneralFunctions.clone(currDateStart), - end: GeneralFunctions.clone(currDateEnd), - allDay: false, - color: '#BEBEBE', - editable: false, - className: 'fc-unavailable' - }; - $calendar.fullCalendar('renderEvent', unavailablePeriod, true); - currDateStart.addDays(1); - currDateEnd.addDays(1); - return; // Go to the next loop. - } - - var start; - var end; - - // Add unavailable period before work starts. - start = Date.parseExact(currDateStart.toString('dd/MM/yyyy') - + ' ' + workingDay.start, 'dd/MM/yyyy HH:mm'); - if (currDateStart < start) { - unavailablePeriod = { - title: EALang['not_working'], - start: GeneralFunctions.clone(currDateStart), - end: GeneralFunctions.clone(start), - allDay: false, - color: '#BEBEBE', - editable: false, - className: 'fc-unavailable' - }; - $calendar.fullCalendar('renderEvent', unavailablePeriod, true); - } - - // Add unavailable period after work ends. - end = Date.parseExact(currDateStart.toString('dd/MM/yyyy') - + ' ' + workingDay.end, 'dd/MM/yyyy HH:mm'); - if (currDateEnd > end) { - unavailablePeriod = { - title: EALang['not_working'], - start: GeneralFunctions.clone(end), - end: GeneralFunctions.clone(currDateEnd), - allDay: false, - color: '#BEBEBE', - editable: false, - className: 'fc-unavailable fc-brake' - }; - $calendar.fullCalendar('renderEvent', unavailablePeriod, false); - } - - // Add unavailable periods during day breaks. - var breakStart; - var breakEnd; - $.each(workingDay.breaks, function(index, currBreak) { - breakStart = Date.parseExact(currDateStart.toString('dd/MM/yyyy') - + ' ' + currBreak.start, 'dd/MM/yyyy HH:mm'); - breakEnd = Date.parseExact(currDateStart.toString('dd/MM/yyyy') - + ' ' + currBreak.end, 'dd/MM/yyyy HH:mm'); - var unavailablePeriod = { - title: EALang['break'], - start: breakStart, - end: breakEnd, - allDay: false, - color: '#BEBEBE', - editable: false, - className: 'fc-unavailable fc-break' - }; - $calendar.fullCalendar('renderEvent', unavailablePeriod, false); - }); - - currDateStart.addDays(1); - currDateEnd.addDays(1); - }); - break; - } - } - }); - } - }, 'json').fail(GeneralFunctions.ajaxFailureHandler); - } - /** * Save Appointment * @@ -1125,458 +816,7 @@ window.BackendCalendar = window.BackendCalendar || {}; success: successCallback, error: errorCallback }); - } - - /** - * Calendar Event "Resize" Callback - * - * The user can change the duration of an event by resizing an appointment object on the calendar. This - * change needs to be stored to the database too and this is done via an ajax call. - * - * @see updateAppointmentData() - */ - function _calendarEventResize(event, dayDelta, minuteDelta, revertFunc, jsEvent, ui, view) { - if (GlobalVariables.user.privileges.appointments.edit == false) { - revertFunc(); - Backend.displayNotification(EALang['no_privileges_edit_appointments']); - return; - } - - if ($('#notification').is(':visible')) { - $('#notification').hide('bind'); - } - - if (event.data.is_unavailable == false) { - // Prepare appointment data. - var appointment = GeneralFunctions.clone(event.data); - - // Must delete the following because only appointment data should be provided to the ajax call. - delete appointment['customer']; - delete appointment['provider']; - delete appointment['service']; - - appointment['end_datetime'] = Date.parseExact( - appointment['end_datetime'], 'yyyy-MM-dd HH:mm:ss') - .add({ minutes: minuteDelta }) - .toString('yyyy-MM-dd HH:mm:ss'); - - // Success callback - var successCallback = function(response) { - if (response.exceptions) { - response.exceptions = GeneralFunctions.parseExceptions(response.exceptions); - GeneralFunctions.displayMessageBox(GeneralFunctions.EXCEPTIONS_TITLE, GeneralFunctions.EXCEPTIONS_MESSAGE); - $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); - return; - } - - if (response.warnings) { - // Display warning information to the user. - response.warnings = GeneralFunctions.parseExceptions(response.warnings); - GeneralFunctions.displayMessageBox(GeneralFunctions.WARNINGS_TITLE, GeneralFunctions.WARNINGS_MESSAGE); - $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.warnings)); - } - - // Display success notification to user. - var undoFunction = function() { - appointment['end_datetime'] = Date.parseExact( - appointment['end_datetime'], 'yyyy-MM-dd HH:mm:ss') - .add({ minutes: -minuteDelta }) - .toString('yyyy-MM-dd HH:mm:ss'); - - var postUrl = GlobalVariables.baseUrl + '/index.php/backend_api/ajax_save_appointment'; - var postData = { - csrfToken: GlobalVariables.csrfToken, - appointment_data: JSON.stringify(appointment) - }; - - $.post(postUrl, postData, function(response) { - $('#notification').hide('blind'); - revertFunc(); - }, 'json').fail(GeneralFunctions.ajaxFailureHandler); - }; - - Backend.displayNotification(EALang['appointment_updated'], [ - { - 'label': 'Undo', - 'function': undoFunction - } - ]); - $('#footer').css('position', 'static'); // Footer position fix. - }; - - // Update appointment data. - _saveAppointment(appointment, undefined, successCallback, undefined); - } else { - // Update unvailable time period. - var unavailable = { - id: event.data.id, - start_datetime: event.start.toString('yyyy-MM-dd HH:mm:ss'), - end_datetime: event.end.toString('yyyy-MM-dd HH:mm:ss'), - id_users_provider: event.data.id_users_provider - }; - - // Define success callback function. - var successCallback = function(response) { - if (response.exceptions) { - response.exceptions = GeneralFunctions.parseExceptions(response.exceptions); - GeneralFunctions.displayMessageBox(GeneralFunctions.EXCEPTIONS_TITLE, GeneralFunctions.EXCEPTIONS_MESSAGE); - $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); - return; - } - - if (response.warnings) { - // Display warning information to the user. - response.warnings = GeneralFunctions.parseExceptions(response.warnings); - GeneralFunctions.displayMessageBox(GeneralFunctions.WARNINGS_TITLE, GeneralFunctions.WARNINGS_MESSAGE); - $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.warnings)); - } - - // Display success notification to user. - var undoFunction = function() { - unavailable['end_datetime'] = Date.parseExact( - unavailable['end_datetime'], 'yyyy-MM-dd HH:mm:ss') - .add({ minutes: -minuteDelta }) - .toString('yyyy-MM-dd HH:mm:ss'); - - var postUrl = GlobalVariables.baseUrl + '/index.php/backend_api/ajax_save_unavailable'; - var postData = { - csrfToken: GlobalVariables.csrfToken, - unavailable: JSON.stringify(unavailable) - }; - - $.post(postUrl, postData, function(response) { - $('#notification').hide('blind'); - revertFunc(); - }, 'json').fail(GeneralFunctions.ajaxFailureHandler); - }; - - Backend.displayNotification(EALang['unavailable_updated'], [ - { - 'label': 'Undo', - 'function': undoFunction - } - ]); - $('#footer').css('position', 'static'); // Footer position fix. - }; - - _saveUnavailable(unavailable, successCallback, undefined); - } - } - - /** - * Calendar Window "Resize" Callback - * - * The calendar element needs to be resized too in order to fit into the window. Nevertheless, if the window - * becomes very small the the calendar won't shrink anymore. - * - * @see _getCalendarHeight() - */ - function _calendarWindowResize(view) { - $('#calendar').fullCalendar('option', 'height', _getCalendarHeight()); - } - - /** - * Calendar Day "Click" Callback - * - * When the user clicks on a day square on the calendar, then he will automatically be transfered to that - * day view calendar. - */ - function _calendarDayClick(date, allDay, jsEvent, view) { - if (allDay) { - $('#calendar').fullCalendar('gotoDate', date); - $('#calendar').fullCalendar('changeView', 'agendaDay'); - } - } - - /** - * Calendar Event "Click" Callback - * - * When the user clicks on an appointment object on the calendar, then a data preview popover is display - * above the calendar item. - */ - function _calendarEventClick(event, jsEvent, view) { - $('.popover').remove(); // Close all open popovers. - - var html; - var displayEdit; - var displayDelete; - - // Depending where the user clicked the event (title or empty space) we - // need to use different selectors to reach the parent element. - var $parent = $(jsEvent.target.offsetParent); - var $altParent = $(jsEvent.target).parents().eq(1); - - if ($parent.hasClass('fc-unavailable') || $altParent.hasClass('fc-unavailable')) { - displayEdit = (($parent.hasClass('fc-custom') || $altParent.hasClass('fc-custom')) - && GlobalVariables.user.privileges.appointments.edit == true) - ? '' : 'hide'; - displayDelete = (($parent.hasClass('fc-custom') || $altParent.hasClass('fc-custom')) - && GlobalVariables.user.privileges.appointments.delete == true) - ? '' : 'hide'; // Same value at the time. - - var notes = ''; - if (event.data) { // Only custom unavailable periods have notes. - notes = 'Notes ' + event.data.notes; - } - - html = - '' + - '' + EALang['start'] + ' ' - + GeneralFunctions.formatDate(event.start, GlobalVariables.dateFormat, true) - + '
' + - '' + EALang['end'] + ' ' - + GeneralFunctions.formatDate(event.end, GlobalVariables.dateFormat, true) - + '
' - + notes - + '
' + - '
' + - '' + - '' + - '' + - '
'; - } else { - displayEdit = (GlobalVariables.user.privileges.appointments.edit == true) - ? '' : 'hide'; - displayDelete = (GlobalVariables.user.privileges.appointments.delete == true) - ? '' : 'hide'; - - html = - '' + - '' + EALang['start'] + ' ' - + GeneralFunctions.formatDate(event.start, GlobalVariables.dateFormat, true) - + '
' + - '' + EALang['end'] + ' ' - + GeneralFunctions.formatDate(event.end, GlobalVariables.dateFormat, true) - + '
' + - '' + EALang['service'] + ' ' - + event.data['service']['name'] - + '
' + - '' + EALang['provider'] + ' ' - + event.data['provider']['first_name'] + ' ' - + event.data['provider']['last_name'] - + '
' + - '' + EALang['customer'] + ' ' - + event.data['customer']['first_name'] + ' ' - + event.data['customer']['last_name'] - + '
' + - '
' + - '' + - '' + - '' + - '
'; - } - - $(jsEvent.target).popover({ - placement: 'top', - title: event.title, - content: html, - html: true, - container: 'body', - trigger: 'manual' - }); - - lastFocusedEventData = event; - $(jsEvent.target).popover('toggle'); - - // Fix popover position - if ($('.popover').length > 0) { - if ($('.popover').position().top < 200) $('.popover').css('top', '200px'); - } - } - - /** - * Calendar Event "Drop" Callback - * - * This event handler is triggered whenever the user drags and drops an event into a different position - * on the calendar. We need to update the database with this change. This is done via an ajax call. - */ - function _calendarEventDrop(event, dayDelta, minuteDelta, allDay, revertFunc, jsEvent, ui, view) { - if (GlobalVariables.user.privileges.appointments.edit == false) { - revertFunc(); - Backend.displayNotification(EALang['no_privileges_edit_appointments']); - return; - } - - if ($('#notification').is(':visible')) { - $('#notification').hide('bind'); - } - - if (event.data.is_unavailable == false) { - // Prepare appointment data. - var appointment = GeneralFunctions.clone(event.data); - - // Must delete the following because only appointment data should be provided to the ajax call. - delete appointment['customer']; - delete appointment['provider']; - delete appointment['service']; - - appointment['start_datetime'] = Date.parseExact( - appointment['start_datetime'], 'yyyy-MM-dd HH:mm:ss') - .add({ days: dayDelta, minutes: minuteDelta }) - .toString('yyyy-MM-dd HH:mm:ss'); - - appointment['end_datetime'] = Date.parseExact( - appointment['end_datetime'], 'yyyy-MM-dd HH:mm:ss') - .add({ days: dayDelta, minutes: minuteDelta }) - .toString('yyyy-MM-dd HH:mm:ss'); - - event.data['start_datetime'] = appointment['start_datetime']; - event.data['end_datetime'] = appointment['end_datetime']; - - // Define success callback function. - var successCallback = function(response) { - if (response.exceptions) { - response.exceptions = GeneralFunctions.parseExceptions(response.exceptions); - GeneralFunctions.displayMessageBox(GeneralFunctions.EXCEPTIONS_TITLE, GeneralFunctions.EXCEPTIONS_MESSAGE); - $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); - return; - } - - if (response.warnings) { - // Display warning information to the user. - response.warnings = GeneralFunctions.parseExceptions(response.warnings); - GeneralFunctions.displayMessageBox(GeneralFunctions.WARNINGS_TITLE, GeneralFunctions.WARNINGS_MESSAGE); - $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.warnings)); - } - - // Define the undo function, if the user needs to reset the last change. - var undoFunction = function() { - appointment['start_datetime'] = Date.parseExact( - appointment['start_datetime'], 'yyyy-MM-dd HH:mm:ss') - .add({ days: -dayDelta, minutes: -minuteDelta }) - .toString('yyyy-MM-dd HH:mm:ss'); - - appointment['end_datetime'] = Date.parseExact( - appointment['end_datetime'], 'yyyy-MM-dd HH:mm:ss') - .add({ days: -dayDelta, minutes: -minuteDelta }) - .toString('yyyy-MM-dd HH:mm:ss'); - - event.data['start_datetime'] = appointment['start_datetime']; - event.data['end_datetime'] = appointment['end_datetime']; - - var postUrl = GlobalVariables.baseUrl + '/index.php/backend_api/ajax_save_appointment'; - var postData = { - csrfToken: GlobalVariables.csrfToken, - appointment_data: JSON.stringify(appointment) - }; - - $.post(postUrl, postData, function(response) { - $('#notification').hide('blind'); - revertFunc(); - }, 'json').fail(GeneralFunctions.ajaxFailureHandler); - }; - - Backend.displayNotification(EALang['appointment_updated'], [ - { - 'label': 'Undo', - 'function': undoFunction - } - ]); - - $('#footer').css('position', 'static'); // Footer position fix. - }; - - // Update appointment data. - _saveAppointment(appointment, undefined, successCallback, undefined); - } else { - // Update unavailable time period. - var unavailable = { - id: event.data.id, - start_datetime: event.start.toString('yyyy-MM-dd HH:mm:ss'), - end_datetime: event.end.toString('yyyy-MM-dd HH:mm:ss'), - id_users_provider: event.data.id_users_provider - }; - - var successCallback = function(response) { - if (response.exceptions) { - response.exceptions = GeneralFunctions.parseExceptions(response.exceptions); - GeneralFunctions.displayMessageBox(GeneralFunctions.EXCEPTIONS_TITLE, GeneralFunctions.EXCEPTIONS_MESSAGE); - $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); - return; - } - - if (response.warnings) { - response.warnings = GeneralFunctions.parseExceptions(response.warnings); - GeneralFunctions.displayMessageBox(GeneralFunctions.WARNINGS_TITLE, GeneralFunctions.WARNINGS_MESSAGE); - $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.warnings)); - } - - var undoFunction = function() { - unavailable['start_datetime'] = Date.parseExact( - unavailable['start_datetime'], 'yyyy-MM-dd HH:mm:ss') - .add({ days: -dayDelta, minutes: -minuteDelta }) - .toString('yyyy-MM-dd HH:mm:ss'); - - unavailable['end_datetime'] = Date.parseExact( - unavailable['end_datetime'], 'yyyy-MM-dd HH:mm:ss') - .add({ days: -dayDelta, minutes: -minuteDelta }) - .toString('yyyy-MM-dd HH:mm:ss'); - - event.data['start_datetime'] = unavailable['start_datetime']; - event.data['end_datetime'] = unavailable['end_datetime']; - - var postUrl = GlobalVariables.baseUrl + '/index.php/backend_api/ajax_save_unavailable'; - var postData = { - csrfToken: GlobalVariables.csrfToken, - unavailable: JSON.stringify(unavailable) - }; - - $.post(postUrl, postData, function(response) { - $('#notification').hide('blind'); - revertFunc(); - }, 'json').fail(GeneralFunctions.ajaxFailureHandler); - }; - - Backend.displayNotification(EALang['unavailable_updated'], [ - { - label: 'Undo', - function: undoFunction - } - ]); - - $('#footer').css('position', 'static'); // Footer position fix. - }; - - _saveUnavailable(unavailable, successCallback, undefined); - } - } - - /** - * Calendar "View Display" Callback - * - * Whenever the calendar changes or refreshes its view certain actions need to be made, in order to - * display proper information to the user. - */ - function _calendarViewDisplay() { - if ($('#select-filter-item').val() === null) { - return; - } - - _refreshCalendarAppointments( - $('#calendar'), - $('#select-filter-item').val(), - $('#select-filter-item option:selected').attr('type'), - $('#calendar').fullCalendar('getView').visStart, - $('#calendar').fullCalendar('getView').visEnd); - - $(window).trigger('resize'); // Places the footer on the bottom. - - // Remove all open popovers. - $('.close-popover').each(function() { - $(this).parents().eq(2).remove(); - }); - - // Add new pop overs. - $('.fv-events').each(function(index, eventHandle) { - $(eventHandle).popover(); - }); - } + } /** * Disable Provider Sync @@ -1874,260 +1114,23 @@ window.BackendCalendar = window.BackendCalendar || {}; $dialog.find('#unavailable-notes').val(''); } - /** - * Convert titles to HTML - * - * On some calendar events the titles contain html markup that is not displayed properly due to the - * fullcalendar plugin. This plugin sets the .fc-event-title value by using the $.text() method and - * not the $.html() method. So in order for the title to displya the html properly we convert all the - * .fc-event-titles where needed into html. - */ - function _convertTitlesToHtml() { - // Convert the titles to html code. - $('.fc-custom').each(function() { - var title = $(this).find('.fc-event-title').text(); - $(this).find('.fc-event-title').html(title); - var time = $(this).find('.fc-event-time').text(); - $(this).find('.fc-event-time').html(time); - }); - } - /** * Initialize Module * * This function makes the necessary initialization for the default backend calendar page. If this module * is used in another page then this function might not be needed. * - * @param {Boolean} defaultEventHandlers Optional (true), Determines whether the default event handlers will be - * set for the current page. + * @param {String} view Optional (default), the calendar view to be loaded. */ - exports.initialize = function(defaultEventHandlers) { - defaultEventHandlers = defaultEventHandlers || true; - - // Dynamic Date Formats - var columnFormat = {}; - - switch(GlobalVariables.dateFormat) { - case 'DMY': - columnFormat = { - month: 'ddd', - week: 'ddd dd/MM', - day: 'dddd dd/MM' - }; - - break; - case 'MDY': - case 'YMD': - columnFormat = { - month: 'ddd', - week: 'ddd MM/dd', - day: 'dddd MM/dd' - }; - break; - default: - throw new Error('Invalid date format setting provided!', GlobalVariables.dateFormat); + exports.initialize = function(view) { + // Load and initialize the calendar view. + if (view === 'table') { + BackendCalendarTableView.initialize(); + } else { + BackendCalendarDefaultView.initialize(); } - - var defaultView = window.innerWidth < 468 ? 'agendaDay' : 'agendaWeek'; - - - // Initialize page calendar - $('#calendar').fullCalendar({ - defaultView: defaultView, - height: _getCalendarHeight(), - editable: true, - firstDay: 1, // Monday - slotMinutes: 30, - snapMinutes: 15, - axisFormat: 'HH:mm', - timeFormat: 'HH:mm{ - HH:mm}', - allDayText: EALang['all_day'], - columnFormat: columnFormat, - titleFormat: { - month: 'MMMM yyyy', - week: "MMMM d[ yyyy]{ '—'[ MMM] d, yyyy}", - day: 'dddd, MMMM d, yyyy' - }, - header: { - left: 'prev,next today', - center: 'title', - right: 'agendaDay,agendaWeek,month' - }, - - // Translations - monthNames: [EALang['january'], EALang['february'], EALang['march'], EALang['april'], - EALang['may'], EALang['june'], EALang['july'], EALang['august'], - EALang['september'],EALang['october'], EALang['november'], - EALang['december']], - monthNamesShort: [EALang['january'].substr(0,3), EALang['february'].substr(0,3), - EALang['march'].substr(0,3), EALang['april'].substr(0,3), - EALang['may'].substr(0,3), EALang['june'].substr(0,3), - EALang['july'].substr(0,3), EALang['august'].substr(0,3), - EALang['september'].substr(0,3),EALang['october'].substr(0,3), - EALang['november'].substr(0,3), EALang['december'].substr(0,3)], - dayNames: [EALang['sunday'], EALang['monday'], EALang['tuesday'], EALang['wednesday'], - EALang['thursday'], EALang['friday'], EALang['saturday']], - dayNamesShort: [EALang['sunday'].substr(0,3), EALang['monday'].substr(0,3), - EALang['tuesday'].substr(0,3), EALang['wednesday'].substr(0,3), - EALang['thursday'].substr(0,3), EALang['friday'].substr(0,3), - EALang['saturday'].substr(0,3)], - dayNamesMin: [EALang['sunday'].substr(0,2), EALang['monday'].substr(0,2), - EALang['tuesday'].substr(0,2), EALang['wednesday'].substr(0,2), - EALang['thursday'].substr(0,2), EALang['friday'].substr(0,2), - EALang['saturday'].substr(0,2)], - buttonText: { - today: EALang['today'], - day: EALang['day'], - week: EALang['week'], - month: EALang['month'] - }, - - // Calendar events need to be declared on initialization. - windowResize: _calendarWindowResize, - viewDisplay: _calendarViewDisplay, - dayClick: _calendarDayClick, - eventClick: _calendarEventClick, - eventResize: _calendarEventResize, - eventDrop: _calendarEventDrop, - eventAfterAllRender: function() { - _convertTitlesToHtml(); - } - }); - - // Trigger once to set the proper footer position after calendar initialization. - _calendarWindowResize(); - - // Fill the select listboxes of the page. - if (GlobalVariables.availableProviders.length > 0) { - var optgroupHtml = ''; - $.each(GlobalVariables.availableProviders, function(index, provider) { - var hasGoogleSync = (provider['settings']['google_sync'] === '1') - ? 'true' : 'false'; - - optgroupHtml += ''; - }); - optgroupHtml += ''; - $('#select-filter-item').append(optgroupHtml); - } - - if (GlobalVariables.availableServices.length > 0) { - optgroupHtml = ''; - $.each(GlobalVariables.availableServices, function(index, service) { - optgroupHtml += ''; - }); - optgroupHtml += ''; - $('#select-filter-item').append(optgroupHtml); - } - - // Privileges Checks - if (GlobalVariables.user.role_slug == Backend.DB_SLUG_PROVIDER) { - $('#select-filter-item optgroup:eq(0)') - .find('option[value="' + GlobalVariables.user.id + '"]').prop('selected', true); - $('#select-filter-item').prop('disabled', true); - } - - if (GlobalVariables.user.role_slug == Backend.DB_SLUG_SECRETARY) { - $('#select-filter-item optgroup:eq(1)').remove(); - } - - if (GlobalVariables.user.role_slug == Backend.DB_SLUG_SECRETARY) { - // Remove the providers that are not connected to the secretary. - $('#select-filter-item option[type="provider"]').each(function(index, option) { - var found = false; - $.each(GlobalVariables.secretaryProviders, function(index, id) { - if ($(option).val() == id) { - found = true; - return false; - } - }); - - if (!found) { - $(option).remove(); - } - }); - - if ($('#select-filter-item option[type="provider"]').length == 0) { - $('#select-filter-item optgroup[type="providers-group"]').remove(); - } - } - - // Bind the default event handlers (if needed). - if (defaultEventHandlers === true) { - _bindEventHandlers(); - $('#select-filter-item').trigger('change'); - } - - // Display the edit dialog if an appointment hash is provided. - if (GlobalVariables.editAppointment != null) { - var $dialog = $('#manage-appointment'); - var appointment = GlobalVariables.editAppointment; - _resetAppointmentDialog(); - - $dialog.find('.modal-header h3').text(EALang['edit_appointment_title']); - $dialog.find('#appointment-id').val(appointment['id']); - $dialog.find('#select-service').val(appointment['id_services']).change(); - $dialog.find('#select-provider').val(appointment['id_users_provider']); - - // Set the start and end datetime of the appointment. - var startDatetime = Date.parseExact(appointment['start_datetime'], - 'yyyy-MM-dd HH:mm:ss'); - $dialog.find('#start-datetime').val(GeneralFunctions.formatDate(startDatetime, GlobalVariables.dateFormat, true)); - - var endDatetime = Date.parseExact(appointment['end_datetime'], - 'yyyy-MM-dd HH:mm:ss'); - $dialog.find('#end-datetime').val(GeneralFunctions.formatDate(endDatetime, GlobalVariables.dateFormat, true)); - - var customer = appointment['customer']; - $dialog.find('#customer-id').val(appointment['id_users_customer']); - $dialog.find('#first-name').val(customer['first_name']); - $dialog.find('#last-name').val(customer['last_name']); - $dialog.find('#email').val(customer['email']); - $dialog.find('#phone-number').val(customer['phone_number']); - $dialog.find('#address').val(customer['address']); - $dialog.find('#city').val(customer['city']); - $dialog.find('#zip-code').val(customer['zip_code']); - $dialog.find('#appointment-notes').val(appointment['notes']); - $dialog.find('#customer-notes').val(customer['notes']); - - $dialog.modal('show'); - } - - // Apply qtip to control tooltips. - $('#calendar-toolbar button').qtip({ - position: { - my: 'top center', - at: 'bottom center' - }, - style: { - classes: 'qtip-green qtip-shadow custom-qtip' - } - }); - - $('#select-filter-item').qtip({ - position: { - my: 'middle left', - at: 'middle right' - }, - style: { - classes: 'qtip-green qtip-shadow custom-qtip' - } - }); - - // Fine tune the footer's position only for this page. - if (window.innerHeight < 700) { - $('#footer').css('position', 'static'); - } - - if ($('#select-filter-item option').length == 0) { - $('#calendar-actions button').prop('disabled', true); - } + _bindEventHandlers(); // General event handling. }; })(window.BackendCalendar); diff --git a/src/assets/js/backend_calendar_default_view.js b/src/assets/js/backend_calendar_default_view.js new file mode 100644 index 00000000..4594057e --- /dev/null +++ b/src/assets/js/backend_calendar_default_view.js @@ -0,0 +1,1037 @@ +/* ---------------------------------------------------------------------------- + * Easy!Appointments - Open Source Web Scheduler + * + * @package EasyAppointments + * @author A.Tselegidis + * @copyright Copyright (c) 2013 - 2016, Alex Tselegidis + * @license http://opensource.org/licenses/GPL-3.0 - GPLv3 + * @link http://easyappointments.org + * @since v1.2.0 + * ---------------------------------------------------------------------------- */ + +/** + * Backend Calendar + * + * This module implements the default calendar view of backend. + * + * @module BackendCalendarDefaultView + */ +window.BackendCalendarDefaultView = window.BackendCalendarDefaultView || {}; + +(function(exports) { + + 'use strict'; + + // Constants + var FILTER_TYPE_PROVIDER = 'provider'; + var FILTER_TYPE_SERVICE = 'service'; + + /** + * Bind event handlers for the calendar view. + */ + function _bindEventHandlers() { + /** + * Event: Calendar Filter Item "Change" + * + * Load the appointments that correspond to the select filter item and display them on the calendar. + */ + $('#select-filter-item').change(function() { + _refreshCalendarAppointments( + $('#calendar'), + $('#select-filter-item').val(), + $('#select-filter-item option:selected').attr('type'), + $('#calendar').fullCalendar('getView').visStart, + $('#calendar').fullCalendar('getView').visEnd); + + // If current value is service, then the sync buttons must be disabled. + if ($('#select-filter-item option:selected').attr('type') === FILTER_TYPE_SERVICE) { + $('#google-sync, #enable-sync, #insert-appointment, #insert-unavailable').prop('disabled', true); + } else { + $('#google-sync, #enable-sync, #insert-appointment, #insert-unavailable').prop('disabled', false); + + // If the user has already the sync enabled then apply the proper style changes. + if ($('#select-filter-item option:selected').attr('google-sync') === 'true') { + $('#enable-sync').addClass('btn-danger enabled'); + $('#enable-sync span:eq(1)').text(EALang['disable_sync']); + $('#google-sync').prop('disabled', false); + } else { + $('#enable-sync').removeClass('btn-danger enabled'); + $('#enable-sync span:eq(1)').text(EALang['enable_sync']); + $('#google-sync').prop('disabled', true); + } + } + }); + } + + /** + * Get Calendar Component Height + * + * This method calculates the proper calendar height, in order to be displayed correctly, even when the + * browser window is resizing. + * + * @return {Number} Returns the calendar element height in pixels. + */ + function _getCalendarHeight() { + var result = window.innerHeight - $('#footer').outerHeight() - $('#header').outerHeight() + - $('#calendar-toolbar').outerHeight() - 60; // 60 for fine tuning + return (result > 500) ? result : 500; // Minimum height is 500px + } + + /** + * Calendar Event "Click" Callback + * + * When the user clicks on an appointment object on the calendar, then a data preview popover is display + * above the calendar item. + */ + function _calendarEventClick(event, jsEvent, view) { + $('.popover').remove(); // Close all open popovers. + + var html; + var displayEdit; + var displayDelete; + + // Depending where the user clicked the event (title or empty space) we + // need to use different selectors to reach the parent element. + var $parent = $(jsEvent.target.offsetParent); + var $altParent = $(jsEvent.target).parents().eq(1); + + if ($parent.hasClass('fc-unavailable') || $altParent.hasClass('fc-unavailable')) { + displayEdit = (($parent.hasClass('fc-custom') || $altParent.hasClass('fc-custom')) + && GlobalVariables.user.privileges.appointments.edit == true) + ? '' : 'hide'; + displayDelete = (($parent.hasClass('fc-custom') || $altParent.hasClass('fc-custom')) + && GlobalVariables.user.privileges.appointments.delete == true) + ? '' : 'hide'; // Same value at the time. + + var notes = ''; + if (event.data) { // Only custom unavailable periods have notes. + notes = 'Notes ' + event.data.notes; + } + + html = + '' + + '' + EALang['start'] + ' ' + + GeneralFunctions.formatDate(event.start, GlobalVariables.dateFormat, true) + + '
' + + '' + EALang['end'] + ' ' + + GeneralFunctions.formatDate(event.end, GlobalVariables.dateFormat, true) + + '
' + + notes + + '
' + + '
' + + '' + + '' + + '' + + '
'; + } else { + displayEdit = (GlobalVariables.user.privileges.appointments.edit == true) + ? '' : 'hide'; + displayDelete = (GlobalVariables.user.privileges.appointments.delete == true) + ? '' : 'hide'; + + html = + '' + + '' + EALang['start'] + ' ' + + GeneralFunctions.formatDate(event.start, GlobalVariables.dateFormat, true) + + '
' + + '' + EALang['end'] + ' ' + + GeneralFunctions.formatDate(event.end, GlobalVariables.dateFormat, true) + + '
' + + '' + EALang['service'] + ' ' + + event.data['service']['name'] + + '
' + + '' + EALang['provider'] + ' ' + + event.data['provider']['first_name'] + ' ' + + event.data['provider']['last_name'] + + '
' + + '' + EALang['customer'] + ' ' + + event.data['customer']['first_name'] + ' ' + + event.data['customer']['last_name'] + + '
' + + '
' + + '' + + '' + + '' + + '
'; + } + + $(jsEvent.target).popover({ + placement: 'top', + title: event.title, + content: html, + html: true, + container: 'body', + trigger: 'manual' + }); + + lastFocusedEventData = event; + $(jsEvent.target).popover('toggle'); + + // Fix popover position + if ($('.popover').length > 0) { + if ($('.popover').position().top < 200) $('.popover').css('top', '200px'); + } + } + + /** + * Calendar Event "Resize" Callback + * + * The user can change the duration of an event by resizing an appointment object on the calendar. This + * change needs to be stored to the database too and this is done via an ajax call. + * + * @see updateAppointmentData() + */ + function _calendarEventResize(event, dayDelta, minuteDelta, revertFunc, jsEvent, ui, view) { + if (GlobalVariables.user.privileges.appointments.edit == false) { + revertFunc(); + Backend.displayNotification(EALang['no_privileges_edit_appointments']); + return; + } + + if ($('#notification').is(':visible')) { + $('#notification').hide('bind'); + } + + if (event.data.is_unavailable == false) { + // Prepare appointment data. + var appointment = GeneralFunctions.clone(event.data); + + // Must delete the following because only appointment data should be provided to the ajax call. + delete appointment['customer']; + delete appointment['provider']; + delete appointment['service']; + + appointment['end_datetime'] = Date.parseExact( + appointment['end_datetime'], 'yyyy-MM-dd HH:mm:ss') + .add({ minutes: minuteDelta }) + .toString('yyyy-MM-dd HH:mm:ss'); + + // Success callback + var successCallback = function(response) { + if (response.exceptions) { + response.exceptions = GeneralFunctions.parseExceptions(response.exceptions); + GeneralFunctions.displayMessageBox(GeneralFunctions.EXCEPTIONS_TITLE, GeneralFunctions.EXCEPTIONS_MESSAGE); + $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); + return; + } + + if (response.warnings) { + // Display warning information to the user. + response.warnings = GeneralFunctions.parseExceptions(response.warnings); + GeneralFunctions.displayMessageBox(GeneralFunctions.WARNINGS_TITLE, GeneralFunctions.WARNINGS_MESSAGE); + $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.warnings)); + } + + // Display success notification to user. + var undoFunction = function() { + appointment['end_datetime'] = Date.parseExact( + appointment['end_datetime'], 'yyyy-MM-dd HH:mm:ss') + .add({ minutes: -minuteDelta }) + .toString('yyyy-MM-dd HH:mm:ss'); + + var postUrl = GlobalVariables.baseUrl + '/index.php/backend_api/ajax_save_appointment'; + var postData = { + csrfToken: GlobalVariables.csrfToken, + appointment_data: JSON.stringify(appointment) + }; + + $.post(postUrl, postData, function(response) { + $('#notification').hide('blind'); + revertFunc(); + }, 'json').fail(GeneralFunctions.ajaxFailureHandler); + }; + + Backend.displayNotification(EALang['appointment_updated'], [ + { + 'label': 'Undo', + 'function': undoFunction + } + ]); + $('#footer').css('position', 'static'); // Footer position fix. + }; + + // Update appointment data. + _saveAppointment(appointment, undefined, successCallback, undefined); + } else { + // Update unvailable time period. + var unavailable = { + id: event.data.id, + start_datetime: event.start.toString('yyyy-MM-dd HH:mm:ss'), + end_datetime: event.end.toString('yyyy-MM-dd HH:mm:ss'), + id_users_provider: event.data.id_users_provider + }; + + // Define success callback function. + var successCallback = function(response) { + if (response.exceptions) { + response.exceptions = GeneralFunctions.parseExceptions(response.exceptions); + GeneralFunctions.displayMessageBox(GeneralFunctions.EXCEPTIONS_TITLE, GeneralFunctions.EXCEPTIONS_MESSAGE); + $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); + return; + } + + if (response.warnings) { + // Display warning information to the user. + response.warnings = GeneralFunctions.parseExceptions(response.warnings); + GeneralFunctions.displayMessageBox(GeneralFunctions.WARNINGS_TITLE, GeneralFunctions.WARNINGS_MESSAGE); + $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.warnings)); + } + + // Display success notification to user. + var undoFunction = function() { + unavailable['end_datetime'] = Date.parseExact( + unavailable['end_datetime'], 'yyyy-MM-dd HH:mm:ss') + .add({ minutes: -minuteDelta }) + .toString('yyyy-MM-dd HH:mm:ss'); + + var postUrl = GlobalVariables.baseUrl + '/index.php/backend_api/ajax_save_unavailable'; + var postData = { + csrfToken: GlobalVariables.csrfToken, + unavailable: JSON.stringify(unavailable) + }; + + $.post(postUrl, postData, function(response) { + $('#notification').hide('blind'); + revertFunc(); + }, 'json').fail(GeneralFunctions.ajaxFailureHandler); + }; + + Backend.displayNotification(EALang['unavailable_updated'], [ + { + 'label': 'Undo', + 'function': undoFunction + } + ]); + $('#footer').css('position', 'static'); // Footer position fix. + }; + + _saveUnavailable(unavailable, successCallback, undefined); + } + } + + /** + * Calendar Window "Resize" Callback + * + * The calendar element needs to be resized too in order to fit into the window. Nevertheless, if the window + * becomes very small the the calendar won't shrink anymore. + * + * @see _getCalendarHeight() + */ + function _calendarWindowResize(view) { + $('#calendar').fullCalendar('option', 'height', _getCalendarHeight()); + } + + /** + * Calendar Day "Click" Callback + * + * When the user clicks on a day square on the calendar, then he will automatically be transfered to that + * day view calendar. + */ + function _calendarDayClick(date, allDay, jsEvent, view) { + if (allDay) { + $('#calendar').fullCalendar('gotoDate', date); + $('#calendar').fullCalendar('changeView', 'agendaDay'); + } + } + + /** + * Calendar Event "Drop" Callback + * + * This event handler is triggered whenever the user drags and drops an event into a different position + * on the calendar. We need to update the database with this change. This is done via an ajax call. + */ + function _calendarEventDrop(event, dayDelta, minuteDelta, allDay, revertFunc, jsEvent, ui, view) { + if (GlobalVariables.user.privileges.appointments.edit == false) { + revertFunc(); + Backend.displayNotification(EALang['no_privileges_edit_appointments']); + return; + } + + if ($('#notification').is(':visible')) { + $('#notification').hide('bind'); + } + + if (event.data.is_unavailable == false) { + // Prepare appointment data. + var appointment = GeneralFunctions.clone(event.data); + + // Must delete the following because only appointment data should be provided to the ajax call. + delete appointment['customer']; + delete appointment['provider']; + delete appointment['service']; + + appointment['start_datetime'] = Date.parseExact( + appointment['start_datetime'], 'yyyy-MM-dd HH:mm:ss') + .add({ days: dayDelta, minutes: minuteDelta }) + .toString('yyyy-MM-dd HH:mm:ss'); + + appointment['end_datetime'] = Date.parseExact( + appointment['end_datetime'], 'yyyy-MM-dd HH:mm:ss') + .add({ days: dayDelta, minutes: minuteDelta }) + .toString('yyyy-MM-dd HH:mm:ss'); + + event.data['start_datetime'] = appointment['start_datetime']; + event.data['end_datetime'] = appointment['end_datetime']; + + // Define success callback function. + var successCallback = function(response) { + if (response.exceptions) { + response.exceptions = GeneralFunctions.parseExceptions(response.exceptions); + GeneralFunctions.displayMessageBox(GeneralFunctions.EXCEPTIONS_TITLE, GeneralFunctions.EXCEPTIONS_MESSAGE); + $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); + return; + } + + if (response.warnings) { + // Display warning information to the user. + response.warnings = GeneralFunctions.parseExceptions(response.warnings); + GeneralFunctions.displayMessageBox(GeneralFunctions.WARNINGS_TITLE, GeneralFunctions.WARNINGS_MESSAGE); + $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.warnings)); + } + + // Define the undo function, if the user needs to reset the last change. + var undoFunction = function() { + appointment['start_datetime'] = Date.parseExact( + appointment['start_datetime'], 'yyyy-MM-dd HH:mm:ss') + .add({ days: -dayDelta, minutes: -minuteDelta }) + .toString('yyyy-MM-dd HH:mm:ss'); + + appointment['end_datetime'] = Date.parseExact( + appointment['end_datetime'], 'yyyy-MM-dd HH:mm:ss') + .add({ days: -dayDelta, minutes: -minuteDelta }) + .toString('yyyy-MM-dd HH:mm:ss'); + + event.data['start_datetime'] = appointment['start_datetime']; + event.data['end_datetime'] = appointment['end_datetime']; + + var postUrl = GlobalVariables.baseUrl + '/index.php/backend_api/ajax_save_appointment'; + var postData = { + csrfToken: GlobalVariables.csrfToken, + appointment_data: JSON.stringify(appointment) + }; + + $.post(postUrl, postData, function(response) { + $('#notification').hide('blind'); + revertFunc(); + }, 'json').fail(GeneralFunctions.ajaxFailureHandler); + }; + + Backend.displayNotification(EALang['appointment_updated'], [ + { + 'label': 'Undo', + 'function': undoFunction + } + ]); + + $('#footer').css('position', 'static'); // Footer position fix. + }; + + // Update appointment data. + _saveAppointment(appointment, undefined, successCallback, undefined); + } else { + // Update unavailable time period. + var unavailable = { + id: event.data.id, + start_datetime: event.start.toString('yyyy-MM-dd HH:mm:ss'), + end_datetime: event.end.toString('yyyy-MM-dd HH:mm:ss'), + id_users_provider: event.data.id_users_provider + }; + + var successCallback = function(response) { + if (response.exceptions) { + response.exceptions = GeneralFunctions.parseExceptions(response.exceptions); + GeneralFunctions.displayMessageBox(GeneralFunctions.EXCEPTIONS_TITLE, GeneralFunctions.EXCEPTIONS_MESSAGE); + $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.exceptions)); + return; + } + + if (response.warnings) { + response.warnings = GeneralFunctions.parseExceptions(response.warnings); + GeneralFunctions.displayMessageBox(GeneralFunctions.WARNINGS_TITLE, GeneralFunctions.WARNINGS_MESSAGE); + $('#message_box').append(GeneralFunctions.exceptionsToHtml(response.warnings)); + } + + var undoFunction = function() { + unavailable['start_datetime'] = Date.parseExact( + unavailable['start_datetime'], 'yyyy-MM-dd HH:mm:ss') + .add({ days: -dayDelta, minutes: -minuteDelta }) + .toString('yyyy-MM-dd HH:mm:ss'); + + unavailable['end_datetime'] = Date.parseExact( + unavailable['end_datetime'], 'yyyy-MM-dd HH:mm:ss') + .add({ days: -dayDelta, minutes: -minuteDelta }) + .toString('yyyy-MM-dd HH:mm:ss'); + + event.data['start_datetime'] = unavailable['start_datetime']; + event.data['end_datetime'] = unavailable['end_datetime']; + + var postUrl = GlobalVariables.baseUrl + '/index.php/backend_api/ajax_save_unavailable'; + var postData = { + csrfToken: GlobalVariables.csrfToken, + unavailable: JSON.stringify(unavailable) + }; + + $.post(postUrl, postData, function(response) { + $('#notification').hide('blind'); + revertFunc(); + }, 'json').fail(GeneralFunctions.ajaxFailureHandler); + }; + + Backend.displayNotification(EALang['unavailable_updated'], [ + { + label: 'Undo', + function: undoFunction + } + ]); + + $('#footer').css('position', 'static'); // Footer position fix. + }; + + _saveUnavailable(unavailable, successCallback, undefined); + } + } + + /** + * Calendar "View Display" Callback + * + * Whenever the calendar changes or refreshes its view certain actions need to be made, in order to + * display proper information to the user. + */ + function _calendarViewDisplay() { + if ($('#select-filter-item').val() === null) { + return; + } + + _refreshCalendarAppointments( + $('#calendar'), + $('#select-filter-item').val(), + $('#select-filter-item option:selected').attr('type'), + $('#calendar').fullCalendar('getView').visStart, + $('#calendar').fullCalendar('getView').visEnd); + + $(window).trigger('resize'); // Places the footer on the bottom. + + // Remove all open popovers. + $('.close-popover').each(function() { + $(this).parents().eq(2).remove(); + }); + + // Add new pop overs. + $('.fv-events').each(function(index, eventHandle) { + $(eventHandle).popover(); + }); + } + + /** + * Convert titles to HTML + * + * On some calendar events the titles contain html markup that is not displayed properly due to the + * fullcalendar plugin. This plugin sets the .fc-event-title value by using the $.text() method and + * not the $.html() method. So in order for the title to displya the html properly we convert all the + * .fc-event-titles where needed into html. + */ + function _convertTitlesToHtml() { + // Convert the titles to html code. + $('.fc-custom').each(function() { + var title = $(this).find('.fc-event-title').text(); + $(this).find('.fc-event-title').html(title); + var time = $(this).find('.fc-event-time').text(); + $(this).find('.fc-event-time').html(time); + }); + } + + /** + * Refresh Calendar Appointments + * + * This method reloads the registered appointments for the selected date period and filter type. + * + * @param {Object} $calendar The calendar jQuery object. + * @param {Number} recordId The selected record id. + * @param {String} filterType The filter type, could be either FILTER_TYPE_PROVIDER or FILTER_TYPE_SERVICE. + * @param {Date} startDate Visible start date of the calendar. + * @param {Date} endDate Visible end date of the calendar. + */ + function _refreshCalendarAppointments($calendar, recordId, filterType, startDate, endDate) { + var postUrl = GlobalVariables.baseUrl + '/index.php/backend_api/ajax_get_calendar_appointments'; + var postData = { + csrfToken: GlobalVariables.csrfToken, + record_id: recordId, + start_date: startDate.toString('yyyy-MM-dd'), + end_date: endDate.toString('yyyy-MM-dd'), + filter_type: filterType + }; + + $.post(postUrl, postData, function(response) { + if (!GeneralFunctions.handleAjaxExceptions(response)) { + return; + } + + // Add appointments to calendar. + var calendarEvents = []; + var $calendar = $('#calendar'); + + $.each(response.appointments, function(index, appointment) { + var event = { + id: appointment['id'], + title: appointment['service']['name'] + ' - ' + + appointment['customer']['first_name'] + ' ' + + appointment['customer']['last_name'], + start: appointment['start_datetime'], + end: appointment['end_datetime'], + allDay: false, + data: appointment // Store appointment data for later use. + }; + + calendarEvents.push(event); + }); + + $calendar.fullCalendar('removeEvents'); + $calendar.fullCalendar('addEventSource', calendarEvents); + + // :: ADD PROVIDER'S UNAVAILABLE TIME PERIODS + var calendarView = $calendar.fullCalendar('getView').name; + + if (filterType === FILTER_TYPE_PROVIDER && calendarView !== 'month') { + $.each(GlobalVariables.availableProviders, function(index, provider) { + if (provider['id'] == recordId) { + var workingPlan = jQuery.parseJSON(provider.settings.working_plan); + var unavailablePeriod; + + switch(calendarView) { + case 'agendaDay': + var selDayName = $calendar.fullCalendar('getView') + .start.toString('dddd').toLowerCase(); + + // Add custom unavailable periods. + $.each(response.unavailables, function(index, unavailable) { + var unavailablePeriod = { + title: EALang['unavailable'] + '
' + ((unavailable.notes.length > 30) + ? unavailable.notes.substring(0, 30) + '...' + : unavailable.notes) + '', + start: Date.parse(unavailable.start_datetime), + end: Date.parse(unavailable.end_datetime), + allDay: false, + color: '#879DB4', + editable: true, + className: 'fc-unavailable fc-custom', + data: unavailable + }; + $calendar.fullCalendar('renderEvent', unavailablePeriod, false); + }); + + // Non-working day + if (workingPlan[selDayName] == null) { + unavailablePeriod = { + title: EALang['not_working'], + start: GeneralFunctions.clone($calendar.fullCalendar('getView').start), + end: GeneralFunctions.clone($calendar.fullCalendar('getView').end), + allDay: false, + color: '#BEBEBE', + editable: false, + className: 'fc-unavailable' + }; + $calendar.fullCalendar('renderEvent', unavailablePeriod, true); + return; // Go to next loop + } + + // Add unavailable period before work starts. + var calendarDateStart = $calendar.fullCalendar('getView').start, + workDateStart = Date.parseExact( + calendarDateStart.toString('dd/MM/yyyy') + ' ' + + workingPlan[selDayName].start, + 'dd/MM/yyyy HH:mm'); + + if (calendarDateStart < workDateStart) { + unavailablePeriod = { + title: EALang['not_working'], + start: calendarDateStart, + end: workDateStart, + allDay: false, + color: '#BEBEBE', + editable: false, + className: 'fc-unavailable' + }; + $calendar.fullCalendar('renderEvent', unavailablePeriod, false); + } + + // Add unavailable period after work ends. + var calendarDateEnd = $calendar.fullCalendar('getView').end; + var workDateEnd = Date.parseExact( + calendarDateStart.toString('dd/MM/yyyy') + ' ' + + workingPlan[selDayName].end, + 'dd/MM/yyyy HH:mm'); // Use calendarDateStart *** + if (calendarDateEnd > workDateEnd) { + var unavailablePeriod = { + title: EALang['not_working'], + start: workDateEnd, + end: calendarDateEnd, + allDay: false, + color: '#BEBEBE', + editable: false, + className: 'fc-unavailable' + }; + $calendar.fullCalendar('renderEvent', unavailablePeriod, false); + } + + // Add unavailable periods for breaks. + var breakStart; + var breakEnd; + $.each(workingPlan[selDayName].breaks, function(index, currBreak) { + breakStart = Date.parseExact(calendarDateStart.toString('dd/MM/yyyy') + + ' ' + currBreak.start, 'dd/MM/yyyy HH:mm'); + breakEnd = Date.parseExact(calendarDateStart.toString('dd/MM/yyyy') + + ' ' + currBreak.end, 'dd/MM/yyyy HH:mm'); + var unavailablePeriod = { + title: EALang['break'], + start: breakStart, + end: breakEnd, + allDay: false, + color: '#BEBEBE', + editable: false, + className: 'fc-unavailable fc-break' + }; + $calendar.fullCalendar('renderEvent', unavailablePeriod, false); + }); + + break; + + case 'agendaWeek': + var currDateStart = GeneralFunctions.clone($calendar.fullCalendar('getView').start); + var currDateEnd = GeneralFunctions.clone(currDateStart).addDays(1); + + // Add custom unavailable periods (they are always displayed on the calendar, even if + // the provider won't work on that day). + $.each(response.unavailables, function(index, unavailable) { + unavailablePeriod = { + title: EALang['unavailable'] + '
' + ((unavailable.notes.length > 30) + ? unavailable.notes.substring(0, 30) + '...' + : unavailable.notes) + '', + start: Date.parse(unavailable.start_datetime), + end: Date.parse(unavailable.end_datetime), + allDay: false, + color: '#879DB4', + editable: true, + className: 'fc-unavailable fc-custom', + data: unavailable + }; + $calendar.fullCalendar('renderEvent', unavailablePeriod, false); + }); + + $.each(workingPlan, function(index, workingDay) { + + if (workingDay == null) { + // Add a full day unavailable event. + unavailablePeriod = { + title: EALang['not_working'], + start: GeneralFunctions.clone(currDateStart), + end: GeneralFunctions.clone(currDateEnd), + allDay: false, + color: '#BEBEBE', + editable: false, + className: 'fc-unavailable' + }; + $calendar.fullCalendar('renderEvent', unavailablePeriod, true); + currDateStart.addDays(1); + currDateEnd.addDays(1); + return; // Go to the next loop. + } + + var start; + var end; + + // Add unavailable period before work starts. + start = Date.parseExact(currDateStart.toString('dd/MM/yyyy') + + ' ' + workingDay.start, 'dd/MM/yyyy HH:mm'); + if (currDateStart < start) { + unavailablePeriod = { + title: EALang['not_working'], + start: GeneralFunctions.clone(currDateStart), + end: GeneralFunctions.clone(start), + allDay: false, + color: '#BEBEBE', + editable: false, + className: 'fc-unavailable' + }; + $calendar.fullCalendar('renderEvent', unavailablePeriod, true); + } + + // Add unavailable period after work ends. + end = Date.parseExact(currDateStart.toString('dd/MM/yyyy') + + ' ' + workingDay.end, 'dd/MM/yyyy HH:mm'); + if (currDateEnd > end) { + unavailablePeriod = { + title: EALang['not_working'], + start: GeneralFunctions.clone(end), + end: GeneralFunctions.clone(currDateEnd), + allDay: false, + color: '#BEBEBE', + editable: false, + className: 'fc-unavailable fc-brake' + }; + $calendar.fullCalendar('renderEvent', unavailablePeriod, false); + } + + // Add unavailable periods during day breaks. + var breakStart; + var breakEnd; + $.each(workingDay.breaks, function(index, currBreak) { + breakStart = Date.parseExact(currDateStart.toString('dd/MM/yyyy') + + ' ' + currBreak.start, 'dd/MM/yyyy HH:mm'); + breakEnd = Date.parseExact(currDateStart.toString('dd/MM/yyyy') + + ' ' + currBreak.end, 'dd/MM/yyyy HH:mm'); + var unavailablePeriod = { + title: EALang['break'], + start: breakStart, + end: breakEnd, + allDay: false, + color: '#BEBEBE', + editable: false, + className: 'fc-unavailable fc-break' + }; + $calendar.fullCalendar('renderEvent', unavailablePeriod, false); + }); + + currDateStart.addDays(1); + currDateEnd.addDays(1); + }); + break; + } + } + }); + } + }, 'json').fail(GeneralFunctions.ajaxFailureHandler); + } + + + exports.initialize = function() { + // Dynamic Date Formats + var columnFormat = {}; + + switch(GlobalVariables.dateFormat) { + case 'DMY': + columnFormat = { + month: 'ddd', + week: 'ddd dd/MM', + day: 'dddd dd/MM' + }; + + break; + case 'MDY': + case 'YMD': + columnFormat = { + month: 'ddd', + week: 'ddd MM/dd', + day: 'dddd MM/dd' + }; + break; + default: + throw new Error('Invalid date format setting provided!', GlobalVariables.dateFormat); + } + + + var defaultView = window.innerWidth < 468 ? 'agendaDay' : 'agendaWeek'; + + + // Initialize page calendar + $('#calendar').fullCalendar({ + defaultView: defaultView, + height: _getCalendarHeight(), + editable: true, + firstDay: 1, // Monday + slotMinutes: 30, + snapMinutes: 15, + axisFormat: 'HH:mm', + timeFormat: 'HH:mm{ - HH:mm}', + allDayText: EALang['all_day'], + columnFormat: columnFormat, + titleFormat: { + month: 'MMMM yyyy', + week: "MMMM d[ yyyy]{ '—'[ MMM] d, yyyy}", + day: 'dddd, MMMM d, yyyy' + }, + header: { + left: 'prev,next today', + center: 'title', + right: 'agendaDay,agendaWeek,month' + }, + + // Translations + monthNames: [EALang['january'], EALang['february'], EALang['march'], EALang['april'], + EALang['may'], EALang['june'], EALang['july'], EALang['august'], + EALang['september'],EALang['october'], EALang['november'], + EALang['december']], + monthNamesShort: [EALang['january'].substr(0,3), EALang['february'].substr(0,3), + EALang['march'].substr(0,3), EALang['april'].substr(0,3), + EALang['may'].substr(0,3), EALang['june'].substr(0,3), + EALang['july'].substr(0,3), EALang['august'].substr(0,3), + EALang['september'].substr(0,3),EALang['october'].substr(0,3), + EALang['november'].substr(0,3), EALang['december'].substr(0,3)], + dayNames: [EALang['sunday'], EALang['monday'], EALang['tuesday'], EALang['wednesday'], + EALang['thursday'], EALang['friday'], EALang['saturday']], + dayNamesShort: [EALang['sunday'].substr(0,3), EALang['monday'].substr(0,3), + EALang['tuesday'].substr(0,3), EALang['wednesday'].substr(0,3), + EALang['thursday'].substr(0,3), EALang['friday'].substr(0,3), + EALang['saturday'].substr(0,3)], + dayNamesMin: [EALang['sunday'].substr(0,2), EALang['monday'].substr(0,2), + EALang['tuesday'].substr(0,2), EALang['wednesday'].substr(0,2), + EALang['thursday'].substr(0,2), EALang['friday'].substr(0,2), + EALang['saturday'].substr(0,2)], + buttonText: { + today: EALang['today'], + day: EALang['day'], + week: EALang['week'], + month: EALang['month'] + }, + + // Calendar events need to be declared on initialization. + windowResize: _calendarWindowResize, + viewDisplay: _calendarViewDisplay, + dayClick: _calendarDayClick, + eventClick: _calendarEventClick, + eventResize: _calendarEventResize, + eventDrop: _calendarEventDrop, + eventAfterAllRender: function() { + _convertTitlesToHtml(); + } + }); + + // Trigger once to set the proper footer position after calendar initialization. + _calendarWindowResize(); + + // Fill the select listboxes of the page. + if (GlobalVariables.availableProviders.length > 0) { + var optgroupHtml = ''; + $.each(GlobalVariables.availableProviders, function(index, provider) { + var hasGoogleSync = (provider['settings']['google_sync'] === '1') + ? 'true' : 'false'; + + optgroupHtml += ''; + }); + optgroupHtml += ''; + $('#select-filter-item').append(optgroupHtml); + } + + if (GlobalVariables.availableServices.length > 0) { + optgroupHtml = ''; + $.each(GlobalVariables.availableServices, function(index, service) { + optgroupHtml += ''; + }); + optgroupHtml += ''; + $('#select-filter-item').append(optgroupHtml); + } + + // Privileges Checks + if (GlobalVariables.user.role_slug == Backend.DB_SLUG_PROVIDER) { + $('#select-filter-item optgroup:eq(0)') + .find('option[value="' + GlobalVariables.user.id + '"]').prop('selected', true); + $('#select-filter-item').prop('disabled', true); + } + + if (GlobalVariables.user.role_slug == Backend.DB_SLUG_SECRETARY) { + $('#select-filter-item optgroup:eq(1)').remove(); + } + + if (GlobalVariables.user.role_slug == Backend.DB_SLUG_SECRETARY) { + // Remove the providers that are not connected to the secretary. + $('#select-filter-item option[type="provider"]').each(function(index, option) { + var found = false; + $.each(GlobalVariables.secretaryProviders, function(index, id) { + if ($(option).val() == id) { + found = true; + return false; + } + }); + + if (!found) { + $(option).remove(); + } + }); + + if ($('#select-filter-item option[type="provider"]').length == 0) { + $('#select-filter-item optgroup[type="providers-group"]').remove(); + } + } + + // Bind the default event handlers. + _bindEventHandlers(); + $('#select-filter-item').trigger('change'); + + // Display the edit dialog if an appointment hash is provided. + if (GlobalVariables.editAppointment != null) { + var $dialog = $('#manage-appointment'); + var appointment = GlobalVariables.editAppointment; + _resetAppointmentDialog(); + + $dialog.find('.modal-header h3').text(EALang['edit_appointment_title']); + $dialog.find('#appointment-id').val(appointment['id']); + $dialog.find('#select-service').val(appointment['id_services']).change(); + $dialog.find('#select-provider').val(appointment['id_users_provider']); + + // Set the start and end datetime of the appointment. + var startDatetime = Date.parseExact(appointment['start_datetime'], + 'yyyy-MM-dd HH:mm:ss'); + $dialog.find('#start-datetime').val(GeneralFunctions.formatDate(startDatetime, GlobalVariables.dateFormat, true)); + + var endDatetime = Date.parseExact(appointment['end_datetime'], + 'yyyy-MM-dd HH:mm:ss'); + $dialog.find('#end-datetime').val(GeneralFunctions.formatDate(endDatetime, GlobalVariables.dateFormat, true)); + + var customer = appointment['customer']; + $dialog.find('#customer-id').val(appointment['id_users_customer']); + $dialog.find('#first-name').val(customer['first_name']); + $dialog.find('#last-name').val(customer['last_name']); + $dialog.find('#email').val(customer['email']); + $dialog.find('#phone-number').val(customer['phone_number']); + $dialog.find('#address').val(customer['address']); + $dialog.find('#city').val(customer['city']); + $dialog.find('#zip-code').val(customer['zip_code']); + $dialog.find('#appointment-notes').val(appointment['notes']); + $dialog.find('#customer-notes').val(customer['notes']); + + $dialog.modal('show'); + } + + // Apply qtip to control tooltips. + $('#calendar-toolbar button').qtip({ + position: { + my: 'top center', + at: 'bottom center' + }, + style: { + classes: 'qtip-green qtip-shadow custom-qtip' + } + }); + + $('#select-filter-item').qtip({ + position: { + my: 'middle left', + at: 'middle right' + }, + style: { + classes: 'qtip-green qtip-shadow custom-qtip' + } + }); + + if ($('#select-filter-item option').length == 0) { + $('#calendar-actions button').prop('disabled', true); + } + + // Fine tune the footer's position only for this page. + if (window.innerHeight < 700) { + $('#footer').css('position', 'static'); + } + }; + +})(window.BackendCalendarDefaultView); diff --git a/src/assets/js/backend_calendar_table_view.js b/src/assets/js/backend_calendar_table_view.js new file mode 100644 index 00000000..1307c923 --- /dev/null +++ b/src/assets/js/backend_calendar_table_view.js @@ -0,0 +1,32 @@ +/* ---------------------------------------------------------------------------- + * Easy!Appointments - Open Source Web Scheduler + * + * @package EasyAppointments + * @author A.Tselegidis + * @copyright Copyright (c) 2013 - 2016, Alex Tselegidis + * @license http://opensource.org/licenses/GPL-3.0 - GPLv3 + * @link http://easyappointments.org + * @since v1.2.0 + * ---------------------------------------------------------------------------- */ + +/** + * Backend Calendar + * + * This module implements the table calendar view of backend. + * + * @module BackendCalendarTableView + */ +window.BackendCalendarTableView = window.BackendCalendarTableView || {}; + +(function(exports) { + 'use strict'; + + function _bindEventHandlers() { + + } + + exports.initialize = function() { + + }; + +})(window.BackendCalendarTableView);