R44 A2-A8 完成员工高级筛选批量操作与统计页
这个提交包含在:
@@ -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;
|
||||
在新工单中引用
屏蔽一个用户