R58 A1-A6:报销详情页内嵌发票绑定与单据打印
这个提交包含在:
@@ -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),
|
||||
});
|
||||
},
|
||||
|
||||
在新工单中引用
屏蔽一个用户