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:
parent
6c326c7209
commit
d43f719038
13
app/frontend/src/javascript/api/local-payment.ts
Normal file
13
app/frontend/src/javascript/api/local-payment.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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 />
|
||||
);
|
||||
}
|
@ -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}
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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} />
|
||||
|
@ -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} />
|
||||
|
@ -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>}
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
20
app/frontend/src/javascript/lib/format.ts
Normal file
20
app/frontend/src/javascript/lib/format.ts
Normal 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);
|
||||
}
|
||||
}
|
22
app/frontend/src/javascript/lib/user.ts
Normal file
22
app/frontend/src/javascript/lib/user.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
@ -0,0 +1,9 @@
|
||||
.local-payment-modal {
|
||||
.local-modal-icons {
|
||||
text-align: center;
|
||||
|
||||
.fas.fa-lock {
|
||||
color: #9edd78;
|
||||
}
|
||||
}
|
||||
}
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user