1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

buy packs using local payment

This commit is contained in:
Sylvain 2021-06-30 15:32:10 +02:00
parent 6c326c7209
commit d43f719038
18 changed files with 367 additions and 109 deletions

View File

@ -0,0 +1,13 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { ShoppingCart } from '../models/payment';
import { PaymentSchedule } from '../models/payment-schedule';
import { Invoice } from '../models/invoice';
export default class LocalPaymentAPI {
static async confirmPayment (cart_items: ShoppingCart): Promise<PaymentSchedule|Invoice> {
const res: AxiosResponse<PaymentSchedule|Invoice> = await apiClient.post('/api/local_payment/confirm_payment', cart_items);
return res?.data;
}
}

View File

@ -1,16 +1,14 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import moment from 'moment';
import '../../lib/i18n';
import { Loader } from '../base/loader';
import { FabModal } from '../base/fab-modal';
import { IFablab } from '../../models/fablab';
import { PaymentSchedule } from '../../models/payment-schedule';
import { IApplication } from '../../models/application';
import FormatLib from '../../lib/format';
declare var Application: IApplication;
declare var Fablab: IFablab;
interface PaymentScheduleSummaryProps {
schedule: PaymentSchedule
@ -25,18 +23,6 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
// is open, the modal dialog showing the full details of the payment schedule?
const [modal, setModal] = useState(false);
/**
* Return the formatted localized date for the given date
*/
const formatDate = (date: Date): string => {
return Intl.DateTimeFormat().format(moment(date).toDate());
}
/**
* Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €")
*/
const formatPrice = (price: number): string => {
return new Intl.NumberFormat(Fablab.intl_locale, {style: 'currency', currency: Fablab.intl_currency}).format(price);
}
/**
* Test if all payment deadlines have the same amount
*/
@ -58,7 +44,7 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
{hasEqualDeadlines() && <ul>
<li>
<span className="schedule-item-info">
{t('app.shared.cart.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length, AMOUNT: formatPrice(schedule.items[0].amount) })}
{t('app.shared.cart.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length, AMOUNT: FormatLib.price(schedule.items[0].amount) })}
</span>
<span className="schedule-item-date">{t('app.shared.cart.first_debit')}</span>
</li>
@ -66,12 +52,12 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
{!hasEqualDeadlines() && <ul>
<li>
<span className="schedule-item-info">{t('app.shared.cart.monthly_payment_NUMBER', { NUMBER: 1 })}</span>
<span className="schedule-item-price">{formatPrice(schedule.items[0].amount)}</span>
<span className="schedule-item-price">{FormatLib.price(schedule.items[0].amount)}</span>
<span className="schedule-item-date">{t('app.shared.cart.debit')}</span>
</li>
<li>
<span className="schedule-item-info">
{t('app.shared.cart.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length - 1, AMOUNT: formatPrice(schedule.items[1].amount) })}
{t('app.shared.cart.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length - 1, AMOUNT: FormatLib.price(schedule.items[1].amount) })}
</span>
</li>
</ul>}
@ -80,9 +66,9 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
<ul className="full-schedule">
{schedule.items.map(item => (
<li key={String(item.due_date)}>
<span className="schedule-item-date">{formatDate(item.due_date)}</span>
<span className="schedule-item-date">{FormatLib.date(item.due_date)}</span>
<span> </span>
<span className="schedule-item-price">{formatPrice(item.amount)}</span>
<span className="schedule-item-price">{FormatLib.price(item.amount)}</span>
</li>
))}
</ul>

View File

@ -1,7 +1,6 @@
import React, { ReactEventHandler, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Loader } from '../base/loader';
import moment from 'moment';
import _ from 'lodash';
import { FabButton } from '../base/fab-button';
import { FabModal } from '../base/fab-modal';
@ -9,11 +8,9 @@ import { UpdateCardModal } from '../payment/update-card-modal';
import { StripeElements } from '../payment/stripe/stripe-elements';
import { StripeConfirm } from '../payment/stripe/stripe-confirm';
import { User, UserRole } from '../../models/user';
import { IFablab } from '../../models/fablab';
import { PaymentSchedule, PaymentScheduleItem, PaymentScheduleItemState } from '../../models/payment-schedule';
import PaymentScheduleAPI from '../../api/payment-schedule';
declare var Fablab: IFablab;
import FormatLib from '../../lib/format';
interface PaymentSchedulesTableProps {
paymentSchedules: Array<PaymentSchedule>,
@ -57,19 +54,6 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
return showExpanded.get(paymentScheduleId);
}
/**
* Return the formatted localized date for the given date
*/
const formatDate = (date: Date): string => {
return Intl.DateTimeFormat().format(moment(date).toDate());
}
/**
* Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €")
*/
const formatPrice = (price: number): string => {
return new Intl.NumberFormat(Fablab.intl_locale, {style: 'currency', currency: Fablab.intl_currency}).format(price);
}
/**
* Return the value for the CSS property 'display', for the payment schedule deadlines
*/
@ -369,8 +353,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
<tr>
<td className="w-35 row-header" onClick={togglePaymentScheduleDetails(p.id)}>{expandCollapseIcon(p.id)}</td>
<td className="w-200">{p.reference}</td>
<td className="w-200">{formatDate(p.created_at)}</td>
<td className="w-120">{formatPrice(p.total)}</td>
<td className="w-200">{FormatLib.date(p.created_at)}</td>
<td className="w-120">{FormatLib.price(p.total)}</td>
{showCustomer && <td className="w-200">{p.user.name}</td>}
<td className="w-200">{downloadButton(TargetType.PaymentSchedule, p.id)}</td>
</tr>
@ -389,8 +373,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
</thead>
<tbody>
{_.orderBy(p.items, 'due_date').map(item => <tr key={item.id}>
<td>{formatDate(item.due_date)}</td>
<td>{formatPrice(item.amount)}</td>
<td>{FormatLib.date(item.due_date)}</td>
<td>{FormatLib.price(item.amount)}</td>
<td>{formatState(item)}</td>
<td>{itemButtons(item, p)}</td>
</tr>)}
@ -414,8 +398,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
confirmButton={t('app.shared.schedules_table.confirm_button')}>
{tempDeadline && <span>
{t('app.shared.schedules_table.confirm_check_cashing_body', {
AMOUNT: formatPrice(tempDeadline.amount),
DATE: formatDate(tempDeadline.due_date)
AMOUNT: FormatLib.price(tempDeadline.amount),
DATE: FormatLib.date(tempDeadline.due_date)
})}
</span>}
</FabModal>

View File

@ -5,7 +5,6 @@ import { WalletInfo } from '../wallet-info';
import { FabModal, ModalSize } from '../base/fab-modal';
import { HtmlTranslate } from '../base/html-translate';
import { CustomAsset, CustomAssetName } from '../../models/custom-asset';
import { IFablab } from '../../models/fablab';
import { ShoppingCart } from '../../models/payment';
import { PaymentSchedule } from '../../models/payment-schedule';
import { User } from '../../models/user';
@ -17,8 +16,7 @@ import SettingAPI from '../../api/setting';
import { SettingName } from '../../models/setting';
import { ComputePriceResult } from '../../models/price';
import { Wallet } from '../../models/wallet';
declare var Fablab: IFablab;
import FormatLib from '../../lib/format';
export interface GatewayFormProps {
@ -28,7 +26,7 @@ export interface GatewayFormProps {
customer: User,
operator: User,
className?: string,
paymentSchedule?: boolean,
paymentSchedule?: PaymentSchedule,
cart?: ShoppingCart,
formId: string,
}
@ -46,6 +44,9 @@ interface AbstractPaymentModalProps {
formId: string,
className?: string,
formClassName?: string,
title?: string,
preventCgv?: boolean,
modalSize?: ModalSize,
}
/**
@ -54,7 +55,7 @@ interface AbstractPaymentModalProps {
* This component must not be called directly but must be extended for each implemented payment gateway
* @see https://reactjs.org/docs/composition-vs-inheritance.html
*/
export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, cart, currentUser, schedule, customer, logoFooter, GatewayForm, formId, className, formClassName }) => {
export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, cart, currentUser, schedule, customer, logoFooter, GatewayForm, formId, className, formClassName, title, preventCgv, modalSize }) => {
// customer's wallet
const [wallet, setWallet] = useState<Wallet>(null);
// server-computed price with all details
@ -78,7 +79,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
/**
* When the component is loaded first, get the name of the currently active payment modal
* When the component loads first, get the name of the currently active payment modal
*/
useEffect(() => {
CustomAssetAPI.get(CustomAssetName.CgvFile).then(asset => setCgv(asset));
@ -100,8 +101,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
setWallet(wallet);
PriceAPI.compute(cart).then((res) => {
setPrice(res);
const wLib = new WalletLib(wallet);
setRemainingPrice(wLib.computeRemainingPrice(res.price));
setRemainingPrice(new WalletLib(wallet).computeRemainingPrice(res.price));
setReady(true);
})
})
@ -118,7 +118,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
* Check if the user accepts the Terms of Sales document
*/
const hasCgv = (): boolean => {
return cgv != null;
return cgv != null && !preventCgv;
}
/**
@ -135,13 +135,6 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
return schedule !== undefined;
}
/**
* Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €")
*/
const formatPrice = (amount: number): string => {
return new Intl.NumberFormat(Fablab.intl_locale, {style: 'currency', currency: Fablab.intl_currency}).format(amount);
}
/**
* Set the component as 'currently submitting'
*/
@ -175,12 +168,23 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
return !submitState && terms;
}
/**
* Build the modal title. If the provided title is a shared translation key, interpolate it through the
* translation service. Otherwise, just display the provided string.
*/
const getTitle = (): string => {
if (title.match(/^app\.shared\./)) {
return t(title);
}
return title;
}
return (
<FabModal title={t('app.shared.payment.online_payment') }
<FabModal title={getTitle()}
isOpen={isOpen}
toggleModal={toggleModal}
width={ModalSize.medium}
width={modalSize}
closeButton={false}
customFooter={logoFooter}
className={`payment-modal ${className ? className : ''}`}>
@ -194,7 +198,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
formId={formId}
cart={cart}
customer={customer}
paymentSchedule={isPaymentSchedule()}>
paymentSchedule={schedule}>
{hasErrors() && <div className="payment-errors">
{errors}
</div>}
@ -214,7 +218,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
disabled={!canSubmit()}
form={formId}
className="validate-btn">
{t('app.shared.payment.confirm_payment_of_', { AMOUNT: formatPrice(remainingPrice) })}
{t('app.shared.payment.confirm_payment_of_', { AMOUNT: FormatLib.price(remainingPrice) })}
</button>}
{submitState && <div className="payment-pending">
<div className="fa-2x">
@ -225,3 +229,9 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
</FabModal>
);
}
AbstractPaymentModal.defaultProps = {
title: 'app.shared.payment.online_payment',
preventCgv: false,
modalSize: ModalSize.medium
};

View File

@ -0,0 +1,138 @@
import React, { FormEvent, useState } from 'react';
import Select from 'react-select';
import { useTranslation } from 'react-i18next';
import { GatewayFormProps } from '../abstract-payment-modal';
import LocalPaymentAPI from '../../../api/local-payment';
import FormatLib from '../../../lib/format';
import SettingAPI from '../../../api/setting';
import { SettingName } from '../../../models/setting';
import { PaymentModal } from '../payment-modal';
import { PaymentSchedule } from '../../../models/payment-schedule';
const ALL_SCHEDULE_METHODS = ['card', 'check'] as const;
type scheduleMethod = typeof ALL_SCHEDULE_METHODS[number];
/**
* Option format, expected by react-select
* @see https://github.com/JedWatson/react-select
*/
type selectOption = { value: scheduleMethod, label: string };
/**
* A form component to ask for confirmation before cashing a payment directly at the FabLab's reception.
* This is intended for use by privileged users.
* The form validation button must be created elsewhere, using the attribute form={formId}.
*/
export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule, cart, customer, operator, formId }) => {
const { t } = useTranslation('admin');
const [method, setMethod] = useState<scheduleMethod>('check');
const [onlinePaymentModal, setOnlinePaymentModal] = useState<boolean>(false);
/**
* Open/closes the online payment modal, used to collect card credentials when paying the payment schedule by card.
*/
const toggleOnlinePaymentModal = (): void => {
setOnlinePaymentModal(!onlinePaymentModal);
}
/**
* Convert all payement methods for schedules to the react-select format
*/
const buildMethodOptions = (): Array<selectOption> => {
return ALL_SCHEDULE_METHODS.map(i => methodToOption(i));
}
/**
* Convert the given payment-method to the react-select format
*/
const methodToOption = (value: scheduleMethod): selectOption => {
if (!value) return { value, label: '' };
return { value, label: t(`app.admin.local_payment.method_${value}`) };
}
/**
* Callback triggered when the user selects a payment method for the current payment schedule.
*/
const handleUpdateMethod = (option: selectOption) => {
setMethod(option.value);
}
/**
* Handle the submission of the form. It will process the local payment.
*/
const handleSubmit = async (event: FormEvent): Promise<void> => {
event.preventDefault();
onSubmit();
if (paymentSchedule && method === 'card') {
// check that the online payment is active
try {
const online = await SettingAPI.get(SettingName.OnlinePaymentModule);
if (online.value !== 'true') {
return onError(t('app.admin.local_payment.online_payment_disabled'))
}
return toggleOnlinePaymentModal();
} catch (e) {
onError(e);
}
}
try {
const document = await LocalPaymentAPI.confirmPayment(cart);
onSuccess(document);
} catch (e) {
onError(e);
}
}
/**
* Callback triggered after a successful payment by online card for a schedule.
*/
const afterCreatePaymentSchedule = (document: PaymentSchedule) => {
toggleOnlinePaymentModal();
onSuccess(document);
}
return (
<form onSubmit={handleSubmit} id={formId} className={className ? className : ''}>
{!paymentSchedule && <p className="payment">{t('app.admin.local_payment.about_to_cash')}</p>}
{paymentSchedule && <div className="payment-schedule">
<div className="schedule-method">
<Select placeholder={ t('app.admin.local_payment.payment_method') }
className="method-select"
onChange={handleUpdateMethod}
options={buildMethodOptions}
defaultValue={methodToOption(method)} />
{method === 'card' && <p>{t('app.admin.local_payment.card_collection_info')}</p>}
{method === 'check' && <p>{t('app.admin.local_payment.check_collection_info', { DEADLINES: paymentSchedule.items.length })}</p>}
</div>
<div className="full-schedule">
<ul>
{paymentSchedule.items.map(item => {
return (
<li key={item.id}>
<span className="schedule-item-date">{FormatLib.date(item.due_date)}</span>
<span> </span>
<span className="schedule-item-price">{FormatLib.price(item.amount)}</span>
</li>
)
})}
</ul>
</div>
<PaymentModal isOpen={onlinePaymentModal}
toggleModal={toggleOnlinePaymentModal}
afterSuccess={afterCreatePaymentSchedule}
onError={onError}
cart={cart}
currentUser={operator}
customer={customer} />
</div>}
{children}
</form>
);
}

View File

@ -0,0 +1,76 @@
import React, { FunctionComponent, ReactNode } from 'react';
import { AbstractPaymentModal, GatewayFormProps } from '../abstract-payment-modal';
import { LocalPaymentForm } from './local-payment-form';
import { ShoppingCart } from '../../../models/payment';
import { PaymentSchedule } from '../../../models/payment-schedule';
import { User } from '../../../models/user';
import { Invoice } from '../../../models/invoice';
import { useTranslation } from 'react-i18next';
import { ModalSize } from '../../base/fab-modal';
interface LocalPaymentModalProps {
isOpen: boolean,
toggleModal: () => void,
afterSuccess: (result: Invoice|PaymentSchedule) => void,
cart: ShoppingCart,
currentUser: User,
schedule?: PaymentSchedule,
customer: User
}
/**
* This component enables a privileged user to confirm a local payments.
*/
export const LocalPaymentModal: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, cart, currentUser, schedule, customer }) => {
const { t } = useTranslation('admin');
/**
* Return the logos, shown in the modal footer.
*/
const logoFooter = (): ReactNode => {
return (
<div className="local-modal-icons">
<i className="fas fa-lock fa-2x" />
</div>
);
}
/**
* Integrates the LocalPaymentForm into the parent AbstractPaymentModal
*/
const renderForm: FunctionComponent<GatewayFormProps> = ({ onSubmit, onSuccess, onError, operator, className, formId, cart, customer, paymentSchedule, children}) => {
return (
<LocalPaymentForm onSubmit={onSubmit}
onSuccess={onSuccess}
onError={onError}
operator={operator}
className={className}
formId={formId}
cart={cart}
customer={customer}
paymentSchedule={paymentSchedule}>
{children}
</LocalPaymentForm>
);
}
return (
<AbstractPaymentModal className="local-payment-modal"
isOpen={isOpen}
toggleModal={toggleModal}
logoFooter={logoFooter()}
title={t('app.admin.local_payment.offline_payment')}
formId="local-payment-form"
formClassName="local-payment-form"
currentUser={currentUser}
cart={cart}
customer={customer}
afterSuccess={afterSuccess}
schedule={schedule}
GatewayForm={renderForm}
modalSize={schedule ? ModalSize.large : ModalSize.medium}
preventCgv />
);
}

View File

@ -67,11 +67,10 @@ export const PayzenCardUpdateModal: React.FC<PayzenCardUpdateModalProps> = ({ is
onSuccess={onSuccess}
onError={handleCardUpdateError}
className="card-form"
paymentSchedule={true}
paymentSchedule={schedule}
operator={operator}
customer={schedule.user as User}
updateCard={true}
paymentScheduleId={schedule.id}
formId={formId} >
{errors && <div className="payzen-errors">
{errors}

View File

@ -17,14 +17,13 @@ import { Invoice } from '../../../models/invoice';
// we use these two additional parameters to update the card, if provided
interface PayzenFormProps extends GatewayFormProps {
updateCard?: boolean,
paymentScheduleId?: number,
}
/**
* A form component to collect the credit card details and to create the payment method on Stripe.
* The form validation button must be created elsewhere, using the attribute form={formId}.
*/
export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, updateCard = false, cart, customer, formId, paymentScheduleId }) => {
export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule, updateCard = false, cart, customer, formId }) => {
const PayZenKR = useRef<KryptonClient>(null);
const [loadingClass, setLoadingClass] = useState<'hidden' | 'loader' | 'loader-overlay'>('loader');
@ -54,7 +53,7 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
*/
const createToken = async (): Promise<CreateTokenResponse> => {
if (updateCard) {
return await PayzenAPI.updateToken(paymentScheduleId);
return await PayzenAPI.updateToken(paymentSchedule?.id);
} else if (paymentSchedule) {
return await PayzenAPI.chargeCreateToken(cart, customer);
} else {

View File

@ -5,14 +5,15 @@ 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';
import { PaymentMethod, ShoppingCart } from '../../models/payment';
import { PaymentModal } from '../payment/payment-modal';
import UserLib from '../../lib/user';
import { LocalPaymentModal } from '../payment/local-payment/local-payment-modal';
import FormatLib from '../../lib/format';
declare var Fablab: IFablab;
type PackableItem = Machine;
@ -38,6 +39,7 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
const [packs, setPacks] = useState<Array<PrepaidPack>>(null);
const [cart, setCart] = useState<ShoppingCart>(null);
const [paymentModal, setPaymentModal] = useState<boolean>(false);
const [localPaymentModal, setLocalPaymentModal] = useState<boolean>(false);
useEffect(() => {
PrepaidPackAPI.index({ priceable_id: item.id, priceable_type: itemType, group_id: customer.group_id, disabled: false })
@ -57,10 +59,10 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
}
/**
* Return the formatted localized amount for the given price (e.g. 20.5 => "20,50 €")
* Open/closes the local payment modal (for admins and managers)
*/
const formatPrice = (price: number): string => {
return new Intl.NumberFormat(Fablab.intl_locale, { style: 'currency', currency: Fablab.intl_currency }).format(price);
const toggleLocalPaymentModal = (): void => {
setLocalPaymentModal(!localPaymentModal);
}
/**
@ -105,6 +107,9 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
{ prepaid_pack: { id: pack.id }}
]
});
if (new UserLib(operator).isPrivileged(customer)) {
return toggleLocalPaymentModal();
}
togglePaymentModal();
}
}
@ -126,8 +131,8 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
return (
<div key={pack.id} className="pack">
<span className="duration">{formatDuration(pack.minutes)}</span>
<span className="amount">{formatPrice(pack.amount)}</span>
{pack.amount < normalPrice && <span className="crossed-out-price">{formatPrice(normalPrice)}</span>}
<span className="amount">{FormatLib.price(pack.amount)}</span>
{pack.amount < normalPrice && <span className="crossed-out-price">{FormatLib.price(normalPrice)}</span>}
<span className="validity">{formatValidity(pack)}</span>
<FabButton className="buy-button" onClick={handleBuyPack(pack)} icon={<i className="fas fa-shopping-cart" />}>
{t('app.logged.propose_packs_modal.buy_this_pack')}
@ -148,13 +153,21 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
<div className="list-of-packs">
{packs?.map(p => renderPack(p))}
</div>
{cart && <PaymentModal isOpen={paymentModal}
{cart && <div>
<PaymentModal isOpen={paymentModal}
toggleModal={togglePaymentModal}
afterSuccess={handlePackBought}
onError={onError}
cart={cart}
currentUser={operator}
customer={customer} />}
customer={customer} />
<LocalPaymentModal isOpen={localPaymentModal}
toggleModal={toggleLocalPaymentModal}
afterSuccess={handlePackBought}
cart={cart}
currentUser={operator}
customer={customer} />
</div>}
</FabModal>
);
}

View File

@ -4,11 +4,9 @@ import { useTranslation } from 'react-i18next';
import { FabPopover } from '../base/fab-popover';
import { CreatePack } from './create-pack';
import PrepaidPackAPI from '../../api/prepaid-pack';
import { IFablab } from '../../models/fablab';
import { DeletePack } from './delete-pack';
import { EditPack } from './edit-pack';
declare var Fablab: IFablab;
import FormatLib from '../../lib/format';
interface ConfigurePacksButtonProps {
packsData: Array<PrepaidPack>,
@ -30,13 +28,6 @@ export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ pack
const [showList, setShowList] = useState<boolean>(false);
const [editPackModal, setEditPackModal] = useState<boolean>(false);
/**
* 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);
}
/**
* Return the number of hours, user-friendly formatted
*/
@ -89,7 +80,7 @@ export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ pack
<ul>
{packs?.map(p =>
<li key={p.id} className={p.disabled ? 'disabled' : ''}>
{formatDuration(p.minutes)} - {formatPrice(p.amount)}
{formatDuration(p.minutes)} - {FormatLib.price(p.amount)}
<span className="pack-actions">
<EditPack onSuccess={handleSuccess} onError={onError} pack={p} />
<DeletePack onSuccess={handleSuccess} onError={onError} pack={p} />

View File

@ -3,6 +3,7 @@ import { IFablab } from '../../models/fablab';
import { FabInput } from '../base/fab-input';
import { FabButton } from '../base/fab-button';
import { Price } from '../../models/price';
import FormatLib from '../../lib/format';
declare var Fablab: IFablab;
@ -19,13 +20,6 @@ export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) =
const [edit, setEdit] = useState<boolean>(false);
const [tempPrice, setTempPrice] = useState<string>(`${price.amount}`);
/**
* Return the formatted localized amount for the price (eg. 20.5 => "20,50 €")
*/
const formatPrice = (): string => {
return new Intl.NumberFormat(Fablab.intl_locale, { style: 'currency', currency: Fablab.intl_currency }).format(price.amount);
}
/**
* Saves the new price
*/
@ -45,7 +39,7 @@ export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) =
return (
<span className="editable-price">
{!edit && <span className="display-price" onClick={toggleEdit}>{formatPrice()}</span>}
{!edit && <span className="display-price" onClick={toggleEdit}>{FormatLib.price(price.amount)}</span>}
{edit && <span>
<FabInput id="price" type="number" step={0.01} defaultValue={price.amount} addOn={Fablab.intl_currency} onChange={setTempPrice} required/>
<FabButton icon={<i className="fas fa-check" />} className="approve-button" onClick={handleValidateEdit} />

View File

@ -6,12 +6,11 @@ import '../lib/i18n';
import { Loader } from './base/loader';
import { User } from '../models/user';
import { Wallet } from '../models/wallet';
import { IFablab } from '../models/fablab';
import WalletLib from '../lib/wallet';
import { ShoppingCart } from '../models/payment';
import FormatLib from '../lib/format';
declare var Application: IApplication;
declare var Fablab: IFablab;
interface WalletInfoProps {
cart: ShoppingCart,
@ -35,12 +34,6 @@ export const WalletInfo: React.FC<WalletInfoProps> = ({ cart, currentUser, walle
setRemainingPrice(wLib.computeRemainingPrice(price));
});
/**
* 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);
}
/**
* Check if the currently connected used is also the person making the reservation.
* If the currently connected user (i.e. the operator), is an admin or a manager, he may book the reservation for someone else.
@ -87,25 +80,25 @@ export const WalletInfo: React.FC<WalletInfoProps> = ({ cart, currentUser, walle
<div className="wallet-info">
{shouldBeShown() && <div>
{isOperatorAndClient() && <div>
<h3>{t('app.shared.wallet.wallet_info.you_have_AMOUNT_in_wallet', {AMOUNT: formatPrice(wallet.amount)})}</h3>
<h3>{t('app.shared.wallet.wallet_info.you_have_AMOUNT_in_wallet', {AMOUNT: FormatLib.price(wallet.amount)})}</h3>
{!hasRemainingPrice() && <p>
{t('app.shared.wallet.wallet_info.wallet_pay_ITEM', {ITEM: getPriceItem()})}
</p>}
{hasRemainingPrice() && <p>
{t('app.shared.wallet.wallet_info.credit_AMOUNT_for_pay_ITEM', {
AMOUNT: formatPrice(remainingPrice),
AMOUNT: FormatLib.price(remainingPrice),
ITEM: getPriceItem()
})}
</p>}
</div>}
{!isOperatorAndClient() && <div>
<h3>{t('app.shared.wallet.wallet_info.client_have_AMOUNT_in_wallet', {AMOUNT: formatPrice(wallet.amount)})}</h3>
<h3>{t('app.shared.wallet.wallet_info.client_have_AMOUNT_in_wallet', {AMOUNT: FormatLib.price(wallet.amount)})}</h3>
{!hasRemainingPrice() && <p>
{t('app.shared.wallet.wallet_info.client_wallet_pay_ITEM', {ITEM: getPriceItem()})}
</p>}
{hasRemainingPrice() && <p>
{t('app.shared.wallet.wallet_info.client_credit_AMOUNT_for_pay_ITEM', {
AMOUNT: formatPrice(remainingPrice),
AMOUNT: FormatLib.price(remainingPrice),
ITEM: getPriceItem()
})}
</p>}

View File

@ -638,9 +638,10 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat
$scope.filterDisabledPlans = function (plan) { return !plan.disabled; };
/**
* Callback triggered by react components
* Callback triggered after a successful prepaid-pack purchase
*/
$scope.onSuccess = function (message) {
growl.success(message);
};

View File

@ -0,0 +1,20 @@
import moment from 'moment';
import { IFablab } from '../models/fablab';
declare var Fablab: IFablab;
export default class FormatLib {
/**
* Return the formatted localized date for the given date
*/
static date = (date: Date): string => {
return Intl.DateTimeFormat().format(moment(date).toDate());
}
/**
* Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €")
*/
static price = (price: number): string => {
return new Intl.NumberFormat(Fablab.intl_locale, {style: 'currency', currency: Fablab.intl_currency}).format(price);
}
}

View File

@ -0,0 +1,22 @@
import { User, UserRole } from '../models/user';
export default class UserLib {
private user: User;
constructor (user: User) {
this.user = user;
}
/**
* Check if the current user has privileged access for resources concerning the provided customer
*/
isPrivileged = (customer: User): boolean => {
if (this.user.role === UserRole.Admin) return true;
if (this.user.role === UserRole.Manager) {
return (this.user.id !== customer.id);
}
return false;
}
}

View File

@ -47,6 +47,7 @@
@import "modules/payment/payzen/payzen-settings";
@import "modules/payment/payzen/payzen-modal";
@import "modules/payment/payzen/payzen-update-card-modal";
@import "modules/payment/local-payment/local-payment-modal";
@import "modules/plan-categories/plan-categories-list";
@import "modules/plan-categories/create-plan-category";
@import "modules/plan-categories/edit-plan-category";

View File

@ -0,0 +1,9 @@
.local-payment-modal {
.local-modal-icons {
text-align: center;
.fas.fa-lock {
color: #9edd78;
}
}
}

View File

@ -1359,6 +1359,15 @@ en:
delete_confirmation: "Are you sure you want to delete this category? If you do, the plans associated with this category won't be sorted anymore."
category_deleted: "The category was successfully deleted"
unable_to_delete: "Unable to delete the category: "
local_payment:
offline_payment: "Offline payment"
about_to_cash: "You're about to confirm the cashing by an external payment mean. Please do not click on the button below until you have fully cashed the requested payment."
payment_method: "Payment method"
method_card: "Online by card"
method_check: "By check"
card_collection_info: "By validating, you'll be prompted for the member's card number. This card will be automatically charged at the deadlines."
check_collection_info: "By validating, you confirm that you have {DEADLINES} checks, allowing you to collect all the monthly payments."
online_payment_disabled: "Online payment is not available. You cannot collect this payment schedule by online card."
#feature tour
tour:
conclusion: