'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' ($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 BASE_SLOT = '00:30:00' # The bookings can be positioned every half hours BOOKING_SNAP = '00:30:00' # We do not allow the creation of slots that are not a multiple of 60 minutes 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) -> calendarSelectCb(start, end, jsEvent, view) eventClick: (event, jsEvent, view)-> calendarEventClickCb(event, jsEvent, view) eventRender: (event, element, view) -> eventRenderCb(event, element) loading: (isLoading, view ) -> 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 = (slot) -> # open a confirmation dialog dialogs.confirm resolve: object: -> 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') , -> # the admin has confirmed, cancel the subscription Slot.cancel {id: slot.slot_id} , (data, status) -> # success # update the canceled_at attribute for resa in $scope.reservations if resa.slot_id == data.id resa.canceled_at = data.canceled_at break # notify the admin growl.success(_t('admin_calendar.reservation_was_successfully_cancelled')) , (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 = (machine) -> if $scope.availability.machine_ids.length == 1 growl.error(_t('admin_calendar.unable_to_remove_the_last_machine_of_the_slot_delete_the_slot_rather')) else # open a confirmation dialog dialogs.confirm resolve: object: -> 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') , -> # the admin has confirmed, remove the machine machines = $scope.availability.machine_ids for key, m_id in machines if m_id == machine.id machines.splice(key, 1) Availability.update {id: $scope.availability.id}, {availability: {machines_attributes: [{id: machine.id, _destroy: true}]}} , (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 growl.success(_t('admin_calendar.the_machine_was_successfully_removed_from_the_slot')) , (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 = (type) -> Export.status({category: 'availabilities', type: type}).then (res) -> unless (res.data.exists) 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 = -> # first, define a shortcut to the lock property locked = $scope.availability.lock # then check if we'll allow reservations locking prevent = !locked # if currently locked, allow unlock anyway if (!locked) prevent = false angular.forEach $scope.reservations, (r) -> if r.canceled_at == null prevent = true # if currently unlocked and has any non-cancelled reservation, disallow locking if (!prevent) # open a confirmation dialog dialogs.confirm resolve: object: -> title: _t('admin_calendar.confirmation_required') msg: if locked then _t("admin_calendar.do_you_really_want_to_allow_reservations") else _t("admin_calendar.do_you_really_want_to_block_this_slot") , -> # the admin has confirmed, lock/unlock the slot Availability.lock {id: $scope.availability.id}, {lock: !locked} , (data) -> # success $scope.availability = data growl.success(if locked then _t('admin_calendar.unlocking_success') else _t('admin_calendar.locking_success') ) uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents' , (error) -> # failed growl.error(if locked then _t('admin_calendar.unlocking_failed') else _t('admin_calendar.locking_failed')) else growl.error(_t('admin_calendar.unlockable_because_reservations')) ## # Confirm and destroy the slot in $scope.availability ## $scope.removeSlot = -> # open a confirmation dialog dialogs.confirm resolve: object: -> title: _t('admin_calendar.confirmation_required') msg: _t("admin_calendar.do_you_really_want_to_delete_this_slot") , -> # the admin has confirmed, delete the slot Availability.delete id: $scope.availability.id, -> 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 ,-> 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' ## getGender = (user) -> if user.profile if user.profile.gender == "true" then 'male' else 'female' else 'other' # Triggered when the admin drag on the agenda to create a new reservable slot. # @see http://fullcalendar.io/docs/selection/select_callback/ ## calendarSelectCb = (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) 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 modalInstance = $uibModal.open templateUrl: '<%= asset_path "admin/calendar/eventModal.html" %>' controller: 'CreateEventModalController' resolve: start: -> start end: -> end machinesPromise: ['Machine', (Machine)-> Machine.query().$promise ] trainingsPromise: ['Training', (Training)-> Training.query().$promise ] spacesPromise: ['Space', (Space)-> Space.query().$promise ] # when the modal is closed, we send the slot to the server for saving modalInstance.result.then (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 , -> uiCalendarConfig.calendars.calendar.fullCalendar('unselect') uiCalendarConfig.calendars.calendar.fullCalendar('unselect') ## # Triggered when the admin clicks on a availability slot in the agenda. # @see http://fullcalendar.io/docs/mouse/eventClick/ ## calendarEventClickCb = (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')) $scope.removeSlot() # if the user has only clicked on the event, display its reservations else Availability.reservations {id: event.id}, (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/ ## eventRenderCb = (event, element) -> element.find('.fc-content').prepend('') if event.tags.length > 0 html = '' for tag in 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) return ## # Triggered when resource fetching starts/stops. # @see https://fullcalendar.io/docs/resource_data/loading/ ## loadingCb = (isLoading, view) -> if (isLoading) # we remove existing events when fetching starts to prevent duplicates 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' , ($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 ## trainings list $scope.trainings = trainingsPromise ## spaces list $scope.spaces = spacesPromise ## 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 = (machine)-> index = $scope.selectedMachines.indexOf(machine) if index > -1 $scope.selectedMachines.splice(index, 1) else $scope.selectedMachines.push(machine) ## # Callback for the modal window validation: save the slot and closes the modal ## $scope.ok = -> if $scope.availability.available_type == 'machines' if $scope.selectedMachines.length > 0 $scope.availability.machine_ids = $scope.selectedMachines.map (m) -> 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] Availability.save availability: $scope.availability , (availability) -> $uibModalInstance.close(availability) ## # Move the modal UI to the next step ## $scope.next = -> $scope.setNbTotalPlaces() if $scope.step == 1 $scope.step++ ## # Move the modal UI to the next step ## $scope.previous = -> $scope.step-- ## # Callback to cancel the slot creation ## $scope.cancel = -> $uibModalInstance.dismiss('cancel') ## # For training avaiabilities, set the maximum number of people allowed to register on this slot ## $scope.setNbTotalPlaces = -> if $scope.availability.available_type == 'training' $scope.availability.nb_total_places = $scope.selectedTraining.nb_total_places else if $scope.availability.available_type == 'space' $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 ## initialize = -> if $scope.trainings.length > 0 $scope.selectedTraining = $scope.trainings[0] if $scope.spaces.length > 0 $scope.selectedSpace = $scope.spaces[0] Tag.query().$promise.then (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', (newValue, oldValue, scope) -> if newValue == 'machines' or newValue == 'space' $scope.endDateReadOnly = true 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() $scope.availability.end_at = $scope.end else $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', (newValue, oldValue, scope) -> # for machine or space availabilities, adjust the end time if $scope.availability.available_type == 'machines' or $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 $scope.availability.start_at = $scope.start ## Maintain consistency between the end time and the date object in the availability object $scope.$watch 'end', (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 $scope.availability.end_at = $scope.end ## !!! MUST BE CALLED AT THE END of the controller initialize() ]