'use client'; import { useEffect, useMemo, useState } from 'react'; import { Search, Plus, X, Download } from 'lucide-react'; import { useOvertimeStore } from '@/stores/useOvertimeStore'; import { useProductStore } from '@/stores/useProductStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { useMemberStore } from '@/stores/useMemberStore'; import { flattenProjects, flattenVersions } from '@/lib/derive'; import { calcDuration, filterOvertimeRecordsForViewer } 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'; import { FieldError } from '@/components/FieldError'; import { RouteGuard } from '@/components/auth/Guard'; import { WorkDateTimePicker } from '@/components/WorkDateTimePicker'; export default function OvertimePage() { return ( ); } function OvertimePageContent() { const { records, fetchRecords, createRecord, deleteRecord, reasons, addReason, updateReason, deleteReason } = useOvertimeStore(); const { overview, fetchOverview } = useProductStore(); const { requirements, fetchRequirements } = useRequirementStore(); const { departments, members, roles, fetchMembers } = useMemberStore(); const user = useAuthStore((s) => s.user); const viewerRole = useMemo(() => roles.find((role) => role.id === user?.roleId), [roles, user?.roleId]); 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 [showReasonDrawer, setShowReasonDrawer] = useState(false); useEffect(() => { fetchOverview(); }, [fetchOverview]); useEffect(() => { fetchRequirements(); }, [fetchRequirements]); useEffect(() => { fetchRecords(); }, [fetchRecords]); useEffect(() => { fetchMembers(); }, [fetchMembers]); 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 visibleRecords = useMemo(() => filterOvertimeRecordsForViewer(records, { viewer: user, viewerRole, members, departments, }), [records, user, viewerRole, members, departments]); const filtered = useMemo(() => { let list = [...visibleRecords]; 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; }, [visibleRecords, search, projectFilter, reasonFilter, monthFilter]); const { paged, page, setPage, total, pageSize, setPageSize } = usePagination(filtered, 20); const handleCreate = () => { 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 (

加班记录

{visibleRecords.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 || '-'} {r.createdAt}
)}
{/* 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} requirements={requirements.map((r) => ({ id: r.id, title: r.title, versionId: r.versionId }))} onClose={() => setShowModal(false)} onSubmit={(data) => { createRecord(data as any); setShowModal(false); }} /> )} {/* Reason Drawer */} {showReasonDrawer && ( setShowReasonDrawer(false)} onAdd={addReason} onUpdate={updateReason} onDelete={deleteReason} /> )}
); } function OvertimeModal({ defaultPerson, products, projects, versions, reasons, requirements, onClose, onSubmit }: { defaultPerson: string; products: { id: string; name: string }[]; projects: { id: string; name: string; productId: string }[]; versions: { id: string; name: string; projectId?: string }[]; reasons: { id: string; name: string }[]; requirements: { id: string; title: string; versionId?: string }[]; onClose: () => void; onSubmit: (data: any) => void; }) { const [productId, setProductId] = useState(''); const [projectId, setProjectId] = useState(''); const [versionId, setVersionId] = useState(''); const [startTime, setStartTime] = useState(''); const [endTime, setEndTime] = useState(''); const [reasonId, setReasonId] = useState(''); const [remark, setRemark] = useState(''); const [requirementId, setRequirementId] = useState(''); const [endTimeError, setEndTimeError] = useState(''); 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 filteredRequirements = versionId ? requirements.filter((r) => r.versionId === versionId) : []; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!projectId || !defaultPerson.trim() || !startTime || !endTime || !reasonId) return; if (new Date(endTime).getTime() <= new Date(startTime).getTime()) { setEndTimeError('结束时间必须晚于开始时间'); return; } setEndTimeError(''); onSubmit({ projectId, versionId: versionId || undefined, requirementId: requirementId || undefined, person: defaultPerson.trim(), startTime, endTime, reasonId, remark: remark.trim() || undefined }); }; return (
e.stopPropagation()}>

新建加班记录

{ setStartTime(next); setEndTimeError(''); }} placeholder="选择加班开始时间" defaultHour={19} />
{ setEndTime(next); setEndTimeError(''); }} placeholder="选择加班结束时间" defaultHour={21} popoverAlign="right" />
{duration > 0 && (
时长:= 4 ? 'text-red-600' : duration >= 2 ? 'text-orange-600' : 'text-[var(--ink)]'}`}>{duration} 小时
)}
{filteredRequirements.length > 0 && (
)}