1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

(feat) per training settings for auto cancel

This commit is contained in:
Sylvain 2023-01-24 16:48:05 +01:00 committed by Sylvain
parent e2edbb419a
commit c804c84113
10 changed files with 77 additions and 20 deletions

View File

@ -26,6 +26,7 @@ class API::SettingsController < API::ApiController
authorize Setting authorize Setting
@settings = [] @settings = []
updated_settings = []
may_transaction params[:transactional] do may_transaction params[:transactional] do
params[:settings].each do |setting| params[:settings].each do |setting|
next if !setting[:name] || !setting[:value] next if !setting[:name] || !setting[:value]
@ -34,8 +35,9 @@ class API::SettingsController < API::ApiController
if !SettingService.update_allowed?(db_setting) if !SettingService.update_allowed?(db_setting)
db_setting.errors.add(:-, "#{I18n.t("settings.#{setting[:name]}")}: #{I18n.t('settings.locked_setting')}") db_setting.errors.add(:-, "#{I18n.t("settings.#{setting[:name]}")}: #{I18n.t('settings.locked_setting')}")
elsif db_setting.save elsif db_setting.save
unless db_setting.value == setting[:value] if db_setting.value != setting[:value] &&
db_setting.history_values.create(value: setting[:value], invoicing_profile: current_user.invoicing_profile) db_setting.history_values.create(value: setting[:value], invoicing_profile: current_user.invoicing_profile)
updated_settings.push(db_setting)
end end
end end
@ -43,7 +45,7 @@ class API::SettingsController < API::ApiController
may_rollback(params[:transactional]) if db_setting.errors.keys.count.positive? may_rollback(params[:transactional]) if db_setting.errors.keys.count.positive?
end end
end end
SettingService.run_after_update(@settings) SettingService.run_after_update(updated_settings)
end end
def show def show

View File

@ -77,6 +77,7 @@ class API::TrainingsController < API::ApiController
def training_params def training_params
params.require(:training) params.require(:training)
.permit(:id, :name, :description, :machine_ids, :plan_ids, :nb_total_places, :public_page, :disabled, .permit(:id, :name, :description, :machine_ids, :plan_ids, :nb_total_places, :public_page, :disabled,
:auto_cancel, :auto_cancel_threshold, :auto_cancel_deadline,
training_image_attributes: %i[id attachment], machine_ids: [], plan_ids: [], training_image_attributes: %i[id attachment], machine_ids: [], plan_ids: [],
advanced_accounting_attributes: %i[code analytical_section]) advanced_accounting_attributes: %i[code analytical_section])
end end

View File

@ -38,13 +38,13 @@ export const TrainingForm: React.FC<TrainingFormProps> = ({ action, training, on
const { t } = useTranslation('admin'); const { t } = useTranslation('admin');
const [machineModule, setMachineModule] = useState<Setting>(null); const [machineModule, setMachineModule] = useState<Setting>(null);
const [isActiveCancellation, setIsActiveCancellation] = useState<boolean>(false);
const [isActiveAccounting, setIsActiveAccounting] = useState<boolean>(false); const [isActiveAccounting, setIsActiveAccounting] = useState<boolean>(false);
const [isActiveAuthorizationValidity, setIsActiveAuthorizationValidity] = useState<boolean>(false); const [isActiveAuthorizationValidity, setIsActiveAuthorizationValidity] = useState<boolean>(false);
const [isActiveValidationRule, setIsActiveValidationRule] = useState<boolean>(false); const [isActiveValidationRule, setIsActiveValidationRule] = useState<boolean>(false);
const { handleSubmit, register, control, setValue, formState } = useForm<Training>({ defaultValues: { ...training } }); const { handleSubmit, register, control, setValue, formState } = useForm<Training>({ defaultValues: { ...training } });
const output = useWatch<Training>({ control }); const output = useWatch<Training>({ control });
const isActiveCancellation = useWatch({ control, name: 'auto_cancel' }) as boolean;
useEffect(() => { useEffect(() => {
SettingAPI.get('machines_module').then(setMachineModule).catch(onError); SettingAPI.get('machines_module').then(setMachineModule).catch(onError);
@ -79,13 +79,6 @@ export const TrainingForm: React.FC<TrainingFormProps> = ({ action, training, on
}).catch(error => onError(error)); }).catch(error => onError(error));
}; };
/**
* Callback triggered when the auto cancellation switch has changed.
*/
const toggleCancellationSwitch = (value: boolean) => {
setIsActiveCancellation(value);
};
/** /**
* Callback triggered when the authorisation validity switch has changed. * Callback triggered when the authorisation validity switch has changed.
*/ */
@ -178,15 +171,15 @@ export const TrainingForm: React.FC<TrainingFormProps> = ({ action, training, on
<p className="description">{t('app.admin.training_form.automatic_cancellation_info')}</p> <p className="description">{t('app.admin.training_form.automatic_cancellation_info')}</p>
</header> </header>
<div className="content"> <div className="content">
<FormSwitch id="active_cancellation" control={control} <FormSwitch id="auto_cancel" control={control}
onChange={toggleCancellationSwitch} formState={formState} formState={formState}
defaultValue={isActiveCancellation} defaultValue={isActiveCancellation}
label={t('app.admin.training_form.automatic_cancellation_switch')} /> label={t('app.admin.training_form.automatic_cancellation_switch')} />
{isActiveCancellation && <> {isActiveCancellation && <>
<FormInput register={register} <FormInput register={register}
type="number" type="number"
step={1} step={1}
id="auto_cancellation_threshold" id="auto_cancel_threshold"
formState={formState} formState={formState}
rules={{ required: isActiveCancellation }} rules={{ required: isActiveCancellation }}
nullable nullable
@ -194,7 +187,7 @@ export const TrainingForm: React.FC<TrainingFormProps> = ({ action, training, on
<FormInput register={register} <FormInput register={register}
type="number" type="number"
step={1} step={1}
id="auto_cancellation_deadline" id="auto_cancel_deadline"
formState={formState} formState={formState}
rules={{ required: isActiveCancellation }} rules={{ required: isActiveCancellation }}
nullable nullable

View File

@ -143,8 +143,11 @@ export const Trainings: React.FC<TrainingsProps> = ({ onError, onSuccess }) => {
<div className='cancel'> <div className='cancel'>
<span>{t('app.admin.trainings.cancellation')}</span> <span>{t('app.admin.trainings.cancellation')}</span>
<p>5 {t('app.admin.trainings.cancellation_minimum')}<span>|</span>48 {t('app.admin.trainings.cancellation_deadline')} {(training.auto_cancel && <p>
</p> {t('app.admin.trainings.cancellation_minimum', { ATTENDEES: training.auto_cancel_threshold })}
<span>|</span>
{t('app.admin.trainings.cancellation_deadline', { DEADLINE: training.auto_cancel_deadline })}
</p>) || <p>---</p>}
</div> </div>
<div className='capacity'> <div className='capacity'>

View File

@ -14,6 +14,9 @@ export interface Training {
disabled?: boolean, disabled?: boolean,
plan_ids?: number[], plan_ids?: number[],
training_image_attributes?: FileType, training_image_attributes?: FileType,
auto_cancel: boolean,
auto_cancel_threshold: number,
auto_cancel_deadline: number,
availabilities?: Array<{ availabilities?: Array<{
id: number, id: number,
start_at: TDateISO, start_at: TDateISO,

View File

@ -203,6 +203,11 @@ class Setting < ApplicationRecord
last_value&.created_at last_value&.created_at
end end
def previous_value
previous_value = history_values.order(HistoryValue.arel_table['created_at'].desc).limit(2).last
previous_value&.value
end
def previous_update def previous_update
previous_value = history_values.order(HistoryValue.arel_table['created_at'].desc).limit(2).last previous_value = history_values.order(HistoryValue.arel_table['created_at'].desc).limit(2).last
previous_value&.created_at previous_value&.created_at

View File

@ -5,12 +5,14 @@
# so this service provides a wrapper around these operations. # so this service provides a wrapper around these operations.
class SettingService class SettingService
class << self class << self
# @param setting [Setting]
def update_allowed?(setting) def update_allowed?(setting)
return false if Rails.application.secrets.locked_settings.include? setting.name return false if Rails.application.secrets.locked_settings.include? setting.name
true true
end end
# @param settings [Array<Setting>]
def run_after_update(settings) def run_after_update(settings)
update_theme_stylesheet(settings) update_theme_stylesheet(settings)
update_home_stylesheet(settings) update_home_stylesheet(settings)
@ -20,11 +22,13 @@ class SettingService
export_projects_to_openlab(settings) export_projects_to_openlab(settings)
validate_admins(settings) validate_admins(settings)
update_accounting_line(settings) update_accounting_line(settings)
update_trainings_auto_cancel(settings)
end end
private private
# rebuild the theme stylesheet # rebuild the theme stylesheet
# @param settings [Array<Setting>]
def update_theme_stylesheet(settings) def update_theme_stylesheet(settings)
return unless (%w[main_color secondary_color] & settings.map(&:name)).count.positive? return unless (%w[main_color secondary_color] & settings.map(&:name)).count.positive?
@ -32,6 +36,7 @@ class SettingService
end end
# rebuild the home page stylesheet # rebuild the home page stylesheet
# @param settings [Array<Setting>]
def update_home_stylesheet(settings) def update_home_stylesheet(settings)
return unless settings.any? { |s| s.name == 'home_css' } return unless settings.any? { |s| s.name == 'home_css' }
@ -39,6 +44,7 @@ class SettingService
end end
# notify about a change in privacy policy # notify about a change in privacy policy
# @param settings [Array<Setting>]
def notify_privacy_update(settings) def notify_privacy_update(settings)
return unless settings.any? { |s| s.name == 'privacy_body' } return unless settings.any? { |s| s.name == 'privacy_body' }
@ -47,6 +53,7 @@ class SettingService
end end
# sync all objects on stripe # sync all objects on stripe
# @param settings [Array<Setting>]
def sync_stripe_objects(settings) def sync_stripe_objects(settings)
return unless (%w[stripe_secret_key online_payment_module] & settings.map(&:name)).count.positive? return unless (%w[stripe_secret_key online_payment_module] & settings.map(&:name)).count.positive?
@ -55,6 +62,7 @@ class SettingService
end end
# generate the statistics since the last update # generate the statistics since the last update
# @param settings [Array<Setting>]
def build_stats(settings) def build_stats(settings)
return unless settings.any? { |s| s.name == 'statistics_module' && s.value == 'true' } return unless settings.any? { |s| s.name == 'statistics_module' && s.value == 'true' }
@ -63,6 +71,7 @@ class SettingService
end end
# export projects to openlab # export projects to openlab
# @param settings [Array<Setting>]
def export_projects_to_openlab(settings) def export_projects_to_openlab(settings)
return unless (%w[openlab_app_id openlab_app_secret] & settings.map(&:name)).count.positive? && return unless (%w[openlab_app_id openlab_app_secret] & settings.map(&:name)).count.positive? &&
Setting.get('openlab_app_id').present? && Setting.get('openlab_app_secret').present? Setting.get('openlab_app_id').present? && Setting.get('openlab_app_secret').present?
@ -71,16 +80,33 @@ class SettingService
end end
# automatically validate the admins # automatically validate the admins
# @param settings [Array<Setting>]
def validate_admins(settings) def validate_admins(settings)
return unless settings.any? { |s| s.name == 'user_validation_required' && s.value == 'true' } return unless settings.any? { |s| s.name == 'user_validation_required' && s.value == 'true' }
User.admins.each { |admin| admin.update(validated_at: Time.current) if admin.validated_at.nil? } User.admins.each { |admin| admin.update(validated_at: Time.current) if admin.validated_at.nil? }
end end
# rebuild accounting lines
# @param settings [Array<Setting>]
def update_accounting_line(settings) def update_accounting_line(settings)
return unless settings.any? { |s| s.name.match(/^accounting_/) || s.name == 'advanced_accounting' } return unless settings.any? { |s| s.name.match(/^accounting_/) || s.name == 'advanced_accounting' }
AccountingWorker.perform_async(:all) AccountingWorker.perform_async(:all)
end end
# update tranings auto_cancel parameters
# @param settings [Array<Setting>]
def update_trainings_auto_cancel(settings)
return unless settings.any? { |s| s.name.match(/^trainings_auto_cancel/) }
tac = settings.find { |s| s.name == 'trainings_auto_cancel' }
threshold = settings.find { |s| s.name == 'trainings_auto_cancel_threshold' }
deadline = settings.find { |s| s.name == 'trainings_auto_cancel_deadline' }
Training.find_each do |t|
TrainingService.update_auto_cancel(t, tac, threshold, deadline)
end
end
end end
end end

View File

@ -34,6 +34,29 @@ class TrainingService
end end
end end
# update the given training, depending on the provided settings
# @param training [Training]
# @param auto_cancel [Setting,NilClass]
# @param threshold [Setting,NilClass]
# @param deadline [Setting,NilClass]
def update_auto_cancel(training, auto_cancel, threshold, deadline)
previous_auto_cancel = auto_cancel.nil? ? Setting.find_by(name: 'trainings_auto_cancel').value : auto_cancel.previous_value
previous_threshold = threshold.nil? ? Setting.find_by(name: 'trainings_auto_cancel_threshold').value : threshold.previous_value
previous_deadline = deadline.nil? ? Setting.find_by(name: 'trainings_auto_cancel_deadline').value : deadline.previous_value
is_default = training.auto_cancel.to_s == previous_auto_cancel &&
[nil, previous_threshold].include?(training.auto_cancel_threshold.to_s) &&
[nil, previous_deadline].include?(training.auto_cancel_deadline.to_s)
return unless is_default
# update parameters if the given training is default
params = {}
params[:auto_cancel] = auto_cancel.value unless auto_cancel.nil?
params[:auto_cancel_threshold] = threshold.value unless threshold.nil?
params[:auto_cancel_deadline] = deadline.value unless deadline.nil?
training.update(params)
end
private private
# @param trainings [ActiveRecord::Relation<Training>] # @param trainings [ActiveRecord::Relation<Training>]

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
json.extract! training, :id, :name, :description, :machine_ids, :nb_total_places, :public_page, :disabled, :slug json.extract! training, :id, :name, :description, :machine_ids, :nb_total_places, :public_page, :disabled, :slug,
:auto_cancel, :auto_cancel_threshold, :auto_cancel_deadline
if training.training_image if training.training_image
json.training_image_attributes do json.training_image_attributes do
json.id training.training_image.id json.id training.training_image.id

View File

@ -437,8 +437,8 @@ en:
name: "Training name" name: "Training name"
associated_machines: "Associated machines" associated_machines: "Associated machines"
cancellation: "Cancellation (attendees | deadline)" cancellation: "Cancellation (attendees | deadline)"
cancellation_minimum: "minimum" cancellation_minimum: "{ATTENDEES} minimum"
cancellation_deadline: "h" cancellation_deadline: "{DEADLINE} h"
capacity: "Capacity (max. attendees)" capacity: "Capacity (max. attendees)"
authorisation: "Time-limited authorisation" authorisation: "Time-limited authorisation"
period_MONTH: "{MONTH} {MONTH, plural, one{month} other{months}}" period_MONTH: "{MONTH} {MONTH, plural, one{month} other{months}}"