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

300 lines
10 KiB
TypeScript
Raw Normal View History

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-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';
import { Loader } from './loader';
import moment from 'moment';
import { IFablab } from '../models/fablab';
import _ from 'lodash';
import { PaymentSchedule, PaymentScheduleItem, PaymentScheduleItemState } from '../models/payment-schedule';
import { FabButton } from './fab-button';
2021-02-04 17:00:02 +01:00
import { FabModal } from './fab-modal';
import PaymentScheduleAPI from '../api/payment-schedule';
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: () => void
2021-01-27 13:59:41 +01:00
}
2021-02-04 17:51:16 +01:00
const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList }) => {
2021-01-27 13:59:41 +01:00
const { t } = useTranslation('admin');
2021-02-04 17:00:02 +01:00
const [showExpanded, setShowExpanded] = useState<Map<number, boolean>>(new Map());
const [showConfirmCashing, setShowConfirmCashing] = useState<boolean>(false);
const [tempDeadline, setTempDeadline] = useState<PaymentScheduleItem>(null);
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.admin.invoices.schedules_table.download')}
</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.admin.invoices.schedules_table.state_${item.state}`);
if (item.state === PaymentScheduleItemState.Paid) {
const key = `app.admin.invoices.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>;
}
/**
* Return the action button(s) for the given deadline
*/
2021-01-27 13:59:41 +01:00
const itemButtons = (item: PaymentScheduleItem): JSX.Element => {
switch (item.state) {
case PaymentScheduleItemState.Paid:
return downloadButton(TargetType.Invoice, item.invoice_id);
case PaymentScheduleItemState.Pending:
return (
<FabButton onClick={handleConfirmCheckPayment(item)}
icon={<i className="fas fa-money-check" />}>
{t('app.admin.invoices.schedules_table.confirm_payment')}
</FabButton>
);
case PaymentScheduleItemState.RequireAction:
return (
<FabButton onClick={handleSolveAction(item)}
icon={<i className="fas fa-wrench" />}>
{t('app.admin.invoices.schedules_table.solve')}
</FabButton>
);
case PaymentScheduleItemState.RequirePaymentMethod:
return (
<FabButton onClick={handleUpdateCard(item)}
icon={<i className="fas fa-credit-card" />}>
{t('app.admin.invoices.schedules_table.update_card')}
</FabButton>
);
2021-01-27 13:59:41 +01:00
default:
return <span />
}
}
const handleConfirmCheckPayment = (item: PaymentScheduleItem): ReactEventHandler => {
return (): void => {
2021-02-04 17:00:02 +01:00
setTempDeadline(item);
toggleConfirmCashingModal();
}
}
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) {
refreshList();
toggleConfirmCashingModal();
}
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);
}
/**
* Dynamically build the content of the modal depending on the currently selected deadline
*/
const cashingModalContent = (): ReactNode => {
if (tempDeadline) {
return (
<span>{t('app.admin.invoices.schedules_table.confirm_check_cashing_body', {
AMOUNT: formatPrice(tempDeadline.amount),
DATE: formatDate(tempDeadline.due_date)
})}</span>
);
}
2021-02-04 17:00:02 +01:00
return <span />;
}
const handleSolveAction = (item: PaymentScheduleItem): ReactEventHandler => {
return (): void => {
/*
TODO
- create component wrapped with <StripeElements>
- stripe.confirmCardSetup(item.client_secret).then(function(result) {
if (result.error) {
// Display error.message in your UI.
} else {
// The setup has succeeded. Display a success message.
}
});
*/
}
}
const handleUpdateCard = (item: PaymentScheduleItem): ReactEventHandler => {
return (): void => {
/*
TODO
- Notify the customer, collect new payment information, and create a new payment method
- Attach the payment method to the customer
- Update the default payment method
- Pay the invoice using the new payment method
*/
}
}
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.admin.invoices.schedules_table.schedule_num')}</th>
<th className="w-200">{t('app.admin.invoices.schedules_table.date')}</th>
<th className="w-120">{t('app.admin.invoices.schedules_table.price')}</th>
{showCustomer && <th className="w-200">{t('app.admin.invoices.schedules_table.customer')}</th>}
<th className="w-200"/>
</tr>
</thead>
<tbody>
{paymentSchedules.map(p => <tr key={p.id}>
<td colSpan={6}>
<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={5}>
<div>
<table className="schedule-items-table">
<thead>
<tr>
<th className="w-120">{t('app.admin.invoices.schedules_table.deadline')}</th>
<th className="w-120">{t('app.admin.invoices.schedules_table.amount')}</th>
<th className="w-200">{t('app.admin.invoices.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>
<td>{itemButtons(item)}</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.admin.invoices.schedules_table.confirm_check_cashing')}
isOpen={showConfirmCashing}
toggleModal={toggleConfirmCashingModal}
onConfirm={onCheckCashingConfirmed}
closeButton={true}
confirmButton={t('app.admin.invoices.schedules_table.confirm_button')}>
{cashingModalContent()}
</FabModal>
</div>
</div>
2021-01-27 13:59:41 +01:00
);
};
PaymentSchedulesTableComponent.defaultProps = { showCustomer: false };
2021-02-04 17:51:16 +01:00
export const PaymentSchedulesTable: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList }) => {
2021-01-27 13:59:41 +01:00
return (
<Loader>
2021-02-04 17:51:16 +01:00
<PaymentSchedulesTableComponent paymentSchedules={paymentSchedules} showCustomer={showCustomer} refreshList={refreshList} />
2021-01-27 13:59:41 +01:00
</Loader>
);
}