client/components/mma/billing/InvoicesTable.tsx (375 lines of code) (raw):

import { css } from '@emotion/react'; import { from, headlineBold20, palette, space, textSans17, textSansBold17, until, } from '@guardian/source/foundations'; import { useState } from 'react'; import { parseDate } from '@/shared/dates'; import type { InvoiceDataApiItem } from '@/shared/productResponse'; import { trackEvent } from '../../../utilities/analytics'; import { DownloadIcon } from '../shared/assets/DownloadIcon'; import { CardDisplay } from '../shared/CardDisplay'; import { DirectDebitDisplay } from '../shared/DirectDebitDisplay'; import { Pagination } from '../shared/Pagination'; import { PaypalDisplay } from '../shared/PaypalDisplay'; import { SepaDisplay } from '../shared/SepaDisplay'; import { InvoiceTableYearSelect } from './InvoiceTableYearSelect'; const invoicePaymentMethods = { CARD: 'card', DIRECT_DEBIT: 'directdebit', PAYPAL: 'paypal', SEPA: 'sepa', }; interface InvoiceInfo extends InvoiceDataApiItem { productUrlPart?: string; currencyISO: string; currency: string; } interface InvoicesTableProps { resultsPerPage: number; invoiceData: InvoiceInfo[]; } export const InvoicesTable = (props: InvoicesTableProps) => { const [ trackingPaginationInteractionCount, setTrackingPaginationInteractionCount, ] = useState<number>(1); const initialPage = 1; const [currentPage, setCurrentPage] = useState<number>(initialPage); const tableHeadings = ['Date', 'Payment method', 'Price', '']; const invoiceYears = [ ...new Set( [...props.invoiceData].map( (invoice) => `${parseDate(invoice.date).date.getFullYear()}`, ), ), ]; const [currentInvoiceYear, setCurrentInvoiceYear] = useState<string>( invoiceYears[0], ); const [currentPaginationPage, setCurrentPaginationPage] = useState<number>(initialPage); const directPaginationUpdate = (newPageNumber: number) => { const targetInvoiceYear = `${parseDate( props.invoiceData[(newPageNumber - 1) * props.resultsPerPage].date, ).date.getFullYear()}`; setCurrentInvoiceYear(targetInvoiceYear); setCurrentPage(newPageNumber); trackEvent({ eventCategory: 'invoice', eventAction: 'click', eventLabel: 'invoice_pagination_select', eventValue: trackingPaginationInteractionCount, }); setTrackingPaginationInteractionCount( trackingPaginationInteractionCount + 1, ); }; const directYearUpdate = (newYear: string) => { const invoiceIndex = props.invoiceData.findIndex( (invoice) => `${parseDate(invoice.date).date.getFullYear()}` === newYear, ); const targetPage = Math.ceil((invoiceIndex + 1) / props.resultsPerPage); setCurrentPaginationPage(targetPage); setCurrentPage(targetPage); }; const tableCss2 = css` display: block; width: 100%; border: 1px solid ${palette.neutral[86]}; ${from.tablet} { display: table; } `; const tableHeaderCss2 = css` display: block; ${from.tablet} { display: table-header-group; } `; const tableBodyCss2 = css` display: block; ${from.tablet} { display: table-row-group; } `; const invoiceYearSelectCss = css` display: none; padding: ${space[3]}px; background-color: ${palette.neutral[97]}; border-bottom: 1px solid ${palette.neutral[86]}; ${from.tablet} { display: table-cell; padding: ${space[5]}px ${space[5]}px ${space[5]}px 0; } `; const tableTitleCss2 = css` display: table-cell; ${headlineBold20}; padding: ${space[5]}px; background-color: ${palette.neutral[97]}; border-bottom: 1px solid ${palette.neutral[86]}; ${until.tablet} { margin: 0; padding: ${space[3]}px; font-size: 1.0625rem; line-height: 1.6; border-bottom: 0; display: block; } `; const thCss2 = css` display: table-cell; text-align: left; ${textSansBold17}; padding: ${space[5]}px; ${until.tablet} { padding: ${space[3]}px; } `; const tableHeadingsRowCss2 = css` display: none; ${from.tablet} { display: table-row; } `; const tableRowCss2 = css` display: block; ${from.tablet} { display: table-row; } `; const tdCss2 = (rowIndex: number, title?: string) => css` display: block; ${textSans17}; padding: ${space[3]}px ${space[3]}px 0; :last-of-type { ${until.tablet} { border-bottom: 1px solid ${palette.neutral[86]}; } padding: ${space[3]}px; } :before { display: ${title ? 'inline-block' : 'none'}; width: calc(60% - ${space[3]}px); padding-right: ${space[3]}px; ${textSansBold17}; content: '${title}'; } ${from.tablet} { display: table-cell; width: auto; padding: ${space[5]}px; margin: 0; border-top: 1px solid ${palette.neutral[86]}; background-color: ${rowIndex % 2 === 0 ? palette.neutral[97] : 'transparent'}; :before { display: none; content: ''; } } `; const paymentDetailsHolderCss = css` display: inline-block; width: calc(40% + ${space[3]}px); ${from.tablet} { width: auto; min-width: 15ch; } `; const invoiceLinkCss = css` color: ${palette.brand[400]}; font-weight: bold; `; const invoiceDownloadLinkCss = css` display: inline-block; width: 22px; margin-left: ${space[6]}px; `; return ( <> <div css={tableCss2}> <header css={tableHeaderCss2}> <div css={tableRowCss2}> <h2 css={tableTitleCss2}>Invoices</h2> <div css={css` display: none; background-color: ${palette.neutral[97]}; border-bottom: 1px solid ${palette.neutral[86]}; ${from.tablet} { display: table-cell; } `} /> <div css={css` display: none; background-color: ${palette.neutral[97]}; border-bottom: 1px solid ${palette.neutral[86]}; ${from.tablet} { display: table-cell; } `} /> <div css={invoiceYearSelectCss}> <InvoiceTableYearSelect years={invoiceYears} selectedYear={currentInvoiceYear} setSelectedYear={setCurrentInvoiceYear} onDirectUpdate={directYearUpdate} /> </div> </div> <div css={tableHeadingsRowCss2}> {tableHeadings.map((tableHeading, index) => ( <div css={thCss2} key={`invoiceTH-${index}`}> {tableHeading} </div> ))} </div> </header> <div css={tableBodyCss2}> {props.invoiceData .filter( (_, index) => index >= (currentPage - 1) * props.resultsPerPage && index < (currentPage - 1) * props.resultsPerPage + props.resultsPerPage, ) .map((tableRow, index) => { const paymentMethodLowercase = tableRow.paymentMethod.toLowerCase(); return ( <div css={tableRowCss2} key={tableRow.invoiceId} > <div css={tdCss2(index, tableHeadings[0])}> {parseDate(tableRow.date).dateStr()} </div> <div css={tdCss2(index, tableHeadings[1])}> <div css={paymentDetailsHolderCss} data-qm-masking="blocklist" > {tableRow.cardType && tableRow.last4 && ( <CardDisplay cssOverrides={css` margin: 0; `} last4={tableRow.last4} type={tableRow.cardType} /> )} {paymentMethodLowercase === invoicePaymentMethods.PAYPAL && ( <PaypalDisplay /> )} {paymentMethodLowercase === invoicePaymentMethods.DIRECT_DEBIT && tableRow.last4 && ( <DirectDebitDisplay accountNumber={ tableRow.last4 } accountName="" sortCode="" onlyAccountEnding /> )} {paymentMethodLowercase === invoicePaymentMethods.SEPA && tableRow.last4 && ( <SepaDisplay accountName="" iban={tableRow.last4} /> )} {paymentMethodLowercase !== invoicePaymentMethods.CARD && paymentMethodLowercase !== invoicePaymentMethods.PAYPAL && paymentMethodLowercase !== invoicePaymentMethods.DIRECT_DEBIT && paymentMethodLowercase !== invoicePaymentMethods.SEPA && ( <span> No Payment Method </span> )} </div> </div> <div css={tdCss2(index, tableHeadings[2])}> {tableRow.hasMultipleSubs ? 'Multiple prices' : `${tableRow.currency}${Number( tableRow.price, ).toFixed(2)} ${ tableRow.currencyISO }`} </div> <div css={tdCss2(index)}> <a css={invoiceLinkCss} href={tableRow.pdfPath} onClick={() => trackEvent({ eventCategory: 'invoice', eventAction: 'click', eventLabel: `view_${tableRow.productUrlPart}_pdf_invoice`, }) } > View invoice (PDF) </a> <a css={invoiceDownloadLinkCss} download={`invoice_${ tableRow.subscriptionName }_${parseDate( tableRow.date, ).dateStr('yyyy-MM-dd')}.pdf`} href={tableRow.pdfPath} onClick={() => trackEvent({ eventCategory: 'invoice', eventAction: 'click', eventLabel: `download_${tableRow.productUrlPart}_pdf_invoice`, }) } > <DownloadIcon /> </a> </div> </div> ); })} </div> </div> {props.resultsPerPage < props.invoiceData.length && ( <Pagination currentPage={currentPaginationPage} setCurrentPage={setCurrentPaginationPage} onDirectUpdate={directPaginationUpdate} numberOfResults={props.invoiceData.length} resultsPerPage={props.resultsPerPage} additionalCSS={css` margin-top: ${space[5]}px; `} /> )} </> ); };