ATTV1 实现考勤统一入口与企微同步
这个提交包含在:
+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,
|
||||
|
||||
在新工单中引用
屏蔽一个用户