R58 A1-A6:报销详情页内嵌发票绑定与单据打印

这个提交包含在:
2026-04-11 14:57:15 +08:00
父节点 88c987ae8f
当前提交 ba48e7d1d4
共修改 14 个文件,包含 1667 行新增和 12 行删除
+109
查看文件
@@ -136,6 +136,7 @@ const TRAVEL_TRANSPORT_EXPENSE_TYPE_CODES = {
car: "travel_taxi",
other: "transport",
};
const PRINTABLE_WORKFLOW_ACTIONS = new Set(["start", "approve", "reject", "return", "transfer", "withdraw"]);
function ensureOwnOrCanViewAll(fastify, request, reimbursement) {
const canViewAll = fastify.hasPermission(request.employee, "expense.view_all");
@@ -195,6 +196,49 @@ function buildWorkflowProgress(workflowInstance, historyItems = []) {
};
}
function buildWorkflowStepNameMap(workflowInstance) {
const steps = getWorkflowStepsSnapshot(workflowInstance);
return new Map(steps.map((step) => [step.id, step.name ?? step.id]));
}
function resolveApprovalHistoryStepName(historyItem, stepNameMap, fallbackStepId = null) {
if (historyItem.workflow_task?.step_name) {
return historyItem.workflow_task.step_name;
}
const payload = historyItem.payload ?? {};
const stepId =
payload.from_step_id ??
payload.step_id ??
payload.to_step_id ??
fallbackStepId ??
null;
if (!stepId) {
return null;
}
return stepNameMap.get(String(stepId)) ?? String(stepId);
}
function buildApprovalHistory(workflowInstance, historyItems = []) {
if (!workflowInstance) {
return [];
}
const stepNameMap = buildWorkflowStepNameMap(workflowInstance);
return historyItems
.filter((item) => PRINTABLE_WORKFLOW_ACTIONS.has(item.action))
.map((item) => ({
id: item.id,
stepName: resolveApprovalHistoryStepName(item, stepNameMap, workflowInstance.current_step_id),
action: item.action,
actorName: item.actor?.name ?? "",
comment: item.comment ?? null,
createdAt: item.created_at?.toISOString?.() ?? null,
}));
}
function ensureApplicantOrAdmin(request, reimbursement) {
if (request.employee?.roleCodes?.includes("admin")) {
return;
@@ -1869,12 +1913,19 @@ async function reimbursementRoutes(fastify) {
submitter_id: true,
owner_id: true,
department_id: true,
created_at: true,
reimbursement_date: true,
total_amount: true,
status: true,
description: true,
project_id: true,
linked_travel_request_id: true,
department: {
select: {
id: true,
name: true,
},
},
project: { select: { id: true, project_no: true, name: true } },
travel_request: {
select: {
@@ -1917,7 +1968,11 @@ async function reimbursementRoutes(fastify) {
status: true,
invoice_code: true,
invoice_number: true,
invoice_date: true,
seller_name: true,
total_amount: true,
original_name: true,
linked_item_id: true,
},
},
workflow_instances: {
@@ -1967,6 +2022,24 @@ async function reimbursementRoutes(fastify) {
const pendingInstance = reimbursement.workflow_instances[0] ?? null;
const pendingTask = pendingInstance?.workflow_tasks?.[0] ?? null;
const latestWorkflowInstance = await fastify.prisma.workflowInstance.findFirst({
where: {
entity_type: "reimbursement",
entity_id: reimbursementId,
},
orderBy: { created_at: "desc" },
select: {
id: true,
current_step_id: true,
created_at: true,
context_data: true,
workflow_definition: {
select: {
definition_json: true,
},
},
},
});
const workflowHistory = pendingInstance
? await fastify.prisma.workflowHistory.findMany({
where: { instance_id: pendingInstance.id },
@@ -1977,6 +2050,29 @@ async function reimbursementRoutes(fastify) {
},
})
: [];
const approvalHistoryRows = latestWorkflowInstance
? await fastify.prisma.workflowHistory.findMany({
where: { instance_id: latestWorkflowInstance.id },
orderBy: { created_at: "asc" },
select: {
id: true,
action: true,
comment: true,
payload: true,
created_at: true,
actor: {
select: {
name: true,
},
},
workflow_task: {
select: {
step_name: true,
},
},
},
})
: [];
const riskFlags = await fastify.prisma.entityRiskFlag.findMany({
where: {
entity_type: "reimbursement",
@@ -1992,6 +2088,14 @@ async function reimbursementRoutes(fastify) {
submitterId: reimbursement.submitter_id,
ownerId: reimbursement.owner_id,
departmentId: reimbursement.department_id,
department: reimbursement.department
? {
id: reimbursement.department.id,
name: reimbursement.department.name,
}
: null,
createdAt: reimbursement.created_at?.toISOString?.() ?? null,
submittedAt: latestWorkflowInstance?.created_at?.toISOString?.() ?? null,
reimbursementDate: reimbursement.reimbursement_date.toISOString().slice(0, 10),
totalAmount: toMoneyString(reimbursement.total_amount),
status: reimbursement.status,
@@ -2024,7 +2128,11 @@ async function reimbursementRoutes(fastify) {
status: item.status,
invoiceCode: item.invoice_code,
invoiceNumber: item.invoice_number,
invoiceDate: item.invoice_date?.toISOString?.()?.slice(0, 10) ?? null,
sellerName: item.seller_name ?? null,
totalAmount: toMoneyString(item.total_amount),
originalName: item.original_name ?? null,
linkedItemId: item.linked_item_id ?? null,
})),
pendingWorkflow: pendingInstance
? {
@@ -2034,6 +2142,7 @@ async function reimbursementRoutes(fastify) {
}
: null,
workflowProgress: pendingInstance ? buildWorkflowProgress(pendingInstance, workflowHistory) : null,
approvalHistory: buildApprovalHistory(latestWorkflowInstance, approvalHistoryRows),
riskFlags: riskFlags.map(buildPersistedRiskFlag),
});
},