1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-08 23:46:14 +01:00
fab-manager/app/frontend/src/javascript/components/payment-schedules-table.tsx

487 lines
18 KiB
TypeScript
Raw Normal View History

2021-02-04 17:00:02 +01:00
import React, { ReactEventHandler, ReactNode, useState } from 'react';
2021-01-27 13:59:41 +01:00
import { useTranslation } from 'react-i18next';
2021-04-08 09:35:09 +02:00
import { Loader } from './base/loader';
2021-01-27 13:59:41 +01:00
import moment from 'moment';
import { IFablab } from '../models/fablab';
import _ from 'lodash';
import { PaymentSchedule, PaymentScheduleItem, PaymentScheduleItemState } from '../models/payment-schedule';
2021-04-08 09:35:09 +02:00
import { FabButton } from './base/fab-button';
import { FabModal } from './base/fab-modal';
2021-02-04 17:00:02 +01:00
import PaymentScheduleAPI from '../api/payment-schedule';
2021-02-08 15:28:47 +01:00
import { StripeElements } from './stripe-elements';
import { StripeConfirm } from './stripe-confirm';
2021-02-09 12:09:26 +01:00
import stripeLogo from '../../../images/powered_by_stripe.png';
import mastercardLogo from '../../../images/mastercard.png';
import visaLogo from '../../../images/visa.png';
import { StripeCardUpdate } from './stripe-card-update';
2021-02-09 16:26:09 +01:00
import { User, UserRole } from '../models/user';
2021-01-27 13:59:41 +01:00
declare var Fablab: IFablab;
interface PaymentSchedulesTableProps {
paymentSchedules: Array<PaymentSchedule>,
2021-02-04 17:51:16 +01:00
showCustomer?: boolean,
refreshList: (onError: (msg: any) => void) => void,
2021-02-09 12:09:26 +01:00
operator: User,
2021-01-27 13:59:41 +01:00
}
/**
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
*/
2021-02-09 12:09:26 +01:00
const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator }) => {
const { t } = useTranslation('shared');
2021-01-27 13:59:41 +01:00
// for each payment schedule: are the details (all deadlines) shown or hidden?
2021-02-04 17:00:02 +01:00
const [showExpanded, setShowExpanded] = useState<Map<number, boolean>>(new Map());
// is open, the modal dialog to confirm the cashing of a check?
2021-02-04 17:00:02 +01:00
const [showConfirmCashing, setShowConfirmCashing] = useState<boolean>(false);
// is open, the modal dialog the resolve a pending card payment?
2021-02-08 15:28:47 +01:00
const [showResolveAction, setShowResolveAction] = useState<boolean>(false);
// the user cannot confirm the action modal (3D secure), unless he has resolved the pending action
2021-02-08 15:28:47 +01:00
const [isConfirmActionDisabled, setConfirmActionDisabled] = useState<boolean>(true);
// is open, the modal dialog to update the card details
2021-02-09 12:09:26 +01:00
const [showUpdateCard, setShowUpdateCard] = useState<boolean>(false);
// when an action is triggered on a deadline, the deadline is saved here until the action is done or cancelled.
2021-02-04 17:00:02 +01:00
const [tempDeadline, setTempDeadline] = useState<PaymentScheduleItem>(null);
// when an action is triggered on a deadline, the parent schedule is saved here until the action is done or cancelled.
2021-02-09 12:09:26 +01:00
const [tempSchedule, setTempSchedule] = useState<PaymentSchedule>(null);
// prevent submitting the form to update the card details, until all required fields are filled correctly
2021-02-09 12:09:26 +01:00
const [canSubmitUpdateCard, setCanSubmitUpdateCard] = useState<boolean>(true);
// errors are saved here, if any, for display purposes.
2021-02-09 12:09:26 +01:00
const [errors, setErrors] = useState<string>(null);
// is open, the modal dialog to cancel the associated subscription?
const [showCancelSubscription, setShowCancelSubscription] = useState<boolean>(false);
2021-01-27 13:59:41 +01:00
/**
* Check if the requested payment schedule is displayed with its deadlines (PaymentScheduleItem) or without them
*/
2021-01-27 13:59:41 +01:00
const isExpanded = (paymentScheduleId: number): boolean => {
2021-02-04 17:00:02 +01:00
return showExpanded.get(paymentScheduleId);
2021-01-27 13:59:41 +01:00
}
/**
* 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
*/
2021-01-27 13:59:41 +01:00
const statusDisplay = (paymentScheduleId: number): string => {
if (isExpanded(paymentScheduleId)) {
return 'table-row'
} else {
return 'none';
}
}
/**
* Return the action icon for showing/hiding the deadlines
*/
2021-01-27 13:59:41 +01:00
const expandCollapseIcon = (paymentScheduleId: number): JSX.Element => {
if (isExpanded(paymentScheduleId)) {
return <i className="fas fa-minus-square" />;
} else {
return <i className="fas fa-plus-square" />
}
}
/**
* Show or hide the deadlines for the provided payment schedule, inverting their current status
*/
2021-01-27 13:59:41 +01:00
const togglePaymentScheduleDetails = (paymentScheduleId: number): ReactEventHandler => {
return (): void => {
if (isExpanded(paymentScheduleId)) {
2021-02-04 17:00:02 +01:00
setShowExpanded((prev) => new Map(prev).set(paymentScheduleId, false));
2021-01-27 13:59:41 +01:00
} else {
2021-02-04 17:00:02 +01:00
setShowExpanded((prev) => new Map(prev).set(paymentScheduleId, true));
2021-01-27 13:59:41 +01:00
}
}
}
/**
* For use with downloadButton()
*/
2021-01-27 13:59:41 +01:00
enum TargetType {
Invoice = 'invoices',
PaymentSchedule = 'payment_schedules'
}
/**
* Return a button to download a PDF file, may be an invoice, or a payment schedule, depending or the provided parameters
*/
2021-01-27 13:59:41 +01:00
const downloadButton = (target: TargetType, id: number): JSX.Element => {
const link = `api/${target}/${id}/download`;
return (
<a href={link} target="_blank" className="download-button">
<i className="fas fa-download" />
{t('app.shared.schedules_table.download')}
2021-01-27 13:59:41 +01:00
</a>
);
}
/**
* Return the human-readable string for the status of the provided deadline.
*/
2021-01-27 13:59:41 +01:00
const formatState = (item: PaymentScheduleItem): JSX.Element => {
let res = t(`app.shared.schedules_table.state_${item.state}`);
2021-01-27 13:59:41 +01:00
if (item.state === PaymentScheduleItemState.Paid) {
const key = `app.shared.schedules_table.method_${item.payment_method}`
res += ` (${t(key)})`;
2021-01-27 13:59:41 +01:00
}
return <span className={`state-${item.state}`}>{res}</span>;
}
2021-02-09 16:26:09 +01:00
/**
* Check if the current operator has administrative rights or is a normal member
*/
const isPrivileged = (): boolean => {
return (operator.role === UserRole.Admin || operator.role == UserRole.Manager);
}
/**
* Return the action button(s) for the given deadline
*/
2021-02-09 12:09:26 +01:00
const itemButtons = (item: PaymentScheduleItem, schedule: PaymentSchedule): JSX.Element => {
2021-01-27 13:59:41 +01:00
switch (item.state) {
case PaymentScheduleItemState.Paid:
return downloadButton(TargetType.Invoice, item.invoice_id);
case PaymentScheduleItemState.Pending:
2021-02-09 16:26:09 +01:00
if (isPrivileged()) {
return (
<FabButton onClick={handleConfirmCheckPayment(item)}
icon={<i className="fas fa-money-check" />}>
{t('app.shared.schedules_table.confirm_payment')}
2021-02-09 16:26:09 +01:00
</FabButton>
);
} else {
return <span>{t('app.shared.schedules_table.please_ask_reception')}</span>
2021-02-09 16:26:09 +01:00
}
case PaymentScheduleItemState.RequireAction:
return (
<FabButton onClick={handleSolveAction(item)}
icon={<i className="fas fa-wrench" />}>
{t('app.shared.schedules_table.solve')}
</FabButton>
);
case PaymentScheduleItemState.RequirePaymentMethod:
return (
2021-02-09 12:09:26 +01:00
<FabButton onClick={handleUpdateCard(item, schedule)}
icon={<i className="fas fa-credit-card" />}>
{t('app.shared.schedules_table.update_card')}
</FabButton>
);
case PaymentScheduleItemState.Error:
2021-02-09 16:26:09 +01:00
if (isPrivileged()) {
return (
<FabButton onClick={handleCancelSubscription(schedule)}
icon={<i className="fas fa-times" />}>
{t('app.shared.schedules_table.cancel_subscription')}
2021-02-09 16:26:09 +01:00
</FabButton>
)
} else {
return <span>{t('app.shared.schedules_table.please_ask_reception')}</span>
2021-02-09 16:26:09 +01:00
}
2021-01-27 13:59:41 +01:00
default:
return <span />
}
}
2021-02-08 15:28:47 +01:00
/**
* Callback triggered when the user's clicks on the "cash check" button: show a confirmation modal
*/
const handleConfirmCheckPayment = (item: PaymentScheduleItem): ReactEventHandler => {
return (): void => {
2021-02-04 17:00:02 +01:00
setTempDeadline(item);
toggleConfirmCashingModal();
}
}
2021-02-08 15:28:47 +01:00
/**
* After the user has confirmed that he wants to cash the check, update the API, refresh the list and close the modal.
*/
2021-02-04 17:00:02 +01:00
const onCheckCashingConfirmed = (): void => {
const api = new PaymentScheduleAPI();
2021-02-08 08:56:01 +01:00
api.cashCheck(tempDeadline.id).then((res) => {
if (res.state === PaymentScheduleItemState.Paid) {
refreshSchedulesTable();
2021-02-08 08:56:01 +01:00
toggleConfirmCashingModal();
}
2021-02-04 17:00:02 +01:00
});
}
/**
* Refresh all payment schedules in the table
*/
const refreshSchedulesTable = (): void => {
refreshList(setErrors);
}
2021-02-04 17:00:02 +01:00
/**
* Show/hide the modal dialog that enable to confirm the cashing of the check for a given deadline.
*/
const toggleConfirmCashingModal = (): void => {
setShowConfirmCashing(!showConfirmCashing);
}
/**
2021-02-08 15:28:47 +01:00
* Show/hide the modal dialog that trigger the card "action".
2021-02-04 17:00:02 +01:00
*/
2021-02-08 15:28:47 +01:00
const toggleResolveActionModal = (): void => {
setShowResolveAction(!showResolveAction);
}
2021-02-08 15:28:47 +01:00
/**
* Callback triggered when the user's clicks on the "resolve" button: show a modal that will trigger the action
*/
const handleSolveAction = (item: PaymentScheduleItem): ReactEventHandler => {
return (): void => {
2021-02-08 15:28:47 +01:00
setTempDeadline(item);
toggleResolveActionModal();
}
}
2021-02-08 15:28:47 +01:00
/**
* After the action was done (successfully or not), ask the API to refresh the item status, then refresh the list and close the modal
*/
const afterAction = (): void => {
toggleConfirmActionButton();
const api = new PaymentScheduleAPI();
api.refreshItem(tempDeadline.id).then(() => {
refreshSchedulesTable();
2021-02-08 15:28:47 +01:00
toggleResolveActionModal();
});
}
/**
* Enable/disable the confirm button of the "action" modal
*/
const toggleConfirmActionButton = (): void => {
setConfirmActionDisabled(!isConfirmActionDisabled);
}
/**
* Callback triggered when the user's clicks on the "update card" button: show a modal to input a new card
*/
2021-02-09 12:09:26 +01:00
const handleUpdateCard = (item: PaymentScheduleItem, paymentSchedule: PaymentSchedule): ReactEventHandler => {
return (): void => {
2021-02-09 12:09:26 +01:00
setTempDeadline(item);
setTempSchedule(paymentSchedule);
toggleUpdateCardModal();
}
}
2021-02-09 12:09:26 +01:00
/**
* Show/hide the modal dialog to update the bank card details
*/
const toggleUpdateCardModal = (): void => {
setShowUpdateCard(!showUpdateCard);
}
/**
* Return the logos, shown in the modal footer.
*/
const logoFooter = (): ReactNode => {
return (
<div className="stripe-modal-icons">
<i className="fa fa-lock fa-2x m-r-sm pos-rlt" />
<img src={stripeLogo} alt="powered by stripe" />
<img src={mastercardLogo} alt="mastercard" />
<img src={visaLogo} alt="visa" />
</div>
);
}
/**
* When the submit button is pushed, disable it to prevent double form submission
*/
const handleCardUpdateSubmit = (): void => {
setCanSubmitUpdateCard(false);
}
/**
* When the card was successfully updated, pay the invoice (using the new payment method) and close the modal
*/
const handleCardUpdateSuccess = (): void => {
const api = new PaymentScheduleAPI();
api.payItem(tempDeadline.id).then(() => {
refreshSchedulesTable();
2021-02-09 12:09:26 +01:00
toggleUpdateCardModal();
}).catch((err) => {
handleCardUpdateError(err);
2021-02-09 12:09:26 +01:00
});
}
/**
* When the card was not updated, show the error
*/
const handleCardUpdateError = (error): void => {
setErrors(error);
setCanSubmitUpdateCard(true);
}
/**
* Callback triggered when the user clicks on the "cancel subscription" button
*/
const handleCancelSubscription = (schedule: PaymentSchedule): ReactEventHandler => {
return (): void => {
setTempSchedule(schedule);
toggleCancelSubscriptionModal();
}
}
/**
* Show/hide the modal dialog to cancel the current subscription
*/
const toggleCancelSubscriptionModal = (): void => {
setShowCancelSubscription(!showCancelSubscription);
}
/**
* When the user has confirmed the cancellation, we transfer the request to the API
*/
const onCancelSubscriptionConfirmed = (): void => {
const api = new PaymentScheduleAPI();
api.cancel(tempSchedule.id).then(() => {
refreshSchedulesTable();
toggleCancelSubscriptionModal();
});
}
2021-01-27 13:59:41 +01:00
return (
<div>
<table className="schedules-table">
<thead>
<tr>
<th className="w-35" />
<th className="w-200">{t('app.shared.schedules_table.schedule_num')}</th>
<th className="w-200">{t('app.shared.schedules_table.date')}</th>
<th className="w-120">{t('app.shared.schedules_table.price')}</th>
{showCustomer && <th className="w-200">{t('app.shared.schedules_table.customer')}</th>}
<th className="w-200"/>
</tr>
</thead>
<tbody>
{paymentSchedules.map(p => <tr key={p.id}>
<td colSpan={showCustomer ? 6 : 5}>
<table className="schedules-table-body">
<tbody>
<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>
{showCustomer && <td className="w-200">{p.user.name}</td>}
<td className="w-200">{downloadButton(TargetType.PaymentSchedule, p.id)}</td>
</tr>
<tr style={{ display: statusDisplay(p.id) }}>
<td className="w-35" />
<td colSpan={showCustomer ? 5 : 4}>
<div>
<table className="schedule-items-table">
<thead>
<tr>
<th className="w-120">{t('app.shared.schedules_table.deadline')}</th>
<th className="w-120">{t('app.shared.schedules_table.amount')}</th>
<th className="w-200">{t('app.shared.schedules_table.state')}</th>
<th className="w-200" />
</tr>
</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>{formatState(item)}</td>
2021-02-09 12:09:26 +01:00
<td>{itemButtons(item, p)}</td>
</tr>)}
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>)}
</tbody>
</table>
2021-02-04 17:00:02 +01:00
<div className="modals">
<FabModal title={t('app.shared.schedules_table.confirm_check_cashing')}
2021-02-04 17:00:02 +01:00
isOpen={showConfirmCashing}
toggleModal={toggleConfirmCashingModal}
onConfirm={onCheckCashingConfirmed}
closeButton={true}
confirmButton={t('app.shared.schedules_table.confirm_button')}>
2021-02-08 15:28:47 +01:00
{tempDeadline && <span>
{t('app.shared.schedules_table.confirm_check_cashing_body', {
2021-02-08 15:28:47 +01:00
AMOUNT: formatPrice(tempDeadline.amount),
DATE: formatDate(tempDeadline.due_date)
})}
</span>}
2021-02-04 17:00:02 +01:00
</FabModal>
<FabModal title={t('app.shared.schedules_table.cancel_subscription')}
isOpen={showCancelSubscription}
toggleModal={toggleCancelSubscriptionModal}
onConfirm={onCancelSubscriptionConfirmed}
closeButton={true}
confirmButton={t('app.shared.schedules_table.confirm_button')}>
{t('app.shared.schedules_table.confirm_cancel_subscription')}
</FabModal>
2021-02-08 15:28:47 +01:00
<StripeElements>
<FabModal title={t('app.shared.schedules_table.resolve_action')}
2021-02-08 15:28:47 +01:00
isOpen={showResolveAction}
toggleModal={toggleResolveActionModal}
onConfirm={afterAction}
confirmButton={t('app.shared.schedules_table.ok_button')}
2021-02-08 15:28:47 +01:00
preventConfirm={isConfirmActionDisabled}>
{tempDeadline && <StripeConfirm clientSecret={tempDeadline.client_secret} onResponse={toggleConfirmActionButton} />}
</FabModal>
<FabModal title={t('app.shared.schedules_table.update_card')}
2021-02-09 12:09:26 +01:00
isOpen={showUpdateCard}
toggleModal={toggleUpdateCardModal}
closeButton={false}
customFooter={logoFooter()}
className="update-card-modal">
{tempDeadline && tempSchedule && <StripeCardUpdate onSubmit={handleCardUpdateSubmit}
onSuccess={handleCardUpdateSuccess}
onError={handleCardUpdateError}
customerId={tempSchedule.user.id}
operator={operator}
className="card-form" >
{errors && <div className="stripe-errors">
{errors}
</div>}
</StripeCardUpdate>}
<div className="submit-card">
{canSubmitUpdateCard && <button type="submit" disabled={!canSubmitUpdateCard} form="stripe-card" className="submit-card-btn">{t('app.shared.schedules_table.validate_button')}</button>}
2021-02-09 12:09:26 +01:00
{!canSubmitUpdateCard && <div className="payment-pending">
<div className="fa-2x">
<i className="fas fa-circle-notch fa-spin" />
</div>
</div>}
</div>
</FabModal>
2021-02-08 15:28:47 +01:00
</StripeElements>
2021-02-04 17:00:02 +01:00
</div>
</div>
2021-01-27 13:59:41 +01:00
);
};
PaymentSchedulesTableComponent.defaultProps = { showCustomer: false };
2021-02-09 12:09:26 +01:00
export const PaymentSchedulesTable: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator }) => {
2021-01-27 13:59:41 +01:00
return (
<Loader>
2021-02-09 12:09:26 +01:00
<PaymentSchedulesTableComponent paymentSchedules={paymentSchedules} showCustomer={showCustomer} refreshList={refreshList} operator={operator} />
2021-01-27 13:59:41 +01:00
</Loader>
);
}