/* eslint-disable camelcase, handle-callback-err, no-return-assign, no-undef, */ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ 'use strict' /** * Controller used in the calendar management page */ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state', '$uibModal', 'moment', 'Availability', 'Slot', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', function ($scope, $state, $uibModal, moment, Availability, Slot, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, _t, uiCalendarConfig, CalendarConfig) { /* PRIVATE STATIC CONSTANTS */ // The calendar is divided in slots of 30 minutes let loadingCb const BASE_SLOT = '00:30:00' // The bookings can be positioned every half hours const BOOKING_SNAP = '00:30:00' // We do not allow the creation of slots that are not a multiple of 60 minutes const SLOT_MULTIPLE = 60 /* PUBLIC SCOPE */ // list of the FabLab machines $scope.machines = machinesPromise // currently selected availability $scope.availability = null // bind the availabilities slots with full-Calendar events $scope.eventSources = [] $scope.eventSources.push({ url: '/api/availabilities', textColor: 'black' }) // fullCalendar (v2) configuration $scope.calendarConfig = CalendarConfig({ slotDuration: BASE_SLOT, snapDuration: BOOKING_SNAP, selectable: true, selecHelper: true, minTime: moment.duration(moment(bookingWindowStart.setting.value).format('HH:mm:ss')), maxTime: moment.duration(moment(bookingWindowEnd.setting.value).format('HH:mm:ss')), select (start, end, jsEvent, view) { return calendarSelectCb(start, end, jsEvent, view) }, eventClick (event, jsEvent, view) { return calendarEventClickCb(event, jsEvent, view) }, eventRender (event, element, view) { return eventRenderCb(event, element) }, loading (isLoading, view) { return loadingCb(isLoading, view) } }) /** * Open a confirmation modal to cancel the booking of a user for the currently selected event. * @param slot {Object} reservation slot of a user, inherited from $resource */ $scope.cancelBooking = function(slot) { // open a confirmation dialog dialogs.confirm( { resolve: { object () { return { title: _t('admin_calendar.confirmation_required'), msg: _t('admin_calendar.do_you_really_want_to_cancel_the_USER_s_reservation_the_DATE_at_TIME_concerning_RESERVATION' , { GENDER: getGender($scope.currentUser), USER: slot.user.name, DATE: moment(slot.start_at).format('L'), TIME: moment(slot.start_at).format('LT'), RESERVATION: slot.reservable.name } , 'messageformat') } } } }, function() { // the admin has confirmed, cancel the subscription Slot.cancel( { id: slot.slot_id }, function (data, status) { // success // update the canceled_at attribute for (let resa of Array.from($scope.reservations)) { if (resa.slot_id === data.id) { resa.canceled_at = data.canceled_at break } } // notify the admin return growl.success(_t('admin_calendar.reservation_was_successfully_cancelled')) }, function (data, status) { // failed growl.error(_t('admin_calendar.reservation_cancellation_failed')) } ) } ) } /** * Open a confirmation modal to remove a machine for the currently selected availability, * except if it is the last machine of the reservation. * @param machine {Object} must contain the machine ID and name */ $scope.removeMachine = function (machine) { if ($scope.availability.machine_ids.length === 1) { return growl.error(_t('admin_calendar.unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather')) } else { // open a confirmation dialog return dialogs.confirm({ resolve: { object () { return { title: _t('admin_calendar.confirmation_required'), msg: _t('admin_calendar.do_you_really_want_to_remove_MACHINE_from_this_slot', { GENDER: getGender($scope.currentUser), MACHINE: machine.name }, 'messageformat') + ' ' + _t('admin_calendar.this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing') + ' ' + _t('admin_calendar.beware_this_cannot_be_reverted') } } } } , function () { // the admin has confirmed, remove the machine const machines = $scope.availability.machine_ids for (let m_id = 0; m_id < machines.length; m_id++) { const key = machines[m_id] if (m_id === machine.id) { machines.splice(key, 1) } } return Availability.update({ id: $scope.availability.id }, { availability: { machines_attributes: [{ id: machine.id, _destroy: true }] } } , function (data, status) { // success // update the machine_ids attribute $scope.availability.machine_ids = data.machine_ids $scope.availability.title = data.title uiCalendarConfig.calendars.calendar.fullCalendar('rerenderEvents') // notify the admin return growl.success(_t('admin_calendar.the_machine_was_successfully_removed_from_the_slot')) } , function(data, status) { // failed growl.error(_t('admin_calendar.deletion_failed')) } ) }) } } /** * Callback to alert the admin that the export request was acknowledged and is * processing right now. */ $scope.alertExport = function(type) { Export.status({ category: 'availabilities', type }).then(function (res) { if (!res.data.exists) { return growl.success(_t('admin_calendar.export_is_running_you_ll_be_notified_when_its_ready')) } }) } /** * Mark the selected slot as unavailable for new reservations or allow reservations again on it */ $scope.toggleLockReservations = function () { // first, define a shortcut to the lock property const locked = $scope.availability.lock // then check if we'll allow reservations locking let prevent = !locked // if currently locked, allow unlock anyway if (!locked) { prevent = false angular.forEach($scope.reservations, function (r) { if (r.canceled_at === null) { return prevent = true } }) // if currently unlocked and has any non-cancelled reservation, disallow locking } if (!prevent) { // open a confirmation dialog dialogs.confirm( { resolve: { object () { return { title: _t('admin_calendar.confirmation_required'), msg: locked ? _t('admin_calendar.do_you_really_want_to_allow_reservations') : _t('admin_calendar.do_you_really_want_to_block_this_slot') } } } }, function() { // the admin has confirmed, lock/unlock the slot Availability.lock( { id: $scope.availability.id }, { lock: !locked }, function (data) { // success $scope.availability = data growl.success(locked ? _t('admin_calendar.unlocking_success') : _t('admin_calendar.locking_success')) uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents') }, function (error) { // failed growl.error(locked ? _t('admin_calendar.unlocking_failed') : _t('admin_calendar.locking_failed')) console.error(error) } ) } ) } else { return growl.error(_t('admin_calendar.unlockable_because_reservations')) } } /** * Confirm and destroy the slot in $scope.availability */ $scope.removeSlot = function() { // open a confirmation dialog dialogs.confirm( { resolve: { object () { return { title: _t('admin_calendar.confirmation_required'), msg: _t('admin_calendar.do_you_really_want_to_delete_this_slot') } } } }, function() { // the admin has confirmed, delete the slot Availability.delete( { id: $scope.availability.id }, function () { uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents', $scope.availability.id) growl.success(_t('admin_calendar.the_slot_START-END_has_been_successfully_deleted', { START: moment(event.start).format('LL LT'), END: moment(event.end).format('LT') })) $scope.availability = null }, function () { growl.error(_t('admin_calendar.unable_to_delete_the_slot_START-END_because_it_s_already_reserved_by_a_member', { START: moment(event.start).format('LL LT'), END: moment(event.end).format('LT') })) }) } ) } /* PRIVATE SCOPE */ /** * Return an enumerable meaninful string for the gender of the provider user * @param user {Object} Database user record * @return {string} 'male' or 'female' */ var getGender = function (user) { if (user.profile) { if (user.profile.gender === 'true') { return 'male' } else { return 'female' } } else { return 'other' } } // Triggered when the admin drag on the agenda to create a new reservable slot. // @see http://fullcalendar.io/docs/selection/select_callback/ // var calendarSelectCb = function (start, end, jsEvent, view) { start = moment.tz(start.toISOString(), Fablab.timezone) end = moment.tz(end.toISOString(), Fablab.timezone) // first we check that the selected slot is an N-hours multiple (ie. not decimal) if (Number.isInteger(parseInt((end.valueOf() - start.valueOf()) / (SLOT_MULTIPLE * 1000), 10) / SLOT_MULTIPLE)) { const today = new Date() if (parseInt((start.valueOf() - today) / (60 * 1000), 10) >= 0) { // then we open a modal window to let the admin specify the slot type const modalInstance = $uibModal.open({ templateUrl: '<%= asset_path "admin/calendar/eventModal.html" %>', controller: 'CreateEventModalController', resolve: { start () { return start }, end () { return end }, machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise }], trainingsPromise: ['Training', function (Training) { return Training.query().$promise }], spacesPromise: ['Space', function (Space) { return Space.query().$promise }] } }) // when the modal is closed, we send the slot to the server for saving modalInstance.result.then( function (availability) { uiCalendarConfig.calendars.calendar.fullCalendar( 'renderEvent', { id: availability.id, title: availability.title, start: availability.start_at, end: availability.end_at, textColor: 'black', backgroundColor: availability.backgroundColor, borderColor: availability.borderColor, tag_ids: availability.tag_ids, tags: availability.tags, machine_ids: availability.machine_ids }, true ) }, function() { uiCalendarConfig.calendars.calendar.fullCalendar('unselect') } ) } } return uiCalendarConfig.calendars.calendar.fullCalendar('unselect') } /** * Triggered when the admin clicks on a availability slot in the agenda. * @see http://fullcalendar.io/docs/mouse/eventClick/ */ var calendarEventClickCb = function (event, jsEvent, view) { $scope.availability = event // if the user has clicked on the delete event button, delete the event if ($(jsEvent.target).hasClass('remove-event')) { return $scope.removeSlot() // if the user has only clicked on the event, display its reservations } else { return Availability.reservations({ id: event.id }, function (reservations) { $scope.reservations = reservations }) } } /** * Triggered when fullCalendar tries to graphicaly render an event block. * Append the event tag into the block, just after the event title. * @see http://fullcalendar.io/docs/event_rendering/eventRender/ */ var eventRenderCb = function (event, element) { element.find('.fc-content').prepend('') if (event.tags.length > 0) { let html = '' for (let tag of Array.from(event.tags)) { html += `${tag.name} ` } element.find('.fc-title').append(`
${html}`) } // force return to prevent coffee-script auto-return to return random value (possiblity falsy) } /** * Triggered when resource fetching starts/stops. * @see https://fullcalendar.io/docs/resource_data/loading/ */ return loadingCb = function (isLoading, view) { if (isLoading) { // we remove existing events when fetching starts to prevent duplicates return uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents') } } } ]) /** * Controller used in the slot creation modal window */ Application.Controllers.controller('CreateEventModalController', ['$scope', '$uibModalInstance', 'moment', 'start', 'end', 'machinesPromise', 'Availability', 'trainingsPromise', 'spacesPromise', 'Tag', 'growl', '_t', function ($scope, $uibModalInstance, moment, start, end, machinesPromise, Availability, trainingsPromise, spacesPromise, Tag, growl, _t) { // $uibModal parameter $scope.start = start // $uibModal parameter $scope.end = end // machines list $scope.machines = machinesPromise.filter(function(m) { return !m.disabled }) // trainings list $scope.trainings = trainingsPromise.filter(function (t) { return !t.disabled }) // spaces list $scope.spaces = spacesPromise.filter(function (s) { return !s.disabled }) // machines associated with the created slot $scope.selectedMachines = [] // training associated with the created slot $scope.selectedTraining = null // space associated with the created slot $scope.selectedSpace = null // UI step $scope.step = 1 // the user is not able to edit the ending time of the availability, unless he set the type to 'training' $scope.endDateReadOnly = true // timepickers configuration $scope.timepickers = { start: { hstep: 1, mstep: 5 }, end: { hstep: 1, mstep: 5 } } // slot details $scope.availability = { start_at: start, end_at: end, available_type: 'machines' // default } /** * Adds or removes the provided machine from the current slot * @param machine {Object} */ $scope.toggleSelection = function (machine) { const index = $scope.selectedMachines.indexOf(machine) if (index > -1) { return $scope.selectedMachines.splice(index, 1) } else { return $scope.selectedMachines.push(machine) } } /** * Callback for the modal window validation: save the slot and closes the modal */ $scope.ok = function () { if ($scope.availability.available_type === 'machines') { if ($scope.selectedMachines.length > 0) { $scope.availability.machine_ids = $scope.selectedMachines.map(function (m) { return m.id }) } else { growl.error(_t('admin_calendar.you_should_select_at_least_a_machine')) return } } else if ($scope.availability.available_type === 'training') { $scope.availability.training_ids = [$scope.selectedTraining.id] } else if ($scope.availability.available_type === 'space') { $scope.availability.space_ids = [$scope.selectedSpace.id] } return Availability.save( { availability: $scope.availability } , function (availability) { $uibModalInstance.close(availability) }) } /** * Move the modal UI to the next step */ $scope.next = function () { if ($scope.step === 1) { $scope.setNbTotalPlaces() } return $scope.step++ } /** * Move the modal UI to the next step */ $scope.previous = function () { return $scope.step-- } /** * Callback to cancel the slot creation */ $scope.cancel = function () { $uibModalInstance.dismiss('cancel') } /** * For training avaiabilities, set the maximum number of people allowed to register on this slot */ $scope.setNbTotalPlaces = function () { if ($scope.availability.available_type === 'training') { return $scope.availability.nb_total_places = $scope.selectedTraining.nb_total_places } else if ($scope.availability.available_type === 'space') { return $scope.availability.nb_total_places = $scope.selectedSpace.default_places } } /* PRIVATE SCOPE */ /** * Kind of constructor: these actions will be realized first when the controller is loaded */ const initialize = function () { if ($scope.trainings.length > 0) { $scope.selectedTraining = $scope.trainings[0] } if ($scope.spaces.length > 0) { $scope.selectedSpace = $scope.spaces[0] } Tag.query().$promise.then(function (data) { $scope.tags = data }) // When we configure a machine availability, do not let the user change the end time, as the total // time must be dividable by 60 minutes (base slot duration). For training availabilities, the user // can configure any duration as it does not matters. $scope.$watch('availability.available_type', function (newValue, oldValue, scope) { if ((newValue === 'machines') || (newValue === 'space')) { $scope.endDateReadOnly = true const diff = moment($scope.end).diff($scope.start, 'hours') // the result is rounded down by moment.js $scope.end = moment($scope.start).add(diff, 'hours').toDate() return $scope.availability.end_at = $scope.end } else { return $scope.endDateReadOnly = false } }) // When the start date is changed, if we are configuring a machine availability, // maintain the relative length of the slot (ie. change the end time accordingly) $scope.$watch('start', function (newValue, oldValue, scope) { // for machine or space availabilities, adjust the end time if (($scope.availability.available_type === 'machines') || ($scope.availability.available_type === 'space')) { end = moment($scope.end) end.add(moment(newValue).diff(oldValue), 'milliseconds') $scope.end = end.toDate() } else { // for training availabilities // prevent the admin from setting the begining after the and if (moment(newValue).add(1, 'hour').isAfter($scope.end)) { $scope.start = oldValue } } // update availability object return $scope.availability.start_at = $scope.start }) // Maintain consistency between the end time and the date object in the availability object return $scope.$watch('end', function (newValue, oldValue, scope) { // we prevent the admin from setting the end of the availability before its begining if (moment($scope.start).add(1, 'hour').isAfter(newValue)) { $scope.end = oldValue } // update availability object return $scope.availability.end_at = $scope.end }) } // !!! MUST BE CALLED AT THE END of the controller return initialize() } ])