/**
* Showcase · table-compact-kintone — 高密度グリッド (V18)
*
* kintone 風の高密度グリッド: 28px 行 (compact density)・1px の薄い罫線・多列・
* 数値列は tabular-nums。日次勤怠サマリを 1 画面で俯瞰する管理者向けレイアウト。
*
* 構成マップ (kintone のブロック → 実 @godxjp/ui プリミティブ):
* ページ枠 ........... PageContainer (density="compact", 小さい見出し)
* ツールバー ......... DataTable.Toolbar + DensityToggle + Button
* 一括操作 ........... DataTable.BulkActions (選択 > 0 で表示)
* 多列グリッド ....... DataTable.Content (compact = 28px 行・薄い罫線)
* 状態セル ........... Badge tone (success 若竹 / warning 山吹 / destructive 茜 / info 群青)
* 数値列 ............. tabular-nums (出勤/退勤/実働/残業/遅刻分)
* ページネーション ... DataTable.Pagination
*
* DNA: compact 28px 密度・固定カラーシグナリング・tabular 数値・小さい見出し・
* 静かな日本語コピー・絵文字なし。すべて実プリミティブ。
*/
import * as React from "react";
import { Badge, DataTable, type ColumnDef } from "@godxjp/ui/data-display";
import { Button, Text } from "@godxjp/ui/general";
import { Flex, PageContainer } from "@godxjp/ui/layout";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@godxjp/ui/navigation";
import type { DensityProp, SortStateProp } from "@godxjp/ui/props";
import { Check, MoreHorizontal, Pencil } from "lucide-react";
// ── 勤怠データ (日次・kintone のレコード相当) ────────────────────────────────────
type Attendance = {
id: string; // 従業員番号
name: string; // 氏名
dept: string; // 部署
shift: string; // シフト
clockIn: string; // 出勤
clockOut: string; // 退勤
work: number; // 実働 (分)
overtime: number; // 残業 (分)
late: number; // 遅刻 (分)
early: number; // 早退 (分)
status: AttendanceStatus; // 勤怠区分
approved: ApprovalState; // 承認
};
type AttendanceStatus = "present" | "late" | "early" | "leave" | "absent";
type ApprovalState = "approved" | "pending" | "rejected";
const STATUS_META: Record<
AttendanceStatus,
{ label: string; tone: "success" | "warning" | "destructive" | "info" }
> = {
present: { label: "出勤", tone: "success" }, // 若竹
late: { label: "遅刻", tone: "warning" }, // 山吹
early: { label: "早退", tone: "warning" }, // 山吹
leave: { label: "有給", tone: "info" }, // 群青
absent: { label: "欠勤", tone: "destructive" }, // 茜
};
const APPROVAL_META: Record<
ApprovalState,
{ label: string; tone: "success" | "warning" | "destructive" }
> = {
approved: { label: "承認済", tone: "success" },
pending: { label: "未承認", tone: "warning" },
rejected: { label: "差戻し", tone: "destructive" },
};
const ROWS: Attendance[] = [
{
id: "E-1024",
name: "佐藤 健太",
dept: "製造一課",
shift: "日勤",
clockIn: "08:58",
clockOut: "18:12",
work: 495,
overtime: 72,
late: 0,
early: 0,
status: "present",
approved: "approved",
},
{
id: "E-1031",
name: "鈴木 美咲",
dept: "製造一課",
shift: "日勤",
clockIn: "09:14",
clockOut: "18:03",
work: 469,
overtime: 63,
late: 14,
early: 0,
status: "late",
approved: "pending",
},
{
id: "E-1042",
name: "高橋 大輔",
dept: "製造二課",
shift: "夜勤",
clockIn: "21:02",
clockOut: "06:01",
work: 480,
overtime: 60,
late: 2,
early: 0,
status: "present",
approved: "approved",
},
{
id: "E-1055",
name: "田中 彩花",
dept: "品質保証",
shift: "日勤",
clockIn: "08:55",
clockOut: "16:40",
work: 405,
overtime: 0,
late: 0,
early: 80,
status: "early",
approved: "pending",
},
{
id: "E-1063",
name: "伊藤 翔",
dept: "物流",
shift: "早番",
clockIn: "06:00",
clockOut: "15:02",
work: 482,
overtime: 62,
late: 0,
early: 0,
status: "present",
approved: "approved",
},
{
id: "E-1078",
name: "渡辺 真奈",
dept: "品質保証",
shift: "日勤",
clockIn: "—",
clockOut: "—",
work: 0,
overtime: 0,
late: 0,
early: 0,
status: "leave",
approved: "approved",
},
{
id: "E-1089",
name: "山本 拓也",
dept: "製造二課",
shift: "夜勤",
clockIn: "—",
clockOut: "—",
work: 0,
overtime: 0,
late: 0,
early: 0,
status: "absent",
approved: "rejected",
},
{
id: "E-1094",
name: "中村 由香",
dept: "製造一課",
shift: "日勤",
clockIn: "09:32",
clockOut: "18:20",
work: 488,
overtime: 80,
late: 32,
early: 0,
status: "late",
approved: "pending",
},
{
id: "E-1102",
name: "小林 直樹",
dept: "物流",
shift: "遅番",
clockIn: "13:01",
clockOut: "22:04",
work: 483,
overtime: 63,
late: 1,
early: 0,
status: "present",
approved: "approved",
},
{
id: "E-1118",
name: "加藤 千尋",
dept: "品質保証",
shift: "日勤",
clockIn: "08:57",
clockOut: "17:35",
work: 458,
overtime: 35,
late: 0,
early: 0,
status: "present",
approved: "approved",
},
{
id: "E-1127",
name: "吉田 颯太",
dept: "製造二課",
shift: "日勤",
clockIn: "09:06",
clockOut: "18:01",
work: 475,
overtime: 61,
late: 6,
early: 0,
status: "late",
approved: "pending",
},
{
id: "E-1133",
name: "山田 結衣",
dept: "製造一課",
shift: "早番",
clockIn: "06:01",
clockOut: "14:30",
work: 449,
overtime: 0,
late: 1,
early: 30,
status: "early",
approved: "pending",
},
];
// 分 → H:MM 表記 (実働・残業列。tabular-nums で桁を揃える)
function toHm(mins: number): string {
if (mins <= 0) return "0:00";
const h = Math.floor(mins / 60);
const m = mins % 60;
return `${h}:${String(m).padStart(2, "0")}`;
}
// 数値セルの共通レンダラ。0 は控えめに、値ありは右寄せ tabular-nums。
function num(value: string, muted = false) {
return (
{value}
);
}
const columns: ColumnDef[] = [
{
key: "id",
header: "社員番号",
width: "w-24",
render: (r) => (
{r.id}
),
},
{ key: "name", header: "氏名", width: "w-28", sortable: true },
{ key: "dept", header: "部署", width: "w-24", hiddenOnMobile: true },
{ key: "shift", header: "シフト", width: "w-20", align: "center", hiddenOnMobile: true },
{
key: "clockIn",
header: "出勤",
width: "w-16",
align: "right",
render: (r) => num(r.clockIn, r.clockIn === "—"),
},
{
key: "clockOut",
header: "退勤",
width: "w-16",
align: "right",
render: (r) => num(r.clockOut, r.clockOut === "—"),
},
{
key: "work",
header: "実働",
width: "w-16",
align: "right",
sortable: true,
render: (r) => num(toHm(r.work), r.work === 0),
},
{
key: "overtime",
header: "残業",
width: "w-16",
align: "right",
sortable: true,
render: (r) => num(toHm(r.overtime), r.overtime === 0),
},
{
key: "late",
header: "遅刻",
width: "w-14",
align: "right",
hiddenOnMobile: true,
render: (r) => num(r.late ? `${r.late}分` : "0", r.late === 0),
},
{
key: "early",
header: "早退",
width: "w-14",
align: "right",
hiddenOnMobile: true,
render: (r) => num(r.early ? `${r.early}分` : "0", r.early === 0),
},
{
key: "status",
header: "勤怠区分",
width: "w-24",
align: "center",
render: (r) => {
const m = STATUS_META[r.status];
return (
{m.label}
);
},
},
{
key: "approved",
header: "承認",
width: "w-24",
align: "center",
hiddenOnMobile: true,
render: (r) => {
const m = APPROVAL_META[r.approved];
return (
{m.label}
);
},
},
{
key: "_actions",
header: "",
ariaLabel: "操作",
width: "w-10",
align: "right",
render: (r) => (
打刻修正
承認
),
},
];
export default function Demo() {
// 未承認の 2 行を初期選択 (一括操作バーを at rest で見せる)
const [selected, setSelected] = React.useState>(new Set(["E-1031", "E-1094"]));
// 残業の多い順に初期ソート
const [sort, setSort] = React.useState({
key: "overtime",
direction: "desc",
});
// 高密度を明示 (controlled compact = 28px 行)
const [density, setDensity] = React.useState("compact");
const rows = React.useMemo(() => {
if (!sort) return ROWS;
const dir = sort.direction === "asc" ? 1 : -1;
return [...ROWS].sort((a, b) => {
const av = a[sort.key as keyof Attendance];
const bv = b[sort.key as keyof Attendance];
return (av < bv ? -1 : av > bv ? 1 : 0) * dir;
});
}, [sort]);
return (
row.id}
selectable
selected={selected}
onSelectChange={setSelected}
density={density}
onDensityChange={setDensity}
sort={sort}
onSortChange={setSort}
>
全 {ROWS.length} 名
{}} />
);
}