1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00

feat: invoices ui improvements (#10813)

This commit is contained in:
Tymoteusz Czech 2025-10-16 14:30:01 +02:00 committed by GitHub
parent 045ef5a20e
commit b81691b89e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 128 additions and 328 deletions

View File

@ -15,14 +15,15 @@ const StyledWrapper = styled(Paper)(({ theme }) => ({
padding: theme.spacing(2),
borderRadius: theme.shape.borderRadiusLarge,
boxShadow: theme.boxShadows.card,
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1),
}));
const StyledRow = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
marginTop: theme.spacing(1),
fontSize: theme.typography.body2.fontSize,
gap: theme.spacing(1),
}));
const StyledItemTitle = styled('span')(({ theme }) => ({
@ -35,17 +36,16 @@ const StyledItemValue = styled('span')(({ theme }) => ({
}));
const StyledButton = styled(Button)(({ theme }) => ({
margin: theme.spacing(0, 0, 2, 0),
margin: theme.spacing(0, 0, 1, 0),
}));
const StyledInfoLabel = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
color: theme.palette.text.secondary,
marginBottom: theme.spacing(1),
}));
const StyledDivider = styled(Divider)(({ theme }) => ({
margin: `${theme.spacing(2.5)} 0`,
margin: theme.spacing(1.5, 0),
borderColor: theme.palette.divider,
}));
@ -70,6 +70,7 @@ export const BillingInfo: FC<BillingInfoProps> = () => {
if (!instanceStatus) {
return (
<StyledWrapper>
<Typography variant='h3'>Billing details</Typography>
<StyledInfoLabel>
Your billing is managed by Unleash
</StyledInfoLabel>
@ -86,6 +87,7 @@ export const BillingInfo: FC<BillingInfoProps> = () => {
if (isCustomBilling) {
return (
<StyledWrapper>
<Typography variant='h3'>Billing details</Typography>
<StyledInfoLabel>
Your billing is managed by Unleash
</StyledInfoLabel>
@ -123,11 +125,12 @@ export const BillingInfo: FC<BillingInfoProps> = () => {
>
{!inactive ? 'Edit billing details' : 'Add billing details'}
</StyledButton>
<StyledInfoLabel>
{inactive
? 'Once we have received your billing information we will upgrade your trial within 1 business day.'
: 'Update your credit card and business information and change which email address we send invoices to.'}
</StyledInfoLabel>
{inactive ? (
<StyledInfoLabel>
Once we have received your billing information we will
upgrade your trial within 1 business day.
</StyledInfoLabel>
) : null}
<GetInTouch />
</StyledWrapper>
);

View File

@ -9,13 +9,23 @@ export const StyledSubgrid = styled('div', {
background: withBackground
? theme.palette.background.elevation1
: 'transparent',
margin: theme.spacing(0.25, 0),
padding: withBackground ? theme.spacing(0, 2, 1) : theme.spacing(0, 2),
margin: theme.spacing(0.5, 0),
padding: withBackground
? theme.spacing(2, 2, 3)
: theme.spacing(0.5, 2, 1.5),
borderRadius: theme.shape.borderRadiusLarge,
gap: theme.spacing(1),
gap: theme.spacing(2),
}));
export const StyledAmountCell = styled('div')(({ theme }) => ({
textAlign: 'right',
paddingRight: theme.spacing(1.5),
}));
export const StyledDescriptionCell = styled('div', {
shouldForwardProp: (prop) => prop !== 'expand',
})<{ expand?: boolean }>(({ expand }) => ({
display: 'flex',
flexDirection: 'column',
gridColumn: expand ? '1 / span 2' : undefined,
}));

View File

@ -6,17 +6,18 @@ import {
AccordionSummary,
AccordionDetails,
Button,
Divider,
} from '@mui/material';
import ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined';
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { formatCurrency } from './formatCurrency.ts';
import { Badge } from 'component/common/Badge/Badge.tsx';
import { BillingInvoiceMainRow } from './BillingInvoiceMainRow/BillingInvoiceMainRow.tsx';
import { BillingInvoiceUsageRow } from './BillingInvoiceUsageRow/BillingInvoiceUsageRow.tsx';
import { BillingInvoiceFooter } from './BillingInvoiceFooter/BillingInvoiceFooter.tsx';
import { StyledAmountCell, StyledSubgrid } from './BillingInvoice.styles.tsx';
import type { DetailedInvoicesSchemaInvoicesItem } from 'openapi';
import { BillingInvoiceUsageRow } from './BillingInvoiceUsageRow/BillingInvoiceUsageRow.tsx';
import { BillingInvoiceMainRow } from './BillingInvoiceMainRow/BillingInvoiceMainRow.tsx';
const StyledAccordion = styled(Accordion)(({ theme }) => ({
background: theme.palette.background.paper,
@ -37,6 +38,9 @@ const HeaderRoot = styled(AccordionSummary)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1.5),
'&.Mui-expanded': {
margin: 0,
},
},
}));
@ -57,15 +61,13 @@ const HeaderRight = styled('div')(({ theme }) => ({
const StyledInvoiceGrid = styled('div')(({ theme }) => ({
display: 'grid',
gridTemplateColumns: '45% 20% 15% 20%',
padding: theme.spacing(0, 2, 3),
padding: theme.spacing(0, 2, 2),
}));
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 }> = ({
@ -75,24 +77,27 @@ const TableBody: FC<{ children: ReactNode; title?: string }> = ({
return <StyledSubgrid withBackground={!!title}>{children}</StyledSubgrid>;
};
const StyledSectionTitle = styled(Typography)(({ theme }) => ({
gridColumn: '1 / -1',
padding: theme.spacing(2, 0, 1),
fontWeight: theme.fontWeight.bold,
}));
const StyledTableRow = styled('div')(({ theme }) => ({
display: 'grid',
gridColumn: '1 / -1',
gridTemplateColumns: 'subgrid',
padding: theme.spacing(1, 0),
}));
const StyledTableTitle = styled('span')(({ theme }) => ({
color: theme.palette.text.primary,
fontSize: theme.typography.body1.fontSize,
}));
const StyledDivider = styled(Divider)(({ theme }) => ({
gridColumn: '1 / -1',
margin: theme.spacing(0, 2),
}));
const CardActions = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'flex-end',
gap: theme.spacing(1),
padding: theme.spacing(1.5, 2, 2),
padding: theme.spacing(0, 2, 2),
}));
type BillingInvoiceProps = DetailedInvoicesSchemaInvoicesItem &
@ -118,7 +123,11 @@ export const BillingInvoice = ({
})
: '';
const hasLimitsColumn = usageLines.some((line) => line.limit);
const isCurrentYear =
new Date(invoiceDate).getFullYear() === new Date().getFullYear();
const year = isCurrentYear
? `, ${new Date(invoiceDate).getFullYear()}`
: '';
return (
<StyledAccordion defaultExpanded={Boolean(defaultExpanded)}>
@ -133,6 +142,7 @@ export const BillingInvoice = ({
sx={{ fontWeight: 700 }}
>
{formattedTitle}
{year}
</Typography>
</HeaderLeft>
<HeaderRight>
@ -155,35 +165,35 @@ export const BillingInvoice = ({
</HeaderRoot>
<AccordionDetails
sx={(theme) => ({
padding: theme.spacing(3, 0, 0),
padding: theme.spacing(2, 0, 0),
borderTop: `1px solid ${theme.palette.divider}`,
})}
>
<StyledInvoiceGrid>
<StyledSubgrid>
<HeaderCell>Description</HeaderCell>
<HeaderCell />
<HeaderCell>Quantity</HeaderCell>
<HeaderCell>
<StyledAmountCell>Amount</StyledAmountCell>
</HeaderCell>
</StyledSubgrid>
{mainLines.map((line) => (
<TableBody key={line.description}>
<TableBody>
<StyledTableRow>
<HeaderCell>Description</HeaderCell>
<HeaderCell />
<HeaderCell>Quantity</HeaderCell>
<HeaderCell>
<StyledAmountCell>Amount</StyledAmountCell>
</HeaderCell>
</StyledTableRow>
{mainLines.map((line) => (
<StyledTableRow key={line.description}>
<BillingInvoiceMainRow {...line} />
</StyledTableRow>
</TableBody>
))}
))}
</TableBody>
{usageLines.length ? (
<TableBody key='usage' title='Usage'>
<StyledTableRow>
<HeaderCell>Usage {monthText}</HeaderCell>
{hasLimitsColumn ? (
<HeaderCell>Included</HeaderCell>
) : (
<HeaderCell />
)}
<HeaderCell>
<StyledTableTitle>
Usage {monthText}
</StyledTableTitle>
</HeaderCell>
<HeaderCell>Included</HeaderCell>
<HeaderCell>Overages</HeaderCell>
<HeaderCell>
<StyledAmountCell>Amount</StyledAmountCell>
@ -191,44 +201,45 @@ export const BillingInvoice = ({
</StyledTableRow>
{usageLines.map((line) => (
<StyledTableRow key={line.description}>
<BillingInvoiceUsageRow
{...line}
showLimits={hasLimitsColumn}
/>
<BillingInvoiceUsageRow {...line} />
</StyledTableRow>
))}
</TableBody>
) : null}
) : (
<StyledDivider />
)}
<BillingInvoiceFooter
totalAmount={totalAmount}
currency={currency}
/>
</StyledInvoiceGrid>
<CardActions>
{invoiceURL ? (
<Button
variant='outlined'
href={invoiceURL}
target='_blank'
rel='noreferrer'
startIcon={<ReceiptLongOutlinedIcon />}
>
View invoice
</Button>
) : null}
{invoicePDF ? (
<Button
variant='outlined'
href={invoicePDF}
target='_blank'
rel='noreferrer'
startIcon={<DownloadOutlinedIcon />}
>
Download PDF
</Button>
) : null}
</CardActions>
{invoiceURL || invoicePDF ? (
<CardActions>
{invoiceURL ? (
<Button
variant='outlined'
href={invoiceURL}
target='_blank'
rel='noreferrer'
startIcon={<ReceiptLongOutlinedIcon />}
>
View invoice
</Button>
) : null}
{invoicePDF ? (
<Button
variant='outlined'
href={invoicePDF}
target='_blank'
rel='noreferrer'
startIcon={<DownloadOutlinedIcon />}
>
Download PDF
</Button>
) : null}
</CardActions>
) : null}
</AccordionDetails>
</StyledAccordion>
);

View File

@ -6,6 +6,7 @@ import { StyledAmountCell, StyledSubgrid } from '../BillingInvoice.styles.tsx';
const StyledTableFooter = styled(StyledSubgrid)(({ theme }) => ({
gridColumn: '3 / -1',
padding: theme.spacing(1, 1, 0, 0),
gap: 0,
}));
const StyledTableFooterRow = styled('div')<{ last?: boolean }>(
@ -17,13 +18,14 @@ const StyledTableFooterRow = styled('div')<{ last?: boolean }>(
...(last
? { fontWeight: theme.typography.fontWeightBold }
: { borderBottom: `1px solid ${theme.palette.divider}` }),
padding: theme.spacing(1.25, 0),
}),
);
const StyledTableFooterCell = styled('div', {
shouldForwardProp: (prop) => prop !== 'colSpan',
})<{ colSpan?: number }>(({ theme, colSpan }) => ({
padding: theme.spacing(1, 0, 1, 0.5),
padding: theme.spacing(0, 0, 0, 0.5),
...(colSpan ? { gridColumn: `span ${colSpan}` } : {}),
}));

View File

@ -2,17 +2,14 @@ import { formatLargeNumbers } from 'component/impact-metrics/metricsFormatters.t
import { formatCurrency } from '../formatCurrency.ts';
import { styled, Typography } from '@mui/material';
import type { DetailedInvoicesLineSchema } from 'openapi';
import { StyledAmountCell } from '../BillingInvoice.styles.tsx';
const StyledDescriptionCell = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(0.5),
}));
import {
StyledAmountCell,
StyledDescriptionCell,
} from '../BillingInvoice.styles.tsx';
const StyledSubText = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
fontSize: theme.typography.body2.fontSize,
fontSize: theme.typography.caption.fontSize,
}));
export const BillingInvoiceMainRow = ({
@ -38,7 +35,7 @@ export const BillingInvoiceMainRow = ({
return (
<>
<StyledDescriptionCell>
<StyledDescriptionCell expand>
<div>{description}</div>
{formattedStart || formattedEnd ? (
<StyledSubText>
@ -46,7 +43,6 @@ export const BillingInvoiceMainRow = ({
</StyledSubText>
) : null}
</StyledDescriptionCell>
<div />
<div>{quantity ? formatLargeNumbers(quantity) : ''}</div>
<StyledAmountCell>
{formatCurrency(totalAmount || 0, currency)}

View File

@ -3,18 +3,18 @@ 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';
import {
StyledAmountCell,
StyledDescriptionCell,
} from '../BillingInvoice.styles.tsx';
const StyledCellWithIndicator = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
maxHeight: theme.spacing(2.5),
}));
type BillingInvoiceRowProps = DetailedInvoicesLineSchema & {
showLimits: boolean;
};
export const BillingInvoiceUsageRow = ({
quantity,
consumption,
@ -22,8 +22,7 @@ export const BillingInvoiceUsageRow = ({
description,
currency,
totalAmount,
showLimits,
}: BillingInvoiceRowProps) => {
}: DetailedInvoicesLineSchema) => {
const percentage =
limit && limit > 0
? Math.min(100, Math.round(((consumption || 0) / limit) * 100))
@ -31,23 +30,19 @@ export const BillingInvoiceUsageRow = ({
return (
<>
<div>{description}</div>
{showLimits ? (
<StyledCellWithIndicator>
<ConsumptionIndicator percentage={percentage || 0} />
<div>
{consumption !== undefined && limit !== undefined
? `${formatLargeNumbers(consumption)}/${formatLargeNumbers(limit)}`
: consumption !== undefined
? formatLargeNumbers(consumption)
: limit !== undefined
? formatLargeNumbers(limit)
: ''}
</div>
</StyledCellWithIndicator>
) : (
<StyledCellWithIndicator />
)}
<StyledDescriptionCell>{description}</StyledDescriptionCell>
<StyledCellWithIndicator>
<ConsumptionIndicator percentage={percentage || 0} />
<div>
{consumption !== undefined && limit !== undefined
? `${formatLargeNumbers(consumption)}/${formatLargeNumbers(limit)}`
: consumption !== undefined
? formatLargeNumbers(consumption)
: limit !== undefined
? formatLargeNumbers(limit)
: ''}
</div>
</StyledCellWithIndicator>
<div>{quantity ? formatLargeNumbers(quantity) : ''}</div>
<StyledAmountCell>
{formatCurrency(totalAmount || 0, currency)}

View File

@ -1,217 +0,0 @@
# Page & container
* **Canvas/background:** very light gray page (#EAEAED approx).
* **Main card:** centered, white background, full-width minus page padding, rounded corners (~1216px), subtle shadow (12px 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: ~2022px, 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 ~1012px.
* 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 isnt 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: ~4045%
* Included: ~20%
* Quantity: ~15%
* Amount: ~25% (right-aligned)
* Row height baseline: ~5664px for the item row (before usage block).
# Line item: “Unleash PAYG Seat”
* **Description column:**
* Primary: “**Unleash PAYG Seat**” (~1415px, medium, #909090).
* Secondary: “Sep 15 Oct 15” (~1213px, 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, ~1415px, 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: ~1620px.
* Top margin from the row above: ~1216px.
* **Section title:** “**Usage September**”
* Small, semibold (~1314px), 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): ~1415px, 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:** ~1820px outer diameter.
* **Stroke:** ~23px 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 panels 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), ~1314px.
* 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 tables Amount column.
# Spacing & rhythm (approx)
* Title to table header: 16px.
* Table header to first row: 812px.
* Row vertical padding: 1216px.
* Line item to usage panel: 1216px.
* Inside usage panel: 1416px around; 1012px between rows.
* Usage panel to subtotal block: ~1216px.
* Subtotal rows spacing: 812px; 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 1015%, Amount 1520%.
* 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.

View File

@ -7,7 +7,7 @@ import { TablePlaceholder } from 'component/common/Table';
const StyledContainer = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(3),
gap: theme.spacing(2),
}));
export const BillingInvoices: FC = () => {