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

这个提交包含在:
2026-03-29 12:27:44 +08:00
父节点 c3873999c2
当前提交 d11f5593d1
共修改 17 个文件,包含 1640 行新增和 337 行删除
+29 -52
查看文件
@@ -2,6 +2,8 @@ const sharp = require("sharp");
const { z } = require("zod");
const usersRoutes = require("./users");
const { buildDefaultChecklist, resolveEffectiveDate, applyEmployeeTransfer } = require("../services/employeeLifecycle");
const { buildEmployeeStats } = require("../services/employeeStats");
const { currentDateString } = require("../utils/date");
const { AppError } = require("../utils/AppError");
const { sendData } = require("../utils/reply");
@@ -103,10 +105,6 @@ async function ensureFeatureEnabled(fastify, request) {
}
}
function resolveEffectiveDate(rawValue) {
return rawValue ? new Date(`${rawValue}T00:00:00.000Z`) : new Date();
}
function canManageProfile(fastify, request, employeeId) {
return request.employee.id === employeeId || fastify.hasPermission(request.employee, "system.users") || fastify.hasPermission(request.employee, "report.dashboard.view");
}
@@ -242,26 +240,6 @@ async function ensureEmployeeContractExists(prisma, employeeId, contractId) {
return contract;
}
function buildDefaultChecklist(scene, employeeId, actorId) {
const common = { employee_id: employeeId };
if (scene === "onboarding") {
return [
{ ...common, scene, task_type: "account_setup", title: "完成账号与权限初始化", status: "pending" },
{ ...common, scene, task_type: "asset_issue", title: "确认办公资产发放", status: "pending" },
{ ...common, scene, task_type: "policy_ack", title: "确认制度与入职须知", status: "pending" },
];
}
if (scene === "transfer") {
return [{ ...common, scene, task_type: "handover_note", title: "确认调岗交接说明", status: "pending" }];
}
return [
{ ...common, scene, task_type: "asset_return", title: "确认资产归还/转交", status: "pending" },
{ ...common, scene, task_type: "access_revoke", title: "确认账号和权限回收", status: "pending" },
{ ...common, scene, task_type: "expense_clearance", title: "确认费用与审批待办已清理", status: "pending" },
{ ...common, scene, task_type: "handover_note", title: "确认离职交接说明", status: "pending" },
];
}
async function maybeAdvanceLifecycle(tx, employeeId) {
const employee = await tx.employee.findUnique({ where: { id: employeeId } });
if (!employee) {
@@ -449,7 +427,12 @@ async function buildEmployeeProfile(prisma, employeeId) {
}
async function employeesRoutes(fastify) {
await usersRoutes(fastify, { basePath: "/employees", allowDepartmentFilter: true });
await usersRoutes(fastify, {
basePath: "/employees",
allowDepartmentFilter: true,
listPermissionCodes: ["system.users", "report.dashboard.view"],
enableBatchActions: false,
});
fastify.get("/avatars/:employeeId", async (request, reply) => {
const employeeId = parseId(request.params?.employeeId);
@@ -549,6 +532,17 @@ async function employeesRoutes(fastify) {
return sendData(reply, request, { items });
});
fastify.get("/employees/stats", { preHandler: fastify.authenticate }, async (request, reply) => {
if (!canViewPeopleDirectory(fastify, request)) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "employee.stats.view" } });
}
const stats = await buildEmployeeStats(fastify.prisma, {
timeZone: fastify.config.appTimezone,
});
return sendData(reply, request, stats);
});
fastify.get("/employees/:id/profile", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify, request);
const employeeId = parseId(request.params?.id);
@@ -726,7 +720,7 @@ async function employeesRoutes(fastify) {
created_by: request.employee.id,
},
});
const checklist = buildDefaultChecklist("onboarding", employeeId, request.employee.id).map((item) => ({ ...item, due_date: body.probationEndDate ? new Date(`${body.probationEndDate}T00:00:00.000Z`) : null }));
const checklist = buildDefaultChecklist("onboarding", employeeId).map((item) => ({ ...item, due_date: body.probationEndDate ? new Date(`${body.probationEndDate}T00:00:00.000Z`) : null }));
await tx.employeeChecklistTask.createMany({ data: checklist, skipDuplicates: false });
});
const profile = await buildEmployeeProfile(fastify.prisma, employeeId);
@@ -766,31 +760,14 @@ async function employeesRoutes(fastify) {
const body = parseBody(TRANSFER_SCHEMA, request.body);
await fastify.prisma.$transaction(async (tx) => {
await ensureEmployeeExists(tx, employeeId);
await tx.employee.update({
where: { id: employeeId },
data: {
...(body.departmentId !== undefined ? { department_id: body.departmentId } : {}),
...(body.directManagerId !== undefined ? { direct_manager_id: body.directManagerId } : {}),
...(body.title !== undefined ? { title: body.title } : {}),
updated_by: request.employee.id,
},
});
await tx.employeeLifecycleEvent.create({
data: {
employee_id: employeeId,
event_type: "transfer",
effective_date: resolveEffectiveDate(body.effectiveDate),
payload_json: {
departmentId: body.departmentId ?? null,
directManagerId: body.directManagerId ?? null,
title: body.title ?? null,
notes: body.notes ?? null,
},
created_by: request.employee.id,
},
});
await tx.employeeChecklistTask.createMany({
data: buildDefaultChecklist("transfer", employeeId, request.employee.id),
await applyEmployeeTransfer(tx, {
employeeId,
actorId: request.employee.id,
departmentId: body.departmentId,
directManagerId: body.directManagerId,
title: body.title,
effectiveDate: body.effectiveDate,
notes: body.notes,
});
});
const profile = await buildEmployeeProfile(fastify.prisma, employeeId);
@@ -823,7 +800,7 @@ async function employeesRoutes(fastify) {
created_by: request.employee.id,
},
});
await tx.employeeChecklistTask.createMany({ data: buildDefaultChecklist("offboarding", employeeId, request.employee.id) });
await tx.employeeChecklistTask.createMany({ data: buildDefaultChecklist("offboarding", employeeId) });
});
const profile = await buildEmployeeProfile(fastify.prisma, employeeId);
return sendData(reply, request, profile);