R44 A2-A8 完成员工高级筛选批量操作与统计页

这个提交包含在:
2026-03-29 12:27:44 +08:00
父节点 c3873999c2
当前提交 d11f5593d1
共修改 17 个文件,包含 1640 行新增和 337 行删除
+121
查看文件
@@ -0,0 +1,121 @@
import { Link } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { fetchEmployeeStats, type EmployeeEmploymentStatus } from "../api/employees";
import { PageHeader } from "../components/common/PageHeader";
import { Button } from "../components/ui/button";
const EMPLOYMENT_STATUS_LABELS: Record<EmployeeEmploymentStatus, string> = {
onboarding: "待入职",
probation: "试用期",
active: "在职",
offboarding: "离职办理中",
inactive: "已离职",
};
function employmentStatusLabel(status: EmployeeEmploymentStatus) {
return EMPLOYMENT_STATUS_LABELS[status] ?? status;
}
export function PeopleStatsPage() {
const statsQuery = useQuery({
queryKey: ["employee-stats"],
queryFn: fetchEmployeeStats,
});
if (statsQuery.isLoading) {
return <div className="card-std p-6 text-sm text-zinc-500">加载员工统计中...</div>;
}
if (statsQuery.isError || !statsQuery.data) {
return <div className="card-std p-6 text-sm text-red-600">加载员工统计失败</div>;
}
const stats = statsQuery.data;
return (
<div className="space-y-6">
<PageHeader
title="员工统计"
subtitle={`统计口径截至 ${stats.asOfDate},仅展示聚合数据。`}
actions={
<Button asChild variant="secondary" type="button">
<Link to="/people">返回人员管理</Link>
</Button>
}
/>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<section className="card-std p-5">
<div className="text-sm text-zinc-500">在职人数</div>
<div className="mt-2 text-3xl font-semibold text-zinc-900">{stats.employmentStatusSummary.total}</div>
<div className="mt-2 text-xs text-zinc-500">按 active 账号员工汇总</div>
</section>
<section className="card-std p-5">
<div className="text-sm text-zinc-500">近 30 天入职</div>
<div className="mt-2 text-3xl font-semibold text-zinc-900">{stats.recentMovements.hires}</div>
<div className="mt-2 text-xs text-zinc-500">
{stats.recentMovements.dateFrom} 至 {stats.recentMovements.dateTo}
</div>
</section>
<section className="card-std p-5">
<div className="text-sm text-zinc-500">近 30 天离职</div>
<div className="mt-2 text-3xl font-semibold text-zinc-900">{stats.recentMovements.departures}</div>
<div className="mt-2 text-xs text-zinc-500">
{stats.recentMovements.dateFrom} 至 {stats.recentMovements.dateTo}
</div>
</section>
<section className="card-std p-5">
<div className="text-sm text-zinc-500">30 天内合同到期</div>
<div className="mt-2 text-3xl font-semibold text-zinc-900">{stats.expiringContracts.count}</div>
<div className="mt-2 text-xs text-zinc-500">
{stats.expiringContracts.dateFrom} 至 {stats.expiringContracts.dateTo}
</div>
</section>
</div>
<div className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
<section className="card-std p-6">
<div className="flex items-center justify-between gap-3">
<div>
<h2 className="text-lg font-semibold text-zinc-900">雇用状态分布</h2>
<p className="mt-1 text-sm text-zinc-500">按当前 active 账号员工的 employment status 聚合。</p>
</div>
</div>
<div className="mt-6 space-y-3">
{stats.employmentStatusSummary.items.length ? (
stats.employmentStatusSummary.items.map((item) => (
<div key={item.status} className="flex items-center justify-between rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-3">
<div className="text-sm font-medium text-zinc-800">{employmentStatusLabel(item.status)}</div>
<div className="text-lg font-semibold text-zinc-900">{item.count}</div>
</div>
))
) : (
<div className="text-sm text-zinc-500">暂无雇用状态统计。</div>
)}
</div>
</section>
<section className="card-std p-6">
<div>
<h2 className="text-lg font-semibold text-zinc-900">部门人数分布</h2>
<p className="mt-1 text-sm text-zinc-500">按当前 active 账号员工的部门归属统计。</p>
</div>
<div className="mt-6 space-y-3">
{stats.departmentHeadcount.length ? (
stats.departmentHeadcount.map((item) => (
<div key={item.departmentId} className="flex items-center justify-between rounded-2xl border border-zinc-200 bg-white px-4 py-3">
<div className="text-sm font-medium text-zinc-800">{item.departmentName}</div>
<div className="text-lg font-semibold text-zinc-900">{item.count}</div>
</div>
))
) : (
<div className="text-sm text-zinc-500">暂无部门人数统计。</div>
)}
</div>
</section>
</div>
</div>
);
}
export default PeopleStatsPage;