| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import dayjs from "dayjs"; |
| | | |
| | | /** è´¹ç¨ç§ç® */ |
| | | export const EXPENSE_SUBJECT_OPTIONS = [ |
| | | { label: "交éè´¹", value: "transport" }, |
| | | { label: "ä½å®¿è´¹", value: "hotel" }, |
| | | { label: "é¤é¥®è´¹", value: "meal" }, |
| | | { label: "å
¶ä»", value: "other" }, |
| | | ]; |
| | | |
| | | const TIER1_CITIES = ["å京", "䏿µ·", "广å·", "æ·±å³"]; |
| | | |
| | | export function expenseSubjectLabel(v) { |
| | | return EXPENSE_SUBJECT_OPTIONS.find((x) => x.value === v)?.label || "â"; |
| | | } |
| | | |
| | | export function statusLabel(v) { |
| | | if (v === "draft") return "è稿"; |
| | | if (v === "approved") return "éè¿"; |
| | | if (v === "paid") return "已仿¬¾"; |
| | | if (v === "rejected") return "驳å"; |
| | | if (v === "cancelled") return "å·²æ¤å"; |
| | | return "å®¡æ ¸ä¸"; |
| | | } |
| | | |
| | | export function statusTagType(v) { |
| | | if (v === "draft") return "info"; |
| | | if (v === "approved" || v === "paid") return "success"; |
| | | if (v === "rejected") return "danger"; |
| | | if (v === "cancelled") return "info"; |
| | | return "warning"; |
| | | } |
| | | |
| | | export function detectTravelTier(destination) { |
| | | const city = (destination || "").trim(); |
| | | if (!city) return "tier3"; |
| | | if (TIER1_CITIES.some((c) => city.includes(c))) return "tier1"; |
| | | const tier2Keywords = ["æå·", "å京", "æ¦æ±", "æé½", "éåº", "西å®", "天津", "èå·", "é¿æ²", "éå·"]; |
| | | if (tier2Keywords.some((c) => city.includes(c))) return "tier2"; |
| | | return "tier3"; |
| | | } |
| | | |
| | | export function getTravelStandardByTier(tier) { |
| | | const map = { |
| | | tier1: { hotelPerNight: 600, transportPerDay: 80, mealPerDay: 100, label: "ä¸çº¿åå¸" }, |
| | | tier2: { hotelPerNight: 450, transportPerDay: 60, mealPerDay: 80, label: "äºçº¿åå¸" }, |
| | | tier3: { hotelPerNight: 350, transportPerDay: 40, mealPerDay: 60, label: "å
¶ä»åå¸" }, |
| | | }; |
| | | return map[tier] || map.tier3; |
| | | } |
| | | |
| | | export function computeTravelDays(startStr, endStr) { |
| | | if (!startStr || !endStr) return null; |
| | | const t0 = dayjs(startStr); |
| | | const t1 = dayjs(endStr); |
| | | if (!t0.isValid() || !t1.isValid() || !t1.isAfter(t0)) return null; |
| | | const days = Math.ceil(t1.diff(t0, "day", true)); |
| | | return Math.max(1, days); |
| | | } |
| | | |
| | | export function createEmptyExpenseDetail() { |
| | | return { |
| | | id: `ed_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`, |
| | | invoiceDate: "", |
| | | expenseSubject: "", |
| | | amount: undefined, |
| | | description: "", |
| | | }; |
| | | } |
| | | |
| | | export function createEmptyForm() { |
| | | return { |
| | | id: undefined, |
| | | reimburseNo: "", |
| | | applicantId: "", |
| | | employeeNo: "", |
| | | employeeName: "", |
| | | reimburseReason: "", |
| | | travelStartTime: "", |
| | | travelEndTime: "", |
| | | travelDays: undefined, |
| | | departurePlace: "", |
| | | destination: "", |
| | | hotelStandard: undefined, |
| | | hotelDays: undefined, |
| | | livingSubsidy: undefined, |
| | | applyAmount: undefined, |
| | | payee: "", |
| | | expenseDetails: [], |
| | | attachmentList: [], |
| | | approvalFlowNodes: [], |
| | | currentNodeIndex: 0, |
| | | needSpecialApproval: false, |
| | | deptId: "", |
| | | deptName: "", |
| | | travelTier: "tier3", |
| | | }; |
| | | } |
| | | |
| | | export function initApprovalFlowNodes(nodes) { |
| | | return (nodes || []).map((n, i) => ({ |
| | | ...n, |
| | | sortOrder: i + 1, |
| | | nodeOrder: i + 1, |
| | | nodeStatus: i === 0 ? "process" : "wait", |
| | | approveOpinion: n.approveOpinion || "", |
| | | approveTime: n.approveTime || "", |
| | | })); |
| | | } |
| | | |
| | | export function advanceApprovalFlow(row, opinion) { |
| | | const nodes = [...(row.approvalFlowNodes || [])]; |
| | | const idx = row.currentNodeIndex ?? 0; |
| | | if (!nodes.length) return { nodes, currentNodeIndex: idx, approvalResult: row.approvalResult }; |
| | | const now = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | nodes[idx] = { |
| | | ...nodes[idx], |
| | | nodeStatus: "finish", |
| | | approveOpinion: opinion || "åæ", |
| | | approveTime: now, |
| | | }; |
| | | const next = idx + 1; |
| | | if (next >= nodes.length) { |
| | | return { nodes, currentNodeIndex: idx, approvalResult: "approved" }; |
| | | } |
| | | nodes[next] = { ...nodes[next], nodeStatus: "process" }; |
| | | return { nodes, currentNodeIndex: next, approvalResult: "pending" }; |
| | | } |
| | | |
| | | export function rejectApprovalFlow(row, opinion) { |
| | | const nodes = [...(row.approvalFlowNodes || [])]; |
| | | const idx = row.currentNodeIndex ?? 0; |
| | | const now = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | if (nodes[idx]) { |
| | | nodes[idx] = { |
| | | ...nodes[idx], |
| | | nodeStatus: "error", |
| | | approveOpinion: opinion || "驳å", |
| | | approveTime: now, |
| | | }; |
| | | } |
| | | return { nodes, currentNodeIndex: idx, approvalResult: "rejected", rejectReason: opinion || "驳å" }; |
| | | } |
| | | |
| | | /** é¨é¨é¢ç®ï¼å¯¹æ¥é¢ç®ç³»ç»åè¿åç©ºï¼ */ |
| | | export function mockDeptBudget(deptId) { |
| | | if (!deptId) return null; |
| | | return null; |
| | | } |
| | | |
| | | export function normalizeImportedRow(raw, idx) { |
| | | const id = raw.id != null && String(raw.id).length ? `imp_${String(raw.id)}_${idx}` : `imp_${Date.now()}_${idx}`; |
| | | const travelDays = |
| | | raw.travelDays != null |
| | | ? Number(raw.travelDays) |
| | | : computeTravelDays(raw.travelStartTime, raw.travelEndTime); |
| | | return { |
| | | id, |
| | | reimburseNo: raw.reimburseNo || `TR${dayjs().format("YYYYMMDD")}${String(idx).padStart(4, "0")}`, |
| | | applicantId: raw.applicantId != null ? String(raw.applicantId) : `imp_user_${idx}`, |
| | | employeeNo: raw.employeeNo ?? raw.applicantNo ?? "", |
| | | employeeName: raw.employeeName ?? raw.applicantName ?? "æªç¥", |
| | | applicantNo: raw.employeeNo ?? raw.applicantNo ?? "", |
| | | applicantName: raw.employeeName ?? raw.applicantName ?? "æªç¥", |
| | | reimburseReason: raw.reimburseReason ?? "", |
| | | travelStartTime: raw.travelStartTime ?? "", |
| | | travelEndTime: raw.travelEndTime ?? "", |
| | | travelDays: travelDays == null || Number.isNaN(travelDays) ? 1 : travelDays, |
| | | departurePlace: raw.departurePlace ?? "", |
| | | destination: raw.destination ?? "", |
| | | hotelStandard: raw.hotelStandard, |
| | | hotelDays: raw.hotelDays, |
| | | livingSubsidy: raw.livingSubsidy, |
| | | applyAmount: raw.applyAmount ?? 0, |
| | | payee: raw.payee ?? "", |
| | | expenseDetails: Array.isArray(raw.expenseDetails) ? raw.expenseDetails : [], |
| | | invoiceAttachments: Array.isArray(raw.invoiceAttachments) ? raw.invoiceAttachments : [], |
| | | approvalFlowNodes: Array.isArray(raw.approvalFlowNodes) ? raw.approvalFlowNodes : [], |
| | | currentNodeIndex: raw.currentNodeIndex ?? 0, |
| | | approvalResult: ["pending", "approved", "rejected"].includes(raw.approvalResult) ? raw.approvalResult : "pending", |
| | | rejectReason: raw.rejectReason ?? "", |
| | | approvalRecords: Array.isArray(raw.approvalRecords) ? raw.approvalRecords : [], |
| | | needSpecialApproval: !!raw.needSpecialApproval, |
| | | deptId: raw.deptId ?? "", |
| | | deptName: raw.deptName ?? "", |
| | | travelTier: raw.travelTier || detectTravelTier(raw.destination), |
| | | createTime: raw.createTime || dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }; |
| | | } |