Templates
Receipt and statement
One record's summary or a period's records on PrintShell; facts in a description list, figures in a table.
This block builds two document types inside PrintShell: one record’s summary with its line items (variant="receipt") and a period’s record statement (variant="statement").
Preview
Section titled “Preview”The brand and mode switches in the page header restyle the preview too. “Print” opens the browser’s print window.
pnpm add @bigbang/ui| Data | Type | Note |
|---|---|---|
variant |
'receipt' | 'statement' |
Summary with line items or period statement. |
| Line items | { quantity, unit }[] |
Amounts are written with Intl.NumberFormat and two decimals. |
| Reference | string |
Only the last four characters show; masking happens on the server. |
import { Button, DescriptionList, Heading, Icon, Stack, StatusBadge, Table, Text } from '@bigbang/ui';import type { StatusKey } from '@bigbang/ui';import type { Lang } from '../../patterns/lang';import { PrintShell } from '../../templates/print-shell/PrintShell';
const copy = { tr: { receipt: 'İşlem özeti', statement: 'Dönem dökümü', number: 'Belge no', date: 'Tarih', print: 'Yazdır', pdf: 'PDF olarak indir', details: 'Ayrıntılar', type: 'Kayıt türü', typeValue: 'Hizmet kaydı', status: 'Durum', owner: 'Oluşturan', ownerValue: 'Kullanıcı A', reference: 'Referans', lines: 'Kalemler', description: 'Açıklama', quantity: 'Adet', unit: 'Birim tutar', total: 'Toplam', grandTotal: 'Genel toplam', items: ['Hizmet A', 'Hizmet B', 'Hizmet C'], period: 'Dönem', periodValue: '1–31 Ekim 2026', records: 'Dönem kayıtları', record: 'Kayıt', footer: 'Bu belge elektronik olarak oluşturulmuştur. Doğrulama kodu: 7K2P-9QX4', }, en: { receipt: 'Record summary', statement: 'Period statement', number: 'Document no.', date: 'Date', print: 'Print', pdf: 'Download as PDF', details: 'Details', type: 'Record type', typeValue: 'Service record', status: 'Status', owner: 'Created by', ownerValue: 'User A', reference: 'Reference', lines: 'Line items', description: 'Description', quantity: 'Quantity', unit: 'Unit amount', total: 'Total', grandTotal: 'Grand total', items: ['Service A', 'Service B', 'Service C'], period: 'Period', periodValue: '1–31 October 2026', records: 'Records in the period', record: 'Record', footer: 'This document was created electronically. Verification code: 7K2P-9QX4', },};
const lineItems = [ { quantity: 2, unit: 500 }, { quantity: 1, unit: 1000 }, { quantity: 4, unit: 250 },];
const statementRows: { day: number; status: StatusKey; amount: number }[] = [ { day: 2, status: 'completed', amount: 1000 }, { day: 6, status: 'completed', amount: 250 }, { day: 11, status: 'cancelled', amount: 500 }, { day: 17, status: 'completed', amount: 750 }, { day: 23, status: 'pending', amount: 1200 }, { day: 29, status: 'completed', amount: 300 },];
export type ReceiptBlockProps = { lang?: Lang; /** `receipt` for one record with line items, `statement` for the records of a period. */ variant?: 'receipt' | 'statement';};
/** A printable document on PrintShell: facts as a description list, figures in a table, a masked reference. */export function ReceiptBlock({ lang = 'tr', variant = 'receipt' }: ReceiptBlockProps) { const t = copy[lang]; const locale = lang === 'tr' ? 'tr-TR' : 'en-GB'; const formatAmount = (value: number) => new Intl.NumberFormat(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value); const date = new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }); const isReceipt = variant === 'receipt'; const total = isReceipt ? lineItems.reduce((sum, item) => sum + item.quantity * item.unit, 0) : statementRows.filter((row) => row.status !== 'cancelled').reduce((sum, row) => sum + row.amount, 0);
return ( <PrintShell lang={lang} title={isReceipt ? t.receipt : t.statement} meta={`${t.number} BB-2026-000123 · ${t.date} ${date.format(new Date(2026, 9, 28))}`} footer={t.footer} actions={ <> <Button variant="secondary" size="sm" startIcon={<Icon name="file" />} onClick={() => window.print()}> {t.print} </Button> <Button variant="tertiary" size="sm" startIcon={<Icon name="download" />}> {t.pdf} </Button> </> } > <Stack gap="2xl"> <section aria-labelledby={`receipt-details-${lang}`}> <Stack gap="md"> <Heading level={2} size="sm" id={`receipt-details-${lang}`}> {t.details} </Heading> <DescriptionList layout="grid" columns={2} items={ isReceipt ? [ { term: t.type, details: t.typeValue }, { term: t.status, details: <StatusBadge status="completed" size="sm" /> }, { term: t.owner, details: t.ownerValue }, { term: t.reference, details: <Text numeric>•••• 1234</Text> }, ] : [ { term: t.period, details: t.periodValue }, { term: t.owner, details: t.ownerValue }, { term: t.reference, details: <Text numeric>•••• 1234</Text> }, { term: t.records, details: statementRows.length }, ] } /> </Stack> </section> {/* The table's scroll region already carries this name, so the section stays unnamed to keep landmarks unique. */} <section> <Stack gap="md"> <Heading level={2} size="sm" id={`receipt-lines-${lang}`}> {isReceipt ? t.lines : t.records} </Heading> {isReceipt ? ( <Table caption={t.lines} captionHidden density="compact"> <Table.Header> <Table.Row> <Table.HeaderCell>{t.description}</Table.HeaderCell> <Table.HeaderCell numeric>{t.quantity}</Table.HeaderCell> <Table.HeaderCell numeric>{t.unit}</Table.HeaderCell> <Table.HeaderCell numeric>{t.total}</Table.HeaderCell> </Table.Row> </Table.Header> <Table.Body> {lineItems.map((item, index) => ( <Table.Row key={t.items[index]}> <Table.RowHeader>{t.items[index]}</Table.RowHeader> <Table.Cell numeric>{item.quantity}</Table.Cell> <Table.Cell numeric>{formatAmount(item.unit)}</Table.Cell> <Table.Cell numeric>{formatAmount(item.quantity * item.unit)}</Table.Cell> </Table.Row> ))} </Table.Body> <Table.Footer> <Table.Row> <Table.RowHeader colSpan={3}>{t.grandTotal}</Table.RowHeader> <Table.Cell numeric>{formatAmount(total)}</Table.Cell> </Table.Row> </Table.Footer> </Table> ) : ( <Table caption={t.records} captionHidden density="compact"> <Table.Header> <Table.Row> <Table.HeaderCell>{t.date}</Table.HeaderCell> <Table.HeaderCell>{t.description}</Table.HeaderCell> <Table.HeaderCell>{t.status}</Table.HeaderCell> <Table.HeaderCell numeric>{t.total}</Table.HeaderCell> </Table.Row> </Table.Header> <Table.Body> {statementRows.map((row, index) => ( <Table.Row key={row.day}> <Table.Cell>{date.format(new Date(2026, 9, row.day))}</Table.Cell> <Table.RowHeader>{`${t.record} ${index + 1}`}</Table.RowHeader> <Table.Cell> <StatusBadge status={row.status} size="sm" /> </Table.Cell> <Table.Cell numeric>{formatAmount(row.amount)}</Table.Cell> </Table.Row> ))} </Table.Body> <Table.Footer> <Table.Row> <Table.RowHeader colSpan={3}>{t.grandTotal}</Table.RowHeader> <Table.Cell numeric>{formatAmount(total)}</Table.Cell> </Table.Row> </Table.Footer> </Table> )} </Stack> </section> </Stack> </PrintShell> );}Structure
Section titled “Structure”- A “Document actions” toolbar: Print and Download as PDF (hidden on paper).
- Header: brand,
h1title, document number and date. - “Details” section: a DescriptionList in grid layout; status as a StatusBadge.
- “Line items” or “Records in the period” section: a Table with right-aligned numeric columns and the total in
tfoot. - Footer: electronic document note and verification code.
Accessibility
Section titled “Accessibility”- Each section is a
sectionnamed by itsh2. - Tables are named by a hidden
caption; description cells are row headers (th scope="row"). - Numeric cells line up with
tabular-nums; amounts never count up. - A cancelled record stays out of the total and its status is read as text.
Components used
Section titled “Components used”DescriptionList, Table, StatusBadge, Button, Heading, Text. Patterns: Data export.