mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-19 13:54:25 +01:00
Ability to configure data sources for preventing booking on overlapping slots
This commit is contained in:
parent
b1245a5248
commit
840c536c75
@ -1,6 +1,7 @@
|
||||
# Changelog Fab-manager
|
||||
|
||||
- Refactored subscription new/renew/free extend interfaces and API
|
||||
- Refactored subscription new/renew/free extend interfaces and API
|
||||
- Ability to configure data sources for preventing booking on overlapping slots
|
||||
- Updated production documentation
|
||||
- Updated SSO documentation
|
||||
- Improved stripe subscription process with better error handling
|
||||
@ -23,6 +24,7 @@
|
||||
- Fix a security issue: updated nokogiri to 1.12.5 to fix [CVE-2021-41098](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-41098)
|
||||
- Fix a security issue: updated puma to 4.3.9 to fix [CVE-2021-41136](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-41136)
|
||||
- Fix a security issue: updated sidekiq to 6.2.1 to fix [CVE-2021-30151](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-30151)
|
||||
- [TODO DEPLOY] `rails db:seed`
|
||||
|
||||
## v5.1.10 2021 October 04
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { IApplication } from '../../models/application';
|
||||
import { react2angular } from 'react2angular';
|
||||
import SettingAPI from '../../api/setting';
|
||||
import { Loader } from '../base/loader';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -12,23 +13,32 @@ interface CheckListSettingProps {
|
||||
name: SettingName,
|
||||
label: string,
|
||||
className?: string,
|
||||
allSettings: Record<SettingName, string>,
|
||||
availableOptions: Array<string>,
|
||||
// availableOptions must be like this [['option1', 'label 1'], ['option2', 'label 2']]
|
||||
availableOptions: Array<Array<string>>,
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
}
|
||||
|
||||
const CheckListSetting: React.FC<CheckListSettingProps> = ({ name, label, className, allSettings, availableOptions, onSuccess, onError }) => {
|
||||
/**
|
||||
* This component allows to configure multiples values for a setting, like a check list.
|
||||
* The result is stored as a string, composed of the checked values, e.g. 'option1,option2'
|
||||
*/
|
||||
const CheckListSetting: React.FC<CheckListSettingProps> = ({ name, label, className, availableOptions, onSuccess, onError }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [value, setValue] = useState<string>(null);
|
||||
|
||||
// on component load, we retrieve the current value of the list from the API
|
||||
useEffect(() => {
|
||||
if (!allSettings) return;
|
||||
|
||||
setValue(allSettings[name]);
|
||||
}, [allSettings]);
|
||||
SettingAPI.get(name)
|
||||
.then(res => setValue(res.value))
|
||||
.catch(err => onError(err));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Callback triggered when a checkbox is ticked or unticked.
|
||||
* This function construct the resulting string, by adding or deleting the provided option identifier.
|
||||
*/
|
||||
const toggleCheckbox = (option: string) => {
|
||||
return (event: BaseSyntheticEvent) => {
|
||||
if (event.target.checked) {
|
||||
@ -42,34 +52,41 @@ const CheckListSetting: React.FC<CheckListSettingProps> = ({ name, label, classN
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the 'save' button is clicked.
|
||||
* Save the built string to the Setting API
|
||||
*/
|
||||
const handleSave = () => {
|
||||
SettingAPI.update(name, value)
|
||||
.then(() => onSuccess(t('app.admin.check_list_setting.customization_of_SETTING_successfully_saved', { SETTING: t(`app.admin.settings.${name}`) })))
|
||||
.catch(err => onError(err));
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify if the provided option is currently ticked (i.e. included in the value string)
|
||||
*/
|
||||
const isChecked = (option) => {
|
||||
return value?.includes(option);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`check-list-setting ${className || ''}`}>
|
||||
<span className="check-list-title">{label}</span>
|
||||
{availableOptions.map(option => <div key={option}>
|
||||
<input id={`setting-${name}-${option}`} type="checkbox" checked={isChecked(option)} onChange={toggleCheckbox(option)} />
|
||||
<label htmlFor={`setting-${name}-${option}`}>{option}</label>
|
||||
<h4 className="check-list-title">{label}</h4>
|
||||
{availableOptions.map(option => <div key={option[0]}>
|
||||
<input id={`setting-${name}-${option[0]}`} type="checkbox" checked={isChecked(option[0])} onChange={toggleCheckbox(option[0])} />
|
||||
<label htmlFor={`setting-${name}-${option[0]}`}>{option[1]}</label>
|
||||
</div>)}
|
||||
<button className="save" onClick={handleSave}>{t('app.admin.buttons.save')}</button>
|
||||
<FabButton className="save" onClick={handleSave}>{t('app.admin.check_list_setting.save')}</FabButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CheckListSettingWrapper: React.FC<CheckListSettingProps> = ({ allSettings, availableOptions, onSuccess, onError, label, className, name }) => {
|
||||
const CheckListSettingWrapper: React.FC<CheckListSettingProps> = ({ availableOptions, onSuccess, onError, label, className, name }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<CheckListSetting allSettings={allSettings} availableOptions={availableOptions} label={label} name={name} onError={onError} onSuccess={onSuccess} className={className} />
|
||||
<CheckListSetting availableOptions={availableOptions} label={label} name={name} onError={onError} onSuccess={onSuccess} className={className} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('checkListSetting', react2angular(CheckListSettingWrapper, ['allSettings', 'className', 'name', 'label', 'availableOptions', 'onSuccess', 'onError']));
|
||||
Application.Components.component('checkListSetting', react2angular(CheckListSettingWrapper, ['className', 'name', 'label', 'availableOptions', 'onSuccess', 'onError']));
|
||||
|
@ -328,6 +328,16 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for allow/prevent book overlapping slots: which kind of slots are used in the overlapping computation
|
||||
*/
|
||||
$scope.availableOverlappingOptions = [
|
||||
['training_reservations', _t('app.admin.settings.overlapping_options.training_reservations')],
|
||||
['machine_reservations', _t('app.admin.settings.overlapping_options.machine_reservations')],
|
||||
['space_reservations', _t('app.admin.settings.overlapping_options.space_reservations')],
|
||||
['events_reservations', _t('app.admin.settings.overlapping_options.events_reservations')]
|
||||
];
|
||||
|
||||
/**
|
||||
* Setup the feature-tour for the admin/settings page.
|
||||
* This is intended as a contextual help (when pressing F1)
|
||||
|
@ -456,12 +456,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
* @param callback {function}
|
||||
*/
|
||||
const validateSameTimeReservations = function (slot, callback) {
|
||||
let sameTimeReservations = [
|
||||
'training_reservations',
|
||||
'machine_reservations',
|
||||
'space_reservations',
|
||||
'events_reservations'
|
||||
].map(function (k) {
|
||||
$scope.settings.overlapping_categories.split(',').map(function (k) {
|
||||
return _.filter($scope.user[k], function (r) {
|
||||
return slot.start.isSame(r.start_at) ||
|
||||
(slot.end.isAfter(r.start_at) && slot.end.isBefore(r.end_at)) ||
|
||||
|
@ -111,6 +111,7 @@ export enum SettingName {
|
||||
PublicAgendaModule = 'public_agenda_module',
|
||||
RenewPackThreshold = 'renew_pack_threshold',
|
||||
PackOnlyForSubscription = 'pack_only_for_subscription',
|
||||
OverlappingCategories = 'overlapping_categories'
|
||||
}
|
||||
|
||||
export type SettingValue = string|boolean|number;
|
||||
|
@ -365,7 +365,7 @@ 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']"
|
||||
"'online_payment_module', 'payment_gateway', 'overlapping_categories']"
|
||||
}).$promise;
|
||||
}]
|
||||
}
|
||||
@ -451,7 +451,7 @@ angular.module('application.router', ['ui.router'])
|
||||
return Setting.query({
|
||||
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']"
|
||||
"'space_explications_alert', 'online_payment_module', 'payment_gateway', 'overlapping_categories']"
|
||||
}).$promise;
|
||||
}]
|
||||
}
|
||||
@ -505,7 +505,7 @@ 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']"
|
||||
"'payment_gateway', 'overlapping_categories']"
|
||||
}).$promise;
|
||||
}]
|
||||
}
|
||||
@ -534,7 +534,7 @@ angular.module('application.router', ['ui.router'])
|
||||
resolve: {
|
||||
subscriptionExplicationsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'subscription_explications_alert' }).$promise; }],
|
||||
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['online_payment_module', 'payment_gateway']" }).$promise; }]
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['online_payment_module', 'payment_gateway', 'overlapping_categories']" }).$promise; }]
|
||||
}
|
||||
})
|
||||
|
||||
@ -1080,7 +1080,7 @@ angular.module('application.router', ['ui.router'])
|
||||
"'reminder_delay', 'visibility_yearly', 'visibility_others', 'wallet_module', 'trainings_module', " +
|
||||
"'display_name_enable', 'machines_sort_by', 'fab_analytics', 'statistics_module', 'address_required', " +
|
||||
"'link_name', 'home_content', 'home_css', 'phone_required', 'upcoming_events_shown', 'public_agenda_module'," +
|
||||
"'renew_pack_threshold', 'pack_only_for_subscription']"
|
||||
"'renew_pack_threshold', 'pack_only_for_subscription', 'overlapping_categories']"
|
||||
}).$promise;
|
||||
}],
|
||||
privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }],
|
||||
|
@ -63,6 +63,7 @@
|
||||
@import "modules/pricing/pack-form";
|
||||
@import "modules/pricing/delete-pack";
|
||||
@import "modules/pricing/edit-pack";
|
||||
@import "modules/settings/check-list-setting";
|
||||
@import "modules/prepaid-packs/propose-packs-modal";
|
||||
@import "modules/prepaid-packs/packs-summary";
|
||||
@import "modules/subscriptions/free-extend-modal";
|
||||
|
@ -0,0 +1,14 @@
|
||||
.check-list-setting {
|
||||
.check-list-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.save {
|
||||
background-color: #999;
|
||||
border-color: #999;
|
||||
}
|
||||
}
|
@ -91,12 +91,14 @@
|
||||
label="app.admin.settings.allow_booking"
|
||||
classes="m-l">
|
||||
</boolean-setting>
|
||||
<div class="alert alert-warning" ng-show="allSettings.book_overlapping_slots !== 'true'" translate>
|
||||
{{ 'app.admin.settings.overlapping_categories_info' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<check-list-setting name="overlapping_categories"
|
||||
<div class="col-md-6" ng-show="allSettings.book_overlapping_slots !== 'true'">
|
||||
<check-list-setting name="'overlapping_categories'"
|
||||
label="'app.admin.settings.overlapping_categories' | translate"
|
||||
all-setting="allSettings"
|
||||
available-options="['training_reservations','machine_reservations','space_reservations','events_reservations']"
|
||||
available-options="availableOverlappingOptions"
|
||||
on-success="onSuccess"
|
||||
on-error="onError">
|
||||
</check-list-setting>
|
||||
|
@ -122,8 +122,11 @@ class Setting < ApplicationRecord
|
||||
renew_pack_threshold
|
||||
pack_only_for_subscription
|
||||
overlapping_categories] }
|
||||
# WARNING: when adding a new key, you may also want to add it in app/policies/setting_policy.rb#public_whitelist
|
||||
# and in config/locales/en.yml#settings
|
||||
# WARNING: when adding a new key, you may also want to add it in:
|
||||
# - config/locales/en.yml#settings
|
||||
# - app/frontend/src/javascript/models/setting.ts#SettingName
|
||||
# - db/seeds.rb (to set the default value)
|
||||
# - app/policies/setting_policy.rb#public_whitelist (if the setting can be read by anyone)
|
||||
|
||||
def value
|
||||
last_value = history_values.order(HistoryValue.arel_table['created_at'].desc).limit(1).first
|
||||
|
@ -40,7 +40,7 @@ class SettingPolicy < ApplicationPolicy
|
||||
recaptcha_site_key feature_tour_display disqus_shortname allowed_cad_extensions openlab_app_id openlab_default
|
||||
online_payment_module stripe_public_key confirmation_required wallet_module trainings_module address_required
|
||||
payment_gateway payzen_endpoint payzen_public_key public_agenda_module renew_pack_threshold statistics_module
|
||||
pack_only_for_subscription]
|
||||
pack_only_for_subscription overlapping_categories]
|
||||
end
|
||||
|
||||
##
|
||||
|
@ -1181,6 +1181,8 @@ en:
|
||||
an_error_occurred_saving_the_setting: "An error occurred while saving the setting. Please try again later."
|
||||
book_overlapping_slots_info: "Allow / prevent the reservation of overlapping slots"
|
||||
allow_booking: "Allow booking"
|
||||
overlapping_categories: "Overlapping categories"
|
||||
overlapping_categories_info: "Preventing booking on overlapping slots will be done by comparing the date and time of the following categories of reservations."
|
||||
default_slot_duration: "Default duration for slots"
|
||||
duration_minutes: "Duration (in minutes)"
|
||||
default_slot_duration_info: "Machine and space availabilities are divided in multiple slots of this duration. This value can be overridden per availability."
|
||||
@ -1237,6 +1239,11 @@ en:
|
||||
pack_only_for_subscription_info_html: "If this option is activated, the purchase and use of a prepaid pack is only possible for the user with a valid subscription."
|
||||
pack_only_for_subscription: "Subscription valid for purchase and use of a prepaid pack"
|
||||
pack_only_for_subscription_info: "Make subscription mandatory for prepaid packs"
|
||||
overlapping_options:
|
||||
training_reservations: "Trainings"
|
||||
machine_reservations: "Machines"
|
||||
space_reservations: "Spaces"
|
||||
events_reservations: "Events"
|
||||
general:
|
||||
general: "General"
|
||||
title: "Title"
|
||||
@ -1397,6 +1404,9 @@ en:
|
||||
card_collection_info: "By validating, you'll be prompted for the member's card number. This card will be automatically charged at the deadlines."
|
||||
check_collection_info: "By validating, you confirm that you have {DEADLINES} checks, allowing you to collect all the monthly payments."
|
||||
online_payment_disabled: "Online payment is not available. You cannot collect this payment schedule by online card."
|
||||
check_list_setting:
|
||||
save: 'Save'
|
||||
customization_of_SETTING_successfully_saved: "Customization of the {SETTING} successfully saved."
|
||||
#feature tour
|
||||
tour:
|
||||
conclusion:
|
||||
|
@ -534,3 +534,4 @@ en:
|
||||
public_agenda_module: "Public agenda module"
|
||||
renew_pack_threshold: "Threshold for packs renewal"
|
||||
pack_only_for_subscription: "Restrict packs for subscribers"
|
||||
overlapping_categories: "Categories for overlapping booking prevention"
|
||||
|
@ -901,6 +901,10 @@ Setting.set('renew_pack_threshold', 0.2) unless Setting.find_by(name: 'renew_pac
|
||||
|
||||
Setting.set('pack_only_for_subscription', true) unless Setting.find_by(name: 'pack_only_for_subscription').try(:value)
|
||||
|
||||
unless Setting.find_by(name: 'overlapping_categories').try(:value)
|
||||
Setting.set('overlapping_categories', 'training_reservations,machine_reservations,space_reservations,events_reservations')
|
||||
end
|
||||
|
||||
if StatisticCustomAggregation.count.zero?
|
||||
# available reservations hours for machines
|
||||
machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: 2)
|
||||
|
Loading…
x
Reference in New Issue
Block a user