diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.styles.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.styles.tsx new file mode 100644 index 0000000000..45f03ae127 --- /dev/null +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.styles.tsx @@ -0,0 +1,15 @@ +import { styled } from '@mui/material'; + +export const StyledSubgrid = styled('div', { + shouldForwardProp: (prop) => prop !== 'withBackground', +})<{ withBackground?: boolean }>(({ theme, withBackground }) => ({ + display: 'grid', + gridTemplateColumns: 'subgrid', + gridColumn: '1 / -1', + background: withBackground + ? theme.palette.background.elevation1 + : 'transparent', + margin: theme.spacing(0.25, 0), + padding: theme.spacing(0, 2), + borderRadius: theme.shape.borderRadiusLarge, +})); diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.tsx index bbff396045..8d0eb1e7e1 100644 --- a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.tsx +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.tsx @@ -9,31 +9,10 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { formatCurrency } from './types.ts'; import { Badge } from 'component/common/Badge/Badge.tsx'; import type { FC, ReactNode } from 'react'; +import type { DetailedInvoicesResponseSchemaInvoicesItem } from 'openapi/index.ts'; import { BillingInvoiceRow } from './BillingInvoiceRow/BillingInvoiceRow.tsx'; - -export type BillingInvoiceSectionItem = { - description: string; - quantity?: number; - amount?: number; - quota?: number; -}; - -type BillingInvoiceSection = { - id: string; - title?: string; - items: BillingInvoiceSectionItem[]; - summary?: { - subtotal: number; - taxExemptNote?: string; - total: number; - }; -}; - -type BillingInvoiceProps = { - title: string; - status?: 'estimate' | 'upcoming' | 'invoiced'; - sections?: BillingInvoiceSection[]; -}; +import { BillingInvoiceFooter } from './BillingInvoiceFooter/BillingInvoiceFooter.tsx'; +import { StyledSubgrid } from './BillingInvoice.styles.tsx'; const CardLikeAccordion = styled(Accordion)(({ theme }) => ({ background: theme.palette.background.paper, @@ -78,19 +57,6 @@ const StyledInvoiceGrid = styled('div')(({ theme }) => ({ padding: theme.spacing(0, 2, 3), })); -const StyledSubgrid = styled('div', { - shouldForwardProp: (prop) => prop !== 'withBackground', -})<{ withBackground?: boolean }>(({ theme, withBackground }) => ({ - display: 'grid', - gridTemplateColumns: 'subgrid', - gridColumn: '1 / -1', - background: withBackground - ? theme.palette.background.elevation1 - : 'transparent', - padding: theme.spacing(0.25, 2), - borderRadius: theme.shape.borderRadiusLarge, -})); - const HeaderCell = styled(Typography)(({ theme }) => ({ fontSize: theme.typography.body2.fontSize, fontWeight: theme.typography.fontWeightMedium, @@ -111,14 +77,14 @@ const StyledSectionTitle = styled(Typography)(({ theme }) => ({ fontWeight: theme.fontWeight.bold, })); -export const StyledTableRow = styled('div')(({ theme }) => ({ +const StyledTableRow = styled('div')(({ theme }) => ({ display: 'grid', gridColumn: '1 / -1', gridTemplateColumns: 'subgrid', padding: theme.spacing(1, 0), })); -const sectionsMock: BillingInvoiceSection[] = [ +const sectionsMock = [ { id: 'seats', items: [ @@ -174,19 +140,20 @@ const sectionsMock: BillingInvoiceSection[] = [ ]; export const BillingInvoice = ({ - title, status, - sections = sectionsMock, -}: BillingInvoiceProps) => { - const total = sections.reduce( - (acc, section) => - acc + - section.items.reduce( - (itemAcc, item) => itemAcc + (item.amount || 0), - 0, - ), - 0, - ); + dueDate, + invoiceDate, + invoicePDF, + invoiceURL, + totalAmount, + lines, +}: DetailedInvoicesResponseSchemaInvoicesItem) => { + const title = invoiceDate + ? new Date(invoiceDate).toLocaleDateString(undefined, { + month: 'long', + day: 'numeric', + }) + : ''; return ( @@ -214,7 +181,7 @@ export const BillingInvoice = ({ Invoiced ) : null} - {formatCurrency(total)} + {formatCurrency(totalAmount)} @@ -231,20 +198,29 @@ export const BillingInvoice = ({ Quantity Amount - {sections.map((section) => ( - - {section.title ? ( + {lines.map((line) => ( + + {/* {line.description ? ( - {section.title} + {line.description} - ) : null} - {section.items.map((item) => ( - - - - ))} + ) : null} */} + + + ))} + + diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceFooter/BillingInvoiceFooter.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceFooter/BillingInvoiceFooter.tsx new file mode 100644 index 0000000000..49d1b61a03 --- /dev/null +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceFooter/BillingInvoiceFooter.tsx @@ -0,0 +1,85 @@ +import type { FC } from 'react'; +import { styled } from '@mui/material'; +import { formatCurrency } from '../types.ts'; +import { StyledSubgrid } from '../BillingInvoice.styles.tsx'; + +const StyledTableFooter = styled(StyledSubgrid)(({ theme }) => ({ + gridColumn: '3 / -1', + padding: theme.spacing(1, 0, 0), +})); + +const StyledTableFooterRow = styled('div')<{ last?: boolean }>( + ({ theme, last }) => ({ + marginRight: theme.spacing(1), + display: 'grid', + gridColumn: '1 / -1', + gridTemplateColumns: 'subgrid', + ...(last + ? { fontWeight: theme.typography.fontWeightBold } + : { borderBottom: `1px solid ${theme.palette.divider}` }), + }), +); + +const StyledTableFooterCell = styled('div', { + shouldForwardProp: (prop) => prop !== 'colSpan', +})<{ colSpan?: number }>(({ theme, colSpan }) => ({ + padding: theme.spacing(1, 0), + ...(colSpan ? { gridColumn: `span ${colSpan}` } : {}), +})); + +interface BillingInvoiceFooterProps { + subTotal?: number; + taxAmount?: number; + totalAmount: number; +} + +const TaxRow: FC<{ value?: number | null }> = ({ value }) => { + if (value === undefined) { + return null; + } + + if (value === null) { + return ( + + Customer tax is exempt + + ); + } + + return ( + <> + Tax + + {formatCurrency(value)} + + + ); +}; + +export const BillingInvoiceFooter = ({ + subTotal, + taxAmount, + totalAmount, +}: BillingInvoiceFooterProps) => { + return ( + + {subTotal ? ( + + Sub total + + {formatCurrency(subTotal)} + + + ) : null} + + + + + Total + + {formatCurrency(totalAmount)} + + + + ); +}; diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceRow/BillingInvoiceRow.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceRow/BillingInvoiceRow.tsx index 592e7b4450..e69881a4b3 100644 --- a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceRow/BillingInvoiceRow.tsx +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceRow/BillingInvoiceRow.tsx @@ -1,7 +1,6 @@ import { formatLargeNumbers } from 'component/impact-metrics/metricsFormatters.ts'; import { formatCurrency } from '../types.ts'; import { ConsumptionIndicator } from '../ConsumptionIndicator/ConsumptionIndicator.tsx'; -import type { BillingInvoiceSectionItem } from '../BillingInvoice.tsx'; import { styled } from '@mui/material'; const StyledCellWithIndicator = styled('div')(({ theme }) => ({ @@ -10,33 +9,39 @@ const StyledCellWithIndicator = styled('div')(({ theme }) => ({ gap: theme.spacing(1), })); +type BillingInvoiceRowProps = { + description: string; + quantity?: number; + amount?: number; + quota?: number; +}; + export const BillingInvoiceRow = ({ - item, -}: { item: BillingInvoiceSectionItem }) => { - const usage = item.quantity || 0; + quantity, + amount, + quota, + description, +}: BillingInvoiceRowProps) => { + const usage = quantity || 0; const percentage = - item.quota && item.quota > 0 - ? Math.min(100, Math.round((usage / item.quota) * 100)) + quota && quota > 0 + ? Math.min(100, Math.round((usage / quota) * 100)) : undefined; return ( <> -
{item.description}
+
{description}
{percentage !== undefined && ( )} - {item.quota !== undefined - ? formatLargeNumbers(item.quota) - : '–'} + {quota !== undefined ? formatLargeNumbers(quota) : '–'} {percentage !== undefined ? ` (${percentage}%)` : ''}
- {item.quantity !== undefined - ? formatLargeNumbers(item.quantity) - : '–'} + {quantity !== undefined ? formatLargeNumbers(quantity) : '–'}
-
{formatCurrency(item.amount || 0)}
+
{formatCurrency(amount || 0)}
); }; diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoices.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoices.tsx index 0f6166e25a..8df1c2d361 100644 --- a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoices.tsx +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoices.tsx @@ -1,6 +1,7 @@ import { Box, styled, Typography } from '@mui/material'; import type { FC } from 'react'; import { BillingInvoice } from './BillingInvoice/BillingInvoice.tsx'; +import { useDetailedInvoices } from 'hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts'; const StyledContainer = styled(Box)(({ theme }) => ({ display: 'flex', @@ -18,10 +19,14 @@ const StyledHeader = styled(Typography)(({ theme }) => ({ type BillingInvoicesProps = {}; export const BillingInvoices: FC = () => { + const { invoices } = useDetailedInvoices(); + return ( Usage and invoices - + {invoices.map((invoice) => ( + + ))} ); }; diff --git a/frontend/src/hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts b/frontend/src/hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts new file mode 100644 index 0000000000..656b28f04f --- /dev/null +++ b/frontend/src/hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts @@ -0,0 +1,75 @@ +import useSWR, { type SWRConfiguration } from 'swr'; +import { useMemo } from 'react'; +import { formatApiPath } from 'utils/formatPath'; +import handleErrorResponses from '../httpErrorResponseHandler.js'; +import type { DetailedInvoicesResponseSchema } from 'openapi'; + +const KEY = `api/admin/invoices/list`; +const path = formatApiPath(KEY); + +export const useDetailedInvoices = (options: SWRConfiguration = {}) => { + const fetcher = () => + fetch(path, { method: 'GET' }) + .then(handleErrorResponses('Detailed invoices')) + .then((res) => res.json()); + + const { data, error, isLoading } = useSWR( + KEY, + fetcher, + options, + ); + + const invoices = useMemo(() => data?.invoices ?? [], [data]); + + // return { invoices, error, loading: isLoading }; + + return { + invoices: [ + // TODO:MOCK + { + status: 'paid', + dueDate: '2023-09-01', + invoiceDate: '2023-08-01', + invoicePDF: 'https://example.com/invoice/1.pdf', + invoiceURL: 'https://example.com/invoice/1', + totalAmount: 100, + lines: [ + { + currency: 'USD', + description: 'Service A', + lookupKey: 'service-a', + quantity: 1, + totalAmount: 100, + }, + { + currency: 'USD', + description: 'Service B', + lookupKey: 'service-b', + quantity: 100, + limit: 120, + totalAmount: 200, + }, + ], + }, + { + status: 'unpaid', + dueDate: '2023-09-15', + invoiceDate: '2023-08-15', + invoicePDF: 'https://example.com/invoice/2.pdf', + invoiceURL: 'https://example.com/invoice/2', + totalAmount: 200, + lines: [ + { + currency: 'USD', + description: 'Service C', + lookupKey: 'service-c', + quantity: 1, + totalAmount: 200, + }, + ], + }, + ], + error, + loading: isLoading, + }; +};