dodil/erp-ui

src/gallery/main.tsx

/** The gallery: every erp-ui component, in the default theme, with realistic CRM data. `npm run dev`. */
import { Box, Button, Container, Flex, Heading, Section, Text } from "@radix-ui/themes";
import { useState } from "react";
import { createRoot } from "react-dom/client";

import {
  ActionButton, ConfirmDialog, DataTable, DetailPage, EmptyState, ErpTheme, ErrorState, FilterBar, FormDialog, KanbanBoard,
  Stat, StatGrid, Timeline, Toaster, formatMoney, sumDecimal, useToast, type Column,
} from "../index";

type Deal = { id: string; name: string; stage: string; amount: string };
const deals: Deal[] = [
  { id: "O-1", name: "Autumn print campaign", stage: "negotiation", amount: "48000.00" },
  { id: "O-2", name: "Lumen spring collection", stage: "proposal", amount: "24000.00" },
  { id: "O-3", name: "Baltic launch", stage: "proposal", amount: "42000.00" },
];
const stages = ["discovery", "proposal", "negotiation", "closed_won"];

function Actions() {
  const toast = useToast();
  return (
    <Flex gap="2" wrap="wrap">
      <FormDialog trigger={<Button>New deal</Button>} title="New deal" submitLabel="Open deal"
        fields={[{ name: "name", label: "Name", required: true }, { name: "amount", label: "Amount", kind: "money", currency: "EUR" },
                 { name: "close", label: "Expected close", kind: "date" }]}
        onSubmit={(v) => toast({ title: "Deal opened", description: `${v.name} · ${formatMoney(v.amount, "EUR")}` })} />
      <ConfirmDialog trigger={<Button color="red" variant="soft">Disqualify</Button>} title="Disqualify Karin Ek?" confirmLabel="Disqualify"
        tone="danger" reason={{ label: "Why" }} onConfirm={() => toast({ title: "Disqualified" })} />
      <ActionButton label="Approve discount" disabledReason="Only the deal desk approves discounts (quote:quote:approve)" />
    </Flex>
  );
}

type Partner = { bp_id: string; legal_name: string; country: string; status: string; open_pipeline: string; since: string };
const partners: Partner[] = [
  { bp_id: "54848654143250435", legal_name: "Acme AB", country: "SE", status: "active", open_pipeline: "124500.00", since: "2024-03-01" },
  { bp_id: "33077777037974209", legal_name: "Acme Group Ltd", country: "GB", status: "active", open_pipeline: "1850000.00", since: "2023-11-12" },
  { bp_id: "48281858453591338", legal_name: "Acme DE GmbH", country: "DE", status: "blocked", open_pipeline: "0", since: "2025-01-20" },
  { bp_id: "20998875476607981", legal_name: "Framework Co", country: "FR", status: "archived", open_pipeline: "9800.50", since: "2022-06-30" },
];
const columns: Column<Partner>[] = [
  { key: "legal_name", header: "Partner" },
  { key: "country", header: "Country" },
  { key: "status", header: "Status", kind: "status" },
  { key: "open_pipeline", header: "Open pipeline (EUR)", kind: "money" },
  { key: "since", header: "Customer since", kind: "date" },
];

function Gallery() {
  const [q, setQ] = useState("");
  const [board, setBoard] = useState(deals);
  const [status, setStatus] = useState<string | null>(null);
  const rows = partners.filter((p) => p.legal_name.toLowerCase().includes(q.toLowerCase()) && (!status || p.status === status));
  return (
    <Container size="4" p="5">
      <Heading size="7" mb="1">erp-ui</Heading>
      <Text color="gray">The core ERP components, in the default theme.</Text>
      <Section size="1">
        <Heading size="4" mb="3">List — FilterBar + DataTable</Heading>
        <Flex direction="column" gap="3">
          <FilterBar search={{ value: q, onChange: setQ, placeholder: "Search partners" }}
                     filters={[{ key: "status", label: "Status", value: status, onChange: setStatus,
                                 options: [{ value: "active", label: "Active" }, { value: "blocked", label: "Blocked" }, { value: "archived", label: "Archived" }] }]} />
          <DataTable label="Partners" rows={rows} columns={columns} rowKey={(r) => r.bp_id} onRowClick={() => {}} />
        </Flex>
      </Section>
      <Section size="1">
        <Heading size="4" mb="3">Record — DetailPage with another module's panel</Heading>
        <Box>
          <DetailPage
            title="Acme DE GmbH" subtitle="acme-de · DE · subsidiary of Acme Group Ltd" status="blocked"
            actions={[{ label: "Unblock", onClick: () => {}, tone: "primary" },
                      { label: "New quote", onClick: () => {}, disabledReason: "Acme DE GmbH is blocked: unpaid invoices" }]}
            sections={[{ title: "Partner", fields: [
              { label: "Tax id", value: "DE811234567" }, { label: "Country", value: "DE" },
              { label: "Roles", value: "customer, supplier" }, { label: "Open pipeline", value: "0", kind: "money", currency: "EUR" },
              { label: "Blocked since", value: "2026-10-02T09:12:00Z", kind: "datetime" }, { label: "Customer since", value: "2025-01-20", kind: "date" }] }]}
            panels={[{ title: "Timeline", content: <Timeline items={[
              { id: "1", kind: "CALL", title: "Rang Anna about the overdue invoice", body: "Promised payment by Friday.", at: "2026-10-14T10:20:00Z", actor: "tom@co.example" },
              { id: "2", kind: "TASK", title: "Check the payment landed", at: "2026-10-17T00:00:00Z", actor: "tom@co.example", status: "open" },
              { id: "3", kind: "EMAIL", title: "Sent the statement", at: "2026-10-02T08:00:00Z", actor: "finance@co.example" },
            ]} /> }]}
          />
        </Box>
      </Section>
      <Section size="1">
        <Heading size="4" mb="3">Numbers — StatGrid</Heading>
        <StatGrid label="Today">
          <Stat label="Overdue" value="3" tone="danger" hint="tasks" />
          <Stat label="Weighted" value={formatMoney(sumDecimal(board.map((d) => d.amount)), "EUR")} hint="exact, never a float" />
          <Stat label="Won" value="12,000.00 EUR" tone="success" />
          <Stat label="Open deals" value={board.length} />
        </StatGrid>
      </Section>
      <Section size="1">
        <Heading size="4" mb="3">Board — KanbanBoard (move with ⋯ or by dragging)</Heading>
        <KanbanBoard label="Pipeline" items={board} itemKey={(d) => d.id} columnOf={(d) => d.stage} itemLabel={(d) => d.name}
          columns={stages.map((s) => ({ id: s, title: s.replace("_", " "), closedReason: s === "closed_won" ? "needs an accepted quote" : undefined,
            footer: <Text size="1" color="gray">{formatMoney(sumDecimal(board.filter((d) => d.stage === s).map((d) => d.amount)), "EUR")}</Text> }))}
          renderCard={(d) => <Flex direction="column"><Text weight="medium">{d.name}</Text><Text size="2">{formatMoney(d.amount, "EUR")}</Text></Flex>}
          onMove={(d, to) => setBoard((b) => b.map((x) => (x.id === d.id ? { ...x, stage: to } : x)))} />
      </Section>
      <Section size="1">
        <Heading size="4" mb="3">Actions — FormDialog, ConfirmDialog, ActionButton, Toaster</Heading>
        <Actions />
      </Section>
      <Section size="1">
        <Heading size="4" mb="3">States — EmptyState, ErrorState</Heading>
        <Flex direction="column" gap="3">
          <EmptyState title="No leads yet" description="Marketing imports trade-show lists." action={<Button variant="soft">Import a list</Button>} />
          <ErrorState error="missing permission: quote:quote:approve" onRetry={() => {}} />
        </Flex>
      </Section>
    </Container>
  );
}

createRoot(document.getElementById("root")!).render(<ErpTheme><Toaster><Gallery /></Toaster></ErpTheme>);