/**
* 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 (