import { useEffect, useMemo, useRef, useState } from "react"; import { Link, useLocation, useParams, useSearchParams } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { createLeaveRequest, fetchLeaveBalances, fetchLeaveRequestDetail, fetchLeaveRequests, fetchLeaveTypes, patchLeaveRequest, submitLeaveRequest, type LeaveRequestItem, type LeaveRequestStatus, type LeaveSession, } from "../api/leave"; import { useAuth } from "../auth/AuthContext"; import { hasPermission } from "../auth/permissions"; import { PageHeader } from "../components/common/PageHeader"; import { MobileDetailHeader } from "../components/mobile/MobileDetailHeader"; import { Button } from "../components/ui/button"; import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs"; import { PageScaffold } from "../components/patterns/PageScaffold"; import { resolveErrorMessage } from "../lib/resolveErrorMessage"; import { formatApprovalHistoryTime, resolveLeaveStatusHint, workflowActionLabel, } from "../lib/approvalDisplay"; type LeaveStatusFilter = "all" | LeaveRequestStatus; type LeaveView = "form" | "records"; const STATUS_TABS: Array<{ value: LeaveStatusFilter; label: string }> = [ { value: "all", label: "全部" }, { value: "draft", label: "草稿" }, { value: "reviewing", label: "审批中" }, { value: "approved", label: "已通过" }, { value: "rejected", label: "已驳回" }, ]; const STATUS_LABEL: Record = { draft: "草稿", reviewing: "审批中", approved: "已通过", rejected: "已驳回", cancelled: "已取消", }; function leaveProgressFallback(status: LeaveRequestStatus | undefined) { if (status === "draft") return "/mobile/approvals?progress=draft"; if (status === "approved") return "/mobile/approvals?progress=approved"; if (status === "rejected" || status === "cancelled") return "/mobile/approvals?progress=returned"; return "/mobile/approvals?progress=reviewing"; } function isLeaveStatusFilter(value: string | null): value is LeaveStatusFilter { return ["all", "draft", "reviewing", "approved", "rejected", "cancelled"].includes(value ?? ""); } function resolveLeaveView(value: string | null): LeaveView { return value === "form" ? "form" : "records"; } function todayDate() { return new Date().toISOString().slice(0, 10); } function formatSession(value: LeaveSession) { return value === "am" ? "上午" : "下午"; } function formatLeaveRange(item: LeaveRequestItem) { return `${item.startDate} ${formatSession(item.startSession)} - ${item.endDate} ${formatSession(item.endSession)}`; } function LeaveRecordCard({ item, isSaving, onEdit, onSubmit, detailTo, currentMobileSource, showActions = true, }: { item: LeaveRequestItem; isSaving: boolean; onEdit: (item: LeaveRequestItem) => void; onSubmit: (item: LeaveRequestItem) => void; detailTo?: string; currentMobileSource?: string; showActions?: boolean; }) { const canEdit = item.status === "draft" || item.status === "rejected"; const canSubmit = item.status === "draft" || item.status === "rejected"; const approvalHistory = item.approvalHistory ?? []; const content = ( <>
{item.leaveType?.name ?? "请假"}
{formatLeaveRange(item)}
{STATUS_LABEL[item.status]}
天数
{item.requestedDays}
附件
{item.attachment?.originalName ?? "-"}
{item.reason ?

{item.reason}

: null}
{resolveLeaveStatusHint(item)}
{approvalHistory.length ? (
最近审批:{workflowActionLabel(approvalHistory[0].action)} {approvalHistory[0].comment ? ` · ${approvalHistory[0].comment}` : ""} {" · "} {formatApprovalHistoryTime(approvalHistory[0].createdAt)}
) : null} ); return (
{detailTo ? ( {content} ) : content} {showActions && (canEdit || canSubmit) ? (
{canEdit ? ( ) : null} {canSubmit ? ( ) : null}
) : null}
); } export function MobileLeavePage() { const queryClient = useQueryClient(); const location = useLocation(); const { leaveRequestId = "" } = useParams(); const [searchParams, setSearchParams] = useSearchParams(); const { permissions } = useAuth(); const canCreateLeave = hasPermission(permissions, "leave.create"); const [view, setView] = useState(() => resolveLeaveView(searchParams.get("view"))); const [statusFilter, setStatusFilter] = useState(() => { const queryStatus = searchParams.get("status"); return isLeaveStatusFilter(queryStatus) ? queryStatus : "all"; }); const [editingItem, setEditingItem] = useState(null); const [leaveTypeId, setLeaveTypeId] = useState(""); const [startDate, setStartDate] = useState(todayDate); const [endDate, setEndDate] = useState(todayDate); const [startSession, setStartSession] = useState("am"); const [endSession, setEndSession] = useState("pm"); const [reason, setReason] = useState(""); const [attachmentFile, setAttachmentFile] = useState(null); const attachmentInputRef = useRef(null); const currentMobileSource = useMemo(() => { const params = new URLSearchParams(location.search); params.set("view", view); params.set("status", statusFilter); const query = params.toString(); return `${location.pathname}${query ? `?${query}` : ""}`; }, [location.pathname, location.search, statusFilter, view]); useEffect(() => { setView(resolveLeaveView(searchParams.get("view"))); const queryStatus = searchParams.get("status"); setStatusFilter(isLeaveStatusFilter(queryStatus) ? queryStatus : "all"); }, [searchParams]); const leaveTypesQuery = useQuery({ queryKey: ["mobile", "leave", "types"], queryFn: () => fetchLeaveTypes({ enabled: true }), }); const balancesQuery = useQuery({ queryKey: ["mobile", "leave", "balances"], queryFn: () => fetchLeaveBalances(), }); const requestsQuery = useQuery({ queryKey: ["mobile", "leave", "requests", statusFilter], queryFn: () => fetchLeaveRequests({ status: statusFilter === "all" ? undefined : statusFilter, scope: "own", page: 1, pageSize: 20, }), }); const detailQuery = useQuery({ enabled: Boolean(leaveRequestId), queryKey: ["mobile", "leave", "detail", leaveRequestId], queryFn: () => fetchLeaveRequestDetail(leaveRequestId), }); const invalidateLeaveQueries = async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["mobile", "leave"] }), queryClient.invalidateQueries({ queryKey: ["leave"] }), ]); }; const resetForm = () => { setEditingItem(null); setLeaveTypeId(""); setStartDate(todayDate()); setEndDate(todayDate()); setStartSession("am"); setEndSession("pm"); setReason(""); setAttachmentFile(null); if (attachmentInputRef.current) { attachmentInputRef.current.value = ""; } }; const createMutation = useMutation({ mutationFn: () => createLeaveRequest({ leaveTypeId, startDate, endDate, startSession, endSession, reason, file: attachmentFile, }), onSuccess: async () => { toast.success("请假草稿已创建"); resetForm(); await invalidateLeaveQueries(); }, onError: (error) => { toast.error(resolveErrorMessage(error)); }, }); const updateMutation = useMutation({ mutationFn: () => { if (!editingItem) { throw new Error("未选择请假单"); } return patchLeaveRequest(editingItem.id, { leaveTypeId, startDate, endDate, startSession, endSession, reason, file: attachmentFile, }); }, onSuccess: async () => { toast.success("请假单已保存"); resetForm(); await invalidateLeaveQueries(); }, onError: (error) => { toast.error(resolveErrorMessage(error)); }, }); const submitMutation = useMutation({ mutationFn: (item: LeaveRequestItem) => submitLeaveRequest(item.id), onSuccess: async () => { toast.success("请假单已提交"); await invalidateLeaveQueries(); }, onError: (error) => { toast.error(resolveErrorMessage(error)); }, }); const leaveTypes = leaveTypesQuery.data?.items ?? []; const records = requestsQuery.data?.items ?? []; const balances = balancesQuery.data?.items ?? []; const balanceSummary = useMemo( () => balances.reduce( (summary, item) => ({ available: summary.available + item.available, reserved: summary.reserved + item.reserved, used: summary.used + item.used, }), { available: 0, reserved: 0, used: 0 }, ), [balances], ); const isSaving = createMutation.isPending || updateMutation.isPending || submitMutation.isPending; const startEditing = (item: LeaveRequestItem) => { setView("form"); const nextParams = new URLSearchParams(searchParams); nextParams.set("view", "form"); nextParams.set("status", statusFilter); setSearchParams(nextParams, { replace: true }); setEditingItem(item); setLeaveTypeId(item.leaveType?.id ?? ""); setStartDate(item.startDate); setEndDate(item.endDate); setStartSession(item.startSession); setEndSession(item.endSession); setReason(item.reason ?? ""); setAttachmentFile(null); window.scrollTo({ top: 0, behavior: "smooth" }); }; function handleViewChange(value: string) { const nextView = resolveLeaveView(value); setView(nextView); const nextParams = new URLSearchParams(searchParams); nextParams.set("view", nextView); nextParams.set("status", statusFilter); setSearchParams(nextParams, { replace: true }); } function handleStatusChange(value: string) { if (!isLeaveStatusFilter(value)) { return; } setStatusFilter(value); const nextParams = new URLSearchParams(searchParams); nextParams.set("view", "records"); nextParams.set("status", value); setSearchParams(nextParams, { replace: true }); } if (leaveRequestId) { const detail = detailQuery.data; return ( {STATUS_LABEL[detail.status]} : null} testId="mobile-leave-detail-header" /> {detailQuery.isLoading ?
加载中...
: null} {detailQuery.isError ? (
{resolveErrorMessage(detailQuery.error)}
) : null} {detailQuery.data ? ( submitMutation.mutate(nextItem)} showActions={false} /> ) : null}
); } return (
请假记录 新建请假
{view === "form" ? ( <>
可用
{balanceSummary.available.toFixed(1)}
冻结
{balanceSummary.reserved.toFixed(1)}
已用
{balanceSummary.used.toFixed(1)}

{editingItem ? "编辑请假" : "新建请假"}

{editingItem ? ( ) : null}