const { toMoneyString } = require("./feeControl"); const REIMBURSEMENT_LEDGER_STATUS_VALUES = Object.freeze([ "draft", "reviewing", "approved", "paid", "rejected", "cancelled", ]); const REIMBURSEMENT_LEDGER_PAYMENT_STATUS_VALUES = Object.freeze(["unpaid", "paid"]); const REIMBURSEMENT_LEDGER_INVOICE_STATUS_VALUES = Object.freeze([ "none", "pending_ocr", "ocr_failed", "available", "bound", "archived", ]); function toDateOnlyString(value) { if (!value) return null; if (typeof value === "string") return value.slice(0, 10); return value.toISOString?.().slice(0, 10) ?? null; } function toDateTimeString(value) { if (!value) return null; if (typeof value === "string") return value; return value.toISOString?.() ?? null; } function dateRange(dateFrom, dateTo) { return { ...(dateFrom ? { gte: new Date(`${dateFrom}T00:00:00.000Z`) } : {}), ...(dateTo ? { lte: new Date(`${dateTo}T23:59:59.999Z`) } : {}), }; } function buildFinanceLedgerWhere(filters = {}) { const clauses = []; const query = filters.q?.trim(); if (filters.status) { clauses.push({ status: filters.status }); } if (filters.paymentStatus === "paid") { clauses.push({ status: "paid" }); } else if (filters.paymentStatus === "unpaid") { clauses.push({ status: { not: "paid" } }); } if (filters.invoiceStatus === "none") { clauses.push({ invoice_pool_items: { none: {} } }); } else if (filters.invoiceStatus) { clauses.push({ invoice_pool_items: { some: { status: filters.invoiceStatus } } }); } const reimbursementDateRange = dateRange(filters.dateFrom, filters.dateTo); if (Object.keys(reimbursementDateRange).length > 0) { clauses.push({ reimbursement_date: reimbursementDateRange }); } if (query) { clauses.push({ OR: [ { form_number: { contains: query, mode: "insensitive" } }, { description: { contains: query, mode: "insensitive" } }, { project: { is: { project_no: { contains: query, mode: "insensitive" } } } }, { project: { is: { name: { contains: query, mode: "insensitive" } } } }, { owner: { name: { contains: query, mode: "insensitive" } } }, { owner: { employee_no: { contains: query, mode: "insensitive" } } }, { submitter: { name: { contains: query, mode: "insensitive" } } }, { submitter: { employee_no: { contains: query, mode: "insensitive" } } }, { department: { name: { contains: query, mode: "insensitive" } } }, ], }); } return clauses.length ? { AND: clauses } : {}; } const FINANCE_LEDGER_SELECT = { id: true, form_number: true, reimbursement_date: true, status: true, total_amount: true, description: true, payment_voucher_no: true, payment_date: true, payment_channel: true, paid_at: true, created_at: true, updated_at: true, owner: { select: { id: true, employee_no: true, name: true } }, submitter: { select: { id: true, employee_no: true, name: true } }, department: { select: { id: true, name: true } }, project: { select: { id: true, project_no: true, name: true } }, paid_by_employee: { select: { id: true, employee_no: true, name: true } }, invoice_pool_items: { select: { id: true, status: true, archived_reason: true, }, }, _count: { select: { attachments: true, invoice_pool_items: true, }, }, }; function summarizeInvoiceStatus(invoices) { const counts = { total: invoices.length, pendingOcr: 0, ocrFailed: 0, available: 0, bound: 0, archived: 0, usedComplete: 0, }; for (const invoice of invoices) { if (invoice.status === "pending_ocr") counts.pendingOcr += 1; if (invoice.status === "ocr_failed") counts.ocrFailed += 1; if (invoice.status === "available") counts.available += 1; if (invoice.status === "bound") counts.bound += 1; if (invoice.status === "archived") counts.archived += 1; if (invoice.archived_reason === "used_complete") counts.usedComplete += 1; } if (counts.total === 0) return { status: "none", counts }; if (counts.ocrFailed > 0) return { status: "ocr_failed", counts }; if (counts.pendingOcr > 0) return { status: "pending_ocr", counts }; if (counts.total === counts.archived) return { status: "archived", counts }; if (counts.bound > 0) return { status: "bound", counts }; if (counts.total === counts.available) return { status: "available", counts }; return { status: "mixed", counts }; } function mapEmployee(employee) { return employee ? { id: employee.id, employeeNo: employee.employee_no, name: employee.name, } : null; } function mapFinanceLedgerItem(item) { const invoiceSummary = summarizeInvoiceStatus(item.invoice_pool_items ?? []); return { id: item.id, formNumber: item.form_number, reimbursementDate: toDateOnlyString(item.reimbursement_date), status: item.status, totalAmount: toMoneyString(item.total_amount), description: item.description ?? null, owner: mapEmployee(item.owner), submitter: mapEmployee(item.submitter), department: item.department ? { id: item.department.id, name: item.department.name } : null, project: item.project ? { id: item.project.id, projectNo: item.project.project_no, name: item.project.name, } : null, payment: { status: item.status === "paid" ? "paid" : "unpaid", paymentVoucherNo: item.payment_voucher_no ?? null, paymentDate: toDateOnlyString(item.payment_date), paymentChannel: item.payment_channel ?? null, paidAt: toDateTimeString(item.paid_at), paidBy: mapEmployee(item.paid_by_employee), }, invoice: invoiceSummary, attachmentCount: item._count?.attachments ?? 0, createdAt: toDateTimeString(item.created_at), updatedAt: toDateTimeString(item.updated_at), }; } async function listFinanceLedger(prisma, filters = {}) { const page = Math.max(1, Number(filters.page ?? 1)); const pageSize = Math.min(100, Math.max(1, Number(filters.pageSize ?? 20))); const where = buildFinanceLedgerWhere(filters); const [total, rows] = await Promise.all([ prisma.reimbursementForm.count({ where }), prisma.reimbursementForm.findMany({ where, orderBy: [{ updated_at: "desc" }, { id: "desc" }], skip: (page - 1) * pageSize, take: pageSize, select: FINANCE_LEDGER_SELECT, }), ]); return { page, pageSize, total, items: rows.map(mapFinanceLedgerItem), }; } async function listFinanceLedgerForExport(prisma, filters = {}, limit = 5000) { const rows = await prisma.reimbursementForm.findMany({ where: buildFinanceLedgerWhere(filters), orderBy: [{ updated_at: "desc" }, { id: "desc" }], take: limit, select: FINANCE_LEDGER_SELECT, }); return rows.map(mapFinanceLedgerItem); } function paymentStatusLabel(status) { return status === "paid" ? "已付款" : "未付款"; } function reimbursementStatusLabel(status) { const labels = { draft: "草稿", reviewing: "审批中", approved: "已通过", paid: "已支付", rejected: "已驳回", cancelled: "已取消", }; return labels[status] ?? status ?? ""; } function invoiceStatusLabel(status) { const labels = { none: "无发票", pending_ocr: "识别中", ocr_failed: "识别失败", available: "可添加", bound: "已绑定", archived: "已归档", mixed: "多状态", }; return labels[status] ?? status ?? ""; } const FINANCE_LEDGER_EXPORT_HEADERS = Object.freeze([ "报销单号", "申请人", "部门", "项目", "金额", "审批状态", "付款状态", "付款日期", "付款凭证号", "付款渠道", "付款操作人", "发票状态", "发票数", "附件数", "报销日期", "更新时间", ]); function mapFinanceLedgerExportRow(item) { return [ item.formNumber ?? item.id, item.owner?.name ?? item.submitter?.name ?? "", item.department?.name ?? "", item.project ? [item.project.projectNo, item.project.name].filter(Boolean).join(" ") : "", item.totalAmount, reimbursementStatusLabel(item.status), paymentStatusLabel(item.payment.status), item.payment.paymentDate ?? "", item.payment.paymentVoucherNo ?? "", item.payment.paymentChannel ?? "", item.payment.paidBy?.name ?? "", invoiceStatusLabel(item.invoice.status), item.invoice.counts.total, item.attachmentCount, item.reimbursementDate ?? "", item.updatedAt ?? "", ]; } module.exports = { FINANCE_LEDGER_EXPORT_HEADERS, REIMBURSEMENT_LEDGER_INVOICE_STATUS_VALUES, REIMBURSEMENT_LEDGER_PAYMENT_STATUS_VALUES, REIMBURSEMENT_LEDGER_STATUS_VALUES, buildFinanceLedgerWhere, invoiceStatusLabel, listFinanceLedger, listFinanceLedgerForExport, mapFinanceLedgerExportRow, paymentStatusLabel, reimbursementStatusLabel, };