R35-2 审批通知补结构化摘要
这个提交包含在:
@@ -42,6 +42,8 @@ const leaveListQuerySchema = z.object({
|
||||
scope: z.enum(["own", "all"]).optional(),
|
||||
page: z.coerce.number().int().min(1).optional(),
|
||||
pageSize: z.coerce.number().int().min(1).max(100).optional(),
|
||||
dateFrom: DATE_ONLY_SCHEMA.optional(),
|
||||
dateTo: DATE_ONLY_SCHEMA.optional(),
|
||||
});
|
||||
|
||||
const leaveBalanceQuerySchema = z.object({
|
||||
@@ -107,11 +109,25 @@ function parseLeaveListQuery(query) {
|
||||
throw buildValidationError(parsed.error.flatten());
|
||||
}
|
||||
|
||||
const dateFrom = parsed.data.dateFrom ? new Date(`${parsed.data.dateFrom}T00:00:00.000Z`) : null;
|
||||
const dateTo = parsed.data.dateTo ? new Date(`${parsed.data.dateTo}T00:00:00.000Z`) : null;
|
||||
if (dateFrom && Number.isNaN(dateFrom.getTime())) {
|
||||
throw buildValidationError({ dateFrom: "invalid" });
|
||||
}
|
||||
if (dateTo && Number.isNaN(dateTo.getTime())) {
|
||||
throw buildValidationError({ dateTo: "invalid" });
|
||||
}
|
||||
if (dateFrom && dateTo && dateFrom > dateTo) {
|
||||
throw buildValidationError({ dateTo: "must_be_after_dateFrom" });
|
||||
}
|
||||
|
||||
return {
|
||||
status: parsed.data.status,
|
||||
scope: parsed.data.scope ?? "own",
|
||||
page: parsed.data.page ?? 1,
|
||||
pageSize: parsed.data.pageSize ?? 20,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -415,6 +431,14 @@ async function leaveRoutes(fastify) {
|
||||
const where = {
|
||||
...(query.status ? { status: query.status } : {}),
|
||||
...(query.scope === "all" && canViewAll ? {} : { owner_id: request.employee.id }),
|
||||
...(query.dateFrom || query.dateTo
|
||||
? {
|
||||
AND: [
|
||||
...(query.dateFrom ? [{ end_date: { gte: query.dateFrom } }] : []),
|
||||
...(query.dateTo ? [{ start_date: { lte: query.dateTo } }] : []),
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
|
||||
@@ -27,6 +27,10 @@ const {
|
||||
const { reserveBudgetForEntity, releaseReservedBudgetIfAny } = require("../services/purchaseBudget");
|
||||
const { sendNotificationEmailSafe } = require("../services/email");
|
||||
const { createNotifications, resolveRecipientsForAssignee } = require("../services/notifications");
|
||||
const {
|
||||
buildPendingTaskCopy,
|
||||
buildWorkflowEntityMeta,
|
||||
} = require("../services/notifications/workflowCopy");
|
||||
const { enqueueImNotifications } = require("../services/notifications/imEnqueue");
|
||||
const { NOTIFICATION_TYPES } = require("../constants/notificationTypes");
|
||||
const { openReadStream, StorageNotFoundError, writeBuffer } = require("../services/storage");
|
||||
@@ -914,6 +918,23 @@ function buildSkippedEmailResult(reason, failed = 0) {
|
||||
return { skipped: true, reason, sent: 0, failed };
|
||||
}
|
||||
|
||||
async function loadPurchaseSubmitSummary(prisma, purchaseRequestId) {
|
||||
return prisma.purchaseRequest.findUnique({
|
||||
where: { id: purchaseRequestId },
|
||||
select: {
|
||||
id: true,
|
||||
request_no: true,
|
||||
total_amount: true,
|
||||
applicant: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function finalizePurchaseSubmitSideEffects({
|
||||
prisma,
|
||||
config,
|
||||
@@ -925,13 +946,39 @@ async function finalizePurchaseSubmitSideEffects({
|
||||
assigneeId,
|
||||
assigneeRole,
|
||||
actorId,
|
||||
loadSummary = loadPurchaseSubmitSummary,
|
||||
resolveRecipients = resolveRecipientsForAssignee,
|
||||
createAppNotifications = createNotifications,
|
||||
enqueueIm = enqueueImNotifications,
|
||||
sendEmailSafe = sendNotificationEmailSafe,
|
||||
}) {
|
||||
const title = `【待办】采购申请 ${requestNo} 待审批`;
|
||||
const body = `采购申请 ${requestNo} 已提交,请及时处理。`;
|
||||
let copy = buildPendingTaskCopy(
|
||||
buildWorkflowEntityMeta("purchase_request", { request_no: requestNo }, purchaseRequestId),
|
||||
);
|
||||
try {
|
||||
const summary = await loadSummary(prisma, purchaseRequestId);
|
||||
copy = buildPendingTaskCopy(
|
||||
buildWorkflowEntityMeta(
|
||||
"purchase_request",
|
||||
{
|
||||
...summary,
|
||||
request_no: summary?.request_no ?? requestNo,
|
||||
},
|
||||
purchaseRequestId,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
logger?.error?.(
|
||||
{
|
||||
err: error,
|
||||
requestId,
|
||||
purchaseRequestId,
|
||||
},
|
||||
"Failed to load purchase submit notification summary",
|
||||
);
|
||||
}
|
||||
|
||||
const { title, body } = copy;
|
||||
const targetPath = `/workflow/tasks/${workflowTaskId}`;
|
||||
|
||||
let recipientEmployeeIds = [];
|
||||
|
||||
@@ -8,6 +8,10 @@ const {
|
||||
createNotifications,
|
||||
resolveRecipientsForAssignee,
|
||||
} = require("../services/notifications");
|
||||
const {
|
||||
buildPendingTaskCopy,
|
||||
buildWorkflowEntityMeta,
|
||||
} = require("../services/notifications/workflowCopy");
|
||||
const { enqueueImNotifications } = require("../services/notifications/imEnqueue");
|
||||
const { NOTIFICATION_TYPES } = require("../constants/notificationTypes");
|
||||
const { sendNotificationEmailSafe } = require("../services/email");
|
||||
@@ -468,6 +472,23 @@ function buildSkippedEmailResult(reason, failed = 0) {
|
||||
return { skipped: true, reason, sent: 0, failed };
|
||||
}
|
||||
|
||||
async function loadReimbursementSubmitSummary(prisma, reimbursementId) {
|
||||
return prisma.reimbursementForm.findUnique({
|
||||
where: { id: reimbursementId },
|
||||
select: {
|
||||
id: true,
|
||||
form_number: true,
|
||||
total_amount: true,
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function finalizeReimbursementSubmitSideEffects({
|
||||
prisma,
|
||||
config,
|
||||
@@ -479,13 +500,39 @@ async function finalizeReimbursementSubmitSideEffects({
|
||||
assigneeId,
|
||||
assigneeRole,
|
||||
actorId,
|
||||
loadSummary = loadReimbursementSubmitSummary,
|
||||
resolveRecipients = resolveRecipientsForAssignee,
|
||||
createAppNotifications = createNotifications,
|
||||
enqueueIm = enqueueImNotifications,
|
||||
sendEmailSafe = sendNotificationEmailSafe,
|
||||
}) {
|
||||
const title = `【待办】报销单 ${formNumber} 待审批`;
|
||||
const body = `报销单 ${formNumber} 已提交,请及时处理。`;
|
||||
let copy = buildPendingTaskCopy(
|
||||
buildWorkflowEntityMeta("reimbursement", { form_number: formNumber }, reimbursementId),
|
||||
);
|
||||
try {
|
||||
const summary = await loadSummary(prisma, reimbursementId);
|
||||
copy = buildPendingTaskCopy(
|
||||
buildWorkflowEntityMeta(
|
||||
"reimbursement",
|
||||
{
|
||||
...summary,
|
||||
form_number: summary?.form_number ?? formNumber,
|
||||
},
|
||||
reimbursementId,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
logger?.error?.(
|
||||
{
|
||||
err: error,
|
||||
requestId,
|
||||
reimbursementId,
|
||||
},
|
||||
"Failed to load reimbursement submit notification summary",
|
||||
);
|
||||
}
|
||||
|
||||
const { title, body } = copy;
|
||||
const targetPath = `/workflow/tasks/${workflowTaskId}`;
|
||||
|
||||
let recipientEmployeeIds = [];
|
||||
|
||||
@@ -10,6 +10,10 @@ const { reserveBudgetForEntity, releaseReservedBudgetIfAny } = require("../servi
|
||||
const { parseWorkflowSteps, resolveActionableStep } = require("../utils/workflow");
|
||||
const { sendNotificationEmailSafe } = require("../services/email");
|
||||
const { createNotifications, resolveRecipientsForAssignee } = require("../services/notifications");
|
||||
const {
|
||||
buildPendingTaskCopy,
|
||||
buildWorkflowEntityMeta,
|
||||
} = require("../services/notifications/workflowCopy");
|
||||
const { enqueueImNotifications } = require("../services/notifications/imEnqueue");
|
||||
const { buildTravelPaymentTrack } = require("../services/travelPayments");
|
||||
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags");
|
||||
@@ -589,6 +593,12 @@ async function loadTravelRequestForUpdate(tx, travelRequestId) {
|
||||
end_date: true,
|
||||
estimated_amount: true,
|
||||
status: true,
|
||||
applicant: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
segments: {
|
||||
orderBy: [{ sort_order: "asc" }, { created_at: "asc" }],
|
||||
select: { id: true },
|
||||
@@ -1126,8 +1136,18 @@ async function createTravelRequestsRoutes(fastify) {
|
||||
assigneeId: workflowTask.assignee_id,
|
||||
assigneeRole: workflowTask.assignee_role,
|
||||
});
|
||||
const title = `【待办】差旅申请 ${requestNo} 待审批`;
|
||||
const body = `差旅申请 ${requestNo} 已提交,请及时处理。`;
|
||||
const pendingCopy = buildPendingTaskCopy(
|
||||
buildWorkflowEntityMeta(
|
||||
"travel_request",
|
||||
{
|
||||
request_no: requestNo,
|
||||
estimated_amount: travelRequest.estimated_amount,
|
||||
applicant: travelRequest.applicant,
|
||||
},
|
||||
travelRequestId,
|
||||
),
|
||||
);
|
||||
const { title, body } = pendingCopy;
|
||||
|
||||
await createNotifications(tx, {
|
||||
recipientEmployeeIds: recipients,
|
||||
|
||||
+95
-123
@@ -16,6 +16,14 @@ const {
|
||||
createNotifications,
|
||||
resolveRecipientsForAssignee,
|
||||
} = require("../services/notifications");
|
||||
const {
|
||||
buildApprovedCopy,
|
||||
buildPendingTaskCopy,
|
||||
buildRejectedCopy,
|
||||
buildReturnedCopy,
|
||||
buildWorkflowEntityMeta,
|
||||
normalizeWorkflowEntityType,
|
||||
} = require("../services/notifications/workflowCopy");
|
||||
const { enqueueImNotifications } = require("../services/notifications/imEnqueue");
|
||||
const { sendNotificationEmailSafe } = require("../services/email");
|
||||
const { NOTIFICATION_TYPES } = require("../constants/notificationTypes");
|
||||
@@ -423,92 +431,19 @@ async function loadReimbursementNotifyContext(tx, reimbursementId) {
|
||||
select: {
|
||||
id: true,
|
||||
form_number: true,
|
||||
total_amount: true,
|
||||
owner_id: true,
|
||||
submitter_id: true,
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getWorkflowEntityMeta(entityType, entityRecord, entityId) {
|
||||
const normalizedEntityType = entityType === "procurement" ? "purchase_request" : entityType;
|
||||
|
||||
if (normalizedEntityType === "reimbursement") {
|
||||
return {
|
||||
label: "报销单",
|
||||
reference: entityRecord?.form_number ?? entityId,
|
||||
targetType: "reimbursement_form",
|
||||
detailPath: `/reimbursements/${entityId}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedEntityType === "leave") {
|
||||
return {
|
||||
label: "请假单",
|
||||
reference: entityId,
|
||||
targetType: "leave_request",
|
||||
detailPath: "/leave-requests",
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedEntityType === "purchase_request") {
|
||||
return {
|
||||
label: "采购申请",
|
||||
reference: entityRecord?.request_no ?? entityId,
|
||||
targetType: "purchase_request",
|
||||
detailPath: "/purchase-requests",
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedEntityType === "travel_request") {
|
||||
return {
|
||||
label: "差旅申请",
|
||||
reference: entityRecord?.request_no ?? entityId,
|
||||
targetType: "travel_request",
|
||||
detailPath: `/travel-requests?id=${entityId}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: "审批单",
|
||||
reference: entityId,
|
||||
targetType: normalizedEntityType,
|
||||
detailPath: `/workflow/tasks/${entityId}`,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPendingTaskCopy(entityMeta, { transfer = false } = {}) {
|
||||
return {
|
||||
title: `【待办】${entityMeta.label} ${entityMeta.reference} ${transfer ? "转审待处理" : "待审批"}`,
|
||||
body: transfer
|
||||
? `${entityMeta.label} ${entityMeta.reference} 已转审给你,请及时处理。`
|
||||
: `${entityMeta.label} ${entityMeta.reference} 已流转到你,请及时处理。`,
|
||||
};
|
||||
}
|
||||
|
||||
function buildApprovedCopy(entityMeta) {
|
||||
return {
|
||||
title: `【已批准】${entityMeta.label} ${entityMeta.reference}`,
|
||||
body:
|
||||
entityMeta.targetType === "reimbursement_form"
|
||||
? `${entityMeta.label} ${entityMeta.reference} 已审批通过,等待财务付款。`
|
||||
: `${entityMeta.label} ${entityMeta.reference} 已审批通过。`,
|
||||
};
|
||||
}
|
||||
|
||||
function buildRejectedCopy(entityMeta, comment) {
|
||||
return {
|
||||
title: `【已驳回】${entityMeta.label} ${entityMeta.reference}`,
|
||||
body: `${entityMeta.label} ${entityMeta.reference} 已被驳回:${comment}`,
|
||||
};
|
||||
}
|
||||
|
||||
function buildReturnedCopy(entityMeta, comment) {
|
||||
return {
|
||||
title: `【已退回】${entityMeta.label} ${entityMeta.reference}`,
|
||||
body: `${entityMeta.label} ${entityMeta.reference} 已退回:${comment}`,
|
||||
};
|
||||
}
|
||||
|
||||
function uniq(values) {
|
||||
return Array.from(new Set((values ?? []).filter(Boolean)));
|
||||
}
|
||||
@@ -519,6 +454,22 @@ async function loadLeaveNotifyContext(tx, leaveRequestId) {
|
||||
select: {
|
||||
id: true,
|
||||
owner_id: true,
|
||||
start_date: true,
|
||||
end_date: true,
|
||||
requested_days: true,
|
||||
leave_type: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -529,8 +480,15 @@ async function loadPurchaseNotifyContext(tx, purchaseRequestId) {
|
||||
select: {
|
||||
id: true,
|
||||
request_no: true,
|
||||
total_amount: true,
|
||||
applicant_id: true,
|
||||
submitter_id: true,
|
||||
applicant: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -541,14 +499,21 @@ async function loadTravelNotifyContext(tx, travelRequestId) {
|
||||
select: {
|
||||
id: true,
|
||||
request_no: true,
|
||||
estimated_amount: true,
|
||||
applicant_employee_id: true,
|
||||
submitter_id: true,
|
||||
applicant: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function loadWorkflowNotifyContext(tx, entityType, entityId) {
|
||||
const normalizedEntityType = entityType === 'procurement' ? 'purchase_request' : entityType;
|
||||
const normalizedEntityType = normalizeWorkflowEntityType(entityType);
|
||||
|
||||
if (normalizedEntityType === 'reimbursement') {
|
||||
const record = await loadReimbursementNotifyContext(tx, entityId);
|
||||
@@ -589,7 +554,7 @@ async function loadWorkflowNotifyContext(tx, entityType, entityId) {
|
||||
}
|
||||
|
||||
function resolveWorkflowOutcomeNotificationType(entityType, outcome) {
|
||||
const normalizedEntityType = entityType === 'procurement' ? 'purchase_request' : entityType;
|
||||
const normalizedEntityType = normalizeWorkflowEntityType(entityType);
|
||||
|
||||
if (outcome === 'approved') {
|
||||
if (normalizedEntityType === 'reimbursement') return NOTIFICATION_TYPES.reimbursementApproved;
|
||||
@@ -673,7 +638,7 @@ async function queueWorkflowOutcomeNotifications({
|
||||
outcome,
|
||||
comment = null,
|
||||
}) {
|
||||
const normalizedEntityType = workflowInstance.entity_type === 'procurement' ? 'purchase_request' : workflowInstance.entity_type;
|
||||
const normalizedEntityType = normalizeWorkflowEntityType(workflowInstance.entity_type);
|
||||
const notificationType = resolveWorkflowOutcomeNotificationType(normalizedEntityType, outcome);
|
||||
if (!notificationType) {
|
||||
return;
|
||||
@@ -685,7 +650,7 @@ async function queueWorkflowOutcomeNotifications({
|
||||
return;
|
||||
}
|
||||
|
||||
const entityMeta = getWorkflowEntityMeta(normalizedEntityType, context.record, workflowInstance.entity_id);
|
||||
const entityMeta = buildWorkflowEntityMeta(normalizedEntityType, context.record, workflowInstance.entity_id);
|
||||
const copy =
|
||||
outcome === 'approved'
|
||||
? buildApprovedCopy(entityMeta)
|
||||
@@ -1476,42 +1441,41 @@ async function workflowRoutes(fastify) {
|
||||
},
|
||||
});
|
||||
|
||||
if (workflowInstance.entity_type === "reimbursement") {
|
||||
try {
|
||||
const reimbursement = await loadReimbursementNotifyContext(
|
||||
tx,
|
||||
workflowInstance.entity_id,
|
||||
);
|
||||
const entityMeta = getWorkflowEntityMeta(
|
||||
workflowInstance.entity_type,
|
||||
reimbursement,
|
||||
workflowInstance.entity_id,
|
||||
);
|
||||
const pendingCopy = buildPendingTaskCopy(entityMeta);
|
||||
const recipients = await resolveRecipientsForAssignee(tx, {
|
||||
assigneeId: actionable.assigneeId,
|
||||
assigneeRole: actionable.assigneeRole,
|
||||
});
|
||||
try {
|
||||
const notificationContext = await loadWorkflowNotifyContext(
|
||||
tx,
|
||||
workflowInstance.entity_type,
|
||||
workflowInstance.entity_id,
|
||||
);
|
||||
const entityMeta = buildWorkflowEntityMeta(
|
||||
workflowInstance.entity_type,
|
||||
notificationContext.record,
|
||||
workflowInstance.entity_id,
|
||||
);
|
||||
const pendingCopy = buildPendingTaskCopy(entityMeta);
|
||||
const recipients = await resolveRecipientsForAssignee(tx, {
|
||||
assigneeId: actionable.assigneeId,
|
||||
assigneeRole: actionable.assigneeRole,
|
||||
});
|
||||
|
||||
await queueTaskAssignedNotifications({
|
||||
tx,
|
||||
config: fastify.config,
|
||||
logger: fastify.log,
|
||||
actorId: request.employee.id,
|
||||
emailTasks,
|
||||
recipientEmployeeIds: recipients,
|
||||
title: pendingCopy.title,
|
||||
body: pendingCopy.body,
|
||||
taskId: nextTask.id,
|
||||
entityType: workflowInstance.entity_type,
|
||||
entityId: workflowInstance.entity_id,
|
||||
});
|
||||
} catch (error) {
|
||||
fastify.log.error(
|
||||
{ err: error, taskId: nextTask.id },
|
||||
"Failed to create task assigned notifications",
|
||||
);
|
||||
}
|
||||
await queueTaskAssignedNotifications({
|
||||
tx,
|
||||
config: fastify.config,
|
||||
logger: fastify.log,
|
||||
actorId: request.employee.id,
|
||||
emailTasks,
|
||||
recipientEmployeeIds: recipients,
|
||||
title: pendingCopy.title,
|
||||
body: pendingCopy.body,
|
||||
taskId: nextTask.id,
|
||||
entityType: workflowInstance.entity_type,
|
||||
entityId: workflowInstance.entity_id,
|
||||
});
|
||||
} catch (error) {
|
||||
fastify.log.error(
|
||||
{ err: error, taskId: nextTask.id },
|
||||
"Failed to create task assigned notifications",
|
||||
);
|
||||
}
|
||||
|
||||
result = {
|
||||
@@ -2138,10 +2102,14 @@ async function workflowRoutes(fastify) {
|
||||
});
|
||||
|
||||
try {
|
||||
const reimbursement = await loadReimbursementNotifyContext(tx, workflowInstance.entity_id);
|
||||
const entityMeta = getWorkflowEntityMeta(
|
||||
const notificationContext = await loadWorkflowNotifyContext(
|
||||
tx,
|
||||
workflowInstance.entity_type,
|
||||
reimbursement,
|
||||
workflowInstance.entity_id,
|
||||
);
|
||||
const entityMeta = buildWorkflowEntityMeta(
|
||||
workflowInstance.entity_type,
|
||||
notificationContext.record,
|
||||
workflowInstance.entity_id,
|
||||
);
|
||||
const pendingCopy = buildPendingTaskCopy(entityMeta, { transfer: true });
|
||||
@@ -2195,8 +2163,12 @@ async function workflowRoutes(fastify) {
|
||||
module.exports = workflowRoutes;
|
||||
|
||||
module.exports.__testables = {
|
||||
buildApprovedCopy,
|
||||
buildPendingTaskCopy,
|
||||
buildRejectedCopy,
|
||||
buildReturnedCopy,
|
||||
buildWorkflowEntityMeta,
|
||||
handlePurchaseApprovalErpSync,
|
||||
listAllowedActionsForTask,
|
||||
getWorkflowEntityMeta,
|
||||
resolveWorkflowOutcomeNotificationType,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
const { formatDateOnly } = require("../leave");
|
||||
const { toMoneyString } = require("../../utils/workflow");
|
||||
|
||||
function normalizeWorkflowEntityType(entityType) {
|
||||
return entityType === "procurement" ? "purchase_request" : entityType;
|
||||
}
|
||||
|
||||
function formatAmount(value) {
|
||||
return toMoneyString(value) ?? "-";
|
||||
}
|
||||
|
||||
function formatRequestedDays(value) {
|
||||
const numeric = Number(value ?? 0);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return "-";
|
||||
}
|
||||
return Number.isInteger(numeric) ? String(numeric) : numeric.toFixed(1);
|
||||
}
|
||||
|
||||
function formatDateRange(startDate, endDate) {
|
||||
const start = startDate ? formatDateOnly(startDate) : null;
|
||||
const end = endDate ? formatDateOnly(endDate) : null;
|
||||
|
||||
if (start && end) {
|
||||
return start === end ? start : `${start} ~ ${end}`;
|
||||
}
|
||||
|
||||
return start ?? end ?? null;
|
||||
}
|
||||
|
||||
function buildReference(normalizedEntityType, entityRecord, entityId) {
|
||||
if (normalizedEntityType === "reimbursement") {
|
||||
return entityRecord?.form_number ?? entityId;
|
||||
}
|
||||
|
||||
if (normalizedEntityType === "leave") {
|
||||
return formatDateRange(entityRecord?.start_date, entityRecord?.end_date) ?? entityId;
|
||||
}
|
||||
|
||||
if (normalizedEntityType === "purchase_request") {
|
||||
return entityRecord?.request_no ?? entityId;
|
||||
}
|
||||
|
||||
if (normalizedEntityType === "travel_request") {
|
||||
return entityRecord?.request_no ?? entityId;
|
||||
}
|
||||
|
||||
return entityId;
|
||||
}
|
||||
|
||||
function buildWorkflowEntityMeta(entityType, entityRecord, entityId) {
|
||||
const normalizedEntityType = normalizeWorkflowEntityType(entityType);
|
||||
|
||||
if (normalizedEntityType === "reimbursement") {
|
||||
return {
|
||||
entityType: normalizedEntityType,
|
||||
label: "报销单",
|
||||
reference: buildReference(normalizedEntityType, entityRecord, entityId),
|
||||
targetType: "reimbursement_form",
|
||||
detailPath: `/reimbursements/${entityId}`,
|
||||
summary: {
|
||||
applicantName: entityRecord?.owner?.name ?? null,
|
||||
amount: formatAmount(entityRecord?.total_amount),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedEntityType === "leave") {
|
||||
return {
|
||||
entityType: normalizedEntityType,
|
||||
label: "请假单",
|
||||
reference: buildReference(normalizedEntityType, entityRecord, entityId),
|
||||
targetType: "leave_request",
|
||||
detailPath: "/leave-requests",
|
||||
summary: {
|
||||
applicantName: entityRecord?.owner?.name ?? null,
|
||||
leaveTypeName: entityRecord?.leave_type?.name ?? null,
|
||||
requestedDays: formatRequestedDays(entityRecord?.requested_days),
|
||||
dateRange: formatDateRange(entityRecord?.start_date, entityRecord?.end_date) ?? "-",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedEntityType === "purchase_request") {
|
||||
return {
|
||||
entityType: normalizedEntityType,
|
||||
label: "采购申请",
|
||||
reference: buildReference(normalizedEntityType, entityRecord, entityId),
|
||||
targetType: "purchase_request",
|
||||
detailPath: "/purchase-requests",
|
||||
summary: {
|
||||
applicantName: entityRecord?.applicant?.name ?? null,
|
||||
amount: formatAmount(entityRecord?.total_amount),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedEntityType === "travel_request") {
|
||||
return {
|
||||
entityType: normalizedEntityType,
|
||||
label: "差旅申请",
|
||||
reference: buildReference(normalizedEntityType, entityRecord, entityId),
|
||||
targetType: "travel_request",
|
||||
detailPath: `/travel-requests?id=${entityId}`,
|
||||
summary: {
|
||||
applicantName: entityRecord?.applicant?.name ?? null,
|
||||
estimatedAmount: formatAmount(entityRecord?.estimated_amount),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
entityType: normalizedEntityType,
|
||||
label: "审批单",
|
||||
reference: entityId,
|
||||
targetType: normalizedEntityType,
|
||||
detailPath: `/workflow/tasks/${entityId}`,
|
||||
summary: {},
|
||||
};
|
||||
}
|
||||
|
||||
function buildSummaryLines(entityMeta) {
|
||||
if (entityMeta.entityType === "reimbursement") {
|
||||
return [
|
||||
`申请人:${entityMeta.summary.applicantName ?? "未知申请人"}`,
|
||||
`金额:${entityMeta.summary.amount ?? "-"}`,
|
||||
`单号:${entityMeta.reference}`,
|
||||
];
|
||||
}
|
||||
|
||||
if (entityMeta.entityType === "leave") {
|
||||
return [
|
||||
`申请人:${entityMeta.summary.applicantName ?? "未知申请人"}`,
|
||||
`假种:${entityMeta.summary.leaveTypeName ?? "请假"}`,
|
||||
`天数:${entityMeta.summary.requestedDays ?? "-"} 天`,
|
||||
`起止日期:${entityMeta.summary.dateRange ?? "-"}`,
|
||||
];
|
||||
}
|
||||
|
||||
if (entityMeta.entityType === "purchase_request") {
|
||||
return [
|
||||
`申请人:${entityMeta.summary.applicantName ?? "未知申请人"}`,
|
||||
`金额:${entityMeta.summary.amount ?? "-"}`,
|
||||
`单号:${entityMeta.reference}`,
|
||||
];
|
||||
}
|
||||
|
||||
if (entityMeta.entityType === "travel_request") {
|
||||
return [
|
||||
`申请人:${entityMeta.summary.applicantName ?? "未知申请人"}`,
|
||||
`预估金额:${entityMeta.summary.estimatedAmount ?? "-"}`,
|
||||
`单号:${entityMeta.reference}`,
|
||||
];
|
||||
}
|
||||
|
||||
return [`单据:${entityMeta.reference}`];
|
||||
}
|
||||
|
||||
function buildSubject(entityMeta) {
|
||||
return entityMeta.reference ? `${entityMeta.label} ${entityMeta.reference}` : entityMeta.label;
|
||||
}
|
||||
|
||||
function buildBody(entityMeta, resultLine) {
|
||||
return [...buildSummaryLines(entityMeta), resultLine].join("\n");
|
||||
}
|
||||
|
||||
function buildPendingTaskCopy(entityMeta, { transfer = false } = {}) {
|
||||
return {
|
||||
title: `【待办】${buildSubject(entityMeta)} ${transfer ? "转审待处理" : "待审批"}`,
|
||||
body: buildBody(
|
||||
entityMeta,
|
||||
transfer ? "动作:该审批已转审给你,请及时处理。" : "动作:请及时处理审批任务。",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function buildApprovedCopy(entityMeta) {
|
||||
return {
|
||||
title: `【已批准】${buildSubject(entityMeta)}`,
|
||||
body: buildBody(
|
||||
entityMeta,
|
||||
entityMeta.targetType === "reimbursement_form"
|
||||
? "结果:已审批通过,等待财务付款。"
|
||||
: "结果:已审批通过。",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function buildRejectedCopy(entityMeta, comment) {
|
||||
const safeComment = String(comment ?? "").trim() || "无";
|
||||
return {
|
||||
title: `【已驳回】${buildSubject(entityMeta)}`,
|
||||
body: buildBody(entityMeta, `结果:已驳回:${safeComment}`),
|
||||
};
|
||||
}
|
||||
|
||||
function buildReturnedCopy(entityMeta, comment) {
|
||||
const safeComment = String(comment ?? "").trim() || "无";
|
||||
return {
|
||||
title: `【已退回】${buildSubject(entityMeta)}`,
|
||||
body: buildBody(entityMeta, `结果:已退回:${safeComment}`),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildApprovedCopy,
|
||||
buildPendingTaskCopy,
|
||||
buildRejectedCopy,
|
||||
buildReturnedCopy,
|
||||
buildWorkflowEntityMeta,
|
||||
normalizeWorkflowEntityType,
|
||||
};
|
||||
在新工单中引用
屏蔽一个用户