mappers.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. /**
  2. * 后端 VO ↔ 新前端页面展示模型映射(不新增页面字段,只做值转换)
  3. */
  4. import type {
  5. AlertItem,
  6. DiskFailPerson,
  7. DiskRecord,
  8. Person,
  9. ProcessLogItem,
  10. SpecialBiz,
  11. SubsidyStandard,
  12. VillageBatch,
  13. VillageMonthlyBatch,
  14. } from '@/types/biz'
  15. import { regionName } from './regionStore'
  16. /** 后端批次 status → 前端 VillageBatch.stage(页面流程文案) */
  17. const BATCH_STATUS_TO_STAGE: Record<string, string> = {
  18. DRAFT: '人员新增录入',
  19. PENDING_TOWN_APPROVE: '镇经发中心初审',
  20. PENDING_INSURANCE_MATCH: '居保状态匹配',
  21. PENDING_STATUS_MAINTAIN: '比对结果已生成',
  22. PENDING_LEADER_APPROVE: '分管领导审批',
  23. READY_EXPORT: '待线下发放',
  24. EXPORTED: '回盘信息确认',
  25. RETURNED: '已回盘确认',
  26. ARCHIVED: '已归档',
  27. REJECTED: '退回修改',
  28. }
  29. /** 后端 status → 月补贴清单展示 status */
  30. const BATCH_STATUS_TO_MONTHLY: Record<string, string> = {
  31. PENDING_STATUS_MAINTAIN: '待提交',
  32. PENDING_LEADER_APPROVE: '待审核',
  33. READY_EXPORT: '已审核',
  34. EXPORTED: '已出盘',
  35. RETURNED: '已回盘',
  36. ARCHIVED: '已回盘',
  37. }
  38. const PERSON_STATUS_LABEL: Record<string, string> = {
  39. DRAFT: '草稿',
  40. PENDING_TOWN_APPROVE: '待镇审批',
  41. PENDING_INSURANCE_MATCH: '待居保匹配',
  42. NORMAL: '正常在库',
  43. PAUSED: '暂停',
  44. STOPPED: '停止',
  45. FUNERAL: '死亡丧葬',
  46. ABNORMAL: '异常待处理',
  47. ARCHIVED: '已归档',
  48. }
  49. const INSURANCE_LABEL: Record<string, string> = {
  50. PENDING: '未匹配',
  51. SUCCESS: '匹配通过',
  52. LOSE_ELIGIBILITY: '丧失资格',
  53. FAIL: '匹配失败',
  54. MANUAL_PASS: '匹配通过',
  55. }
  56. const ALERT_STATUS_LABEL: Record<string, string> = {
  57. PENDING: '待处理',
  58. PROCESSING: '处理中',
  59. CLOSED: '已关闭',
  60. }
  61. const ALERT_LEVEL_LABEL: Record<string, string> = {
  62. HIGH: '高',
  63. MEDIUM: '中',
  64. LOW: '低',
  65. }
  66. const SPECIAL_TYPE_LABEL: Record<string, string> = {
  67. FUNERAL: '丧葬补贴',
  68. PAUSE: '暂停发放',
  69. STOP: '停止发放',
  70. RESUME: '恢复发放',
  71. STANDARD_ADJUST: '调标差额补发',
  72. SUPPLEMENT: '纳入下月补发',
  73. REFUND: '退款',
  74. HEIR_UPDATE: '继承人信息变更',
  75. }
  76. const SPECIAL_TYPE_CODE: Record<string, string> = {
  77. 丧葬补贴: 'FUNERAL',
  78. 丧葬: 'FUNERAL',
  79. 暂停发放: 'PAUSE',
  80. 暂停: 'PAUSE',
  81. 停止发放: 'STOP',
  82. 停止: 'STOP',
  83. 恢复发放: 'RESUME',
  84. 恢复: 'RESUME',
  85. 调标差额补发: 'STANDARD_ADJUST',
  86. 调标补发: 'STANDARD_ADJUST',
  87. 纳入下月补发: 'SUPPLEMENT',
  88. 补发: 'SUPPLEMENT',
  89. }
  90. /** 发放年月形态:20 开头 6 位(含调标月键如 202699) */
  91. export function isPayMonthLike(value?: string): boolean {
  92. const s = String(value || '').replace(/-/g, '')
  93. return /^20\d{4}$/.test(s)
  94. }
  95. export function payMonthToMonth(payMonth?: string): string {
  96. if (!payMonth) return ''
  97. const s = String(payMonth).replace(/-/g, '')
  98. if (s.length === 6) return `${s.slice(0, 4)}-${s.slice(4)}`
  99. return payMonth
  100. }
  101. export function monthToPayMonth(month?: string): string {
  102. if (!month) return ''
  103. return String(month).replace(/-/g, '')
  104. }
  105. /**
  106. * 统一批次号展示:优先发放年月 payMonth(如 202608)
  107. * 历史库里可能把业务号前 6 位(如 214808)误填进 batchNo,需避开
  108. */
  109. export function resolveUnifiedBatchNo(raw: Record<string, unknown>): string {
  110. const pay = String(raw.payMonth || '').replace(/-/g, '')
  111. const no = String(raw.batchNo || '').replace(/-/g, '')
  112. if (isPayMonthLike(pay)) return pay
  113. if (isPayMonthLike(no)) return no
  114. // 都不是合法发放月:仍回传原值,便于排查脏数据
  115. return String(raw.batchNo || raw.payMonth || raw.id || '')
  116. }
  117. export function fmtTime(ts?: number | string | null): string {
  118. if (ts == null || ts === '') return ''
  119. const n = Number(ts)
  120. if (!Number.isFinite(n) || n <= 0) return String(ts)
  121. const d = new Date(n)
  122. if (Number.isNaN(d.getTime())) return String(ts)
  123. const p = (x: number) => String(x).padStart(2, '0')
  124. return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
  125. }
  126. export function mapPerson(raw: Record<string, unknown>, batchNoHint?: string): Person {
  127. const biz = String(raw.bizStatus || '')
  128. const ins = String(raw.insuranceMatchResult || raw.insuranceStatus || '')
  129. let status = PERSON_STATUS_LABEL[biz] || biz || '草稿'
  130. // 居保失败待人工:页面用「待人工确认」
  131. if (ins === 'FAIL' || status === '异常待处理') status = '待人工确认'
  132. // 资格备注
  133. const remarks = String(raw.remarks || '')
  134. let retireQualifyType: Person['retireQualifyType']
  135. if (remarks.includes('无土地')) retireQualifyType = '无土地有资格'
  136. else if (remarks.includes('有土地') || remarks.includes('资格已核验')) retireQualifyType = '有土地退养'
  137. return {
  138. id: String(raw.id || '') as unknown as number,
  139. batchNo: batchNoHint || '',
  140. name: String(raw.name || ''),
  141. idCard: String(raw.idNumber || ''),
  142. town: regionName(String(raw.townId || '')),
  143. village: regionName(String(raw.villageId || '')),
  144. bankCard: String(raw.bankCardNumber || ''),
  145. bankName: String(raw.bankName || ''),
  146. socialInsuranceCode: raw.socialInsuranceCode ? String(raw.socialInsuranceCode) : undefined,
  147. // 月补只认街镇标准表关联结果;无标准时勿回退 0/陈旧冗余值
  148. standard:
  149. raw.monthlyStandard == null || raw.monthlyStandard === ''
  150. ? undefined
  151. : Number(raw.monthlyStandard),
  152. startMonth: payMonthToMonth(String(raw.enjoyStartMonth || '')),
  153. status,
  154. source: String(raw.source || ''),
  155. insuranceStatus: INSURANCE_LABEL[ins] || String(raw.insuranceStatus || '未匹配'),
  156. remark: remarks || undefined,
  157. retireQualifyType,
  158. }
  159. }
  160. const FLOW_STAGE_LABEL: Record<string, string> = {
  161. STAGE1_PERSONNEL_INPUT: '人员新增录入',
  162. STAGE2_TOWN_PRECHECK: '镇经发中心初审',
  163. STAGE3_INSURANCE_MATCH: '居保状态匹配',
  164. STAGE4_MONTHLY_LIST: '生成月补贴清单',
  165. STAGE5_LEADER_ONLINE_APPROVE: '分管领导审批',
  166. STAGE6_COMPARE_FILE: '生成对照文件',
  167. STAGE7_OFFLINE_SIGN_PAY: '待线下发放',
  168. STAGE8_RETURN_CONFIRM: '回盘信息确认',
  169. }
  170. export function flowStageLabel(code?: string): string {
  171. if (!code) return ''
  172. return FLOW_STAGE_LABEL[code] || BATCH_STATUS_TO_STAGE[code] || code
  173. }
  174. function resolveSubmittedAt(raw: Record<string, unknown>): string | undefined {
  175. const fromField =
  176. fmtTime(raw.submitTime as number | string) || fmtTime(raw.updateTime as number | string)
  177. if (fromField) return fromField
  178. const remarks = String(raw.remarks || '')
  179. const m = remarks.match(/SUBMIT_TIME\|(\d+)/)
  180. if (m?.[1]) return fmtTime(m[1])
  181. return undefined
  182. }
  183. export function mapVillageBatch(raw: Record<string, unknown>): VillageBatch {
  184. const status = String(raw.status || '')
  185. const stage =
  186. BATCH_STATUS_TO_STAGE[status] ||
  187. flowStageLabel(String(raw.currentStage || '')) ||
  188. status
  189. const batchNo = resolveUnifiedBatchNo(raw)
  190. const payMonth = isPayMonthLike(String(raw.payMonth || ''))
  191. ? String(raw.payMonth).replace(/-/g, '')
  192. : batchNo
  193. return {
  194. id: String(raw.id || ''),
  195. batchNo,
  196. month: payMonthToMonth(payMonth),
  197. town: regionName(String(raw.townId || '')),
  198. village: regionName(String(raw.villageId || '')),
  199. peopleCount: Number(raw.peopleCount || 0),
  200. totalAmount: Number(raw.totalAmount || 0),
  201. stage,
  202. submitter: String(raw.submitter || ''),
  203. submittedAt: resolveSubmittedAt(raw),
  204. statusCode: status || undefined,
  205. exportStatus: raw.exportStatus ? String(raw.exportStatus) : undefined,
  206. returnStatus: raw.returnStatus ? String(raw.returnStatus) : undefined,
  207. exportDiskNo: raw.exportDiskNo ? String(raw.exportDiskNo) : undefined,
  208. exportFileName: raw.exportFileName ? String(raw.exportFileName) : undefined,
  209. townOpinion: raw.townOpinion ? String(raw.townOpinion) : undefined,
  210. remarks: raw.remarks ? String(raw.remarks) : undefined,
  211. normalAmount: Number(raw.normalAmount || 0) || undefined,
  212. supplementAmount: Number(raw.supplementAmount || 0) || undefined,
  213. deductAmount: Number(raw.deductAmount || 0) || undefined,
  214. }
  215. }
  216. export function mapProcessLog(raw: Record<string, unknown>): ProcessLogItem {
  217. return {
  218. id: String(raw.id || ''),
  219. batchId: raw.batchId ? String(raw.batchId) : undefined,
  220. batchNo: raw.batchNo ? String(raw.batchNo) : undefined,
  221. stage: flowStageLabel(String(raw.stage || '')),
  222. action: String(raw.action || raw.approveNode || '办理'),
  223. approveNode: raw.approveNode ? String(raw.approveNode) : undefined,
  224. operator: String(raw.operator || raw.operatorRole || ''),
  225. operatorRole: raw.operatorRole ? String(raw.operatorRole) : undefined,
  226. operatorOrg: raw.operatorOrg ? String(raw.operatorOrg) : undefined,
  227. opinion: raw.opinion ? String(raw.opinion) : undefined,
  228. result: String(raw.result || raw.status || ''),
  229. status: raw.status ? String(raw.status) : undefined,
  230. operateTime: fmtTime(raw.operateTime as number),
  231. fileUrls: raw.fileUrls ? String(raw.fileUrls) : undefined,
  232. }
  233. }
  234. export function mapVillageMonthly(raw: Record<string, unknown>): VillageMonthlyBatch {
  235. const b = mapVillageBatch(raw)
  236. const status = String(raw.status || '')
  237. return {
  238. id: b.id,
  239. batchNo: b.batchNo,
  240. month: b.month,
  241. town: b.town,
  242. village: b.village,
  243. peopleCount: b.peopleCount,
  244. normalAmount: Number(raw.normalAmount || 0),
  245. extraAmount: Number(raw.supplementAmount || 0),
  246. deductAmount: Number(raw.deductAmount || 0),
  247. totalAmount: Number(raw.totalAmount || b.totalAmount || 0),
  248. status: BATCH_STATUS_TO_MONTHLY[status] || b.stage,
  249. stage: b.stage,
  250. submitter: b.submitter,
  251. submittedAt: b.submittedAt,
  252. }
  253. }
  254. export function mapDisk(raw: Record<string, unknown>): DiskRecord {
  255. const status = String(raw.status || '')
  256. const returnStatusRaw = String(raw.returnStatus || '未回盘')
  257. let diskStatus = '待回盘'
  258. let returnStatus = '待确认'
  259. if (status === 'RETURNED' || status === 'ARCHIVED') {
  260. diskStatus = '已回盘'
  261. if (returnStatusRaw.includes('部分') || returnStatusRaw.includes('异常')) returnStatus = '部分失败'
  262. else if (returnStatusRaw.includes('成功') || returnStatusRaw === '全部成功') returnStatus = '成功'
  263. else returnStatus = '成功'
  264. } else if (status === 'EXPORTED') {
  265. diskStatus = '待回盘'
  266. returnStatus = '待确认'
  267. }
  268. return {
  269. id: String(raw.id || ''),
  270. batchNo: String(raw.batchNo || ''),
  271. town: regionName(String(raw.townId || '')),
  272. fileName: String(raw.exportFileName || `${raw.batchNo || ''}-月补贴发放文件.xlsx`),
  273. peopleCount: Number(raw.peopleCount || 0),
  274. amount: Number(raw.totalAmount || 0),
  275. status: diskStatus,
  276. returnStatus,
  277. failCount: 0,
  278. returnFileName: undefined,
  279. }
  280. }
  281. export function mapReturnFail(raw: Record<string, unknown>, diskId: string): DiskFailPerson {
  282. return {
  283. id: String(raw.id || raw.paymentDetailId || ''),
  284. diskId,
  285. batchNo: String(raw.batchId || ''),
  286. town: regionName(String(raw.townId || '')),
  287. village: regionName(String(raw.villageId || '')),
  288. personName: String(raw.name || ''),
  289. idCard: String(raw.idNumber || ''),
  290. bankCard: undefined,
  291. failReason: String(raw.failReasonDesc || raw.failReasonCode || ''),
  292. amount: Number(raw.amount ?? raw.actualAmount ?? 0),
  293. payStatus: raw.success === false ? '待确认' : '已确认发放',
  294. }
  295. }
  296. function alertBatchLabel(raw: Record<string, unknown>): string {
  297. const no = String(raw.batchNo || '').trim()
  298. if (no) return no
  299. const id = String(raw.batchId || '').trim()
  300. if (id) return id
  301. // 兼容历史:正文里「批次 VB…」
  302. const content = String(raw.content || '')
  303. const m = content.match(/批次\s*([A-Za-z0-9_-]+)/)
  304. return m?.[1] || ''
  305. }
  306. export function mapAlert(raw: Record<string, unknown>): AlertItem {
  307. const hs = String(raw.handleStatus || '')
  308. const level = String(raw.level || '')
  309. return {
  310. id: String(raw.id || ''),
  311. batchNo: alertBatchLabel(raw),
  312. personName: String(raw.personnelName || ''),
  313. town: regionName(String(raw.townId || '')),
  314. village: regionName(String(raw.villageId || '')),
  315. type: String(raw.type || ''),
  316. level: ALERT_LEVEL_LABEL[level] || level,
  317. content: String(raw.content || ''),
  318. status: ALERT_STATUS_LABEL[hs] || hs || '待处理',
  319. result: raw.handleResult ? String(raw.handleResult) : undefined,
  320. createdAt: fmtTime(raw.createTime as number),
  321. failReason: raw.failReasonCode ? String(raw.failReasonCode) : undefined,
  322. }
  323. }
  324. export function mapSpecial(raw: Record<string, unknown>): SpecialBiz {
  325. const bizType = String(raw.bizType || '')
  326. return {
  327. id: String(raw.id || ''),
  328. batchNo: String(raw.batchNo || ''),
  329. town: regionName(String(raw.townId || '')),
  330. village: '',
  331. type: SPECIAL_TYPE_LABEL[bizType] || bizType,
  332. personName: String(raw.name || ''),
  333. reason: String(raw.reason || ''),
  334. amount: Number(raw.amount || 0),
  335. status: String(raw.approveStatus || ''),
  336. heirName: raw.heirName ? String(raw.heirName) : undefined,
  337. createdAt: fmtTime(raw.createTime as number),
  338. }
  339. }
  340. export function specialTypeToCode(type: string): string {
  341. return SPECIAL_TYPE_CODE[type] || type
  342. }
  343. export function mapStandard(raw: Record<string, unknown>): SubsidyStandard {
  344. // effectiveTime 可能是毫秒时间戳或 yyyy-MM-dd
  345. let date = ''
  346. const effRaw = raw.effectiveTime
  347. if (typeof effRaw === 'number' && Number.isFinite(effRaw)) {
  348. const d = new Date(effRaw)
  349. date = Number.isNaN(d.getTime())
  350. ? ''
  351. : `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
  352. } else {
  353. const eff = String(effRaw || '')
  354. date = eff.length >= 10 ? eff.slice(0, 10) : eff
  355. }
  356. const year = date ? Number(date.slice(0, 4)) : new Date().getFullYear()
  357. // id 可能是字符串,页面用 number 比较「是否最新」——用 hash
  358. const idStr = String(raw.id || '')
  359. let idNum = Number(idStr)
  360. if (!Number.isFinite(idNum)) {
  361. idNum = [...idStr].reduce((s, c) => s + c.charCodeAt(0), 0)
  362. }
  363. return {
  364. id: idNum,
  365. town: String(raw.townName || regionName(String(raw.townId || ''))),
  366. year: Number.isFinite(year) ? year : new Date().getFullYear(),
  367. monthly: Number(raw.amount || 0),
  368. funeral: Number(raw.funeralAmount || 0),
  369. date,
  370. reason: String(raw.adjustReason || raw.remarks || ''),
  371. }
  372. }
  373. /** 审批单合成 ID:B:batchId / S:specialId */
  374. export function approvalIdBatch(batchId: string) {
  375. return `B:${batchId}`
  376. }
  377. export function approvalIdSpecial(specialId: string) {
  378. return `S:${specialId}`
  379. }
  380. export function parseApprovalId(id: string): { kind: 'batch' | 'special'; bizId: string } | null {
  381. if (id.startsWith('B:')) return { kind: 'batch', bizId: id.slice(2) }
  382. if (id.startsWith('S:')) return { kind: 'special', bizId: id.slice(2) }
  383. // 兼容直接传批次/特殊单 ID
  384. if (id.startsWith('VB') || id.startsWith('MB')) return { kind: 'batch', bizId: id }
  385. return { kind: 'special', bizId: id }
  386. }
  387. export function personIdStr(id: number | string): string {
  388. return String(id)
  389. }