import React, { ReactEventHandler, ReactNode, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Loader } from './base/loader'; import moment from 'moment'; import { IFablab } from '../models/fablab'; import _ from 'lodash'; import { PaymentSchedule, PaymentScheduleItem, PaymentScheduleItemState } from '../models/payment-schedule'; import { FabButton } from './base/fab-button'; import { FabModal } from './base/fab-modal'; import PaymentScheduleAPI from '../api/payment-schedule'; import { StripeElements } from './stripe-elements'; import { StripeConfirm } from './stripe-confirm'; 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'; import { User, UserRole } from '../models/user'; declare var Fablab: IFablab; interface PaymentSchedulesTableProps { paymentSchedules: Array, showCustomer?: boolean, refreshList: (onError: (msg: any) => void) => void, operator: User, } /** * This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices */ const PaymentSchedulesTableComponent: React.FC = ({ paymentSchedules, showCustomer, refreshList, operator }) => { const { t } = useTranslation('shared'); // for each payment schedule: are the details (all deadlines) shown or hidden? const [showExpanded, setShowExpanded] = useState>(new Map()); // is open, the modal dialog to confirm the cashing of a check? const [showConfirmCashing, setShowConfirmCashing] = useState(false); // is open, the modal dialog the resolve a pending card payment? const [showResolveAction, setShowResolveAction] = useState(false); // the user cannot confirm the action modal (3D secure), unless he has resolved the pending action const [isConfirmActionDisabled, setConfirmActionDisabled] = useState(true); // is open, the modal dialog to update the card details const [showUpdateCard, setShowUpdateCard] = useState(false); // when an action is triggered on a deadline, the deadline is saved here until the action is done or cancelled. const [tempDeadline, setTempDeadline] = useState(null); // when an action is triggered on a deadline, the parent schedule is saved here until the action is done or cancelled. const [tempSchedule, setTempSchedule] = useState(null); // prevent submitting the form to update the card details, until all required fields are filled correctly const [canSubmitUpdateCard, setCanSubmitUpdateCard] = useState(true); // errors are saved here, if any, for display purposes. const [errors, setErrors] = useState(null); // is open, the modal dialog to cancel the associated subscription? const [showCancelSubscription, setShowCancelSubscription] = useState(false); /** * Check if the requested payment schedule is displayed with its deadlines (PaymentScheduleItem) or without them */ const isExpanded = (paymentScheduleId: number): boolean => { 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 */ const statusDisplay = (paymentScheduleId: number): string => { if (isExpanded(paymentScheduleId)) { return 'table-row' } else { return 'none'; } } /** * Return the action icon for showing/hiding the deadlines */ const expandCollapseIcon = (paymentScheduleId: number): JSX.Element => { if (isExpanded(paymentScheduleId)) { return ; } else { return } } /** * Show or hide the deadlines for the provided payment schedule, inverting their current status */ const togglePaymentScheduleDetails = (paymentScheduleId: number): ReactEventHandler => { return (): void => { if (isExpanded(paymentScheduleId)) { setShowExpanded((prev) => new Map(prev).set(paymentScheduleId, false)); } else { setShowExpanded((prev) => new Map(prev).set(paymentScheduleId, true)); } } } /** * For use with downloadButton() */ 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 */ const downloadButton = (target: TargetType, id: number): JSX.Element => { const link = `api/${target}/${id}/download`; return ( {t('app.shared.schedules_table.download')} ); } /** * Return the human-readable string for the status of the provided deadline. */ const formatState = (item: PaymentScheduleItem): JSX.Element => { let res = t(`app.shared.schedules_table.state_${item.state}`); if (item.state === PaymentScheduleItemState.Paid) { const key = `app.shared.schedules_table.method_${item.payment_method}` res += ` (${t(key)})`; } return {res}; } /** * 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 */ const itemButtons = (item: PaymentScheduleItem, schedule: PaymentSchedule): JSX.Element => { switch (item.state) { case PaymentScheduleItemState.Paid: return downloadButton(TargetType.Invoice, item.invoice_id); case PaymentScheduleItemState.Pending: if (isPrivileged()) { return ( }> {t('app.shared.schedules_table.confirm_payment')} ); } else { return {t('app.shared.schedules_table.please_ask_reception')} } case PaymentScheduleItemState.RequireAction: return ( }> {t('app.shared.schedules_table.solve')} ); case PaymentScheduleItemState.RequirePaymentMethod: return ( }> {t('app.shared.schedules_table.update_card')} ); case PaymentScheduleItemState.Error: if (isPrivileged()) { return ( }> {t('app.shared.schedules_table.cancel_subscription')} ) } else { return {t('app.shared.schedules_table.please_ask_reception')} } default: return } } /** * Callback triggered when the user's clicks on the "cash check" button: show a confirmation modal */ const handleConfirmCheckPayment = (item: PaymentScheduleItem): ReactEventHandler => { return (): void => { setTempDeadline(item); toggleConfirmCashingModal(); } } /** * After the user has confirmed that he wants to cash the check, update the API, refresh the list and close the modal. */ const onCheckCashingConfirmed = (): void => { const api = new PaymentScheduleAPI(); api.cashCheck(tempDeadline.id).then((res) => { if (res.state === PaymentScheduleItemState.Paid) { refreshSchedulesTable(); toggleConfirmCashingModal(); } }); } /** * Refresh all payment schedules in the table */ const refreshSchedulesTable = (): void => { refreshList(setErrors); } /** * Show/hide the modal dialog that enable to confirm the cashing of the check for a given deadline. */ const toggleConfirmCashingModal = (): void => { setShowConfirmCashing(!showConfirmCashing); } /** * Show/hide the modal dialog that trigger the card "action". */ const toggleResolveActionModal = (): void => { setShowResolveAction(!showResolveAction); } /** * 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 => { setTempDeadline(item); toggleResolveActionModal(); } } /** * 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(); 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 */ const handleUpdateCard = (item: PaymentScheduleItem, paymentSchedule: PaymentSchedule): ReactEventHandler => { return (): void => { setTempDeadline(item); setTempSchedule(paymentSchedule); toggleUpdateCardModal(); } } /** * 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 (
powered by stripe mastercard visa
); } /** * 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(); toggleUpdateCardModal(); }).catch((err) => { handleCardUpdateError(err); }); } /** * 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(); }); } return (
{showCustomer && } {paymentSchedules.map(p => )}
{t('app.shared.schedules_table.schedule_num')} {t('app.shared.schedules_table.date')} {t('app.shared.schedules_table.price')}{t('app.shared.schedules_table.customer')}
{showCustomer && }
{expandCollapseIcon(p.id)} {p.reference} {formatDate(p.created_at)} {formatPrice(p.total)}{p.user.name}{downloadButton(TargetType.PaymentSchedule, p.id)}
{_.orderBy(p.items, 'due_date').map(item => )}
{t('app.shared.schedules_table.deadline')} {t('app.shared.schedules_table.amount')} {t('app.shared.schedules_table.state')}
{formatDate(item.due_date)} {formatPrice(item.amount)} {formatState(item)} {itemButtons(item, p)}
{tempDeadline && {t('app.shared.schedules_table.confirm_check_cashing_body', { AMOUNT: formatPrice(tempDeadline.amount), DATE: formatDate(tempDeadline.due_date) })} } {t('app.shared.schedules_table.confirm_cancel_subscription')} {tempDeadline && } {tempDeadline && tempSchedule && {errors &&
{errors}
}
}
{canSubmitUpdateCard && } {!canSubmitUpdateCard &&
}
); }; PaymentSchedulesTableComponent.defaultProps = { showCustomer: false }; export const PaymentSchedulesTable: React.FC = ({ paymentSchedules, showCustomer, refreshList, operator }) => { return ( ); }