feat(detail-page-structure-progress-actions): 统一详情处理进度与操作区
这个提交包含在:
@@ -19,6 +19,11 @@ const { enqueueReimbursementJournalExport } = require("../services/integrations/
|
||||
const { ensureRiskFlag, buildPersistedRiskFlag, toMoneyNumber, toMoneyString } = require("../services/feeControl");
|
||||
const { decryptStringOrThrow, maskBankAccount } = require("../services/crypto");
|
||||
const { generatePaymentOrderXls, PaymentOrderXlsError } = require("../services/paymentOrderXls");
|
||||
const {
|
||||
buildPaymentProcessingSemantics,
|
||||
requiresBankReceipt,
|
||||
resolveItemPaymentChannel,
|
||||
} = require("../services/paymentProcessingProgress");
|
||||
const { deleteObject, openReadStream, StorageNotFoundError, writeBuffer } = require("../services/storage");
|
||||
const {
|
||||
buildAttachmentContentDisposition,
|
||||
@@ -138,16 +143,8 @@ function mapReceiptAttachment(item) {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveItemPaymentChannel(batch, item) {
|
||||
return item.payment_channel_override ?? batch.default_payment_channel ??
|
||||
(batch.entity_type === "reimbursement" ? "bank" : null);
|
||||
}
|
||||
|
||||
async function ensureBankReceiptForExecutableBatch(tx, batch) {
|
||||
const requiresBankReceipt = (batch.items ?? []).some((item) =>
|
||||
["pending", "failed"].includes(item.status) && resolveItemPaymentChannel(batch, item) === "bank",
|
||||
);
|
||||
if (!requiresBankReceipt) {
|
||||
if (!requiresBankReceipt(batch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -817,9 +814,64 @@ function mapBatch(batch, openRiskFlags = [], legacyPayees = new Map(), options =
|
||||
};
|
||||
}
|
||||
|
||||
function isoDateTimeOrNull(value) {
|
||||
if (!value) return null;
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
||||
}
|
||||
|
||||
function buildPaymentBatchProcessingProgress(batch, actorNames) {
|
||||
const semantics = buildPaymentProcessingSemantics({
|
||||
status: batch.status,
|
||||
createdAt: batch.created_at,
|
||||
reviewedAt: batch.reviewed_at,
|
||||
executedAt: batch.executed_at,
|
||||
cancelledAt: batch.cancelled_at,
|
||||
cancelledReason: batch.cancelled_reason,
|
||||
requiresBankReceipt: requiresBankReceipt(batch),
|
||||
receiptCount: batch._count?.receipt_attachments ?? 0,
|
||||
paymentOrderExportCount: batch.payment_order_export_count ?? 0,
|
||||
});
|
||||
const actorIds = {
|
||||
creator: batch.created_by,
|
||||
reviewer: batch.reviewed_by,
|
||||
executor: batch.executed_by,
|
||||
canceller: batch.cancelled_by,
|
||||
};
|
||||
return {
|
||||
stages: [semantics.created, semantics.review, semantics.payment].map((stage) => ({
|
||||
key: stage.key,
|
||||
label: stage.label,
|
||||
state: stage.state,
|
||||
actorName: stage.actorSource ? actorNames.get(actorIds[stage.actorSource]) ?? null : null,
|
||||
occurredAt: isoDateTimeOrNull(stage.occurredAt),
|
||||
result: stage.result,
|
||||
comment: stage.comment,
|
||||
detail: stage.detail,
|
||||
targetHref: null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async function mapBatchForResponse(tx, batch, openRiskFlags, config) {
|
||||
const legacyPayees = await loadLegacyPayees(tx, batch, config);
|
||||
return mapBatch(batch, openRiskFlags, legacyPayees, { includePayee: true });
|
||||
const actorIds = Array.from(new Set([
|
||||
batch.created_by,
|
||||
batch.reviewed_by,
|
||||
batch.executed_by,
|
||||
batch.cancelled_by,
|
||||
].filter(Boolean)));
|
||||
const actors = actorIds.length
|
||||
? await tx.employee.findMany({
|
||||
where: { id: { in: actorIds } },
|
||||
select: { id: true, name: true },
|
||||
})
|
||||
: [];
|
||||
const actorNames = new Map(actors.map((actor) => [actor.id, actor.name]));
|
||||
return {
|
||||
...mapBatch(batch, openRiskFlags, legacyPayees, { includePayee: true }),
|
||||
processingProgress: buildPaymentBatchProcessingProgress(batch, actorNames),
|
||||
};
|
||||
}
|
||||
|
||||
async function listOpenRiskFlagsForBatch(tx, batch) {
|
||||
@@ -2183,7 +2235,10 @@ async function paymentBatchesRoutes(fastify) {
|
||||
reviewed_by: request.employee.id,
|
||||
updated_by: request.employee.id,
|
||||
},
|
||||
include: { items: true },
|
||||
include: {
|
||||
items: true,
|
||||
_count: { select: { receipt_attachments: { where: { archived_at: null } } } },
|
||||
},
|
||||
});
|
||||
|
||||
await tx.auditLog.create({
|
||||
@@ -2359,7 +2414,10 @@ async function paymentBatchesRoutes(fastify) {
|
||||
executed_by: request.employee.id,
|
||||
updated_by: request.employee.id,
|
||||
},
|
||||
include: { items: true },
|
||||
include: {
|
||||
items: true,
|
||||
_count: { select: { receipt_attachments: { where: { archived_at: null } } } },
|
||||
},
|
||||
});
|
||||
|
||||
await tx.auditLog.create({
|
||||
|
||||
在新工单中引用
屏蔽一个用户