mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-11-28 09:24:24 +01:00
wip
This commit is contained in:
parent
8cd87f94d5
commit
043ff6d47e
@ -4,6 +4,7 @@
|
||||
class API::ProjectCategoriesController < ApplicationController
|
||||
before_action :set_project_category, only: %i[update destroy]
|
||||
before_action :authenticate_user!, only: %i[create update destroy]
|
||||
|
||||
def index
|
||||
@project_categories = ProjectCategory.all
|
||||
end
|
||||
|
58
app/controllers/api/reservation_contexts_controller.rb
Normal file
58
app/controllers/api/reservation_contexts_controller.rb
Normal file
@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# API Controller for resources of type ReservationContext
|
||||
class API::ReservationContextsController < API::APIController
|
||||
before_action :authenticate_user!, except: [:index]
|
||||
before_action :set_reservation_context, only: %i[show update destroy]
|
||||
|
||||
def index
|
||||
@reservation_contexts = ReservationContext.all
|
||||
@reservation_contexts = @reservation_contexts.applicable_on(params[:applicable_on]) if params[:applicable_on].present?
|
||||
@reservation_contexts = @reservation_contexts.order(:created_at)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
authorize ReservationContext
|
||||
@reservation_context = ReservationContext.new(reservation_context_params)
|
||||
if @reservation_context.save
|
||||
render :show, status: :created, location: @reservation_context
|
||||
else
|
||||
render json: @reservation_context.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
authorize ReservationContext
|
||||
if @reservation_context.update(reservation_context_params)
|
||||
render :show, status: :ok, location: @reservation_context
|
||||
else
|
||||
render json: @reservation_context.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize ReservationContext
|
||||
if @reservation_context.safe_destroy
|
||||
head :no_content
|
||||
else
|
||||
render json: @reservation_context.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def applicable_on_values
|
||||
authorize ReservationContext
|
||||
render json: ReservationContext::APPLICABLE_ON, status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_reservation_context
|
||||
@reservation_context = ReservationContext.find(params[:id])
|
||||
end
|
||||
|
||||
def reservation_context_params
|
||||
params.require(:reservation_context).permit(:name, applicable_on: [])
|
||||
end
|
||||
end
|
10
app/frontend/src/javascript/api/reservation_context.ts
Normal file
10
app/frontend/src/javascript/api/reservation_context.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { ReservationContext } from '../models/reservation';
|
||||
|
||||
export default class ReservationContextAPI {
|
||||
static async index (): Promise<Array<ReservationContext>> {
|
||||
const res: AxiosResponse<Array<ReservationContext>> = await apiClient.get('/api/reservation_contexts');
|
||||
return res?.data;
|
||||
}
|
||||
}
|
@ -12,8 +12,8 @@
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
Application.Controllers.controller('SettingsController', ['$scope', '$rootScope', '$filter', '$uibModal', 'dialogs', 'Setting', 'growl', 'settingsPromise', 'privacyDraftsPromise', 'cgvFile', 'cguFile', 'logoFile', 'logoBlackFile', 'faviconFile', 'profileImageFile', 'CSRF', '_t', 'Member', 'uiTourService',
|
||||
function ($scope, $rootScope, $filter, $uibModal, dialogs, Setting, growl, settingsPromise, privacyDraftsPromise, cgvFile, cguFile, logoFile, logoBlackFile, faviconFile, profileImageFile, CSRF, _t, Member, uiTourService) {
|
||||
Application.Controllers.controller('SettingsController', ['$scope', '$rootScope', '$filter', '$uibModal', 'dialogs', 'Setting', 'growl', 'settingsPromise', 'privacyDraftsPromise', 'cgvFile', 'cguFile', 'logoFile', 'logoBlackFile', 'faviconFile', 'profileImageFile', 'reservationContextsPromise', 'reservationContextApplicableOnValuesPromise', 'CSRF', '_t', 'Member', 'uiTourService', 'ReservationContext',
|
||||
function ($scope, $rootScope, $filter, $uibModal, dialogs, Setting, growl, settingsPromise, privacyDraftsPromise, cgvFile, cguFile, logoFile, logoBlackFile, faviconFile, profileImageFile, reservationContextsPromise, reservationContextApplicableOnValuesPromise, CSRF, _t, Member, uiTourService, ReservationContext) {
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// timepickers steps configuration
|
||||
@ -76,6 +76,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
|
||||
$scope.mainColorSetting = { name: 'main_color', value: settingsPromise.main_color };
|
||||
$scope.secondColorSetting = { name: 'secondary_color', value: settingsPromise.secondary_color };
|
||||
$scope.nameGenre = { name: 'name_genre', value: settingsPromise.name_genre };
|
||||
$scope.reservationContextFeature = { name: 'reservation_context_feature', value: settingsPromise.reservation_context_feature };
|
||||
$scope.cguFile = cguFile.custom_asset;
|
||||
$scope.cgvFile = cgvFile.custom_asset;
|
||||
$scope.customLogo = logoFile.custom_asset;
|
||||
@ -83,6 +84,11 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
|
||||
$scope.customFavicon = faviconFile.custom_asset;
|
||||
$scope.profileImage = profileImageFile.custom_asset;
|
||||
|
||||
// necessary initialisation for reservation context
|
||||
$scope.reservationContexts = reservationContextsPromise;
|
||||
$scope.reservationContextApplicableOnValues = reservationContextApplicableOnValuesPromise;
|
||||
$scope.newReservationContext = null;
|
||||
|
||||
// By default, we display the currently published privacy policy
|
||||
$scope.privacyPolicy = {
|
||||
version: null,
|
||||
@ -314,21 +320,6 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
|
||||
$scope.codeMirrorEditor = editor;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a success message forwarded from a child react component
|
||||
*/
|
||||
$scope.onSuccess = function (message) {
|
||||
growl.success(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered by react components
|
||||
*/
|
||||
$scope.onError = function (message) {
|
||||
console.error(message);
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for allow/prevent book overlapping slots: which kind of slots are used in the overlapping computation
|
||||
*/
|
||||
@ -476,6 +467,63 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
// Functions for reservation context feature
|
||||
|
||||
$scope.onReservationContextFeatureChange = function (value) {
|
||||
$scope.reservationContextFeature.value = value;
|
||||
};
|
||||
|
||||
$scope.saveReservationContext = function (data, id) {
|
||||
if (id != null) {
|
||||
return ReservationContext.update({ id }, data);
|
||||
} else {
|
||||
return ReservationContext.save(data, function (resp) { $scope.reservationContexts[$scope.reservationContexts.length - 1].id = resp.id; });
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removeReservationContext = function (index) {
|
||||
if ($scope.reservationContexts[index].related_to > 0) {
|
||||
growl.error(_t('app.admin.settings.unable_to_delete_reservation_context_already_related_to_reservations'));
|
||||
return false;
|
||||
}
|
||||
return dialogs.confirm({
|
||||
resolve: {
|
||||
object () {
|
||||
return {
|
||||
title: _t('app.admin.settings.confirmation_required'),
|
||||
msg: _t('app.admin.settings.do_you_really_want_to_delete_this_reservation_context')
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
, function () { // delete confirmed
|
||||
ReservationContext.delete($scope.reservationContexts[index], null, function () { $scope.reservationContexts.splice(index, 1); }
|
||||
, function () { growl.error(_t('app.admin.settings.unable_to_delete_reservation_context_an_error_occured')); });
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addReservationContext = function () {
|
||||
$scope.newReservationContext = {
|
||||
name: '',
|
||||
related_to: 0
|
||||
};
|
||||
return $scope.reservationContexts.push($scope.newReservationContext);
|
||||
};
|
||||
|
||||
$scope.cancelReservationContext = function (rowform, index) {
|
||||
if ($scope.reservationContexts[index].id != null) {
|
||||
return rowform.$cancel();
|
||||
} else {
|
||||
return $scope.reservationContexts.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.translateApplicableOnValue = function (value) {
|
||||
if (!value) { return; }
|
||||
if (angular.isArray(value)) { return value.map(v => _t(`app.admin.reservation_contexts.${v}`)).join(', '); }
|
||||
return _t(`app.admin.reservation_contexts.${value}`);
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/**
|
||||
|
@ -15,8 +15,8 @@
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
Application.Controllers.controller('StatisticsController', ['$scope', '$state', '$transitions', '$rootScope', '$uibModal', 'es', 'Member', '_t', 'membersPromise', 'statisticsPromise', 'uiTourService', 'settingsPromise',
|
||||
function ($scope, $state, $transitions, $rootScope, $uibModal, es, Member, _t, membersPromise, statisticsPromise, uiTourService, settingsPromise) {
|
||||
Application.Controllers.controller('StatisticsController', ['$scope', '$state', '$transitions', '$rootScope', '$uibModal', 'es', 'Member', '_t', 'membersPromise', 'statisticsPromise', 'uiTourService', 'settingsPromise', 'reservationContextsPromise', 'reservationContextApplicableOnValuesPromise',
|
||||
function ($scope, $state, $transitions, $rootScope, $uibModal, es, Member, _t, membersPromise, statisticsPromise, uiTourService, settingsPromise, reservationContextsPromise, reservationContextApplicableOnValuesPromise) {
|
||||
/* PRIVATE STATIC CONSTANTS */
|
||||
|
||||
// search window size
|
||||
@ -133,6 +133,18 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
|
||||
}
|
||||
};
|
||||
|
||||
$scope.reservationContextFeatureEnabled = settingsPromise.reservation_context_feature === 'true';
|
||||
|
||||
$scope.reservationContexts = reservationContextsPromise;
|
||||
|
||||
$scope.reservationContexts = reservationContextsPromise.filter(rc => rc.applicable_on.length > 0);
|
||||
|
||||
$scope.reservationContextApplicableOnValues = reservationContextApplicableOnValuesPromise;
|
||||
|
||||
$scope.reservationContextIsApplicable = function (esTypeKey) {
|
||||
return $scope.reservationContextApplicableOnValues.includes(esTypeKey);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a localized name for the given field
|
||||
*/
|
||||
@ -220,17 +232,9 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
|
||||
*/
|
||||
$scope.formatDate = function (date) { return moment(date).format('LL'); };
|
||||
|
||||
/**
|
||||
* Parse the sex and return a user-friendly string
|
||||
* @param sex {string} 'male' | 'female'
|
||||
*/
|
||||
$scope.formatSex = function (sex) {
|
||||
if (sex === 'male') {
|
||||
return _t('app.admin.statistics.man');
|
||||
}
|
||||
if (sex === 'female') {
|
||||
return _t('app.admin.statistics.woman');
|
||||
}
|
||||
$scope.formatReservationContext = function (id) {
|
||||
if (id === null || id === undefined) { return; }
|
||||
return $scope.reservationContexts.find(rc => rc.id === id).name;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -645,6 +649,15 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
|
||||
{ key: 'ca', label: _t('app.admin.statistics.revenue'), values: ['input_number'] }
|
||||
];
|
||||
|
||||
if ($scope.reservationContextFeatureEnabled && $scope.reservationContextIsApplicable($scope.selectedIndex.es_type_key)) {
|
||||
const reservationContextValues = $scope.reservationContexts.map((rc) => {
|
||||
return { key: rc.id, label: rc.name };
|
||||
});
|
||||
$scope.filters.push({
|
||||
key: 'reservationContextId', label: _t('app.admin.statistics.reservation_context'), values: reservationContextValues
|
||||
});
|
||||
}
|
||||
|
||||
// if no plans were created, there's no types for statisticIndex=subscriptions
|
||||
if ($scope.type.active) {
|
||||
$scope.filters.splice(4, 0, { key: 'subType', label: _t('app.admin.statistics.type'), values: $scope.type.active.subtypes });
|
||||
|
@ -390,8 +390,8 @@ Application.Controllers.controller('ShowMachineController', ['$scope', '$state',
|
||||
* This controller workflow is pretty similar to the trainings reservation controller.
|
||||
*/
|
||||
|
||||
Application.Controllers.controller('ReserveMachineController', ['$scope', '$transition$', '_t', 'moment', 'Auth', '$timeout', 'Member', 'Availability', 'plansPromise', 'groupsPromise', 'machinePromise', 'settingsPromise', 'uiCalendarConfig', 'CalendarConfig', 'Reservation', 'growl', 'helpers', 'AuthService',
|
||||
function ($scope, $transition$, _t, moment, Auth, $timeout, Member, Availability, plansPromise, groupsPromise, machinePromise, settingsPromise, uiCalendarConfig, CalendarConfig, Reservation, growl, helpers, AuthService) {
|
||||
Application.Controllers.controller('ReserveMachineController', ['$scope', '$transition$', '_t', 'moment', 'Auth', '$timeout', 'Member', 'Availability', 'plansPromise', 'groupsPromise', 'machinePromise', 'settingsPromise', 'reservationContextsPromise', 'uiCalendarConfig', 'CalendarConfig', 'Reservation', 'growl', 'helpers', 'AuthService',
|
||||
function ($scope, $transition$, _t, moment, Auth, $timeout, Member, Availability, plansPromise, groupsPromise, machinePromise, settingsPromise, reservationContextsPromise, uiCalendarConfig, CalendarConfig, Reservation, growl, helpers, AuthService) {
|
||||
/* PRIVATE STATIC CONSTANTS */
|
||||
|
||||
// Slot free to be booked
|
||||
@ -445,6 +445,13 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
||||
// will be set to a Promise and resolved after the payment is sone
|
||||
$scope.afterPaymentPromise = null;
|
||||
|
||||
// reservation contexts stuff
|
||||
if (settingsPromise.reservation_context_feature === 'true') {
|
||||
$scope.reservationContexts = reservationContextsPromise;
|
||||
} else {
|
||||
$scope.reservationContexts = [];
|
||||
}
|
||||
|
||||
// fullCalendar (v2) configuration
|
||||
$scope.calendarConfig = CalendarConfig({
|
||||
minTime: moment.duration(moment.utc(settingsPromise.booking_window_start.match(/\d{4}-\d{2}-\d{2}(?: |T)\d{2}:\d{2}:\d{2}/)[0]).format('HH:mm:ss')),
|
||||
|
@ -328,8 +328,8 @@ Application.Controllers.controller('ShowSpaceController', ['$scope', '$state', '
|
||||
* per slots.
|
||||
*/
|
||||
|
||||
Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transition$', 'Auth', '$timeout', 'Availability', 'Member', 'plansPromise', 'groupsPromise', 'settingsPromise', 'spacePromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Reservation', 'helpers', 'AuthService',
|
||||
function ($scope, $transition$, Auth, $timeout, Availability, Member, plansPromise, groupsPromise, settingsPromise, spacePromise, _t, uiCalendarConfig, CalendarConfig, Reservation, helpers, AuthService) {
|
||||
Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transition$', 'Auth', '$timeout', 'Availability', 'Member', 'plansPromise', 'groupsPromise', 'settingsPromise', 'spacePromise', 'reservationContextsPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Reservation', 'helpers', 'AuthService',
|
||||
function ($scope, $transition$, Auth, $timeout, Availability, Member, plansPromise, groupsPromise, settingsPromise, spacePromise, reservationContextsPromise, _t, uiCalendarConfig, CalendarConfig, Reservation, helpers, AuthService) {
|
||||
/* PRIVATE STATIC CONSTANTS */
|
||||
|
||||
// Color of the selected event backgound
|
||||
@ -407,6 +407,13 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi
|
||||
// Global config: is the user validation required ?
|
||||
$scope.enableUserValidationRequired = settingsPromise.user_validation_required === 'true';
|
||||
|
||||
// reservation contexts stuff
|
||||
if (settingsPromise.reservation_context_feature === 'true') {
|
||||
$scope.reservationContexts = reservationContextsPromise;
|
||||
} else {
|
||||
$scope.reservationContexts = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the last selected slot's appearance to looks like 'added to cart'
|
||||
*/
|
||||
|
@ -98,8 +98,8 @@ Application.Controllers.controller('ShowTrainingController', ['$scope', '$state'
|
||||
* training can be reserved during the reservation process (the shopping cart may contain only one training and a subscription).
|
||||
*/
|
||||
|
||||
Application.Controllers.controller('ReserveTrainingController', ['$scope', '$transition$', 'Auth', 'AuthService', '$timeout', 'Availability', 'Member', 'plansPromise', 'groupsPromise', 'settingsPromise', 'trainingPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Reservation', 'helpers',
|
||||
function ($scope, $transition$, Auth, AuthService, $timeout, Availability, Member, plansPromise, groupsPromise, settingsPromise, trainingPromise, _t, uiCalendarConfig, CalendarConfig, Reservation, helpers) {
|
||||
Application.Controllers.controller('ReserveTrainingController', ['$scope', '$transition$', 'Auth', 'AuthService', '$timeout', 'Availability', 'Member', 'plansPromise', 'groupsPromise', 'settingsPromise', 'trainingPromise', 'reservationContextsPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Reservation', 'helpers',
|
||||
function ($scope, $transition$, Auth, AuthService, $timeout, Availability, Member, plansPromise, groupsPromise, settingsPromise, trainingPromise, reservationContextsPromise, _t, uiCalendarConfig, CalendarConfig, Reservation, helpers) {
|
||||
/* PRIVATE STATIC CONSTANTS */
|
||||
|
||||
// Color of the selected event backgound
|
||||
@ -180,6 +180,13 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra
|
||||
// Global config: is the user validation required ?
|
||||
$scope.enableUserValidationRequired = settingsPromise.user_validation_required === 'true';
|
||||
|
||||
// reservation contexts stuff
|
||||
if (settingsPromise.reservation_context_feature === 'true') {
|
||||
$scope.reservationContexts = reservationContextsPromise;
|
||||
} else {
|
||||
$scope.reservationContexts = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the last selected slot's appearance to looks like 'added to cart'
|
||||
*/
|
||||
|
@ -36,7 +36,8 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
reservableId: '@',
|
||||
reservableType: '@',
|
||||
reservableName: '@',
|
||||
limitToOneSlot: '@'
|
||||
limitToOneSlot: '@',
|
||||
reservationContexts: '='
|
||||
},
|
||||
templateUrl: '/shared/_cart.html',
|
||||
link ($scope, element, attributes) {
|
||||
@ -88,6 +89,11 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
// currently logged-in user
|
||||
$scope.currentUser = $rootScope.currentUser;
|
||||
|
||||
$scope.reservationContextsData = {
|
||||
options: $scope.reservationContexts || [],
|
||||
selected: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the provided slot to the shopping cart (state transition from free to 'about to be reserved')
|
||||
* and increment the total amount of the cart if needed.
|
||||
@ -162,7 +168,10 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
* Validates the shopping chart and redirect the user to the payment step
|
||||
*/
|
||||
$scope.payCart = function () {
|
||||
// first, we check that a user was selected
|
||||
if ($scope.reservationContextsData.options.length && angular.isUndefinedOrNull($scope.reservationContextsData.selected)) {
|
||||
return growl.error(_t('app.shared.cart.please_select_a_reservation_context'));
|
||||
}
|
||||
// first, we check that a user was selected
|
||||
if (Object.keys($scope.user).length > 0) {
|
||||
// check selected user has a subscription, if any slot is restricted for subscriptions
|
||||
const slotValidations = [];
|
||||
@ -725,6 +734,9 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
reservable_type: $scope.reservableType,
|
||||
slots_reservations_attributes: []
|
||||
};
|
||||
if (!angular.isUndefinedOrNull($scope.reservationContextsData.selected)) {
|
||||
reservation.reservation_context_id = $scope.reservationContextsData.selected.id;
|
||||
}
|
||||
angular.forEach(slots, function (slot) {
|
||||
reservation.slots_reservations_attributes.push({
|
||||
offered: slot.offered || false,
|
||||
|
@ -52,3 +52,9 @@ export interface ReservationIndexFilter extends ApiFilter {
|
||||
reservable_type?: ReservableType | Array<ReservableType>,
|
||||
user_id?: number
|
||||
}
|
||||
|
||||
export interface ReservationContext {
|
||||
id?: number,
|
||||
name: string,
|
||||
related_to?: number
|
||||
}
|
||||
|
@ -88,7 +88,8 @@ export const bookingSettings = [
|
||||
'display_name_enable',
|
||||
'book_overlapping_slots',
|
||||
'slot_duration',
|
||||
'overlapping_categories'
|
||||
'overlapping_categories',
|
||||
'reservation_context_feature'
|
||||
] as const;
|
||||
|
||||
export const themeSettings = [
|
||||
|
@ -417,9 +417,11 @@ angular.module('application.router', ['ui.router'])
|
||||
return Setting.query({
|
||||
names: "['machine_explications_alert', 'booking_window_start', 'booking_window_end', 'booking_move_enable', " +
|
||||
"'booking_move_delay', 'booking_cancel_enable', 'booking_cancel_delay', 'subscription_explications_alert', " +
|
||||
"'online_payment_module', 'payment_gateway', 'overlapping_categories', 'user_validation_required', 'user_validation_required_list']"
|
||||
"'online_payment_module', 'payment_gateway', 'overlapping_categories', 'user_validation_required', 'user_validation_required_list', " +
|
||||
"'reservation_context_feature']"
|
||||
}).$promise;
|
||||
}]
|
||||
}],
|
||||
reservationContextsPromise: ['ReservationContext', function (ReservationContext) { return ReservationContext.query({ applicable_on: 'machine' }).$promise; }]
|
||||
}
|
||||
})
|
||||
.state('app.admin.machines_edit', {
|
||||
@ -506,9 +508,10 @@ angular.module('application.router', ['ui.router'])
|
||||
names: "['booking_window_start', 'booking_window_end', 'booking_move_enable', 'booking_move_delay', " +
|
||||
"'booking_cancel_enable', 'booking_cancel_delay', 'subscription_explications_alert', " +
|
||||
"'space_explications_alert', 'online_payment_module', 'payment_gateway', 'overlapping_categories', " +
|
||||
"'user_validation_required', 'user_validation_required_list']"
|
||||
"'user_validation_required', 'user_validation_required_list', 'reservation_context_feature']"
|
||||
}).$promise;
|
||||
}]
|
||||
}],
|
||||
reservationContextsPromise: ['ReservationContext', function (ReservationContext) { return ReservationContext.query({ applicable_on: 'space' }).$promise; }]
|
||||
}
|
||||
})
|
||||
|
||||
@ -560,9 +563,11 @@ angular.module('application.router', ['ui.router'])
|
||||
names: "['booking_window_start', 'booking_window_end', 'booking_move_enable', 'booking_move_delay', " +
|
||||
"'booking_cancel_enable', 'booking_cancel_delay', 'subscription_explications_alert', " +
|
||||
"'training_explications_alert', 'training_information_message', 'online_payment_module', " +
|
||||
"'payment_gateway', 'overlapping_categories', 'user_validation_required', 'user_validation_required_list']"
|
||||
"'payment_gateway', 'overlapping_categories', 'user_validation_required', 'user_validation_required_list', " +
|
||||
"'reservation_context_feature']"
|
||||
}).$promise;
|
||||
}]
|
||||
}],
|
||||
reservationContextsPromise: ['ReservationContext', function (ReservationContext) { return ReservationContext.query({ applicable_on: 'training' }).$promise; }]
|
||||
}
|
||||
})
|
||||
// notifications
|
||||
@ -1140,7 +1145,9 @@ angular.module('application.router', ['ui.router'])
|
||||
resolve: {
|
||||
membersPromise: ['Member', function (Member) { return Member.mapping().$promise; }],
|
||||
statisticsPromise: ['Statistics', function (Statistics) { return Statistics.query().$promise; }],
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display']" }).$promise; }]
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display', 'reservation_context_feature']" }).$promise; }],
|
||||
reservationContextsPromise: ['ReservationContext', function (ReservationContext) { return ReservationContext.query().$promise; }],
|
||||
reservationContextApplicableOnValuesPromise: ['ReservationContext', function (ReservationContext) { return ReservationContext.applicableOnValues().$promise; }]
|
||||
}
|
||||
})
|
||||
.state('app.admin.stats_graphs', {
|
||||
@ -1181,14 +1188,16 @@ angular.module('application.router', ['ui.router'])
|
||||
"'extended_prices_in_same_day', 'recaptcha_site_key', 'recaptcha_secret_key', 'user_validation_required', " +
|
||||
"'user_validation_required_list', 'machines_module', 'user_change_group', 'show_username_in_admin_list', " +
|
||||
"'store_module', 'machine_reservation_deadline', 'training_reservation_deadline', 'event_reservation_deadline', " +
|
||||
"'space_reservation_deadline']"
|
||||
"'space_reservation_deadline', 'reservation_context_feature']"
|
||||
}).$promise;
|
||||
}],
|
||||
privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }],
|
||||
cguFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgu-file' }).$promise; }],
|
||||
cgvFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgv-file' }).$promise; }],
|
||||
faviconFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'favicon-file' }).$promise; }],
|
||||
profileImageFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'profile-image-file' }).$promise; }]
|
||||
profileImageFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'profile-image-file' }).$promise; }],
|
||||
reservationContextsPromise: ['ReservationContext', function (ReservationContext) { return ReservationContext.query().$promise; }],
|
||||
reservationContextApplicableOnValuesPromise: ['ReservationContext', function (ReservationContext) { return ReservationContext.applicableOnValues().$promise; }]
|
||||
}
|
||||
})
|
||||
|
||||
|
16
app/frontend/src/javascript/services/reservation_context.js
Normal file
16
app/frontend/src/javascript/services/reservation_context.js
Normal file
@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
Application.Services.factory('ReservationContext', ['$resource', function ($resource) {
|
||||
return $resource('/api/reservation_contexts/:id',
|
||||
{ id: '@id' }, {
|
||||
update: {
|
||||
method: 'PUT'
|
||||
},
|
||||
applicableOnValues: {
|
||||
method: 'GET',
|
||||
url: '/api/reservation_contexts/applicable_on_values',
|
||||
isArray: true
|
||||
}
|
||||
}
|
||||
);
|
||||
}]);
|
@ -250,3 +250,74 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default m-t-lg">
|
||||
<div class="panel-heading">
|
||||
<span class="font-sbold" translate>{{ 'app.admin.settings.reservation_context_feature_title' }}</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.reservation_context_feature_info' | translate"></p>
|
||||
<boolean-setting name="'reservation_context_feature'"
|
||||
label="'app.admin.settings.reservation_context_feature' | translate"
|
||||
class-name="'m-l'"
|
||||
on-success="onSuccess"
|
||||
on-error="onError"
|
||||
on-change="onReservationContextFeatureChange">
|
||||
</boolean-setting>
|
||||
</div>
|
||||
<div class="row p-l p-r" ng-if="reservationContextFeature.value === 'true'">
|
||||
<h3 class="" translate>{{ 'app.admin.settings.reservation_context_options' }}</h3>
|
||||
|
||||
<button type="button" class="btn btn-warning m-b m-t" ng-click="addReservationContext()" translate>{{ 'app.admin.settings.add_a_reservation_context' }}</button>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40%" translate>{{ 'app.admin.reservation_contexts.name' }}</th>
|
||||
<th style="width:40%" translate>{{ 'app.admin.reservation_contexts.applicable_on' }}</th>
|
||||
<th style="width:20%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="reservationContext in reservationContexts">
|
||||
<td>
|
||||
<span editable-text="reservationContext.name" e-cols="100" e-name="name" e-form="rowform" e-required>
|
||||
{{ reservationContext.name }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div editable-ui-select="reservationContext.applicable_on" e-multiple e-form="rowform" e-name="applicable_on" e-ng-model="reservationContext.applicable_on" e-style="min-width: 200px">
|
||||
{{ translateApplicableOnValue(reservationContext.applicable_on) }}
|
||||
<editable-ui-select-match placeholder="">
|
||||
{{ translateApplicableOnValue($item) }}
|
||||
</editable-ui-select-match>
|
||||
<editable-ui-select-choices repeat="ao in reservationContextApplicableOnValues | filter: $select.search track by $index">
|
||||
{{ translateApplicableOnValue(ao) }}
|
||||
</editable-ui-select-choices>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<!-- form -->
|
||||
<form editable-form name="rowform" onbeforesave="saveReservationContext($data, reservationContext.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="newReservationContext == reservationContext">
|
||||
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
|
||||
<i class="fa fa-check"></i>
|
||||
</button>
|
||||
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelReservationContext(rowform, $index)" class="btn btn-default">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</form>
|
||||
<div class="buttons" ng-show="!rowform.$visible">
|
||||
<button class="btn btn-default" ng-click="rowform.$show()">
|
||||
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'app.shared.buttons.edit' }}</span>
|
||||
</button>
|
||||
<button class="btn btn-danger" ng-click="removeReservationContext($index)">
|
||||
<i class="fa fa-trash-o"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -257,7 +257,9 @@
|
||||
<th ng-if="['booking', 'hour'].includes(type.active.key)" translate>{{ 'app.admin.statistics.reservation_date' }}</th>
|
||||
<th ng-if="!['booking', 'hour'].includes(type.active.key)" translate>{{ 'app.admin.statistics.date' }}</th>
|
||||
<th translate>{{ 'app.admin.statistics.user' }}</th>
|
||||
<th translate>{{ 'app.admin.statistics.gender' }}</th>
|
||||
<th ng-if="reservationContextFeatureEnabled && reservationContextIsApplicable(selectedIndex.es_type_key)">
|
||||
{{ 'app.admin.statistics.reservation_context' | translate }}
|
||||
</th>
|
||||
<th translate>{{ 'app.admin.statistics.age' }}</th>
|
||||
<th translate>{{ 'app.admin.statistics.type' }}</th>
|
||||
<th ng-if="!type.active.simple">{{type.active.label}}</th>
|
||||
@ -280,7 +282,9 @@
|
||||
<a ng-show="datum._source.userId" ui-sref="app.admin.members_edit({id:datum._source.userId})">{{getUserNameFromId(datum._source.userId)}}</a>
|
||||
<span class="text-gray text-italic" ng-hide="datum._source.userId" translate>{{ 'app.admin.statistics.deleted_user' }}</span>
|
||||
</td>
|
||||
<td>{{formatSex(datum._source.gender)}}</td>
|
||||
<td ng-if="reservationContextFeatureEnabled && reservationContextIsApplicable(selectedIndex.es_type_key)">
|
||||
{{ formatReservationContext(datum._source.reservationContextId) }}
|
||||
</td>
|
||||
<td>
|
||||
<span ng-if="datum._source.age">{{datum._source.age}} {{ 'app.admin.statistics.years_old' | translate }}</span>
|
||||
<span ng-if="!datum._source.age" translate>{{ 'app.admin.statistics.unknown' }}</span>
|
||||
|
@ -75,7 +75,8 @@
|
||||
after-payment="afterPayment"
|
||||
reservable-id="{{machine.id}}"
|
||||
reservable-type="Machine"
|
||||
reservable-name="{{machine.name}}"></cart>
|
||||
reservable-name="{{machine.name}}"
|
||||
reservation-contexts="reservationContexts"></cart>
|
||||
|
||||
<uib-alert type="warning m">
|
||||
<p class="text-sm">
|
||||
|
@ -18,6 +18,10 @@
|
||||
</div>
|
||||
|
||||
<div class="widget-content no-bg auto wrapper" ng-if="events.reserved.length > 0">
|
||||
<div ng-if="reservationContextsData.options.length > 0" class="m-b">
|
||||
<div class="font-sbold m-b-sm " translate>{{ 'app.shared.cart.select_the_reservation_context' }}</div>
|
||||
<select ng-model="reservationContextsData.selected" class="form-control m-t-sm" ng-options="o.name for o in reservationContextsData.options"></select>
|
||||
</div>
|
||||
|
||||
<div class="font-sbold m-b-sm " translate>{{ 'app.shared.cart.you_ve_just_selected_the_slot' }}</div>
|
||||
|
||||
|
@ -47,7 +47,8 @@
|
||||
after-payment="afterPayment"
|
||||
reservable-id="{{space.id}}"
|
||||
reservable-type="Space"
|
||||
reservable-name="{{space.name}}"></cart>
|
||||
reservable-name="{{space.name}}"
|
||||
reservation-contexts="reservationContexts"></cart>
|
||||
|
||||
|
||||
<uib-alert type="warning m" ng-show="spaceExplicationsAlert">
|
||||
|
@ -60,7 +60,8 @@
|
||||
reservable-id="{{training.id}}"
|
||||
reservable-type="Training"
|
||||
reservable-name="{{training.name}}"
|
||||
limit-to-one-slot="true"></cart>
|
||||
limit-to-one-slot="true"
|
||||
reservation-contexts="reservationContexts"></cart>
|
||||
|
||||
|
||||
<uib-alert type="info m">
|
||||
|
@ -47,6 +47,20 @@ module ExcelHelper
|
||||
[data, styles, types]
|
||||
end
|
||||
|
||||
def add_hardcoded_cells(index, hit, data, styles, types)
|
||||
if index.concerned_by_reservation_context?
|
||||
add_reservation_context_cell(hit, data, styles, types)
|
||||
end
|
||||
end
|
||||
|
||||
def add_reservation_context_cell(hit, data, styles, types)
|
||||
reservation_contexts = ReservationContext.pluck(:id, :name).to_h
|
||||
|
||||
data.push reservation_contexts[hit['_source']['reservationContextId']]
|
||||
styles.push nil
|
||||
types.push :text
|
||||
end
|
||||
|
||||
# append a cell containing the CA amount
|
||||
def add_ca_cell(index, hit, data, styles, types)
|
||||
return unless index.ca
|
||||
|
@ -200,6 +200,7 @@ module SettingsHelper
|
||||
projects_list_date_filters_presence
|
||||
project_categories_filter_placeholder
|
||||
project_categories_wording
|
||||
reservation_context_feature
|
||||
].freeze
|
||||
end
|
||||
# rubocop:enable Metrics/ModuleLength
|
||||
|
@ -13,6 +13,8 @@ class CartItem::MachineReservation < CartItem::Reservation
|
||||
|
||||
belongs_to :plan
|
||||
|
||||
belongs_to :reservation_context
|
||||
|
||||
def type
|
||||
'machine'
|
||||
end
|
||||
|
@ -83,6 +83,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
::Reservation.new(
|
||||
reservable_id: reservable_id,
|
||||
reservable_type: reservable_type,
|
||||
reservation_context_id: reservation_context_id,
|
||||
slots_reservations_attributes: slots_params,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: customer).id
|
||||
)
|
||||
|
@ -13,6 +13,8 @@ class CartItem::SpaceReservation < CartItem::Reservation
|
||||
|
||||
belongs_to :plan
|
||||
|
||||
belongs_to :reservation_context
|
||||
|
||||
def type
|
||||
'space'
|
||||
end
|
||||
|
@ -13,6 +13,8 @@ class CartItem::TrainingReservation < CartItem::Reservation
|
||||
|
||||
belongs_to :plan
|
||||
|
||||
belongs_to :reservation_context
|
||||
|
||||
def price
|
||||
base_amount = reservable&.amount_by_group(customer.group_id)&.amount
|
||||
is_privileged = operator.admin? || (operator.manager? && operator.id != customer.id)
|
||||
|
@ -6,6 +6,7 @@ module StatReservationConcern
|
||||
|
||||
included do
|
||||
attribute :reservationId, Integer
|
||||
attribute :reservationContextId, Integer
|
||||
attribute :ca, Float
|
||||
attribute :name, String
|
||||
end
|
||||
|
@ -23,6 +23,8 @@ class Reservation < ApplicationRecord
|
||||
|
||||
has_many :prepaid_pack_reservations, dependent: :destroy
|
||||
|
||||
belongs_to :reservation_context
|
||||
|
||||
validates :reservable_id, :reservable_type, presence: true
|
||||
validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) }
|
||||
validate :training_not_fully_reserved, if: -> { reservable.is_a?(Training) }
|
||||
|
27
app/models/reservation_context.rb
Normal file
27
app/models/reservation_context.rb
Normal file
@ -0,0 +1,27 @@
|
||||
class ReservationContext < ApplicationRecord
|
||||
has_many :reservations
|
||||
has_many :cart_item_reservations, dependent: :nullify, class_name: "CartItem::Reservation"
|
||||
|
||||
APPLICABLE_ON = %w[machine space training]
|
||||
|
||||
scope :applicable_on, ->(applicable_on) { where("applicable_on @> ?", "{#{applicable_on.presence_in(APPLICABLE_ON)}}")}
|
||||
|
||||
validates :name, presence: true
|
||||
validate :validate_applicable_on
|
||||
|
||||
def safe_destroy
|
||||
if reservations.count.zero?
|
||||
destroy
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_applicable_on
|
||||
return if applicable_on.all? { |applicable_on| applicable_on.in? APPLICABLE_ON }
|
||||
|
||||
errors.add(:applicable_on, :invalid)
|
||||
end
|
||||
end
|
@ -2,4 +2,11 @@ class StatisticIndex < ApplicationRecord
|
||||
has_many :statistic_types
|
||||
has_many :statistic_fields
|
||||
has_one :statistic_graph
|
||||
|
||||
def concerned_by_reservation_context?
|
||||
return false unless es_type_key.in? ReservationContext::APPLICABLE_ON
|
||||
return false unless Setting.get('reservation_context_feature')
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
7
app/policies/reservation_context_policy.rb
Normal file
7
app/policies/reservation_context_policy.rb
Normal file
@ -0,0 +1,7 @@
|
||||
class ReservationContextPolicy < ApplicationPolicy
|
||||
%w(create update destroy applicable_on_values).each do |action|
|
||||
define_method "#{action}?" do
|
||||
user.admin?
|
||||
end
|
||||
end
|
||||
end
|
@ -47,7 +47,7 @@ class SettingPolicy < ApplicationPolicy
|
||||
machines_banner_cta_url trainings_banner_active trainings_banner_text trainings_banner_cta_active trainings_banner_cta_label
|
||||
trainings_banner_cta_url events_banner_active events_banner_text events_banner_cta_active events_banner_cta_label
|
||||
events_banner_cta_url projects_list_member_filter_presence projects_list_date_filters_presence
|
||||
project_categories_filter_placeholder project_categories_wording]
|
||||
project_categories_filter_placeholder project_categories_wording reservation_context_feature]
|
||||
end
|
||||
|
||||
##
|
||||
|
@ -157,14 +157,16 @@ class CartService
|
||||
reservable: reservable,
|
||||
cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes],
|
||||
plan: plan_info[:plan],
|
||||
new_subscription: plan_info[:new_subscription])
|
||||
new_subscription: plan_info[:new_subscription],
|
||||
reservation_context_id: cart_item[:reservation_context_id])
|
||||
when Training
|
||||
CartItem::TrainingReservation.new(customer_profile: @customer.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
reservable: reservable,
|
||||
cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes],
|
||||
plan: plan_info[:plan],
|
||||
new_subscription: plan_info[:new_subscription])
|
||||
new_subscription: plan_info[:new_subscription],
|
||||
reservation_context_id: cart_item[:reservation_context_id])
|
||||
when Event
|
||||
CartItem::EventReservation.new(customer_profile: @customer.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
@ -178,7 +180,8 @@ class CartService
|
||||
reservable: reservable,
|
||||
cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes],
|
||||
plan: plan_info[:plan],
|
||||
new_subscription: plan_info[:new_subscription])
|
||||
new_subscription: plan_info[:new_subscription],
|
||||
reservation_context_id: cart_item[:reservation_context_id])
|
||||
else
|
||||
Rails.logger.warn "the reservable #{reservable} is not implemented"
|
||||
raise NotImplementedError
|
||||
|
@ -17,7 +17,9 @@ class Statistics::Builders::ReservationsBuilderService
|
||||
subType: r["#{category}_type".to_sym],
|
||||
ca: r[:ca],
|
||||
name: r["#{category}_name".to_sym],
|
||||
reservationId: r[:reservation_id] }.merge(user_info_stat(r)))
|
||||
reservationId: r[:reservation_id],
|
||||
reservationContextId: r[:reservation_context_id]
|
||||
}.merge(user_info_stat(r)))
|
||||
stat[:stat] = (type == 'booking' ? 1 : r[:nb_hours])
|
||||
stat["#{category}Id".to_sym] = r["#{category}_id".to_sym]
|
||||
|
||||
|
@ -46,7 +46,7 @@ class Statistics::FetcherService
|
||||
Reservation
|
||||
.where("reservable_type = 'Machine' AND slots_reservations.canceled_at IS NULL AND " \
|
||||
'reservations.created_at >= :start_date AND reservations.created_at <= :end_date', options)
|
||||
.eager_load(:slots, :slots_reservations, :invoice_items, statistic_profile: [:group])
|
||||
.eager_load(:slots, :slots_reservations, :invoice_items, :reservation_context, statistic_profile: [:group])
|
||||
.find_each do |r|
|
||||
next unless r.reservable
|
||||
|
||||
@ -58,7 +58,9 @@ class Statistics::FetcherService
|
||||
machine_name: r.reservable.name,
|
||||
slot_dates: r.slots.map(&:start_at).map(&:to_date),
|
||||
nb_hours: (r.slots.map(&:duration).map(&:to_i).reduce(:+) / 3600.0).to_f,
|
||||
ca: calcul_ca(r.original_invoice) }.merge(user_info(profile))
|
||||
ca: calcul_ca(r.original_invoice),
|
||||
reservation_context_id: r.reservation_context_id
|
||||
}.merge(user_info(profile))
|
||||
yield result
|
||||
end
|
||||
end
|
||||
@ -70,7 +72,7 @@ class Statistics::FetcherService
|
||||
Reservation
|
||||
.where("reservable_type = 'Space' AND slots_reservations.canceled_at IS NULL AND " \
|
||||
'reservations.created_at >= :start_date AND reservations.created_at <= :end_date', options)
|
||||
.eager_load(:slots, :slots_reservations, :invoice_items, statistic_profile: [:group])
|
||||
.eager_load(:slots, :slots_reservations, :invoice_items, :reservation_context, statistic_profile: [:group])
|
||||
.find_each do |r|
|
||||
next unless r.reservable
|
||||
|
||||
@ -82,7 +84,9 @@ class Statistics::FetcherService
|
||||
space_type: r.reservable.slug,
|
||||
slot_dates: r.slots.map(&:start_at).map(&:to_date),
|
||||
nb_hours: (r.slots.map(&:duration).map(&:to_i).reduce(:+) / 3600.0).to_f,
|
||||
ca: calcul_ca(r.original_invoice) }.merge(user_info(profile))
|
||||
ca: calcul_ca(r.original_invoice),
|
||||
reservation_context_id: r.reservation_context_id
|
||||
}.merge(user_info(profile))
|
||||
yield result
|
||||
end
|
||||
end
|
||||
@ -94,7 +98,7 @@ class Statistics::FetcherService
|
||||
Reservation
|
||||
.where("reservable_type = 'Training' AND slots_reservations.canceled_at IS NULL AND " \
|
||||
'reservations.created_at >= :start_date AND reservations.created_at <= :end_date', options)
|
||||
.eager_load(:slots, :slots_reservations, :invoice_items, statistic_profile: [:group])
|
||||
.eager_load(:slots, :slots_reservations, :invoice_items, :reservation_context, statistic_profile: [:group])
|
||||
.find_each do |r|
|
||||
next unless r.reservable
|
||||
|
||||
@ -107,7 +111,9 @@ class Statistics::FetcherService
|
||||
training_name: r.reservable.name,
|
||||
training_date: slot.start_at.to_date,
|
||||
nb_hours: difference_in_hours(slot.start_at, slot.end_at),
|
||||
ca: calcul_ca(r.original_invoice) }.merge(user_info(profile))
|
||||
ca: calcul_ca(r.original_invoice),
|
||||
reservation_context_id: r.reservation_context_id
|
||||
}.merge(user_info(profile))
|
||||
yield result
|
||||
end
|
||||
end
|
||||
|
6
app/views/api/reservation_contexts/index.json.jbuilder
Normal file
6
app/views/api/reservation_contexts/index.json.jbuilder
Normal file
@ -0,0 +1,6 @@
|
||||
user_is_admin = (current_user and current_user.admin?)
|
||||
|
||||
json.array!(@reservation_contexts) do |reservation_context|
|
||||
json.extract! reservation_context, :id, :name, :applicable_on
|
||||
json.related_to reservation_context.reservations.count if user_is_admin
|
||||
end
|
1
app/views/api/reservation_contexts/show.json.jbuilder
Normal file
1
app/views/api/reservation_contexts/show.json.jbuilder
Normal file
@ -0,0 +1 @@
|
||||
json.extract! @reservation_context, :id, :name, :applicable_on
|
@ -26,7 +26,9 @@ wb.add_worksheet(name: ExcelService.name_safe(index.label)) do |sheet|
|
||||
fields.each do |f|
|
||||
columns.push f.label
|
||||
end
|
||||
columns.push t('export.reservation_context') if index.concerned_by_reservation_context?
|
||||
columns.push t('export.revenue') if index.ca
|
||||
|
||||
sheet.add_row columns, style: header
|
||||
|
||||
# data rows
|
||||
@ -37,6 +39,7 @@ wb.add_worksheet(name: ExcelService.name_safe(index.label)) do |sheet|
|
||||
fields.each do |f|
|
||||
format_xlsx_cell(hit['_source'][f.key], data, styles, types, source_data_type: f.data_type, date_format: date)
|
||||
end
|
||||
add_hardcoded_cells(index, hit, data, styles, types)
|
||||
add_ca_cell(index, hit, data, styles, types)
|
||||
|
||||
sheet.add_row data, style: styles, types: types
|
||||
|
@ -35,6 +35,7 @@ indices.each do |index|
|
||||
index.statistic_fields.each do |f|
|
||||
format_xlsx_cell(hit['_source'][f.key], data, styles, types, source_data_type: f.data_type, date_format: date)
|
||||
end
|
||||
add_hardcoded_cells(index, hit, data, styles, types)
|
||||
# proceed the 'ca' field if requested
|
||||
add_ca_cell(index, hit, data, styles, types)
|
||||
|
||||
|
@ -1068,7 +1068,7 @@ en:
|
||||
currency_error: "The inputted value is not a valid currency"
|
||||
error_while_saving: "An error occurred while saving the currency: "
|
||||
currency_updated: "The PayZen currency was successfully updated to {CURRENCY}."
|
||||
# select a payment gateway
|
||||
#select a payment gateway
|
||||
select_gateway_modal:
|
||||
select_gateway_title: "Select a payment gateway"
|
||||
gateway_info: "To securely collect and process payments online, Fab-manager needs to use an third-party service authorized by the financial institutions, called a payment gateway."
|
||||
@ -1316,7 +1316,7 @@ en:
|
||||
invalidate_member_error: "An error occurred: impossible to invalidate from this member."
|
||||
supporting_documents: "Supporting documents"
|
||||
change_role: "Change role"
|
||||
# extend a subscription for free
|
||||
#extend a subscription for free
|
||||
free_extend_modal:
|
||||
extend_subscription: "Extend the subscription"
|
||||
offer_free_days_infos: "You are about to extend the user's subscription by offering him free additional days."
|
||||
@ -1534,6 +1534,7 @@ en:
|
||||
create_plans_to_start: "Start by creating new subscription plans."
|
||||
click_here: "Click here to create your first one."
|
||||
average_cart: "Average cart:"
|
||||
reservation_context: Reservation context
|
||||
#statistics graphs
|
||||
stats_graphs:
|
||||
statistics: "Statistics"
|
||||
@ -1783,6 +1784,15 @@ en:
|
||||
projects_list_date_filters_presence: "Presence of date filters on projects list"
|
||||
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
|
||||
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
|
||||
reservation_context_feature_title: Reservation context
|
||||
reservation_context_feature_info: "If you enable this feature, members will have to enter the context of their reservation when reserving."
|
||||
reservation_context_feature: "Enable the feature \"Reservation context\""
|
||||
reservation_context_options: Reservation context options
|
||||
add_a_reservation_context: Add a new context
|
||||
confirmation_required: Confirmation required
|
||||
do_you_really_want_to_delete_this_reservation_context: "Do you really want to delete this context?"
|
||||
unable_to_delete_reservation_context_already_related_to_reservations: "Unable to delete this context because it is already associated to a reservation"
|
||||
unable_to_delete_reservation_context_an_error_occured: "Unable to delete: an error occurred"
|
||||
overlapping_options:
|
||||
training_reservations: "Trainings"
|
||||
machine_reservations: "Machines"
|
||||
@ -2457,3 +2467,9 @@ en:
|
||||
cta_switch: "Display a button"
|
||||
cta_label: "Button label"
|
||||
cta_url: "Button link"
|
||||
reservation_contexts:
|
||||
name: "Name"
|
||||
applicable_on: "Applicable on"
|
||||
machine: Machine
|
||||
training: Training
|
||||
space: Space
|
||||
|
@ -386,7 +386,7 @@ fr:
|
||||
deleted_user: "Utilisateur supprimé"
|
||||
select_type: "Veuillez sélectionner un type pour continuer"
|
||||
no_modules_available: "Aucun module réservable n'est disponible. Veuillez activer au moins un module (machines, espaces ou formations) dans la section Personnalisation."
|
||||
#import external iCal calendar
|
||||
# import external iCal calendar
|
||||
icalendar:
|
||||
icalendar_import: "Import iCalendar"
|
||||
intro: "Fab-manager vous permet d'importer automatiquement des événements de calendrier, au format iCalendar RFC 5545, depuis des URL externes. Ces URL seront synchronisée toutes les heures et les événements seront affichés dans le calendrier publique. Vous pouvez aussi déclencher une synchronisation en cliquant sur le bouton correspondant, en face de chaque import."
|
||||
@ -1534,6 +1534,7 @@ fr:
|
||||
create_plans_to_start: "Pour commencer, créez de nouvelles formules d'abonnement."
|
||||
click_here: "Cliquez ici pour créer votre première formule."
|
||||
average_cart: "Panier moyen :"
|
||||
reservation_context: Nature de la réservation
|
||||
#statistics graphs
|
||||
stats_graphs:
|
||||
statistics: "Statistiques"
|
||||
@ -1783,6 +1784,15 @@ fr:
|
||||
projects_list_date_filters_presence: "Permettre la recherche de projets par dates"
|
||||
project_categories_filter_placeholder: "Dans la galerie de projets, renommer le filtre \"Toutes les catégories\""
|
||||
project_categories_wording: "Dans la fiche projet, renommer l'intitulé de l'encart Catégories"
|
||||
reservation_context_feature_title: Natures des réservations
|
||||
reservation_context_feature_info: "Si vous activez cette fonctionnalité, le membre devra obligatoirement saisir la nature de sa réservation au moment de réserver."
|
||||
reservation_context_feature: Activer la saisie obligatoire de la nature des réservations
|
||||
reservation_context_options: Natures des réservations possibles
|
||||
add_a_reservation_context: Ajouter une nouvelle nature
|
||||
confirmation_required: Confirmation requise
|
||||
do_you_really_want_to_delete_this_reservation_context: "Êtes-vous sûr de vouloir supprimer cette nature de réservation ?"
|
||||
unable_to_delete_reservation_context_already_related_to_reservations: "Impossible de supprimer cette nature de réservation car elle est actuellement associée à des réservations."
|
||||
unable_to_delete_reservation_context_an_error_occured: "Impossible de supprimer : une erreur est survenue."
|
||||
overlapping_options:
|
||||
training_reservations: "Formations"
|
||||
machine_reservations: "Machines"
|
||||
@ -2457,3 +2467,9 @@ fr:
|
||||
cta_switch: "Afficher un bouton"
|
||||
cta_label: "Libellé du bouton"
|
||||
cta_url: "Lien du bouton"
|
||||
reservation_contexts:
|
||||
name: "Nom"
|
||||
applicable_on: "Applicable à"
|
||||
machine: Machine
|
||||
training: Formation
|
||||
space: Espace
|
||||
|
@ -372,12 +372,14 @@ en:
|
||||
user_tags: "User tags"
|
||||
no_tags: "No tags"
|
||||
user_validation_required_alert: "Warning!<br>Your administrator must validate your account. Then, you'll then be able to access all the booking features."
|
||||
# feature-tour modal
|
||||
select_the_reservation_context: "Select the context of the reservation"
|
||||
please_select_a_reservation_context: "Please select the context of the reservation first"
|
||||
#feature-tour modal
|
||||
tour:
|
||||
previous: "Previous"
|
||||
next: "Next"
|
||||
end: "End the tour"
|
||||
# help modal
|
||||
#help modal
|
||||
help:
|
||||
title: "Help"
|
||||
what_to_do: "What do you want to do?"
|
||||
@ -386,7 +388,7 @@ en:
|
||||
stripe_confirm_modal:
|
||||
resolve_action: "Resolve the action"
|
||||
ok_button: "OK"
|
||||
# 2nd factor authentication for card payments
|
||||
#2nd factor authentication for card payments
|
||||
stripe_confirm:
|
||||
pending: "Pending for action..."
|
||||
success: "Thank you, your card setup is complete. The payment will be proceeded shortly."
|
||||
|
@ -372,6 +372,8 @@ fr:
|
||||
user_tags: "Étiquettes de l'utilisateur"
|
||||
no_tags: "Aucune étiquette"
|
||||
user_validation_required_alert: "Attention !<br>Votre administrateur doit valider votre compte. Vous pourrez alors accéder à l'ensemble des fonctionnalités de réservation."
|
||||
select_the_reservation_context: Sélectionnez la nature de la réservation
|
||||
please_select_a_reservation_context: "Veuillez tout d'abord sélectionner la nature de la réservation"
|
||||
#feature-tour modal
|
||||
tour:
|
||||
previous: "Précédent"
|
||||
|
@ -1,5 +1,5 @@
|
||||
en:
|
||||
time:
|
||||
formats:
|
||||
# See http://apidock.com/ruby/DateTime/strftime for a list of available directives
|
||||
#See http://apidock.com/ruby/DateTime/strftime for a list of available directives
|
||||
hour_minute: "%I:%M %p"
|
@ -505,6 +505,7 @@ en:
|
||||
male: "Man"
|
||||
female: "Woman"
|
||||
deleted_user: "Deleted user"
|
||||
reservation_context: "Reservation context"
|
||||
#initial price's category for events, created to replace the old "reduced amount" property
|
||||
price_category:
|
||||
reduced_fare: "Reduced fare"
|
||||
@ -701,6 +702,7 @@ en:
|
||||
projects_list_date_filters_presence: "Presence of dates filter on projects list"
|
||||
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
|
||||
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
|
||||
reservation_context_feature: "Force member to select the nature of his reservation when reserving"
|
||||
#statuses of projects
|
||||
statuses:
|
||||
new: "New"
|
||||
|
@ -505,6 +505,7 @@ fr:
|
||||
male: "Homme"
|
||||
female: "Femme"
|
||||
deleted_user: "Utilisateur supprimé"
|
||||
reservation_context: "Nature de la réservation"
|
||||
#initial price's category for events, created to replace the old "reduced amount" property
|
||||
price_category:
|
||||
reduced_fare: "Tarif réduit"
|
||||
@ -701,6 +702,7 @@ fr:
|
||||
projects_list_date_filters_presence: "Filtre de présence de dates sur la liste des projets"
|
||||
project_categories_filter_placeholder: "Dans la galerie de projets, renommer le filtre \"Toutes les catégories\""
|
||||
project_categories_wording: "Dans la fiche projet, renommer l'intitulé de l'encart Catégories"
|
||||
reservation_context_feature: "Obligation pour le membre de saisir la nature de sa réservation au moment de réserver"
|
||||
#statuses of projects
|
||||
statuses:
|
||||
new: "Nouveau"
|
||||
|
@ -217,6 +217,9 @@ Rails.application.routes.draw do
|
||||
end
|
||||
# export accounting data to csv or equivalent
|
||||
post 'accounting/export' => 'accounting_exports#export'
|
||||
resources :reservation_contexts do
|
||||
get :applicable_on_values, on: :collection
|
||||
end
|
||||
|
||||
# i18n
|
||||
# regex allows using dots in URL for 'state'
|
||||
|
10
db/migrate/20230718133636_create_reservation_contexts.rb
Normal file
10
db/migrate/20230718133636_create_reservation_contexts.rb
Normal file
@ -0,0 +1,10 @@
|
||||
class CreateReservationContexts < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :reservation_contexts do |t|
|
||||
t.string :name
|
||||
t.string :applicable_on, array: true, default: []
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
class AddReservationContextIdToReservation < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_reference :reservations, :reservation_context, index: true, foreign_key: true
|
||||
end
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
class AddReservationContextIdToCartItemReservations < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_reference :cart_item_reservations, :reservation_context, foreign_key: true
|
||||
end
|
||||
end
|
@ -733,3 +733,5 @@ Setting.set('projects_list_member_filter_presence', false) unless Setting.find_b
|
||||
Setting.set('projects_list_date_filters_presence', false) unless Setting.find_by(name: 'projects_list_date_filters_presence')
|
||||
Setting.set('project_categories_filter_placeholder', 'Toutes les catégories') unless Setting.find_by(name: 'project_categories_filter_placeholder').try(:value)
|
||||
Setting.set('project_categories_wording', 'Catégories') unless Setting.find_by(name: 'project_categories_wording').try(:value)
|
||||
|
||||
Setting.set('reservation_context_feature', false) unless Setting.find_by(name: 'reservation_context_feature')
|
||||
|
@ -762,7 +762,8 @@ CREATE TABLE public.cart_item_reservations (
|
||||
operator_profile_id bigint,
|
||||
type character varying,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone NOT NULL
|
||||
updated_at timestamp without time zone NOT NULL,
|
||||
reservation_context_id bigint
|
||||
);
|
||||
|
||||
|
||||
@ -3009,6 +3010,38 @@ CREATE SEQUENCE public.projects_themes_id_seq
|
||||
ALTER SEQUENCE public.projects_themes_id_seq OWNED BY public.projects_themes.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservation_contexts; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public.reservation_contexts (
|
||||
id bigint NOT NULL,
|
||||
name character varying,
|
||||
applicable_on character varying[] DEFAULT '{}'::character varying[],
|
||||
created_at timestamp(6) without time zone NOT NULL,
|
||||
updated_at timestamp(6) without time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservation_contexts_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.reservation_contexts_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservation_contexts_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.reservation_contexts_id_seq OWNED BY public.reservation_contexts.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservations; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
@ -3021,7 +3054,8 @@ CREATE TABLE public.reservations (
|
||||
reservable_id integer,
|
||||
reservable_type character varying,
|
||||
nb_reserve_places integer,
|
||||
statistic_profile_id integer
|
||||
statistic_profile_id integer,
|
||||
reservation_context_id bigint
|
||||
);
|
||||
|
||||
|
||||
@ -4865,6 +4899,13 @@ ALTER TABLE ONLY public.projects_spaces ALTER COLUMN id SET DEFAULT nextval('pub
|
||||
ALTER TABLE ONLY public.projects_themes ALTER COLUMN id SET DEFAULT nextval('public.projects_themes_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservation_contexts id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.reservation_contexts ALTER COLUMN id SET DEFAULT nextval('public.reservation_contexts_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservations id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
@ -5788,6 +5829,14 @@ ALTER TABLE ONLY public.projects_themes
|
||||
ADD CONSTRAINT projects_themes_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservation_contexts reservation_contexts_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.reservation_contexts
|
||||
ADD CONSTRAINT reservation_contexts_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservations reservations_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@ -6287,6 +6336,13 @@ CREATE INDEX index_cart_item_reservations_on_plan_id ON public.cart_item_reserva
|
||||
CREATE INDEX index_cart_item_reservations_on_reservable ON public.cart_item_reservations USING btree (reservable_type, reservable_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_cart_item_reservations_on_reservation_context_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_cart_item_reservations_on_reservation_context_id ON public.cart_item_reservations USING btree (reservation_context_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_cart_item_slots_on_cart_item; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@ -7036,6 +7092,13 @@ CREATE INDEX index_projects_themes_on_theme_id ON public.projects_themes USING b
|
||||
CREATE INDEX index_reservations_on_reservable_type_and_reservable_id ON public.reservations USING btree (reservable_type, reservable_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_reservations_on_reservation_context_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_reservations_on_reservation_context_id ON public.reservations USING btree (reservation_context_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_reservations_on_statistic_profile_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@ -8260,6 +8323,14 @@ ALTER TABLE ONLY public.accounting_periods
|
||||
ADD CONSTRAINT fk_rails_cc9abff81f FOREIGN KEY (closed_by) REFERENCES public.users(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservations fk_rails_cfaaf202e7; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.reservations
|
||||
ADD CONSTRAINT fk_rails_cfaaf202e7 FOREIGN KEY (reservation_context_id) REFERENCES public.reservation_contexts(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: wallet_transactions fk_rails_d07bc24ce3; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@ -8308,6 +8379,14 @@ ALTER TABLE ONLY public.payment_schedule_items
|
||||
ADD CONSTRAINT fk_rails_d6030dd0e7 FOREIGN KEY (payment_schedule_id) REFERENCES public.payment_schedules(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: cart_item_reservations fk_rails_daa5b1b8b9; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.cart_item_reservations
|
||||
ADD CONSTRAINT fk_rails_daa5b1b8b9 FOREIGN KEY (reservation_context_id) REFERENCES public.reservation_contexts(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: product_stock_movements fk_rails_dc802d5f48; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@ -8818,6 +8897,9 @@ INSERT INTO "schema_migrations" (version) VALUES
|
||||
('20230328094808'),
|
||||
('20230328094809'),
|
||||
('20230626122844'),
|
||||
('20230626122947');
|
||||
('20230626122947'),
|
||||
('20230718133636'),
|
||||
('20230718134350'),
|
||||
('20230720085857');
|
||||
|
||||
|
||||
|
14
test/fixtures/reservation_contexts.yml
vendored
Normal file
14
test/fixtures/reservation_contexts.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
reservation_context_1:
|
||||
id: 1
|
||||
name: Pédagogique
|
||||
applicable_on: [machine, training, space]
|
||||
created_at: 2023-07-20 15:39:08.259759000 Z
|
||||
updated_at: 2023-07-20 15:39:08.259759000 Z
|
||||
|
||||
reservation_context_2:
|
||||
id: 2
|
||||
name: Recherche
|
||||
applicable_on: [machine, training, space]
|
||||
created_at: 2023-07-20 15:39:08.265840000 Z
|
||||
updated_at: 2023-07-20 15:39:08.265840000 Z
|
6
test/fixtures/settings.yml
vendored
6
test/fixtures/settings.yml
vendored
@ -610,3 +610,9 @@ setting_103:
|
||||
name: projects_list_date_filters_presence
|
||||
created_at: 2023-04-05 09:16:08.000511500 Z
|
||||
updated_at: 2023-04-05 09:16:08.000511500 Z
|
||||
|
||||
setting_104:
|
||||
id: 104
|
||||
name: reservation_context_feature
|
||||
created_at: 2023-04-05 09:16:08.000511500 Z
|
||||
updated_at: 2023-04-05 09:16:08.000511500 Z
|
||||
|
8
test/frontend/__fixtures__/reservation_contexts.ts
Normal file
8
test/frontend/__fixtures__/reservation_contexts.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { ReservationContext } from 'models/reservation';
|
||||
|
||||
const reservationContexts: Array<ReservationContext> = [
|
||||
{ id: 1, name: 'Research' },
|
||||
{ id: 2, name: 'Pedagogical' }
|
||||
];
|
||||
|
||||
export default reservationContexts;
|
@ -849,6 +849,12 @@ export const settings: Array<Setting> = [
|
||||
value: 'Catégories',
|
||||
last_update: '2022-12-23T14:39:12+0100',
|
||||
localized: 'Project categories overridden name'
|
||||
},
|
||||
{
|
||||
name: 'reservation_context_feature',
|
||||
value: 'false',
|
||||
last_update: '2022-12-23T14:39:12+0100',
|
||||
localized: 'Reservation context feature'
|
||||
}
|
||||
];
|
||||
|
||||
|
88
test/integration/reservation_contexts_test.rb
Normal file
88
test/integration/reservation_contexts_test.rb
Normal file
@ -0,0 +1,88 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ReservationContextsTest < ActionDispatch::IntegrationTest
|
||||
def setup
|
||||
@admin = User.find_by(username: 'admin')
|
||||
login_as(@admin, scope: :user)
|
||||
end
|
||||
|
||||
test 'create a reservation_context' do
|
||||
applicable_on = ["machine", "space", "training"]
|
||||
post '/api/reservation_contexts',
|
||||
params: {
|
||||
name: 'Enseignant',
|
||||
applicable_on: applicable_on
|
||||
}.to_json,
|
||||
headers: default_headers
|
||||
|
||||
# Check response format & reservation_context
|
||||
assert_equal 201, response.status, response.body
|
||||
assert_match Mime[:json].to_s, response.content_type
|
||||
|
||||
# Check the correct reservation_context was created
|
||||
res = json_response(response.body)
|
||||
reservation_context = ReservationContext.where(id: res[:id]).first
|
||||
assert_not_nil reservation_context, 'reservation_context was not created in database'
|
||||
|
||||
assert_equal 'Enseignant', res[:name]
|
||||
assert_equal applicable_on, res[:applicable_on]
|
||||
end
|
||||
|
||||
test 'update a reservation_context' do
|
||||
applicable_on = ["machine"]
|
||||
patch '/api/reservation_contexts/1',
|
||||
params: {
|
||||
name: 'Nouveau nom',
|
||||
applicable_on: applicable_on
|
||||
}.to_json,
|
||||
headers: default_headers
|
||||
|
||||
# Check response format & reservation_context
|
||||
assert_equal 200, response.status, response.body
|
||||
assert_match Mime[:json].to_s, response.content_type
|
||||
|
||||
# Check the reservation_context was updated
|
||||
res = json_response(response.body)
|
||||
assert_equal 1, res[:id]
|
||||
assert_equal 'Nouveau nom', res[:name]
|
||||
assert_equal applicable_on, res[:applicable_on]
|
||||
end
|
||||
|
||||
test 'list all reservation_contexts' do
|
||||
logout @admin
|
||||
get '/api/reservation_contexts'
|
||||
|
||||
# Check response format & reservation_context
|
||||
assert_equal 200, response.status, response.body
|
||||
assert_match Mime[:json].to_s, response.content_type
|
||||
|
||||
# Check the list items are ok
|
||||
reservation_contexts = json_response(response.body)
|
||||
assert_equal ReservationContext.count, reservation_contexts.count
|
||||
assert_equal reservation_contexts(:reservation_context_1).name, reservation_contexts[0][:name]
|
||||
end
|
||||
|
||||
test "list all applicable_on possible values" do
|
||||
get '/api/reservation_contexts/applicable_on_values'
|
||||
|
||||
# Check response format & reservation_context
|
||||
assert_equal 200, response.status, response.body
|
||||
assert_match Mime[:json].to_s, response.content_type
|
||||
|
||||
# Check the list items are ok
|
||||
applicable_on_values = json_response(response.body)
|
||||
assert_equal %w[machine space training], applicable_on_values
|
||||
end
|
||||
|
||||
test 'delete a reservation_context' do
|
||||
reservation_context = ReservationContext.create!(name: 'Gone too soon')
|
||||
delete "/api/reservation_contexts/#{reservation_context.id}"
|
||||
assert_response :success
|
||||
assert_empty response.body
|
||||
assert_raise ActiveRecord::RecordNotFound do
|
||||
reservation_context.reload
|
||||
end
|
||||
end
|
||||
end
|
44
test/meta/i18n_test.rb
Normal file
44
test/meta/i18n_test.rb
Normal file
@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class I18nTest < ActiveSupport::TestCase
|
||||
# Do not use I18n.default_locale, reference locale can be different
|
||||
REFERENCE_LOCALE = :fr
|
||||
LOCALES_TO_CHECK = [:en]
|
||||
|
||||
SKIP_FILES = %w[
|
||||
devise
|
||||
rails
|
||||
].freeze
|
||||
|
||||
FILE_IDS = Rails.root.glob("config/locales/*.#{REFERENCE_LOCALE}.yml")
|
||||
.map { File.basename(_1) }
|
||||
.map { _1.split(".")[0..-3].join(".") } # view.admin.fr.yml -> view.admin
|
||||
.reject { SKIP_FILES.include?(_1) }
|
||||
|
||||
LOCALES_TO_CHECK.each do |locale|
|
||||
FILE_IDS.each do |file_id|
|
||||
test "#{file_id}.#{locale}.yml have same keys as #{file_id}.#{REFERENCE_LOCALE}.yml" do
|
||||
reference_keys = read_keys(REFERENCE_LOCALE, file_id)
|
||||
locale_keys = read_keys(locale, file_id)
|
||||
|
||||
reference_keys.each_with_index do |reference_key, index|
|
||||
next if index.zero?
|
||||
|
||||
unless reference_key[0] == "#"
|
||||
assert_equal reference_key, locale_keys[index], "invalid key at line #{index + 1}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def read_keys(locale, file_id)
|
||||
file = Rails.root.join("config/locales/#{file_id}.#{locale}.yml")
|
||||
|
||||
File.read(file)
|
||||
.split("\n")
|
||||
.map { _1.split(":", 2).first.to_s.strip }
|
||||
end
|
||||
end
|
60
test/models/reservation_context_test.rb
Normal file
60
test/models/reservation_context_test.rb
Normal file
@ -0,0 +1,60 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ReservationContextTest < ActiveSupport::TestCase
|
||||
test 'fixtures are valid' do
|
||||
ReservationContext.find_each do |reservation_context|
|
||||
assert reservation_context.valid?
|
||||
end
|
||||
end
|
||||
|
||||
test "applicable_on validation" do
|
||||
reservation_context = reservation_contexts(:reservation_context_1)
|
||||
|
||||
reservation_context.applicable_on << "wrong"
|
||||
|
||||
assert reservation_context.invalid?
|
||||
assert_equal reservation_context.errors.details, { applicable_on: [{ error: :invalid }] }
|
||||
end
|
||||
|
||||
test "name validation" do
|
||||
reservation_context = reservation_contexts(:reservation_context_1)
|
||||
|
||||
reservation_context.name = nil
|
||||
|
||||
assert reservation_context.invalid?
|
||||
assert_equal reservation_context.errors.details, { name: [{ error: :blank }] }
|
||||
end
|
||||
|
||||
test "#safe_destroy" do
|
||||
reservation_context = reservation_contexts(:reservation_context_1)
|
||||
reservation = reservations(:reservation_1).tap { |r| r.update!(reservation_context: reservation_context) }
|
||||
|
||||
assert_not reservation_context.safe_destroy
|
||||
|
||||
reservation.update!(reservation_context_id: nil)
|
||||
|
||||
assert reservation_context.safe_destroy
|
||||
assert reservation_context.destroyed?
|
||||
end
|
||||
|
||||
test "scope applicable_on" do
|
||||
assert_equal reservation_contexts(:reservation_context_1, :reservation_context_2), ReservationContext.applicable_on("space")
|
||||
|
||||
reservation_context = reservation_contexts(:reservation_context_1)
|
||||
reservation_context.applicable_on.delete("space")
|
||||
reservation_context.save!
|
||||
|
||||
assert_equal [reservation_contexts(:reservation_context_2)], ReservationContext.applicable_on("space")
|
||||
end
|
||||
|
||||
test "foreign key on reservations" do
|
||||
reservation_context = reservation_contexts(:reservation_context_1)
|
||||
reservation = reservations(:reservation_1).tap { |r| r.update!(reservation_context: reservation_context) }
|
||||
|
||||
assert_raise ActiveRecord::InvalidForeignKey do
|
||||
reservation_context.destroy!
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user