import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage"; import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { createPerformanceCycle, fetchPerformanceCycleOverview, fetchPerformanceCycles, patchPerformanceCycle, type PerformanceReviewCycleType, } from "../api/performance"; import { PageHeader } from "../components/common/PageHeader"; import { StatusBadge } from "../components/common/StatusBadge"; import { Button } from "../components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "../components/ui/dialog"; import { Input } from "../components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; import { toast } from "sonner"; type CycleFormState = { name: string; cycleType: PerformanceReviewCycleType; startDate: string; endDate: string; }; const CYCLE_TYPE_OPTIONS: Array<{ value: PerformanceReviewCycleType; label: string }> = [ { value: "monthly", label: "月度" }, { value: "quarterly", label: "季度" }, { value: "semi_annual", label: "半年度" }, { value: "annual", label: "年度" }, ]; function buildEmptyCycleForm(): CycleFormState { return { name: "", cycleType: "quarterly", startDate: "", endDate: "", }; } function errorMessage(error: unknown, fallback: string) { return resolveUserErrorMessage(error, fallback); } function cycleTypeLabel(value: PerformanceReviewCycleType) { return CYCLE_TYPE_OPTIONS.find((item) => item.value === value)?.label ?? value; } function cycleStatusLabel(status: string) { return status === "active" ? "进行中" : status === "completed" ? "已完成" : "草稿"; } export function PerformancePage() { const queryClient = useQueryClient(); const [selectedCycleId, setSelectedCycleId] = useState(""); const [cycleDialogOpen, setCycleDialogOpen] = useState(false); const [editingCycleId, setEditingCycleId] = useState(null); const [cycleForm, setCycleForm] = useState(buildEmptyCycleForm()); const cyclesQuery = useQuery({ queryKey: ["performance-cycles"], queryFn: fetchPerformanceCycles, }); const cycles = cyclesQuery.data?.items ?? []; const selectedCycle = cycles.find((item) => item.id === selectedCycleId) ?? cycles[0] ?? null; const overviewQuery = useQuery({ enabled: Boolean(selectedCycle?.id), queryKey: ["performance-cycle-overview", selectedCycle?.id], queryFn: () => fetchPerformanceCycleOverview(selectedCycle!.id), }); useEffect(() => { if (!cycles.length) { setSelectedCycleId(""); return; } if (!cycles.some((item) => item.id === selectedCycleId)) { const preferred = cycles.find((item) => item.status === "active") ?? cycles[0]; setSelectedCycleId(preferred.id); } }, [cycles, selectedCycleId]); const cycleMutation = useMutation({ mutationFn: async () => { if (editingCycleId) { return patchPerformanceCycle(editingCycleId, cycleForm); } return createPerformanceCycle(cycleForm); }, onSuccess: async (cycle) => { await queryClient.invalidateQueries({ queryKey: ["performance-cycles"] }); setSelectedCycleId(cycle.id); setCycleDialogOpen(false); setEditingCycleId(null); setCycleForm(buildEmptyCycleForm()); toast.success(editingCycleId ? "考核周期已更新" : "考核周期已创建"); }, onError: (error) => toast.error(errorMessage(error, editingCycleId ? "更新周期失败" : "创建周期失败")), }); const activateMutation = useMutation({ mutationFn: (cycleId: string) => patchPerformanceCycle(cycleId, { status: "active" }), onSuccess: async (cycle) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["performance-cycles"] }), queryClient.invalidateQueries({ queryKey: ["employee-stats"] }), ]); setSelectedCycleId(cycle.id); toast.success("考核周期已激活"); }, onError: (error) => toast.error(errorMessage(error, "激活周期失败")), }); const completeMutation = useMutation({ mutationFn: (cycleId: string) => patchPerformanceCycle(cycleId, { status: "completed" }), onSuccess: async (cycle) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["performance-cycles"] }), queryClient.invalidateQueries({ queryKey: ["performance-cycle-overview", cycle.id] }), queryClient.invalidateQueries({ queryKey: ["employee-stats"] }), ]); setSelectedCycleId(cycle.id); toast.success("考核周期已关闭"); }, onError: (error) => toast.error(errorMessage(error, "关闭周期失败")), }); const reviewedItems = overviewQuery.data?.items.filter((item) => item.review) ?? []; const averageScore = reviewedItems.length ? reviewedItems.reduce((sum, item) => sum + (item.review?.overallScore ?? 0), 0) / reviewedItems.length : null; function openCreateDialog() { setEditingCycleId(null); setCycleForm(buildEmptyCycleForm()); setCycleDialogOpen(true); } function openEditDialog(cycle = selectedCycle) { if (!cycle) { return; } setEditingCycleId(cycle.id); setCycleForm({ name: cycle.name, cycleType: cycle.cycleType, startDate: cycle.startDate ?? "", endDate: cycle.endDate ?? "", }); setCycleDialogOpen(true); } return (
} />

考核周期列表

draft 周期可编辑并激活;active 周期可关闭为 completed。

{cycles.length} 个周期
名称 类型 状态 日期 操作 {cycles.length ? ( cycles.map((cycle) => ( setSelectedCycleId(cycle.id)} >
{cycle.name}
{cycleTypeLabel(cycle.cycleType)} {cycle.startDate ?? "-"} 至 {cycle.endDate ?? "-"}
{cycle.status === "draft" ? ( <> ) : null} {cycle.status === "active" ? ( ) : null}
)) ) : ( {cyclesQuery.isLoading ? "加载考核周期中..." : "暂无考核周期"} )}
{selectedCycle ? ( <>
当前查看周期
{selectedCycle.name}
{selectedCycle.startDate ?? "-"} 至 {selectedCycle.endDate ?? "-"}
已评分人数
{reviewedItems.length}
按当前概览中已有评分记录统计
平均分
{averageScore == null ? "-" : averageScore.toFixed(1)}
仅统计已评分记录

评分概览

查看全员在当前选中周期的评分状态,点击可进入员工档案绩效标签。

员工 部门 / 职位 总分 评分人 状态 操作 {overviewQuery.data?.items.length ? ( overviewQuery.data.items.map((item) => (
{item.employee.name}
{item.employee.employeeNo}
{item.employee.department?.name ?? "-"}
{item.employee.title}
{item.review ? item.review.overallScore.toFixed(1) : "-"} {item.review?.reviewer?.name ?? "-"}
)) ) : ( {overviewQuery.isLoading ? "加载评分概览中..." : "当前周期暂无可展示的评分数据"} )}
) : null} {editingCycleId ? "编辑考核周期" : "新建考核周期"}
setCycleForm((prev) => ({ ...prev, name: event.target.value }))} />
setCycleForm((prev) => ({ ...prev, startDate: event.target.value }))} />
setCycleForm((prev) => ({ ...prev, endDate: event.target.value }))} />
); } export default PerformancePage;