/** * Showcase · table-grouped-subtotals — グループ集計 (V11) * * 勤怠の部署別グループ集計。グループヘッダー行(部署名 + 人数 + 右寄せ数値小計)と * 折りたたみ可能なグループ、最下部に総計行を備えた DataTable パターン。 * * GAP: DataTable には「グループ行 / 折りたたみグループ / グループ小計 / 総計フッター」が * ないため、real な Table ファミリ(Table/TableHeader/TableBody/TableRow/TableHead/ * TableCell)+ Button(折りたたみトグル) + Badge(人数/状態) + Card で COMPOSE している。 * 生の や手書きコントロールは一切使わず、全て @godxjp/ui プリミティブのみ。 * * DNA: compact 密度・小見出し・tabular-nums の数値列・固定カラーシグナル * (success 若竹 / warning 山吹 / attention 朱 = 遅刻 / info 群青)・静かな JP コピー・絵文字なし。 */ import * as React from "react"; import { ChevronDown, ChevronRight } from "lucide-react"; import { Button, Text } from "@godxjp/ui/general"; import { Badge, Card, CardAction, CardContent, CardHeader, CardTitle, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@godxjp/ui/data-display"; import { Flex, PageContainer } from "@godxjp/ui/layout"; // ── 型 ──────────────────────────────────────────────────────────────────────── type AttendanceStatus = "present" | "late" | "leave-early"; type Employee = { id: string; name: string; /** 勤務日数 */ days: number; /** 総労働時間(時間) */ work: number; /** 残業時間(時間) */ overtime: number; /** 遅刻回数 */ late: number; status: AttendanceStatus; }; type Group = { id: string; /** 部署名 */ label: string; members: Employee[]; }; // ── ダミーデータ(2026年5月度・部署別)───────────────────────────────────────── const GROUPS: Group[] = [ { id: "sales", label: "営業部", members: [ { id: "E-1042", name: "佐藤 健一", days: 21, work: 176.5, overtime: 12.0, late: 0, status: "present", }, { id: "E-1051", name: "鈴木 美咲", days: 20, work: 168.0, overtime: 4.5, late: 2, status: "late", }, { id: "E-1063", name: "高橋 涼", days: 21, work: 171.0, overtime: 8.0, late: 0, status: "present", }, { id: "E-1077", name: "田中 由美", days: 19, work: 152.5, overtime: 0.0, late: 0, status: "leave-early", }, ], }, { id: "dev", label: "開発部", members: [ { id: "E-2008", name: "伊藤 大輔", days: 21, work: 180.0, overtime: 22.5, late: 0, status: "present", }, { id: "E-2015", name: "渡辺 翔太", days: 21, work: 178.5, overtime: 18.0, late: 1, status: "late", }, { id: "E-2031", name: "山本 彩", days: 20, work: 165.0, overtime: 9.5, late: 0, status: "present", }, ], }, { id: "support", label: "カスタマーサポート部", members: [ { id: "E-3004", name: "中村 拓也", days: 22, work: 176.0, overtime: 6.0, late: 0, status: "present", }, { id: "E-3019", name: "小林 結衣", days: 20, work: 160.0, overtime: 2.0, late: 3, status: "late", }, ], }, ]; // ── フォーマッタ ──────────────────────────────────────────────────────────────── const hours = new Intl.NumberFormat("ja-JP", { minimumFractionDigits: 1, maximumFractionDigits: 1, }); const fmtH = (n: number) => `${hours.format(n)}h`; const STATUS_LABEL: Record = { present: "出勤", late: "遅刻あり", "leave-early": "早退あり", }; // 固定カラーシグナル: 出勤=success若竹 / 遅刻=attention(=warning 山吹) / 早退=info群青 const STATUS_TONE: Record = { present: "success", late: "warning", "leave-early": "info", }; type Subtotal = { days: number; work: number; overtime: number; late: number }; function subtotal(members: Employee[]): Subtotal { return members.reduce( (acc, m) => ({ days: acc.days + m.days, work: acc.work + m.work, overtime: acc.overtime + m.overtime, late: acc.late + m.late, }), { days: 0, work: 0, overtime: 0, late: 0 }, ); } function grandTotal(groups: Group[]): Subtotal { return groups.reduce( (acc, g) => { const s = subtotal(g.members); return { days: acc.days + s.days, work: acc.work + s.work, overtime: acc.overtime + s.overtime, late: acc.late + s.late, }; }, { days: 0, work: 0, overtime: 0, late: 0 }, ); } // ── グループヘッダー行(部署名 + 人数バッジ + 右寄せ数値小計 + 折りたたみ)────────── function GroupHeaderRow({ group, open, onToggle, }: { group: Group; open: boolean; onToggle: () => void; }) { const s = subtotal(group.members); const Chevron = open ? ChevronDown : ChevronRight; return ( `s with no single container to name. `aria-expanded` alone is complete and valid here — `aria-controls` is optional, and a broken one is worse than none. */ className="h-7 font-medium" >
従業員番号 氏名 / 部署 勤務日数 総労働 残業 遅刻 状態 {groups.map((g) => { const open = !closed.has(g.id); return ( toggle(g.id)} /> {open && g.members.map((m) => )} ); })} {/* 総計フッター行 */} 総計 · 全{groups.reduce((n, g) => n + g.members.length, 0)}名 {total.days}日 {fmtH(total.work)} {fmtH(total.overtime)} {total.late}回
); } export default function Demo() { return ( {/* 全展開(既定)— 各グループの小計と明細が同時に見える */} 2026年5月度 勤怠集計 締め: 2026-05-31 {/* 折りたたみ済みの状態を静的に提示 — 「カスタマーサポート部」は閉じて小計のみ表示 */} 折りたたみ状態 閉じたグループは小計のみ表示(クリックで展開) ); }