import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { CalendarDays, Plus, LogIn, LogOut } from "lucide-react"; import { getTimesheet, getAttendance, getLeave, getOvertime, punch, createLeave, createOvertime, cancelLeave, } from "@/lib/api"; import { Card, Button, Badge, Stat, PageHeader, Modal, Field, Input, Select, Textarea, Tabs, Progress, EmptyState, LoadingState, } from "@/components/ui"; import { formatDate, formatTime, minutesToHM, REQ_STATUS_META, LEAVE_LABELS, classNames, } from "@/lib/format"; import type { LeaveType } from "@/types"; const THIS_MONTH = new Date().toISOString().slice(0, 7); export function AttendancePage() { const qc = useQueryClient(); const [tab, setTab] = useState("timesheet"); const [leaveOpen, setLeaveOpen] = useState(false); const [otOpen, setOtOpen] = useState(false); const tsQ = useQuery({ queryKey: ["timesheet"], queryFn: () => getTimesheet({}) }); const attQ = useQuery({ queryKey: ["attendance", THIS_MONTH], queryFn: () => getAttendance({ month: THIS_MONTH }) }); const leaveQ = useQuery({ queryKey: ["leave-mine"], queryFn: () => getLeave() }); const otQ = useQuery({ queryKey: ["ot-mine"], queryFn: () => getOvertime() }); const punchM = useMutation({ mutationFn: punch, onSuccess: () => { qc.invalidateQueries({ queryKey: ["attendance", THIS_MONTH] }); qc.invalidateQueries({ queryKey: ["timesheet"] }); }, }); const ts = tsQ.data; return (
} /> {ts && (
월 근로 달성률
{ts.fulfillmentPct.toFixed(0)}%
= 100 ? "#12B76A" : "#11224F"} />
)}
l.status === "pending").length }, { key: "overtime", label: "초과근무", badge: otQ.data?.filter((o) => o.status === "pending").length }, ]} /> {tab === "leave" && } {tab === "overtime" && }
{tab === "timesheet" && ( attQ.isLoading ? : (attQ.data?.length ?? 0) === 0 ? } /> : (
{attQ.data!.map((a) => ( ))}
날짜출근퇴근근무시간비고
{formatDate(a.date)} {formatTime(a.clockIn)} {formatTime(a.clockOut)} {minutesToHM(a.workMinutes)} {a.note || "—"}
) )} {tab === "leave" && ( leaveQ.isLoading ? : (leaveQ.data?.length ?? 0) === 0 ? : ( {leaveQ.data!.map((l) => { const m = REQ_STATUS_META[l.status]; return ( ); })}
종류기간일수사유상태
{LEAVE_LABELS[l.type]} {formatDate(l.startDate)}{l.endDate && l.endDate !== l.startDate ? ` ~ ${formatDate(l.endDate)}` : ""} {l.days}일 {l.reason} {l.status === "pending" && }
) )} {tab === "overtime" && ( otQ.isLoading ? : (otQ.data?.length ?? 0) === 0 ? : ( {otQ.data!.map((o) => { const m = REQ_STATUS_META[o.status]; return ( ); })}
날짜시간사유상태
{formatDate(o.date)} {minutesToHM(o.minutes)} {o.reason}
) )}
setLeaveOpen(false)} onDone={() => qc.invalidateQueries({ queryKey: ["leave-mine"] })} /> setOtOpen(false)} onDone={() => qc.invalidateQueries({ queryKey: ["ot-mine"] })} /> ); } function LeaveModal({ open, onClose, onDone }: { open: boolean; onClose: () => void; onDone: () => void }) { const [type, setType] = useState("annual"); const [startDate, setStart] = useState(""); const [endDate, setEnd] = useState(""); const [reason, setReason] = useState(""); const m = useMutation({ mutationFn: () => createLeave({ type, startDate, endDate: endDate || startDate, reason }), onSuccess: () => { onDone(); onClose(); setReason(""); }, }); return ( } >
setStart(e.target.value)} /> setEnd(e.target.value)} />