1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

(ui) Plan limit form and modal

This commit is contained in:
vincent 2023-01-24 18:59:01 +01:00 committed by Sylvain
parent 9294bc4c88
commit c964ec8a6d
17 changed files with 403 additions and 78 deletions

View File

@ -25,6 +25,7 @@ import { PartnerModal } from './partner-modal';
import { PlanPricingForm } from './plan-pricing-form';
import { AdvancedAccountingForm } from '../accounting/advanced-accounting-form';
import { FabTabs } from '../base/fab-tabs';
import { PlanLimitForm } from './plan-limit-form';
declare const Application: IApplication;
@ -202,6 +203,9 @@ export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuc
formState={formState}
rules={{ required: true }} />
</div>
{action === 'update' && <FabAlert level="info">
{t('app.admin.plan_form.edit_amount_info')}
</FabAlert>}
<FormInput register={register}
formState={formState}
id="amount"
@ -268,24 +272,30 @@ export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuc
</div>
</section>
{categories?.length > 0 && <FormSelect options={categories}
formState={formState}
control={control}
id="plan_category_id"
tooltip={t('app.admin.plan_form.category_help')}
label={t('app.admin.plan_form.category')} />}
{action === 'update' && <FabAlert level="warning">
{t('app.admin.plan_form.edit_amount_info')}
</FabAlert>}
<section>
<header>
<p className="title">{t('app.admin.plan_form.display')} </p>
</header>
<div className="content">
{categories?.length > 0 && <FormSelect options={categories}
formState={formState}
control={control}
id="plan_category_id"
tooltip={t('app.admin.plan_form.category_help')}
label={t('app.admin.plan_form.category')} />}
<FormInput register={register}
formState={formState}
id="ui_weight"
type="number"
label={t('app.admin.plan_form.visual_prominence')}
tooltip={t('app.admin.plan_form.visual_prominence_help')} />
</div>
</section>
<FormInput register={register}
formState={formState}
id="ui_weight"
type="number"
label={t('app.admin.plan_form.visual_prominence')}
tooltip={t('app.admin.plan_form.visual_prominence_help')} />
<section>
<AdvancedAccountingForm register={register} onError={onError} />
</section>
<AdvancedAccountingForm register={register} onError={onError} />
{action === 'update' && <PlanPricingForm formState={formState}
control={control}
onError={onError}
@ -315,7 +325,8 @@ export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuc
{
id: 'usageLimits',
title: t('app.admin.plan_form.tab_usage_limits'),
content: <pre>plop</pre>
content: <PlanLimitForm control={control}
formState={formState} />
}
]} />
</form>

View File

@ -0,0 +1,110 @@
import { useState } from 'react';
import { Control, FormState } from 'react-hook-form/dist/types/form';
import { FormSwitch } from '../form/form-switch';
import { useTranslation } from 'react-i18next';
import { FabButton } from '../base/fab-button';
import { PencilSimple, Trash } from 'phosphor-react';
import { PlanLimitModal } from './plan-limit-modal';
interface PlanLimitFormProps<TContext extends object> {
control: Control<any, TContext>,
formState: FormState<any>
}
/**
* Form tab to manage a subscription's usage limit
*/
export const PlanLimitForm = <TContext extends object> ({ control, formState }: PlanLimitFormProps<TContext>) => {
const { t } = useTranslation('admin');
const [isOpen, setIsOpen] = useState<boolean>(false);
/**
* Opens/closes the product stock edition modal
*/
const toggleModal = (): void => {
setIsOpen(!isOpen);
};
return (
<div className="plan-limit-form">
<section>
<header>
<p className="title">{t('app.admin.plan_limit_form.usage_limitation')}</p>
<p className="description">{t('app.admin.plan_limit_form.usage_limitation_info')}</p>
</header>
<div className="content">
<FormSwitch control={control}
formState={formState}
defaultValue={false}
label={t('app.admin.plan_limit_form.usage_limitation_switch')}
id="active_limitation" />
</div>
</section>
<div className="plan-limit-grp">
<header>
<p>{t('app.admin.plan_limit_form.all_limitations')}</p>
<div className="grpBtn">
<FabButton onClick={toggleModal} className="is-main">
{t('app.admin.plan_limit_form.new_usage_limitation')}
</FabButton>
</div>
</header>
<div className='plan-limit-list'>
<p className="title">{t('app.admin.plan_limit_form.by_categories')}</p>
<div className="plan-limit-item">
<div>
<span>{t('app.admin.plan_limit_form.category')}</span>
<p>Plop</p>
</div>
<div>
<span>{t('app.admin.plan_limit_form.max_hours_per_day')}</span>
<p>5</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>
</div>
</div>
</div>
<div className='plan-limit-list'>
<p className="title">{t('app.admin.plan_limit_form.by_machine')}</p>
<div className="plan-limit-item">
<div>
<span>{t('app.admin.plan_limit_form.machine')}</span>
<p>Pouet</p>
</div>
<div>
<span>{t('app.admin.plan_limit_form.max_hours_per_day')}</span>
<p>5</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>
</div>
</div>
</div>
</div>
<PlanLimitModal isOpen={isOpen}
toggleModal={toggleModal} />
</div>
);
};

View File

@ -0,0 +1,98 @@
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { FabAlert } from '../base/fab-alert';
import { FabModal, ModalSize } from '../base/fab-modal';
import { useForm } from 'react-hook-form';
import { FormSelect } from '../form/form-select';
import { FormInput } from '../form/form-input';
type typeSelectOption = { value: any, label: string };
interface PlanLimitModalProps {
isOpen: boolean,
toggleModal: () => void,
}
/**
* Form to manage subscriptions limitations of use
*/
export const PlanLimitModal: React.FC<PlanLimitModalProps> = ({ isOpen, toggleModal }) => {
const { t } = useTranslation('admin');
const { register, control, formState } = useForm<any>();
const [limitType, setLimitType] = React.useState<'categories' | 'machine'>('categories');
/**
* Toggle the form between 'categories' and 'machine'
*/
const toggleLimitType = (evt: React.MouseEvent<HTMLButtonElement, MouseEvent>, type: 'categories' | 'machine') => {
evt.preventDefault();
setLimitType(type);
};
/**
* Creates options to the react-select format
*/
const buildMachinesCategoriesOptions = (): Array<typeSelectOption> => {
return [
{ value: '0', label: 'yep' },
{ value: '1', label: 'nope' }
];
};
/**
* Creates options to the react-select format
*/
const buildMachinesOptions = (): Array<typeSelectOption> => {
return [
{ value: '0', label: 'pif' },
{ value: '1', label: 'paf' },
{ value: '2', label: 'pouf' }
];
};
return (
<FabModal title={t('app.admin.plan_limit_modal.title')}
width={ModalSize.large}
isOpen={isOpen}
toggleModal={toggleModal}
closeButton>
<form className='plan-limit-modal'>
<p className='subtitle'>{t('app.admin.plan_limit_modal.limit_reservations')}</p>
<div className="grp">
<button onClick={evt => toggleLimitType(evt, 'categories')}
className={limitType === 'categories' ? 'is-active' : ''}>
{t('app.admin.plan_limit_modal.by_categories')}
</button>
<button onClick={evt => toggleLimitType(evt, 'machine')}
className={limitType === 'machine' ? 'is-active' : ''}>
{t('app.admin.plan_limit_modal.by_machine')}
</button>
</div>
{limitType === 'categories' && <>
<FabAlert level='info'>{t('app.admin.plan_limit_modal.categories_info')}</FabAlert>
<FormSelect options={buildMachinesCategoriesOptions()}
control={control}
id="machines_category"
rules={{ required: limitType === 'categories' }}
formState={formState}
label={t('app.admin.plan_limit_modal.category')} />
</>}
{limitType === 'machine' && <>
<FabAlert level='info'>{t('app.admin.plan_limit_modal.machine_info')}</FabAlert>
<FormSelect options={buildMachinesOptions()}
control={control}
id="machine"
rules={{ required: limitType === 'machine' }}
formState={formState}
label={t('app.admin.plan_limit_modal.machine')} />
</>}
<FormInput id="hours_limit"
type="number"
register={register}
rules={{ required: true, min: 1 }}
step={1}
formState={formState}
label={t('app.admin.plan_limit_modal.max_hours_per_day')} />
</form>
</FabModal>
);
};

View File

@ -92,32 +92,36 @@ export const PlanPricingForm = <TContext extends object>({ register, control, fo
};
return (
<div data-testid="plan-pricing-form">
<h4>{t('app.admin.plan_pricing_form.prices')}</h4>
{plans && <FormSelect options={plans}
label={t('app.admin.plan_pricing_form.copy_prices_from')}
tooltip={t('app.admin.plan_pricing_form.copy_prices_from_help')}
control={control}
onChange={handleCopyPrices}
id="parent_plan_id" />}
{<FabTabs tabs={[
machines && {
id: 'machines',
title: t('app.admin.plan_pricing_form.machines'),
content: fields.map((price, index) => {
if (price.priceable_type !== 'Machine') return false;
return renderPriceElement(price, index);
}).filter(Boolean)
},
spaces && {
id: 'spaces',
title: t('app.admin.plan_pricing_form.spaces'),
content: fields.map((price, index) => {
if (price.priceable_type !== 'Space') return false;
return renderPriceElement(price, index);
}).filter(Boolean)
}
]} />}
</div>
<section className="plan-pricing-form" data-testid="plan-pricing-form">
<header>
<p className="title">{t('app.admin.plan_pricing_form.prices')}</p>
<p className="description">{t('app.admin.plan_pricing_form.copy_prices_from_help')}</p>
</header>
<div className="content">
{plans && <FormSelect options={plans}
control={control}
label={t('app.admin.plan_pricing_form.copy_prices_from')}
onChange={handleCopyPrices}
id="parent_plan_id" />}
{<FabTabs tabs={[
machines && {
id: 'machines',
title: t('app.admin.plan_pricing_form.machines'),
content: fields.map((price, index) => {
if (price.priceable_type !== 'Machine') return false;
return renderPriceElement(price, index);
}).filter(Boolean)
},
spaces && {
id: 'spaces',
title: t('app.admin.plan_pricing_form.spaces'),
content: fields.map((price, index) => {
if (price.priceable_type !== 'Space') return false;
return renderPriceElement(price, index);
}).filter(Boolean)
}
]} />}
</div>
</section>
);
};

View File

@ -178,7 +178,7 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
<span>{t('app.admin.store.product_stock_form.external')}</span>
<p>{currentFormValues?.stock?.external}</p>
</div>
<FabButton onClick={toggleModal} icon={<PencilSimple size={20} weight="fill" />} className="is-black">Modifier</FabButton>
<FabButton onClick={toggleModal} icon={<PencilSimple size={20} weight="fill" />} className="is-black">{t('app.admin.store.product_stock_form.edit')}</FabButton>
</div>
{fields.length > 0 && <div className="ongoing-stocks">

View File

@ -97,6 +97,9 @@
@import "modules/plan-categories/plan-categories-list";
@import "modules/plans/plan-card";
@import "modules/plans/plan-form";
@import "modules/plans/plan-limit-form";
@import "modules/plans/plan-limit-modal";
@import "modules/plans/plan-pricing-form";
@import "modules/plans/plans-filter";
@import "modules/plans/plans-list";
@import "modules/prepaid-packs/packs-summary";

View File

@ -26,6 +26,7 @@
background-color: var(--gray-soft-lightest);
cursor: default;
}
&:focus { outline: none; }
}
}
}

View File

@ -15,8 +15,6 @@
flex-direction: column;
gap: 3.2rem;
.fab-alert { margin: 0; }
section { @include layout-settings; }
.save-btn { align-self: flex-start; }
}

View File

@ -15,8 +15,6 @@
flex-direction: column;
gap: 3.2rem;
.fab-alert { margin: 0; }
section { @include layout-settings; }
.save-btn { align-self: flex-start; }
}

View File

@ -37,28 +37,4 @@
}
}
}
//.plan-sheet {
// margin-top: 4rem;
//}
//.duration {
// display: flex;
// flex-direction: row;
// .form-item:first-child {
// margin-right: 32px;
// }
//}
//.partner {
// display: flex;
// flex-direction: column;
// align-items: flex-end;
// .fab-alert {
// width: 100%;
// }
//}
//.submit-btn {
// float: right;
//}
}

View File

@ -0,0 +1,64 @@
.plan-limit-form {
display: flex;
flex-direction: column;
gap: 3.2rem 0;
section { @include layout-settings; }
.plan-limit-grp {
header {
@include header();
p {
@include title-base;
margin: 0;
}
}
.plan-limit-list {
max-height: 65vh;
margin-bottom: 6.4rem;
display: flex;
flex-direction: column;
overflow-y: auto;
.title { @include text-base(500); }
.plan-limit-item {
width: 100%;
margin-bottom: 2.4rem;
padding: 1.6rem;
display: flex;
justify-content: space-between;
gap: 3.2rem;
border: 1px solid var(--gray-soft-dark);
border-radius: var(--border-radius);
background-color: var(--gray-soft-lightest);
span {
@include text-xs;
color: var(--gray-hard-light);
}
p {
margin: 0;
@include text-base(600);
}
.actions {
display: flex;
justify-content: flex-end;
align-items: center;
.grpBtn {
overflow: hidden;
display: flex;
border-radius: var(--border-radius-sm);
button {
@include btn;
border-radius: 0;
color: var(--gray-soft-lightest);
&:hover { opacity: 0.75; }
}
.edit-btn {background: var(--gray-hard-darkest) }
.delete-btn {background: var(--main) }
}
}
}
}
}
}

View File

@ -0,0 +1,30 @@
.plan-limit-modal {
.grp {
margin-bottom: 3.2rem;
display: flex;
justify-content: space-between;
align-items: center;
button {
flex: 1;
padding: 1.6rem;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--gray-soft-lightest);
border: 1px solid var(--gray-soft-dark);
color: var(--gray-soft-darkest);
@include text-base;
&.is-active {
border: 1px solid var(--gray-soft-darkest);
background-color: var(--gray-hard-darkest);
color: var(--gray-soft-lightest);
}
}
button:first-of-type {
border-radius: var(--border-radius) 0 0 var(--border-radius);
}
button:last-of-type {
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
}
}

View File

@ -0,0 +1,12 @@
.plan-pricing-form {
.fab-tabs .tabs li {
margin-bottom: 1.6rem;
&:hover { background-color: var(--gray-soft); }
&.react-tabs__tab--selected:hover { background-color: transparent; }
}
.react-tabs__tab-panel {
max-height: 50vh;
overflow-y: auto;
}
}

View File

@ -15,8 +15,6 @@
flex-direction: column;
gap: 3.2rem;
.fab-alert { margin: 0; }
section { @include layout-settings; }
.save-btn { align-self: flex-start; }
}

View File

@ -15,8 +15,6 @@
flex-direction: column;
gap: 3.2rem;
.fab-alert { margin: 0; }
section { @include layout-settings; }
.save-btn { align-self: flex-start; }
}

View File

@ -37,6 +37,7 @@
border-radius: var(--border-radius);
& > * { margin-bottom: 0; }
& > *:not(:last-child) { margin-bottom: 3.2rem; }
.fab-alert { margin: 0 0 1.6rem; }
}
@media (min-width: 1024px) {

View File

@ -164,6 +164,7 @@ en:
group: "Group"
transversal: "Transversal plan"
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups."
display: "Display"
category: "Category"
category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions."
number_of_periods: "Number of periods"
@ -182,7 +183,7 @@ en:
description: "Description"
information_sheet: "Information sheet"
notified_partner: "Notified partner"
new_user: "New user ..."
new_user: "New user"
alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
disabled: "Disable subscription"
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it."
@ -194,6 +195,27 @@ en:
save: "Save"
create_success: "Plan(s) successfully created. Don't forget to redefine prices."
update_success: "The plan was updated successfully"
plan_limit_form:
usage_limitation: "Limitation of use"
usage_limitation_info: "Define a maximum number of reservation hours per day and per machine category. Machine categories that have no parameters configured will not be subject to any limitation."
usage_limitation_switch: "Restrict machine reservations to a number of hours per day."
new_usage_limitation: "Add a limitation of use"
all_limitations: "All limitations"
by_categories: "By machines categories"
by_machine: "By machine"
category: "Machines category"
machine: "Machine name"
max_hours_per_day: "Max. hours/day"
plan_limit_modal:
title: "Manage limitation of use"
limit_reservations: "Limit reservations"
by_categories: "By machines categories"
by_machine: "By machine"
category: "Machines category"
machine: "Machine name"
categories_info: "If you select all machine categories, the limits will apply across the board. Please note that if you have already created limitations for specific categories, these will be permanently overwritten."
machine_info: "If you select all machines, the limits will apply across the board. Please note that if you have already created limitations for machines, these will be permanently overwritten."
max_hours_per_day: "Maximum number of reservation hours per day"
partner_modal:
title: "Create a new partner"
create_partner: "Create the partner"
@ -2300,6 +2322,7 @@ en:
stocks: "Stock:"
internal: "Private stock"
external: "Public stock"
edit: "Edit"
all: "All types"
remaining_stock: "Remaining stock"
type_in: "Add"