mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-12-03 14:24:23 +01:00
547 lines
21 KiB
Plaintext
547 lines
21 KiB
Plaintext
/* 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.statistic_profile) {
|
|
if (user.statistic_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('<span class="remove-event">x </span>');
|
|
if (event.tags.length > 0) {
|
|
let html = '';
|
|
for (let tag of Array.from(event.tags)) {
|
|
html += `<span class='label label-success text-white'>${tag.name}</span> `;
|
|
}
|
|
element.find('.fc-title').append(`<br/>${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();
|
|
}
|
|
]);
|