/** * Showcase · table-filter-chips — フィルターチップ (V6) * * An active-filter chip bar above an attendance DataTable. Each applied filter * renders as a removable Badge chip (× clears that one); a "すべて解除" button * clears them all. The chip set drives the DataTable's filtered rows live — * the table count, empty state, and pagination all react to the active chips. * * Composition map (intent → real @godxjp/ui primitive): * page chrome ............ PageContainer (compact density, small heading) * filter controls ........ SearchInput (free text) + Select (data-driven) * active-filter chips ..... Badge (tone) + an inline Button(icon) per chip * clear-all .............. Button(ghost) * the list ............... DataTable (sortable, selectable, bulk, empty) * status cell ............ Badge tone (fixed color signaling) * * DNA: compact density, tabular-nums on times, fixed tone mapping * (出勤 success若竹 / 遅刻 warning山吹 / 早退 attention→warning / 欠勤 danger茜 / * 申請中 info群青), quiet JP copy, no emoji. * * GAP: DataTable has no built-in filter-chip bar (it ships Toolbar / BulkActions * / DensityToggle / Pagination only). The chip bar is composed here from real * primitives (Badge + Button + SearchInput + Select) and the filtering is done * in the consumer, then passed to DataTable as the already-filtered `data`. */ import * as React from "react"; import { X, Filter } from "lucide-react"; import { Button, Text } from "@godxjp/ui/general"; import { Badge, Card, CardContent, DataTable, EmptyState, type ColumnDef, } from "@godxjp/ui/data-display"; import { SearchInput, Select } from "@godxjp/ui/data-entry"; import { Flex, PageContainer } from "@godxjp/ui/layout"; import type { SortStateProp } from "@godxjp/ui/props"; // ── Domain ────────────────────────────────────────────────────────────────── type AttendanceStatus = "present" | "late" | "early" | "absent" | "requested"; interface Record_ { id: string; name: string; dept: "営業部" | "開発部" | "管理部" | "物流部"; date: string; clockIn: string; clockOut: string; status: AttendanceStatus; overtime: number; // minutes } // status → {label, tone}. attention(遅刻/早退) uses warning山吹; 欠勤 uses danger茜. const STATUS_META: Record< AttendanceStatus, { label: string; tone: "success" | "warning" | "destructive" | "info" } > = { present: { label: "出勤", tone: "success" }, late: { label: "遅刻", tone: "warning" }, early: { label: "早退", tone: "warning" }, absent: { label: "欠勤", tone: "destructive" }, requested: { label: "申請中", tone: "info" }, }; const DEPTS = ["営業部", "開発部", "管理部", "物流部"] as const; const ROWS: Record_[] = [ { id: "K-1043", name: "田中 健太", dept: "営業部", date: "06/03", clockIn: "08:58", clockOut: "18:12", status: "present", overtime: 12, }, { id: "K-1044", name: "佐藤 美咲", dept: "開発部", date: "06/03", clockIn: "09:24", clockOut: "18:40", status: "late", overtime: 40, }, { id: "K-1045", name: "鈴木 大輔", dept: "物流部", date: "06/03", clockIn: "07:55", clockOut: "16:30", status: "early", overtime: 0, }, { id: "K-1046", name: "高橋 由美", dept: "管理部", date: "06/03", clockIn: "—", clockOut: "—", status: "absent", overtime: 0, }, { id: "K-1047", name: "伊藤 翔", dept: "営業部", date: "06/03", clockIn: "08:50", clockOut: "19:05", status: "present", overtime: 65, }, { id: "K-1048", name: "渡辺 彩", dept: "開発部", date: "06/03", clockIn: "—", clockOut: "—", status: "requested", overtime: 0, }, { id: "K-1049", name: "山本 拓也", dept: "物流部", date: "06/03", clockIn: "09:12", clockOut: "18:20", status: "late", overtime: 20, }, { id: "K-1050", name: "中村 真央", dept: "管理部", date: "06/03", clockIn: "08:45", clockOut: "17:50", status: "present", overtime: 0, }, { id: "K-1051", name: "小林 直樹", dept: "営業部", date: "06/03", clockIn: "08:59", clockOut: "20:10", status: "present", overtime: 130, }, { id: "K-1052", name: "加藤 千夏", dept: "開発部", date: "06/03", clockIn: "—", clockOut: "—", status: "requested", overtime: 0, }, { id: "K-1053", name: "吉田 蓮", dept: "物流部", date: "06/03", clockIn: "—", clockOut: "—", status: "absent", overtime: 0, }, { id: "K-1054", name: "山田 さくら", dept: "管理部", date: "06/03", clockIn: "09:31", clockOut: "18:00", status: "late", overtime: 0, }, ]; const STATUS_OPTIONS = (Object.keys(STATUS_META) as AttendanceStatus[]).map((k) => ({ value: k, label: STATUS_META[k].label, })); const DEPT_OPTIONS = DEPTS.map((d) => ({ value: d, label: d })); const OT_OPTIONS = [ { value: "any", label: "残業: すべて" }, { value: "ot", label: "残業あり" }, { value: "long", label: "60分以上" }, ]; const minutes = (m: number) => `${m}分`; // ── Columns ───────────────────────────────────────────────────────────────── const columns: ColumnDef[] = [ { key: "id", header: "ID", width: "w-24", render: (r) => ( {r.id} ), }, { key: "name", header: "従業員", sortable: true }, { key: "dept", header: "部署", hiddenOnMobile: true }, { key: "clockIn", header: "出勤", align: "right", render: (r) => {r.clockIn}, }, { key: "clockOut", header: "退勤", align: "right", hiddenOnMobile: true, render: (r) => {r.clockOut}, }, { key: "overtime", header: "残業", align: "right", sortable: true, render: (r) => {r.overtime > 0 ? minutes(r.overtime) : "—"}, }, { key: "status", header: "状態", align: "center", render: (r) => ( {STATUS_META[r.status].label} ), }, ]; // ── Chip ──────────────────────────────────────────────────────────────────── function FilterChip({ tone, label, onRemove, }: { tone?: "success" | "warning" | "destructive" | "info" | "neutral"; label: string; onRemove: () => void; }) { return ( {label} ); } export default function Demo() { const [query, setQuery] = React.useState("田"); const [status, setStatus] = React.useState("late"); const [dept, setDept] = React.useState(undefined); const [ot, setOt] = React.useState("any"); const [selected, setSelected] = React.useState>(new Set()); const [sort, setSort] = React.useState(undefined); const hasFilters = query.trim() !== "" || !!status || !!dept || ot !== "any"; const clearAll = () => { setQuery(""); setStatus(undefined); setDept(undefined); setOt("any"); }; // Filter → sort. The active chips drive these filtered rows. const rows = React.useMemo(() => { let out = ROWS.filter((r) => { if (query.trim() && !`${r.name}${r.id}`.includes(query.trim())) return false; if (status && r.status !== status) return false; if (dept && r.dept !== dept) return false; if (ot === "ot" && r.overtime <= 0) return false; if (ot === "long" && r.overtime < 60) return false; return true; }); if (sort) { const dir = sort.direction === "asc" ? 1 : -1; out = [...out].sort((a, b) => { const av = a[sort.key as keyof Record_]; const bv = b[sort.key as keyof Record_]; return (av < bv ? -1 : av > bv ? 1 : 0) * dir; }); } return out; }, [query, status, dept, ot, sort]); return ( {/* Filter controls */} { setDept(v || undefined); }} placeholder="部署" />