'use client'; import { useEffect, useMemo, useState } from 'react'; import { Search, Plus, ChevronDown, Clock, Pencil, Trash2, X, Download } from 'lucide-react'; import { useOvertimeStore } from '@/stores/useOvertimeStore'; import { useProductStore } from '@/stores/useProductStore'; import { flattenProjects, flattenVersions } from '@/lib/derive'; import { calcDuration } from '@/lib/overtime'; import type { OvertimeRecord } from '@/lib/overtime'; import { Pagination, usePagination } from '@/components/Pagination'; import { DictDrawer } from '@/components/requirement/DictDrawer'; import { MonthPicker } from '@/components/MonthPicker'; import { FilterSelect } from '@/components/FilterSelect'; export default function OvertimePage() { const { records, fetchRecords, createRecord, updateRecord, deleteRecord, reasons, addReason, updateReason, deleteReason } = useOvertimeStore(); const { overview, fetchOverview } = useProductStore(); const allProjects = useMemo(() => flattenProjects(overview), [overview]); const allVersions = useMemo(() => flattenVersions(overview), [overview]); const [search, setSearch] = useState(''); const [projectFilter, setProjectFilter] = useState('all'); const [reasonFilter, setReasonFilter] = useState('all'); const [monthFilter, setMonthFilter] = useState(''); const [showModal, setShowModal] = useState(false); const [editing, setEditing] = useState(null); const [showReasonDrawer, setShowReasonDrawer] = useState(false); useEffect(() => { fetchOverview(); }, [fetchOverview]); useEffect(() => { fetchRecords(); }, [fetchRecords]); const projectName = (id: string) => allProjects.find((p) => p.id === id)?.name ?? '-'; const versionName = (id?: string) => id ? (allVersions.find((v) => v.id === id)?.name ?? '-') : '-'; const reasonName = (id: string) => reasons.find((r) => r.id === id)?.name ?? '-'; const filtered = useMemo(() => { let list = [...records]; if (search) list = list.filter((r) => r.person.includes(search)); if (projectFilter !== 'all') list = list.filter((r) => r.projectId === projectFilter); if (reasonFilter !== 'all') list = list.filter((r) => r.reasonId === reasonFilter); if (monthFilter) list = list.filter((r) => r.startTime.slice(0, 7) === monthFilter); list.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()); return list; }, [records, search, projectFilter, reasonFilter, monthFilter]); const { paged, page, setPage, total, pageSize, setPageSize } = usePagination(filtered, 20); const handleCreate = () => { setEditing(null); setShowModal(true); }; const handleEdit = (r: OvertimeRecord) => { setEditing(r); setShowModal(true); }; const handleExport = () => { const header = ['项目', '版本', '加班人', '开始时间', '结束时间', '时长(h)', '加班原因', '备注']; const rows = filtered.map((r) => [ projectName(r.projectId), versionName(r.versionId), r.person, r.startTime.replace('T', ' '), r.endTime.replace('T', ' '), String(r.duration), reasonName(r.reasonId), r.remark || '', ]); const bom = ''; const csv = [header.join(','), ...rows.map((row) => row.map((c) => `"${c.replace(/"/g, '""')}"`).join(','))].join('\r\n'); const blob = new Blob([bom + csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `加班记录${monthFilter || '_全部'}.csv`; a.click(); URL.revokeObjectURL(url); }; return (

加班记录

{records.length}
{/* Filters */}
setSearch(e.target.value)} placeholder="搜索人员" className="h-8 w-48 rounded-lg border border-[var(--line)] bg-[var(--bg)] pl-8 pr-3 text-[13px] text-[var(--ink)] placeholder:text-[var(--ink-muted)] focus:border-[var(--accent)] focus:outline-none" />
({ value: p.id, label: p.name }))} placeholder="全部项目" allLabel="全部项目" /> ({ value: r.id, label: r.name }))} placeholder="全部原因" allLabel="全部原因" />
{/* Table */}
{filtered.length === 0 ? (

暂无加班记录

) : ( <>
{paged.map((r) => ( ))}
项目 版本 加班人 开始时间 结束时间 时长 加班原因 备注 操作
{projectName(r.projectId)} {versionName(r.versionId)} {r.person} {r.startTime.replace('T', ' ')} {r.endTime.replace('T', ' ')} = 4 ? 'text-red-600' : r.duration >= 2 ? 'text-orange-600' : 'text-[var(--ink)]'}`}> {r.duration}h {reasonName(r.reasonId)} {r.remark || '-'}
)}
{/* Modal */} {showModal && ( ({ id: p.id, name: p.name }))} projects={allProjects.map((p) => ({ id: p.id, name: p.name, productId: p.productId }))} versions={allVersions} reasons={reasons} onClose={() => setShowModal(false)} onSubmit={(data) => { if (editing) updateRecord(editing.id, data); else createRecord(data as any); setShowModal(false); }} /> )} {/* Reason Drawer */} {showReasonDrawer && ( setShowReasonDrawer(false)} onAdd={addReason} onUpdate={updateReason} onDelete={deleteReason} /> )}
); } function OvertimeModal({ initial, products, projects, versions, reasons, onClose, onSubmit }: { initial: OvertimeRecord | null; products: { id: string; name: string }[]; projects: { id: string; name: string; productId: string }[]; versions: { id: string; name: string; projectId?: string }[]; reasons: { id: string; name: string }[]; onClose: () => void; onSubmit: (data: any) => void; }) { const [productId, setProductId] = useState(''); const [projectId, setProjectId] = useState(initial?.projectId ?? ''); const [versionId, setVersionId] = useState(initial?.versionId ?? ''); const [person, setPerson] = useState(initial?.person ?? ''); const [startTime, setStartTime] = useState(initial?.startTime ?? ''); const [endTime, setEndTime] = useState(initial?.endTime ?? ''); const [reasonId, setReasonId] = useState(initial?.reasonId ?? ''); const [remark, setRemark] = useState(initial?.remark ?? ''); // 初始编辑时反推 productId useEffect(() => { if (initial?.projectId) { const proj = projects.find((p) => p.id === initial.projectId); if (proj) setProductId(proj.productId); } }, [initial, projects]); const duration = startTime && endTime ? calcDuration(startTime, endTime) : 0; const filteredProjects = productId ? projects.filter((p) => p.productId === productId) : projects; const filteredVersions = projectId ? versions.filter((v) => (v as any).projectId === projectId) : []; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!projectId || !person.trim() || !startTime || !endTime || !reasonId) return; onSubmit({ projectId, versionId: versionId || undefined, person: person.trim(), startTime, endTime, reasonId, remark: remark.trim() || undefined }); }; return (
e.stopPropagation()}>

{initial ? '编辑加班记录' : '新建加班记录'}

setPerson(e.target.value)} required placeholder="姓名" className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
setStartTime(e.target.value)} required className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
setEndTime(e.target.value)} required className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
{duration > 0 && (
时长:= 4 ? 'text-red-600' : duration >= 2 ? 'text-orange-600' : 'text-[var(--ink)]'}`}>{duration} 小时
)}