Mikkel Manniche
Dansk English Deutsch

Vorlage · React · HTML

Business finance

Ein Finanz-Dashboard für Unternehmen in Tiles: Einnahmen gegen Zahlungen, ein Wachstumsring gegen das Ziel, eine Aktien-Sparkline, Aktivität und eine Checkliste zur Verifizierung.

Vorschau

React

Installiere die Vorlage mit shadcn. Sie landet als normale Komponenten in deinem Projekt, die du selbst anpasst.

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/business-finance.json
npx shadcn@latest add @manniche/business-finance
business-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 BusinessFinance
business-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

Die ganze Seite als eine Datei mit eingebautem CSS und ohne JavaScript. Umschalter, Tabs und Filter funktionieren allein mit HTML und CSS.

In neuem Tab öffnen

Die Farbe in deiner eigenen App

Wenn shadcn bereits eingerichtet ist, ergeben diese Variablen dieselbe Farbe. Hell in :root, dunkel in .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);
}

Installieren

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/business-finance.json

Abhängigkeiten

    Dateien

    • business-finance.tsx
    • business-finance.html

    Manniche Lab · Open Source

    Kostenlose Werkzeuge für KI-Agenten und Webentwicklung.