/** * Showcase · case2 — 従業員ダッシュボード (/me) * * The employee self-service home (`/me/dashboard`), built ENTIRELY from real * @godxjp/ui primitives. reference-design handoff recreated as a "skeleton" * (intent + look), not a transcription of its prototype DOM. * * Composition map (prototype block → @godxjp/ui primitive): * page chrome ............ AppShell + Sidebar + Topbar slots * page header + status ... PageContainer (extra slot) + Badge(tone) * PunchCard FSM .......... Card + Button + Badge(tone) + Alert — renders ONLY * the valid actions for the current session state * (Off → Working → Break → Closed); checkout locked * on break shown via Alert tone="warning". * month KPI 2×2 .......... ResponsiveGrid + StatCard (tabular value + delta) * 7-day attendance ....... Card + Table + Badge(tone), tabular-nums * today summary .......... Card + Descriptions-style key/value rows * * DNA applied: default density (comfortable touch on the punch buttons where * tapped), small headings, fixed color signaling (success 若竹 / warning 山吹 / * attention 朱 / info 群青), tabular numerics, bilingual JP·VI labels on the * shared employee surface, quiet factual copy, no emoji. */ import * as React from "react"; import { CalendarDays, ClipboardList, Clock, Coffee, LayoutDashboard, LogIn, MapPin, RotateCcw, Settings, Square, } from "lucide-react"; import { Button, Text } from "@godxjp/ui/general"; import { Alert, AlertDescription, AlertTitle } from "@godxjp/ui/feedback"; import { Badge, Card, CardAction, CardContent, CardHeader, CardTitle, Descriptions, StatCard, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, type BadgeProps, } from "@godxjp/ui/data-display"; import { AppShell, Flex, PageContainer, ResponsiveGrid, Separator, Sidebar, type SidebarSectionProp, } from "@godxjp/ui/layout"; /** Badge tone vocabulary (derive from BadgeProps — not separately re-exported). */ type BadgeTone = NonNullable; // ── /me sidebar (single section — the employee surface is minimal) ──────────── const NAV_SECTIONS: SidebarSectionProp[] = [ { label: "マイページ", items: [ { id: "dashboard", label: "ダッシュボード", icon: LayoutDashboard }, { id: "my-shifts", label: "マイシフト", icon: CalendarDays }, { id: "timesheet", label: "勤怠表", icon: ClipboardList }, { id: "settings", label: "設定", icon: Settings }, ], }, ]; // ── Punch FSM ───────────────────────────────────────────────────────────────── // // The cardinal attendance UX rule: render ONLY the actions valid for the current // session state — never four disabled buttons. State derives from the last punch // log (CheckIn / BreakStart / BreakEnd / CheckOut). type PunchState = "off" | "working" | "break" | "closed"; const PUNCH_STATUS: Record< PunchState, { label: string; tone: Extract; meta: string } > = { off: { label: "未出勤 · Chưa chấm công", tone: "neutral", meta: "本日の打刻はまだありません" }, working: { label: "勤務中 · Đang làm", tone: "success", meta: "出勤 09:30 · 渋谷店 · 経過 4時間12分", }, break: { label: "休憩中 · Nghỉ giải lao", tone: "warning", meta: "休憩開始 13:48 · 経過 12分" }, closed: { label: "退勤済 · Đã tan ca", tone: "neutral", meta: "09:30 → 18:12 · 休憩 24分 · 残業 0時間", }, }; /** A punch button descriptor. `comfortable` density floors these at the 44px touch target. */ type PunchAction = { key: string; label: string; icon: React.ComponentType<{ className?: string }>; variant: NonNullable["variant"]>; next: PunchState; }; const PUNCH_ACTIONS: Record = { off: [{ key: "in", label: "出勤 · Check In", icon: LogIn, variant: "default", next: "working" }], working: [ { key: "break-start", label: "休憩開始 · Nghỉ", icon: Coffee, variant: "outline", next: "break", }, { key: "out", label: "退勤 · Check Out", icon: Square, variant: "destructive", next: "closed" }, ], break: [ { key: "break-end", label: "休憩終了 · Kết thúc nghỉ", icon: RotateCcw, variant: "default", next: "working", }, ], closed: [], }; function PunchCard() { // Seeded "working" so the open/active state is visible at rest (no click needed). const [state, setState] = React.useState("working"); const [clock, setClock] = React.useState("13:42"); const status = PUNCH_STATUS[state]; const actions = PUNCH_ACTIONS[state]; // The defining behaviour: on break, Check Out is locked until the break ends. const checkoutLocked = state === "break"; return ( 打刻 · Chấm công {status.label} {/* Big tabular clock — the signature employee block. */} {/* Hero numeral at the system's max step (2xl) — the golden scale tops here by design. */} {clock} {status.meta} {/* Checkout-locked notice (attention, not destructive — it's a fixable hold). */} {checkoutLocked ? ( 退勤は休憩終了後 · Check Out đang khoá 休憩中は退勤できません。先に「休憩終了」を打刻してください。 ) : null} {/* Valid actions only — comfortable density floors each button at 44px (touch). */} {state === "closed" ? (
) : ( {actions.map((a) => { const Icon = a.icon; return ( ); })} )} {/* Geofence caption — the documented punch footer line. */} {/* Demo state stepper — lets a reader walk every FSM state at rest. */} 状態: {(["off", "working", "break", "closed"] as PunchState[]).map((s) => ( ))}
); } // ── Month KPI 2×2 ────────────────────────────────────────────────────────────── const KPIS: Array<{ label: string; value: string; hint: string; delta?: string; inverse?: boolean; }> = [ { label: "勤務時間 · Giờ làm", value: "96.5h", hint: "計画 168h · 達成 57%", delta: "+2.0h" }, { label: "残業 · Overtime", value: "4.2h", hint: "先月比", delta: "-1.8h", inverse: true }, { label: "出勤日数 · Ngày đi làm", value: "13", hint: "22営業日中" }, { label: "遅刻 · Đến trễ", value: "1", hint: "12分 · 5/7", delta: "+1", inverse: true }, ]; // ── 7-day attendance ──────────────────────────────────────────────────────────── type DayStatus = "present" | "late" | "leave" | "holiday"; const STATUS_DEF: Record< DayStatus, { label: string; tone: BadgeTone; variant: BadgeProps["variant"] } > = { present: { label: "通常 · Bình thường", tone: "success", variant: "outline" }, late: { label: "遅刻 · Đến trễ", tone: "warning", variant: "outline" }, leave: { label: "有給 · Nghỉ phép", tone: "info", variant: "outline" }, holiday: { label: "休日 · Nghỉ", tone: "neutral", variant: "outline" }, }; const DAYS: Array<{ date: string; weekday: string; work: string; ot: string; status: DayStatus; }> = [ { date: "5/08", weekday: "木", work: "8.0h", ot: "0.0h", status: "present" }, { date: "5/07", weekday: "水", work: "8.5h", ot: "0.5h", status: "present" }, { date: "5/06", weekday: "火", work: "7.7h", ot: "0.0h", status: "late" }, { date: "5/05", weekday: "月", work: "—", ot: "—", status: "holiday" }, { date: "5/04", weekday: "日", work: "—", ot: "—", status: "holiday" }, { date: "5/03", weekday: "土", work: "—", ot: "—", status: "leave" }, { date: "5/02", weekday: "金", work: "8.2h", ot: "1.0h", status: "present" }, ]; // ── Today summary ──────────────────────────────────────────────────────────────── const SUMMARY: Array<{ label: string; value: string }> = [ { label: "シフト · Ca", value: "早番 09:00–18:00" }, { label: "出勤 · Check In", value: "09:30" }, { label: "休憩 · Nghỉ", value: "12:00–12:24 (24分)" }, { label: "退勤予定 · Dự kiến", value: "18:00" }, { label: "勤務地 · Nơi làm", value: "渋谷店" }, ]; export default function EmployeeMeShowcase() { const [activeNav, setActiveNav] = React.useState("dashboard"); const sidebar = ( ); return ( マイページ} topbarRight={ 勤務中 · 4時間12分 2026年5月8日 · 木曜日 } > {/* Punch (left) + month KPI 2×2 (right) — stack
{KPIS.map((k) => ( ))}
{/* 7-day attendance (left, wide) + today summary (right) */} 最近7日 · 7 ngày gần nhất 勤務 47.4h · 残業 1.5h 日付 勤務 残業 状態 {DAYS.map((d) => { const def = STATUS_DEF[d.status]; return ( {d.date} ({d.weekday}) {d.work} {d.ot} {def.label} ); })}
本日のサマリー · Hôm nay {SUMMARY.map((s) => ( {s.value} ))}
); }