From bd781a14e952b68b5b98af782cddec1388c9b116 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 21 Dec 2021 17:13:40 +0100 Subject: [PATCH] Add extended price --- app/controllers/api/prices_controller.rb | 14 +++- app/frontend/src/javascript/api/price.ts | 10 +++ ...sx => configure-extended-price-button.tsx} | 34 +++++----- ...timeslot.tsx => create-extended-price.tsx} | 43 ++++++------ .../pricing/delete-extended-price.tsx | 56 ++++++++++++++++ .../pricing/edit-extended-price.tsx | 66 +++++++++++++++++++ ...eslot-form.tsx => extended-price-form.tsx} | 20 +++--- .../components/pricing/spaces-pricing.tsx | 4 +- app/models/price.rb | 2 +- app/policies/price_policy.rb | 4 ++ config/locales/app.admin.en.yml | 32 ++++----- config/routes.rb | 2 +- 12 files changed, 218 insertions(+), 69 deletions(-) rename app/frontend/src/javascript/components/pricing/{configure-timeslot-button.tsx => configure-extended-price-button.tsx} (51%) rename app/frontend/src/javascript/components/pricing/{create-timeslot.tsx => create-extended-price.tsx} (52%) create mode 100644 app/frontend/src/javascript/components/pricing/delete-extended-price.tsx create mode 100644 app/frontend/src/javascript/components/pricing/edit-extended-price.tsx rename app/frontend/src/javascript/components/pricing/{timeslot-form.tsx => extended-price-form.tsx} (77%) diff --git a/app/controllers/api/prices_controller.rb b/app/controllers/api/prices_controller.rb index 9eb21c145..de84774a2 100644 --- a/app/controllers/api/prices_controller.rb +++ b/app/controllers/api/prices_controller.rb @@ -4,9 +4,10 @@ # Prices are used in reservations (Machine, Space) class API::PricesController < API::ApiController before_action :authenticate_user! + before_action :set_price, only: %i[update destroy] def create - @price = Price.new(price_params) + @price = Price.new(price_create_params) @price.amount *= 100 authorize @price @@ -24,7 +25,6 @@ class API::PricesController < API::ApiController def update authorize Price - @price = Price.find(params[:id]) price_parameters = price_params price_parameters[:amount] = price_parameters[:amount] * 100 if @price.update(price_parameters) @@ -34,6 +34,12 @@ class API::PricesController < API::ApiController end end + def destroy + authorize @price + @price.destroy + head :no_content + end + def compute cs = CartService.new(current_user) cart = cs.from_hash(params) @@ -42,6 +48,10 @@ class API::PricesController < API::ApiController private + def set_price + @price = Price.find(params[:id]) + end + def price_create_params params.require(:price).permit(:amount, :duration, :group_id, :plan_id, :priceable_id, :priceable_type) end diff --git a/app/frontend/src/javascript/api/price.ts b/app/frontend/src/javascript/api/price.ts index d5ab87ec4..d85616a1e 100644 --- a/app/frontend/src/javascript/api/price.ts +++ b/app/frontend/src/javascript/api/price.ts @@ -14,11 +14,21 @@ export default class PriceAPI { return res?.data; } + static async create (price: Price): Promise { + const res: AxiosResponse = await apiClient.post('/api/prices', { price }); + return res?.data; + } + static async update (price: Price): Promise { const res: AxiosResponse = await apiClient.patch(`/api/prices/${price.id}`, { price }); return res?.data; } + static async destroy (priceId: number): Promise { + const res: AxiosResponse = await apiClient.delete(`/api/prices/${priceId}`); + return res?.data; + } + private static filtersToQuery (filters?: PriceIndexFilter): string { if (!filters) return ''; diff --git a/app/frontend/src/javascript/components/pricing/configure-timeslot-button.tsx b/app/frontend/src/javascript/components/pricing/configure-extended-price-button.tsx similarity index 51% rename from app/frontend/src/javascript/components/pricing/configure-timeslot-button.tsx rename to app/frontend/src/javascript/components/pricing/configure-extended-price-button.tsx index 71c2e6393..66705ccc0 100644 --- a/app/frontend/src/javascript/components/pricing/configure-timeslot-button.tsx +++ b/app/frontend/src/javascript/components/pricing/configure-extended-price-button.tsx @@ -2,11 +2,13 @@ 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 { CreateExtendedPrice } from './create-extended-price'; import PriceAPI from '../../api/price'; import FormatLib from '../../lib/format'; +import { EditExtendedPrice } from './edit-extended-price'; +import { DeleteExtendedPrice } from './delete-extended-price'; -interface ConfigureTimeslotButtonProps { +interface ConfigureExtendedPriceButtonProps { prices: Array, onError: (message: string) => void, onSuccess: (message: string) => void, @@ -16,13 +18,13 @@ interface ConfigureTimeslotButtonProps { } /** - * This component is a button that shows the list of timeslots. - * It also triggers modal dialogs to configure (add/delete/edit/remove) timeslots. + * This component is a button that shows the list of extendedPrices. + * It also triggers modal dialogs to configure (add/delete/edit/remove) extendedPrices. */ -export const ConfigureTimeslotButton: React.FC = ({ prices, onError, onSuccess, groupId, priceableId, priceableType }) => { +export const ConfigureExtendedPriceButton: React.FC = ({ prices, onError, onSuccess, groupId, priceableId, priceableType }) => { const { t } = useTranslation('admin'); - const [timeslots, setTimeslots] = useState>(prices); + const [extendedPrices, setExtendedPrices] = useState>(prices); const [showList, setShowList] = useState(false); /** @@ -33,13 +35,13 @@ export const ConfigureTimeslotButton: React.FC = ( }; /** - * Callback triggered when the timeslot was successfully created/deleted/updated. - * We refresh the list of timeslots. + * Callback triggered when the extendedPrice was successfully created/deleted/updated. + * We refresh the list of extendedPrices. */ const handleSuccess = (message: string) => { onSuccess(message); PriceAPI.index({ group_id: groupId, priceable_id: priceableId, priceable_type: priceableType }) - .then(data => setTimeslots(data)) + .then(data => setExtendedPrices(data)) .catch(error => onError(error)); }; @@ -47,7 +49,7 @@ export const ConfigureTimeslotButton: React.FC = ( * Render the button used to trigger the "new pack" modal */ const renderAddButton = (): ReactNode => { - return = ( - {showList && + {showList &&
    - {timeslots?.map(timeslot => -
  • - {timeslot.duration} {t('app.admin.calendar.minutes')} - {FormatLib.price(timeslot.amount)} + {extendedPrices?.map(extendedPrice => +
  • + {extendedPrice.duration} {t('app.admin.calendar.minutes')} - {FormatLib.price(extendedPrice.amount)} + +
  • )}
- {timeslots?.length === 0 && {t('app.admin.configure_timeslots_button.no_timeslots')}} + {extendedPrices?.length === 0 && {t('app.admin.configure_extendedPrices_button.no_extendedPrices')}}
} ); diff --git a/app/frontend/src/javascript/components/pricing/create-timeslot.tsx b/app/frontend/src/javascript/components/pricing/create-extended-price.tsx similarity index 52% rename from app/frontend/src/javascript/components/pricing/create-timeslot.tsx rename to app/frontend/src/javascript/components/pricing/create-extended-price.tsx index b3e15f7a2..d965fd4e5 100644 --- a/app/frontend/src/javascript/components/pricing/create-timeslot.tsx +++ b/app/frontend/src/javascript/components/pricing/create-extended-price.tsx @@ -1,12 +1,12 @@ import React, { useState } from 'react'; import { FabModal } from '../base/fab-modal'; -import { TimeslotForm } from './timeslot-form'; +import { ExtendedPriceForm } from './extended-price-form'; import { Price } from '../../models/price'; -import PrepaidPackAPI from '../../api/prepaid-pack'; +import PriceAPI from '../../api/price'; import { useTranslation } from 'react-i18next'; import { FabAlert } from '../base/fab-alert'; -interface CreateTimeslotProps { +interface CreateExtendedPriceProps { onSuccess: (message: string) => void, onError: (message: string) => void, groupId: number, @@ -16,9 +16,9 @@ interface CreateTimeslotProps { /** * This component shows a button. - * When clicked, we show a modal dialog handing the process of creating a new time slot + * When clicked, we show a modal dialog handing the process of creating a new extended price */ -export const CreateTimeslot: React.FC = ({ onSuccess, onError, groupId, priceableId, priceableType }) => { +export const CreateExtendedPrice: React.FC = ({ onSuccess, onError, groupId, priceableId, priceableType }) => { const { t } = useTranslation('admin'); const [isOpen, setIsOpen] = useState(false); @@ -31,23 +31,22 @@ export const CreateTimeslot: React.FC = ({ onSuccess, onErr }; /** - * Callback triggered when the user has validated the creation of the new time slot + * Callback triggered when the user has validated the creation of the new extended price */ - const handleSubmit = (timeslot: Price): void => { + const handleSubmit = (extendedPrice: 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; + const newExtendedPrice = Object.assign({} as Price, extendedPrice); + newExtendedPrice.group_id = groupId; + newExtendedPrice.priceable_id = priceableId; + newExtendedPrice.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)); + PriceAPI.create(newExtendedPrice) + .then(() => { + onSuccess(t('app.admin.create_extendedPrice.extendedPrice_successfully_created')); + toggleModal(); + }) + .catch(error => onError(error)); }; return ( @@ -55,15 +54,15 @@ export const CreateTimeslot: React.FC = ({ onSuccess, onErr - {t('app.admin.create_timeslot.new_timeslot_info', { TYPE: priceableType })} + {t('app.admin.create_extendedPrice.new_extendedPrice_info', { TYPE: priceableType })} - + ); diff --git a/app/frontend/src/javascript/components/pricing/delete-extended-price.tsx b/app/frontend/src/javascript/components/pricing/delete-extended-price.tsx new file mode 100644 index 000000000..2d1807f99 --- /dev/null +++ b/app/frontend/src/javascript/components/pricing/delete-extended-price.tsx @@ -0,0 +1,56 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FabButton } from '../base/fab-button'; +import { FabModal } from '../base/fab-modal'; +import { Price } from '../../models/price'; +import PriceAPI from '../../api/price'; + +interface DeleteExtendedPriceProps { + onSuccess: (message: string) => void, + onError: (message: string) => void, + price: Price, +} + +/** + * This component shows a button. + * When clicked, we show a modal dialog to ask the user for confirmation about the deletion of the provided extended price. + */ +export const DeleteExtendedPrice: React.FC = ({ onSuccess, onError, price }) => { + const { t } = useTranslation('admin'); + + const [deletionModal, setDeletionModal] = useState(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 extended price + */ + const onDeleteConfirmed = (): void => { + PriceAPI.destroy(price.id).then(() => { + onSuccess(t('app.admin.delete_extendedPrice.extendedPrice_deleted')); + }).catch((error) => { + onError(t('app.admin.delete_extendedPrice.unable_to_delete') + error); + }); + toggleDeletionModal(); + }; + + return ( +
+ } onClick={toggleDeletionModal} /> + + {t('app.admin.delete_extendedPrice.delete_confirmation')} + +
+ ); +}; diff --git a/app/frontend/src/javascript/components/pricing/edit-extended-price.tsx b/app/frontend/src/javascript/components/pricing/edit-extended-price.tsx new file mode 100644 index 000000000..1324da069 --- /dev/null +++ b/app/frontend/src/javascript/components/pricing/edit-extended-price.tsx @@ -0,0 +1,66 @@ +import React, { useState } from 'react'; +import { FabModal } from '../base/fab-modal'; +import { ExtendedPriceForm } from './extended-price-form'; +import { Price } from '../../models/price'; +import PriceAPI from '../../api/price'; +import { useTranslation } from 'react-i18next'; +import { FabButton } from '../base/fab-button'; + +interface EditExtendedPriceProps { + price: Price, + onSuccess: (message: string) => void, + onError: (message: string) => void +} + +/** + * This component shows a button. + * When clicked, we show a modal dialog handing the process of creating a new extended price + */ +export const EditExtendedPrice: React.FC = ({ price, onSuccess, onError }) => { + const { t } = useTranslation('admin'); + + const [isOpen, setIsOpen] = useState(false); + const [extendedPriceData, setExtendedPriceData] = useState(price); + + /** + * Open/closes the "edit extended price" modal dialog + */ + const toggleModal = (): void => { + setIsOpen(!isOpen); + }; + + /** + * When the user clicks on the edition button open te edition modal + */ + const handleRequestEdit = (): void => { + toggleModal(); + }; + + /** + * Callback triggered when the user has validated the changes of the extended price + */ + const handleUpdate = (price: Price): void => { + PriceAPI.update(price) + .then(() => { + onSuccess(t('app.admin.edit_extendedPrice.extendedPrice_successfully_updated')); + setExtendedPriceData(price); + toggleModal(); + }) + .catch(error => onError(error)); + }; + + return ( +
+ } onClick={handleRequestEdit} /> + + {extendedPriceData && } + +
+ ); +}; diff --git a/app/frontend/src/javascript/components/pricing/timeslot-form.tsx b/app/frontend/src/javascript/components/pricing/extended-price-form.tsx similarity index 77% rename from app/frontend/src/javascript/components/pricing/timeslot-form.tsx rename to app/frontend/src/javascript/components/pricing/extended-price-form.tsx index bc31fba97..f8bca2487 100644 --- a/app/frontend/src/javascript/components/pricing/timeslot-form.tsx +++ b/app/frontend/src/javascript/components/pricing/extended-price-form.tsx @@ -14,11 +14,11 @@ interface PackFormProps { } /** - * A form component to create/edit a time slot. + * A form component to create/edit a extended price. * 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); +export const ExtendedPriceForm: React.FC = ({ formId, onSubmit, price }) => { + const [extendedPriceData, updateExtendedPriceData] = useImmer(price || {} as Price); const { t } = useTranslation('admin'); @@ -27,23 +27,23 @@ export const TimeslotForm: React.FC = ({ formId, onSubmit, price */ const handleSubmit = (event: BaseSyntheticEvent): void => { event.preventDefault(); - onSubmit(timeslotData); + onSubmit(extendedPriceData); }; /** - * Callback triggered when the user inputs an amount for the current time slot. + * Callback triggered when the user inputs an amount for the current extended price. */ const handleUpdateAmount = (amount: string) => { - updateTimeslotData(draft => { + updateExtendedPriceData(draft => { draft.amount = parseFloat(amount); }); }; /** - * Callback triggered when the user inputs a number of minutes for the current time slot. + * Callback triggered when the user inputs a number of minutes for the current extended price. */ const handleUpdateHours = (minutes: string) => { - updateTimeslotData(draft => { + updateExtendedPriceData(draft => { draft.duration = parseInt(minutes, 10); }); }; @@ -53,7 +53,7 @@ export const TimeslotForm: React.FC = ({ formId, onSubmit, price = ({ formId, onSubmit, price type="number" step={0.01} min={0} - defaultValue={timeslotData?.amount || ''} + defaultValue={extendedPriceData?.amount || ''} onChange={handleUpdateAmount} icon={} addOn={Fablab.intl_currency} diff --git a/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx b/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx index 5536000dc..b2f6dbc35 100644 --- a/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx @@ -9,7 +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 { ConfigureExtendedPriceButton } from './configure-extended-price-button'; import PriceAPI from '../../api/price'; import { Price } from '../../models/price'; import { useImmer } from 'use-immer'; @@ -108,7 +108,7 @@ const SpacesPricing: React.FC = ({ onError, onSuccess }) => {space.name} {groups?.map(group => {prices && } - 'pricing#index' put 'pricing' => 'pricing#update' - resources :prices, only: %i[create index update] do + resources :prices, only: %i[create index update destroy] do post 'compute', on: :collection end resources :prepaid_packs