// 員工打卡鐘：後台操作員自己打上/下班卡；有權限的角色可查看全體紀錄、匯出 CSV、代改/刪除紀錄。
//
// 資料來源 GET /api/admin/attendance（依角色自動限定「只看自己」或「全體」，見 lib/adminAccess.ts
// 的 attendance 分類）；打卡 POST 同路徑；代改/刪除走 POST /api/admin/attendance/[id] 的 PATCH/DELETE。
// 「員工」沿用現有後台帳號（admin_credentials），沒有另外的員工身份系統。

const ATT_TYPE = {
  in:  { label: "上班", tone: "success" },
  out: { label: "下班", tone: "default" },
};

// 跟 shell.jsx 的 CLOCK_OUT_REMINDER_HOURS、app/api/admin/overtime|attendance 路由的
// STANDARD_WORK_HOURS 保持一致：工作滿這個時數，打卡卡片才會出現「申請加班」的選項。
const STANDARD_WORK_HOURS = 8;

const OT_STATUS = {
  pending:  { label: "待審核", tone: "warn" },
  approved: { label: "已核准", tone: "success" },
  rejected: { label: "已駁回", tone: "default" },
};

const ATT_RANGE_OPTS = [
  { value: "thisMonth", label: "本月" },
  { value: "thisWeek",  label: "本週" },
  { value: "lastWeek",  label: "上週" },
  { value: "all",       label: "全部期間" },
  { value: "custom",    label: "自訂區間" },
];

// twDate / twDateTime 共用定義在 ui.jsx（全站共用，避免跟其他頁面同名 const 互相覆蓋）
const twClock = (d) => d.toLocaleTimeString("zh-TW", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, timeZone: "Asia/Taipei" });
// 給 <input type="datetime-local"> 用（無時區，直接取台北當地時間字串）
const twInputValue = (s) => (s ? new Date(s).toLocaleString("sv-SE", { timeZone: "Asia/Taipei" }).slice(0, 16) : "");

function attComputeRange(kind) {
  if (kind === "all") return { from: null, to: null };
  const now = new Date();
  const ymd = (dt) => `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, "0")}-${String(dt.getDate()).padStart(2, "0")}`;
  const twMidnight = (dt) => new Date(`${ymd(dt)}T00:00:00+08:00`).toISOString();

  if (kind === "thisMonth") {
    const from = new Date(now.getFullYear(), now.getMonth(), 1);
    const to = new Date(now.getFullYear(), now.getMonth() + 1, 1);
    return { from: twMidnight(from), to: twMidnight(to) };
  }
  const d = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const dow = (d.getDay() + 6) % 7;
  const monday = new Date(d); monday.setDate(d.getDate() - dow);
  if (kind === "lastWeek") monday.setDate(monday.getDate() - 7);
  const nextMonday = new Date(monday); nextMonday.setDate(monday.getDate() + 7);
  return { from: twMidnight(monday), to: twMidnight(nextMonday) };
}

function attCustomRange(fromYmd, toYmd) {
  const from = fromYmd ? new Date(`${fromYmd}T00:00:00+08:00`).toISOString() : null;
  const to = toYmd
    ? new Date(new Date(`${toYmd}T00:00:00+08:00`).getTime() + 86400000).toISOString()
    : null;
  return { from, to };
}

const fmtHours = (ms) => {
  if (!ms || ms <= 0) return "0 小時";
  const totalMin = Math.round(ms / 60000);
  const h = Math.floor(totalMin / 60);
  const m = totalMin % 60;
  return m ? `${h} 小時 ${m} 分` : `${h} 小時`;
};

// 工時統計：把「上班」→「下班」的打卡事件依時間順序配成一段段工作時段來加總工時。
// 只用目前已載入、篩選範圍內的 records（已排除作廢紀錄），不用另外打 API。
// 配對規則：同一人的紀錄依時間排序後，遇到「上班」記下開始時間；遇到「下班」就跟
// 最近一筆還沒配對的「上班」湊成一段、算時數。落單的「上班」（忘記打下班卡，或
// 篩選區間切到一半、下班卡在範圍外）算進 openCount，不計入工時。
function computeHoursSummary(records) {
  const byEmployee = {};
  for (const r of records) {
    if (r.voidedAt) continue;
    (byEmployee[r.adminEmail] ??= []).push(r);
  }

  return Object.entries(byEmployee).map(([email, list]) => {
    const sorted = [...list].sort((a, b) => new Date(a.clockedAt) - new Date(b.clockedAt));
    let openIn = null;
    let totalMs = 0;
    let openCount = 0;
    const days = new Set();
    for (const r of sorted) {
      if (r.type === "in") {
        if (openIn) openCount++; // 連續兩筆上班卡：前一筆視為沒配到對
        openIn = r;
      } else if (r.type === "out" && openIn) {
        const ms = new Date(r.clockedAt) - new Date(openIn.clockedAt);
        if (ms > 0) { totalMs += ms; days.add(twDate(openIn.clockedAt)); }
        openIn = null;
      }
    }
    if (openIn) openCount++; // 目前仍在上班中，或忘記打下班卡

    return { adminEmail: email, employeeName: sorted[0]?.employeeName ?? email.split("@")[0], totalMs, days: days.size, openCount };
  }).sort((a, b) => b.totalMs - a.totalMs);
}

// 加班申請列表：一般員工只看得到自己的申請（後端強制鎖 email），
// 有審核權限的角色（ops_admin/super_admin）看得到全體，且能核准/駁回別人的申請
// ——不能審核自己的（見 API 同一條規則），也不能審核還沒下班、時數未定案的申請。
const OvertimeSection = ({ requests, canManageAll, canReview, adminEmail, onAction }) => {
  if (requests.length === 0) return null;
  return (
    <Card padding={0} style={{ marginBottom: 20 }}>
      <div style={{ padding: "14px 16px", borderBottom: "1px solid var(--border)", fontSize: 14, fontWeight: 600 }}>加班申請</div>
      <Table
        columns={[
          ...(canManageAll ? [{ title: "員工", render: r => <span style={{ fontSize: 13.5 }}>{r.employeeName}</span> }] : []),
          { title: "上班時間", render: r => <span className="mono" style={{ fontSize: 12.5 }}>{twDateTime(r.clockInAt)}</span> },
          { title: "下班時間", render: r => r.clockOutAt
            ? <span className="mono" style={{ fontSize: 12.5 }}>{twDateTime(r.clockOutAt)}</span>
            : <span style={{ fontSize: 12.5, color: "var(--ink-4)" }}>尚未下班</span> },
          { title: "加班時數", render: r => r.overtimeHours != null
            ? <span className="mono" style={{ fontWeight: 500 }}>{r.overtimeHours} 小時</span>
            : <span style={{ fontSize: 12.5, color: "var(--ink-4)" }}>計算中</span> },
          { title: "備註", render: r => <span style={{ fontSize: 13, color: "var(--ink-3)" }}>{r.note || "—"}</span> },
          { title: "狀態", render: r => <Badge tone={OT_STATUS[r.status]?.tone} size="sm">{OT_STATUS[r.status]?.label ?? r.status}</Badge> },
          ...(canReview ? [{
            title: "", align: "right",
            render: r => {
              if (r.status !== "pending" || !r.clockOutAt) return null;
              if (r.adminEmail === adminEmail) return <span style={{ fontSize: 11.5, color: "var(--ink-4)" }}>本人申請</span>;
              return (
                <div style={{ display: "flex", gap: 6, justifyContent: "flex-end" }}>
                  <Button size="sm" variant="secondary" onClick={() => onAction({ type: "reject", req: r })}>駁回</Button>
                  <Button size="sm" variant="primary" onClick={() => onAction({ type: "approve", req: r })}>核准</Button>
                </div>
              );
            },
          }] : []),
        ]}
        rows={requests}
      />
    </Card>
  );
};

const HoursSummary = ({ records, canManageAll }) => {
  const summary = useMemo(() => computeHoursSummary(records), [records]);
  if (summary.length === 0) return null;

  if (!canManageAll) {
    const s = summary[0];
    return (
      <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 12, marginBottom: 20 }}>
        <StatTile label="總工時" value={fmtHours(s.totalMs)} icon="clock" tone="accent"/>
        <StatTile label="出勤天數" value={`${s.days} 天`} icon="calendar" tone="info"/>
        <StatTile label="平均工時/天" value={s.days ? fmtHours(s.totalMs / s.days) : "—"} icon="chart"
          sub={s.openCount > 0 ? `另有 ${s.openCount} 筆尚未打下班卡，未計入工時` : undefined}/>
      </div>
    );
  }

  return (
    <Card padding={0} style={{ marginBottom: 20 }}>
      <div style={{ padding: "14px 16px", borderBottom: "1px solid var(--border)", fontSize: 14, fontWeight: 600 }}>工時統計</div>
      <Table
        columns={[
          { title: "員工", render: r => <span style={{ fontSize: 13.5 }}>{r.employeeName}</span> },
          { title: "出勤天數", render: r => <span className="mono">{r.days} 天</span> },
          { title: "總工時", render: r => <span className="mono" style={{ fontWeight: 500 }}>{fmtHours(r.totalMs)}</span> },
          { title: "平均工時/天", render: r => <span className="mono">{r.days ? fmtHours(r.totalMs / r.days) : "—"}</span> },
          { title: "", render: r => r.openCount > 0
            ? <Badge tone="warn" size="sm">{r.openCount} 筆未打下班卡</Badge>
            : null },
        ]}
        rows={summary}
      />
    </Card>
  );
};

// 打卡卡片：大時鐘 + 目前狀態 + 打卡按鈕 + 加班申請
// myLast 獨立於下方清單的篩選條件（清單可能被切到別的員工/區間），
// 只反映「登入者自己」最新一筆打卡，按鈕狀態才不會因為篩選條件而顯示錯誤。
// myOvertime 是目前這次上班（如果還在上班中）掛著的加班申請，clockOutAt 還是 null
// 代表還在計算中；null（沒有申請過）才會顯示「申請加班」按鈕。
const ClockInCard = ({ myLast, myOvertime, onPunched, onOvertimeApplied }) => {
  const [now, setNow] = useState(new Date());
  const [note, setNote] = useState("");
  const [otNote, setOtNote] = useState("");
  const [loading, setLoading] = useState(false);
  const [applying, setApplying] = useState(false);
  const toast = useToast();

  useEffect(() => {
    const t = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(t);
  }, []);

  const nextType = myLast?.type === "in" ? "out" : "in";
  const elapsedHours = myLast?.type === "in" ? (now - new Date(myLast.clockedAt)) / 3_600_000 : 0;
  const overtimeEligible = myLast?.type === "in" && elapsedHours >= STANDARD_WORK_HOURS;

  const punch = async () => {
    setLoading(true);
    const res = await fetch("/api/admin/attendance", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ type: nextType, note: note.trim() || undefined }),
    });
    setLoading(false);
    if (res.ok) {
      setNote("");
      toast({ title: nextType === "in" ? "上班打卡成功" : "下班打卡成功", message: twClock(new Date()), icon: "check" });
      onPunched();
      // 立刻重新檢查下班卡提醒（見 app.jsx），不用等下一輪 5 分鐘的輪詢才消掉通知
      window.__recheckClockOut?.();
      // 下班打卡當下，掛著的加班申請時數才會定案、變成真正可審核，順便刷新審核者的通知鈴鐺
      window.__recheckOvertime?.();
    } else {
      const d = await res.json().catch(() => ({}));
      toast({ title: "打卡失敗", message: d.error ?? "請稍後再試", icon: "alert" });
    }
  };

  const applyOvertime = async () => {
    setApplying(true);
    const res = await fetch("/api/admin/overtime", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ note: otNote.trim() || undefined }),
    });
    setApplying(false);
    if (res.ok) {
      setOtNote("");
      toast({ title: "已送出加班申請", message: "打下班卡後會自動算出加班時數，送管理員審核", icon: "check" });
      onOvertimeApplied();
    } else {
      const d = await res.json().catch(() => ({}));
      toast({ title: "申請失敗", message: d.error ?? "請稍後再試", icon: "alert" });
    }
  };

  return (
    <Card padding={24} style={{ marginBottom: 20 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 28, flexWrap: "wrap" }}>
        <div>
          <div className="mono" style={{ fontSize: 40, fontWeight: 600, lineHeight: 1 }}>{twClock(now)}</div>
          <div style={{ fontSize: 12.5, color: "var(--ink-3)", marginTop: 6 }}>
            {now.toLocaleDateString("zh-TW", { timeZone: "Asia/Taipei", year: "numeric", month: "long", day: "numeric", weekday: "long" })}
          </div>
        </div>
        <div style={{ width: 1, alignSelf: "stretch", background: "var(--border)" }}/>
        <div style={{ flex: 1, minWidth: 220 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
            <span style={{ fontSize: 12.5, color: "var(--ink-3)" }}>目前狀態</span>
            <Badge tone={myLast?.type === "in" ? "success" : "default"} size="sm">
              {myLast?.type === "in" ? "上班中" : "未上班"}
            </Badge>
          </div>
          {myLast && (
            <div style={{ fontSize: 12, color: "var(--ink-4)" }}>
              最近一筆：{ATT_TYPE[myLast.type].label}於 {twDateTime(myLast.clockedAt)}
            </div>
          )}
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <Input value={note} onChange={e => setNote(e.target.value)} placeholder="備註（選填）" style={{ width: 180 }}/>
          <Button variant={nextType === "in" ? "primary" : "danger"} size="lg" icon="clock" onClick={punch} disabled={loading}>
            {loading ? "打卡中…" : nextType === "in" ? "上班打卡" : "下班打卡"}
          </Button>
        </div>
      </div>

      {overtimeEligible && (
        <div style={{
          marginTop: 18, paddingTop: 18, borderTop: "1px solid var(--border)",
          display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap",
        }}>
          <Badge tone="warn" size="sm">已工作超過 {STANDARD_WORK_HOURS} 小時</Badge>
          {myOvertime ? (
            <span style={{ fontSize: 12.5, color: "var(--ink-3)" }}>
              已送出加班申請，打下班卡後會自動算出時數、送管理員審核
            </span>
          ) : (
            <>
              <span style={{ fontSize: 12.5, color: "var(--ink-3)" }}>要申請加班嗎？</span>
              <Input value={otNote} onChange={e => setOtNote(e.target.value)} placeholder="加班原因（選填）" style={{ width: 200 }}/>
              <Button variant="secondary" size="sm" icon="plus" onClick={applyOvertime} disabled={applying}>
                {applying ? "送出中…" : "申請加班"}
              </Button>
            </>
          )}
        </div>
      )}
    </Card>
  );
};

// 代改/刪除紀錄的 Modal（僅 canEdit 才會被叫出）
const EditRecordModal = ({ record, onClose, onSaved }) => {
  const [type, setType] = useState(record.type);
  const [clockedAt, setClockedAt] = useState(twInputValue(record.clockedAt));
  const [note, setNote] = useState(record.note ?? "");
  const [reason, setReason] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");
  const toast = useToast();

  const save = async () => {
    if (reason.trim().length < 4) { setError("請填寫修正原因（至少 4 個字）"); return; }
    setError(""); setLoading(true);
    const res = await fetch(`/api/admin/attendance/${record.id}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        type,
        clockedAt: new Date(`${clockedAt}:00+08:00`).toISOString(),
        note: note.trim(),
        reason: reason.trim(),
      }),
    });
    setLoading(false);
    if (res.ok) {
      toast({ title: "已修正紀錄", icon: "check" });
      onSaved();
    } else {
      const d = await res.json().catch(() => ({}));
      setError(d.error ?? "修正失敗，請稍後再試");
    }
  };

  return (
    <Modal open onClose={onClose} width={440}>
      <div style={{ padding: 24 }}>
        <div style={{ fontSize: 17, fontWeight: 600, marginBottom: 16 }}>修正打卡紀錄</div>
        <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
          <div>
            <label style={labelStyle}>員工</label>
            <div style={{ fontSize: 13.5, color: "var(--ink-2)" }}>{record.employeeName}（{record.adminEmail}）</div>
          </div>
          <div>
            <label style={labelStyle}>類型</label>
            <Select value={type} onChange={setType} options={[{ value: "in", label: "上班" }, { value: "out", label: "下班" }]}/>
          </div>
          <div>
            <label style={labelStyle}>打卡時間（台北時間）</label>
            <input type="datetime-local" value={clockedAt} onChange={e => setClockedAt(e.target.value)}
              style={{ width: "100%", padding: "8px 12px", fontSize: 14, border: "1px solid var(--border-strong)", borderRadius: 8, outline: 0 }}/>
          </div>
          <div>
            <label style={labelStyle}>備註</label>
            <Input value={note} onChange={e => setNote(e.target.value)} placeholder="備註（選填）"/>
          </div>
          <div>
            <label style={labelStyle}>修正原因 <span style={{ color: "var(--danger)" }}>*</span></label>
            <textarea value={reason} onChange={e => setReason(e.target.value)} rows={2}
              placeholder="例：員工出差忘記打卡，依打卡機備援紀錄補登。"
              style={{ width: "100%", padding: "8px 12px", fontSize: 14, border: "1px solid var(--border-strong)", borderRadius: 8, fontFamily: "var(--font-sans)", resize: "vertical", outline: 0 }}/>
          </div>
          {error && <div style={{ fontSize: 12.5, color: "var(--danger)" }}>{error}</div>}
        </div>
        <div style={{ display: "flex", gap: 8, marginTop: 20, justifyContent: "flex-end" }}>
          <Button variant="secondary" onClick={onClose} disabled={loading}>取消</Button>
          <Button variant="primary" onClick={save} disabled={loading}>{loading ? "儲存中…" : "儲存修正"}</Button>
        </div>
      </div>
    </Modal>
  );
};

const AttendancePage = ({ adminEmail }) => {
  const [range, setRange] = useState("thisMonth");
  const [customFrom, setCustomFrom] = useState("");
  const [customTo, setCustomTo] = useState("");
  const [employeeFilter, setEmployeeFilter] = useState("all");
  const [records, setRecords] = useState([]);
  const [employees, setEmployees] = useState([]);
  const [canManageAll, setCanManageAll] = useState(false);
  const [canEdit, setCanEdit] = useState(false);
  const [loading, setLoading] = useState(true);
  const [editing, setEditing] = useState(null);
  const [deleting, setDeleting] = useState(null);
  const [myLast, setMyLast] = useState(null);
  const [myOvertime, setMyOvertime] = useState(null);
  const [overtimeRequests, setOvertimeRequests] = useState([]);
  const [otCanManageAll, setOtCanManageAll] = useState(false);
  const [otCanReview, setOtCanReview] = useState(false);
  const [otAction, setOtAction] = useState(null); // { type: "approve"|"reject", req }
  const toast = useToast();

  const loadMyLast = () => {
    fetch(`/api/admin/attendance?employee=${encodeURIComponent(adminEmail)}`)
      .then(r => r.ok ? r.json() : null)
      // 已作廢的紀錄不算數，跳過找下一筆，打卡按鈕的方向才不會被作廢掉的舊紀錄誤導
      .then(d => { if (d) setMyLast((d.records ?? []).find(r => !r.voidedAt) ?? null); })
      .catch(() => {});
  };
  useEffect(loadMyLast, [adminEmail]);

  // 目前這次上班（如果還在上班中）掛著的加班申請：clockOutAt 還是 null 就是「還在計算中」，
  // 用來決定打卡卡片要顯示「申請加班」按鈕還是「已申請」的提示。
  const loadMyOvertime = () => {
    fetch(`/api/admin/overtime?employee=${encodeURIComponent(adminEmail)}`)
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (d) setMyOvertime((d.requests ?? []).find(r => !r.clockOutAt) ?? null); })
      .catch(() => {});
  };
  useEffect(loadMyOvertime, [adminEmail]);

  const loadOvertimeRequests = () => {
    const p = new URLSearchParams();
    if (employeeFilter !== "all") p.set("employee", employeeFilter);
    fetch(`/api/admin/overtime?${p.toString()}`)
      .then(r => r.ok ? r.json() : null)
      .then(d => {
        if (!d) return;
        setOvertimeRequests(d.requests ?? []);
        setOtCanManageAll(!!d.canManageAll);
        setOtCanReview(!!d.canReview);
      })
      .catch(() => {});
  };
  useEffect(loadOvertimeRequests, [employeeFilter]);

  const load = () => {
    setLoading(true);
    const p = new URLSearchParams();
    const { from, to } = range === "custom" ? attCustomRange(customFrom, customTo) : attComputeRange(range);
    if (from) p.set("from", from);
    if (to) p.set("to", to);
    if (employeeFilter !== "all") p.set("employee", employeeFilter);
    fetch(`/api/admin/attendance?${p.toString()}`)
      .then(r => r.ok ? r.json() : null)
      .then(d => {
        if (!d) return;
        setRecords(d.records ?? []);
        setEmployees(d.employees ?? []);
        setCanManageAll(!!d.canManageAll);
        setCanEdit(!!d.canEdit);
      })
      .catch(() => {})
      .finally(() => setLoading(false));
  };
  useEffect(load, [range, customFrom, customTo, employeeFilter]);

  const handleExport = () => {
    if (records.length === 0) {
      toast({ title: "沒有可匯出的資料", icon: "alert" });
      return;
    }
    const headers = ["員工", "Email", "類型", "打卡時間", "備註", "管理員修正", "已作廢", "作廢者", "作廢原因"];
    const rows = records.map(r => [
      r.employeeName, r.adminEmail, ATT_TYPE[r.type]?.label ?? r.type,
      twDateTime(r.clockedAt), r.note ?? "", r.editedBy ?? "",
      r.voidedAt ? "是" : "", r.voidedBy ?? "", r.voidReason ?? "",
    ]);
    const stamp = new Date().toLocaleDateString("en-CA", { timeZone: "Asia/Taipei" });
    csvDownload(`attendance_${stamp}.csv`, headers, rows);
    toast({ title: "CSV 已匯出", message: `${records.length} 筆打卡紀錄`, icon: "download" });
  };

  const confirmVoid = async ({ reason }) => {
    const res = await fetch(`/api/admin/attendance/${deleting.id}`, {
      method: "DELETE",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ reason }),
    });
    setDeleting(null);
    if (res.ok) {
      toast({ title: "已作廢紀錄", icon: "check" });
      load();
    } else {
      const d = await res.json().catch(() => ({}));
      toast({ title: "作廢失敗", message: d.error ?? "請稍後再試", icon: "alert" });
    }
  };

  const submitOvertimeAction = async ({ reason }) => {
    if (!otAction) return;
    const res = await fetch(`/api/admin/overtime/${otAction.req.id}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ action: otAction.type, reason }),
    });
    setOtAction(null);
    if (res.ok) {
      toast({ title: otAction.type === "approve" ? "已核准加班" : "已駁回加班申請", icon: "check" });
      loadOvertimeRequests();
      // 立刻重新檢查通知鈴鐺的待審核件數（見 app.jsx），不用等下次登入才刷新
      window.__recheckOvertime?.();
    } else {
      const d = await res.json().catch(() => ({}));
      toast({ title: "操作失敗", message: d.error ?? "請稍後再試", icon: "alert" });
    }
  };

  const columns = [
    ...(canManageAll ? [{
      title: "員工",
      render: r => (
        <div style={{ display: "flex", alignItems: "center", gap: 8, opacity: r.voidedAt ? 0.5 : 1 }}>
          {r.employeeAvatar
            ? <img src={r.employeeAvatar} style={{ width: 24, height: 24, borderRadius: "50%", objectFit: "cover" }}/>
            : <Avatar hue={25} glyph={r.employeeName[0]?.toUpperCase() ?? "?"} size={24}/>}
          <span style={{ fontSize: 13.5 }}>{r.employeeName}</span>
        </div>
      ),
    }] : []),
    { title: "類型", render: r => (
      <span style={{ opacity: r.voidedAt ? 0.5 : 1, textDecoration: r.voidedAt ? "line-through" : "none" }}>
        <Badge tone={ATT_TYPE[r.type]?.tone} size="sm">{ATT_TYPE[r.type]?.label ?? r.type}</Badge>
      </span>
    ) },
    { title: "打卡時間", render: r => (
      <span className="mono" style={{ fontSize: 13, opacity: r.voidedAt ? 0.5 : 1, textDecoration: r.voidedAt ? "line-through" : "none" }}>
        {twDateTime(r.clockedAt)}
      </span>
    ) },
    { title: "備註", render: r => <span style={{ fontSize: 13, color: "var(--ink-3)", opacity: r.voidedAt ? 0.5 : 1 }}>{r.note || "—"}</span> },
    { title: "狀態", render: r => (
      <div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
        {r.editedBy && <Badge tone="warn" size="sm">管理員修正</Badge>}
        {r.voidedAt && (
          <span title={r.voidReason ?? ""}>
            <Badge tone="danger" size="sm">
              已作廢 · {r.voidedBy?.split("@")[0] ?? "?"} · {twDateTime(r.voidedAt)}
            </Badge>
          </span>
        )}
      </div>
    ) },
    ...(canEdit ? [{
      title: "", align: "right",
      // 自己的紀錄不能自己改/作廢（見 API 的同一條規則）：修正一定要換另一個有權限的
      // 管理員來動手，打卡紀錄才有公信力。已作廢的紀錄也不能再動。這裡不顯示按鈕，
      // 避免點了才被 403/409 打槍。
      render: r => {
        if (r.voidedAt) return null;
        if (r.adminEmail === adminEmail) return <span style={{ fontSize: 11.5, color: "var(--ink-4)" }}>本人紀錄</span>;
        return (
          <div style={{ display: "flex", gap: 4, justifyContent: "flex-end" }}>
            <Button size="sm" variant="ghost" icon="edit" onClick={() => setEditing(r)}/>
            <Button size="sm" variant="ghost" icon="ban" onClick={() => setDeleting(r)}/>
          </div>
        );
      },
    }] : []),
  ];

  return (
    <div className="fade-in">
      <PageHeader
        title="員工打卡鐘"
        subtitle={canManageAll ? "全體員工上下班打卡紀錄" : "我的上下班打卡紀錄"}
        actions={<>
          {canManageAll && (
            <Select value={employeeFilter} onChange={setEmployeeFilter} icon="users"
              options={[{ value: "all", label: "全部員工" }, ...employees.map(e => ({ value: e.email, label: e.name }))]}/>
          )}
          <Select value={range} onChange={setRange} icon="calendar" options={ATT_RANGE_OPTS}/>
          {range === "custom" && (
            <>
              <Input type="date" value={customFrom} onChange={e => setCustomFrom(e.target.value)} style={{ width: 150 }}/>
              <span style={{ color: "var(--ink-4)" }}>~</span>
              <Input type="date" value={customTo} onChange={e => setCustomTo(e.target.value)} style={{ width: 150 }}/>
            </>
          )}
          <Button variant="secondary" icon="refresh" onClick={load}>重新整理</Button>
          <Button variant="primary" icon="download" onClick={handleExport}>匯出 CSV</Button>
        </>}
      />

      <ClockInCard
        myLast={myLast} myOvertime={myOvertime}
        onPunched={() => { load(); loadMyLast(); loadMyOvertime(); loadOvertimeRequests(); }}
        onOvertimeApplied={() => { loadMyOvertime(); loadOvertimeRequests(); }}
      />

      <OvertimeSection
        requests={overtimeRequests} canManageAll={otCanManageAll} canReview={otCanReview}
        adminEmail={adminEmail} onAction={setOtAction}
      />

      <HoursSummary records={records} canManageAll={canManageAll}/>

      <Table columns={columns} rows={records} empty={loading ? "載入中…" : "目前沒有打卡紀錄"}/>

      {editing && (
        <EditRecordModal record={editing} onClose={() => setEditing(null)} onSaved={() => { setEditing(null); load(); }}/>
      )}

      {deleting && (
        <ConfirmDialog
          open onClose={() => setDeleting(null)}
          variant="danger" icon="ban" confirmText="確認作廢"
          reasonLabel="作廢原因"
          reasonPlaceholder="例：重複打卡誤植，作廢多餘紀錄。"
          title="作廢打卡紀錄"
          description={`確認作廢「${deleting.employeeName}」於 ${twDateTime(deleting.clockedAt)} 的${ATT_TYPE[deleting.type]?.label}打卡紀錄？原始紀錄仍會保留、標示為已作廢，不會真的消失。`}
          onConfirm={confirmVoid}
        />
      )}

      {otAction && (
        <ConfirmDialog
          open onClose={() => setOtAction(null)}
          variant={otAction.type === "approve" ? "info" : "danger"}
          icon={otAction.type === "approve" ? "check" : "alert"}
          confirmText={otAction.type === "approve" ? "確認核准" : "確認駁回"}
          requireReason={otAction.type === "reject"}
          reasonLabel="駁回原因"
          reasonPlaceholder="例：時數認列有誤，請重新確認。"
          title={otAction.type === "approve"
            ? `核准加班申請 · ${otAction.req.overtimeHours} 小時`
            : `駁回加班申請 · ${otAction.req.overtimeHours} 小時`}
          description={otAction.type === "approve"
            ? `確認核准「${otAction.req.employeeName}」這次加班 ${otAction.req.overtimeHours} 小時，核准後這筆加班算成功。`
            : `確認駁回「${otAction.req.employeeName}」的加班申請，這筆加班不會被採計。`}
          onConfirm={submitOvertimeAction}
        />
      )}
    </div>
  );
};

window.AttendancePage = AttendancePage;
