From 1e5de5b8b7f7b9d71c10f29a922da135a9218526 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Fri, 3 Oct 2025 09:24:27 +0200 Subject: [PATCH] feat: new invoice billing page view (#10721) Mockup of more detailed summary of used traffic on hosted offer. See previous PR #10718 --- .../BillingInvoice/BillingInvoice.tsx | 252 ++++++++++++++++++ .../BillingInvoiceRow/BillingInvoiceRow.tsx | 42 +++ .../ConsumptionIndicator.tsx | 18 ++ .../BillingInvoices/BillingInvoice/spec.md | 217 +++++++++++++++ .../BillingInvoices/BillingInvoice/types.ts | 57 ++++ .../BillingInvoices/BillingInvoices.tsx | 2 + .../PercentageCircle/PercentageDonut.tsx | 4 +- 7 files changed, 591 insertions(+), 1 deletion(-) create mode 100644 frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.tsx create mode 100644 frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceRow/BillingInvoiceRow.tsx create mode 100644 frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/ConsumptionIndicator/ConsumptionIndicator.tsx create mode 100644 frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/spec.md create mode 100644 frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/types.ts diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.tsx new file mode 100644 index 0000000000..bbff396045 --- /dev/null +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.tsx @@ -0,0 +1,252 @@ +import { + Typography, + styled, + Accordion, + AccordionSummary, + AccordionDetails, +} from '@mui/material'; +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 { 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[]; +}; + +const CardLikeAccordion = styled(Accordion)(({ theme }) => ({ + background: theme.palette.background.paper, + borderRadius: theme.shape.borderRadiusLarge, + boxShadow: theme.boxShadows.card, + '&:before': { display: 'none' }, + '&.MuiAccordion-root': { + margin: 0, + border: 'none', + }, +})); + +const HeaderRoot = styled(AccordionSummary)(({ theme }) => ({ + padding: theme.spacing(2, 4), + gap: theme.spacing(1.5), + '& .MuiAccordionSummary-content': { + margin: 0, + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1.5), + }, +})); + +const HeaderLeft = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1.5), + flex: 1, + minWidth: 0, +})); + +const HeaderRight = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(2), +})); + +const StyledInvoiceGrid = styled('div')(({ theme }) => ({ + display: 'grid', + gridTemplateColumns: '45% 20% 15% 20%', + + 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, + color: theme.palette.text.secondary, + padding: theme.spacing(0, 0, 1), +})); + +const TableBody: FC<{ children: ReactNode; title?: string }> = ({ + children, + title, +}) => { + return {children}; +}; + +const StyledSectionTitle = styled(Typography)(({ theme }) => ({ + gridColumn: '1 / -1', + padding: theme.spacing(2, 0), + fontWeight: theme.fontWeight.bold, +})); + +export const StyledTableRow = styled('div')(({ theme }) => ({ + display: 'grid', + gridColumn: '1 / -1', + gridTemplateColumns: 'subgrid', + padding: theme.spacing(1, 0), +})); + +const sectionsMock: BillingInvoiceSection[] = [ + { + 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 = ({ + title, + status, + sections = sectionsMock, +}: BillingInvoiceProps) => { + const total = sections.reduce( + (acc, section) => + acc + + section.items.reduce( + (itemAcc, item) => itemAcc + (item.amount || 0), + 0, + ), + 0, + ); + + return ( + + } + id={`billing-invoice-${title}-header`} + > + + + {title} + + + + {status === 'estimate' ? ( + Estimate + ) : null} + {status === 'upcoming' ? ( + Next invoice + ) : null} + {status === 'invoiced' ? ( + Invoiced + ) : null} + + {formatCurrency(total)} + + + + ({ + padding: theme.spacing(3, 0, 0), + borderTop: `1px solid ${theme.palette.divider}`, + })} + > + + + Description + Included + Quantity + Amount + + {sections.map((section) => ( + + {section.title ? ( + + {section.title} + + ) : null} + {section.items.map((item) => ( + + + + ))} + + ))} + + + + ); +}; diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceRow/BillingInvoiceRow.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceRow/BillingInvoiceRow.tsx new file mode 100644 index 0000000000..592e7b4450 --- /dev/null +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceRow/BillingInvoiceRow.tsx @@ -0,0 +1,42 @@ +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 }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), +})); + +export const BillingInvoiceRow = ({ + item, +}: { item: BillingInvoiceSectionItem }) => { + const usage = item.quantity || 0; + const percentage = + item.quota && item.quota > 0 + ? Math.min(100, Math.round((usage / item.quota) * 100)) + : undefined; + + return ( + <> +
{item.description}
+ + {percentage !== undefined && ( + + )} + {item.quota !== undefined + ? formatLargeNumbers(item.quota) + : '–'} + {percentage !== undefined ? ` (${percentage}%)` : ''} + +
+ {item.quantity !== undefined + ? formatLargeNumbers(item.quantity) + : '–'} +
+
{formatCurrency(item.amount || 0)}
+ + ); +}; diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/ConsumptionIndicator/ConsumptionIndicator.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/ConsumptionIndicator/ConsumptionIndicator.tsx new file mode 100644 index 0000000000..a0b03355d4 --- /dev/null +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/ConsumptionIndicator/ConsumptionIndicator.tsx @@ -0,0 +1,18 @@ +import type { FC } from 'react'; +import { PercentageDonut } from 'component/common/PercentageCircle/PercentageDonut'; + +type ConsumptionIndicatorProps = { + percentage: number; +}; + +export const ConsumptionIndicator: FC = ({ + percentage, +}) => { + return ( + + ); +}; diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/spec.md b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/spec.md new file mode 100644 index 0000000000..8648c911ba --- /dev/null +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/spec.md @@ -0,0 +1,217 @@ +# Page & container + +* **Canvas/background:** very light gray page (#EAEAED approx). +* **Main card:** centered, white background, full-width minus page padding, rounded corners (~12–16px), subtle shadow (1–2px blur, low opacity). +* **Outer padding:** ~24px around the card; ~16px vertical spacing between major blocks inside. + +# Header row (card top bar) + +* **Left:** page section title: **“October 15th”**. + + * Font: sans-serif UI (think Inter/ system). + * Size/weight: ~20–22px, semibold. + * Color: near-black (#202021). +* **Right:** total and controls, aligned on baseline with the title: + + * **Chip:** “Estimated” + + * Pill shape (full rounding ~9999px). + * Height ~24px; horizontal padding ~10–12px. + * Background: warm orange (#AD6321 to #BC8042 range). (warning) + * Text: small/uppercase or all-caps feel (~12px, medium), light cream text (#FDF4E6). + * **Grand total (top):** **$8,500** to the far right. + + * Size/weight: ~16px, bold/semibold; color dark gray (#4A4A4A). + * **Caret icon:** small chevron pointing **up** (expanded state) near far/right edge with hit target ~24px square. + +A thin divider isn’t drawn; the white card continues into the line-item table. + +# Line-item table header + +Four columns; left-aligned except Amount (right): + +1. **Description** (left) +2. **Included** +3. **Quantity** +4. **Amount** (right) + +* Header labels: small uppercase/medium weight (~12px), muted gray (#6E6E70). +* Column grid (desktop): + + * Description: ~40–45% + * Included: ~20% + * Quantity: ~15% + * Amount: ~25% (right-aligned) +* Row height baseline: ~56–64px for the item row (before usage block). + +# Line item: “Unleash PAYG Seat” + +* **Description column:** + + * Primary: “**Unleash PAYG Seat**” (~14–15px, medium, #909090). + * Secondary: “Sep 15 – Oct 15” (~12–13px, regular, #B6B6B7). +* **Included column:** empty for this item (the usage breakdown below covers entitlements). +* **Quantity:** **41** (center/left aligned to column). +* **Amount (right):** **$3,076** (right-aligned, ~14–15px, medium). + +# Usage block (nested section) + +A light panel under the line item spans full width of the card content. + +* **Container:** + + * Background: light neutral/blue-gray (#F7F7FA). + * Corner radius: matches card (~12px). + * Inner padding: ~16–20px. + * Top margin from the row above: ~12–16px. +* **Section title:** “**Usage September**” + + * Small, semibold (~13–14px), dark (#202021). + +Each usage metric is a **row with 4 columns** mirroring the header: **Label | Included | Actual | Amount**. + +### Metric rows (in order) + +1. **Frontend traffic** + + * **Included:** `10/10M requests` + + * Preceded by a small **circular progress ring** (see component spec below) in **accent purple**. + * **Actual:** `1,085M requests` + * **Amount:** `$5,425` + +2. **Service connections** + + * **Included:** `7/7 connections` (ring shows full/complete) + * **Actual:** `20 connections` + * **Amount:** `$0` + +3. **Release templates** + + * **Included:** `3/5 templates` (ring partially filled) + * **Actual:** *(empty / em dash not shown)* + * **Amount:** `$0` + +4. **Edge Frontend Traffic** + + * **Included:** `2/10M requests` (ring small partial) + * **Actual:** *(empty)* + * **Amount:** `$0` + +5. **Edge Service Connections** + + * **Included:** `5/5 connections` (ring full) + * **Actual:** *(empty)* + * **Amount:** `$0` + +* **Typography/colors inside usage rows:** + + * Labels (left): ~14–15px, dark (#202021). + * Included & Actual values: ~14px, regular, dark (#202021). + * Amounts on rows with $0: muted gray (~#818182) OR same dark but visual weight is from the value; non-zero amount ($5,425) uses dark color. +* **Row spacing:** ~12px vertical space between rows; no visible row borders. + +### Circular progress ring (Included column) + +* **Size:** ~18–20px outer diameter. +* **Stroke:** ~2–3px width. +* **Background track:** very light gray/lavender (#EAEAED to #F0F0F4). +* **Progress arc:** accent **purple/indigo** (appears around #6A5AE0 to #7B6EF6; treat as a single brand accent). +* **States:** + + * **Complete (7/7, 5/5, 10/10M):** arc forms a full ring; consider adding a subtle filled dot/gradient start (optional). + * **Partial (3/5, 2/10M):** arc angle proportional to current / max. + * **Empty:** (not shown here) would be track only. +* **Alignment:** the ring sits before the Included text with ~8px gap; ring and text are vertically centered. + +# Subtotals & totals (footer of card) + +* **Block container:** right side summary inside the same light usage panel’s parent (i.e., still white card). +* **Rows:** + + 1. **Sub total** — value **$8,500** + + * Label: small gray (#9B9CA0 / #D5D5D5 seen on screen due to anti-aliasing), ~13–14px. + * Value: right-aligned; medium weight; dark (#202021). + * Divider line below (hairline, #EAEAED). + 2. **Customer tax is exempt** + + * Single line, gray text (~13px), no value column, sits aligned to the label column, no icon. + 3. **Total** — value **$8,500** + + * Label: small label “Total”. + * Value: bold (~16px), right-aligned, dark (#202021). +* **Column behavior:** labels left, amounts right; the value column aligns with the table’s Amount column. + +# Spacing & rhythm (approx) + +* Title to table header: 16px. +* Table header to first row: 8–12px. +* Row vertical padding: 12–16px. +* Line item to usage panel: 12–16px. +* Inside usage panel: 14–16px around; 10–12px between rows. +* Usage panel to subtotal block: ~12–16px. +* Subtotal rows spacing: 8–12px; divider thickness 1px. + +# Colors (usable palette approximations) + +* **Text / primary:** #202021 +* **Text / secondary:** #6E6E70, #818182, #909090, #B6B6B7 +* **Background / page:** #EAEAED +* **Background / card:** #FFFFFF +* **Background / nested panel:** #F7F7FA +* **Border / hairline:** #EAEAED +* **Accent (progress rings):** purple/indigo ~#6A5AE0–#7B6EF6 (pick 1) +* **Chip (Estimated):** bg #AD6321–#BC8042 (warning), text #FDF4E6 + +# Alignment & responsiveness + +* **Four-column grid** collapses on small screens. Suggested: + + * Tablet: Description 50%, Included 25%, Quantity 10–15%, Amount 15–20%. + * Mobile: stack as: Description (with quantity & amount in a two-column subrow), then the usage block full-width below. +* **Amount column** is right-aligned everywhere. +* **Numbers** use thousands separators as shown: `1,085M`, currency with **$** and no decimals. +* **Date** uses en dash (–) between start and end. + +# Interactions & states + +* **Caret (collapse/expand):** toggles visibility of the line-item content (including the usage block and subtotal area). Rotates 180° to point down when collapsed. +* **Estimated chip:** non-interactive indicator; cursor default. +* **Usage rows:** non-interactive display; the rings are purely indicative (no hover shown). +* **Row hover (optional):** subtle background tint (#F9F9FB) or keep static. + +# Accessibility + +* **Color contrast:** ensure text vs. light panel meets WCAG AA (raise text color if needed). +* **Tab order:** title → chip → caret → table headers → row cells → usage rows → summary. +* **ARIA:** + + * Caret button: `aria-expanded` true/false. + * Progress rings: use `role="img"` with `aria-label` like “3 of 5 templates”. + +# Data in this example (verbatim) + +* **Header:** October 15th · Chip: Estimated · Top-right total: $8,500. +* **Columns:** Description | Included | Quantity | Amount. +* **Row:** Unleash PAYG Seat · Sep 15 – Oct 15 · Quantity 41 · Amount $3,076. +* **Usage September** (rows): + + * Frontend traffic · 10/10M requests · 1,085M requests · $5,425 + * Service connections · 7/7 connections · 20 connections · $0 + * Release templates · 3/5 templates · — · $0 + * Edge Frontend Traffic · 2/10M requests · — · $0 + * Edge Service Connections · 5/5 connections · — · $0 +* **Summary:** Sub total $8,500 · “Customer tax is exempt” · Total $8,500. + +# Components to build + +* **Card** (rounded, shadow). +* **Header bar** (title, chip, total, caret). +* **Four-column table** (responsive grid). +* **Usage panel** (light background, rounded). +* **Usage row** with **ProgressRing** + 3 texts + right amount. +* **Summary list** (two-column label/value with divider). +* **Pill/Chip** component for “Estimated”. + +If you want, I can translate this into a component tree and props next. diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/types.ts b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/types.ts new file mode 100644 index 0000000000..6e75515113 --- /dev/null +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/types.ts @@ -0,0 +1,57 @@ +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/component/admin/billing/BillingInvoices/BillingInvoices.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoices.tsx index 95146ce8ee..0f6166e25a 100644 --- a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoices.tsx +++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoices.tsx @@ -1,5 +1,6 @@ import { Box, styled, Typography } from '@mui/material'; import type { FC } from 'react'; +import { BillingInvoice } from './BillingInvoice/BillingInvoice.tsx'; const StyledContainer = styled(Box)(({ theme }) => ({ display: 'flex', @@ -20,6 +21,7 @@ export const BillingInvoices: FC = () => { return ( Usage and invoices + ); }; diff --git a/frontend/src/component/common/PercentageCircle/PercentageDonut.tsx b/frontend/src/component/common/PercentageCircle/PercentageDonut.tsx index 1598e51cd5..3194b3526e 100644 --- a/frontend/src/component/common/PercentageCircle/PercentageDonut.tsx +++ b/frontend/src/component/common/PercentageCircle/PercentageDonut.tsx @@ -27,6 +27,7 @@ type PercentageDonutProps = { disabled?: boolean | null; donut?: boolean; children?: ReactNode; + strokeRatio?: number; }; export const PercentageDonut = ({ @@ -34,6 +35,7 @@ export const PercentageDonut = ({ size = '4rem', disabled = false, children, + strokeRatio = 0.2, }: PercentageDonutProps) => { const theme = useTheme(); @@ -50,7 +52,7 @@ export const PercentageDonut = ({ // See https://stackoverflow.com/a/70659532. const r = 100 / (2 * Math.PI); const d = 2 * r; - const strokeWidth = d * 0.2; + const strokeWidth = d * strokeRatio; const color = disabled ? theme.palette.neutral.border