diff --git a/app/frontend/src/javascript/components/plans/plan-form.tsx b/app/frontend/src/javascript/components/plans/plan-form.tsx index e7c18d891..77b596868 100644 --- a/app/frontend/src/javascript/components/plans/plan-form.tsx +++ b/app/frontend/src/javascript/components/plans/plan-form.tsx @@ -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 = ({ action, plan, onError, onSuc formState={formState} rules={{ required: true }} /> + {action === 'update' && + {t('app.admin.plan_form.edit_amount_info')} + } = ({ action, plan, onError, onSuc - {categories?.length > 0 && } - {action === 'update' && - {t('app.admin.plan_form.edit_amount_info')} - } +
+
+

{t('app.admin.plan_form.display')}

+
+
+ {categories?.length > 0 && } + +
+
- +
+ +
- {action === 'update' && = ({ action, plan, onError, onSuc { id: 'usageLimits', title: t('app.admin.plan_form.tab_usage_limits'), - content:
plop
+ content: } ]} /> diff --git a/app/frontend/src/javascript/components/plans/plan-limit-form.tsx b/app/frontend/src/javascript/components/plans/plan-limit-form.tsx new file mode 100644 index 000000000..d5e41b05e --- /dev/null +++ b/app/frontend/src/javascript/components/plans/plan-limit-form.tsx @@ -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 { + control: Control, + formState: FormState +} + +/** + * Form tab to manage a subscription's usage limit + */ +export const PlanLimitForm = ({ control, formState }: PlanLimitFormProps) => { + const { t } = useTranslation('admin'); + + const [isOpen, setIsOpen] = useState(false); + + /** + * Opens/closes the product stock edition modal + */ + const toggleModal = (): void => { + setIsOpen(!isOpen); + }; + + return ( +
+
+
+

{t('app.admin.plan_limit_form.usage_limitation')}

+

{t('app.admin.plan_limit_form.usage_limitation_info')}

+
+
+ +
+
+ +
+
+

{t('app.admin.plan_limit_form.all_limitations')}

+
+ + {t('app.admin.plan_limit_form.new_usage_limitation')} + +
+
+ +
+

{t('app.admin.plan_limit_form.by_categories')}

+
+
+ {t('app.admin.plan_limit_form.category')} +

Plop

+
+
+ {t('app.admin.plan_limit_form.max_hours_per_day')} +

5

+
+ +
+
+ + + + + + +
+
+
+
+ +
+

{t('app.admin.plan_limit_form.by_machine')}

+
+
+ {t('app.admin.plan_limit_form.machine')} +

Pouet

+
+
+ {t('app.admin.plan_limit_form.max_hours_per_day')} +

5

+
+ +
+
+ + + + + + +
+
+
+
+
+ + +
+ ); +}; diff --git a/app/frontend/src/javascript/components/plans/plan-limit-modal.tsx b/app/frontend/src/javascript/components/plans/plan-limit-modal.tsx new file mode 100644 index 000000000..42b54b2d9 --- /dev/null +++ b/app/frontend/src/javascript/components/plans/plan-limit-modal.tsx @@ -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 = ({ isOpen, toggleModal }) => { + const { t } = useTranslation('admin'); + + const { register, control, formState } = useForm(); + const [limitType, setLimitType] = React.useState<'categories' | 'machine'>('categories'); + + /** + * Toggle the form between 'categories' and 'machine' + */ + const toggleLimitType = (evt: React.MouseEvent, type: 'categories' | 'machine') => { + evt.preventDefault(); + setLimitType(type); + }; + + /** + * Creates options to the react-select format + */ + const buildMachinesCategoriesOptions = (): Array => { + return [ + { value: '0', label: 'yep' }, + { value: '1', label: 'nope' } + ]; + }; + /** + * Creates options to the react-select format + */ + const buildMachinesOptions = (): Array => { + return [ + { value: '0', label: 'pif' }, + { value: '1', label: 'paf' }, + { value: '2', label: 'pouf' } + ]; + }; + + return ( + +
+

{t('app.admin.plan_limit_modal.limit_reservations')}

+
+ + +
+ {limitType === 'categories' && <> + {t('app.admin.plan_limit_modal.categories_info')} + + } + {limitType === 'machine' && <> + {t('app.admin.plan_limit_modal.machine_info')} + + } + + +
+ ); +}; diff --git a/app/frontend/src/javascript/components/plans/plan-pricing-form.tsx b/app/frontend/src/javascript/components/plans/plan-pricing-form.tsx index 637ecfe76..13d31f4e7 100644 --- a/app/frontend/src/javascript/components/plans/plan-pricing-form.tsx +++ b/app/frontend/src/javascript/components/plans/plan-pricing-form.tsx @@ -92,32 +92,36 @@ export const PlanPricingForm = ({ register, control, fo }; return ( -
-

{t('app.admin.plan_pricing_form.prices')}

- {plans && } - { { - 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) - } - ]} />} -
+
+
+

{t('app.admin.plan_pricing_form.prices')}

+

{t('app.admin.plan_pricing_form.copy_prices_from_help')}

+
+
+ {plans && } + { { + 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) + } + ]} />} +
+
); }; diff --git a/app/frontend/src/javascript/components/store/product-stock-form.tsx b/app/frontend/src/javascript/components/store/product-stock-form.tsx index bf12a121f..40cfaf423 100644 --- a/app/frontend/src/javascript/components/store/product-stock-form.tsx +++ b/app/frontend/src/javascript/components/store/product-stock-form.tsx @@ -178,7 +178,7 @@ export const ProductStockForm = ({ currentFormValues, {t('app.admin.store.product_stock_form.external')}

{currentFormValues?.stock?.external}

- } className="is-black">Modifier + } className="is-black">{t('app.admin.store.product_stock_form.edit')} {fields.length > 0 &&
diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index 22fc9555c..f89015592 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -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"; diff --git a/app/frontend/src/stylesheets/modules/base/fab-tabs.scss b/app/frontend/src/stylesheets/modules/base/fab-tabs.scss index 0b1019752..ecffcfd65 100644 --- a/app/frontend/src/stylesheets/modules/base/fab-tabs.scss +++ b/app/frontend/src/stylesheets/modules/base/fab-tabs.scss @@ -26,6 +26,7 @@ background-color: var(--gray-soft-lightest); cursor: default; } + &:focus { outline: none; } } } } diff --git a/app/frontend/src/stylesheets/modules/events/event-form.scss b/app/frontend/src/stylesheets/modules/events/event-form.scss index e113f927e..92d91a1ae 100644 --- a/app/frontend/src/stylesheets/modules/events/event-form.scss +++ b/app/frontend/src/stylesheets/modules/events/event-form.scss @@ -15,8 +15,6 @@ flex-direction: column; gap: 3.2rem; - .fab-alert { margin: 0; } - section { @include layout-settings; } .save-btn { align-self: flex-start; } } diff --git a/app/frontend/src/stylesheets/modules/machines/machine-form.scss b/app/frontend/src/stylesheets/modules/machines/machine-form.scss index 90c099238..28364b434 100644 --- a/app/frontend/src/stylesheets/modules/machines/machine-form.scss +++ b/app/frontend/src/stylesheets/modules/machines/machine-form.scss @@ -15,8 +15,6 @@ flex-direction: column; gap: 3.2rem; - .fab-alert { margin: 0; } - section { @include layout-settings; } .save-btn { align-self: flex-start; } } diff --git a/app/frontend/src/stylesheets/modules/plans/plan-form.scss b/app/frontend/src/stylesheets/modules/plans/plan-form.scss index 4b7841d72..7dde64762 100644 --- a/app/frontend/src/stylesheets/modules/plans/plan-form.scss +++ b/app/frontend/src/stylesheets/modules/plans/plan-form.scss @@ -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; - //} } diff --git a/app/frontend/src/stylesheets/modules/plans/plan-limit-form.scss b/app/frontend/src/stylesheets/modules/plans/plan-limit-form.scss new file mode 100644 index 000000000..117e66eb2 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/plans/plan-limit-form.scss @@ -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) } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/frontend/src/stylesheets/modules/plans/plan-limit-modal.scss b/app/frontend/src/stylesheets/modules/plans/plan-limit-modal.scss new file mode 100644 index 000000000..08d11d0eb --- /dev/null +++ b/app/frontend/src/stylesheets/modules/plans/plan-limit-modal.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/app/frontend/src/stylesheets/modules/plans/plan-pricing-form.scss b/app/frontend/src/stylesheets/modules/plans/plan-pricing-form.scss new file mode 100644 index 000000000..68a9e5756 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/plans/plan-pricing-form.scss @@ -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; + } +} \ No newline at end of file diff --git a/app/frontend/src/stylesheets/modules/spaces/space-form.scss b/app/frontend/src/stylesheets/modules/spaces/space-form.scss index 0db00f748..f8caba094 100644 --- a/app/frontend/src/stylesheets/modules/spaces/space-form.scss +++ b/app/frontend/src/stylesheets/modules/spaces/space-form.scss @@ -15,8 +15,6 @@ flex-direction: column; gap: 3.2rem; - .fab-alert { margin: 0; } - section { @include layout-settings; } .save-btn { align-self: flex-start; } } diff --git a/app/frontend/src/stylesheets/modules/trainings/training-form.scss b/app/frontend/src/stylesheets/modules/trainings/training-form.scss index adf701132..1eeb90524 100644 --- a/app/frontend/src/stylesheets/modules/trainings/training-form.scss +++ b/app/frontend/src/stylesheets/modules/trainings/training-form.scss @@ -15,8 +15,6 @@ flex-direction: column; gap: 3.2rem; - .fab-alert { margin: 0; } - section { @include layout-settings; } .save-btn { align-self: flex-start; } } diff --git a/app/frontend/src/stylesheets/variables/layout.scss b/app/frontend/src/stylesheets/variables/layout.scss index f5a51adad..b61ccb975 100644 --- a/app/frontend/src/stylesheets/variables/layout.scss +++ b/app/frontend/src/stylesheets/variables/layout.scss @@ -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) { diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index f00a72928..6cd746770 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -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"