mirror of
https://github.com/Unleash/unleash.git
synced 2025-11-24 20:06:55 +01:00
feat: invoice sections (#10744)
Currency formatting and cleaner invoice components
This commit is contained in:
parent
fab5dc8725
commit
183d436e59
@ -10,6 +10,11 @@ export const StyledSubgrid = styled('div', {
|
|||||||
? theme.palette.background.elevation1
|
? theme.palette.background.elevation1
|
||||||
: 'transparent',
|
: 'transparent',
|
||||||
margin: theme.spacing(0.25, 0),
|
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,
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const StyledAmountCell = styled('div')(({ theme }) => ({
|
||||||
|
textAlign: 'right',
|
||||||
|
paddingRight: theme.spacing(1.5),
|
||||||
|
}));
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { FC, ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
Typography,
|
Typography,
|
||||||
styled,
|
styled,
|
||||||
@ -6,13 +7,12 @@ import {
|
|||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
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 { 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 { BillingInvoiceRow } from './BillingInvoiceRow/BillingInvoiceRow.tsx';
|
||||||
import { BillingInvoiceFooter } from './BillingInvoiceFooter/BillingInvoiceFooter.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 }) => ({
|
const CardLikeAccordion = styled(Accordion)(({ theme }) => ({
|
||||||
background: theme.palette.background.paper,
|
background: theme.palette.background.paper,
|
||||||
@ -73,7 +73,7 @@ const TableBody: FC<{ children: ReactNode; title?: string }> = ({
|
|||||||
|
|
||||||
const StyledSectionTitle = styled(Typography)(({ theme }) => ({
|
const StyledSectionTitle = styled(Typography)(({ theme }) => ({
|
||||||
gridColumn: '1 / -1',
|
gridColumn: '1 / -1',
|
||||||
padding: theme.spacing(2, 0),
|
padding: theme.spacing(2, 0, 1),
|
||||||
fontWeight: theme.fontWeight.bold,
|
fontWeight: theme.fontWeight.bold,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -84,69 +84,14 @@ const StyledTableRow = styled('div')(({ theme }) => ({
|
|||||||
padding: theme.spacing(1, 0),
|
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 = ({
|
export const BillingInvoice = ({
|
||||||
status,
|
status,
|
||||||
dueDate,
|
|
||||||
invoiceDate,
|
invoiceDate,
|
||||||
invoicePDF,
|
invoicePDF,
|
||||||
invoiceURL,
|
invoiceURL,
|
||||||
totalAmount,
|
totalAmount,
|
||||||
mainLines,
|
mainLines,
|
||||||
|
usageLines,
|
||||||
}: DetailedInvoicesSchemaInvoicesItem) => {
|
}: DetailedInvoicesSchemaInvoicesItem) => {
|
||||||
const title = invoiceDate
|
const title = invoiceDate
|
||||||
? new Date(invoiceDate).toLocaleDateString(undefined, {
|
? new Date(invoiceDate).toLocaleDateString(undefined, {
|
||||||
@ -155,6 +100,8 @@ export const BillingInvoice = ({
|
|||||||
})
|
})
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
const currency = mainLines[0]?.currency || usageLines?.[0]?.currency;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardLikeAccordion defaultExpanded>
|
<CardLikeAccordion defaultExpanded>
|
||||||
<HeaderRoot
|
<HeaderRoot
|
||||||
@ -181,7 +128,7 @@ export const BillingInvoice = ({
|
|||||||
<Badge color='success'>Invoiced</Badge>
|
<Badge color='success'>Invoiced</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
<Typography variant='body1' sx={{ fontWeight: 700 }}>
|
<Typography variant='body1' sx={{ fontWeight: 700 }}>
|
||||||
{formatCurrency(totalAmount)}
|
{formatCurrency(totalAmount, currency)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</HeaderRight>
|
</HeaderRight>
|
||||||
</HeaderRoot>
|
</HeaderRoot>
|
||||||
@ -196,31 +143,32 @@ export const BillingInvoice = ({
|
|||||||
<HeaderCell>Description</HeaderCell>
|
<HeaderCell>Description</HeaderCell>
|
||||||
<HeaderCell>Included</HeaderCell>
|
<HeaderCell>Included</HeaderCell>
|
||||||
<HeaderCell>Quantity</HeaderCell>
|
<HeaderCell>Quantity</HeaderCell>
|
||||||
<HeaderCell>Amount</HeaderCell>
|
<HeaderCell>
|
||||||
|
<StyledAmountCell>Amount</StyledAmountCell>
|
||||||
|
</HeaderCell>
|
||||||
</StyledSubgrid>
|
</StyledSubgrid>
|
||||||
{mainLines.map((line) => (
|
{mainLines.map((line) => (
|
||||||
<TableBody
|
<TableBody key={line.description}>
|
||||||
key={line.description}
|
|
||||||
// TODO: split into "usage" category
|
|
||||||
title={line.description}
|
|
||||||
>
|
|
||||||
{/* {line.description ? (
|
|
||||||
<StyledSectionTitle>
|
|
||||||
{line.description}
|
|
||||||
</StyledSectionTitle>
|
|
||||||
) : null} */}
|
|
||||||
<StyledTableRow key={line.description}>
|
<StyledTableRow key={line.description}>
|
||||||
<BillingInvoiceRow
|
<BillingInvoiceRow {...line} />
|
||||||
description={line.description}
|
|
||||||
quota={line.limit}
|
|
||||||
quantity={line.quantity}
|
|
||||||
amount={line.totalAmount}
|
|
||||||
/>
|
|
||||||
</StyledTableRow>
|
</StyledTableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
))}
|
))}
|
||||||
|
{usageLines.length ? (
|
||||||
|
<TableBody key='usage' title='Usage'>
|
||||||
|
<StyledSectionTitle>Usage</StyledSectionTitle>
|
||||||
|
{usageLines.map((line) => (
|
||||||
|
<StyledTableRow key={line.description}>
|
||||||
|
<BillingInvoiceRow {...line} />
|
||||||
|
</StyledTableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<BillingInvoiceFooter totalAmount={totalAmount} />
|
<BillingInvoiceFooter
|
||||||
|
totalAmount={totalAmount}
|
||||||
|
currency={currency}
|
||||||
|
/>
|
||||||
</StyledInvoiceGrid>
|
</StyledInvoiceGrid>
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</CardLikeAccordion>
|
</CardLikeAccordion>
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import { formatCurrency } from '../types.ts';
|
import { formatCurrency } from '../formatCurrency.ts';
|
||||||
import { StyledSubgrid } from '../BillingInvoice.styles.tsx';
|
import { StyledAmountCell, StyledSubgrid } from '../BillingInvoice.styles.tsx';
|
||||||
|
|
||||||
const StyledTableFooter = styled(StyledSubgrid)(({ theme }) => ({
|
const StyledTableFooter = styled(StyledSubgrid)(({ theme }) => ({
|
||||||
gridColumn: '3 / -1',
|
gridColumn: '3 / -1',
|
||||||
padding: theme.spacing(1, 0, 0),
|
padding: theme.spacing(1, 1, 0, 0),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledTableFooterRow = styled('div')<{ last?: boolean }>(
|
const StyledTableFooterRow = styled('div')<{ last?: boolean }>(
|
||||||
@ -27,18 +27,8 @@ const StyledTableFooterCell = styled('div', {
|
|||||||
...(colSpan ? { gridColumn: `span ${colSpan}` } : {}),
|
...(colSpan ? { gridColumn: `span ${colSpan}` } : {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface BillingInvoiceFooterProps {
|
const TaxRow: FC<{ value?: number }> = ({ value }) => {
|
||||||
subTotal?: number;
|
|
||||||
taxAmount?: number;
|
|
||||||
totalAmount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TaxRow: FC<{ value?: number | null }> = ({ value }) => {
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === null) {
|
|
||||||
return (
|
return (
|
||||||
<StyledTableFooterCell colSpan={2}>
|
<StyledTableFooterCell colSpan={2}>
|
||||||
Customer tax is exempt
|
Customer tax is exempt
|
||||||
@ -50,24 +40,34 @@ const TaxRow: FC<{ value?: number | null }> = ({ value }) => {
|
|||||||
<>
|
<>
|
||||||
<StyledTableFooterCell>Tax</StyledTableFooterCell>
|
<StyledTableFooterCell>Tax</StyledTableFooterCell>
|
||||||
<StyledTableFooterCell>
|
<StyledTableFooterCell>
|
||||||
{formatCurrency(value)}
|
<StyledAmountCell>{formatCurrency(value)}</StyledAmountCell>
|
||||||
</StyledTableFooterCell>
|
</StyledTableFooterCell>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BillingInvoiceFooterProps = {
|
||||||
|
subTotal?: number;
|
||||||
|
taxAmount?: number;
|
||||||
|
totalAmount: number;
|
||||||
|
currency?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const BillingInvoiceFooter = ({
|
export const BillingInvoiceFooter = ({
|
||||||
subTotal,
|
subTotal,
|
||||||
taxAmount,
|
taxAmount,
|
||||||
totalAmount,
|
totalAmount,
|
||||||
|
currency,
|
||||||
}: BillingInvoiceFooterProps) => {
|
}: BillingInvoiceFooterProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledTableFooter>
|
<StyledTableFooter>
|
||||||
{subTotal ? (
|
{subTotal || !taxAmount ? (
|
||||||
<StyledTableFooterRow>
|
<StyledTableFooterRow>
|
||||||
<StyledTableFooterCell>Sub total</StyledTableFooterCell>
|
<StyledTableFooterCell>Sub total</StyledTableFooterCell>
|
||||||
<StyledTableFooterCell>
|
<StyledTableFooterCell>
|
||||||
{formatCurrency(subTotal)}
|
<StyledAmountCell>
|
||||||
|
{formatCurrency(subTotal || totalAmount, currency)}
|
||||||
|
</StyledAmountCell>
|
||||||
</StyledTableFooterCell>
|
</StyledTableFooterCell>
|
||||||
</StyledTableFooterRow>
|
</StyledTableFooterRow>
|
||||||
) : null}
|
) : null}
|
||||||
@ -77,7 +77,9 @@ export const BillingInvoiceFooter = ({
|
|||||||
<StyledTableFooterRow last>
|
<StyledTableFooterRow last>
|
||||||
<StyledTableFooterCell>Total</StyledTableFooterCell>
|
<StyledTableFooterCell>Total</StyledTableFooterCell>
|
||||||
<StyledTableFooterCell>
|
<StyledTableFooterCell>
|
||||||
{formatCurrency(totalAmount)}
|
<StyledAmountCell>
|
||||||
|
{formatCurrency(totalAmount, currency)}
|
||||||
|
</StyledAmountCell>
|
||||||
</StyledTableFooterCell>
|
</StyledTableFooterCell>
|
||||||
</StyledTableFooterRow>
|
</StyledTableFooterRow>
|
||||||
</StyledTableFooter>
|
</StyledTableFooter>
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { formatLargeNumbers } from 'component/impact-metrics/metricsFormatters.ts';
|
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 { ConsumptionIndicator } from '../ConsumptionIndicator/ConsumptionIndicator.tsx';
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
|
import type { DetailedInvoicesLineSchema } from 'openapi';
|
||||||
|
import { StyledAmountCell } from '../BillingInvoice.styles.tsx';
|
||||||
|
|
||||||
const StyledCellWithIndicator = styled('div')(({ theme }) => ({
|
const StyledCellWithIndicator = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -14,34 +16,34 @@ type BillingInvoiceRowProps = {
|
|||||||
quantity?: number;
|
quantity?: number;
|
||||||
amount?: number;
|
amount?: number;
|
||||||
quota?: number;
|
quota?: number;
|
||||||
|
usage?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BillingInvoiceRow = ({
|
export const BillingInvoiceRow = ({
|
||||||
quantity,
|
quantity,
|
||||||
amount,
|
consumption,
|
||||||
quota,
|
limit,
|
||||||
description,
|
description,
|
||||||
}: BillingInvoiceRowProps) => {
|
currency,
|
||||||
const usage = quantity || 0;
|
totalAmount,
|
||||||
|
}: DetailedInvoicesLineSchema) => {
|
||||||
const percentage =
|
const percentage =
|
||||||
quota && quota > 0
|
limit && limit > 0
|
||||||
? Math.min(100, Math.round((usage / quota) * 100))
|
? Math.min(100, Math.round(((consumption || 0) / limit) * 100))
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>{description}</div>
|
<div>{description}</div>
|
||||||
<StyledCellWithIndicator>
|
<StyledCellWithIndicator>
|
||||||
{percentage !== undefined && (
|
<ConsumptionIndicator percentage={percentage || 0} />
|
||||||
<ConsumptionIndicator percentage={percentage} />
|
{limit !== undefined ? formatLargeNumbers(limit) : '–'}
|
||||||
)}
|
|
||||||
{quota !== undefined ? formatLargeNumbers(quota) : '–'}
|
|
||||||
{percentage !== undefined ? ` (${percentage}%)` : ''}
|
{percentage !== undefined ? ` (${percentage}%)` : ''}
|
||||||
</StyledCellWithIndicator>
|
</StyledCellWithIndicator>
|
||||||
<div>
|
<div>{quantity ? formatLargeNumbers(quantity) : '–'}</div>
|
||||||
{quantity !== undefined ? formatLargeNumbers(quantity) : '–'}
|
<StyledAmountCell>
|
||||||
</div>
|
{formatCurrency(totalAmount || 0, currency)}
|
||||||
<div>{formatCurrency(amount || 0)}</div>
|
</StyledAmountCell>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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 || ''}`;
|
||||||
|
};
|
||||||
@ -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')}`;
|
|
||||||
@ -25,9 +25,9 @@ export const useDetailedInvoices = (options: SWRConfiguration = {}) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
invoices: [
|
invoices: [
|
||||||
// TODO:MOCK
|
// FIXME: MOCK
|
||||||
{
|
{
|
||||||
status: 'paid',
|
status: 'upcoming',
|
||||||
dueDate: '2023-09-01',
|
dueDate: '2023-09-01',
|
||||||
invoiceDate: '2023-08-01',
|
invoiceDate: '2023-08-01',
|
||||||
invoicePDF: 'https://example.com/invoice/1.pdf',
|
invoicePDF: 'https://example.com/invoice/1.pdf',
|
||||||
@ -38,7 +38,9 @@ export const useDetailedInvoices = (options: SWRConfiguration = {}) => {
|
|||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
description: 'Service C',
|
description: 'Service C',
|
||||||
lookupKey: 'service-c',
|
lookupKey: 'service-c',
|
||||||
quantity: 1,
|
quantity: 0,
|
||||||
|
consumption: 100,
|
||||||
|
limit: 120,
|
||||||
totalAmount: 200,
|
totalAmount: 200,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -48,20 +50,31 @@ export const useDetailedInvoices = (options: SWRConfiguration = {}) => {
|
|||||||
description: 'Service A',
|
description: 'Service A',
|
||||||
lookupKey: 'service-a',
|
lookupKey: 'service-a',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
|
consumption: 100,
|
||||||
totalAmount: 100,
|
totalAmount: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
description: 'Service B',
|
description: 'Backend streaming connections',
|
||||||
lookupKey: 'service-b',
|
lookupKey: 'service-b',
|
||||||
quantity: 100,
|
quantity: 324_000,
|
||||||
limit: 120,
|
limit: 3_000_000,
|
||||||
|
consumption: 3_000_000,
|
||||||
totalAmount: 200,
|
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',
|
dueDate: '2023-09-15',
|
||||||
invoiceDate: '2023-08-15',
|
invoiceDate: '2023-08-15',
|
||||||
invoicePDF: 'https://example.com/invoice/2.pdf',
|
invoicePDF: 'https://example.com/invoice/2.pdf',
|
||||||
@ -69,7 +82,7 @@ export const useDetailedInvoices = (options: SWRConfiguration = {}) => {
|
|||||||
totalAmount: 200,
|
totalAmount: 200,
|
||||||
mainLines: [
|
mainLines: [
|
||||||
{
|
{
|
||||||
currency: 'USD',
|
currency: 'EUR',
|
||||||
description: 'Service C',
|
description: 'Service C',
|
||||||
lookupKey: 'service-c',
|
lookupKey: 'service-c',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user