diff --git a/CHANGELOG.md b/CHANGELOG.md index 877999927..2df03b3e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog Fab-manager +- [TODO DEPLOY] `rails db:seed` + ## v5.0.7 2021 June 24 - Fix a bug: unable to export members list if no subscriptions was taken diff --git a/app/controllers/api/prices_controller.rb b/app/controllers/api/prices_controller.rb index b51e6f7b0..61d7f80ba 100644 --- a/app/controllers/api/prices_controller.rb +++ b/app/controllers/api/prices_controller.rb @@ -6,8 +6,6 @@ class API::PricesController < API::ApiController before_action :authenticate_user! def index - authorize Price - @prices = PriceService.list(params) end diff --git a/app/frontend/src/javascript/components/machines/propose-packs-modal.tsx b/app/frontend/src/javascript/components/machines/propose-packs-modal.tsx new file mode 100644 index 000000000..168c78997 --- /dev/null +++ b/app/frontend/src/javascript/components/machines/propose-packs-modal.tsx @@ -0,0 +1,113 @@ +import React, { BaseSyntheticEvent, useEffect, useState } from 'react'; +import { Machine } from '../../models/machine'; +import { FabModal, ModalSize } from '../base/fab-modal'; +import PrepaidPackAPI from '../../api/prepaid-pack'; +import { User } from '../../models/user'; +import { PrepaidPack } from '../../models/prepaid-pack'; +import { useTranslation } from 'react-i18next'; +import { IFablab } from '../../models/fablab'; +import { FabButton } from '../base/fab-button'; +import PriceAPI from '../../api/price'; +import { Price } from '../../models/price'; + +declare var Fablab: IFablab; + +interface ProposePacksModalProps { + isOpen: boolean, + toggleModal: () => void, + machine: Machine, + customer: User, + onError: (message: string) => void, + onDecline: (machine: Machine) => void, +} + +/** + * Modal dialog shown to offer prepaid-packs for purchase, to the current user. + */ +export const ProposePacksModal: React.FC = ({ isOpen, toggleModal, machine, customer, onError, onDecline }) => { + const { t } = useTranslation('logged'); + + const [price, setPrice] = useState(null); + const [packs, setPacks] = useState>(null); + + useEffect(() => { + PrepaidPackAPI.index({ priceable_id: machine.id, priceable_type: 'Machine', group_id: customer.group_id, disabled: false }) + .then(data => setPacks(data)) + .catch(error => onError(error)); + PriceAPI.index({ priceable_id: machine.id, priceable_type: 'Machine', group_id: customer.group_id, plan_id: null }) + .then(data => setPrice(data[0])) + .catch(error => onError(error)); + }, [machine]); + + + /** + * Return the formatted localized amount for the given price (e.g. 20.5 => "20,50 €") + */ + const formatPrice = (price: number): string => { + return new Intl.NumberFormat(Fablab.intl_locale, { style: 'currency', currency: Fablab.intl_currency }).format(price); + } + + /** + * Convert the hourly-based price of the given prive, to a total price, based on the duration of the given pack + */ + const hourlyPriceToTotal = (price: Price, pack: PrepaidPack): number => { + const hours = pack.minutes / 60; + return price.amount * hours; + } + + /** + * Return the number of hours, user-friendly formatted + */ + const formatDuration = (minutes: number): string => { + return t('app.logged.propose_packs_modal.pack_DURATION', { DURATION: minutes / 60 }); + } + /** + * The user has declined to buy a pack + */ + const handlePacksRefused = (): void => { + onDecline(machine); + } + + /** + * The user has accepted to buy the provided pack, process with teh payment + */ + const handleBuyPack = (pack: PrepaidPack) => { + return (event: BaseSyntheticEvent): void => { + console.log(pack); + } + } + + /** + * Render the given prepaid-pack + */ + const renderPack = (pack: PrepaidPack) => { + if (!price) return; + + const normalPrice = hourlyPriceToTotal(price, pack) + return ( +
+ {formatDuration(pack.minutes)} + {formatPrice(pack.amount)} + {pack.amount < normalPrice && {formatPrice(normalPrice)}} + }> + {t('app.logged.propose_packs_modal.buy_this_pack')} + +
+ ) + } + + return ( + +

{t('app.logged.propose_packs_modal.packs_proposed')}

+
+ {packs?.map(p => renderPack(p))} +
+
+ ); +} diff --git a/app/frontend/src/javascript/components/machines/reserve-button.tsx b/app/frontend/src/javascript/components/machines/reserve-button.tsx index 36434779a..ab525ca07 100644 --- a/app/frontend/src/javascript/components/machines/reserve-button.tsx +++ b/app/frontend/src/javascript/components/machines/reserve-button.tsx @@ -8,6 +8,7 @@ import { react2angular } from 'react2angular'; import { IApplication } from '../../models/application'; import { useTranslation } from 'react-i18next'; import { Loader } from '../base/loader'; +import { ProposePacksModal } from './propose-packs-modal'; declare var Application: IApplication; @@ -33,6 +34,7 @@ const ReserveButtonComponent: React.FC = ({ currentUser, mac const [user, setUser] = useState(currentUser); const [pendingTraining, setPendingTraining] = useState(false); const [trainingRequired, setTrainingRequired] = useState(false); + const [proposePacks, setProposePacks] = useState(false); // handle login from an external process useEffect(() => setUser(currentUser), [currentUser]); @@ -78,6 +80,13 @@ const ReserveButtonComponent: React.FC = ({ currentUser, mac setTrainingRequired(!trainingRequired); }; + /** + * Open/closes the modal dialog inviting the user to buy a prepaid-pack + */ + const toggleProposePacksModal = (): void => { + setProposePacks(!proposePacks); + } + /** * Check that the current user has passed the required training before allowing him to book */ @@ -94,11 +103,11 @@ const ReserveButtonComponent: React.FC = ({ currentUser, mac } // if the currently logged user has completed the training for this machine, or this machine does not require - // a prior training, just let him reserve. - // Moreover, if all associated trainings are disabled, let the user reserve too. + // a prior training, move forward to the prepaid-packs verification. + // Moreover, if there's no enabled associated trainings, also move to the next step. if (machine.current_user_is_trained || machine.trainings.length === 0 || machine.trainings.map(t => t.disabled).reduce((acc, val) => acc && val, true)) { - return onReserveMachine(machine); + return checkPrepaidPack(); } // if the currently logged user booked a training for this machine, tell him that he must wait @@ -112,6 +121,20 @@ const ReserveButtonComponent: React.FC = ({ currentUser, mac setTrainingRequired(true); }; + /** + * Once the training condition has been verified, we check if there are prepaid-packs to propose to the customer. + */ + const checkPrepaidPack = (): void => { + // if the customer has already bought a pack or if there's no active packs for this machine, + // let the customer reserve + if (machine.current_user_has_packs || !machine.has_prepaid_packs_for_current_user) { + return onReserveMachine(machine); + } + + // otherwise, we show a dialog modal to propose the customer to buy an available pack + setProposePacks(true); + } + return (