/* ---------------------------------------------------------------------------- * Easy!Appointments - Online Appointment Scheduler * * @package EasyAppointments * @author A.Tselegidis <alextselegidis@gmail.com> * @copyright Copyright (c) Alex Tselegidis * @license https://opensource.org/licenses/GPL-3.0 - GPLv3 * @link https://easyappointments.org * @since v1.5.0 * ---------------------------------------------------------------------------- */ /** * Working plan exceptions modal component. * * This module implements the working plan exceptions modal functionality. */ App.Components.WorkingPlanExceptionsModal = (function () { const $modal = $('#working-plan-exceptions-modal'); const $date = $('#working-plan-exceptions-date'); const $start = $('#working-plan-exceptions-start'); const $end = $('#working-plan-exceptions-end'); const $breaks = $('#working-plan-exceptions-breaks'); const $save = $('#working-plan-exceptions-save'); const $addBreak = $('.working-plan-exceptions-add-break'); const $isNonWorkingDay = $('#working-plan-exceptions-is-non-working-day'); const moment = window.moment; let deferred = null; let enableSubmit = false; let enableCancel = false; /** * Reset the modal fields back to the original empty state. */ function resetModal() { $addBreak.prop('disabled', false); $date.val(''); $start.val(''); $end.val(''); $breaks.find('tbody').html(renderNoBreaksRow()); $isNonWorkingDay.prop('checked', false); toggleFieldsByNonWorkingDay(false); } /** * Render a single table row as a placeholder to empty breaks table. */ function renderNoBreaksRow() { return $(` <tr class="no-breaks-row"> <td colspan="3" class="text-center"> ${lang('no_breaks')} </td> </tr> `); } /** * Toggle the state of the fields depending on the non-working day checkbox value. * * @param {Boolean} isNonWorkingDay */ function toggleFieldsByNonWorkingDay(isNonWorkingDay) { $start.prop('disabled', isNonWorkingDay).toggleClass('text-decoration-line-through', isNonWorkingDay); $end.prop('disabled', isNonWorkingDay).toggleClass('text-decoration-line-through', isNonWorkingDay); $addBreak.prop('disabled', isNonWorkingDay); $breaks.find('button').prop('disabled', isNonWorkingDay); $breaks.toggleClass('text-decoration-line-through', isNonWorkingDay); } /** * Validate the modal form fields and return false if the validation fails. * * @returns {Boolean} */ function validate() { $modal.find('.is-invalid').removeClass('is-invalid'); const date = App.Utils.UI.getDateTimePickerValue($date); if (!date) { $date.addClass('is-invalid'); } const start = App.Utils.UI.getDateTimePickerValue($start); if (!start) { $start.addClass('is-invalid'); } const end = App.Utils.UI.getDateTimePickerValue($end); if (!end) { $end.addClass('is-invalid'); } return !$modal.find('.is-invalid').length; } /** * Event: On Modal "Hidden" * * This event is used to automatically reset the modal back to the original state. */ function onModalHidden() { resetModal(); } /** * Serialize the entered break entries. * * @returns {Array} */ function getBreaks() { const breaks = []; $breaks .find('tbody tr') .not('.no-breaks-row') .each((index, tr) => { const $tr = $(tr); if ($tr.find('input:text').length) { return true; } const start = $tr.find('.working-plan-exceptions-break-start').text(); const end = $tr.find('.working-plan-exceptions-break-end').text(); breaks.push({ start: moment(start, vars('time_format') === 'regular' ? 'h:mm a' : 'HH:mm').format('HH:mm'), end: moment(end, vars('time_format') === 'regular' ? 'h:mm a' : 'HH:mm').format('HH:mm'), }); }); // Sort breaks increasingly by hour within day breaks.sort((break1, break2) => { // We can do a direct string comparison since we have time based on 24 hours clock. return break1.start.localeCompare(break2.start); }); return breaks; } /** * Event: On Save "Click" * * Serialize the entire working plan exception and resolved the promise so that external code can save it. */ function onSaveClick() { if (!deferred) { return; } if (!validate()) { return; } const date = moment(App.Utils.UI.getDateTimePickerValue($date)).format('YYYY-MM-DD'); const isNonWorkingDay = $isNonWorkingDay.prop('checked'); const workingPlanException = isNonWorkingDay ? null : { start: moment(App.Utils.UI.getDateTimePickerValue($start)).format('HH:mm'), end: moment(App.Utils.UI.getDateTimePickerValue($end)).format('HH:mm'), breaks: getBreaks(), }; deferred.resolve(date, workingPlanException); $modal.modal('hide'); resetModal(); } /** * Enable the inline-editable table cell functionality for the provided target element.. * * @param {jQuery} $target */ function editableTimeCell($target) { $target.editable( (value) => { // Do not return the value because the user needs to press the "Save" button. return value; }, { event: 'edit', height: '30px', submit: $('<button/>', { 'type': 'button', 'class': 'd-none submit-editable', 'text': lang('save'), }).get(0).outerHTML, cancel: $('<button/>', { 'type': 'button', 'class': 'd-none cancel-editable', 'text': lang('cancel'), }).get(0).outerHTML, onblur: 'ignore', onreset: () => { if (!enableCancel) { return false; // disable ESC button } }, onsubmit: () => { if (!enableSubmit) { return false; // disable Enter button } }, }, ); } function resetTimeSelection() { App.Utils.UI.setDateTimePickerValue($start, moment('08:00', 'HH:mm').toDate()); App.Utils.UI.setDateTimePickerValue($end, moment('20:00', 'HH:mm').toDate()); } /** * Open the modal and start adding a new working plan exception. * * @returns {*|jQuery.Deferred} */ function add() { deferred = $.Deferred(); App.Utils.UI.setDateTimePickerValue($date, new Date()); resetTimeSelection(); $isNonWorkingDay.prop('checked', false); $breaks.find('tbody').html(renderNoBreaksRow()); $modal.modal('show'); return deferred.promise(); } /** * Modify the provided working plan exception for the selected date. * * @param {String} date * @param {Object} workingPlanException * * @return {*|jQuery.Deferred} */ function edit(date, workingPlanException) { deferred = $.Deferred(); const isNonWorkingDay = !Boolean(workingPlanException); App.Utils.UI.setDateTimePickerValue($date, moment(date, 'YYYY-MM-DD').toDate()); if (isNonWorkingDay === false) { App.Utils.UI.setDateTimePickerValue($start, moment(workingPlanException.start, 'HH:mm').toDate()); App.Utils.UI.setDateTimePickerValue($end, moment(workingPlanException.end, 'HH:mm').toDate()); if (!workingPlanException.breaks) { $breaks.find('tbody').html(renderNoBreaksRow()); } workingPlanException.breaks.forEach((workingPlanExceptionBreak) => { renderBreakRow(workingPlanExceptionBreak).appendTo($breaks.find('tbody')); }); editableTimeCell( $breaks.find('tbody .working-plan-exceptions-break-start, tbody .working-plan-exceptions-break-end'), ); } else { App.Utils.UI.setDateTimePickerValue($start, moment('08:00', 'HH:mm').toDate()); App.Utils.UI.setDateTimePickerValue($end, moment('20:00', 'HH:mm').toDate()); $breaks.find('tbody').html(renderNoBreaksRow()); } $isNonWorkingDay.prop('checked', isNonWorkingDay); toggleFieldsByNonWorkingDay(isNonWorkingDay); $modal.modal('show'); return deferred.promise(); } /** * Render a break table row based on the provided break period object. * * @param {Object} breakPeriod * * @return {jQuery} */ function renderBreakRow(breakPeriod) { const timeFormat = vars('time_format') === 'regular' ? 'h:mm a' : 'HH:mm'; return $('<tr/>', { 'html': [ $('<td/>', { 'class': 'working-plan-exceptions-break-start editable', 'text': moment(breakPeriod.start, 'HH:mm').format(timeFormat), }), $('<td/>', { 'class': 'working-plan-exceptions-break-end editable', 'text': moment(breakPeriod.end, 'HH:mm').format(timeFormat), }), $('<td/>', { 'html': [ $('<button/>', { 'type': 'button', 'class': 'btn btn-outline-secondary btn-sm me-2 working-plan-exceptions-edit-break', 'title': lang('edit'), 'html': [ $('<span/>', { 'class': 'fas fa-edit', }), ], }), $('<button/>', { 'type': 'button', 'class': 'btn btn-outline-secondary btn-sm working-plan-exceptions-delete-break', 'title': lang('delete'), 'html': [ $('<span/>', { 'class': 'fas fa-trash-alt', }), ], }), $('<button/>', { 'type': 'button', 'class': 'btn btn-outline-secondary btn-sm me-2 working-plan-exceptions-save-break d-none', 'title': lang('save'), 'html': [ $('<span/>', { 'class': 'fas fa-check-circle', }), ], }), $('<button/>', { 'type': 'button', 'class': 'btn btn-outline-secondary btn-sm working-plan-exceptions-cancel-break d-none', 'title': lang('cancel'), 'html': [ $('<span/>', { 'class': 'fas fa-ban', }), ], }), ], }), ], }); } /** * Event: Add Break "Click" */ function onAddBreakClick() { const $newBreak = renderBreakRow({ start: '12:00', end: '14:00', }).appendTo('#working-plan-exceptions-breaks tbody'); // Bind editable and event handlers. editableTimeCell($newBreak.find('.working-plan-exceptions-break-start, .working-plan-exceptions-break-end')); $newBreak.find('.working-plan-exceptions-edit-break').trigger('click'); $addBreak.prop('disabled', true); } /** * Event: Edit Break "Click" */ function onEditBreakClick() { // Reset previous editable table cells. const $previousEdits = $(this).closest('table').find('.editable'); $previousEdits.each((index, editable) => { if (editable.reset) { editable.reset(); } }); // Make all cells in current row editable. let $tr = $(this).closest('tr'); $tr.children().trigger('edit'); App.Utils.UI.initializeTimePicker( $tr.find('.working-plan-exceptions-break-start input, .working-plan-exceptions-break-end input'), ); $(this).closest('tr').find('.working-plan-exceptions-break-start').focus(); // Show save - cancel buttons. $tr = $(this).closest('tr'); $tr.find('.working-plan-exceptions-edit-break, .working-plan-exceptions-delete-break').addClass('d-none'); $tr.find('.working-plan-exceptions-save-break, .working-plan-exceptions-cancel-break').removeClass('d-none'); $tr.find('select,input:text').addClass('form-control input-sm'); $addBreak.prop('disabled', true); } /** * Event: Delete Break "Click" */ function onDeleteBreakClick() { $(this).closest('tr').remove(); } /** * Event: Save Break "Click" */ function onSaveBreakClick() { // Break's start time must always be prior to break's end. const $tr = $(this).closest('tr'); const start = moment( $tr.find('.working-plan-exceptions-break-start input').val(), vars('time_format') === 'regular' ? 'h:mm a' : 'HH:mm', ); const end = moment( $tr.find('.working-plan-exceptions-break-end input').val(), vars('time_format') === 'regular' ? 'h:mm a' : 'HH:mm', ); if (start > end) { $tr.find('.working-plan-exceptions-break-end input').val( start.add(1, 'hour').format(vars('time_format') === 'regular' ? 'h:mm a' : 'HH:mm'), ); } enableSubmit = true; $tr.find('.editable .submit-editable').trigger('click'); enableSubmit = false; $tr.find('.working-plan-exceptions-save-break, .working-plan-exceptions-cancel-break').addClass('d-none'); $tr.closest('table') .find('.working-plan-exceptions-edit-break, .working-plan-exceptions-delete-break') .removeClass('d-none'); $addBreak.prop('disabled', false); } /** * Event: Cancel Break "Click" */ function onCancelBreakClick() { const $tr = $(this).closest('tr'); enableCancel = true; $tr.find('.cancel-editable').trigger('click'); enableCancel = false; $breaks .find('.working-plan-exceptions-edit-break, .working-plan-exceptions-delete-break') .removeClass('d-none'); $tr.find('.working-plan-exceptions-save-break, .working-plan-exceptions-cancel-break').addClass('d-none'); $addBreak.prop('disabled', false); } /** * Event: Is Non-Working Day "Change" */ function onIsNonWorkingDayChange() { const isNonWorkingDay = $isNonWorkingDay.prop('checked'); resetTimeSelection(); toggleFieldsByNonWorkingDay(isNonWorkingDay); } /** * Initialize the module. */ function initialize() { App.Utils.UI.initializeDatePicker($date); App.Utils.UI.initializeTimePicker($start); App.Utils.UI.initializeTimePicker($end); $modal .on('hidden.bs.modal', onModalHidden) .on('click', '.working-plan-exceptions-add-break', onAddBreakClick) .on('click', '.working-plan-exceptions-edit-break', onEditBreakClick) .on('click', '.working-plan-exceptions-delete-break', onDeleteBreakClick) .on('click', '.working-plan-exceptions-save-break', onSaveBreakClick) .on('click', '.working-plan-exceptions-cancel-break', onCancelBreakClick); $save.on('click', onSaveClick); $isNonWorkingDay.on('change', onIsNonWorkingDayChange); } document.addEventListener('DOMContentLoaded', initialize); return { add, edit, }; })();