feat: 完成 R13/R14/R15/R16 小团队管理核心能力
这个提交包含在:
+516
-1
@@ -1,8 +1,523 @@
|
||||
const { z } = require("zod");
|
||||
|
||||
const usersRoutes = require("./users");
|
||||
const { AppError } = require("../utils/AppError");
|
||||
const { sendData } = require("../utils/reply");
|
||||
const { buildAuditLogData } = require("../utils/audit");
|
||||
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags");
|
||||
|
||||
const UUID_SCHEMA = z.string().uuid();
|
||||
const DATE_SCHEMA = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
||||
const PROFILE_PATCH_SCHEMA = z
|
||||
.object({
|
||||
departmentId: UUID_SCHEMA.optional(),
|
||||
directManagerId: UUID_SCHEMA.nullable().optional(),
|
||||
title: z.string().trim().min(1).max(100).optional(),
|
||||
probationEndDate: DATE_SCHEMA.nullable().optional(),
|
||||
lastWorkingDate: DATE_SCHEMA.nullable().optional(),
|
||||
workLocation: z.string().trim().max(100).nullable().optional(),
|
||||
emergencyContactName: z.string().trim().max(100).nullable().optional(),
|
||||
emergencyContactPhone: z.string().trim().max(50).nullable().optional(),
|
||||
notes: z.string().trim().max(1000).nullable().optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine((value) => Object.keys(value).length > 0, { message: "empty_patch" });
|
||||
|
||||
const ONBOARDING_SCHEMA = z.object({
|
||||
effectiveDate: DATE_SCHEMA.optional(),
|
||||
probationEndDate: DATE_SCHEMA.nullable().optional(),
|
||||
notes: z.string().trim().max(500).nullable().optional(),
|
||||
}).strict();
|
||||
|
||||
const REGULARIZE_SCHEMA = z.object({
|
||||
effectiveDate: DATE_SCHEMA.optional(),
|
||||
notes: z.string().trim().max(500).nullable().optional(),
|
||||
}).strict();
|
||||
|
||||
const TRANSFER_SCHEMA = z.object({
|
||||
effectiveDate: DATE_SCHEMA.optional(),
|
||||
departmentId: UUID_SCHEMA.optional(),
|
||||
directManagerId: UUID_SCHEMA.nullable().optional(),
|
||||
title: z.string().trim().min(1).max(100).optional(),
|
||||
notes: z.string().trim().max(500).nullable().optional(),
|
||||
}).strict();
|
||||
|
||||
const OFFBOARDING_SCHEMA = z.object({
|
||||
effectiveDate: DATE_SCHEMA.optional(),
|
||||
lastWorkingDate: DATE_SCHEMA.nullable().optional(),
|
||||
notes: z.string().trim().max(500).nullable().optional(),
|
||||
}).strict();
|
||||
|
||||
function buildValidationError(details, statusCode = 400) {
|
||||
return new AppError({ code: "VALID_001", message: "参数校验失败", statusCode, details });
|
||||
}
|
||||
|
||||
function parseId(value) {
|
||||
const parsed = UUID_SCHEMA.safeParse(value);
|
||||
if (!parsed.success) {
|
||||
throw buildValidationError({ id: "invalid_uuid" });
|
||||
}
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
function parseBody(schema, body) {
|
||||
const parsed = schema.safeParse(body ?? {});
|
||||
if (!parsed.success) {
|
||||
throw buildValidationError(parsed.error.flatten());
|
||||
}
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
async function ensureFeatureEnabled(fastify, request) {
|
||||
const gate = await evaluateFeatureFlag({
|
||||
prisma: fastify.prisma,
|
||||
config: fastify.config,
|
||||
flagKey: FEATURE_FLAG_KEYS.employeeLifecycle,
|
||||
departmentId: request.employee.department_id ?? null,
|
||||
strictDepartment: false,
|
||||
});
|
||||
if (!gate.enabled) {
|
||||
throw buildValidationError({ featureFlag: "employee_lifecycle_v1_disabled", reason: gate.reason }, 409);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
function canManageLifecycle(fastify, request) {
|
||||
return fastify.hasPermission(request.employee, "system.users") || fastify.hasPermission(request.employee, "system.settings") || fastify.hasPermission(request.employee, "*");
|
||||
}
|
||||
|
||||
function mapChecklistTask(item) {
|
||||
return {
|
||||
id: item.id,
|
||||
employeeId: item.employee_id,
|
||||
scene: item.scene,
|
||||
taskType: item.task_type,
|
||||
title: item.title,
|
||||
status: item.status,
|
||||
dueDate: item.due_date?.toISOString?.()?.slice(0, 10) ?? null,
|
||||
completedAt: item.completed_at?.toISOString?.() ?? null,
|
||||
completedBy: item.completed_by ?? null,
|
||||
payloadJson: item.payload_json ?? null,
|
||||
createdAt: item.created_at?.toISOString?.() ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function mapLifecycleEvent(item) {
|
||||
return {
|
||||
id: item.id,
|
||||
employeeId: item.employee_id,
|
||||
eventType: item.event_type,
|
||||
effectiveDate: item.effective_date?.toISOString?.()?.slice(0, 10) ?? null,
|
||||
payloadJson: item.payload_json ?? null,
|
||||
createdAt: item.created_at?.toISOString?.() ?? null,
|
||||
createdBy: item.created_by ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async function getLifecycleBlockers(prisma, employeeId) {
|
||||
const [openAssetCount, pendingWorkflowTaskCount] = await Promise.all([
|
||||
prisma.assetCard.count({ where: { assignee_employee_id: employeeId } }),
|
||||
prisma.workflowTask.count({ where: { assignee_id: employeeId, status: "pending" } }),
|
||||
]);
|
||||
return {
|
||||
openAssetCount,
|
||||
pendingWorkflowTaskCount,
|
||||
canCompleteOffboarding: openAssetCount === 0 && pendingWorkflowTaskCount === 0,
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureEmployeeExists(prisma, employeeId) {
|
||||
const employee = await prisma.employee.findUnique({ where: { id: employeeId } });
|
||||
if (!employee) {
|
||||
throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404 });
|
||||
}
|
||||
return employee;
|
||||
}
|
||||
|
||||
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) {
|
||||
return { autoChanged: false, completedOffboarding: false, blockers: null };
|
||||
}
|
||||
|
||||
const pendingOnboarding = await tx.employeeChecklistTask.count({ where: { employee_id: employeeId, scene: "onboarding", status: "pending" } });
|
||||
if (employee.employment_status === "onboarding" && pendingOnboarding === 0) {
|
||||
await tx.employee.update({ where: { id: employeeId }, data: { employment_status: "probation" } });
|
||||
return { autoChanged: true, completedOffboarding: false, blockers: null };
|
||||
}
|
||||
|
||||
const pendingOffboarding = await tx.employeeChecklistTask.count({ where: { employee_id: employeeId, scene: "offboarding", status: "pending" } });
|
||||
if (employee.employment_status === "offboarding" && pendingOffboarding === 0) {
|
||||
const [openAssetCount, pendingWorkflowTaskCount] = await Promise.all([
|
||||
tx.assetCard.count({ where: { assignee_employee_id: employeeId } }),
|
||||
tx.workflowTask.count({ where: { assignee_id: employeeId, status: "pending" } }),
|
||||
]);
|
||||
|
||||
if (openAssetCount === 0 && pendingWorkflowTaskCount === 0) {
|
||||
await tx.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: {
|
||||
employment_status: "inactive",
|
||||
status: "archived",
|
||||
archived_at: new Date(),
|
||||
token_version: { increment: 1 },
|
||||
},
|
||||
});
|
||||
return { autoChanged: true, completedOffboarding: true, blockers: { openAssetCount, pendingWorkflowTaskCount } };
|
||||
}
|
||||
|
||||
return { autoChanged: false, completedOffboarding: false, blockers: { openAssetCount, pendingWorkflowTaskCount } };
|
||||
}
|
||||
|
||||
return { autoChanged: false, completedOffboarding: false, blockers: null };
|
||||
}
|
||||
|
||||
async function buildEmployeeProfile(prisma, employeeId) {
|
||||
const [employee, checklistTasks, lifecycleEvents, assignedAssets, blockers] = await Promise.all([
|
||||
prisma.employee.findUnique({
|
||||
where: { id: employeeId },
|
||||
select: {
|
||||
id: true,
|
||||
employee_no: true,
|
||||
name: true,
|
||||
phone: true,
|
||||
email: true,
|
||||
status: true,
|
||||
employment_status: true,
|
||||
department_id: true,
|
||||
title: true,
|
||||
direct_manager_id: true,
|
||||
hire_date: true,
|
||||
probation_end_date: true,
|
||||
last_working_date: true,
|
||||
work_location: true,
|
||||
emergency_contact_name: true,
|
||||
emergency_contact_phone: true,
|
||||
notes: true,
|
||||
archived_at: true,
|
||||
roles: { select: { role_code: true } },
|
||||
department: { select: { id: true, name: true } },
|
||||
direct_manager: { select: { id: true, employee_no: true, name: true } },
|
||||
},
|
||||
}),
|
||||
prisma.employeeChecklistTask.findMany({ where: { employee_id: employeeId }, orderBy: [{ created_at: "desc" }] }),
|
||||
prisma.employeeLifecycleEvent.findMany({ where: { employee_id: employeeId }, orderBy: [{ effective_date: "desc" }, { created_at: "desc" }] }),
|
||||
prisma.assetCard.findMany({
|
||||
where: { assignee_employee_id: employeeId },
|
||||
orderBy: { asset_no: "asc" },
|
||||
select: { id: true, asset_no: true, name: true, status: true, location: true },
|
||||
}),
|
||||
getLifecycleBlockers(prisma, employeeId),
|
||||
]);
|
||||
|
||||
if (!employee) {
|
||||
throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404 });
|
||||
}
|
||||
|
||||
return {
|
||||
employee: {
|
||||
id: employee.id,
|
||||
employeeNo: employee.employee_no,
|
||||
name: employee.name,
|
||||
phone: employee.phone,
|
||||
email: employee.email,
|
||||
status: employee.status,
|
||||
employmentStatus: employee.employment_status,
|
||||
departmentId: employee.department_id,
|
||||
title: employee.title,
|
||||
directManagerId: employee.direct_manager_id ?? null,
|
||||
hireDate: employee.hire_date?.toISOString?.()?.slice(0, 10) ?? null,
|
||||
probationEndDate: employee.probation_end_date?.toISOString?.()?.slice(0, 10) ?? null,
|
||||
lastWorkingDate: employee.last_working_date?.toISOString?.()?.slice(0, 10) ?? null,
|
||||
workLocation: employee.work_location ?? null,
|
||||
emergencyContactName: employee.emergency_contact_name ?? null,
|
||||
emergencyContactPhone: employee.emergency_contact_phone ?? null,
|
||||
notes: employee.notes ?? null,
|
||||
archivedAt: employee.archived_at?.toISOString?.() ?? null,
|
||||
roleCodes: employee.roles.map((item) => item.role_code),
|
||||
department: employee.department ? { id: employee.department.id, name: employee.department.name } : null,
|
||||
directManager: employee.direct_manager ? { id: employee.direct_manager.id, employeeNo: employee.direct_manager.employee_no, name: employee.direct_manager.name } : null,
|
||||
},
|
||||
lifecycle: {
|
||||
events: lifecycleEvents.map(mapLifecycleEvent),
|
||||
checklistTasks: checklistTasks.map(mapChecklistTask),
|
||||
blockers,
|
||||
},
|
||||
assignedAssets: assignedAssets.map((item) => ({
|
||||
id: item.id,
|
||||
assetNo: item.asset_no,
|
||||
name: item.name,
|
||||
status: item.status,
|
||||
location: item.location ?? null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async function employeesRoutes(fastify) {
|
||||
await usersRoutes(fastify, { basePath: "/employees" });
|
||||
|
||||
fastify.get("/employees/:id/profile", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
await ensureFeatureEnabled(fastify, request);
|
||||
const employeeId = parseId(request.params?.id);
|
||||
if (!canManageProfile(fastify, request, employeeId)) {
|
||||
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "employee.profile.view" } });
|
||||
}
|
||||
const profile = await buildEmployeeProfile(fastify.prisma, employeeId);
|
||||
return sendData(reply, request, profile);
|
||||
});
|
||||
|
||||
fastify.patch("/employees/:id/profile", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
await ensureFeatureEnabled(fastify, request);
|
||||
const employeeId = parseId(request.params?.id);
|
||||
if (!canManageProfile(fastify, request, employeeId)) {
|
||||
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "employee.profile.write" } });
|
||||
}
|
||||
const body = parseBody(PROFILE_PATCH_SCHEMA, request.body);
|
||||
const managerMode = canManageLifecycle(fastify, request);
|
||||
const selfEditableKeys = ["workLocation", "emergencyContactName", "emergencyContactPhone", "notes"];
|
||||
if (!managerMode && request.employee.id === employeeId) {
|
||||
const invalidKeys = Object.keys(body).filter((key) => !selfEditableKeys.includes(key));
|
||||
if (invalidKeys.length) {
|
||||
throw buildValidationError({ body: `self_profile_fields_forbidden:${invalidKeys.join(",")}` }, 403);
|
||||
}
|
||||
}
|
||||
|
||||
await ensureEmployeeExists(fastify.prisma, employeeId);
|
||||
await fastify.prisma.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 } : {}),
|
||||
...(body.probationEndDate !== undefined ? { probation_end_date: body.probationEndDate ? new Date(`${body.probationEndDate}T00:00:00.000Z`) : null } : {}),
|
||||
...(body.lastWorkingDate !== undefined ? { last_working_date: body.lastWorkingDate ? new Date(`${body.lastWorkingDate}T00:00:00.000Z`) : null } : {}),
|
||||
...(body.workLocation !== undefined ? { work_location: body.workLocation ?? null } : {}),
|
||||
...(body.emergencyContactName !== undefined ? { emergency_contact_name: body.emergencyContactName ?? null } : {}),
|
||||
...(body.emergencyContactPhone !== undefined ? { emergency_contact_phone: body.emergencyContactPhone ?? null } : {}),
|
||||
...(body.notes !== undefined ? { notes: body.notes ?? null } : {}),
|
||||
updated_by: request.employee.id,
|
||||
},
|
||||
});
|
||||
const profile = await buildEmployeeProfile(fastify.prisma, employeeId);
|
||||
return sendData(reply, request, profile);
|
||||
});
|
||||
|
||||
fastify.post("/employees/:id/onboarding/start", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
await ensureFeatureEnabled(fastify, request);
|
||||
if (!canManageLifecycle(fastify, request)) {
|
||||
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
|
||||
}
|
||||
const employeeId = parseId(request.params?.id);
|
||||
const body = parseBody(ONBOARDING_SCHEMA, request.body);
|
||||
const effectiveDate = resolveEffectiveDate(body.effectiveDate);
|
||||
await fastify.prisma.$transaction(async (tx) => {
|
||||
await ensureEmployeeExists(tx, employeeId);
|
||||
await tx.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: {
|
||||
employment_status: "onboarding",
|
||||
probation_end_date: body.probationEndDate ? new Date(`${body.probationEndDate}T00:00:00.000Z`) : null,
|
||||
notes: body.notes !== undefined ? body.notes ?? null : undefined,
|
||||
updated_by: request.employee.id,
|
||||
},
|
||||
});
|
||||
await tx.employeeLifecycleEvent.create({
|
||||
data: {
|
||||
employee_id: employeeId,
|
||||
event_type: "onboard",
|
||||
effective_date: effectiveDate,
|
||||
payload_json: { notes: body.notes ?? null },
|
||||
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 }));
|
||||
await tx.employeeChecklistTask.createMany({ data: checklist, skipDuplicates: false });
|
||||
});
|
||||
const profile = await buildEmployeeProfile(fastify.prisma, employeeId);
|
||||
return sendData(reply, request, profile);
|
||||
});
|
||||
|
||||
fastify.post("/employees/:id/regularize", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
await ensureFeatureEnabled(fastify, request);
|
||||
if (!canManageLifecycle(fastify, request)) {
|
||||
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
|
||||
}
|
||||
const employeeId = parseId(request.params?.id);
|
||||
const body = parseBody(REGULARIZE_SCHEMA, request.body);
|
||||
await fastify.prisma.$transaction(async (tx) => {
|
||||
await ensureEmployeeExists(tx, employeeId);
|
||||
await tx.employee.update({ where: { id: employeeId }, data: { employment_status: "active", updated_by: request.employee.id } });
|
||||
await tx.employeeLifecycleEvent.create({
|
||||
data: {
|
||||
employee_id: employeeId,
|
||||
event_type: "regularize",
|
||||
effective_date: resolveEffectiveDate(body.effectiveDate),
|
||||
payload_json: { notes: body.notes ?? null },
|
||||
created_by: request.employee.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
const profile = await buildEmployeeProfile(fastify.prisma, employeeId);
|
||||
return sendData(reply, request, profile);
|
||||
});
|
||||
|
||||
fastify.post("/employees/:id/transfer", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
await ensureFeatureEnabled(fastify, request);
|
||||
if (!canManageLifecycle(fastify, request)) {
|
||||
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
|
||||
}
|
||||
const employeeId = parseId(request.params?.id);
|
||||
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),
|
||||
});
|
||||
});
|
||||
const profile = await buildEmployeeProfile(fastify.prisma, employeeId);
|
||||
return sendData(reply, request, profile);
|
||||
});
|
||||
|
||||
fastify.post("/employees/:id/offboarding/start", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
await ensureFeatureEnabled(fastify, request);
|
||||
if (!canManageLifecycle(fastify, request)) {
|
||||
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
|
||||
}
|
||||
const employeeId = parseId(request.params?.id);
|
||||
const body = parseBody(OFFBOARDING_SCHEMA, request.body);
|
||||
await fastify.prisma.$transaction(async (tx) => {
|
||||
await ensureEmployeeExists(tx, employeeId);
|
||||
await tx.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: {
|
||||
employment_status: "offboarding",
|
||||
...(body.lastWorkingDate !== undefined ? { last_working_date: body.lastWorkingDate ? new Date(`${body.lastWorkingDate}T00:00:00.000Z`) : null } : {}),
|
||||
updated_by: request.employee.id,
|
||||
},
|
||||
});
|
||||
await tx.employeeLifecycleEvent.create({
|
||||
data: {
|
||||
employee_id: employeeId,
|
||||
event_type: "offboard",
|
||||
effective_date: resolveEffectiveDate(body.effectiveDate),
|
||||
payload_json: { notes: body.notes ?? null, lastWorkingDate: body.lastWorkingDate ?? null },
|
||||
created_by: request.employee.id,
|
||||
},
|
||||
});
|
||||
await tx.employeeChecklistTask.createMany({ data: buildDefaultChecklist("offboarding", employeeId, request.employee.id) });
|
||||
});
|
||||
const profile = await buildEmployeeProfile(fastify.prisma, employeeId);
|
||||
return sendData(reply, request, profile);
|
||||
});
|
||||
|
||||
fastify.get("/employees/:id/lifecycle", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
await ensureFeatureEnabled(fastify, request);
|
||||
const employeeId = parseId(request.params?.id);
|
||||
if (!canManageProfile(fastify, request, employeeId)) {
|
||||
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "employee.lifecycle.view" } });
|
||||
}
|
||||
const profile = await buildEmployeeProfile(fastify.prisma, employeeId);
|
||||
return sendData(reply, request, profile.lifecycle);
|
||||
});
|
||||
|
||||
async function updateChecklistTaskStatus(request, reply, nextStatus) {
|
||||
await ensureFeatureEnabled(fastify, request);
|
||||
const employeeId = parseId(request.params?.id);
|
||||
const taskId = parseId(request.params?.taskId);
|
||||
if (!canManageProfile(fastify, request, employeeId)) {
|
||||
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "employee.lifecycle.write" } });
|
||||
}
|
||||
|
||||
const result = await fastify.prisma.$transaction(async (tx) => {
|
||||
await ensureEmployeeExists(tx, employeeId);
|
||||
const task = await tx.employeeChecklistTask.findFirst({ where: { id: taskId, employee_id: employeeId } });
|
||||
if (!task) {
|
||||
throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404 });
|
||||
}
|
||||
await tx.employeeChecklistTask.update({
|
||||
where: { id: taskId },
|
||||
data: {
|
||||
status: nextStatus,
|
||||
completed_at: nextStatus === "completed" ? new Date() : task.completed_at,
|
||||
completed_by: nextStatus === "completed" ? request.employee.id : task.completed_by,
|
||||
},
|
||||
});
|
||||
return maybeAdvanceLifecycle(tx, employeeId);
|
||||
});
|
||||
|
||||
if (result.completedOffboarding) {
|
||||
await fastify.prisma.auditLog.create({
|
||||
data: buildAuditLogData(request, {
|
||||
actorId: request.employee.id,
|
||||
action: "EMPLOYEE_OFFBOARDING_COMPLETED",
|
||||
targetType: "employee",
|
||||
targetId: employeeId,
|
||||
extra: result.blockers ?? {},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const profile = await buildEmployeeProfile(fastify.prisma, employeeId);
|
||||
return sendData(reply, request, { ...profile.lifecycle, autoAdvanced: result.autoChanged });
|
||||
}
|
||||
|
||||
fastify.post("/employees/:id/checklist-tasks/:taskId/complete", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
return updateChecklistTaskStatus(request, reply, "completed");
|
||||
});
|
||||
|
||||
fastify.post("/employees/:id/checklist-tasks/:taskId/skip", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
return updateChecklistTaskStatus(request, reply, "skipped");
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = employeesRoutes;
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户