From d6a4675209b18d2e2ac6b70deb8a6b65bc77c3c1 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 21 Dec 2021 14:37:38 +0100 Subject: [PATCH] wip --- .../pricing/configure-timeslot-button.tsx | 75 +++++++++++++++++++ .../components/pricing/create-timeslot.tsx | 70 +++++++++++++++++ .../components/pricing/spaces-pricing.tsx | 17 +++-- .../components/pricing/timeslot-form.tsx | 74 ++++++++++++++++++ .../templates/admin/pricing/spaces.html | 33 +------- config/locales/app.admin.en.yml | 18 +++++ 6 files changed, 249 insertions(+), 38 deletions(-) create mode 100644 app/frontend/src/javascript/components/pricing/configure-timeslot-button.tsx create mode 100644 app/frontend/src/javascript/components/pricing/create-timeslot.tsx create mode 100644 app/frontend/src/javascript/components/pricing/timeslot-form.tsx diff --git a/app/frontend/src/javascript/components/pricing/configure-timeslot-button.tsx b/app/frontend/src/javascript/components/pricing/configure-timeslot-button.tsx new file mode 100644 index 000000000..71c2e6393 --- /dev/null +++ b/app/frontend/src/javascript/components/pricing/configure-timeslot-button.tsx @@ -0,0 +1,75 @@ +import React, { ReactNode, useState } from 'react'; +import { Price } from '../../models/price'; +import { useTranslation } from 'react-i18next'; +import { FabPopover } from '../base/fab-popover'; +import { CreateTimeslot } from './create-timeslot'; +import PriceAPI from '../../api/price'; +import FormatLib from '../../lib/format'; + +interface ConfigureTimeslotButtonProps { + prices: Array, + onError: (message: string) => void, + onSuccess: (message: string) => void, + groupId: number, + priceableId: number, + priceableType: string, +} + +/** + * This component is a button that shows the list of timeslots. + * It also triggers modal dialogs to configure (add/delete/edit/remove) timeslots. + */ +export const ConfigureTimeslotButton: React.FC = ({ prices, onError, onSuccess, groupId, priceableId, priceableType }) => { + const { t } = useTranslation('admin'); + + const [timeslots, setTimeslots] = useState>(prices); + const [showList, setShowList] = useState(false); + + /** + * Open/closes the popover listing the existing packs + */ + const toggleShowList = (): void => { + setShowList(!showList); + }; + + /** + * Callback triggered when the timeslot was successfully created/deleted/updated. + * We refresh the list of timeslots. + */ + const handleSuccess = (message: string) => { + onSuccess(message); + PriceAPI.index({ group_id: groupId, priceable_id: priceableId, priceable_type: priceableType }) + .then(data => setTimeslots(data)) + .catch(error => onError(error)); + }; + + /** + * Render the button used to trigger the "new pack" modal + */ + const renderAddButton = (): ReactNode => { + return ; + }; + + return ( +
+ + {showList && +
    + {timeslots?.map(timeslot => +
  • + {timeslot.duration} {t('app.admin.calendar.minutes')} - {FormatLib.price(timeslot.amount)} + + +
  • )} +
+ {timeslots?.length === 0 && {t('app.admin.configure_timeslots_button.no_timeslots')}} +
} +
+ ); +}; diff --git a/app/frontend/src/javascript/components/pricing/create-timeslot.tsx b/app/frontend/src/javascript/components/pricing/create-timeslot.tsx new file mode 100644 index 000000000..b3e15f7a2 --- /dev/null +++ b/app/frontend/src/javascript/components/pricing/create-timeslot.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import { FabModal } from '../base/fab-modal'; +import { TimeslotForm } from './timeslot-form'; +import { Price } from '../../models/price'; +import PrepaidPackAPI from '../../api/prepaid-pack'; +import { useTranslation } from 'react-i18next'; +import { FabAlert } from '../base/fab-alert'; + +interface CreateTimeslotProps { + onSuccess: (message: string) => void, + onError: (message: string) => void, + groupId: number, + priceableId: number, + priceableType: string, +} + +/** + * This component shows a button. + * When clicked, we show a modal dialog handing the process of creating a new time slot + */ +export const CreateTimeslot: React.FC = ({ onSuccess, onError, groupId, priceableId, priceableType }) => { + const { t } = useTranslation('admin'); + + const [isOpen, setIsOpen] = useState(false); + + /** + * Open/closes the "new pack" modal dialog + */ + const toggleModal = (): void => { + setIsOpen(!isOpen); + }; + + /** + * Callback triggered when the user has validated the creation of the new time slot + */ + const handleSubmit = (timeslot: Price): void => { + // set the already-known attributes of the new pack + const newTimeslot = Object.assign({} as Price, timeslot); + newTimeslot.group_id = groupId; + newTimeslot.priceable_id = priceableId; + newTimeslot.priceable_type = priceableType; + + // create it on the API + console.log('newTimeslot :', newTimeslot); + // PrepaidPackAPI.create(newPack) + // .then(() => { + // onSuccess(t('app.admin.create_timeslot.timeslot_successfully_created')); + // toggleModal(); + // }) + // .catch(error => onError(error)); + }; + + return ( +
+ + + + {t('app.admin.create_timeslot.new_timeslot_info', { TYPE: priceableType })} + + + +
+ ); +}; diff --git a/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx b/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx index 5ea7220c5..5536000dc 100644 --- a/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx @@ -9,6 +9,7 @@ import GroupAPI from '../../api/group'; import { Group } from '../../models/group'; import { IApplication } from '../../models/application'; import { EditablePrice } from './editable-price'; +import { ConfigureTimeslotButton } from './configure-timeslot-button'; import PriceAPI from '../../api/price'; import { Price } from '../../models/price'; import { useImmer } from 'use-immer'; @@ -61,11 +62,8 @@ const SpacesPricing: React.FC = ({ onError, onSuccess }) => return FormatLib.price(price); }; - /** - * Find the price matching the given criterion - */ - const findPriceBy = (spaceId, groupId): Price => { - return prices.find(price => price.priceable_id === spaceId && price.group_id === groupId); + const findPricesBy = (spaceId, groupId): Array => { + return prices.filter(price => price.priceable_id === spaceId && price.group_id === groupId); }; /** @@ -109,7 +107,14 @@ const SpacesPricing: React.FC = ({ onError, onSuccess }) => {spaces?.map(space => {space.name} {groups?.map(group => - {prices && } + {prices && } + )} )} diff --git a/app/frontend/src/javascript/components/pricing/timeslot-form.tsx b/app/frontend/src/javascript/components/pricing/timeslot-form.tsx new file mode 100644 index 000000000..bc31fba97 --- /dev/null +++ b/app/frontend/src/javascript/components/pricing/timeslot-form.tsx @@ -0,0 +1,74 @@ +import React, { BaseSyntheticEvent } from 'react'; +import { Price } from '../../models/price'; +import { useTranslation } from 'react-i18next'; +import { useImmer } from 'use-immer'; +import { FabInput } from '../base/fab-input'; +import { IFablab } from '../../models/fablab'; + +declare let Fablab: IFablab; + +interface PackFormProps { + formId: string, + onSubmit: (pack: Price) => void, + price?: Price, +} + +/** + * A form component to create/edit a time slot. + * The form validation must be created elsewhere, using the attribute form={formId}. + */ +export const TimeslotForm: React.FC = ({ formId, onSubmit, price }) => { + const [timeslotData, updateTimeslotData] = useImmer(price || {} as Price); + + const { t } = useTranslation('admin'); + + /** + * Callback triggered when the user sends the form. + */ + const handleSubmit = (event: BaseSyntheticEvent): void => { + event.preventDefault(); + onSubmit(timeslotData); + }; + + /** + * Callback triggered when the user inputs an amount for the current time slot. + */ + const handleUpdateAmount = (amount: string) => { + updateTimeslotData(draft => { + draft.amount = parseFloat(amount); + }); + }; + + /** + * Callback triggered when the user inputs a number of minutes for the current time slot. + */ + const handleUpdateHours = (minutes: string) => { + updateTimeslotData(draft => { + draft.duration = parseInt(minutes, 10); + }); + }; + + return ( +
+ + } + required /> + + } + addOn={Fablab.intl_currency} + required /> + + ); +}; diff --git a/app/frontend/templates/admin/pricing/spaces.html b/app/frontend/templates/admin/pricing/spaces.html index 4165e5ca8..eb4ca3899 100644 --- a/app/frontend/templates/admin/pricing/spaces.html +++ b/app/frontend/templates/admin/pricing/spaces.html @@ -1,32 +1 @@ - - - + diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 570dd1a24..784b2874c 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -378,6 +378,9 @@ en: packs: "Prepaid packs" no_packs: "No packs for now" pack_DURATION: "{DURATION} hours" + configure_timeslots_button: + timeslots: "Time slots" + no_timeslots: "No time slot for now" pack_form: hours: "Hours" amount: "Price" @@ -404,6 +407,21 @@ en: edit_pack: "Edit the pack" confirm_changes: "Confirm changes" pack_successfully_updated: "The prepaid pack was successfully updated." + create_timeslot: + new_timeslot: "New time slot" + new_timeslot_info: "..." + create_timeslot: "Create this time slot" + timeslot_successfully_created: "The new time slot was successfully created." + delete_timeslot: + timeslot_deleted: "The time slot was successfully deleted." + unable_to_delete: "Unable to delete the time slot: " + delete_timeslot: "Delete the time slot" + confirm_delete: "Delete" + delete_confirmation: "Are you sure you want to delete this time slot? This won't be possible if it was already bought by users." + edit_timeslot: + edit_timeslot: "Edit the time slot" + confirm_changes: "Confirm changes" + timeslot_successfully_updated: "The time slot was successfully updated." #ajouter un code promotionnel coupons_new: add_a_coupon: "Add a coupon"