mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-11-29 10:24:20 +01:00
(wip) configure usage limits for plans
This commit is contained in:
parent
65afcbe2a9
commit
0f142680b8
@ -80,11 +80,12 @@ class API::PlansController < API::ApiController
|
|||||||
end
|
end
|
||||||
|
|
||||||
@parameters = @parameters.require(:plan)
|
@parameters = @parameters.require(:plan)
|
||||||
.permit(:base_name, :type, :group_id, :amount, :interval, :interval_count, :is_rolling,
|
.permit(:base_name, :type, :group_id, :amount, :interval, :interval_count, :is_rolling, :limiting,
|
||||||
:training_credit_nb, :ui_weight, :disabled, :monthly_payment, :description, :plan_category_id,
|
:training_credit_nb, :ui_weight, :disabled, :monthly_payment, :description, :plan_category_id,
|
||||||
plan_file_attributes: %i[id attachment _destroy],
|
plan_file_attributes: %i[id attachment _destroy],
|
||||||
prices_attributes: %i[id amount],
|
prices_attributes: %i[id amount],
|
||||||
advanced_accounting_attributes: %i[code analytical_section])
|
advanced_accounting_attributes: %i[code analytical_section],
|
||||||
|
plan_limitations_attributes: %i[limitable_id limitable_type limit])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -58,10 +58,10 @@ export const FormSelect = <TFieldValues extends FieldValues, TContext extends ob
|
|||||||
<AbstractSelect ref={ref}
|
<AbstractSelect ref={ref}
|
||||||
classNamePrefix="rs"
|
classNamePrefix="rs"
|
||||||
className="rs"
|
className="rs"
|
||||||
value={options.find(c => c.value === value)}
|
value={value === null ? null : options.find(c => c.value === value)}
|
||||||
onChange={val => {
|
onChange={val => {
|
||||||
onChangeCb(val.value);
|
onChangeCb(val?.value);
|
||||||
onChange(val.value);
|
onChange(val?.value);
|
||||||
}}
|
}}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
import { FieldArrayWithId, UseFieldArrayRemove } from 'react-hook-form/dist/types/fieldArray';
|
||||||
|
import { UseFormRegister } from 'react-hook-form';
|
||||||
|
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { X } from 'phosphor-react';
|
||||||
|
import { FormInput } from './form-input';
|
||||||
|
import { FieldArrayPath } from 'react-hook-form/dist/types/path';
|
||||||
|
|
||||||
|
interface FormUnsavedListProps<TFieldValues, TFieldArrayName extends FieldArrayPath<TFieldValues>, TKeyName extends string> {
|
||||||
|
fields: Array<FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>>,
|
||||||
|
remove: UseFieldArrayRemove,
|
||||||
|
register: UseFormRegister<TFieldValues>,
|
||||||
|
className?: string,
|
||||||
|
title: string,
|
||||||
|
shouldRenderField?: (field: FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>) => boolean,
|
||||||
|
formAttributeName: string,
|
||||||
|
renderFieldAttribute: (field: FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>, attribute: string) => ReactNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component render a list of unsaved attributes, created elsewhere than in the form (e.g. in a modal dialog)
|
||||||
|
* and pending for the form to be saved.
|
||||||
|
*/
|
||||||
|
export const FormUnsavedList = <TFieldValues extends FieldValues = FieldValues, TFieldArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>, TKeyName extends string = 'id'>({ fields, remove, register, className, title, shouldRenderField, formAttributeName, renderFieldAttribute }: FormUnsavedListProps<TFieldValues, TFieldArrayName, TKeyName>) => {
|
||||||
|
const { t } = useTranslation('shared');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render an unsaved field
|
||||||
|
*/
|
||||||
|
const renderUnsavedField = (field: FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>, index: number): ReactNode => {
|
||||||
|
return (
|
||||||
|
<div key={index} className="unsaved-field">
|
||||||
|
{Object.keys(field).map(attribute => (
|
||||||
|
<div className="grp" key={index}>
|
||||||
|
{renderFieldAttribute(field, attribute)}
|
||||||
|
<FormInput id={`${formAttributeName}.${index}.${attribute}`} register={register} type="hidden" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<p className="cancel-action" onClick={() => remove(index)}>
|
||||||
|
{t('app.shared.form_unsaved_list.cancel')}
|
||||||
|
<X size={20} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={`form-unsaved-list ${className || ''}`}>
|
||||||
|
<span className="title">{title}</span>
|
||||||
|
<span className="save-notice">{t('app.shared.form_unsaved_list.save_reminder')}</span>
|
||||||
|
{fields.map((field, index) => {
|
||||||
|
if (typeof shouldRenderField === 'function' && !shouldRenderField(field)) return false;
|
||||||
|
return renderUnsavedField(field, index);
|
||||||
|
}).filter(Boolean)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -326,7 +326,9 @@ export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuc
|
|||||||
id: 'usageLimits',
|
id: 'usageLimits',
|
||||||
title: t('app.admin.plan_form.tab_usage_limits'),
|
title: t('app.admin.plan_form.tab_usage_limits'),
|
||||||
content: <PlanLimitForm control={control}
|
content: <PlanLimitForm control={control}
|
||||||
formState={formState} />
|
register={register}
|
||||||
|
formState={formState}
|
||||||
|
onError={onError} />
|
||||||
}
|
}
|
||||||
]} />
|
]} />
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,23 +1,45 @@
|
|||||||
import { useState } from 'react';
|
import { ReactNode, useEffect, useState } from 'react';
|
||||||
import { Control, FormState } from 'react-hook-form/dist/types/form';
|
import { Control, FormState } from 'react-hook-form/dist/types/form';
|
||||||
import { FormSwitch } from '../form/form-switch';
|
import { FormSwitch } from '../form/form-switch';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FabButton } from '../base/fab-button';
|
import { FabButton } from '../base/fab-button';
|
||||||
import { PencilSimple, Trash } from 'phosphor-react';
|
import { PencilSimple, Trash, X } from 'phosphor-react';
|
||||||
import { PlanLimitModal } from './plan-limit-modal';
|
import { PlanLimitModal } from './plan-limit-modal';
|
||||||
|
import { Plan, PlanLimitation } from '../../models/plan';
|
||||||
|
import { useFieldArray, UseFormRegister } from 'react-hook-form';
|
||||||
|
import { FormInput } from '../form/form-input';
|
||||||
|
import { Machine } from '../../models/machine';
|
||||||
|
import { MachineCategory } from '../../models/machine-category';
|
||||||
|
import MachineAPI from '../../api/machine';
|
||||||
|
import MachineCategoryAPI from '../../api/machine-category';
|
||||||
|
import { FormUnsavedList } from '../form/form-unsaved-list';
|
||||||
|
|
||||||
interface PlanLimitFormProps<TContext extends object> {
|
interface PlanLimitFormProps<TContext extends object> {
|
||||||
control: Control<any, TContext>,
|
register: UseFormRegister<Plan>,
|
||||||
formState: FormState<any>
|
control: Control<Plan, TContext>,
|
||||||
|
formState: FormState<Plan>,
|
||||||
|
onError: (message: string) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Form tab to manage a subscription's usage limit
|
* Form tab to manage a subscription's usage limit
|
||||||
*/
|
*/
|
||||||
export const PlanLimitForm = <TContext extends object> ({ control, formState }: PlanLimitFormProps<TContext>) => {
|
export const PlanLimitForm = <TContext extends object> ({ register, control, formState, onError }: PlanLimitFormProps<TContext>) => {
|
||||||
const { t } = useTranslation('admin');
|
const { t } = useTranslation('admin');
|
||||||
|
const { fields, append, remove } = useFieldArray<Plan, 'plan_limitations_attributes'>({ control, name: 'plan_limitations_attributes' });
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
const [machines, setMachines] = useState<Array<Machine>>([]);
|
||||||
|
const [categories, setCategories] = useState<Array<MachineCategory>>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
MachineAPI.index({ disabled: false })
|
||||||
|
.then(setMachines)
|
||||||
|
.catch(onError);
|
||||||
|
MachineCategoryAPI.index()
|
||||||
|
.then(setCategories)
|
||||||
|
.catch(onError);
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens/closes the product stock edition modal
|
* Opens/closes the product stock edition modal
|
||||||
@ -26,6 +48,43 @@ export const PlanLimitForm = <TContext extends object> ({ control, formState }:
|
|||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered when a new limit was added or an existing limit was modified
|
||||||
|
*/
|
||||||
|
const onPlanLimitSuccess = (planLimit: PlanLimitation): void => {
|
||||||
|
append({ ...planLimit });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render an attribute of an unsaved limitation of use
|
||||||
|
*/
|
||||||
|
const renderOngoingLimitAttribute = (limit: PlanLimitation, attribute: string): ReactNode => {
|
||||||
|
switch (attribute) {
|
||||||
|
case 'limitable_id':
|
||||||
|
if (limit.limitable_type === 'MachineCategory') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span>{t('app.admin.plan_limit_form.category')}</span>
|
||||||
|
<p>{categories?.find(c => c.id === limit.limitable_id)?.name}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span>{t('app.admin.plan_limit_form.machine')}</span>
|
||||||
|
<p>{machines?.find(m => m.id === limit.limitable_id)?.name}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case 'limit':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span>{t('app.admin.plan_limit_form.max_hours_per_day')}</span>
|
||||||
|
<p>{limit.limit}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="plan-limit-form">
|
<div className="plan-limit-form">
|
||||||
<section>
|
<section>
|
||||||
@ -38,7 +97,7 @@ export const PlanLimitForm = <TContext extends object> ({ control, formState }:
|
|||||||
formState={formState}
|
formState={formState}
|
||||||
defaultValue={false}
|
defaultValue={false}
|
||||||
label={t('app.admin.plan_limit_form.usage_limitation_switch')}
|
label={t('app.admin.plan_limit_form.usage_limitation_switch')}
|
||||||
id="active_limitation" />
|
id="limiting" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -80,6 +139,14 @@ export const PlanLimitForm = <TContext extends object> ({ control, formState }:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<FormUnsavedList fields={fields}
|
||||||
|
remove={remove}
|
||||||
|
register={register}
|
||||||
|
title={t('app.admin.plan_limit_form.ongoing_limit')}
|
||||||
|
shouldRenderField={(limit: PlanLimitation) => limit.limitable_type === 'MachineCategory'}
|
||||||
|
formAttributeName="plan_limitations_attributes"
|
||||||
|
renderFieldAttribute={renderOngoingLimitAttribute} />
|
||||||
|
|
||||||
<div className='plan-limit-list'>
|
<div className='plan-limit-list'>
|
||||||
<p className="title">{t('app.admin.plan_limit_form.by_machine')}</p>
|
<p className="title">{t('app.admin.plan_limit_form.by_machine')}</p>
|
||||||
<div className="plan-limit-item">
|
<div className="plan-limit-item">
|
||||||
@ -108,8 +175,19 @@ export const PlanLimitForm = <TContext extends object> ({ control, formState }:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<FormUnsavedList fields={fields}
|
||||||
|
remove={remove}
|
||||||
|
register={register}
|
||||||
|
title={t('app.admin.plan_limit_form.ongoing_limit')}
|
||||||
|
shouldRenderField={(limit: PlanLimitation) => limit.limitable_type === 'Machine'}
|
||||||
|
formAttributeName="plan_limitations_attributes"
|
||||||
|
renderFieldAttribute={renderOngoingLimitAttribute} />
|
||||||
|
|
||||||
<PlanLimitModal isOpen={isOpen}
|
<PlanLimitModal isOpen={isOpen}
|
||||||
toggleModal={toggleModal} />
|
machines={machines}
|
||||||
|
categories={categories}
|
||||||
|
toggleModal={toggleModal}
|
||||||
|
onSuccess={onPlanLimitSuccess} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,51 +2,68 @@ import * as React from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FabAlert } from '../base/fab-alert';
|
import { FabAlert } from '../base/fab-alert';
|
||||||
import { FabModal, ModalSize } from '../base/fab-modal';
|
import { FabModal, ModalSize } from '../base/fab-modal';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { FormSelect } from '../form/form-select';
|
import { FormSelect } from '../form/form-select';
|
||||||
import { FormInput } from '../form/form-input';
|
import { FormInput } from '../form/form-input';
|
||||||
|
import { LimitableType, PlanLimitation } from '../../models/plan';
|
||||||
|
import { Machine } from '../../models/machine';
|
||||||
|
import { MachineCategory } from '../../models/machine-category';
|
||||||
|
import { SelectOption } from '../../models/select';
|
||||||
|
import { FabButton } from '../base/fab-button';
|
||||||
|
|
||||||
type typeSelectOption = { value: any, label: string };
|
|
||||||
interface PlanLimitModalProps {
|
interface PlanLimitModalProps {
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
toggleModal: () => void,
|
toggleModal: () => void,
|
||||||
|
onSuccess: (limit: PlanLimitation) => void,
|
||||||
|
machines: Array<Machine>
|
||||||
|
categories: Array<MachineCategory>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Form to manage subscriptions limitations of use
|
* Form to manage subscriptions limitations of use
|
||||||
*/
|
*/
|
||||||
export const PlanLimitModal: React.FC<PlanLimitModalProps> = ({ isOpen, toggleModal }) => {
|
export const PlanLimitModal: React.FC<PlanLimitModalProps> = ({ isOpen, toggleModal, machines, categories, onSuccess }) => {
|
||||||
const { t } = useTranslation('admin');
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
const { register, control, formState } = useForm<any>();
|
const { register, control, formState, setValue, handleSubmit } = useForm<PlanLimitation>({ defaultValues: { limitable_type: 'MachineCategory' } });
|
||||||
const [limitType, setLimitType] = React.useState<'categories' | 'machine'>('categories');
|
const limitType = useWatch({ control, name: 'limitable_type' });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle the form between 'categories' and 'machine'
|
* Toggle the form between 'categories' and 'machine'
|
||||||
*/
|
*/
|
||||||
const toggleLimitType = (evt: React.MouseEvent<HTMLButtonElement, MouseEvent>, type: 'categories' | 'machine') => {
|
const toggleLimitType = (evt: React.MouseEvent<HTMLButtonElement, MouseEvent>, type: LimitableType) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
setLimitType(type);
|
setValue('limitable_type', type);
|
||||||
|
setValue('limitable_id', null);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates options to the react-select format
|
* Callback triggered when the user validates the new limit.
|
||||||
|
* We do not use handleSubmit() directly to prevent the propagaion of the "submit" event to the parent form
|
||||||
*/
|
*/
|
||||||
const buildMachinesCategoriesOptions = (): Array<typeSelectOption> => {
|
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
return [
|
if (event) {
|
||||||
{ value: '0', label: 'yep' },
|
event.stopPropagation();
|
||||||
{ value: '1', label: 'nope' }
|
event.preventDefault();
|
||||||
];
|
}
|
||||||
|
return handleSubmit((data: PlanLimitation) => {
|
||||||
|
onSuccess(data);
|
||||||
|
toggleModal();
|
||||||
|
})(event);
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Creates options to the react-select format
|
* Creates options to the react-select format
|
||||||
*/
|
*/
|
||||||
const buildMachinesOptions = (): Array<typeSelectOption> => {
|
const buildOptions = (): Array<SelectOption<number>> => {
|
||||||
return [
|
if (limitType === 'MachineCategory') {
|
||||||
{ value: '0', label: 'pif' },
|
return categories.map(cat => {
|
||||||
{ value: '1', label: 'paf' },
|
return { value: cat.id, label: cat.name };
|
||||||
{ value: '2', label: 'pouf' }
|
});
|
||||||
];
|
} else {
|
||||||
|
return machines.map(machine => {
|
||||||
|
return { value: machine.id, label: machine.name };
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -55,43 +72,35 @@ export const PlanLimitModal: React.FC<PlanLimitModalProps> = ({ isOpen, toggleMo
|
|||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
toggleModal={toggleModal}
|
toggleModal={toggleModal}
|
||||||
closeButton>
|
closeButton>
|
||||||
<form className='plan-limit-modal'>
|
<form className='plan-limit-modal' onSubmit={onSubmit}>
|
||||||
<p className='subtitle'>{t('app.admin.plan_limit_modal.limit_reservations')}</p>
|
<p className='subtitle'>{t('app.admin.plan_limit_modal.limit_reservations')}</p>
|
||||||
<div className="grp">
|
<div className="grp">
|
||||||
<button onClick={evt => toggleLimitType(evt, 'categories')}
|
<button onClick={evt => toggleLimitType(evt, 'MachineCategory')}
|
||||||
className={limitType === 'categories' ? 'is-active' : ''}>
|
className={limitType === 'MachineCategory' ? 'is-active' : ''}>
|
||||||
{t('app.admin.plan_limit_modal.by_categories')}
|
{t('app.admin.plan_limit_modal.by_categories')}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={evt => toggleLimitType(evt, 'machine')}
|
<button onClick={evt => toggleLimitType(evt, 'Machine')}
|
||||||
className={limitType === 'machine' ? 'is-active' : ''}>
|
className={limitType === 'Machine' ? 'is-active' : ''}>
|
||||||
{t('app.admin.plan_limit_modal.by_machine')}
|
{t('app.admin.plan_limit_modal.by_machine')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
<FabAlert level='info'>{t('app.admin.plan_limit_modal.machine_info')}</FabAlert>
|
||||||
<FormSelect options={buildMachinesOptions()}
|
<FormInput register={register} id="limitable_type" type="hidden" />
|
||||||
|
<FormSelect options={buildOptions()}
|
||||||
control={control}
|
control={control}
|
||||||
id="machine"
|
id="limitable_id"
|
||||||
rules={{ required: limitType === 'machine' }}
|
rules={{ required: true }}
|
||||||
formState={formState}
|
formState={formState}
|
||||||
label={t('app.admin.plan_limit_modal.machine')} />
|
label={t('app.admin.plan_limit_modal.machine')} />
|
||||||
</>}
|
<FormInput id="limit"
|
||||||
<FormInput id="hours_limit"
|
|
||||||
type="number"
|
type="number"
|
||||||
register={register}
|
register={register}
|
||||||
rules={{ required: true, min: 1 }}
|
rules={{ required: true, min: 1 }}
|
||||||
|
nullable
|
||||||
step={1}
|
step={1}
|
||||||
formState={formState}
|
formState={formState}
|
||||||
label={t('app.admin.plan_limit_modal.max_hours_per_day')} />
|
label={t('app.admin.plan_limit_modal.max_hours_per_day')} />
|
||||||
|
<FabButton type="submit">{t('app.admin.plan_limit_modal.confirm')}</FabButton>
|
||||||
</form>
|
</form>
|
||||||
</FabModal>
|
</FabModal>
|
||||||
);
|
);
|
||||||
|
@ -12,6 +12,14 @@ export interface Partner {
|
|||||||
email: string
|
email: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LimitableType = 'Machine'|'MachineCategory';
|
||||||
|
export interface PlanLimitation {
|
||||||
|
id?: number,
|
||||||
|
limitable_id: number,
|
||||||
|
limitable_type: LimitableType,
|
||||||
|
limit: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface Plan {
|
export interface Plan {
|
||||||
id?: number,
|
id?: number,
|
||||||
base_name: string,
|
base_name: string,
|
||||||
@ -35,7 +43,8 @@ export interface Plan {
|
|||||||
partner_id?: number,
|
partner_id?: number,
|
||||||
partnership?: boolean,
|
partnership?: boolean,
|
||||||
partners?: Array<Partner>,
|
partners?: Array<Partner>,
|
||||||
advanced_accounting_attributes?: AdvancedAccounting
|
advanced_accounting_attributes?: AdvancedAccounting,
|
||||||
|
plan_limitations_attributes: Array<PlanLimitation>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlansDuration {
|
export interface PlansDuration {
|
||||||
|
@ -5,6 +5,32 @@
|
|||||||
|
|
||||||
section { @include layout-settings; }
|
section { @include layout-settings; }
|
||||||
|
|
||||||
|
.ongoing-limits {
|
||||||
|
margin: 2.4rem 0;
|
||||||
|
.save-notice {
|
||||||
|
@include text-xs;
|
||||||
|
margin-left: 1rem;
|
||||||
|
color: var(--alert);
|
||||||
|
}
|
||||||
|
.unsaved-plan-limit {
|
||||||
|
background-color: var(--gray-soft-light);
|
||||||
|
border: 0;
|
||||||
|
padding: 1.2rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
.cancel-action {
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
margin-left: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.plan-limit-grp {
|
.plan-limit-grp {
|
||||||
header {
|
header {
|
||||||
@include header();
|
@include header();
|
||||||
|
@ -42,6 +42,8 @@ class Machine < ApplicationRecord
|
|||||||
|
|
||||||
belongs_to :machine_category
|
belongs_to :machine_category
|
||||||
|
|
||||||
|
has_many :plan_limitations, dependent: :destroy
|
||||||
|
|
||||||
after_create :create_statistic_subtype
|
after_create :create_statistic_subtype
|
||||||
after_create :create_machine_prices
|
after_create :create_machine_prices
|
||||||
after_create :update_gateway_product
|
after_create :update_gateway_product
|
||||||
|
@ -4,4 +4,5 @@
|
|||||||
class MachineCategory < ApplicationRecord
|
class MachineCategory < ApplicationRecord
|
||||||
has_many :machines, dependent: :nullify
|
has_many :machines, dependent: :nullify
|
||||||
accepts_nested_attributes_for :machines, allow_destroy: true
|
accepts_nested_attributes_for :machines, allow_destroy: true
|
||||||
|
has_many :plan_limitations, dependent: :destroy
|
||||||
end
|
end
|
||||||
|
@ -21,6 +21,9 @@ class Plan < ApplicationRecord
|
|||||||
has_many :cart_item_subscriptions, class_name: 'CartItem::Subscription', dependent: :destroy
|
has_many :cart_item_subscriptions, class_name: 'CartItem::Subscription', dependent: :destroy
|
||||||
has_many :cart_item_payment_schedules, class_name: 'CartItem::PaymentSchedule', dependent: :destroy
|
has_many :cart_item_payment_schedules, class_name: 'CartItem::PaymentSchedule', dependent: :destroy
|
||||||
|
|
||||||
|
has_many :plan_limitations, dependent: :destroy
|
||||||
|
accepts_nested_attributes_for :plan_limitations, allow_destroy: true
|
||||||
|
|
||||||
extend FriendlyId
|
extend FriendlyId
|
||||||
friendly_id :base_name, use: :slugged
|
friendly_id :base_name, use: :slugged
|
||||||
|
|
||||||
|
12
app/models/plan_limitation.rb
Normal file
12
app/models/plan_limitation.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Allows to set booking limits on some resources, per plan.
|
||||||
|
class PlanLimitation < ApplicationRecord
|
||||||
|
belongs_to :plan
|
||||||
|
|
||||||
|
belongs_to :limitable, polymorphic: true
|
||||||
|
belongs_to :machine, foreign_type: 'Machine', foreign_key: 'limitable_id', inverse_of: :plan_limitations
|
||||||
|
belongs_to :machine_category, foreign_type: 'MachineCategory', foreign_key: 'limitable_id', inverse_of: :plan_limitations
|
||||||
|
|
||||||
|
validates :limitable_id, :limitable_type, :limit, presence: true
|
||||||
|
end
|
@ -27,3 +27,7 @@ if plan.advanced_accounting
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
json.plan_limitations_attributes plan.plan_limitations do |limitation|
|
||||||
|
json.extract! limitation, :id, :limitable_id, :limitable_type, :limit
|
||||||
|
end
|
||||||
|
|
||||||
|
@ -1 +1,3 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
json.partial! 'api/plans/plan', plan: @plan
|
json.partial! 'api/plans/plan', plan: @plan
|
||||||
|
@ -206,6 +206,9 @@ en:
|
|||||||
category: "Machines category"
|
category: "Machines category"
|
||||||
machine: "Machine name"
|
machine: "Machine name"
|
||||||
max_hours_per_day: "Max. hours/day"
|
max_hours_per_day: "Max. hours/day"
|
||||||
|
ongoing_limit: "Ongoing settings"
|
||||||
|
save_reminder: "Don't forget to save your settings"
|
||||||
|
cancel: "Cancel this limitation"
|
||||||
plan_limit_modal:
|
plan_limit_modal:
|
||||||
title: "Manage limitation of use"
|
title: "Manage limitation of use"
|
||||||
limit_reservations: "Limit reservations"
|
limit_reservations: "Limit reservations"
|
||||||
@ -216,6 +219,7 @@ en:
|
|||||||
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."
|
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."
|
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"
|
max_hours_per_day: "Maximum number of reservation hours per day"
|
||||||
|
confirm: "Confirm"
|
||||||
partner_modal:
|
partner_modal:
|
||||||
title: "Create a new partner"
|
title: "Create a new partner"
|
||||||
create_partner: "Create the partner"
|
create_partner: "Create the partner"
|
||||||
|
8
db/migrate/20230307123611_add_limiting_to_plan.rb
Normal file
8
db/migrate/20230307123611_add_limiting_to_plan.rb
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# From this migration, any subscription plan can define restrictions on the reservation of resources
|
||||||
|
class AddLimitingToPlan < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_column :plans, :limiting, :boolean
|
||||||
|
end
|
||||||
|
end
|
14
db/migrate/20230307123841_create_plan_limitations.rb
Normal file
14
db/migrate/20230307123841_create_plan_limitations.rb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# This table saves the restrictions settings, per plan and resource
|
||||||
|
class CreatePlanLimitations < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_table :plan_limitations do |t|
|
||||||
|
t.references :plan, foreign_key: true, index: true
|
||||||
|
t.references :limitable, polymorphic: true
|
||||||
|
t.integer :limit, null: false, default: 0
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user