1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-30 19:52:20 +01:00

(ui) display trainings in list

This commit is contained in:
Sylvain 2023-01-23 17:31:33 +01:00
parent 117bd36caa
commit 305b5425bc
16 changed files with 254 additions and 160 deletions

View File

@ -28,4 +28,9 @@ export default class TrainingAPI {
});
return res?.data;
}
static async destroy (trainingId: number): Promise<void> {
const res: AxiosResponse<void> = await apiClient.delete(`/api/trainings/${trainingId}`);
return res?.data;
}
}

View File

@ -0,0 +1,61 @@
import { ReactNode, useState } from 'react';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { FabButton } from './fab-button';
import { FabModal } from './fab-modal';
import { Trash } from 'phosphor-react';
interface DestroyButtonProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
itemId: number,
itemType: string,
apiDestroy: (itemId: number) => Promise<void>,
confirmationMessage?: string|ReactNode,
className?: string,
iconSize?: number
}
/**
* This component shows a button.
* When clicked, we show a modal dialog to ask the user for confirmation about the deletion of the provided item.
*/
export const DestroyButton: React.FC<DestroyButtonProps> = ({ onSuccess, onError, itemId, itemType, apiDestroy, confirmationMessage, className, iconSize = 24 }) => {
const { t } = useTranslation('admin');
const [deletionModal, setDeletionModal] = useState<boolean>(false);
/**
* Opens/closes the deletion modal
*/
const toggleDeletionModal = (): void => {
setDeletionModal(!deletionModal);
};
/**
* The deletion has been confirmed by the user.
* Call the API to trigger the deletion of the given item
*/
const onDeleteConfirmed = (): void => {
apiDestroy(itemId).then(() => {
onSuccess(t('app.admin.destroy_button.deleted', { TYPE: itemType }));
}).catch((error) => {
onError(t('app.admin.destroy_button.unable_to_delete', { TYPE: itemType }) + error);
});
toggleDeletionModal();
};
return (
<div className={`destroy-button ${className || ''}`}>
<FabButton type='button' className="destroy-button-cta" icon={<Trash size={iconSize} weight="fill" />} onClick={toggleDeletionModal} />
<FabModal title={t('app.admin.destroy_button.delete_item', { TYPE: itemType })}
isOpen={deletionModal}
toggleModal={toggleDeletionModal}
closeButton={true}
confirmButton={t('app.admin.destroy_button.confirm_delete')}
onConfirm={onDeleteConfirmed}>
<span>{confirmationMessage || t('app.admin.destroy_button.delete_confirmation', { TYPE: itemType })}</span>
</FabModal>
</div>
);
};

View File

@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next';
import { FabPopover } from '../../base/fab-popover';
import { CreatePack } from './create-pack';
import PrepaidPackAPI from '../../../api/prepaid-pack';
import { DeletePack } from './delete-pack';
import { EditPack } from './edit-pack';
import FormatLib from '../../../lib/format';
import { DestroyButton } from '../../base/destroy-button';
interface ConfigurePacksButtonProps {
packsData: Array<PrepaidPack>,
@ -76,7 +76,13 @@ export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ pack
{formatDuration(p.minutes)} - {FormatLib.price(p.amount)}
<span className="pack-actions">
<EditPack onSuccess={handleSuccess} onError={onError} pack={p} />
<DeletePack onSuccess={handleSuccess} onError={onError} pack={p} />
<DestroyButton onSuccess={handleSuccess}
onError={onError}
itemId={p.id}
itemType={t('app.admin.configure_packs_button.pack')}
apiDestroy={PrepaidPackAPI.destroy}
iconSize={12}
confirmationMessage={t('app.admin.configure_packs_button.delete_confirmation')} />
</span>
</li>)}
</ul>

View File

@ -1,68 +0,0 @@
import { useState } from 'react';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { FabButton } from '../../base/fab-button';
import { FabModal } from '../../base/fab-modal';
import { Loader } from '../../base/loader';
import { PrepaidPack } from '../../../models/prepaid-pack';
import PrepaidPackAPI from '../../../api/prepaid-pack';
interface DeletePackProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
pack: PrepaidPack,
}
/**
* This component shows a button.
* When clicked, we show a modal dialog to ask the user for confirmation about the deletion of the provided pack.
*/
const DeletePack: React.FC<DeletePackProps> = ({ onSuccess, onError, pack }) => {
const { t } = useTranslation('admin');
const [deletionModal, setDeletionModal] = useState<boolean>(false);
/**
* Opens/closes the deletion modal
*/
const toggleDeletionModal = (): void => {
setDeletionModal(!deletionModal);
};
/**
* The deletion has been confirmed by the user.
* Call the API to trigger the deletion of the temporary set plan-category
*/
const onDeleteConfirmed = (): void => {
PrepaidPackAPI.destroy(pack.id).then(() => {
onSuccess(t('app.admin.delete_pack.pack_deleted'));
}).catch((error) => {
onError(t('app.admin.delete_pack.unable_to_delete') + error);
});
toggleDeletionModal();
};
return (
<div className="delete-pack">
<FabButton type='button' className="remove-pack-button" icon={<i className="fa fa-trash" />} onClick={toggleDeletionModal} />
<FabModal title={t('app.admin.delete_pack.delete_pack')}
isOpen={deletionModal}
toggleModal={toggleDeletionModal}
closeButton={true}
confirmButton={t('app.admin.delete_pack.confirm_delete')}
onConfirm={onDeleteConfirmed}>
<span>{t('app.admin.delete_pack.delete_confirmation')}</span>
</FabModal>
</div>
);
};
const DeletePackWrapper: React.FC<DeletePackProps> = (props) => {
return (
<Loader>
<DeletePack {...props} />
</Loader>
);
};
export { DeletePackWrapper as DeletePack };

View File

@ -1,15 +1,18 @@
import * as React from 'react';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { IApplication } from '../../models/application';
import { Loader } from '../base/loader';
import { react2angular } from 'react2angular';
import { ErrorBoundary } from '../base/error-boundary';
import { useTranslation } from 'react-i18next';
import { useForm, SubmitHandler } from 'react-hook-form';
import { useForm, SubmitHandler, useWatch } from 'react-hook-form';
import { FormSwitch } from '../form/form-switch';
import { FormInput } from '../form/form-input';
import { FabButton } from '../base/fab-button';
import { EditorialBlockForm } from '../editorial-block/editorial-block-form';
import { SettingName, SettingValue, trainingSettings } from '../../models/setting';
import SettingAPI from '../../api/setting';
import SettingLib from '../../lib/setting';
declare const Application: IApplication;
@ -21,20 +24,22 @@ interface TrainingsSettingsProps {
/**
* Trainings settings
*/
export const TrainingsSettings: React.FC<TrainingsSettingsProps> = () => {
export const TrainingsSettings: React.FC<TrainingsSettingsProps> = ({ onError, onSuccess }) => {
const { t } = useTranslation('admin');
const { register, control, formState, handleSubmit } = useForm();
const { register, control, formState, handleSubmit, reset } = useForm<Record<SettingName, SettingValue>>();
const [isActiveAutoCancellation, setIsActiveAutoCancellation] = useState<boolean>(false);
const isActiveAutoCancellation = useWatch({ control, name: 'trainings_auto_cancel' }) as boolean;
const [isActiveAuthorizationValidity, setIsActiveAuthorizationValidity] = useState<boolean>(false);
const [isActiveValidationRule, setIsActiveValidationRule] = useState<boolean>(false);
/**
* Callback triggered when the auto cancellation switch has changed.
*/
const toggleAutoCancellation = (value: boolean) => {
setIsActiveAutoCancellation(value);
};
useEffect(() => {
SettingAPI.query(trainingSettings)
.then(settings => {
const data = SettingLib.bulkMapToObject(settings);
reset(data);
})
.catch(onError);
}, []);
/**
* Callback triggered when the authorisation validity switch has changed.
@ -53,8 +58,12 @@ export const TrainingsSettings: React.FC<TrainingsSettingsProps> = () => {
/**
* Callback triggered when the form is submitted: save the settings
*/
const onSubmit: SubmitHandler<any> = (data) => {
console.log(data);
const onSubmit: SubmitHandler<Record<SettingName, SettingValue>> = (data) => {
SettingAPI.bulkUpdate(SettingLib.objectToBulkMap(data)).then(() => {
onSuccess(t('app.admin.trainings_settings.update_success'));
}, reason => {
onError(reason);
});
};
return (
@ -78,20 +87,20 @@ export const TrainingsSettings: React.FC<TrainingsSettingsProps> = () => {
</header>
<div className="content">
<FormSwitch id="active_auto_cancellation" control={control}
onChange={toggleAutoCancellation} formState={formState}
<FormSwitch id="trainings_auto_cancel" control={control}
formState={formState}
defaultValue={isActiveAutoCancellation}
label={t('app.admin.trainings_settings.automatic_cancellation_switch')} />
{isActiveAutoCancellation && <>
<FormInput id="auto_cancellation_threshold"
<FormInput id="trainings_auto_cancel_threshold"
type="number"
register={register}
rules={{ required: isActiveAutoCancellation, min: 0 }}
step={1}
formState={formState}
label={t('app.admin.trainings_settings.automatic_cancellation_threshold')} />
<FormInput id="auto_cancellation_deadline"
<FormInput id="trainings_auto_cancel_deadline"
type="number"
register={register}
rules={{ required: isActiveAutoCancellation, min: 1 }}

View File

@ -7,7 +7,13 @@ import { useTranslation } from 'react-i18next';
import { FabButton } from '../base/fab-button';
import Select from 'react-select';
import { SelectOption } from '../../models/select';
import { CalendarBlank, PencilSimple, Trash } from 'phosphor-react';
import { CalendarBlank, PencilSimple } from 'phosphor-react';
import { useEffect, useState } from 'react';
import type { Training } from '../../models/training';
import type { Machine } from '../../models/machine';
import TrainingAPI from '../../api/training';
import MachineAPI from '../../api/machine';
import { DestroyButton } from '../base/destroy-button';
declare const Application: IApplication;
@ -19,9 +25,13 @@ interface TrainingsProps {
/**
* Admin list of trainings
*/
export const Trainings: React.FC<TrainingsProps> = () => {
export const Trainings: React.FC<TrainingsProps> = ({ onError, onSuccess }) => {
const { t } = useTranslation('admin');
const [trainings, setTrainings] = useState<Array<Training>>([]);
const [machines, setMachines] = useState<Array<Machine>>([]);
const [filter, setFilter] = useState<boolean>(null);
// Styles the React-select component
const customStyles = {
control: base => ({
@ -35,18 +45,53 @@ export const Trainings: React.FC<TrainingsProps> = () => {
})
};
useEffect(() => {
MachineAPI.index({ disabled: false })
.then(setMachines)
.catch(onError);
}, []);
useEffect(() => {
TrainingAPI.index({ disabled: filter })
.then(setTrainings)
.catch(onError);
}, [filter]);
/** Creates filtering options to the react-select format */
const buildFilterOptions = (): Array<SelectOption<any>> => {
const buildFilterOptions = (): Array<SelectOption<boolean>> => {
return [
{ value: 'all', label: t('app.admin.trainings.status_all') },
{ value: 'enabled', label: t('app.admin.trainings.status_enabled') },
{ value: 'disabled', label: t('app.admin.trainings.status_disabled') }
{ value: null, label: t('app.admin.trainings.status_all') },
{ value: false, label: t('app.admin.trainings.status_enabled') },
{ value: true, label: t('app.admin.trainings.status_disabled') }
];
};
/** Handel filter change */
const onFilterChange = (option: SelectOption<any>) => {
console.log(option);
const onFilterChange = (option: SelectOption<boolean>) => {
setFilter(option.value);
};
/**
* List of machines names for teh given ids
*/
const machinesNames = (ids: Array<number>): string => {
return machines.filter(m => ids.includes(m.id)).map(m => m.name).join(', ');
};
/**
*
* Check if the given training has associated non-disabled machines
*/
const hasMachines = (training: Training): boolean => {
const activesMachines = machines.map(m => m.id);
return training.machine_ids.filter(id => activesMachines.includes(id)).length > 0;
};
/**
* Redirect the user to the given training edition page
*/
const toTrainingEdit = (training: Training): void => {
window.location.href = `/#!/admin/trainings/${training.id}/edit`;
};
/** Link to calendar page */
@ -84,61 +129,66 @@ export const Trainings: React.FC<TrainingsProps> = () => {
{/* map
ajouter la classe .is-override si l'item a au moins un réglage spécifique (différent des paramètres généraux)
*/}
<div className='trainings-list-item'>
<div className='name'>
<span>{t('app.admin.trainings.name')}</span>
<p>All you can learn : super training</p>
</div>
{trainings.map(training => (
<div className='trainings-list-item' key={training.id}>
<div className='name'>
<span>{t('app.admin.trainings.name')}</span>
<p>{training.name}</p>
</div>
<div className='machines'>
<span>{t('app.admin.trainings.associated_machines')}</span>
<p>Découpeuse laser, Découpeuse vinyle, Shopbot / Grande fraiseuse, Petite Fraiseuse, Imprimante 3D</p>
</div>
{(hasMachines(training) && <div className='machines'>
<span>{t('app.admin.trainings.associated_machines')}</span>
<p>{machinesNames(training.machine_ids)}</p>
</div>) || <div/>}
<div className='cancel'>
<span>{t('app.admin.trainings.cancellation')}</span>
<p>5 {t('app.admin.trainings.cancellation_minimum')}<span>|</span>48 {t('app.admin.trainings.cancellation_deadline')}
{/* si l'item a un réglage spécifique (différent des paramètres généraux) */}
{true && <span className='override'>{t('app.admin.trainings.override')}</span> }
</p>
</div>
<div className='cancel'>
<span>{t('app.admin.trainings.cancellation')}</span>
<p>5 {t('app.admin.trainings.cancellation_minimum')}<span>|</span>48 {t('app.admin.trainings.cancellation_deadline')}
{/* si l'item a un réglage spécifique (différent des paramètres généraux) */}
{true && <span className='override'>{t('app.admin.trainings.override')}</span> }
</p>
</div>
<div className='capacity'>
<span>{t('app.admin.trainings.capacity')}</span>
<p>10</p>
</div>
<div className='capacity'>
<span>{t('app.admin.trainings.capacity')}</span>
<p>{training.nb_total_places}</p>
</div>
<div className='authorisation'>
<span>{t('app.admin.trainings.authorisation')}</span>
<p>
{t('app.admin.trainings.active_true')}
<span>|</span>{t('app.admin.trainings.period_MONTH', { MONTH: 48 })}
{/* si l'item a un réglage spécifique (différent des paramètres généraux) */}
{true && <span className='override'>{t('app.admin.trainings.override')}</span> }
</p>
</div>
<div className='authorisation'>
<span>{t('app.admin.trainings.authorisation')}</span>
<p>
{t('app.admin.trainings.active_true')}
<span>|</span>{t('app.admin.trainings.period_MONTH', { MONTH: 48 })}
{/* si l'item a un réglage spécifique (différent des paramètres généraux) */}
{true && <span className='override'>{t('app.admin.trainings.override')}</span> }
</p>
</div>
<div className='rule'>
<span>{t('app.admin.trainings.validation_rule')}</span>
<p>
{t('app.admin.trainings.active_false')}
<span>|</span>
{/* si l'item a un réglage spécifique (différent des paramètres généraux) */}
{true && <span className='override'>{t('app.admin.trainings.override')}</span> }
</p>
</div>
<div className='rule'>
<span>{t('app.admin.trainings.validation_rule')}</span>
<p>
{t('app.admin.trainings.active_false')}
<span>|</span>
{/* si l'item a un réglage spécifique (différent des paramètres généraux) */}
{true && <span className='override'>{t('app.admin.trainings.override')}</span> }
</p>
</div>
<div className='actions'>
<div className='grpBtn'>
<FabButton className='edit-btn'>
<PencilSimple size={20} weight="fill" />
</FabButton>
<FabButton className='delete-btn'>
<Trash size={20} weight="fill" />
</FabButton>
<div className='actions'>
<div className='grpBtn'>
<FabButton className='edit-btn' onClick={() => toTrainingEdit(training)}>
<PencilSimple size={20} weight="fill" />
</FabButton>
<DestroyButton onSuccess={onSuccess}
className="delete-btn"
onError={onError}
itemId={training.id}
itemType={t('app.admin.trainings.training')}
apiDestroy={TrainingAPI.destroy} />
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>

View File

@ -354,6 +354,20 @@ Application.Controllers.controller('TrainingsAdminController', ['$scope', '$stat
});
};
/**
* 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) {
growl.error(message);
};
/**
* Setup the feature-tour for the admin/trainings page.
* This is intended as a contextual help (when pressing F1)

View File

@ -233,6 +233,12 @@ export const storeSettings = [
'store_hidden'
] as const;
export const trainingSettings = [
'trainings_auto_cancel',
'trainings_auto_cancel_threshold',
'trainings_auto_cancel_deadline'
] as const;
export const allSettings = [
...homePageSettings,
...privacyPolicySettings,
@ -258,7 +264,8 @@ export const allSettings = [
...pricingSettings,
...poymentSettings,
...displaySettings,
...storeSettings
...storeSettings,
...trainingSettings
] as const;
export type SettingName = typeof allSettings[number];

View File

@ -23,6 +23,7 @@
@import "modules/authentication-provider/openid-connect-data-mapping-form";
@import "modules/authentication-provider/provider-form";
@import "modules/authentication-provider/type-mapping-modal";
@import "modules/base/destroy-button";
@import "modules/base/editorial-block";
@import "modules/base/fab-alert";
@import "modules/base/fab-button";
@ -94,7 +95,6 @@
@import "modules/pricing/editable-price";
@import "modules/pricing/machines/configure-packs-button";
@import "modules/pricing/machines/create-pack";
@import "modules/pricing/machines/delete-pack";
@import "modules/pricing/machines/edit-pack";
@import "modules/pricing/machines/machines-pricing";
@import "modules/pricing/machines/pack-form";

View File

@ -0,0 +1,8 @@
.destroy-button {
display: inline;
& > button.destroy-button-cta {
background-color: var(--alert);
color: white;
}
}

View File

@ -38,6 +38,7 @@
}
.pack-actions button {
min-height: unset;
font-size: 10px;
vertical-align: middle;
line-height: 10px;

View File

@ -1,8 +0,0 @@
.delete-pack {
display: inline;
.remove-pack-button {
background-color: #cb1117;
color: white;
}
}

View File

@ -26,7 +26,7 @@
<div class="col-md-12">
<uib-tabset justified="true" active="tabs.active">
<uib-tab heading="{{ 'app.admin.trainings.trainings_settings' | translate }}" index="1" class="manage-trainings">
<trainings-settings on-error="onError" on-success="on-success"></trainings-settings>
<trainings-settings on-error="onError" on-success="onSuccess"></trainings-settings>
</uib-tab>
<uib-tab heading="{{ 'app.admin.trainings.all_trainings' | translate }}" index="0" class="manage-trainings">
<trainings on-error="onError" on-success="on-success"></trainings>

View File

@ -165,7 +165,10 @@ class Setting < ApplicationRecord
advanced_accounting
external_id
prevent_invoices_zero
invoice_VAT-name] }
invoice_VAT-name
trainings_auto_cancel
trainings_auto_cancel_threshold
trainings_auto_cancel_deadline] }
# 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

View File

@ -1,6 +1,12 @@
en:
app:
admin:
destroy_button:
deleted: "The {TYPE} was successfully deleted."
unable_to_delete: "Unable to delete the {TYPE}: "
delete_item: "Delete the {TYPE}"
confirm_delete: "Delete"
delete_confirmation: "Are you sure you want to delete this {TYPE}?"
machines:
the_fablab_s_machines: "The FabLab's machines"
all_machines: "All machines"
@ -491,6 +497,7 @@ en:
cta_label: "Button label"
cta_url: "url"
save: "Save"
update_success: "The trainings settings were successfully updated"
#events tracking and management
events:
events_monitoring: "Events monitoring"
@ -669,9 +676,11 @@ en:
machines: "Machines"
price_updated: "Price successfully updated"
configure_packs_button:
pack: "prepaid pack"
packs: "Prepaid packs"
no_packs: "No packs for now"
pack_DURATION: "{DURATION} hours"
delete_confirmation: "Are you sure you want to delete this prepaid pack? This won't be possible if the pack was already bought by users."
configure_extended_prices_button:
extended_prices: "Extended prices"
no_extended_prices: "No extended price for now"
@ -695,12 +704,6 @@ en:
new_pack_info: "A prepaid pack allows users to buy {TYPE, select, Machine{machine} Space{space} other{}} hours before booking any slots. These packs can provide discounts on volumes purchases."
create_pack: "Create this pack"
pack_successfully_created: "The new prepaid pack was successfully created."
delete_pack:
pack_deleted: "The prepaid pack was successfully deleted."
unable_to_delete: "Unable to delete the prepaid pack: "
delete_pack: "Delete the prepaid pack"
confirm_delete: "Delete"
delete_confirmation: "Are you sure you want to delete this prepaid pack? This won't be possible if the pack was already bought by users."
edit_pack:
edit_pack: "Edit the pack"
confirm_changes: "Confirm changes"

View File

@ -664,6 +664,9 @@ en:
external_id: "external identifier"
prevent_invoices_zero: "prevent building invoices at 0"
invoice_VAT-name: "VAT name"
trainings_auto_cancel: "Trainings automatic cancellation"
trainings_auto_cancel_threshold: "Minimum participants for automatic cancellation"
trainings_auto_cancel_deadline: "Automatic cancellation deadline"
#statuses of projects
statuses:
new: "New"