ATTV1 实现考勤统一入口与企微同步

这个提交包含在:
2026-05-17 16:56:24 +08:00
父节点 d50b7db143
当前提交 916cc3b7f7
共修改 51 个文件,包含 3323 行新增和 1762 行删除
+142 -2
查看文件
@@ -56,7 +56,7 @@ const REMIND_TASK_BODY_SCHEMA = z.object({
const TASK_LIST_QUERY_SCHEMA = z.object({
status: z.enum(["pending", "completed", "rejected", "cancelled", "skipped"]).optional(),
entityType: z.enum(["reimbursement", "leave", "purchase_request", "travel_request"]).optional(),
entityType: z.enum(["reimbursement", "leave", "attendance_correction", "purchase_request", "travel_request"]).optional(),
page: z.coerce.number().int().min(1).optional(),
pageSize: z.coerce.number().int().min(1).max(100).optional(),
});
@@ -373,6 +373,7 @@ function mapTaskSummary(task, allowedActions = []) {
function fallbackBusinessTitle(entityType) {
if (entityType === "reimbursement") return "报销单";
if (entityType === "leave") return "请假申请";
if (entityType === "attendance_correction") return "考勤修正";
if (entityType === "purchase_request") return "采购申请";
if (entityType === "travel_request") return "差旅申请";
return "业务单据";
@@ -421,6 +422,29 @@ function buildTaskBusinessSummary(task) {
};
}
if (workflowInstance.entity_type === "attendance_correction" && workflowInstance.attendance_correction) {
const correction = workflowInstance.attendance_correction;
const kindLabel =
correction.kind === "missed_punch"
? "补卡"
: correction.kind === "out"
? "外出"
: correction.kind === "overtime"
? "加班"
: "考勤修正";
return {
...fallback,
reference: correction.work_date?.toISOString?.().slice(0, 10) ?? workflowInstance.entity_id,
title: `${kindLabel}:${correction.reason ?? "考勤修正"}`,
applicantName: correction.owner?.name ?? null,
duration:
correction.start_at && correction.end_at
? `${correction.start_at.toISOString().slice(11, 16)}-${correction.end_at.toISOString().slice(11, 16)}`
: null,
businessStatus: correction.status ?? fallback.businessStatus,
};
}
if (workflowInstance.entity_type === "purchase_request" && workflowInstance.purchase_request) {
const purchaseRequest = workflowInstance.purchase_request;
const amount = toMoneyString(purchaseRequest.total_amount);
@@ -546,7 +570,10 @@ function listAllowedActionsForTask(request, task, actionable) {
const approvePermission = resolveEntityPermission(task.workflow_instance?.entity_type, "approve");
const canApprove = approvePermission && hasPermission(request.employee.permissionCodes, approvePermission);
if (canApprove) {
actions.push("approve", "reject", "return");
actions.push("approve", "reject");
if (task.workflow_instance?.entity_type !== "attendance_correction") {
actions.push("return");
}
}
if (canApprove && hasPermission(request.employee.permissionCodes, "expense.transfer")) {
@@ -714,6 +741,28 @@ async function loadLeaveNotifyContext(tx, leaveRequestId) {
});
}
async function loadAttendanceCorrectionNotifyContext(tx, correctionId) {
return tx.attendanceCorrection.findUnique({
where: { id: correctionId },
select: {
id: true,
owner_id: true,
kind: true,
status: true,
work_date: true,
start_at: true,
end_at: true,
reason: true,
owner: {
select: {
id: true,
name: true,
},
},
},
});
}
async function loadPurchaseNotifyContext(tx, purchaseRequestId) {
return tx.purchaseRequest.findUnique({
where: { id: purchaseRequestId },
@@ -771,6 +820,14 @@ async function loadWorkflowNotifyContext(tx, entityType, entityId) {
};
}
if (normalizedEntityType === 'attendance_correction') {
const record = await loadAttendanceCorrectionNotifyContext(tx, entityId);
return {
record,
recipientEmployeeIds: uniq([record?.owner_id]),
};
}
if (normalizedEntityType === 'purchase_request') {
const record = await loadPurchaseNotifyContext(tx, entityId);
return {
@@ -839,6 +896,10 @@ function collectWorkflowApplicantIds(workflowInstance, record) {
return uniq([record?.owner_id, workflowInstance.submitter_id]);
}
if (entityType === "attendance_correction") {
return uniq([record?.owner_id, workflowInstance.submitter_id]);
}
if (entityType === "purchase_request") {
return uniq([record?.applicant_id, record?.submitter_id, workflowInstance.submitter_id]);
}
@@ -895,6 +956,7 @@ function resolveWorkflowOutcomeNotificationType(entityType, outcome) {
if (outcome === 'approved') {
if (normalizedEntityType === 'reimbursement') return NOTIFICATION_TYPES.reimbursementApproved;
if (normalizedEntityType === 'leave') return NOTIFICATION_TYPES.leaveApproved;
if (normalizedEntityType === 'attendance_correction') return NOTIFICATION_TYPES.attendanceCorrectionApproved;
if (normalizedEntityType === 'purchase_request') return NOTIFICATION_TYPES.purchaseRequestApproved;
if (normalizedEntityType === 'travel_request') return NOTIFICATION_TYPES.travelRequestApproved;
}
@@ -902,6 +964,7 @@ function resolveWorkflowOutcomeNotificationType(entityType, outcome) {
if (outcome === 'rejected') {
if (normalizedEntityType === 'reimbursement') return NOTIFICATION_TYPES.reimbursementRejected;
if (normalizedEntityType === 'leave') return NOTIFICATION_TYPES.leaveRejected;
if (normalizedEntityType === 'attendance_correction') return NOTIFICATION_TYPES.attendanceCorrectionRejected;
if (normalizedEntityType === 'purchase_request') return NOTIFICATION_TYPES.purchaseRequestRejected;
if (normalizedEntityType === 'travel_request') return NOTIFICATION_TYPES.travelRequestRejected;
}
@@ -1547,6 +1610,14 @@ async function executeApproveWorkflowTask({
createdBy: request.employee.id,
});
}
} else if (workflowInstance.entity_type === "attendance_correction") {
await tx.attendanceCorrection.update({
where: { id: workflowInstance.entity_id },
data: {
status: "approved",
updated_by: request.employee.id,
},
});
} else if (workflowInstance.entity_type === "purchase_request") {
await tx.purchaseRequest.update({
where: { id: workflowInstance.entity_id },
@@ -1633,6 +1704,7 @@ async function executeApproveWorkflowTask({
workflowInstanceId: workflowInstance.id,
nextTaskId: null,
reimbursementStatus: workflowInstance.entity_type === "reimbursement" ? "approved" : null,
attendanceCorrectionStatus: workflowInstance.entity_type === "attendance_correction" ? "approved" : null,
purchaseRequestStatus: workflowInstance.entity_type === "purchase_request" ? "approved" : null,
travelRequestStatus: workflowInstance.entity_type === "travel_request" ? "approved" : null,
};
@@ -1736,6 +1808,14 @@ async function executeRejectWorkflowTask({
createdBy: request.employee.id,
});
}
} else if (workflowInstance.entity_type === "attendance_correction") {
await tx.attendanceCorrection.update({
where: { id: workflowInstance.entity_id },
data: {
status: "rejected",
updated_by: request.employee.id,
},
});
} else if (workflowInstance.entity_type === "purchase_request") {
await tx.purchaseRequest.update({
where: { id: workflowInstance.entity_id },
@@ -1807,6 +1887,7 @@ async function executeRejectWorkflowTask({
outcome: "rejected",
workflowInstanceId: workflowInstance.id,
reimbursementStatus: workflowInstance.entity_type === "reimbursement" ? "rejected" : null,
attendanceCorrectionStatus: workflowInstance.entity_type === "attendance_correction" ? "rejected" : null,
purchaseRequestStatus: workflowInstance.entity_type === "purchase_request" ? "rejected" : null,
travelRequestStatus: workflowInstance.entity_type === "travel_request" ? "rejected" : null,
};
@@ -1978,6 +2059,21 @@ async function workflowRoutes(fastify) {
},
},
},
attendance_correction: {
select: {
status: true,
kind: true,
work_date: true,
start_at: true,
end_at: true,
reason: true,
owner: {
select: {
name: true,
},
},
},
},
purchase_request: {
select: {
request_no: true,
@@ -2130,6 +2226,27 @@ async function workflowRoutes(fastify) {
},
},
},
attendance_correction: {
select: {
id: true,
status: true,
kind: true,
work_date: true,
missed_punch_at: true,
start_at: true,
end_at: true,
reason: true,
location_summary: true,
compensation_policy: true,
owner: {
select: {
id: true,
employee_no: true,
name: true,
},
},
},
},
purchase_request: {
select: {
id: true,
@@ -2372,6 +2489,29 @@ async function workflowRoutes(fastify) {
: null,
}
: null,
attendanceCorrection: task.workflow_instance?.attendance_correction
? {
id: task.workflow_instance.attendance_correction.id,
status: task.workflow_instance.attendance_correction.status,
kind: task.workflow_instance.attendance_correction.kind,
workDate: formatDateOnly(task.workflow_instance.attendance_correction.work_date),
missedPunchAt:
task.workflow_instance.attendance_correction.missed_punch_at?.toISOString?.() ?? null,
startAt: task.workflow_instance.attendance_correction.start_at?.toISOString?.() ?? null,
endAt: task.workflow_instance.attendance_correction.end_at?.toISOString?.() ?? null,
reason: task.workflow_instance.attendance_correction.reason ?? null,
locationSummary: task.workflow_instance.attendance_correction.location_summary ?? null,
compensationPolicy:
task.workflow_instance.attendance_correction.compensation_policy ?? null,
owner: task.workflow_instance.attendance_correction.owner
? {
id: task.workflow_instance.attendance_correction.owner.id,
employeeNo: task.workflow_instance.attendance_correction.owner.employee_no,
name: task.workflow_instance.attendance_correction.owner.name,
}
: null,
}
: null,
purchaseRequest: task.workflow_instance?.purchase_request
? {
id: task.workflow_instance.purchase_request.id,