Skabelon · React · HTML
Business finance
Et økonomidashboard til en virksomhed i Tiles: indtægter mod betalinger, en vækstring mod målet, en aktie-sparkline, aktivitet og en tjekliste til verificering.
Forhåndsvisning
React
Installér skabelonen med shadcn. Den lægges i dit projekt som almindelige komponenter, som du selv retter i.
npx shadcn@latest add https://mikkelmanniche.dk/lab/r/business-finance.jsonnpx shadcn@latest add @manniche/business-financebusiness-finance.tsx
// A business finance screen in Tiles: income against payments, a growth ring, a stock sparkline, activity and a
// verification checklist.
import { cn } from '@/lib/utils'
import { BigNumber, DeltaPill } from '@/registry/manniche/chart-kit/chart-kit'
import type { ValueFormat } from '@/registry/manniche/chart-kit/chart-utils'
import { DataTile, TileLegend } from '@/registry/manniche/data-tile/data-tile'
import { AreaChart, type AreaPoint } from '@/registry/manniche/area-chart/area-chart'
import { Donut } from '@/registry/manniche/donut/donut'
import { Sparkline } from '@/registry/manniche/sparkline/sparkline'
import { TransactionList, type Transaction } from '@/registry/manniche/transaction-list/transaction-list'
export type VerificationStep = { id: string; label: string; done: boolean; hint?: string }
export type BusinessFinanceProps = {
data: {
/** Income per period, with `previous` holding payments for the same period. */
cashflow: AreaPoint[]
income: number
incomeChange: number
/** Growth toward the target, 0 to 1. */
growth: number
growthTarget: string
stock: { label: string; value: number }[]
stockName: string
activity: Transaction[]
verification: VerificationStep[]
}
format?: ValueFormat
title?: string
note?: string
today?: string
/** The current time. Used for `today` when that is not given. */
now?: Date
labels?: { income?: string; payments?: string; growth?: string; /** Under the share in the growth ring. Default "of target". */ ofTarget?: string; stock?: string; activity?: string; verification?: string; stepsDone?: (done: number, total: number) => string; done?: string; todo?: string }
className?: string
}
const DEFAULT_FORMAT: ValueFormat = { currency: 'EUR', decimals: 0 }
/** Business finance: cash flow, growth ring, stock sparkline, activity and a verification checklist. */
// "YYYY-MM-DD" of `now` in local time, for TransactionList's "Today" and "Yesterday".
const dayOf = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
export function BusinessFinance({ data, format = DEFAULT_FORMAT, title = 'Business finance', note = 'Example data.', today, now, labels, className }: BusinessFinanceProps) {
const day = today ?? (now ? dayOf(now) : undefined)
const l = {
income: 'Income',
payments: 'Payments',
growth: 'Growth',
ofTarget: 'of target',
stock: 'Stock',
activity: 'Activity',
verification: 'Verification',
stepsDone: (d: number, t: number) => `${d} of ${t} steps done`,
done: 'Done',
todo: 'To do',
...labels,
}
const doneCount = data.verification.filter((s) => s.done).length
const pct = Math.round(data.growth * 100)
return (
<div className={cn('@container w-full font-sans text-foreground', className)}>
<div className="grid gap-4 @3xl:grid-cols-6 @6xl:grid-cols-12">
<h1 className="text-[22px] leading-tight font-semibold tracking-tight @3xl:col-span-6 @6xl:col-span-12">{title}</h1>
<DataTile
title={`${l.income} / ${l.payments}`}
className="@3xl:col-span-6 @6xl:col-span-8"
footer={<span>{note}</span>}
action={<TileLegend items={[{ label: l.income, color: 'var(--chart-1)' }, { label: l.payments, color: 'var(--chart-3)', shape: 'dashed' }]} />}
>
<div className="flex flex-wrap items-center gap-3">
<BigNumber value={data.income} format={format} size="lg" />
<DeltaPill value={data.incomeChange} format={{ decimals: 1, suffix: '%', sign: true }} />
</div>
<AreaChart className="mt-4" data={data.cashflow} label={`${l.income} and ${l.payments}`} format={format} compare labels={{ previous: l.payments }} />
</DataTile>
<DataTile title={l.growth} inverted className="@3xl:col-span-3 @6xl:col-span-4">
{/* One segment against a whole of 100, held highlighted so the centre reads the share of the target rather
than "Total 100%". */}
<Donut
data={[{ label: l.growth, value: pct }]}
total={100}
activeIndex={0}
label={`${l.growth}, ${pct} percent of ${data.growthTarget}`}
format={{ suffix: '%' }}
labels={{ ofTotal: l.ofTarget }}
/>
</DataTile>
<DataTile title={l.stock} className="@3xl:col-span-3 @6xl:col-span-4">
<p className="text-sm font-medium text-muted-foreground">{data.stockName}</p>
<Sparkline className="mt-3" data={data.stock} label={`${l.stock}, ${data.stockName}`} height={72} />
</DataTile>
<DataTile title={l.verification} className="@3xl:col-span-3 @6xl:col-span-4" footer={<span>{l.stepsDone(doneCount, data.verification.length)}</span>}>
<ol className="space-y-3">
{data.verification.map((s) => (
<li key={s.id} className="flex items-start gap-3 text-sm">
<span
aria-hidden
className={cn('mt-0.5 grid size-5 shrink-0 place-items-center rounded-full border text-[11px]', s.done ? 'border-transparent bg-foreground text-background' : 'border-border text-transparent')}
>
✓
</span>
<span>
<span className="font-medium">{s.label}</span>
<span className="sr-only"> ({s.done ? l.done : l.todo})</span>
{s.hint && <span className="block text-xs text-muted-foreground">{s.hint}</span>}
</span>
</li>
))}
</ol>
</DataTile>
<DataTile title={l.activity} className="@3xl:col-span-6 @6xl:col-span-4">
<TransactionList data={data.activity} label={l.activity} today={day} format={{ ...format, decimals: 2 }} limit={4} />
</DataTile>
</div>
</div>
)
}
export default BusinessFinancebusiness-finance-demo.tsx
import { BusinessFinance } from '@/components/business-finance'
// Example data for the demo, not a real business. "Today" is Thursday 8 October 2026.
const months = ['Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct']
const income = [18200, 19400, 21000, 20100, 22800, 24100, 25600]
const payments = [14100, 15200, 15900, 16800, 17100, 18400, 19000]
const data = {
cashflow: months.map((label, i) => ({ label, value: income[i], previous: payments[i] })),
income: 25600,
incomeChange: 6.2,
growth: 0.68,
growthTarget: 'Yearly target',
stockName: 'Example Holdings (fictional)',
stock: [41, 43, 42, 46, 45, 49, 48, 52, 51, 55].map((value, i) => ({ value, label: `Week ${i + 1}` })),
activity: [
{ id: 'a1', name: 'Invoice 1042, Birch & Co', category: 'Invoice', date: '2026-10-08T10:12', amount: 2400 },
{ id: 'a2', name: 'Office rent', category: 'Rent', date: '2026-10-07T07:00', amount: -1350 },
{ id: 'a3', name: 'Supplier, Harbour Print', category: 'Supplies', date: '2026-10-06T15:30', amount: -412.5, status: 'pending' as const },
{ id: 'a4', name: 'Invoice 1041, Lumen Studio', category: 'Invoice', date: '2026-10-05T09:02', amount: 1800 },
],
verification: [
{ id: 'v1', label: 'Company details', done: true },
{ id: 'v2', label: 'Owner identity', done: true },
{ id: 'v3', label: 'Business address', done: true, hint: 'Confirmed by letter' },
{ id: 'v4', label: 'Bank statement', done: false, hint: 'Upload the last three months' },
],
}
export default function BusinessFinanceDemo() {
return <BusinessFinance data={data} today="2026-10-08" />
}HTML
Hele siden som én fil med al CSS indbygget og intet JavaScript. Knapper, faner og filtre virker med HTML og CSS alene.
Farven i din egen app
Har du allerede shadcn sat op, giver disse variabler samme farve. Lys i :root, mørk i .dark.
:root {
--primary: oklch(0.52 0.2 262);
--primary-foreground: oklch(0.99 0 0);
--accent: oklch(0.95 0.034 262);
--accent-foreground: oklch(0.36 0.140 262);
--ring: oklch(0.52 0.2 262);
}
.dark {
--primary: oklch(0.74 0.150 262);
--primary-foreground: oklch(0.18 0.024 262);
--accent: oklch(0.29 0.050 262);
--accent-foreground: oklch(0.9 0.050 262);
--ring: oklch(0.74 0.150 262);
}Installér
npx shadcn@latest add https://mikkelmanniche.dk/lab/r/business-finance.json