diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.styles.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.styles.tsx index 45f03ae127..4512d796ea 100644 --- a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.styles.tsx +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.styles.tsx @@ -10,6 +10,11 @@ export const StyledSubgrid = styled('div', { ? theme.palette.background.elevation1 : 'transparent', margin: theme.spacing(0.25, 0), - padding: theme.spacing(0, 2), + padding: withBackground ? theme.spacing(0, 2, 1) : theme.spacing(0, 2), borderRadius: theme.shape.borderRadiusLarge, })); + +export const StyledAmountCell = styled('div')(({ theme }) => ({ + textAlign: 'right', + paddingRight: theme.spacing(1.5), +})); diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.tsx index 12b26c9be3..b78b3e2610 100644 --- a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.tsx +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.tsx @@ -1,3 +1,4 @@ +import type { FC, ReactNode } from 'react'; import { Typography, styled, @@ -6,13 +7,12 @@ import { AccordionDetails, } from '@mui/material'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { formatCurrency } from './types.ts'; +import { formatCurrency } from './formatCurrency.ts'; import { Badge } from 'component/common/Badge/Badge.tsx'; -import type { FC, ReactNode } from 'react'; -import type { DetailedInvoicesSchemaInvoicesItem } from 'openapi/index.ts'; import { BillingInvoiceRow } from './BillingInvoiceRow/BillingInvoiceRow.tsx'; import { BillingInvoiceFooter } from './BillingInvoiceFooter/BillingInvoiceFooter.tsx'; -import { StyledSubgrid } from './BillingInvoice.styles.tsx'; +import { StyledAmountCell, StyledSubgrid } from './BillingInvoice.styles.tsx'; +import type { DetailedInvoicesSchemaInvoicesItem } from 'openapi'; const CardLikeAccordion = styled(Accordion)(({ theme }) => ({ background: theme.palette.background.paper, @@ -73,7 +73,7 @@ const TableBody: FC<{ children: ReactNode; title?: string }> = ({ const StyledSectionTitle = styled(Typography)(({ theme }) => ({ gridColumn: '1 / -1', - padding: theme.spacing(2, 0), + padding: theme.spacing(2, 0, 1), fontWeight: theme.fontWeight.bold, })); @@ -84,69 +84,14 @@ const StyledTableRow = styled('div')(({ theme }) => ({ padding: theme.spacing(1, 0), })); -const sectionsMock = [ - { - id: 'seats', - items: [ - { - description: 'Unleash PAYG Seat', - quota: 50, - quantity: 41, - amount: 3_076, - }, - ], - }, - { - id: 'usage', - title: 'Usage: September', - items: [ - { - description: 'Frontend traffic', - quota: 10_000_000, - quantity: 1_085_000_000, - amount: 5_425, - }, - { - description: 'Service connections', - quota: 7, - quantity: 20, - amount: 0, - }, - { - description: 'Release templates', - quota: 5, - quantity: 3, - amount: 0, - }, - { - description: 'Edge Frontend Traffic', - quota: 10_000_000, - quantity: 2_000_000, - amount: 0, - }, - { - description: 'Edge Service Connections', - quota: 5, - quantity: 5, - amount: 0, - }, - ], - summary: { - subtotal: 8_500, - taxExemptNote: 'Customer tax is exempt', - total: 8_500, - }, - }, -]; - export const BillingInvoice = ({ status, - dueDate, invoiceDate, invoicePDF, invoiceURL, totalAmount, mainLines, + usageLines, }: DetailedInvoicesSchemaInvoicesItem) => { const title = invoiceDate ? new Date(invoiceDate).toLocaleDateString(undefined, { @@ -155,6 +100,8 @@ export const BillingInvoice = ({ }) : ''; + const currency = mainLines[0]?.currency || usageLines?.[0]?.currency; + return ( Invoiced ) : null} - {formatCurrency(totalAmount)} + {formatCurrency(totalAmount, currency)} @@ -196,31 +143,32 @@ export const BillingInvoice = ({ Description Included Quantity - Amount + + Amount + {mainLines.map((line) => ( - - {/* {line.description ? ( - - {line.description} - - ) : null} */} + - + ))} + {usageLines.length ? ( + + Usage + {usageLines.map((line) => ( + + + + ))} + + ) : 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 index 49d1b61a03..161055e5b2 100644 --- a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceFooter/BillingInvoiceFooter.tsx +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceFooter/BillingInvoiceFooter.tsx @@ -1,11 +1,11 @@ import type { FC } from 'react'; import { styled } from '@mui/material'; -import { formatCurrency } from '../types.ts'; -import { StyledSubgrid } from '../BillingInvoice.styles.tsx'; +import { formatCurrency } from '../formatCurrency.ts'; +import { StyledAmountCell, StyledSubgrid } from '../BillingInvoice.styles.tsx'; const StyledTableFooter = styled(StyledSubgrid)(({ theme }) => ({ gridColumn: '3 / -1', - padding: theme.spacing(1, 0, 0), + padding: theme.spacing(1, 1, 0, 0), })); const StyledTableFooterRow = styled('div')<{ last?: boolean }>( @@ -27,18 +27,8 @@ const StyledTableFooterCell = styled('div', { ...(colSpan ? { gridColumn: `span ${colSpan}` } : {}), })); -interface BillingInvoiceFooterProps { - subTotal?: number; - taxAmount?: number; - totalAmount: number; -} - -const TaxRow: FC<{ value?: number | null }> = ({ value }) => { +const TaxRow: FC<{ value?: number }> = ({ value }) => { if (value === undefined) { - return null; - } - - if (value === null) { return ( Customer tax is exempt @@ -50,24 +40,34 @@ const TaxRow: FC<{ value?: number | null }> = ({ value }) => { <> Tax - {formatCurrency(value)} + {formatCurrency(value)} ); }; +type BillingInvoiceFooterProps = { + subTotal?: number; + taxAmount?: number; + totalAmount: number; + currency?: string; +}; + export const BillingInvoiceFooter = ({ subTotal, taxAmount, totalAmount, + currency, }: BillingInvoiceFooterProps) => { return ( - {subTotal ? ( + {subTotal || !taxAmount ? ( Sub total - {formatCurrency(subTotal)} + + {formatCurrency(subTotal || totalAmount, currency)} + ) : null} @@ -77,7 +77,9 @@ export const BillingInvoiceFooter = ({ Total - {formatCurrency(totalAmount)} + + {formatCurrency(totalAmount, currency)} + 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 e69881a4b3..ea02adfb11 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,9 @@ import { formatLargeNumbers } from 'component/impact-metrics/metricsFormatters.ts'; -import { formatCurrency } from '../types.ts'; +import { formatCurrency } from '../formatCurrency.ts'; import { ConsumptionIndicator } from '../ConsumptionIndicator/ConsumptionIndicator.tsx'; import { styled } from '@mui/material'; +import type { DetailedInvoicesLineSchema } from 'openapi'; +import { StyledAmountCell } from '../BillingInvoice.styles.tsx'; const StyledCellWithIndicator = styled('div')(({ theme }) => ({ display: 'flex', @@ -14,34 +16,34 @@ type BillingInvoiceRowProps = { quantity?: number; amount?: number; quota?: number; + usage?: number; }; export const BillingInvoiceRow = ({ quantity, - amount, - quota, + consumption, + limit, description, -}: BillingInvoiceRowProps) => { - const usage = quantity || 0; + currency, + totalAmount, +}: DetailedInvoicesLineSchema) => { const percentage = - quota && quota > 0 - ? Math.min(100, Math.round((usage / quota) * 100)) + limit && limit > 0 + ? Math.min(100, Math.round(((consumption || 0) / limit) * 100)) : undefined; return ( <>
{description}
- {percentage !== undefined && ( - - )} - {quota !== undefined ? formatLargeNumbers(quota) : '–'} + + {limit !== undefined ? formatLargeNumbers(limit) : '–'} {percentage !== undefined ? ` (${percentage}%)` : ''} -
- {quantity !== undefined ? formatLargeNumbers(quantity) : '–'} -
-
{formatCurrency(amount || 0)}
+
{quantity ? formatLargeNumbers(quantity) : '–'}
+ + {formatCurrency(totalAmount || 0, currency)} + ); }; diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/formatCurrency.test.ts b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/formatCurrency.test.ts new file mode 100644 index 0000000000..9677df8ccf --- /dev/null +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/formatCurrency.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest'; +import { formatCurrency } from './formatCurrency.ts'; + +describe('formatCurrency', () => { + it('formats USD currency', () => { + expect(formatCurrency(1000, 'USD')).toMatchInlineSnapshot(`"$1,000"`); + expect(formatCurrency(1234.56, 'USD')).toMatchInlineSnapshot( + `"$1,234.56"`, + ); + expect(formatCurrency(1000000, 'USD')).toMatchInlineSnapshot( + `"$1,000,000"`, + ); + expect(formatCurrency(0, 'USD')).toMatchInlineSnapshot(`"$0"`); + expect(formatCurrency(-500, 'USD')).toMatchInlineSnapshot(`"$-500"`); + }); + + it('formats EUR currency', () => { + expect(formatCurrency(1000, 'EUR')).toMatchInlineSnapshot(`"€ 1 000"`); + expect(formatCurrency(1234.56, 'EUR')).toMatchInlineSnapshot( + `"€ 1 234,56"`, + ); + expect(formatCurrency(1000000, 'EUR')).toMatchInlineSnapshot( + `"€ 1 000 000"`, + ); + expect(formatCurrency(0, 'EUR')).toMatchInlineSnapshot(`"€ 0"`); + expect(formatCurrency(-500, 'EUR')).toMatchInlineSnapshot(`"€ −500"`); + }); + + it('formats other currencies', () => { + expect(formatCurrency(1000, 'GBP')).toMatchInlineSnapshot(`"1000 GBP"`); + expect(formatCurrency(100000, 'JPY')).toMatchInlineSnapshot( + `"100000 JPY"`, + ); + expect(formatCurrency(500, 'SEK')).toMatchInlineSnapshot(`"500 SEK"`); + expect(formatCurrency(1000, '')).toMatchInlineSnapshot(`"1000"`); + }); + + it('formats without currency', () => { + expect(formatCurrency(1000)).toMatchInlineSnapshot(`"1000"`); + expect(formatCurrency(1234.56)).toMatchInlineSnapshot(`"1234.56"`); + expect(formatCurrency(0)).toMatchInlineSnapshot(`"0"`); + expect(formatCurrency(-500)).toMatchInlineSnapshot(`"-500"`); + }); + + it('handles edge cases', () => { + expect(formatCurrency(0.01, 'USD')).toMatchInlineSnapshot(`"$0.01"`); + expect(formatCurrency(999999999, 'EUR')).toMatchInlineSnapshot( + `"€ 999 999 999"`, + ); + expect(formatCurrency(10.999, 'USD')).toMatchInlineSnapshot( + `"$10.999"`, + ); + expect(formatCurrency(10.999, 'EUR')).toMatchInlineSnapshot( + `"€ 10,999"`, + ); + }); +}); diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/formatCurrency.ts b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/formatCurrency.ts new file mode 100644 index 0000000000..3253d932de --- /dev/null +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/formatCurrency.ts @@ -0,0 +1,10 @@ +export const formatCurrency = (value: number, currency?: string) => { + if (currency === 'USD') { + return `$${value.toLocaleString('en-US')}`; + } + if (currency === 'EUR') { + return `€\u2009${value.toLocaleString('no-NO')}`; + } + + return `${value}${currency ? ' ' : ''}${currency || ''}`; +}; diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/types.ts b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/types.ts deleted file mode 100644 index 6e75515113..0000000000 --- a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/types.ts +++ /dev/null @@ -1,57 +0,0 @@ -export interface UsageMetric { - id: string; - label: string; - includedCurrent: number; - includedMax: number; - includedUnit: string; - actual?: string; - amount: number; -} - -export const defaultMetrics: UsageMetric[] = [ - { - id: 'frontend-traffic', - label: 'Frontend traffic', - includedCurrent: 10, - includedMax: 10, - includedUnit: 'M requests', - actual: '1,085M requests', - amount: 5425, - }, - { - id: 'service-connections', - label: 'Service connections', - includedCurrent: 7, - includedMax: 7, - includedUnit: 'connections', - actual: '20 connections', - amount: 0, - }, - { - id: 'release-templates', - label: 'Release templates', - includedCurrent: 3, - includedMax: 5, - includedUnit: 'templates', - amount: 0, - }, - { - id: 'edge-frontend-traffic', - label: 'Edge Frontend Traffic', - includedCurrent: 2, - includedMax: 10, - includedUnit: 'M requests', - amount: 0, - }, - { - id: 'edge-service-connections', - label: 'Edge Service Connections', - includedCurrent: 5, - includedMax: 5, - includedUnit: 'connections', - amount: 0, - }, -]; - -export const formatCurrency = (value: number) => - `$${value.toLocaleString('en-US')}`; diff --git a/frontend/src/hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts b/frontend/src/hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts index e195792117..70f9c87acf 100644 --- a/frontend/src/hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts +++ b/frontend/src/hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts @@ -25,9 +25,9 @@ export const useDetailedInvoices = (options: SWRConfiguration = {}) => { return { invoices: [ - // TODO:MOCK + // FIXME: MOCK { - status: 'paid', + status: 'upcoming', dueDate: '2023-09-01', invoiceDate: '2023-08-01', invoicePDF: 'https://example.com/invoice/1.pdf', @@ -38,7 +38,9 @@ export const useDetailedInvoices = (options: SWRConfiguration = {}) => { currency: 'USD', description: 'Service C', lookupKey: 'service-c', - quantity: 1, + quantity: 0, + consumption: 100, + limit: 120, totalAmount: 200, }, ], @@ -48,20 +50,31 @@ export const useDetailedInvoices = (options: SWRConfiguration = {}) => { description: 'Service A', lookupKey: 'service-a', quantity: 1, + consumption: 100, totalAmount: 100, }, { currency: 'USD', - description: 'Service B', + description: 'Backend streaming connections', lookupKey: 'service-b', - quantity: 100, - limit: 120, + quantity: 324_000, + limit: 3_000_000, + consumption: 3_000_000, totalAmount: 200, }, + { + currency: 'USD', + description: 'Frontend traffic bundle', + lookupKey: 'frontend-traffic-bundle', + quantity: 0, + consumption: 2_345_239, + limit: 5_000_000, + totalAmount: 0, + }, ], }, { - status: 'unpaid', + status: 'invoiced', dueDate: '2023-09-15', invoiceDate: '2023-08-15', invoicePDF: 'https://example.com/invoice/2.pdf', @@ -69,7 +82,7 @@ export const useDetailedInvoices = (options: SWRConfiguration = {}) => { totalAmount: 200, mainLines: [ { - currency: 'USD', + currency: 'EUR', description: 'Service C', lookupKey: 'service-c', quantity: 1,