realBiz.ts 131 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031
  1. /**
  2. * sjnmtybt-server 真实接口实现(对齐新前端页面流程与既有函数签名)
  3. * 页面仍调用 @/api/biz;useMock=false 时走到本模块。
  4. */
  5. import service, { clearGetCache, del, get, post, put } from '@/utils/request'
  6. import { getAppConfig } from '@/utils/appConfig'
  7. import {
  8. FLOW_STAGES,
  9. filterTodosByRole,
  10. getBarChartTitle,
  11. getDashboardScopeLabel,
  12. LEADER_WORKBENCH_TIPS,
  13. SOCIAL_WORKBENCH_TIPS,
  14. TOWN_REVIEW_TIPS,
  15. VILLAGE_WORKBENCH_TIPS,
  16. } from '@/utils/buildDashboard'
  17. import { batchStatusLabel, processActionLabelOf, roleLabelOf } from '@/utils/statusLabels'
  18. import type {
  19. AccountItem,
  20. AlertItem,
  21. Approval,
  22. BatchMemberPerson,
  23. DashboardData,
  24. DeathRecord,
  25. DiskFailPerson,
  26. DiskRecord,
  27. HandleAlertPayload,
  28. MonthlyBatch,
  29. OpLog,
  30. Person,
  31. SpecialBiz,
  32. SubsidyStandard,
  33. VillageBatch,
  34. VillageMonthlyBatch,
  35. AdjustReissuePerson,
  36. BatchFlowOverview,
  37. ProcessLogItem,
  38. } from '@/types/biz'
  39. import {
  40. ensureTowns,
  41. ensureAllVillages,
  42. ensureVillages,
  43. invalidateRegionCache,
  44. regionName,
  45. resolveScopeIds,
  46. townIdOf,
  47. type RegionRow,
  48. } from './regionStore'
  49. import {
  50. approvalIdBatch,
  51. approvalIdSpecial,
  52. approvalIdQualify,
  53. flowStageLabel,
  54. mapAlert,
  55. mapDisk,
  56. mapPerson,
  57. mapProcessLog,
  58. mapReturnFail,
  59. mapSpecial,
  60. mapStandard,
  61. mapVillageBatch,
  62. mapVillageMonthly,
  63. monthToPayMonth,
  64. parseApprovalId,
  65. personIdStr,
  66. sanitizePersonnelRemark,
  67. specialTypeToCode,
  68. isPayMonthLike,
  69. } from './mappers'
  70. type PageResult<T> = { list?: T[]; total?: number; pageNum?: number; pageSize?: number }
  71. async function pageAll<T>(
  72. fetcher: (pageNum: number, pageSize: number) => Promise<PageResult<T> | null | undefined>,
  73. pageSize = 100,
  74. ): Promise<T[]> {
  75. const all: T[] = []
  76. for (let pageNum = 1; ; pageNum++) {
  77. const res = await fetcher(pageNum, pageSize)
  78. const list = res?.list || []
  79. all.push(...list)
  80. const total = Number(res?.total || 0)
  81. if (list.length < pageSize || (total > 0 && all.length >= total)) break
  82. }
  83. return all
  84. }
  85. function currentPayMonth() {
  86. return monthToPayMonth(getAppConfig().currentMonth)
  87. }
  88. function operatorOf() {
  89. const cfg = getAppConfig()
  90. const role = cfg.roles.find((r) => r.key === cfg.currentRole)
  91. return role?.label || cfg.currentRole || '联调'
  92. }
  93. type MonthStatRow = {
  94. payMonth?: string
  95. peopleCount?: number
  96. totalAmount?: number | string
  97. actualAmount?: number | string
  98. failCount?: number
  99. batchCount?: number
  100. }
  101. type RegionDashRow = {
  102. payMonth?: string
  103. townId?: string
  104. villageId?: string
  105. grain?: string
  106. personnelCount?: number
  107. currentMonth?: MonthStatRow
  108. series?: MonthStatRow[]
  109. }
  110. type BatchStatRow = {
  111. townId?: string
  112. villageId?: string
  113. shouldPayCount?: number
  114. shouldPayAmount?: number | string
  115. actualPayAmount?: number | string
  116. }
  117. function formatPayMonthLabel(ym?: string) {
  118. const s = String(ym || '')
  119. if (s.length === 6) return `${s.slice(0, 4)}-${s.slice(4)}`
  120. return s || '-'
  121. }
  122. function seriesToHalfYearCharts(series: MonthStatRow[] | undefined, months = 6) {
  123. const list = Array.isArray(series) ? series.slice(-months) : []
  124. return {
  125. people: list.map((m) => ({
  126. name: formatPayMonthLabel(m.payMonth),
  127. value: Number(m.peopleCount || 0),
  128. })),
  129. amount: list.map((m) => ({
  130. name: formatPayMonthLabel(m.payMonth),
  131. value: Number(m.totalAmount || 0),
  132. })),
  133. }
  134. }
  135. /** 本批次各村发放人数/金额(镇级工作台) */
  136. async function buildTownVillageChartsFromApi(townId: string, payMonth: string) {
  137. await ensureVillages(townId)
  138. const batches = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
  139. post<PageResult<Record<string, unknown>>>('/batches/page', {
  140. pageNum,
  141. pageSize,
  142. townId,
  143. payMonth,
  144. batchLevel: 'TOWN',
  145. lite: true,
  146. }),
  147. )
  148. const peopleMap = new Map<string, number>()
  149. const amountMap = new Map<string, number>()
  150. for (const b of batches) {
  151. if (String(b.status || '') === 'ARCHIVED') continue
  152. const vName = regionName(String(b.villageId || '')) || String(b.villageId || '未知村')
  153. peopleMap.set(vName, (peopleMap.get(vName) || 0) + Number(b.peopleCount || 0))
  154. amountMap.set(vName, (amountMap.get(vName) || 0) + Number(b.totalAmount || 0))
  155. }
  156. const names = Array.from(new Set([...peopleMap.keys(), ...amountMap.keys()])).sort((a, b) =>
  157. a.localeCompare(b, 'zh-CN'),
  158. )
  159. return {
  160. people: names.map((name) => ({ name, value: peopleMap.get(name) || 0 })),
  161. amount: names.map((name) => ({ name, value: amountMap.get(name) || 0 })),
  162. }
  163. }
  164. function emptyDashboard(role: ReturnType<typeof getAppConfig>['roles'][0], payMonth: string): DashboardData {
  165. const isDistrict = Boolean(role.allData)
  166. return {
  167. stats: { normal: 0, pending: 0, alert: 0, amount: 0, monthIncrease: 0, monthDecrease: 0 },
  168. batchInfo: {
  169. batchNo: payMonth,
  170. scope: getDashboardScopeLabel(role),
  171. peopleCount: 0,
  172. amount: 0,
  173. todoCount: 0,
  174. alertCount: 0,
  175. },
  176. barChartTitle: getBarChartTitle(role),
  177. workflow: [...FLOW_STAGES],
  178. workflowActive: 0,
  179. townAmountChart: [],
  180. batchCompareChart: [],
  181. statusPieMonth: [],
  182. statusPieYear: [],
  183. todos: [],
  184. recentAlerts: [],
  185. villageTips: role.village ? VILLAGE_WORKBENCH_TIPS : undefined,
  186. districtOverview: isDistrict
  187. ? {
  188. focus: role.key === 'agri' ? 'qualify' : 'amount',
  189. townCount: 0,
  190. villageCount: 0,
  191. peopleCount: 0,
  192. amount: 0,
  193. withLandCount: 0,
  194. withoutLandCount: 0,
  195. qualifiedCount: 0,
  196. townPeopleChart: [],
  197. townAmountChart: [],
  198. townWithLandChart: [],
  199. townWithoutLandChart: [],
  200. qualifyPie: [],
  201. halfYearPeopleChart: [],
  202. halfYearAmountChart: [],
  203. townSummary: [],
  204. }
  205. : undefined,
  206. }
  207. }
  208. type WorkbenchApi = {
  209. summary?: Record<string, unknown> & {
  210. totalPeople?: number
  211. totalAmount?: number | string
  212. pendingAlertCount?: number
  213. townStats?: BatchStatRow[]
  214. }
  215. region?: RegionDashRow
  216. personnelCount?: number
  217. withLandCount?: number
  218. withoutLandCount?: number
  219. qualifiedCount?: number
  220. pendingTownApproveCount?: number
  221. pendingLeaderApproveCount?: number
  222. monthBatchCount?: number
  223. monthPeopleCount?: number
  224. monthAmount?: number | string
  225. monthIncreaseCount?: number
  226. monthDecreaseCount?: number
  227. townQualify?: Array<{
  228. townId?: string
  229. peopleCount?: number
  230. villageCount?: number
  231. withLandCount?: number
  232. withoutLandCount?: number
  233. qualifiedCount?: number
  234. amount?: number | string
  235. }>
  236. villageCharts?: Array<{ villageId?: string; peopleCount?: number; amount?: number | string }>
  237. batchStatusCounts?: Record<string, number>
  238. recentAlerts?: Record<string, unknown>[]
  239. monthBatches?: Record<string, unknown>[]
  240. todos?: Array<{
  241. id?: string
  242. type?: string
  243. title?: string
  244. node?: string
  245. path?: string
  246. bizId?: string
  247. read?: boolean
  248. }>
  249. }
  250. /** 线上旧包无 /workbench 时,用已有 summary + region-stats 拼装 */
  251. async function fetchWorkbenchLegacy(
  252. payMonth: string,
  253. townId?: string,
  254. villageId?: string,
  255. months = 6,
  256. ): Promise<WorkbenchApi | null> {
  257. const [summary, region] = await Promise.all([
  258. get<WorkbenchApi['summary']>(
  259. '/dashboard/summary',
  260. { payMonth, townId: townId || undefined },
  261. { silent: true },
  262. ).catch(() => null),
  263. get<RegionDashRow>(
  264. '/dashboard/region-stats',
  265. {
  266. payMonth,
  267. townId: townId || undefined,
  268. villageId: villageId || undefined,
  269. months,
  270. },
  271. { silent: true },
  272. ).catch(() => null),
  273. ])
  274. if (!summary && !region) return null
  275. const monthBatches = await pageAll((pageNum, pageSize) =>
  276. post<PageResult<Record<string, unknown>>>(
  277. '/batches/page',
  278. {
  279. pageNum,
  280. pageSize,
  281. payMonth,
  282. townId: townId || undefined,
  283. villageId: villageId || undefined,
  284. batchLevel: townId ? 'TOWN' : 'DISTRICT',
  285. lite: true,
  286. },
  287. { silent: true },
  288. ).catch(() => null),
  289. )
  290. const batchStatusCounts: Record<string, number> = {}
  291. for (const b of monthBatches) {
  292. const st = String(b.status || '')
  293. if (!st) continue
  294. batchStatusCounts[st] = (batchStatusCounts[st] || 0) + 1
  295. }
  296. const alertList = await pageAll((pageNum, pageSize) =>
  297. post<PageResult<Record<string, unknown>>>(
  298. '/alerts/page',
  299. {
  300. pageNum,
  301. pageSize,
  302. handleStatus: 'PENDING',
  303. townId: townId || undefined,
  304. villageId: villageId || undefined,
  305. },
  306. { silent: true },
  307. ).catch(() => null),
  308. )
  309. let villageCharts: WorkbenchApi['villageCharts']
  310. if (townId && !villageId) {
  311. const charts = await buildTownVillageChartsFromApi(townId, payMonth).catch(() => null)
  312. if (charts) {
  313. villageCharts = charts.people.map((p, idx) => ({
  314. villageId: p.name,
  315. peopleCount: p.value,
  316. amount: charts.amount[idx]?.value || 0,
  317. }))
  318. }
  319. }
  320. return {
  321. summary: summary || undefined,
  322. region: region || undefined,
  323. personnelCount: Number(region?.personnelCount || 0),
  324. monthPeopleCount: Number(region?.currentMonth?.peopleCount || 0),
  325. monthAmount: region?.currentMonth?.totalAmount,
  326. monthBatchCount: monthBatches.length,
  327. pendingTownApproveCount: Number(batchStatusCounts.PENDING_TOWN_APPROVE || 0),
  328. pendingLeaderApproveCount: Number(batchStatusCounts.PENDING_LEADER_APPROVE || 0),
  329. batchStatusCounts,
  330. monthBatches,
  331. recentAlerts: alertList,
  332. villageCharts,
  333. }
  334. }
  335. async function loadWorkbenchApi(
  336. payMonth: string,
  337. townId?: string,
  338. villageId?: string,
  339. months = 6,
  340. roleKey?: string,
  341. ): Promise<WorkbenchApi | null> {
  342. const wb = await get<WorkbenchApi>(
  343. '/dashboard/workbench',
  344. {
  345. payMonth,
  346. townId: townId || undefined,
  347. villageId: villageId || undefined,
  348. months,
  349. operator: operatorOf(),
  350. roleKey: roleKey || getAppConfig().currentRole,
  351. },
  352. { silent: true, timeout: 180_000 },
  353. ).catch(() => null)
  354. if (wb) return wb
  355. return fetchWorkbenchLegacy(payMonth, townId, villageId, months)
  356. }
  357. /** 报表/工作台共用:一次拉人员合计、街镇资格、本月金额,禁止为此再 pageAll 人员 */
  358. export async function fetchWorkbenchStats(params?: {
  359. payMonth?: string
  360. town?: string
  361. village?: string
  362. months?: number
  363. roleKey?: string
  364. }): Promise<WorkbenchApi | null> {
  365. await ensureTowns()
  366. const cfg = getAppConfig()
  367. const role = cfg.roles.find((r) => r.key === (params?.roleKey || cfg.currentRole))
  368. const { townId, villageId } = await resolveScopeIds(
  369. params?.town || role?.town,
  370. params?.village || role?.village,
  371. )
  372. const payMonth = params?.payMonth || currentPayMonth()
  373. return loadWorkbenchApi(
  374. payMonth,
  375. townId || undefined,
  376. villageId || undefined,
  377. params?.months ?? 1,
  378. params?.roleKey || cfg.currentRole,
  379. )
  380. }
  381. /** 标记工作台待办已读 */
  382. export async function markWorkbenchTodoRead(todoKey: string) {
  383. if (!todoKey) return
  384. await post('/dashboard/workbench/todos/read', {
  385. todoKey,
  386. operator: operatorOf(),
  387. }).catch(() => undefined)
  388. }
  389. /** —— 工作台:优先单接口聚合;旧后台无 workbench 时自动降级 —— */
  390. export async function fetchDashboard(params: {
  391. roleKey: string
  392. currentMonth: string
  393. }): Promise<DashboardData> {
  394. await ensureTowns()
  395. const role =
  396. getAppConfig().roles.find((r) => r.key === params.roleKey) || getAppConfig().roles[0]
  397. const { townId, villageId } = await resolveScopeIds(role.town, role.village)
  398. if (townId) await ensureVillages(townId)
  399. const payMonth = monthToPayMonth(params.currentMonth)
  400. const base = emptyDashboard(role, payMonth)
  401. base.batchInfo.scope =
  402. role.village && role.town
  403. ? `${role.town}/${role.village}`
  404. : role.town
  405. ? role.town
  406. : role.allData
  407. ? townId
  408. ? regionName(townId) || role.town || '全区'
  409. : '全区'
  410. : base.batchInfo.scope
  411. const wb = await loadWorkbenchApi(
  412. payMonth,
  413. townId || undefined,
  414. villageId || undefined,
  415. 6,
  416. params.roleKey,
  417. )
  418. if (!wb) return base
  419. const summary = wb.summary
  420. const region = wb.region
  421. const halfYear = seriesToHalfYearCharts(region?.series, 6)
  422. let regionPeople = Number(region?.currentMonth?.peopleCount || 0)
  423. let regionAmount = Number(region?.currentMonth?.totalAmount || 0)
  424. if (regionPeople <= 0) regionPeople = Number(wb.monthPeopleCount || 0)
  425. if (regionAmount <= 0) regionAmount = Number(wb.monthAmount || 0)
  426. // 发放人数/金额禁止回退到在册人数或镇级汇总,避免村级卡片 1367 人配上被截断的 53 万
  427. if (halfYear.people.length) {
  428. const lastIdx = halfYear.people.length - 1
  429. if (regionPeople > 0) halfYear.people[lastIdx].value = regionPeople
  430. if (regionAmount > 0) halfYear.amount[lastIdx].value = regionAmount
  431. }
  432. const pendingAlert = Number(summary?.pendingAlertCount || 0)
  433. const pendingTown = Number(wb.pendingTownApproveCount || 0)
  434. const pendingLeader = Number(wb.pendingLeaderApproveCount || 0)
  435. const pending =
  436. role.key === 'town' ? pendingTown : role.key === 'leader' ? pendingLeader : pendingTown + pendingLeader
  437. base.stats.amount = regionAmount
  438. base.stats.alert = pendingAlert
  439. base.stats.normal = Number(region?.personnelCount ?? wb.personnelCount ?? regionPeople)
  440. base.stats.pending = pending
  441. base.stats.monthIncrease = Number(wb.monthIncreaseCount || 0)
  442. base.stats.monthDecrease = Number(wb.monthDecreaseCount || 0)
  443. base.batchInfo.peopleCount = regionPeople
  444. base.batchInfo.amount = regionAmount
  445. base.batchInfo.alertCount = pendingAlert
  446. const monthBatches = (wb.monthBatches || []).map(mapVillageBatch)
  447. const statusCounts = wb.batchStatusCounts || {}
  448. base.statusPieMonth = Object.entries(statusCounts)
  449. .map(([code, value]) => ({
  450. name: batchStatusLabel(code),
  451. value: Number(value) || 0,
  452. }))
  453. .filter((i) => i.value > 0)
  454. base.statusPieYear = base.statusPieMonth
  455. base.recentAlerts = (wb.recentAlerts || []).map(mapAlert)
  456. const apiTodos = (wb.todos || []).map((t) => ({
  457. id: String(t.id || ''),
  458. type: String(t.type || '待办'),
  459. title: String(t.title || ''),
  460. node: String(t.node || ''),
  461. path: t.path ? String(t.path) : undefined,
  462. bizId: t.bizId ? String(t.bizId) : undefined,
  463. read: !!t.read,
  464. }))
  465. const allowedPaths = getAppConfig().rolePermissions[role.key] || []
  466. const rawTodos =
  467. apiTodos.length > 0
  468. ? apiTodos
  469. : role.allData
  470. ? []
  471. : monthBatches.map((b) => ({
  472. id: `BATCH|${b.id}`,
  473. type: '批次流转',
  474. title: `${b.batchNo} ${b.town}${b.village}月度批次`,
  475. node: b.stage,
  476. path: '/approvals',
  477. bizId: b.id,
  478. read: false,
  479. }))
  480. base.todos = filterTodosByRole(rawTodos, role.key, allowedPaths)
  481. base.batchInfo.todoCount = base.todos.filter((t) => !t.read).length
  482. let workflowActive = FLOW_STAGES.length - 1
  483. for (let i = 0; i < FLOW_STAGES.length; i++) {
  484. if (monthBatches.some((b) => b.stage.includes(FLOW_STAGES[i]) || (i === 0 && b.stage === '退回修改'))) {
  485. workflowActive = i
  486. break
  487. }
  488. }
  489. base.workflowActive = workflowActive
  490. if (role.village || role.key === 'village') {
  491. base.villageHalfYearPeopleChart = halfYear.people
  492. base.villageHalfYearAmountChart = halfYear.amount
  493. base.townAmountChart = halfYear.amount
  494. base.batchCompareChart = halfYear.amount.map((i, idx) => ({
  495. ...i,
  496. count: halfYear.people[idx]?.value,
  497. }))
  498. }
  499. if (townId && !villageId && (role.key === 'town' || role.key === 'social' || role.key === 'leader')) {
  500. const villageCharts = {
  501. people: (wb.villageCharts || []).map((v) => ({
  502. name: regionName(String(v.villageId || '')) || String(v.villageId || '未知村'),
  503. value: Number(v.peopleCount || 0),
  504. })),
  505. amount: (wb.villageCharts || []).map((v) => ({
  506. name: regionName(String(v.villageId || '')) || String(v.villageId || '未知村'),
  507. value: Number(v.amount || 0),
  508. })),
  509. }
  510. base.townVillagePeopleChart = villageCharts.people
  511. base.townVillageAmountChart = villageCharts.amount
  512. base.townAmountChart = villageCharts.amount
  513. if (halfYear.people.length) {
  514. base.batchCompareChart = halfYear.amount.map((i, idx) => ({
  515. ...i,
  516. count: halfYear.people[idx]?.value,
  517. }))
  518. }
  519. }
  520. const withLand = Number(wb.withLandCount || 0)
  521. const withoutLand = Number(wb.withoutLandCount || 0)
  522. if (role.key === 'town') {
  523. base.townReview = {
  524. pendingCount: pendingTown,
  525. withLandCount: withLand,
  526. withoutLandCount: withoutLand,
  527. pendingVillageCount: new Set(monthBatches.filter((b) => b.statusCode === 'PENDING_TOWN_APPROVE').map((b) => b.village)).size,
  528. qualifyPie: [
  529. { name: '有土地退养', value: withLand },
  530. { name: '无土地有资格', value: withoutLand },
  531. ].filter((i) => i.value > 0),
  532. villageReviews: (wb.villageCharts || []).map((v) => ({
  533. village: regionName(String(v.villageId || '')) || String(v.villageId || '未知村'),
  534. total: Number(v.peopleCount || 0),
  535. pending: 0,
  536. withLand: 0,
  537. withoutLand: 0,
  538. })),
  539. pendingPeople: [],
  540. tips: TOWN_REVIEW_TIPS,
  541. }
  542. }
  543. if (role.key === 'social') {
  544. base.socialWorkbench = {
  545. matchPendingCount: Number(statusCounts.PENDING_INSURANCE_MATCH || statusCounts.INSURANCE_MATCHING || 0),
  546. compareReadyCount: Number(statusCounts.READY_EXPORT || 0),
  547. diskPendingCount:
  548. Number(statusCounts.READY_EXPORT || 0) + Number(statusCounts.EXPORTED || 0),
  549. alertCount: pendingAlert,
  550. tips: SOCIAL_WORKBENCH_TIPS,
  551. }
  552. }
  553. if (role.key === 'leader') {
  554. base.leaderWorkbench = {
  555. pendingApprovalCount: pendingLeader,
  556. specialPendingCount: 0,
  557. issueReadyCount: Number(statusCounts.READY_EXPORT || 0),
  558. alertCount: pendingAlert,
  559. tips: LEADER_WORKBENCH_TIPS,
  560. }
  561. }
  562. if (role.allData && base.districtOverview) {
  563. const townStats = (summary?.townStats as BatchStatRow[] | undefined) || []
  564. const byTownPay = new Map<string, { people: number; amount: number }>()
  565. for (const s of townStats) {
  566. const name = regionName(String(s.townId || '')) || String(s.townId || '未知')
  567. const curT = byTownPay.get(name) || { people: 0, amount: 0 }
  568. curT.people += Number(s.shouldPayCount || 0)
  569. curT.amount += Number(s.shouldPayAmount || 0)
  570. byTownPay.set(name, curT)
  571. }
  572. const qRows = wb.townQualify || []
  573. const names = Array.from(
  574. new Set([
  575. ...byTownPay.keys(),
  576. ...qRows.map((r) => regionName(String(r.townId || '')) || String(r.townId || '未知')),
  577. ]),
  578. ).sort((a, b) => a.localeCompare(b, 'zh-CN'))
  579. const qByName = new Map(
  580. qRows.map((r) => [
  581. regionName(String(r.townId || '')) || String(r.townId || '未知'),
  582. r,
  583. ]),
  584. )
  585. base.districtOverview.halfYearPeopleChart = halfYear.people
  586. base.districtOverview.halfYearAmountChart = halfYear.amount
  587. base.districtOverview.peopleCount = regionPeople
  588. base.districtOverview.amount = regionAmount
  589. base.districtOverview.withLandCount = withLand
  590. base.districtOverview.withoutLandCount = withoutLand
  591. base.districtOverview.qualifiedCount = Number(wb.qualifiedCount || 0)
  592. base.districtOverview.townCount = names.length
  593. base.districtOverview.villageCount = qRows.reduce((s, r) => s + Number(r.villageCount || 0), 0)
  594. base.districtOverview.townPeopleChart = names.map((n) => ({
  595. name: n,
  596. value: Number(qByName.get(n)?.peopleCount || 0) || byTownPay.get(n)?.people || 0,
  597. }))
  598. base.districtOverview.townAmountChart = names.map((n) => ({
  599. name: n,
  600. value: Number(qByName.get(n)?.amount || 0) || byTownPay.get(n)?.amount || 0,
  601. }))
  602. base.districtOverview.townWithLandChart = names.map((n) => ({
  603. name: n,
  604. value: Number(qByName.get(n)?.withLandCount || 0),
  605. }))
  606. base.districtOverview.townWithoutLandChart = names.map((n) => ({
  607. name: n,
  608. value: Number(qByName.get(n)?.withoutLandCount || 0),
  609. }))
  610. base.districtOverview.qualifyPie = [
  611. { name: '有土地退养', value: withLand },
  612. { name: '无土地有资格', value: withoutLand },
  613. {
  614. name: '未标注资格',
  615. value: Math.max(0, Number(wb.personnelCount || 0) - withLand - withoutLand),
  616. },
  617. ].filter((i) => i.value > 0)
  618. base.districtOverview.townSummary = names.map((n) => {
  619. const q = qByName.get(n)
  620. const pay = byTownPay.get(n)
  621. return {
  622. town: n,
  623. villageCount: Number(q?.villageCount || 0),
  624. peopleCount: Number(q?.peopleCount || 0) || Number(pay?.people || 0),
  625. amount: Number(q?.amount || 0) || Number(pay?.amount || 0),
  626. withLandCount: Number(q?.withLandCount || 0),
  627. withoutLandCount: Number(q?.withoutLandCount || 0),
  628. qualifiedCount: Number(q?.qualifiedCount || 0),
  629. }
  630. })
  631. base.townAmountChart = base.districtOverview.townAmountChart
  632. }
  633. return base
  634. }
  635. /** —— 人员 / 村批 —— */
  636. export async function fetchVillageBatches(): Promise<VillageBatch[]> {
  637. await ensureTowns()
  638. const cfg = getAppConfig()
  639. const role = cfg.roles.find((r) => r.key === cfg.currentRole)
  640. const { townId } = await resolveScopeIds(role?.town, role?.village)
  641. if (townId) await ensureVillages(townId)
  642. const districtPage = await post<PageResult<Record<string, unknown>>>('/batches/page', {
  643. pageNum: 1,
  644. pageSize: 50,
  645. batchLevel: 'DISTRICT',
  646. lite: true,
  647. })
  648. let list = districtPage?.list || []
  649. if (!list.length) {
  650. const townPage = await post<PageResult<Record<string, unknown>>>('/batches/page', {
  651. pageNum: 1,
  652. pageSize: 50,
  653. batchLevel: 'TOWN',
  654. lite: true,
  655. })
  656. const towns = townPage?.list || []
  657. const seen = new Set<string>()
  658. const synthesized: Record<string, unknown>[] = []
  659. for (const b of towns) {
  660. const parent = String(b.parentBatchId || '')
  661. const id = parent || String(b.id || '')
  662. if (!id || seen.has(id)) continue
  663. seen.add(id)
  664. synthesized.push({
  665. ...b,
  666. id,
  667. batchLevel: 'DISTRICT',
  668. townId: '',
  669. villageId: '',
  670. batchNo: String(b.payMonth || b.batchNo || ''),
  671. })
  672. }
  673. list = synthesized
  674. }
  675. const mapped = list.map(mapVillageBatch)
  676. return mapped.sort((a, b) => {
  677. const ak = isPayMonthLike(a.batchNo) ? a.batchNo : `0${a.batchNo}`
  678. const bk = isPayMonthLike(b.batchNo) ? b.batchNo : `0${b.batchNo}`
  679. if (ak !== bk) return bk.localeCompare(ak)
  680. return String(b.id).localeCompare(String(a.id))
  681. })
  682. }
  683. /** 已停用:村合作社不再创建村批 */
  684. export async function ensureVillageMonthBatch(_town?: string, _village?: string, _payMonth?: string) {
  685. return
  686. }
  687. export type PeoplePageParams = {
  688. batchNo?: string
  689. town?: string
  690. village?: string
  691. name?: string
  692. idCard?: string
  693. phone?: string
  694. /** 业务状态码,如 NORMAL / PAUSED */
  695. bizStatus?: string
  696. /** 居保匹配码 SUCCESS / FAIL / PENDING… */
  697. insuranceMatchResult?: string
  698. /** 退养资格 WITH_LAND / WITHOUT_LAND / INELIGIBLE / UNSET */
  699. retireQualifyType?: string
  700. source?: string
  701. /** 排除业务状态,逗号分隔 */
  702. bizStatusNotIn?: string
  703. /** 仅包含业务状态,逗号分隔 */
  704. bizStatusIn?: string
  705. /** 非本村视角隐藏他村 DRAFT */
  706. hideDraftUnlessVillage?: boolean
  707. pageNum?: number
  708. pageSize?: number
  709. }
  710. export async function fetchPeoplePage(
  711. params?: PeoplePageParams,
  712. ): Promise<{ list: Person[]; total: number; pageNum: number; pageSize: number }> {
  713. await ensureTowns()
  714. const cfg = getAppConfig()
  715. const role = cfg.roles.find((r) => r.key === cfg.currentRole)
  716. const town = params?.town || role?.town
  717. const village = params?.village || role?.village
  718. const { townId, villageId } = await resolveScopeIds(town, village)
  719. if (townId) await ensureVillages(town || townId)
  720. else await ensureAllVillages()
  721. const pageNum = params?.pageNum || 1
  722. const pageSize = params?.pageSize || 10
  723. const res = await post<PageResult<Record<string, unknown>>>('/personnel/page', {
  724. pageNum,
  725. pageSize,
  726. townId: townId || undefined,
  727. villageId: villageId || undefined,
  728. name: params?.name || undefined,
  729. idNumber: params?.idCard || undefined,
  730. phoneNumber: params?.phone || undefined,
  731. bizStatus: params?.bizStatus || undefined,
  732. insuranceMatchResult: params?.insuranceMatchResult || undefined,
  733. retireQualifyType:
  734. params?.retireQualifyType && params.retireQualifyType !== 'UNSET'
  735. ? params.retireQualifyType
  736. : undefined,
  737. retireQualifyUnset: params?.retireQualifyType === 'UNSET' ? true : undefined,
  738. source: params?.source || undefined,
  739. bizStatusNotIn: params?.bizStatusNotIn || undefined,
  740. bizStatusIn: params?.bizStatusIn || undefined,
  741. hideDraftUnlessVillage: params?.hideDraftUnlessVillage ? true : undefined,
  742. payMonth: params?.batchNo || undefined,
  743. }, { timeout: 90_000 })
  744. const list = (res?.list || []).map((p) => mapPerson(p, params?.batchNo || currentPayMonth()))
  745. return {
  746. list,
  747. total: Number(res?.total || 0),
  748. pageNum: Number(res?.pageNum || pageNum),
  749. pageSize: Number(res?.pageSize || pageSize),
  750. }
  751. }
  752. export async function fetchPeople(params?: {
  753. batchNo?: string
  754. town?: string
  755. village?: string
  756. }): Promise<Person[]> {
  757. const page = await fetchPeoplePage({
  758. ...params,
  759. pageNum: 1,
  760. pageSize: 500,
  761. })
  762. // 兼容旧调用:尽量拉全本村(单页上限 500;超大村可再扩)
  763. if (page.total <= page.list.length) return page.list
  764. const all = [...page.list]
  765. const pages = Math.max(1, Math.ceil(page.total / 500))
  766. for (let n = 2; n <= pages; n++) {
  767. const more = await fetchPeoplePage({ ...params, pageNum: n, pageSize: 500 })
  768. all.push(...more.list)
  769. if (more.list.length < 500) break
  770. }
  771. return all
  772. }
  773. export async function downloadPersonnelImportTemplate(town?: string, village?: string) {
  774. const { townId, villageId } = await resolveScopeIds(town, village)
  775. const res = await service.get('/personnel/import-template', {
  776. params: {
  777. townId: townId || undefined,
  778. villageId: villageId || undefined,
  779. },
  780. responseType: 'blob',
  781. })
  782. const blob = res.data as Blob
  783. const url = URL.createObjectURL(blob)
  784. const a = document.createElement('a')
  785. a.href = url
  786. a.download = '人员导入模板.xlsx'
  787. a.click()
  788. URL.revokeObjectURL(url)
  789. }
  790. export type PersonnelImportFailRow = {
  791. rowNum?: number
  792. error?: string
  793. cells?: string[]
  794. name?: string
  795. idNumber?: string
  796. phoneNumber?: string
  797. town?: string
  798. village?: string
  799. bankCardNumber?: string
  800. [key: string]: unknown
  801. }
  802. export type PersonnelImportResult = {
  803. fileName?: string
  804. successCount?: number
  805. failCount?: number
  806. errors?: string[]
  807. failRows?: PersonnelImportFailRow[]
  808. message?: string
  809. }
  810. export type PersonnelImportHistoryItem = {
  811. id?: string
  812. operator?: string
  813. operateTime?: number
  814. fileName?: string
  815. successCount?: number
  816. failCount?: number
  817. errorSummary?: string
  818. townId?: string
  819. villageId?: string
  820. result?: string
  821. }
  822. export async function importPersonnelExcel(file: File, town?: string, village?: string) {
  823. const { townId, villageId } = await resolveScopeIds(town, village)
  824. if (!townId || !villageId) throw new Error('请先选择街镇与村居')
  825. const form = new FormData()
  826. form.append('file', file)
  827. return post<PersonnelImportResult>('/personnel/import', form as unknown as Record<string, unknown>, {
  828. params: { townId, villageId },
  829. timeout: 600000,
  830. })
  831. }
  832. export async function exportPersonnelImportFailRows(failRows: PersonnelImportFailRow[]) {
  833. if (!failRows?.length) throw new Error('没有可导出的失败行')
  834. const res = await service.post('/personnel/import/fail-export', { failRows }, {
  835. responseType: 'blob',
  836. timeout: 120000,
  837. })
  838. const blob = res.data as Blob
  839. if (blob && blob.type && blob.type.includes('application/json')) {
  840. const text = await blob.text()
  841. let msg = '导出失败'
  842. try {
  843. const j = JSON.parse(text) as { message?: string; msg?: string }
  844. msg = j.message || j.msg || msg
  845. } catch {
  846. /* ignore */
  847. }
  848. throw new Error(msg)
  849. }
  850. const url = URL.createObjectURL(blob)
  851. const a = document.createElement('a')
  852. a.href = url
  853. a.download = '人员导入失败行.xlsx'
  854. document.body.appendChild(a)
  855. a.click()
  856. a.remove()
  857. URL.revokeObjectURL(url)
  858. }
  859. export async function fetchPersonnelImportHistory(limit = 50): Promise<PersonnelImportHistoryItem[]> {
  860. const list = await get<PersonnelImportHistoryItem[]>('/personnel/import/history', { limit })
  861. return Array.isArray(list) ? list : []
  862. }
  863. export async function fetchAlertDetail(alertId: string): Promise<AlertItem | null> {
  864. if (!alertId) return null
  865. await ensureTowns()
  866. try {
  867. const raw = await get<Record<string, unknown>>(`/alerts/${encodeURIComponent(alertId)}`)
  868. return raw ? mapAlert(raw) : null
  869. } catch {
  870. return null
  871. }
  872. }
  873. export async function fetchPersonDetail(personnelId: string): Promise<Person | null> {
  874. if (!personnelId) return null
  875. await ensureTowns()
  876. try {
  877. const raw = await get<Record<string, unknown>>(
  878. `/personnel/${encodeURIComponent(personnelId)}`,
  879. )
  880. return raw ? mapPerson(raw) : null
  881. } catch {
  882. return null
  883. }
  884. }
  885. export async function savePerson(payload: Partial<Person>) {
  886. const cfg = getAppConfig()
  887. const role = cfg.roles.find((r) => r.key === cfg.currentRole)
  888. const town = payload.town || role?.town
  889. const village = payload.village || role?.village
  890. const { townId, villageId } = await resolveScopeIds(town, village)
  891. if (!townId || !villageId) throw new Error('请先选择街镇与村居')
  892. const isEdit = Boolean(payload.id)
  893. const blank = (v: unknown) => (v == null || v === '' ? undefined : String(v).trim())
  894. const numOrU = (v: unknown) =>
  895. v == null || v === '' || Number.isNaN(Number(v)) ? undefined : Number(v)
  896. const raw = await post<Record<string, unknown>>(
  897. '/personnel',
  898. {
  899. id: isEdit ? String(payload.id) : undefined,
  900. name: payload.name,
  901. idNumber: payload.idCard,
  902. townId,
  903. villageId,
  904. bankCardNumber: payload.bankCard,
  905. bankName: blank(payload.bankName),
  906. bankType: blank(payload.bankType),
  907. bankBranchCode: blank(payload.bankBranchCode),
  908. socialInsuranceCode: blank(payload.socialInsuranceCode),
  909. phoneNumber: blank(payload.phoneNumber),
  910. address: blank(payload.address),
  911. householdType: blank(payload.householdType),
  912. villageCode: blank(payload.villageCode),
  913. entrustPlanCode: blank(payload.entrustPlanCode),
  914. entrustPlanName: blank(payload.entrustPlanName),
  915. monthlyStandard: numOrU(payload.standard),
  916. baseAmount: numOrU(payload.baseAmount),
  917. oneTimeAmount: numOrU(payload.oneTimeAmount),
  918. landSubsidyIncrease: numOrU(payload.landSubsidyIncrease),
  919. funeralDiffAmount: numOrU(payload.funeralDiffAmount),
  920. enjoyStartMonth: monthToPayMonth(payload.startMonth) || (isEdit ? undefined : currentPayMonth()),
  921. enjoyEndMonth: monthToPayMonth(payload.enjoyEndMonth) || undefined,
  922. supplementStartMonth: monthToPayMonth(payload.supplementStartMonth) || undefined,
  923. nextPayStartMonth: monthToPayMonth(payload.nextPayStartMonth) || undefined,
  924. payeeName: blank(payload.payeeName) || blank(payload.name),
  925. deathDate: isEdit ? String(payload.deathDate ?? '').trim() : blank(payload.deathDate),
  926. heirName: isEdit ? String(payload.heirName ?? '').trim() : blank(payload.heirName),
  927. heirRelation: isEdit ? String(payload.heirRelation ?? '').trim() : blank(payload.heirRelation),
  928. heirIdNumber: isEdit ? String(payload.heirIdNumber ?? '').trim() : blank(payload.heirIdNumber),
  929. heirBankCard: isEdit ? String(payload.heirBankCard ?? '').trim() : blank(payload.heirBankCard),
  930. heirPhone: isEdit ? String(payload.heirPhone ?? '').trim() : blank(payload.heirPhone),
  931. source: payload.source || (isEdit ? '管理员修改' : '村级录入'),
  932. remarks: (() => {
  933. const edited = payload.remark
  934. const raw = payload.remarkRaw
  935. if (raw != null && sanitizePersonnelRemark(raw) === String(edited || '').trim()) {
  936. return raw
  937. }
  938. return edited
  939. })(),
  940. operatorRoleKey: cfg.currentRole,
  941. expectedUpdateTime: isEdit ? payload.updateTimeMs : undefined,
  942. },
  943. { skipCache: true },
  944. )
  945. clearGetCache()
  946. return raw ? mapPerson(raw) : null
  947. }
  948. export async function submitRetireQualify(
  949. personnelId: string,
  950. payload?: { opinion?: string; fileUrls?: string },
  951. config?: { silent?: boolean },
  952. ) {
  953. const raw = await post<Record<string, unknown>>(
  954. `/personnel/${encodeURIComponent(personnelId)}/qualify/submit`,
  955. {
  956. opinion: payload?.opinion,
  957. fileUrls: payload?.fileUrls,
  958. operator: operatorOf(),
  959. },
  960. config,
  961. )
  962. return raw ? mapPerson(raw) : null
  963. }
  964. export async function submitRetireQualifyBatch(payload: {
  965. personnelIds: string[]
  966. opinion?: string
  967. fileUrls?: string
  968. }) {
  969. const res = await post<Record<string, unknown>>(
  970. '/personnel/qualify/submit-batch',
  971. {
  972. personnelIds: payload.personnelIds,
  973. opinion: payload.opinion,
  974. fileUrls: payload.fileUrls,
  975. operator: operatorOf(),
  976. },
  977. { timeout: 180_000 },
  978. )
  979. return {
  980. ok: res?.ok !== false,
  981. message: String(res?.message || ''),
  982. successCount: Number(res?.successCount || 0),
  983. pendingCount: Number(res?.pendingCount || 0),
  984. failCount: Number(res?.failCount || 0),
  985. }
  986. }
  987. function qualifyTypeCode(
  988. retireQualifyType?: '有土地退养' | '无土地有资格' | '无资格',
  989. ) {
  990. return retireQualifyType === '无资格'
  991. ? 'INELIGIBLE'
  992. : retireQualifyType === '无土地有资格'
  993. ? 'WITHOUT_LAND'
  994. : retireQualifyType === '有土地退养'
  995. ? 'WITH_LAND'
  996. : undefined
  997. }
  998. export async function reviewRetireQualify(payload: {
  999. personnelId: string
  1000. pass: boolean
  1001. retireQualifyType?: '有土地退养' | '无土地有资格' | '无资格'
  1002. opinion?: string
  1003. fileUrls?: string
  1004. }) {
  1005. await post(`/personnel/${encodeURIComponent(payload.personnelId)}/qualify/review`, {
  1006. pass: payload.pass,
  1007. retireQualifyType: qualifyTypeCode(payload.retireQualifyType),
  1008. opinion: payload.opinion,
  1009. fileUrls: payload.fileUrls,
  1010. operator: operatorOf(),
  1011. })
  1012. return { ok: true }
  1013. }
  1014. export async function reviewRetireQualifyBatch(payload: {
  1015. personnelIds: string[]
  1016. pass: boolean
  1017. retireQualifyType?: '有土地退养' | '无土地有资格' | '无资格'
  1018. opinion?: string
  1019. fileUrls?: string
  1020. }) {
  1021. const res = await post<Record<string, unknown>>('/personnel/qualify/review-batch', {
  1022. personnelIds: payload.personnelIds,
  1023. pass: payload.pass,
  1024. retireQualifyType: qualifyTypeCode(payload.retireQualifyType),
  1025. opinion: payload.opinion,
  1026. fileUrls: payload.fileUrls,
  1027. operator: operatorOf(),
  1028. }, { timeout: 180_000 })
  1029. return {
  1030. ok: res?.ok !== false,
  1031. message: String(res?.message || ''),
  1032. successCount: Number(res?.successCount || 0),
  1033. failCount: Number(res?.failCount || 0),
  1034. }
  1035. }
  1036. export async function generateDistrictMonthBatch(
  1037. payMonth?: string,
  1038. matchInsurance = true,
  1039. simulateFirstDay = false,
  1040. ) {
  1041. const res = await post<Record<string, unknown>>('/batches/generate-district-month', undefined, {
  1042. params: {
  1043. payMonth: (payMonth || currentPayMonth()).replace(/-/g, ''),
  1044. matchInsurance,
  1045. simulateFirstDay: simulateFirstDay || undefined,
  1046. },
  1047. timeout: 1_200_000,
  1048. })
  1049. clearGetCache()
  1050. return res
  1051. }
  1052. export async function fetchPendingSpecialCount(town?: string): Promise<number> {
  1053. await ensureTowns()
  1054. const { townId } = await resolveScopeIds(town)
  1055. const specials = await get<PageResult<Record<string, unknown>>>('/special-biz/page', {
  1056. pageNum: 1,
  1057. pageSize: 200,
  1058. approveStatus: '待分管领导审批',
  1059. })
  1060. const rows = specials?.list || []
  1061. if (!townId) return rows.length
  1062. return rows.filter((s) => String(s.townId || '') === townId).length
  1063. }
  1064. export async function submitVillageBatch(batchId: string) {
  1065. await post(`/batches/${encodeURIComponent(batchId)}/submit`)
  1066. return { ok: true }
  1067. }
  1068. /** 更新批次纳入人员(草稿/退回可改) */
  1069. export async function updateBatchMembers(batchId: string, personnelIds: string[]) {
  1070. await put(
  1071. `/batches/${encodeURIComponent(batchId)}/members`,
  1072. {
  1073. personnelIds,
  1074. operator: operatorOf(),
  1075. },
  1076. { timeout: 60000 },
  1077. )
  1078. return { ok: true }
  1079. }
  1080. /** 批次已纳入人员 ID(发放明细) */
  1081. export async function fetchBatchMemberIds(batchId: string): Promise<string[]> {
  1082. if (!batchId) return []
  1083. const list = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
  1084. post<PageResult<Record<string, unknown>>>('/payments/details/page', {
  1085. pageNum,
  1086. pageSize,
  1087. batchId,
  1088. }),
  1089. )
  1090. return list.map((d) => String(d.personnelId || '')).filter(Boolean)
  1091. }
  1092. function numOr0(v: unknown) {
  1093. const n = Number(v)
  1094. return Number.isFinite(n) ? n : 0
  1095. }
  1096. function attachPaymentAmounts(
  1097. base: Person,
  1098. d: Record<string, unknown>,
  1099. batchNo: string,
  1100. ): BatchMemberPerson {
  1101. const monthlyAmount = numOr0(d.monthlyAmount)
  1102. const oneTimeAmount = numOr0(d.oneTimeAmount)
  1103. const supplementAmount = numOr0(d.supplementAmount)
  1104. const funeralAmount = numOr0(d.funeralAmount)
  1105. const deductAmount = numOr0(d.deductAmount)
  1106. const payableRaw = d.totalAmount
  1107. const payableAmount =
  1108. payableRaw == null || payableRaw === ''
  1109. ? monthlyAmount + oneTimeAmount + supplementAmount + funeralAmount - deductAmount
  1110. : numOr0(payableRaw)
  1111. return {
  1112. ...base,
  1113. batchNo,
  1114. // 列表「月补标准」只用发放明细,禁止回退人员底数陈旧月标(同批不一致的根因之一)
  1115. standard: monthlyAmount,
  1116. bankCard: String(d.bankCardNumber || base.bankCard || ''),
  1117. remark: String(d.remarks || base.remark || ''),
  1118. paymentDetailId: String(d.id || ''),
  1119. monthlyAmount,
  1120. oneTimeAmount,
  1121. supplementAmount,
  1122. funeralAmount,
  1123. deductAmount,
  1124. payableAmount,
  1125. districtAmount:
  1126. d.districtAmount == null || d.districtAmount === '' ? undefined : numOr0(d.districtAmount),
  1127. townAmount: d.townAmount == null || d.townAmount === '' ? undefined : numOr0(d.townAmount),
  1128. actualAmount: d.actualAmount == null || d.actualAmount === '' ? undefined : numOr0(d.actualAmount),
  1129. returnResult: d.returnResult ? String(d.returnResult) : undefined,
  1130. changeFlag: d.changeFlag === 'NEW' || d.changeFlag === 'KEEP' || d.changeFlag === 'REMOVE'
  1131. ? (d.changeFlag as 'NEW' | 'KEEP' | 'REMOVE')
  1132. : undefined,
  1133. }
  1134. }
  1135. /**
  1136. * 本村发放历史:按发放明细行展示(月补/补发/应发等),不用人员底数里陈旧的 monthlyStandard。
  1137. */
  1138. /** 指定人员的历史发放明细(各批次) */
  1139. export async function fetchPersonnelPaymentHistory(
  1140. personnelId: string,
  1141. ): Promise<BatchMemberPerson[]> {
  1142. if (!personnelId) return []
  1143. // 区划已缓存则不再请求;人员详情与明细分页并行
  1144. const townsPromise = ensureTowns().catch(() => undefined)
  1145. const [page1, personRaw] = await Promise.all([
  1146. post<PageResult<Record<string, unknown>>>('/payments/details/page', {
  1147. pageNum: 1,
  1148. pageSize: 200,
  1149. personnelId,
  1150. }),
  1151. get<Record<string, unknown>>(`/personnel/${encodeURIComponent(personnelId)}`, undefined, {
  1152. silent: true,
  1153. }).catch(() => null),
  1154. ])
  1155. await townsPromise
  1156. const details = [...(page1?.list || [])]
  1157. const total = Number(page1?.total || details.length)
  1158. if (total > details.length) {
  1159. for (let pageNum = 2; pageNum <= 5 && details.length < total; pageNum++) {
  1160. const more = await post<PageResult<Record<string, unknown>>>('/payments/details/page', {
  1161. pageNum,
  1162. pageSize: 200,
  1163. personnelId,
  1164. })
  1165. const rows = more?.list || []
  1166. if (!rows.length) break
  1167. details.push(...rows)
  1168. if (rows.length < 200) break
  1169. }
  1170. }
  1171. if (!details.length) return []
  1172. const basePerson = personRaw ? mapPerson(personRaw) : null
  1173. const result: BatchMemberPerson[] = []
  1174. for (const d of details) {
  1175. const batchNo = String(d.batchNo || d.payMonth || '')
  1176. const base =
  1177. basePerson ||
  1178. mapPerson(
  1179. {
  1180. id: personnelId,
  1181. name: d.name,
  1182. idNumber: d.idNumber,
  1183. townId: d.townId,
  1184. villageId: d.villageId,
  1185. bankCardNumber: d.bankCardNumber,
  1186. bankName: d.payBank,
  1187. monthlyStandard: d.monthlyAmount,
  1188. remarks: d.remarks,
  1189. bizStatus: 'NORMAL',
  1190. },
  1191. batchNo,
  1192. )
  1193. result.push(attachPaymentAmounts(base, d, batchNo))
  1194. }
  1195. return result.sort((a, b) => String(b.batchNo || '').localeCompare(String(a.batchNo || '')))
  1196. }
  1197. /** 指定人员的告警/处理记录 */
  1198. export async function fetchPersonnelAlerts(params: {
  1199. personnelId?: string
  1200. personName?: string
  1201. town?: string
  1202. village?: string
  1203. }): Promise<AlertItem[]> {
  1204. await ensureTowns()
  1205. const { townId, villageId } = await resolveScopeIds(params.town, params.village)
  1206. const list = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
  1207. post<PageResult<Record<string, unknown>>>('/alerts/page', {
  1208. pageNum,
  1209. pageSize,
  1210. personnelId: params.personnelId || undefined,
  1211. personnelName: params.personName || undefined,
  1212. townId: townId || undefined,
  1213. villageId: villageId || undefined,
  1214. }),
  1215. )
  1216. let mapped = list.map(mapAlert)
  1217. // 后端未带 personnelId 过滤时,前端再按姓名兜底
  1218. if (params.personnelId) {
  1219. const byId = mapped.filter((a) => a.personnelId === params.personnelId)
  1220. if (byId.length) mapped = byId
  1221. else if (params.personName) {
  1222. mapped = mapped.filter((a) => a.personName === params.personName)
  1223. }
  1224. }
  1225. return mapped.sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || '')))
  1226. }
  1227. export async function fetchVillagePaymentHistory(params?: {
  1228. town?: string
  1229. village?: string
  1230. }): Promise<BatchMemberPerson[]> {
  1231. await ensureTowns()
  1232. const cfg = getAppConfig()
  1233. const role = cfg.roles.find((r) => r.key === cfg.currentRole)
  1234. const town = params?.town || role?.town
  1235. const village = params?.village || role?.village
  1236. const { townId, villageId } = await resolveScopeIds(town, village)
  1237. if (townId) await ensureVillages(townId)
  1238. const details = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
  1239. post<PageResult<Record<string, unknown>>>('/payments/details/page', {
  1240. pageNum,
  1241. pageSize,
  1242. townId: townId || undefined,
  1243. villageId: villageId || undefined,
  1244. }),
  1245. )
  1246. if (!details.length) return []
  1247. const roster = await fetchPeople({ town, village }).catch(() => [] as Person[])
  1248. const byId = new Map(roster.map((p) => [String(p.id), p]))
  1249. const result: BatchMemberPerson[] = []
  1250. for (const d of details) {
  1251. const pid = String(d.personnelId || '')
  1252. const batchNo = String(d.batchNo || d.payMonth || '')
  1253. const base =
  1254. (pid && byId.get(pid)) ||
  1255. mapPerson(
  1256. {
  1257. id: pid || String(d.id || ''),
  1258. name: d.name,
  1259. idNumber: d.idNumber,
  1260. townId: d.townId,
  1261. villageId: d.villageId,
  1262. bankCardNumber: d.bankCardNumber,
  1263. bankName: d.payBank,
  1264. monthlyStandard: d.monthlyAmount,
  1265. remarks: d.remarks,
  1266. bizStatus: 'NORMAL',
  1267. },
  1268. batchNo,
  1269. )
  1270. result.push(attachPaymentAmounts(base, d, batchNo))
  1271. }
  1272. return result.sort((a, b) => {
  1273. const bn = String(b.batchNo || '').localeCompare(String(a.batchNo || ''))
  1274. if (bn !== 0) return bn
  1275. return String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN')
  1276. })
  1277. }
  1278. /**
  1279. * 本批次人员明细:只返回发放明细中的人(不是全村人员库)。
  1280. * 草稿/退回批会先走 ensure-current-month,剔除暂停/停止/死亡等默认不应在册人员。
  1281. * 附带本批次金额拆分(月补/补发/扣减/应发合计)。
  1282. */
  1283. export async function fetchBatchMemberPeoplePage(
  1284. batchId: string,
  1285. opts?: {
  1286. batchNo?: string
  1287. town?: string
  1288. village?: string
  1289. pruneDefaults?: boolean
  1290. parentBatchId?: string
  1291. pageNum?: number
  1292. pageSize?: number
  1293. name?: string
  1294. idCard?: string
  1295. sortBy?: string
  1296. sortOrder?: string
  1297. onlyFuneral?: boolean
  1298. onlySupplement?: boolean
  1299. },
  1300. ): Promise<{
  1301. list: BatchMemberPerson[]
  1302. total: number
  1303. pageNum: number
  1304. pageSize: number
  1305. summary?: Record<string, unknown>
  1306. }> {
  1307. const pageNum = opts?.pageNum || 1
  1308. const pageSize = opts?.pageSize || 10
  1309. if (!batchId) return { list: [], total: 0, pageNum, pageSize }
  1310. await ensureTowns()
  1311. const rawTown = opts?.town
  1312. const town = rawTown && rawTown !== '全区' && rawTown !== '本镇' ? rawTown : undefined
  1313. const village = opts?.village
  1314. const villageKey = village === '本镇' || village === '全区' ? undefined : village
  1315. const { townId, villageId } = await resolveScopeIds(town, villageKey)
  1316. const queryBatchId = opts?.parentBatchId || batchId
  1317. const detailsRes = await post<PageResult<Record<string, unknown>>>('/payments/details/page', {
  1318. pageNum,
  1319. pageSize,
  1320. batchId: queryBatchId,
  1321. townId: townId || undefined,
  1322. villageId: villageId || undefined,
  1323. name: opts?.name?.trim() || undefined,
  1324. idNumber: opts?.idCard?.trim() || undefined,
  1325. sortBy: opts?.sortBy || undefined,
  1326. sortOrder: opts?.sortOrder || undefined,
  1327. onlyFuneral: opts?.onlyFuneral || undefined,
  1328. onlySupplement: opts?.onlySupplement || undefined,
  1329. })
  1330. const details = detailsRes?.list || []
  1331. const batchNo = opts?.batchNo || currentPayMonth()
  1332. const result: BatchMemberPerson[] = []
  1333. for (const d of details) {
  1334. const pid = String(d.personnelId || '')
  1335. if (!pid) continue
  1336. const base = mapPerson(
  1337. {
  1338. id: pid,
  1339. name: d.name,
  1340. idNumber: d.idNumber,
  1341. townId: d.townId,
  1342. villageId: d.villageId,
  1343. bankCardNumber: d.bankCardNumber,
  1344. monthlyStandard: d.monthlyAmount,
  1345. remarks: d.remarks,
  1346. bizStatus: d.bizStatus || 'NORMAL',
  1347. retireQualifyType: d.retireQualifyType,
  1348. },
  1349. batchNo,
  1350. )
  1351. result.push(attachPaymentAmounts(base, d, batchNo))
  1352. }
  1353. return {
  1354. list: result,
  1355. total: Number(detailsRes?.total || result.length),
  1356. pageNum,
  1357. pageSize,
  1358. summary: detailsRes?.summary,
  1359. }
  1360. }
  1361. export async function fetchBatchMemberPeople(
  1362. batchId: string,
  1363. opts?: {
  1364. batchNo?: string
  1365. town?: string
  1366. village?: string
  1367. pruneDefaults?: boolean
  1368. parentBatchId?: string
  1369. pageNum?: number
  1370. pageSize?: number
  1371. },
  1372. ): Promise<BatchMemberPerson[]> {
  1373. const page = await fetchBatchMemberPeoplePage(batchId, {
  1374. ...opts,
  1375. pageNum: opts?.pageNum || 1,
  1376. pageSize: opts?.pageSize || 10,
  1377. })
  1378. return page.list
  1379. }
  1380. /** 批次详情(原始 BatchVO → VillageBatch) */
  1381. export async function fetchBatchDetail(batchId: string): Promise<VillageBatch | null> {
  1382. if (!batchId) return null
  1383. await ensureTowns()
  1384. const raw = await get<Record<string, unknown>>(`/batches/${encodeURIComponent(batchId)}`)
  1385. if (!raw) return null
  1386. if (raw.townId) await ensureVillages(String(raw.townId))
  1387. return mapVillageBatch(raw)
  1388. }
  1389. /** 批次流程总览(可执行动作 + 阶段说明) */
  1390. export async function fetchBatchOverview(batchId: string): Promise<BatchFlowOverview | null> {
  1391. if (!batchId) return null
  1392. try {
  1393. const raw = await get<Record<string, unknown>>(
  1394. `/workflow/batches/${encodeURIComponent(batchId)}/overview`,
  1395. )
  1396. if (!raw) return null
  1397. const logs = Array.isArray(raw.recentLogs)
  1398. ? (raw.recentLogs as Record<string, unknown>[]).map(mapProcessLog)
  1399. : []
  1400. return {
  1401. batchId: String(raw.batchId || batchId),
  1402. batchNo: String(raw.batchNo || ''),
  1403. currentStage: flowStageLabel(String(raw.currentStage || '')),
  1404. status: String(raw.status || ''),
  1405. stageDescription: raw.stageDescription ? String(raw.stageDescription) : undefined,
  1406. availableActions: Array.isArray(raw.availableActions)
  1407. ? (raw.availableActions as string[])
  1408. : [],
  1409. recentLogs: logs,
  1410. }
  1411. } catch {
  1412. return null
  1413. }
  1414. }
  1415. /** 批次流程时间线 */
  1416. export async function fetchBatchTimeline(batchId: string): Promise<ProcessLogItem[]> {
  1417. if (!batchId) return []
  1418. try {
  1419. const res = await get<PageResult<Record<string, unknown>> | Record<string, unknown>[]>(
  1420. `/process-logs/batches/${encodeURIComponent(batchId)}/timeline`,
  1421. )
  1422. const list = Array.isArray(res) ? res : res?.list || []
  1423. return list
  1424. .map(mapProcessLog)
  1425. .sort((a, b) => String(b.operateTime || '').localeCompare(String(a.operateTime || '')))
  1426. } catch {
  1427. return []
  1428. }
  1429. }
  1430. /** 人员状态/特殊业务时间线(统一走 POST /process-logs/page,避免未重启后端时 GET timeline 404) */
  1431. export async function fetchPersonnelTimeline(personnelId: string): Promise<ProcessLogItem[]> {
  1432. if (!personnelId) return []
  1433. try {
  1434. const page = await post<PageResult<Record<string, unknown>>>('/process-logs/page', {
  1435. personnelId,
  1436. pageNum: 1,
  1437. pageSize: 100,
  1438. })
  1439. return (page?.list || [])
  1440. .map(mapProcessLog)
  1441. .sort((a, b) => String(b.operateTime || '').localeCompare(String(a.operateTime || '')))
  1442. } catch {
  1443. return []
  1444. }
  1445. }
  1446. /** 补充流程备注 */
  1447. export async function addBatchProcessNote(payload: {
  1448. batchId: string
  1449. opinion: string
  1450. fileUrls?: string
  1451. }) {
  1452. await post('/process-logs/note', {
  1453. batchId: payload.batchId,
  1454. opinion: payload.opinion,
  1455. fileUrls: payload.fileUrls,
  1456. operator: operatorOf(),
  1457. })
  1458. return { ok: true }
  1459. }
  1460. /** —— 居保 —— */
  1461. /** 人工确认需要 batchId:记录最近一次打开的居保批次 */
  1462. let lastInsuranceBatchId = ''
  1463. const STAGE_TO_BATCH_STATUS: Record<string, string> = {
  1464. 人员新增录入: 'DRAFT',
  1465. 镇经发中心初审: 'PENDING_TOWN_APPROVE',
  1466. 居保状态匹配: 'PENDING_INSURANCE_MATCH',
  1467. 比对结果已生成: 'PENDING_STATUS_MAINTAIN',
  1468. 分管领导审批: 'PENDING_LEADER_APPROVE',
  1469. 待线下发放: 'READY_EXPORT',
  1470. 回盘信息确认: 'EXPORTED',
  1471. 已回盘确认: 'RETURNED',
  1472. 已归档: 'ARCHIVED',
  1473. 退回修改: 'REJECTED',
  1474. }
  1475. export async function fetchInsuranceBatchesPage(params?: {
  1476. tab?: 'todo' | 'history'
  1477. batchNo?: string
  1478. stage?: string
  1479. townId?: string
  1480. pageNum?: number
  1481. pageSize?: number
  1482. }): Promise<{ list: VillageBatch[]; total: number; pageNum: number; pageSize: number }> {
  1483. await ensureTowns()
  1484. const pageNum = params?.pageNum || 1
  1485. const pageSize = params?.pageSize || 10
  1486. const statusIn = params?.stage
  1487. ? STAGE_TO_BATCH_STATUS[params.stage]
  1488. : params?.tab === 'history'
  1489. ? 'PENDING_LEADER_APPROVE,READY_EXPORT,EXPORTED,RETURNED,ARCHIVED'
  1490. : 'PENDING_INSURANCE_MATCH,PENDING_STATUS_MAINTAIN'
  1491. const townId = params?.townId || undefined
  1492. const batchLevel = townId ? 'TOWN' : 'DISTRICT'
  1493. const res = await post<PageResult<Record<string, unknown>>>('/batches/page', {
  1494. pageNum,
  1495. pageSize,
  1496. batchLevel,
  1497. townId,
  1498. batchNo: params?.batchNo || undefined,
  1499. statusIn: statusIn || undefined,
  1500. lite: true,
  1501. withTownTotals: true,
  1502. })
  1503. let list = (res?.list || []).map(mapVillageBatch)
  1504. if (!list.length && !params?.batchNo && !townId) {
  1505. const fallback = await post<PageResult<Record<string, unknown>>>('/batches/page', {
  1506. pageNum,
  1507. pageSize,
  1508. batchLevel: 'TOWN',
  1509. batchNo: params?.batchNo || undefined,
  1510. statusIn: statusIn || undefined,
  1511. lite: true,
  1512. withTownTotals: true,
  1513. })
  1514. list = (fallback?.list || []).map(mapVillageBatch)
  1515. return {
  1516. list,
  1517. total: Number(fallback?.total || list.length),
  1518. pageNum,
  1519. pageSize,
  1520. }
  1521. }
  1522. return { list, total: Number(res?.total || list.length), pageNum, pageSize }
  1523. }
  1524. export async function fetchInsuranceBatches(params?: {
  1525. tab?: 'todo' | 'history'
  1526. }): Promise<VillageBatch[]> {
  1527. const page = await fetchInsuranceBatchesPage({ ...params, pageNum: 1, pageSize: 10 })
  1528. return page.list
  1529. }
  1530. function mapInsuranceResultRow(r: Record<string, unknown>, batchNo?: string): Person {
  1531. const match = String(r.matchResult || r.insuranceMatchResult || r.result || '')
  1532. let biz = String(r.bizStatus || '')
  1533. if (!biz) {
  1534. if (match === 'SUCCESS' || match === 'MANUAL_PASS') biz = 'NORMAL'
  1535. else if (match === 'FAIL' || match === 'LOSE_ELIGIBILITY') biz = 'ABNORMAL'
  1536. else biz = 'PENDING_INSURANCE_MATCH'
  1537. }
  1538. const rawMsg = String(r.message || r.remarks || r.failReason || '')
  1539. let remark = rawMsg
  1540. if (match === 'MANUAL_PASS') {
  1541. if (!remark || remark === '匹配成功' || remark === '居保比对通过') {
  1542. remark = '人工确认匹配成功'
  1543. }
  1544. } else if (match === 'SUCCESS') {
  1545. if (!remark || remark === '匹配成功' || remark === '居保比对通过') {
  1546. remark = '系统自动匹配成功'
  1547. }
  1548. }
  1549. return mapPerson(
  1550. {
  1551. id: r.personnelId || r.id,
  1552. name: r.name || r.personnelName,
  1553. idNumber: r.idNumber,
  1554. townId: r.townId,
  1555. villageId: r.villageId,
  1556. insuranceMatchResult: match,
  1557. bizStatus: biz,
  1558. retireQualifyType: r.retireQualifyType,
  1559. remarks: remark,
  1560. },
  1561. batchNo,
  1562. )
  1563. }
  1564. export async function fetchInsuranceResultsPage(params?: {
  1565. batchId?: string
  1566. batchNo?: string
  1567. town?: string
  1568. village?: string
  1569. pageNum?: number
  1570. pageSize?: number
  1571. }): Promise<{ list: Person[]; total: number; pageNum: number; pageSize: number }> {
  1572. const pageNum = params?.pageNum || 1
  1573. const pageSize = params?.pageSize || 10
  1574. let batchId = params?.batchId || lastInsuranceBatchId
  1575. if (!batchId) {
  1576. const found = await fetchInsuranceBatchesPage({
  1577. batchNo: params?.batchNo,
  1578. pageNum: 1,
  1579. pageSize: 1,
  1580. })
  1581. batchId = found.list[0]?.id || ''
  1582. }
  1583. if (!batchId) return { list: [], total: 0, pageNum, pageSize }
  1584. lastInsuranceBatchId = batchId
  1585. try {
  1586. const townName = params?.town && params.town !== '全区' && params.town !== '本镇' ? params.town : ''
  1587. const villageName =
  1588. params?.village && params.village !== '全区' && params.village !== '本镇' ? params.village : ''
  1589. const { townId } = await resolveScopeIds(townName || undefined, villageName || undefined)
  1590. const raw = await get<PageResult<Record<string, unknown>> | Record<string, unknown>[]>(
  1591. `/insurance-match/batches/${encodeURIComponent(batchId)}/results`,
  1592. {
  1593. townId: townId || undefined,
  1594. pageNum,
  1595. pageSize,
  1596. },
  1597. { timeout: 120_000 },
  1598. )
  1599. const rows = Array.isArray(raw) ? raw : raw?.list || []
  1600. const total = Array.isArray(raw) ? raw.length : Number(raw?.total || rows.length)
  1601. let mapped = rows.map((r) => mapInsuranceResultRow(r, params?.batchNo))
  1602. if (townName) mapped = mapped.filter((r) => r.town === townName)
  1603. return { list: mapped, total, pageNum, pageSize }
  1604. } catch {
  1605. return { list: [], total: 0, pageNum, pageSize }
  1606. }
  1607. }
  1608. export async function fetchInsuranceResults(params?: {
  1609. batchId?: string
  1610. batchNo?: string
  1611. town?: string
  1612. village?: string
  1613. pageNum?: number
  1614. pageSize?: number
  1615. }): Promise<Person[]> {
  1616. const page = await fetchInsuranceResultsPage({
  1617. ...params,
  1618. pageNum: params?.pageNum || 1,
  1619. pageSize: params?.pageSize || 10,
  1620. })
  1621. return page.list
  1622. }
  1623. export async function triggerInsuranceMatch(
  1624. batchIds?: string | string[],
  1625. personnelIds?: string | string[],
  1626. ) {
  1627. const ids = batchIds == null ? [] : Array.isArray(batchIds) ? batchIds : [batchIds]
  1628. const pids = personnelIds == null
  1629. ? []
  1630. : Array.isArray(personnelIds)
  1631. ? personnelIds.map(String)
  1632. : [String(personnelIds)]
  1633. let count = 0
  1634. let lastSummary: Record<string, unknown> | null = null
  1635. for (const batchId of ids) {
  1636. lastInsuranceBatchId = batchId
  1637. const res = await post<{
  1638. nextStatus?: string
  1639. failCount?: number
  1640. successCount?: number
  1641. manualCount?: number
  1642. items?: Array<Record<string, unknown>>
  1643. }>('/insurance-match/trigger', {
  1644. batchId,
  1645. personnelIds: pids.length ? pids : undefined,
  1646. operator: operatorOf(),
  1647. })
  1648. lastSummary = {
  1649. batchId,
  1650. ...(res || {}),
  1651. failItems: (res?.items || []).filter((i) => {
  1652. const r = String(i.matchResult || i.match_result || '')
  1653. return r === 'FAIL' || r === 'LOSE_ELIGIBILITY' || r === 'PENDING'
  1654. }),
  1655. }
  1656. // 全员成功时后端已进领导审;仍补齐明细。有 FAIL 时不要假装已生成可提交清单
  1657. if (!res?.failCount) {
  1658. await post(`/payments/batches/${encodeURIComponent(batchId)}/generate`).catch(() => undefined)
  1659. }
  1660. count += 1
  1661. }
  1662. return { ok: true, count, lastSummary }
  1663. }
  1664. /** 居保匹配任务执行日志 */
  1665. export async function fetchInsuranceTaskLogs(batchId: string, limit = 30) {
  1666. if (!batchId) return [] as Record<string, unknown>[]
  1667. const list = await get<Record<string, unknown>[]>(
  1668. `/insurance-match/batches/${encodeURIComponent(batchId)}/task-logs`,
  1669. { limit },
  1670. )
  1671. return Array.isArray(list) ? list : []
  1672. }
  1673. export async function confirmInsuranceManual(
  1674. personId: number | string,
  1675. opts?: { batchId?: string; pass?: boolean; remark?: string },
  1676. ) {
  1677. const bid = opts?.batchId || lastInsuranceBatchId
  1678. if (!bid) {
  1679. throw new Error('请先打开比对结果后再人工确认')
  1680. }
  1681. lastInsuranceBatchId = bid
  1682. const pass = opts?.pass !== false
  1683. await post('/insurance-match/manual-confirm', {
  1684. personnelId: personIdStr(personId),
  1685. batchId: bid,
  1686. pass,
  1687. remark: opts?.remark || (pass ? '人工确认匹配成功' : '人工确认暂停发放'),
  1688. operator: operatorOf(),
  1689. })
  1690. return { ok: true }
  1691. }
  1692. /** 居保完成后提交分管领导(推进批次状态,非仅生成明细) */
  1693. export async function submitToLeader(batchId: string) {
  1694. await post(`/batches/${encodeURIComponent(batchId)}/submit-leader`)
  1695. return { ok: true }
  1696. }
  1697. /** 批次内居保未闭环人数(未匹配/失败等;提交领导前校验) */
  1698. export async function fetchUnresolvedInsuranceCount(batchId: string): Promise<number> {
  1699. if (!batchId) return 0
  1700. const res = await get<{ count?: number }>(
  1701. `/batches/${encodeURIComponent(batchId)}/unresolved-insurance-count`,
  1702. undefined,
  1703. { silent: true },
  1704. )
  1705. return Number(res?.count || 0)
  1706. }
  1707. /** 批量查询居保未闭环人数(月补贴清单列表用) */
  1708. export async function fetchUnresolvedInsuranceCounts(
  1709. batchIds: string[],
  1710. ): Promise<Record<string, number>> {
  1711. const ids = (batchIds || []).filter(Boolean)
  1712. if (!ids.length) return {}
  1713. const res = await post<Record<string, number>>(
  1714. '/batches/unresolved-insurance-counts',
  1715. { batchIds: ids },
  1716. { silent: true },
  1717. )
  1718. return res || {}
  1719. }
  1720. export async function exportCompareList(_batchId?: string) {
  1721. return {
  1722. ok: false as const,
  1723. message: '比对清单文件导出接口尚未对接,请在「查看比对结果」中核对名单',
  1724. fileName: '',
  1725. }
  1726. }
  1727. export async function returnBatchToTown(batchId: string, reason?: string) {
  1728. await post('/batches/town-approve', {
  1729. bizId: batchId,
  1730. action: 'REJECT',
  1731. opinion: reason || '居保匹配异常,退回镇经发中心',
  1732. operator: operatorOf(),
  1733. })
  1734. return { ok: true }
  1735. }
  1736. /** —— 审批 —— */
  1737. const TOWN_HISTORY_SKIP = new Set(['DRAFT', 'PENDING_TOWN_APPROVE'])
  1738. const LEADER_HISTORY_STATUSES = new Set(['READY_EXPORT', 'EXPORTED', 'RETURNED', 'ARCHIVED'])
  1739. function toTownApproval(vb: VillageBatch, status: string): Approval {
  1740. return {
  1741. id: approvalIdBatch(vb.id),
  1742. batchId: vb.id,
  1743. bizType: '月度批次人员申请',
  1744. batchNo: vb.batchNo,
  1745. town: vb.town,
  1746. village: vb.village,
  1747. title: `${vb.village}${vb.month}批次人员申请`,
  1748. applicant: `${vb.village}合作社`,
  1749. node: '镇经发中心初审',
  1750. status,
  1751. createdAt: vb.submittedAt || '',
  1752. peopleCount: vb.peopleCount,
  1753. totalAmount: vb.totalAmount,
  1754. }
  1755. }
  1756. function toQualifyApproval(p: Person, status: string): Approval {
  1757. return {
  1758. id: approvalIdQualify(String(p.id)),
  1759. bizType: '退养资格审批',
  1760. batchNo: '—',
  1761. town: p.town,
  1762. village: p.village,
  1763. title: `${p.name || ''}退养资格审批`,
  1764. applicant: `${p.village || ''}合作社`,
  1765. node: '镇经发中心初审',
  1766. status,
  1767. createdAt: p.updateTime || p.createTime || '',
  1768. peopleCount: 1,
  1769. personName: p.name,
  1770. personnelId: String(p.id),
  1771. idCard: p.idCard,
  1772. }
  1773. }
  1774. function toLeaderApproval(vb: VillageBatch, status: string): Approval {
  1775. return {
  1776. id: approvalIdBatch(vb.id),
  1777. batchId: vb.id,
  1778. bizType: '月补贴清单确认',
  1779. batchNo: vb.batchNo,
  1780. town: vb.town,
  1781. village: vb.village || '本镇',
  1782. title: `${vb.town}月补贴清单提交分管领导`,
  1783. applicant: `${vb.town}社区事务受理中心`,
  1784. node: '分管领导线上审批',
  1785. status,
  1786. createdAt: vb.submittedAt || '',
  1787. peopleCount: vb.peopleCount,
  1788. totalAmount: vb.totalAmount,
  1789. }
  1790. }
  1791. export async function fetchApprovals(params?: {
  1792. tab?: string
  1793. node?: string
  1794. town?: string
  1795. village?: string
  1796. batchNo?: string
  1797. bizTypeIncludes?: string
  1798. pageNum?: number
  1799. pageSize?: number
  1800. status?: string
  1801. }): Promise<{ list: Approval[]; total: number; pageNum: number; pageSize: number }> {
  1802. await ensureTowns()
  1803. const { townId, villageId } = await resolveScopeIds(params?.town, params?.village)
  1804. if (townId) await ensureVillages(townId)
  1805. const pageNum = params?.pageNum || 1
  1806. const pageSize = params?.pageSize || 10
  1807. const tab = params?.tab === 'history' ? 'history' : params?.tab === 'all' ? 'all' : 'pending'
  1808. const wantTown = !params?.node || params.node.includes('镇经发')
  1809. const wantLeader = !params?.node || params.node.includes('分管领导')
  1810. if (wantTown && !wantLeader) {
  1811. if (tab !== 'history') {
  1812. const people = await fetchPeoplePage({
  1813. town: params?.town,
  1814. village: params?.village,
  1815. bizStatus: 'PENDING_TOWN_APPROVE',
  1816. pageNum,
  1817. pageSize,
  1818. })
  1819. return {
  1820. list: people.list.map((p) => toQualifyApproval(p, '待审批')),
  1821. total: people.total,
  1822. pageNum,
  1823. pageSize,
  1824. }
  1825. }
  1826. const status = params?.status
  1827. const action =
  1828. status === '已通过'
  1829. ? 'QUALIFY_PASS'
  1830. : status === '已驳回'
  1831. ? 'QUALIFY_REJECT'
  1832. : 'QUALIFY_PASS,QUALIFY_REJECT'
  1833. const logs = await post<PageResult<Record<string, unknown>>>('/process-logs/page', {
  1834. pageNum,
  1835. pageSize,
  1836. townId: townId || undefined,
  1837. villageId: villageId || undefined,
  1838. bizType: 'RETIRE_QUALIFY',
  1839. action,
  1840. })
  1841. const rows = logs?.list || []
  1842. const list: Approval[] = []
  1843. await Promise.all(
  1844. rows.map(async (log) => {
  1845. const pid = String(log.personnelId || '')
  1846. if (!pid) return
  1847. const raw = await get<Record<string, unknown>>(`/personnel/${encodeURIComponent(pid)}`).catch(
  1848. () => null,
  1849. )
  1850. if (!raw) return
  1851. const p = mapPerson(raw)
  1852. const row = toQualifyApproval(
  1853. p,
  1854. String(log.action) === 'QUALIFY_PASS' ? '已通过' : '已驳回',
  1855. )
  1856. const mapped = mapProcessLog(log)
  1857. if (mapped.operateTime) row.createdAt = mapped.operateTime
  1858. list.push(row)
  1859. }),
  1860. )
  1861. return { list, total: Number(logs?.total || list.length), pageNum, pageSize }
  1862. }
  1863. const list: Approval[] = []
  1864. let extraTotal = 0
  1865. if (wantLeader && tab !== 'history') {
  1866. const batchPage = await post<PageResult<Record<string, unknown>>>('/batches/page', {
  1867. pageNum: 1,
  1868. pageSize: 20,
  1869. townId: townId || undefined,
  1870. batchLevel: 'TOWN',
  1871. status: 'PENDING_LEADER_APPROVE',
  1872. batchNo: params?.batchNo || undefined,
  1873. lite: true,
  1874. })
  1875. const specials = await collectSpecialApprovals({
  1876. townId,
  1877. town: params?.town,
  1878. approveStatuses: ['待分管领导审批'],
  1879. pageNum,
  1880. pageSize,
  1881. })
  1882. if (pageNum === 1) {
  1883. for (const b of batchPage?.list || []) list.push(toLeaderApproval(mapVillageBatch(b), '待审批'))
  1884. }
  1885. list.push(...specials.list)
  1886. extraTotal = Number(batchPage?.total || 0) + specials.total
  1887. } else if (wantLeader) {
  1888. const status = params?.status
  1889. const specials = await collectSpecialApprovals({
  1890. townId,
  1891. town: params?.town,
  1892. approveStatuses:
  1893. status === '已通过' ? ['已通过'] : status === '已驳回' ? ['已驳回'] : ['已通过', '已驳回'],
  1894. pageNum,
  1895. pageSize,
  1896. })
  1897. list.push(...specials.list)
  1898. extraTotal = specials.total
  1899. }
  1900. return { list, total: extraTotal || list.length, pageNum, pageSize }
  1901. }
  1902. async function pageTownStatRows(opts: {
  1903. townId?: string
  1904. status?: string
  1905. batchNo?: string
  1906. }) {
  1907. return pageAll<Record<string, unknown>>((pageNum, pageSize) =>
  1908. post<PageResult<Record<string, unknown>>>('/batches/page', {
  1909. pageNum,
  1910. pageSize,
  1911. townId: opts.townId || undefined,
  1912. batchLevel: 'TOWN',
  1913. status: opts.status,
  1914. batchNo: opts.batchNo,
  1915. lite: true,
  1916. withTownTotals: true,
  1917. }),
  1918. )
  1919. }
  1920. async function pageDistrictBatchRows(opts?: { status?: string; batchNo?: string }) {
  1921. return pageAll<Record<string, unknown>>((pageNum, pageSize) =>
  1922. post<PageResult<Record<string, unknown>>>('/batches/page', {
  1923. pageNum,
  1924. pageSize,
  1925. batchLevel: 'DISTRICT',
  1926. status: opts?.status,
  1927. batchNo: opts?.batchNo,
  1928. lite: true,
  1929. withDistrictTotals: true,
  1930. }),
  1931. )
  1932. }
  1933. async function collectSpecialApprovals(opts: {
  1934. townId?: string
  1935. town?: string
  1936. approveStatuses: string[]
  1937. pageNum?: number
  1938. pageSize?: number
  1939. }): Promise<{ list: Approval[]; total: number }> {
  1940. const query: Record<string, unknown> = {
  1941. pageNum: opts.pageNum || 1,
  1942. pageSize: opts.pageSize || 10,
  1943. }
  1944. if (opts.approveStatuses.length === 1) {
  1945. query.approveStatus = opts.approveStatuses[0]
  1946. }
  1947. const specials = await get<PageResult<Record<string, unknown>>>('/special-biz/page', query)
  1948. const specialRows = specials?.list || []
  1949. const townIdsNeedVillage = new Set<string>()
  1950. if (opts.townId) townIdsNeedVillage.add(opts.townId)
  1951. for (const s of specialRows) {
  1952. if (s.townId) townIdsNeedVillage.add(String(s.townId))
  1953. }
  1954. await Promise.all([...townIdsNeedVillage].map((tid) => ensureVillages(tid)))
  1955. const peopleById = new Map<string, Record<string, unknown>>()
  1956. const missingIds = [
  1957. ...new Set(
  1958. specialRows
  1959. .filter((s) => s.personnelId && (!s.villageId || !s.name || !s.idNumber))
  1960. .map((s) => String(s.personnelId)),
  1961. ),
  1962. ]
  1963. await Promise.all(
  1964. missingIds.map(async (id) => {
  1965. const p = await get<Record<string, unknown>>(`/personnel/${encodeURIComponent(id)}`).catch(
  1966. () => null,
  1967. )
  1968. if (p) peopleById.set(id, p)
  1969. }),
  1970. )
  1971. const list: Approval[] = []
  1972. for (const s of specialRows) {
  1973. const statusRaw = String(s.approveStatus || '')
  1974. if (opts.approveStatuses.length && !opts.approveStatuses.includes(statusRaw)) continue
  1975. let sTownId = String(s.townId || '')
  1976. let sVillageId = String(s.villageId || '')
  1977. let personName = String(s.name || '')
  1978. let idCard = s.idNumber ? String(s.idNumber) : ''
  1979. const personnelId = s.personnelId ? String(s.personnelId) : ''
  1980. const person = personnelId ? peopleById.get(personnelId) : undefined
  1981. if (person) {
  1982. if (!sVillageId && person.villageId) sVillageId = String(person.villageId)
  1983. if (!sTownId && person.townId) sTownId = String(person.townId)
  1984. if (!personName && person.name) personName = String(person.name)
  1985. if (!idCard && person.idNumber) idCard = String(person.idNumber)
  1986. } else if (personnelId && (!sVillageId || !personName)) {
  1987. try {
  1988. const detail = await get<Record<string, unknown>>(
  1989. `/personnel/${encodeURIComponent(personnelId)}`,
  1990. )
  1991. if (detail) {
  1992. if (!sVillageId && detail.villageId) sVillageId = String(detail.villageId)
  1993. if (!sTownId && detail.townId) sTownId = String(detail.townId)
  1994. if (!personName && detail.name) personName = String(detail.name)
  1995. if (!idCard && detail.idNumber) idCard = String(detail.idNumber)
  1996. }
  1997. } catch {
  1998. /* ignore */
  1999. }
  2000. }
  2001. if (sTownId) await ensureVillages(sTownId)
  2002. if (opts.townId && sTownId && sTownId !== opts.townId) continue
  2003. const townLabel = regionName(sTownId) || opts.town || ''
  2004. const villageResolved = sVillageId ? regionName(sVillageId) : ''
  2005. const villageLabel =
  2006. villageResolved && !villageResolved.startsWith('V_') ? villageResolved : ''
  2007. if (!opts.townId && opts.town && townLabel && townLabel !== opts.town) continue
  2008. const deathDate = s.deathDate ? String(s.deathDate) : ''
  2009. const reason = String(s.reason || '')
  2010. const bizTypeRaw = String(s.bizType || '')
  2011. const typeLabel =
  2012. ({ FUNERAL: '丧葬补贴', STOP: '停止发放', PAUSE: '暂停发放', RESUME: '恢复发放' } as Record<
  2013. string,
  2014. string
  2015. >)[bizTypeRaw] || mapSpecial(s).type
  2016. const isDeathReg = Boolean(deathDate) || reason.includes('死亡登记')
  2017. const status =
  2018. statusRaw === '待分管领导审批' || statusRaw === '待审批' || !statusRaw ? '待审批' : statusRaw
  2019. list.push({
  2020. id: approvalIdSpecial(String(s.id || '')),
  2021. bizType: isDeathReg ? '死亡登记' : typeLabel,
  2022. batchNo: s.batchNo ? String(s.batchNo) : '—',
  2023. town: townLabel,
  2024. village: villageLabel || '—',
  2025. title: isDeathReg
  2026. ? `${personName || ''}死亡登记(待改人员状态)`
  2027. : `${personName || townLabel}${typeLabel}申请`,
  2028. applicant: `${townLabel || ''}社区事务受理中心`,
  2029. node: '分管领导线上审批',
  2030. status,
  2031. createdAt: fmtTimeSafe(s.createTime as number),
  2032. peopleCount: personnelId ? 1 : undefined,
  2033. totalAmount: s.amount != null ? Number(s.amount) : undefined,
  2034. personName: personName || undefined,
  2035. personnelId: personnelId || undefined,
  2036. reason: reason || undefined,
  2037. fileUrls: s.fileUrls ? String(s.fileUrls) : undefined,
  2038. deathDate: deathDate || undefined,
  2039. amount: s.amount != null ? Number(s.amount) : undefined,
  2040. opinion:
  2041. statusRaw === '待分管领导审批' || statusRaw === '待审批' || !statusRaw
  2042. ? undefined
  2043. : s.opinion
  2044. ? String(s.opinion)
  2045. : undefined,
  2046. heirName: s.heirName ? String(s.heirName) : undefined,
  2047. idCard: idCard || undefined,
  2048. })
  2049. }
  2050. return { list, total: Number(specials?.total || list.length) }
  2051. }
  2052. export async function reviewPersonQualify(payload: {
  2053. personId: number | string
  2054. retireQualifyType: '有土地退养' | '无土地有资格' | '无资格'
  2055. }) {
  2056. return reviewRetireQualify({
  2057. personnelId: personIdStr(payload.personId),
  2058. pass: payload.retireQualifyType !== '无资格',
  2059. retireQualifyType: payload.retireQualifyType,
  2060. })
  2061. }
  2062. export async function decideApprovals(payload: {
  2063. ids: string[]
  2064. pass: boolean
  2065. opinion?: string
  2066. fileUrls?: string
  2067. retireQualifyType?: '有土地退养' | '无土地有资格' | '无资格'
  2068. }) {
  2069. const qualifyIds: string[] = []
  2070. const others: string[] = []
  2071. for (const id of payload.ids || []) {
  2072. const parsed = parseApprovalId(id)
  2073. if (!parsed) continue
  2074. if (parsed.kind === 'qualify') qualifyIds.push(parsed.bizId)
  2075. else others.push(id)
  2076. }
  2077. if (qualifyIds.length) {
  2078. const r = await reviewRetireQualifyBatch({
  2079. personnelIds: qualifyIds,
  2080. pass: payload.pass,
  2081. retireQualifyType: payload.retireQualifyType,
  2082. opinion: payload.opinion,
  2083. fileUrls: payload.fileUrls,
  2084. })
  2085. if (!r.ok) {
  2086. throw new Error(r.message || '批量资格审批失败')
  2087. }
  2088. }
  2089. for (const id of others) {
  2090. await decideApproval({
  2091. id,
  2092. pass: payload.pass,
  2093. opinion: payload.opinion,
  2094. fileUrls: payload.fileUrls,
  2095. retireQualifyType: payload.retireQualifyType,
  2096. })
  2097. }
  2098. return { ok: true }
  2099. }
  2100. export async function decideApproval(payload: {
  2101. id: string
  2102. pass: boolean
  2103. opinion?: string
  2104. fileUrls?: string
  2105. retireQualifyType?: '有土地退养' | '无土地有资格' | '无资格'
  2106. }) {
  2107. const parsed = parseApprovalId(payload.id)
  2108. if (!parsed) return { ok: false }
  2109. if (parsed.kind === 'qualify') {
  2110. await reviewRetireQualify({
  2111. personnelId: parsed.bizId,
  2112. pass: payload.pass,
  2113. retireQualifyType: payload.retireQualifyType,
  2114. opinion: payload.opinion,
  2115. fileUrls: payload.fileUrls,
  2116. })
  2117. return { ok: true }
  2118. }
  2119. const body = {
  2120. bizId: parsed.bizId,
  2121. action: payload.pass ? 'PASS' : 'REJECT',
  2122. opinion: payload.opinion || (payload.pass ? '同意' : '退回修改'),
  2123. operator: operatorOf(),
  2124. fileUrls: payload.fileUrls || undefined,
  2125. }
  2126. if (parsed.kind === 'batch') {
  2127. await post('/batches/leader-approve', body)
  2128. } else {
  2129. await post('/special-biz/leader-approve', body)
  2130. }
  2131. clearGetCache()
  2132. return { ok: true }
  2133. }
  2134. /** —— 状态维护 / 标准 —— */
  2135. export async function fetchStatusBatches(): Promise<VillageBatch[]> {
  2136. const all = await fetchVillageBatches()
  2137. return all.filter((b) =>
  2138. ['分管领导审批', '待线下发放', '回盘信息确认'].includes(b.stage),
  2139. )
  2140. }
  2141. const standardIdMap = new Map<number, string>()
  2142. export async function fetchStandards(town?: string): Promise<SubsidyStandard[]> {
  2143. await ensureTowns()
  2144. const townId = town ? townIdOf(town) : undefined
  2145. // 后端返回 PageResult,不是裸数组
  2146. const page = await get<PageResult<Record<string, unknown>>>('/amount-standards', {
  2147. townId: townId || undefined,
  2148. pageNum: 1,
  2149. pageSize: 200,
  2150. })
  2151. const rows = Array.isArray(page) ? page : page?.list || []
  2152. const list = rows.map((r) => {
  2153. const m = mapStandard(r)
  2154. standardIdMap.set(m.id, String(r.id))
  2155. return m
  2156. })
  2157. return list.sort((a, b) => (b.year !== a.year ? b.year - a.year : b.date.localeCompare(a.date)))
  2158. }
  2159. /** 本镇当前有效丧葬补贴标准(元);优先 /amount-standards/current,失败则回退标准列表 */
  2160. export async function fetchCurrentFuneralAmount(town: string): Promise<number | null> {
  2161. if (!town) return null
  2162. await ensureTowns()
  2163. const townId = townIdOf(town)
  2164. if (!townId) return null
  2165. try {
  2166. const raw = await get<Record<string, unknown>>(
  2167. '/amount-standards/current',
  2168. { townId, standardType: 'FUNERAL' },
  2169. { silent: true },
  2170. )
  2171. const amt = Number(raw?.funeralAmount ?? raw?.amount ?? 0)
  2172. if (Number.isFinite(amt) && amt > 0) return amt
  2173. } catch {
  2174. /* 回退列表 */
  2175. }
  2176. try {
  2177. const list = await fetchStandards(town)
  2178. const cur = list.find((s) => s.current && s.funeral > 0) || list.find((s) => s.funeral > 0)
  2179. return cur && cur.funeral > 0 ? cur.funeral : null
  2180. } catch {
  2181. return null
  2182. }
  2183. }
  2184. function dateToEpochMs(date: string): number {
  2185. const s = String(date || '').trim()
  2186. if (/^\d{13}$/.test(s)) return Number(s)
  2187. if (/^\d{10}$/.test(s)) return Number(s) * 1000
  2188. const m = s.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/)
  2189. if (m) {
  2190. const t = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])).getTime()
  2191. if (!Number.isNaN(t)) return t
  2192. }
  2193. const n = Number(s)
  2194. if (Number.isFinite(n) && n > 1_000_000_000) return n < 20_000_000_000 ? n * 1000 : n
  2195. throw new Error('生效日期格式无效')
  2196. }
  2197. export async function createStandard(payload: {
  2198. town: string
  2199. year: number
  2200. monthly: number
  2201. funeral: number
  2202. date: string
  2203. reason: string
  2204. }) {
  2205. const townId = townIdOf(payload.town)
  2206. if (!townId) return { ok: false as const, message: '未识别街镇' }
  2207. let effectiveTime: number
  2208. try {
  2209. effectiveTime = dateToEpochMs(payload.date)
  2210. } catch {
  2211. return { ok: false as const, message: '请选择有效的生效日期' }
  2212. }
  2213. // 新增标准只落库,不走 yearly-adjust(那会生成调标补发批)
  2214. const saved = await post<Record<string, unknown>>('/amount-standards', {
  2215. townId,
  2216. standardType: 'YEARLY_ADJUST',
  2217. amount: payload.monthly,
  2218. funeralAmount: payload.funeral,
  2219. effectiveTime,
  2220. adjustReason: payload.reason,
  2221. operator: operatorOf(),
  2222. allowDirect: getAppConfig().currentRole === 'admin',
  2223. })
  2224. const pendingTip = String(saved?.remarks || '')
  2225. if (pendingTip.includes('待分管领导审批')) {
  2226. return { ok: true as const, pendingTip: '标准变更已提交,待分管领导审批后生效' }
  2227. }
  2228. return { ok: true as const }
  2229. }
  2230. export async function updateStandard(payload: {
  2231. id: number
  2232. monthly: number
  2233. funeral: number
  2234. date: string
  2235. reason: string
  2236. rawId?: string
  2237. town?: string
  2238. }) {
  2239. const realId = payload.rawId || standardIdMap.get(payload.id)
  2240. let effectiveTime: number
  2241. try {
  2242. effectiveTime = dateToEpochMs(payload.date)
  2243. } catch {
  2244. return { ok: false as const, message: '请选择有效的生效日期' }
  2245. }
  2246. const townId = payload.town ? townIdOf(payload.town) : ''
  2247. if (!townId) return { ok: false as const, message: '未识别街镇' }
  2248. const saved = await post<Record<string, unknown>>('/amount-standards', {
  2249. id: realId,
  2250. townId,
  2251. standardType: 'YEARLY_ADJUST',
  2252. amount: payload.monthly,
  2253. funeralAmount: payload.funeral,
  2254. effectiveTime,
  2255. adjustReason: payload.reason,
  2256. operator: operatorOf(),
  2257. allowDirect: getAppConfig().currentRole === 'admin',
  2258. })
  2259. const pendingTip = String(saved?.remarks || '')
  2260. if (pendingTip.includes('待分管领导审批')) {
  2261. return { ok: true as const, pendingTip: '标准变更已提交,待分管领导审批后生效' }
  2262. }
  2263. return { ok: true as const }
  2264. }
  2265. /** 管理员删除补助标准(软删) */
  2266. export async function deleteStandard(payload: { id: number; rawId?: string }) {
  2267. const realId = payload.rawId || standardIdMap.get(payload.id)
  2268. if (!realId) return { ok: false as const, message: '未找到标准ID' }
  2269. const res = await del<{ ok?: boolean; message?: string; promotedId?: string }>(
  2270. `/amount-standards/${encodeURIComponent(realId)}`,
  2271. )
  2272. return {
  2273. ok: true as const,
  2274. message: res?.message,
  2275. promotedId: res?.promotedId,
  2276. }
  2277. }
  2278. export async function updateDeathStatus(deathId: string) {
  2279. // deathId 可能是人员 ID 或告警 ID;死亡闭环走特殊业务审批,此处仅兼容旧入口
  2280. try {
  2281. await put('/personnel/status', {
  2282. personnelId: deathId,
  2283. bizStatus: 'STOPPED',
  2284. payThisMonth: false,
  2285. remarks: '已完成死亡状态更新,停止发放',
  2286. allowDirect: true,
  2287. operator: operatorOf(),
  2288. })
  2289. } catch {
  2290. /* ignore */
  2291. }
  2292. return { ok: true }
  2293. }
  2294. export async function applyPersonStatusAction(payload: {
  2295. personId: number | string
  2296. action: string
  2297. amount?: number
  2298. reason?: string
  2299. fileUrls?: string
  2300. }) {
  2301. const personnelId = personIdStr(payload.personId)
  2302. const map: Record<string, string> = {
  2303. 停止: 'STOP',
  2304. '暂停/停止': 'STOP',
  2305. 暂停: 'PAUSE',
  2306. 暂停发放: 'PAUSE',
  2307. 停止发放: 'STOP',
  2308. 恢复: 'RESUME',
  2309. 恢复发放: 'RESUME',
  2310. 丧葬: 'FUNERAL',
  2311. 死亡状态更新: 'FUNERAL',
  2312. 补发: 'SUPPLEMENT',
  2313. 调标补发: 'STANDARD_ADJUST',
  2314. }
  2315. const bizType = map[payload.action]
  2316. // 暂停/停止/恢复一律走特殊业务 → 分管领导审批,禁止直改状态
  2317. if (bizType === 'STOP' || bizType === 'PAUSE' || bizType === 'RESUME'
  2318. || bizType === 'FUNERAL' || bizType === 'SUPPLEMENT') {
  2319. await post('/special-biz', {
  2320. personnelId,
  2321. bizType,
  2322. reason: payload.reason || payload.action,
  2323. amount: payload.amount,
  2324. fileUrls: payload.fileUrls,
  2325. operator: operatorOf(),
  2326. })
  2327. }
  2328. return { ok: true }
  2329. }
  2330. /** —— 特殊业务 —— */
  2331. export async function fetchSpecialsPage(params?: {
  2332. town?: string
  2333. bizType?: string
  2334. approveStatus?: string
  2335. pageNum?: number
  2336. pageSize?: number
  2337. }): Promise<{ list: SpecialBiz[]; total: number; pageNum: number; pageSize: number }> {
  2338. await ensureTowns()
  2339. const { townId } = await resolveScopeIds(params?.town, undefined)
  2340. const pageNum = params?.pageNum || 1
  2341. const pageSize = params?.pageSize || 10
  2342. const res = await get<PageResult<Record<string, unknown>>>('/special-biz/page', {
  2343. pageNum,
  2344. pageSize,
  2345. townId: townId || undefined,
  2346. bizType: params?.bizType || undefined,
  2347. approveStatus: params?.approveStatus || undefined,
  2348. })
  2349. return {
  2350. list: (res?.list || []).map(mapSpecial),
  2351. total: Number(res?.total || 0),
  2352. pageNum,
  2353. pageSize,
  2354. }
  2355. }
  2356. export async function fetchSpecials(params?: { town?: string }): Promise<SpecialBiz[]> {
  2357. const page = await fetchSpecialsPage({ ...params, pageNum: 1, pageSize: 10 })
  2358. return page.list
  2359. }
  2360. /** 审批详情:按人员 ID 拉底数(特殊业务单关联 1 人) */
  2361. export async function fetchSpecialBizPerson(personnelId: string): Promise<Person | null> {
  2362. if (!personnelId) return null
  2363. await ensureTowns()
  2364. try {
  2365. const detail = await get<Record<string, unknown>>(
  2366. `/personnel/${encodeURIComponent(personnelId)}`,
  2367. )
  2368. if (!detail) return null
  2369. return mapPerson(detail)
  2370. } catch {
  2371. return null
  2372. }
  2373. }
  2374. export async function fetchDeaths(params?: {
  2375. town?: string
  2376. pageNum?: number
  2377. pageSize?: number
  2378. }): Promise<DeathRecord[]> {
  2379. await ensureTowns()
  2380. const { townId } = await resolveScopeIds(params?.town, undefined)
  2381. const townName = params?.town || (townId ? regionName(townId) : '')
  2382. if (townId) await ensureVillages(townId)
  2383. // 1) 死亡登记/丧葬特殊业务单(新流程)
  2384. const specialRes = await get<PageResult<Record<string, unknown>>>('/special-biz/page', {
  2385. bizType: 'FUNERAL',
  2386. pageNum: params?.pageNum || 1,
  2387. pageSize: params?.pageSize || 10,
  2388. townId: townId || undefined,
  2389. })
  2390. // 2) 人员底数已是 FUNERAL 的历史记录(旧直改流程)——只取当前页,禁止循环拉全量
  2391. const funeralPeopleRes = await post<PageResult<Record<string, unknown>>>('/personnel/page', {
  2392. pageNum: params?.pageNum || 1,
  2393. pageSize: params?.pageSize || 10,
  2394. townId: townId || undefined,
  2395. bizStatus: 'FUNERAL',
  2396. })
  2397. const funeralPeople = funeralPeopleRes?.list || []
  2398. const byKey = new Map<string, DeathRecord>()
  2399. const preferPut = (key: string, rec: DeathRecord, preferSpecial: boolean) => {
  2400. const prev = byKey.get(key)
  2401. if (!prev) {
  2402. byKey.set(key, rec)
  2403. return
  2404. }
  2405. const prevSpecial = !String(prev.id).startsWith('P:')
  2406. if (preferSpecial && !prevSpecial) {
  2407. byKey.set(key, rec)
  2408. return
  2409. }
  2410. if (prevSpecial === preferSpecial) {
  2411. if (String(rec.createdAt || '') > String(prev.createdAt || '')) byKey.set(key, rec)
  2412. }
  2413. }
  2414. for (const s of specialRes?.list || []) {
  2415. const tid = String(s.townId || '')
  2416. // 有镇 ID 且不匹配则跳过;镇 ID 缺失时先保留,后面用名称再滤
  2417. if (townId && tid && tid !== townId) continue
  2418. const reason = String(s.reason || '')
  2419. const deathDate = String(s.deathDate || '')
  2420. const isDeathReg =
  2421. Boolean(deathDate) || reason.includes('死亡登记') || reason.includes('死亡日期')
  2422. const isFuneralOnly =
  2423. !isDeathReg && (reason.includes('申请丧葬补贴') || reason.includes('丧葬补贴'))
  2424. // 纯丧葬补贴申请归「丧葬补贴」页;死亡登记/无法区分的历史单留在本页
  2425. if (isFuneralOnly) continue
  2426. let town = regionName(tid) || townName
  2427. let village = regionName(String(s.villageId || ''))
  2428. if (townName && town && town !== townName && !tid) continue
  2429. if (townName && !town) town = townName
  2430. const personnelId = s.personnelId ? String(s.personnelId) : ''
  2431. const approveStatus = String(s.approveStatus || '待分管领导审批')
  2432. // 列表状态:驳回/待审原样;通过后展示「死亡丧葬」(与人员落地态一致)
  2433. let listStatus = approveStatus
  2434. if (approveStatus === '已通过' || approveStatus.includes('通过')) {
  2435. listStatus = '死亡丧葬'
  2436. } else if (approveStatus.includes('驳回')) {
  2437. listStatus = '已驳回'
  2438. }
  2439. const rec: DeathRecord = {
  2440. id: String(s.id || ''),
  2441. personnelId: personnelId || undefined,
  2442. personName: String(s.name || ''),
  2443. idCard: String(s.idNumber || ''),
  2444. town,
  2445. village,
  2446. deathDate,
  2447. heirName: s.heirName ? String(s.heirName) : '',
  2448. heirIdCard: s.heirIdNumber ? String(s.heirIdNumber) : '',
  2449. heirBankCard: s.heirBankCard ? String(s.heirBankCard) : '',
  2450. heirContact: s.heirPhone ? String(s.heirPhone) : '',
  2451. source: '社区事务受理中心手工录入',
  2452. status: listStatus,
  2453. createdAt: fmtTimeSafe(s.createTime as number),
  2454. opinion: s.opinion ? String(s.opinion) : undefined,
  2455. reason: reason || undefined,
  2456. fileUrls: s.fileUrls ? String(s.fileUrls) : undefined,
  2457. }
  2458. preferPut(personnelId || rec.id, rec, true)
  2459. }
  2460. for (const p of funeralPeople) {
  2461. const pid = String(p.id || '')
  2462. if (!pid) continue
  2463. // 已有特殊业务死亡登记单时,不再用底数 FUNERAL 覆盖(避免驳回后仍显示「死亡丧葬」)
  2464. if (byKey.has(pid)) continue
  2465. if (townId && p.townId && String(p.townId) !== townId) continue
  2466. const town = regionName(String(p.townId || '')) || townName
  2467. if (townName && town && town !== townName) continue
  2468. const deathDate = String(p.deathDate || '')
  2469. const remarks = String(p.remarks || '')
  2470. const rec: DeathRecord = {
  2471. id: `P:${pid}`,
  2472. personnelId: pid,
  2473. personName: String(p.name || ''),
  2474. idCard: String(p.idNumber || ''),
  2475. town: town || townName,
  2476. village: regionName(String(p.villageId || '')),
  2477. deathDate,
  2478. heirName: p.heirName ? String(p.heirName) : '',
  2479. heirIdCard: p.heirIdNumber ? String(p.heirIdNumber) : '',
  2480. heirBankCard: p.heirBankCard ? String(p.heirBankCard) : '',
  2481. heirContact: p.heirPhone ? String(p.heirPhone) : '',
  2482. source: '历史死亡状态',
  2483. status: '死亡丧葬(历史)',
  2484. createdAt: deathDate
  2485. ? `${deathDate} 00:00:00`
  2486. : fmtTimeSafe((p.updateTime as number) || (p.createTime as number)),
  2487. reason: remarks || '人员状态已为死亡丧葬(历史直改,无审批留痕)',
  2488. fileUrls: undefined,
  2489. }
  2490. preferPut(pid, rec, false)
  2491. }
  2492. const list = [...byKey.values()].sort((a, b) =>
  2493. String(b.createdAt || '').localeCompare(String(a.createdAt || '')),
  2494. )
  2495. ;(list as DeathRecord[] & { __total?: number }).__total = Number(
  2496. specialRes?.total || funeralPeopleRes?.total || list.length,
  2497. )
  2498. return list
  2499. }
  2500. export async function fetchDeathsPage(params?: {
  2501. town?: string
  2502. pageNum?: number
  2503. pageSize?: number
  2504. }): Promise<{ list: DeathRecord[]; total: number; pageNum: number; pageSize: number }> {
  2505. const pageNum = params?.pageNum || 1
  2506. const pageSize = params?.pageSize || 10
  2507. const list = await fetchDeaths({ ...params, pageNum, pageSize })
  2508. const total = Number((list as DeathRecord[] & { __total?: number }).__total || list.length)
  2509. return { list, total, pageNum, pageSize }
  2510. }
  2511. function fmtTimeSafe(ts?: number | string | null): string {
  2512. if (ts == null || ts === '') return ''
  2513. const n = Number(ts)
  2514. if (!Number.isFinite(n) || n <= 0) return String(ts)
  2515. const d = new Date(n)
  2516. if (Number.isNaN(d.getTime())) return ''
  2517. const p = (x: number) => String(x).padStart(2, '0')
  2518. return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
  2519. }
  2520. /** 上传死亡证明等附件,返回相对 url */
  2521. export async function uploadBizFile(file: File): Promise<{ url: string; name: string }> {
  2522. const form = new FormData()
  2523. form.append('file', file)
  2524. const data = await post<{ url?: string; name?: string }>(
  2525. '/files/upload',
  2526. form as unknown as Record<string, unknown>,
  2527. )
  2528. const url = String(data?.url || '')
  2529. if (!url) throw new Error('附件上传失败')
  2530. return { url, name: String(data?.name || file.name) }
  2531. }
  2532. export async function createDeathRecord(payload: {
  2533. personId?: string | number
  2534. personName: string
  2535. idCard: string
  2536. town: string
  2537. village: string
  2538. deathDate: string
  2539. heirName?: string
  2540. heirRelation?: string
  2541. heirIdCard?: string
  2542. heirBankCard?: string
  2543. heirContact?: string
  2544. fileUrls?: string
  2545. }) {
  2546. let personnelId = payload.personId ? personIdStr(payload.personId) : ''
  2547. const { townId } = await resolveScopeIds(payload.town, payload.village)
  2548. if (!personnelId) {
  2549. const found = await post<PageResult<Record<string, unknown>>>('/personnel/page', {
  2550. pageNum: 1,
  2551. pageSize: 5,
  2552. idNumber: payload.idCard,
  2553. townId: townId || undefined,
  2554. })
  2555. personnelId = found?.list?.[0]?.id ? String(found.list[0].id) : ''
  2556. }
  2557. if (!personnelId) {
  2558. return { ok: false as const, message: '未找到本镇在册人员,请从列表中选择' }
  2559. }
  2560. const detail = await get<Record<string, unknown>>(`/personnel/${encodeURIComponent(personnelId)}`)
  2561. const st = String(detail?.bizStatus || '')
  2562. if (st === 'FUNERAL' || st === 'STOPPED') {
  2563. return { ok: false as const, message: '该人员已是死亡/停止状态,不可重复录入' }
  2564. }
  2565. // 提交特殊业务 FUNERAL → 待分管领导审批;通过后才改人员状态
  2566. try {
  2567. const reasonParts = [`死亡登记;死亡日期 ${payload.deathDate}`]
  2568. if (payload.heirRelation) reasonParts.push(`与逝者关系:${payload.heirRelation}`)
  2569. // 先落继承人信息(含关系),审批通过后再改死亡状态
  2570. // 必须带回逝者银行卡:后端编辑时 null 会触发 putKeep 保留,但缺字段时部分路径仍可能清空
  2571. if (detail?.id) {
  2572. const ut = Number(detail.updateTime)
  2573. await post('/personnel', {
  2574. id: personnelId,
  2575. name: detail.name || payload.personName,
  2576. idNumber: detail.idNumber || payload.idCard,
  2577. townId: detail.townId || townId,
  2578. villageId: detail.villageId,
  2579. bankCardNumber: detail.bankCardNumber || undefined,
  2580. bankName: detail.bankName || undefined,
  2581. bankType: detail.bankType || undefined,
  2582. bankBranchCode: detail.bankBranchCode || undefined,
  2583. heirName: payload.heirName || undefined,
  2584. heirRelation: payload.heirRelation || undefined,
  2585. heirIdNumber: payload.heirIdCard || undefined,
  2586. heirBankCard: payload.heirBankCard || undefined,
  2587. heirPhone: payload.heirContact || undefined,
  2588. source: '死亡登记',
  2589. expectedUpdateTime: Number.isFinite(ut) && ut > 0 ? ut : undefined,
  2590. }).catch(() => undefined)
  2591. }
  2592. const created = await post<Record<string, unknown>>('/special-biz', {
  2593. personnelId,
  2594. bizType: 'FUNERAL',
  2595. deathDate: payload.deathDate,
  2596. reason: reasonParts.join(';'),
  2597. heirName: payload.heirName,
  2598. heirIdNumber: payload.heirIdCard,
  2599. heirBankCard: payload.heirBankCard,
  2600. heirPhone: payload.heirContact,
  2601. fileUrls: payload.fileUrls,
  2602. townId: detail?.townId || townId,
  2603. operator: operatorOf(),
  2604. })
  2605. return { ok: true as const, id: String(created?.id || '') }
  2606. } catch (e: unknown) {
  2607. const msg = e instanceof Error ? e.message : '提交失败'
  2608. return { ok: false as const, message: msg }
  2609. }
  2610. }
  2611. export async function createSpecialBiz(payload: {
  2612. type: string
  2613. personId?: string | number
  2614. personName: string
  2615. town: string
  2616. village: string
  2617. reason: string
  2618. amount: number
  2619. heirName?: string
  2620. fileUrls?: string
  2621. /** 多发退款方案:DEDUCT_NEXT | RETURN_FISCAL */
  2622. refundMethod?: 'DEDUCT_NEXT' | 'RETURN_FISCAL'
  2623. }) {
  2624. const { townId, villageId } = await resolveScopeIds(payload.town, payload.village)
  2625. const bizType = specialTypeToCode(payload.type)
  2626. let personnelId = payload.personId ? personIdStr(payload.personId) : ''
  2627. if (bizType !== 'STANDARD_ADJUST' && !personnelId) {
  2628. const people = await post<PageResult<Record<string, unknown>>>('/personnel/page', {
  2629. pageNum: 1,
  2630. pageSize: 20,
  2631. name: payload.personName,
  2632. townId: townId || undefined,
  2633. villageId: villageId || undefined,
  2634. })
  2635. personnelId = people?.list?.[0]?.id ? String(people.list[0].id) : ''
  2636. if (!personnelId) throw new Error(`未找到本镇人员:${payload.personName}`)
  2637. }
  2638. if (bizType !== 'STANDARD_ADJUST' && !personnelId) {
  2639. throw new Error('请选择本镇在册人员')
  2640. }
  2641. // 校验所选人确属本镇
  2642. if (personnelId && townId) {
  2643. const detail = await get<Record<string, unknown>>(
  2644. `/personnel/${encodeURIComponent(personnelId)}`,
  2645. )
  2646. if (detail?.townId && String(detail.townId) !== townId) {
  2647. throw new Error('只能为本镇在册人员发起特殊业务')
  2648. }
  2649. }
  2650. const created = await post<Record<string, unknown>>('/special-biz', {
  2651. personnelId: personnelId || undefined,
  2652. bizType,
  2653. reason: payload.reason,
  2654. amount: payload.amount,
  2655. townId: townId || undefined,
  2656. heirName: payload.heirName,
  2657. fileUrls: payload.fileUrls,
  2658. refundMethod: bizType === 'REFUND' ? payload.refundMethod || 'DEDUCT_NEXT' : undefined,
  2659. operator: operatorOf(),
  2660. operatorRoleKey: getAppConfig().currentRole,
  2661. })
  2662. return { ok: true, id: String(created?.id || '') }
  2663. }
  2664. export async function exportSpecialSummary(_type?: string) {
  2665. return { ok: false as const, fileName: '', message: '特殊业务汇总导出未对接后端接口' }
  2666. }
  2667. export async function triggerAdjustReissue(town?: string) {
  2668. const cfg = getAppConfig()
  2669. const role = cfg.roles.find((r) => r.key === cfg.currentRole)
  2670. const townName = town || role?.town
  2671. if (!townName) {
  2672. return { ok: false, message: '请指定街镇', batchNo: currentPayMonth(), count: 0, villageCount: 0 }
  2673. }
  2674. const { townId } = await resolveScopeIds(townName)
  2675. if (!townId) return { ok: false, message: '未识别街镇', batchNo: currentPayMonth(), count: 0, villageCount: 0 }
  2676. const created = await post<Record<string, unknown>>('/special-biz', {
  2677. bizType: 'STANDARD_ADJUST',
  2678. townId,
  2679. reason: '年度调标差额补发',
  2680. operator: operatorOf(),
  2681. })
  2682. const related = String(created?.relatedBatchIds || '')
  2683. const villageCount = related ? related.split(/[,,]/).filter(Boolean).length : 0
  2684. return {
  2685. ok: true,
  2686. batchNo: String(created?.batchNo || currentPayMonth()),
  2687. villageCount,
  2688. count: Number(created?.amount ? 0 : 0) || villageCount,
  2689. message: undefined as string | undefined,
  2690. }
  2691. }
  2692. /** 从备注解析旧/新月标(批次或明细备注:旧月标1280|新月标2113) */
  2693. function parseOldNewMonthly(text?: string): { oldMonthly: number; newMonthly: number } | null {
  2694. const s = String(text || '')
  2695. const oldM = s.match(/旧月标\s*([0-9]+(?:\.[0-9]+)?)/)
  2696. const newM = s.match(/新月标\s*([0-9]+(?:\.[0-9]+)?)/)
  2697. if (!oldM || !newM) return null
  2698. const oldMonthly = Number(oldM[1])
  2699. const newMonthly = Number(newM[1])
  2700. if (!(newMonthly > 0) || !(oldMonthly >= 0)) return null
  2701. return { oldMonthly, newMonthly }
  2702. }
  2703. /** 从明细备注解析应发月数与单月差额 */
  2704. function parseAdjustMonthsDiff(text?: string): { months: number; diff: number } | null {
  2705. const s = String(text || '')
  2706. const m = s.match(/应发\s*(\d+)\s*月\s*[×xX*]\s*差额\s*([0-9]+(?:\.[0-9]+)?)/)
  2707. if (!m) return null
  2708. return { months: Number(m[1]), diff: Number(m[2]) }
  2709. }
  2710. /** 从金额标准表取当前新标 + 上一档旧标 */
  2711. async function loadTownAdjustStandards(townId: string): Promise<{
  2712. oldMonthly: number
  2713. newMonthly: number
  2714. } | null> {
  2715. try {
  2716. const page = await get<PageResult<Record<string, unknown>>>('/amount-standards', {
  2717. townId,
  2718. pageNum: 1,
  2719. pageSize: 100,
  2720. })
  2721. const list = (page?.list || []).filter((r) => {
  2722. const t = String(r.standardType || '')
  2723. return (t === 'YEARLY_ADJUST' || t === 'MONTHLY') && Number(r.amount || 0) > 0
  2724. })
  2725. if (!list.length) return null
  2726. list.sort((a, b) => Number(b.effectiveTime || 0) - Number(a.effectiveTime || 0))
  2727. const cur =
  2728. list.find((r) => r.current === true || r.current === 'true') || list[0]
  2729. const curAmt = Number(cur.amount || 0)
  2730. const curEff = Number(cur.effectiveTime || 0)
  2731. const prev = list.find((r) => {
  2732. if (String(r.id) === String(cur.id)) return false
  2733. const amt = Number(r.amount || 0)
  2734. if (!(amt > 0)) return false
  2735. const eff = Number(r.effectiveTime || 0)
  2736. return !curEff || !eff || eff < curEff
  2737. })
  2738. if (!prev) return null
  2739. const oldMonthly = Number(prev.amount || 0)
  2740. if (!(curAmt > oldMonthly)) return null
  2741. return { oldMonthly, newMonthly: curAmt }
  2742. } catch {
  2743. return null
  2744. }
  2745. }
  2746. export async function fetchAdjustReissueRoster(params?: {
  2747. town?: string
  2748. batchNo?: string
  2749. village?: string
  2750. }): Promise<AdjustReissuePerson[]> {
  2751. await ensureTowns()
  2752. const { townId, villageId } = await resolveScopeIds(params?.town, params?.village)
  2753. const townStandards = townId ? await loadTownAdjustStandards(townId) : null
  2754. // 调标村批一般为 YYYY99 或备注含调标;按发放明细补发额组装(单页,禁止循环拉全量)
  2755. const batchRes = await post<PageResult<Record<string, unknown>>>('/batches/page', {
  2756. pageNum: 1,
  2757. pageSize: 20,
  2758. batchLevel: 'DISTRICT',
  2759. batchNo: params?.batchNo,
  2760. lite: true,
  2761. })
  2762. const batches = batchRes?.list || []
  2763. const adjustBatches = batches.filter((b) => {
  2764. const no = String(b.batchNo || b.payMonth || '')
  2765. const remarks = String(b.remarks || '')
  2766. const id = String(b.id || '')
  2767. return (
  2768. no.endsWith('99') ||
  2769. id.includes('99') ||
  2770. remarks.includes('YEARLY_ADJUST') ||
  2771. remarks.includes('调标') ||
  2772. Number(b.supplementAmount || 0) > 0
  2773. )
  2774. })
  2775. const roster: AdjustReissuePerson[] = []
  2776. for (const b of adjustBatches) {
  2777. if (params?.village && regionName(String(b.villageId || '')) !== params.village) continue
  2778. const batchBasis =
  2779. parseOldNewMonthly(String(b.remarks || '')) || townStandards
  2780. const detailsRes = await post<PageResult<Record<string, unknown>>>('/payments/details/page', {
  2781. pageNum: 1,
  2782. pageSize: 10,
  2783. batchId: b.id,
  2784. townId: townId || undefined,
  2785. villageId: villageId || undefined,
  2786. })
  2787. const details = detailsRes?.list || []
  2788. for (const d of details) {
  2789. const amt = Number(d.supplementAmount ?? d.totalAmount ?? 0)
  2790. if (!amt) continue
  2791. const detailBasis =
  2792. Number(d.oldMonthlyAmount) > 0 && Number(d.newMonthlyAmount) > 0
  2793. ? { oldMonthly: Number(d.oldMonthlyAmount), newMonthly: Number(d.newMonthlyAmount) }
  2794. : parseOldNewMonthly(String(d.remarks || ''))
  2795. const monthsDiff =
  2796. Number(d.adjustMonths) > 0
  2797. ? { months: Number(d.adjustMonths), diff: Number(d.supplementAmount || 0) / Number(d.adjustMonths) }
  2798. : parseAdjustMonthsDiff(String(d.remarks || ''))
  2799. let newStandard = 0
  2800. let oldStandard = 0
  2801. if (detailBasis) {
  2802. oldStandard = detailBasis.oldMonthly
  2803. newStandard = detailBasis.newMonthly
  2804. } else if (batchBasis) {
  2805. oldStandard = batchBasis.oldMonthly
  2806. newStandard = batchBasis.newMonthly
  2807. } else if (monthsDiff && monthsDiff.diff > 0) {
  2808. // 仅有差额时:明细月标若为新标则反推旧标;否则用差额反推
  2809. const monthly = Number(d.monthlyAmount || 0)
  2810. if (monthly > monthsDiff.diff) {
  2811. newStandard = monthly
  2812. oldStandard = Math.max(0, monthly - monthsDiff.diff)
  2813. } else {
  2814. newStandard = monthsDiff.diff
  2815. oldStandard = 0
  2816. }
  2817. } else {
  2818. const monthly = Number(d.monthlyAmount || 0)
  2819. if (monthly > 0) {
  2820. // 历史脏数据曾把月标写成 0;若已有新标则用差额反推
  2821. newStandard = monthly
  2822. const months = monthsDiff?.months || 0
  2823. const unitDiff = months > 0 ? amt / months : 0
  2824. oldStandard = Math.max(0, monthly - (unitDiff || 0))
  2825. }
  2826. }
  2827. // 仍缺新标时:用「补发额 / 月数 + 旧标」补全
  2828. if (newStandard <= 0 && oldStandard > 0 && monthsDiff?.months) {
  2829. newStandard = oldStandard + amt / monthsDiff.months
  2830. }
  2831. if (oldStandard <= 0 && newStandard > 0 && monthsDiff?.months) {
  2832. oldStandard = Math.max(0, newStandard - amt / monthsDiff.months)
  2833. }
  2834. if (newStandard <= 0 && monthsDiff?.diff) {
  2835. newStandard = (oldStandard || 0) + monthsDiff.diff
  2836. }
  2837. if (oldStandard <= 0 && newStandard > 0 && monthsDiff?.diff) {
  2838. oldStandard = Math.max(0, newStandard - monthsDiff.diff)
  2839. }
  2840. roster.push({
  2841. id: String(d.id || ''),
  2842. batchNo: String(b.batchNo || b.payMonth || params?.batchNo || ''),
  2843. personId: String(d.personnelId || '') as unknown as number,
  2844. name: String(d.name || ''),
  2845. idCard: String(d.idNumber || ''),
  2846. town: regionName(String(d.townId || b.townId || '')),
  2847. village: regionName(String(d.villageId || b.villageId || '')),
  2848. oldStandard: Number(oldStandard.toFixed(2)),
  2849. newStandard: Number(newStandard.toFixed(2)),
  2850. amount: amt,
  2851. status: '待提交',
  2852. remark: '年度调标差额补发',
  2853. })
  2854. }
  2855. }
  2856. return roster
  2857. }
  2858. /** —— 月补贴 —— */
  2859. export async function fetchVillageMonthlyBatchesPage(params?: {
  2860. town?: string
  2861. pageNum?: number
  2862. pageSize?: number
  2863. /** 默认 true;列表首屏可传 false 先出表,再异步补全镇人数/金额 */
  2864. withTownTotals?: boolean
  2865. }): Promise<{ list: VillageMonthlyBatch[]; total: number; pageNum: number; pageSize: number }> {
  2866. await ensureTowns()
  2867. const cfg = getAppConfig()
  2868. const role = cfg.roles.find((r) => r.key === cfg.currentRole)
  2869. const { townId } = await resolveScopeIds(params?.town || role?.town)
  2870. if (townId) await ensureVillages(townId)
  2871. const pageNum = params?.pageNum || 1
  2872. const pageSize = params?.pageSize || 10
  2873. const withTownTotals = params?.withTownTotals !== false
  2874. const res = await post<PageResult<Record<string, unknown>>>('/batches/page', {
  2875. pageNum,
  2876. pageSize,
  2877. townId: townId || undefined,
  2878. batchLevel: 'TOWN',
  2879. statusIn:
  2880. 'PENDING_STATUS_MAINTAIN,PENDING_LEADER_APPROVE,READY_EXPORT,EXPORTED,RETURNED,ARCHIVED',
  2881. lite: true,
  2882. withTownTotals,
  2883. })
  2884. const list = (res?.list || []).map(mapVillageMonthly)
  2885. return { list, total: Number(res?.total || list.length), pageNum, pageSize }
  2886. }
  2887. export async function fetchVillageMonthlyBatches(town?: string): Promise<VillageMonthlyBatch[]> {
  2888. const page = await fetchVillageMonthlyBatchesPage({ town, pageNum: 1, pageSize: 10 })
  2889. return page.list
  2890. }
  2891. export async function fetchMonthlyBatches(): Promise<MonthlyBatch[]> {
  2892. const villages = await fetchVillageMonthlyBatches()
  2893. const map = new Map<string, MonthlyBatch>()
  2894. for (const v of villages) {
  2895. const key = `${v.batchNo}|${v.town}`
  2896. const cur = map.get(key)
  2897. if (!cur) {
  2898. map.set(key, {
  2899. id: `MB${v.batchNo}${townIdOf(v.town)}`,
  2900. batchNo: v.batchNo,
  2901. month: v.month,
  2902. town: v.town,
  2903. peopleCount: v.peopleCount,
  2904. normalAmount: v.normalAmount,
  2905. extraAmount: v.extraAmount,
  2906. deductAmount: v.deductAmount,
  2907. totalAmount: v.totalAmount,
  2908. status: v.status,
  2909. })
  2910. } else {
  2911. cur.peopleCount += v.peopleCount
  2912. cur.normalAmount += v.normalAmount
  2913. cur.extraAmount += v.extraAmount
  2914. cur.deductAmount += v.deductAmount
  2915. cur.totalAmount += v.totalAmount
  2916. }
  2917. }
  2918. return [...map.values()]
  2919. }
  2920. export async function calculateMonthly() {
  2921. await generateDistrictMonthBatch(currentPayMonth(), true).catch(() => undefined)
  2922. return { ok: true }
  2923. }
  2924. export async function confirmMonthly(id: string) {
  2925. await post('/payments/export-disk', { batchId: id, bypassDeadline: true }).catch(() => undefined)
  2926. return { ok: true }
  2927. }
  2928. /** 导出居保匹配结果 xlsx(按 batchId 或 townId) */
  2929. export async function exportInsuranceMatch(params: {
  2930. batchId?: string
  2931. town?: string
  2932. townId?: string
  2933. }) {
  2934. await ensureTowns()
  2935. let townId = params.townId || ''
  2936. if (!townId && params.town) {
  2937. const scope = await resolveScopeIds(params.town, undefined)
  2938. townId = scope.townId || ''
  2939. }
  2940. if (!params.batchId && !townId) {
  2941. return { ok: false as const, fileName: '', message: '请指定批次或街镇' }
  2942. }
  2943. try {
  2944. const res = await service.get('/insurance-match/export-xlsx', {
  2945. params: {
  2946. batchId: params.batchId || undefined,
  2947. townId: townId || undefined,
  2948. },
  2949. responseType: 'blob',
  2950. timeout: 120_000,
  2951. })
  2952. const blob = res.data as Blob
  2953. if (blob && blob.type && blob.type.includes('application/json')) {
  2954. const text = await blob.text()
  2955. let msg = '导出失败'
  2956. try {
  2957. const j = JSON.parse(text) as { message?: string; msg?: string }
  2958. msg = j.message || j.msg || msg
  2959. } catch {
  2960. /* ignore */
  2961. }
  2962. return { ok: false as const, fileName: '', message: msg }
  2963. }
  2964. let fileName = '居保匹配结果.xlsx'
  2965. const cd = String(res.headers?.['content-disposition'] || '')
  2966. const m = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(cd)
  2967. if (m) {
  2968. fileName = decodeURIComponent((m[1] || m[2] || '').trim())
  2969. }
  2970. const url = URL.createObjectURL(blob)
  2971. const a = document.createElement('a')
  2972. a.href = url
  2973. a.download = fileName
  2974. document.body.appendChild(a)
  2975. a.click()
  2976. a.remove()
  2977. URL.revokeObjectURL(url)
  2978. return { ok: true as const, fileName, message: '' }
  2979. } catch (e) {
  2980. return {
  2981. ok: false as const,
  2982. fileName: '',
  2983. message: e instanceof Error ? e.message : '导出失败',
  2984. }
  2985. }
  2986. }
  2987. export async function exportMonthlyFile(id?: string) {
  2988. if (!id) {
  2989. return { ok: false as const, message: '缺少批次ID', fileName: '' }
  2990. }
  2991. try {
  2992. const res = await service.get(`/payments/batches/${encodeURIComponent(id)}/export-xlsx`, {
  2993. responseType: 'blob',
  2994. timeout: 120_000,
  2995. })
  2996. const blob = res.data as Blob
  2997. if (blob && blob.type && blob.type.includes('application/json')) {
  2998. const text = await blob.text()
  2999. let msg = '导出失败'
  3000. try {
  3001. const j = JSON.parse(text) as { message?: string; msg?: string }
  3002. msg = j.message || j.msg || msg
  3003. } catch {
  3004. /* ignore */
  3005. }
  3006. return { ok: false as const, fileName: '', message: msg }
  3007. }
  3008. let fileName = `月补贴发放清单_${currentPayMonth()}.xlsx`
  3009. const cd = String(res.headers?.['content-disposition'] || '')
  3010. const m = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(cd)
  3011. if (m) {
  3012. fileName = decodeURIComponent((m[1] || m[2] || '').trim())
  3013. }
  3014. const url = URL.createObjectURL(blob)
  3015. const a = document.createElement('a')
  3016. a.href = url
  3017. a.download = fileName
  3018. document.body.appendChild(a)
  3019. a.click()
  3020. a.remove()
  3021. URL.revokeObjectURL(url)
  3022. return { ok: true as const, fileName, message: '' }
  3023. } catch (e) {
  3024. return {
  3025. ok: false as const,
  3026. fileName: '',
  3027. message: e instanceof Error ? e.message : '导出失败',
  3028. }
  3029. }
  3030. }
  3031. export async function issueVillageMonthlyDisk(batchId: string) {
  3032. const res = await post<Record<string, unknown>>('/payments/export-disk', {
  3033. batchId,
  3034. bypassDeadline: true,
  3035. })
  3036. return {
  3037. ok: true as const,
  3038. fileName: String(res?.fileName || res?.exportDiskNo || `${currentPayMonth()}-月补贴发放文件`),
  3039. fileUrl: res?.fileUrl ? String(res.fileUrl) : undefined,
  3040. message: String(res?.offlineTip || ''),
  3041. }
  3042. }
  3043. /** 下载后端存储的相对路径文件(出盘/附件等;DMS /static/ 经 resolve 代理) */
  3044. export async function downloadStoredFile(fileUrl: string, fileName?: string) {
  3045. if (!fileUrl) throw new Error('缺少文件地址')
  3046. const { resolveBizFileHref } = await import('@/utils/bizFiles')
  3047. let href = fileUrl.trim()
  3048. if (!href.startsWith('http')) {
  3049. href = resolveBizFileHref(href)
  3050. if (href.includes('/files/resolve') && !href.includes('download=')) {
  3051. href += (href.includes('?') ? '&' : '?') + 'download=true'
  3052. }
  3053. }
  3054. // axios baseURL=/api:去掉前缀避免 /api/api/...
  3055. const reqPath = href.startsWith('http')
  3056. ? href
  3057. : href.startsWith('/api/')
  3058. ? href.slice('/api'.length)
  3059. : href
  3060. const res = await service.get(reqPath, {
  3061. baseURL: href.startsWith('http') ? '' : undefined,
  3062. responseType: 'blob',
  3063. timeout: 120_000,
  3064. })
  3065. const blob = res.data as Blob
  3066. const url = URL.createObjectURL(blob)
  3067. const a = document.createElement('a')
  3068. a.href = url
  3069. a.download = fileName || 'download'
  3070. document.body.appendChild(a)
  3071. a.click()
  3072. a.remove()
  3073. URL.revokeObjectURL(url)
  3074. }
  3075. /** 快照测算轮次 */
  3076. export async function snapshotCalcRound(batchId: string) {
  3077. const res = await post<Record<string, unknown>>(
  3078. `/payments/batches/${encodeURIComponent(batchId)}/calc-rounds`,
  3079. undefined,
  3080. { params: { operator: operatorOf() } },
  3081. )
  3082. return { ok: true as const, ...(res || {}) }
  3083. }
  3084. /** 查询测算轮次列表 */
  3085. export async function listCalcRounds(batchId: string) {
  3086. const list = await get<Record<string, unknown>[]>(
  3087. `/payments/batches/${encodeURIComponent(batchId)}/calc-rounds`,
  3088. )
  3089. return Array.isArray(list) ? list : []
  3090. }
  3091. /** 锁定/解锁测算 */
  3092. export async function lockCalc(batchId: string, locked: boolean) {
  3093. await post(
  3094. `/payments/batches/${encodeURIComponent(batchId)}/calc-lock`,
  3095. undefined,
  3096. { params: { locked, operator: operatorOf() } },
  3097. )
  3098. return { ok: true as const, locked }
  3099. }
  3100. /** 推进退款进度 */
  3101. export async function advanceRefundProgress(id: string, stage: string) {
  3102. await post(`/special-biz/${encodeURIComponent(id)}/refund-progress`, { stage })
  3103. return { ok: true as const }
  3104. }
  3105. /** 退款进度统计 */
  3106. export async function fetchRefundStats() {
  3107. const res = await get<Record<string, unknown>>('/special-biz/refund-stats')
  3108. return res || { total: 0, byStage: {} as Record<string, number> }
  3109. }
  3110. export type ReportTopicType = 'pay-abnormal' | 'personnel-change' | 'special-biz'
  3111. /** 专题报表 topics(分页) */
  3112. export async function fetchReportTopicPage(
  3113. type: ReportTopicType,
  3114. params?: {
  3115. townId?: string
  3116. payMonth?: string
  3117. bizType?: string
  3118. pageNum?: number
  3119. pageSize?: number
  3120. },
  3121. ): Promise<{ list: Record<string, unknown>[]; total: number }> {
  3122. const path =
  3123. type === 'pay-abnormal'
  3124. ? '/reports/topics/pay-abnormal'
  3125. : type === 'personnel-change'
  3126. ? '/reports/topics/personnel-change'
  3127. : '/reports/topics/special-biz'
  3128. const res = await get<PageResult<Record<string, unknown>>>(path, {
  3129. townId: params?.townId || undefined,
  3130. payMonth: params?.payMonth || undefined,
  3131. bizType: params?.bizType || undefined,
  3132. pageNum: params?.pageNum || 1,
  3133. pageSize: params?.pageSize || 20,
  3134. })
  3135. return {
  3136. list: Array.isArray(res?.list) ? res.list : [],
  3137. total: Number(res?.total || 0),
  3138. }
  3139. }
  3140. /** @deprecated use fetchReportTopicPage */
  3141. export async function fetchReportTopic(
  3142. type: ReportTopicType,
  3143. params?: { townId?: string; payMonth?: string; bizType?: string },
  3144. ) {
  3145. const page = await fetchReportTopicPage(type, { ...params, pageNum: 1, pageSize: 20 })
  3146. return page.list
  3147. }
  3148. /** 报表页首屏:summary + region-stats,避免拉完整 workbench 扫全库人员 */
  3149. export async function fetchReportPageBootstrap(params?: {
  3150. payMonth?: string
  3151. town?: string
  3152. village?: string
  3153. }): Promise<{
  3154. personnelCount: number
  3155. townQualify: WorkbenchApi['townQualify']
  3156. villageCharts: WorkbenchApi['villageCharts']
  3157. } | null> {
  3158. await ensureTowns()
  3159. const cfg = getAppConfig()
  3160. const role = cfg.roles.find((r) => r.key === cfg.currentRole)
  3161. const { townId, villageId } = await resolveScopeIds(
  3162. params?.town || role?.town,
  3163. params?.village || role?.village,
  3164. )
  3165. const payMonth = params?.payMonth || currentPayMonth()
  3166. const [summary, region] = await Promise.all([
  3167. get<WorkbenchApi['summary']>('/dashboard/summary', {
  3168. payMonth,
  3169. townId: townId || undefined,
  3170. }).catch(() => null),
  3171. get<RegionDashRow>('/dashboard/region-stats', {
  3172. payMonth,
  3173. townId: townId || undefined,
  3174. villageId: villageId || undefined,
  3175. months: 1,
  3176. }).catch(() => null),
  3177. ])
  3178. if (!summary && !region) return null
  3179. const townQualify = (summary?.townStats || []).map((s) => ({
  3180. townId: String(s.townId || ''),
  3181. peopleCount: Number(s.shouldPayCount || 0),
  3182. villageCount: 0,
  3183. withLandCount: 0,
  3184. withoutLandCount: 0,
  3185. qualifiedCount: 0,
  3186. amount: s.shouldPayAmount,
  3187. }))
  3188. let villageCharts: WorkbenchApi['villageCharts']
  3189. if (townId && !villageId) {
  3190. const charts = await buildTownVillageChartsFromApi(townId, payMonth).catch(() => null)
  3191. if (charts) {
  3192. villageCharts = charts.people.map((p, idx) => ({
  3193. villageId: p.name,
  3194. peopleCount: p.value,
  3195. amount: charts.amount[idx]?.value || 0,
  3196. }))
  3197. }
  3198. }
  3199. return {
  3200. personnelCount: Number(region?.personnelCount || summary?.totalPeople || 0),
  3201. townQualify,
  3202. villageCharts,
  3203. }
  3204. }
  3205. /** 月度汇总导出 csv */
  3206. export async function exportReportSummary(payMonth?: string) {
  3207. const res = await service.get('/reports/export/summary.xlsx', {
  3208. params: { payMonth: payMonth || undefined },
  3209. responseType: 'blob',
  3210. timeout: 120_000,
  3211. })
  3212. const blob = res.data as Blob
  3213. if (blob.size <= 80) {
  3214. throw new Error('所选月份暂无批次/发放明细,无法导出月度汇总')
  3215. }
  3216. let fileName = `monthly-summary-${payMonth || 'all'}.csv`
  3217. const cd = String(res.headers?.['content-disposition'] || '')
  3218. const m = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(cd)
  3219. if (m) fileName = decodeURIComponent((m[1] || m[2] || '').trim())
  3220. const url = URL.createObjectURL(blob)
  3221. const a = document.createElement('a')
  3222. a.href = url
  3223. a.download = fileName
  3224. document.body.appendChild(a)
  3225. a.click()
  3226. a.remove()
  3227. URL.revokeObjectURL(url)
  3228. return { ok: true as const, fileName }
  3229. }
  3230. /** 同比(人数/金额 vs 上月) */
  3231. export async function fetchYoy(payMonth?: string) {
  3232. return get<Record<string, unknown>>('/reports/yoy', {
  3233. payMonth: payMonth || currentPayMonth(),
  3234. })
  3235. }
  3236. function parseSysUserRemarks(remarks?: string) {
  3237. const out: Record<string, string> = {}
  3238. const s = String(remarks || '')
  3239. if (!s.startsWith('SYS_USER|')) return out
  3240. const parts = s.split('|')
  3241. for (let i = 1; i + 1 < parts.length; i += 2) {
  3242. out[parts[i]] = parts[i + 1]
  3243. }
  3244. return out
  3245. }
  3246. function mapAdminUser(raw: Record<string, unknown>, idx: number): AccountItem {
  3247. const parsed = parseSysUserRemarks(String(raw.remarks || ''))
  3248. const username =
  3249. parsed.username ||
  3250. String(raw.username || raw.content || '')
  3251. .replace(/^SYS_USER\|/, '')
  3252. .trim()
  3253. const status = String(raw.status || raw.result || '启用')
  3254. const opTime = raw.operateTime
  3255. let lastLogin = ''
  3256. if (typeof opTime === 'number' && opTime > 0) {
  3257. lastLogin = fmtTimeSafe(opTime)
  3258. }
  3259. return {
  3260. id: String(raw.id || idx + 1),
  3261. username,
  3262. name: parsed.displayName || username,
  3263. roleKey: parsed.role || 'OPERATOR',
  3264. org: parsed.org || '—',
  3265. status: status.includes('禁') ? '禁用' : '启用',
  3266. lastLogin,
  3267. }
  3268. }
  3269. /** 系统用户列表 */
  3270. export async function fetchAccounts(): Promise<AccountItem[]> {
  3271. const list = await get<Record<string, unknown>[]>('/admin/users')
  3272. if (!Array.isArray(list)) return []
  3273. return list.map((r, idx) => mapAdminUser(r, idx))
  3274. }
  3275. export async function saveAccount(payload: {
  3276. username: string
  3277. password?: string
  3278. displayName?: string
  3279. role?: string
  3280. }) {
  3281. const res = await post<Record<string, unknown>>('/admin/users', {
  3282. username: payload.username,
  3283. password: payload.password || '123456',
  3284. displayName: payload.displayName || payload.username,
  3285. role: payload.role || 'OPERATOR',
  3286. })
  3287. return { ok: true as const, id: String(res?.id || '') }
  3288. }
  3289. export async function setAccountStatus(id: string, enabled: boolean) {
  3290. await post(`/admin/users/${encodeURIComponent(id)}/status`, undefined, {
  3291. params: { enabled },
  3292. })
  3293. return { ok: true as const }
  3294. }
  3295. export async function resetAccountPassword(id: string, password?: string) {
  3296. await post(`/admin/users/${encodeURIComponent(id)}/reset-password`, {
  3297. password: password || '123456',
  3298. })
  3299. return { ok: true as const }
  3300. }
  3301. /** 角色切换/登录审计写入 */
  3302. export async function recordLoginAudit(payload: {
  3303. username?: string
  3304. roleKey?: string
  3305. roleLabel?: string
  3306. ip?: string
  3307. result?: string
  3308. }) {
  3309. await post('/admin/login-logs', {
  3310. username: payload.username || operatorOf(),
  3311. roleKey: payload.roleKey || getAppConfig().currentRole,
  3312. roleLabel: payload.roleLabel || operatorOf(),
  3313. ip: payload.ip || '',
  3314. result: payload.result || '成功',
  3315. }).catch(() => undefined)
  3316. return { ok: true as const }
  3317. }
  3318. export async function exportProcessTimeline(params: { batchId?: string; personnelId?: string }) {
  3319. const res = await service.get('/process-logs/export', {
  3320. params,
  3321. responseType: 'blob',
  3322. timeout: 120_000,
  3323. })
  3324. const blob = res.data as Blob
  3325. const url = URL.createObjectURL(blob)
  3326. const a = document.createElement('a')
  3327. a.href = url
  3328. a.download = 'process-timeline.csv'
  3329. document.body.appendChild(a)
  3330. a.click()
  3331. a.remove()
  3332. URL.revokeObjectURL(url)
  3333. return { ok: true as const }
  3334. }
  3335. export async function fetchBatchProgress(batchId: string) {
  3336. const list = await get<Array<Record<string, unknown>>>(
  3337. `/process-logs/batches/${encodeURIComponent(batchId)}/progress`,
  3338. )
  3339. return Array.isArray(list) ? list : []
  3340. }
  3341. const ADMIN_OP_LOG_EXCLUDE = 'SYS_USER,SYS_CONFIG,LOGIN_LOG'
  3342. function mapAdminOpLog(raw: Record<string, unknown>, idx: number): OpLog {
  3343. const mapped = mapProcessLog(raw)
  3344. const role = roleLabelOf(mapped.operatorRole) || mapped.operatorRole || mapped.operatorOrg || ''
  3345. return {
  3346. id: mapped.id || String(idx + 1),
  3347. rawId: mapped.id,
  3348. time: mapped.operateTime || fmtTimeSafe(raw.operateTime as number),
  3349. user: mapped.operator || '',
  3350. role,
  3351. module: processActionLabelOf(mapped.bizType) || mapped.bizType || '流程',
  3352. action: mapped.actionLabel || processActionLabelOf(mapped.action),
  3353. result: mapped.result || '成功',
  3354. bizType: mapped.bizType,
  3355. opinion: mapped.opinion,
  3356. batchNo: mapped.batchNo,
  3357. personnelId: mapped.personnelId,
  3358. remarks: mapped.remarks,
  3359. fileUrls: mapped.fileUrls,
  3360. operatorOrg: mapped.operatorOrg,
  3361. stage: mapped.stage,
  3362. }
  3363. }
  3364. function parseLoginRemark(remarks?: string) {
  3365. if (!remarks) return ''
  3366. const parts = remarks.split('|')
  3367. const ipIdx = parts.indexOf('IP')
  3368. const ip = ipIdx >= 0 ? String(parts[ipIdx + 1] || '').trim() : ''
  3369. const roleLabel = String(parts[2] || '').trim()
  3370. return [roleLabel, ip ? `IP ${ip}` : ''].filter(Boolean).join(' · ')
  3371. }
  3372. export async function fetchLoginLogsPage(params?: { pageNum?: number; pageSize?: number }) {
  3373. const pageNum = params?.pageNum || 1
  3374. const pageSize = params?.pageSize || 10
  3375. const res = await post<PageResult<Record<string, unknown>>>('/process-logs/page', {
  3376. pageNum,
  3377. pageSize,
  3378. bizType: 'LOGIN_LOG',
  3379. })
  3380. const list = (res?.list || []).map((r, idx) => {
  3381. const item = mapAdminOpLog(r, idx)
  3382. const extra = parseLoginRemark(item.remarks) || item.opinion || item.operatorOrg || ''
  3383. return {
  3384. ...item,
  3385. module: '登录',
  3386. role: extra || item.role,
  3387. }
  3388. })
  3389. return { list, total: Number(res?.total || 0), pageNum, pageSize }
  3390. }
  3391. export async function fetchLoginLogs(limit = 100) {
  3392. const page = await fetchLoginLogsPage({ pageNum: 1, pageSize: limit })
  3393. return page.list
  3394. }
  3395. export async function exportOpLogs() {
  3396. const res = await service.get('/admin/op-logs/export', {
  3397. responseType: 'blob',
  3398. timeout: 120_000,
  3399. })
  3400. const blob = res.data as Blob
  3401. const url = URL.createObjectURL(blob)
  3402. const a = document.createElement('a')
  3403. a.href = url
  3404. a.download = 'op-logs.csv'
  3405. document.body.appendChild(a)
  3406. a.click()
  3407. a.remove()
  3408. URL.revokeObjectURL(url)
  3409. return { ok: true as const }
  3410. }
  3411. export async function cleanupOpLogs(days = 90) {
  3412. const res = await post<Record<string, unknown>>('/admin/op-logs/cleanup', undefined, {
  3413. params: { days },
  3414. })
  3415. return { ok: true as const, removed: Number(res?.removed || 0), days }
  3416. }
  3417. export async function fetchValidateRules() {
  3418. return get<Record<string, unknown>>('/admin/validate-rules')
  3419. }
  3420. export async function saveValidateRules(rules: Record<string, unknown>) {
  3421. await post('/admin/validate-rules', rules)
  3422. return { ok: true as const }
  3423. }
  3424. /** 待审批的标准变更提示(如有) */
  3425. export async function fetchStandardPendingTip(town?: string): Promise<string> {
  3426. const res = await get<PageResult<Record<string, unknown>>>('/special-biz/page', {
  3427. pageNum: 1,
  3428. pageSize: 20,
  3429. bizType: 'STANDARD_CHANGE',
  3430. approveStatus: '待分管领导审批',
  3431. })
  3432. const rows = res?.list || []
  3433. if (!rows.length) return ''
  3434. if (town) {
  3435. await ensureTowns()
  3436. const townId = townIdOf(town)
  3437. const hit = rows.find((r) => !townId || String(r.townId || '') === townId)
  3438. if (!hit) return ''
  3439. return `存在待领导审批的标准变更(${town}),审批通过后新标准才生效`
  3440. }
  3441. return `存在 ${rows.length} 条待领导审批的标准变更,审批通过后新标准才生效`
  3442. }
  3443. export async function confirmVillageMonthlyReturn(payload: {
  3444. batchId: string
  3445. failCount?: number
  3446. }) {
  3447. await post('/return-disk/confirm', {
  3448. batchId: payload.batchId,
  3449. manualAllSuccess: !payload.failCount,
  3450. operator: operatorOf(),
  3451. })
  3452. return { ok: true }
  3453. }
  3454. /** —— 回盘 —— */
  3455. export async function fetchDisksPage(params?: {
  3456. town?: string
  3457. pageNum?: number
  3458. pageSize?: number
  3459. }): Promise<{ list: DiskRecord[]; total: number; pageNum: number; pageSize: number }> {
  3460. await ensureTowns()
  3461. const { townId } = await resolveScopeIds(params?.town)
  3462. if (townId) await ensureVillages(townId)
  3463. const pageNum = params?.pageNum || 1
  3464. const pageSize = params?.pageSize || 10
  3465. const res = await post<PageResult<Record<string, unknown>>>('/batches/page', {
  3466. pageNum,
  3467. pageSize,
  3468. townId: townId || undefined,
  3469. batchLevel: 'TOWN',
  3470. statusIn: 'EXPORTED,RETURNED,ARCHIVED',
  3471. lite: true,
  3472. withTownTotals: true,
  3473. })
  3474. const filtered = res?.list || []
  3475. if (!townId) {
  3476. const townIds = [...new Set(filtered.map((b) => String(b.townId || '')).filter(Boolean))]
  3477. await Promise.all(townIds.map((id) => ensureVillages(id)))
  3478. }
  3479. const list = filtered.map(mapDisk)
  3480. return { list, total: Number(res?.total || list.length), pageNum, pageSize }
  3481. }
  3482. export async function fetchDisks(town?: string): Promise<DiskRecord[]> {
  3483. const page = await fetchDisksPage({ town, pageNum: 1, pageSize: 10 })
  3484. return page.list
  3485. }
  3486. export async function exportDisk() {
  3487. return { ok: true }
  3488. }
  3489. export async function confirmReturnDisk(payload?: { id?: string; failCount?: number }) {
  3490. if (!payload?.id) return { ok: false }
  3491. await post('/return-disk/confirm', {
  3492. batchId: payload.id,
  3493. manualAllSuccess: !payload.failCount,
  3494. operator: operatorOf(),
  3495. })
  3496. return { ok: true }
  3497. }
  3498. export async function uploadReturnDiskFile(payload: { id: string; file: File }) {
  3499. const form = new FormData()
  3500. form.append('file', payload.file)
  3501. const uploaded = await post<Record<string, unknown>>(
  3502. `/return-disk/upload?batchId=${encodeURIComponent(payload.id)}`,
  3503. form as unknown as Record<string, unknown>,
  3504. )
  3505. // 上传解析后确认回盘(对齐旧前端:导入+确认)
  3506. const confirmed = await post<Record<string, unknown>>('/return-disk/confirm', {
  3507. batchId: payload.id,
  3508. fileName: payload.file.name,
  3509. operator: operatorOf(),
  3510. }).catch(() => uploaded)
  3511. return {
  3512. ok: true as const,
  3513. failCount: Number(confirmed?.failCount ?? uploaded?.failCount ?? 0),
  3514. alertCount: Number(confirmed?.alertCount ?? 0),
  3515. message: String(confirmed?.message || uploaded?.message || '回盘文件已处理'),
  3516. }
  3517. }
  3518. export async function fetchDiskFailPeoplePage(
  3519. diskId: string,
  3520. params?: { pageNum?: number; pageSize?: number },
  3521. ): Promise<{ list: DiskFailPerson[]; total: number; pageNum: number; pageSize: number }> {
  3522. const pageNum = params?.pageNum || 1
  3523. const pageSize = params?.pageSize || 200
  3524. const res = await post<PageResult<Record<string, unknown>>>('/return-disk/details/page', {
  3525. pageNum,
  3526. pageSize,
  3527. batchId: diskId,
  3528. })
  3529. const failed = (res?.list || []).filter((d) => d.success === false)
  3530. const list = failed.map((d) => mapReturnFail(d, diskId))
  3531. return { list, total: failed.length, pageNum, pageSize }
  3532. }
  3533. export async function fetchDiskFailPeople(diskId: string): Promise<DiskFailPerson[]> {
  3534. const page = await fetchDiskFailPeoplePage(diskId, { pageNum: 1, pageSize: 10 })
  3535. return page.list
  3536. }
  3537. export async function confirmDiskFailPaid(failPersonId: string) {
  3538. // 纳入待补发/已确认:走待补发台账 reissue 或仅标记
  3539. try {
  3540. await post('/return-arrears/reissue', {
  3541. arrearsIds: [failPersonId],
  3542. operator: operatorOf(),
  3543. })
  3544. } catch {
  3545. /* 明细 ID 可能不是台账 ID,忽略 */
  3546. }
  3547. return { ok: true as const }
  3548. }
  3549. export async function archiveToNextMonth(id?: string) {
  3550. if (id) await post(`/dashboard/archive/${encodeURIComponent(id)}`)
  3551. return { ok: true }
  3552. }
  3553. /** —— 告警 —— */
  3554. export async function fetchAlertsPage(params?: {
  3555. town?: string
  3556. village?: string
  3557. pageNum?: number
  3558. pageSize?: number
  3559. }): Promise<{ list: AlertItem[]; total: number; pageNum: number; pageSize: number }> {
  3560. await ensureTowns()
  3561. const { townId, villageId } = await resolveScopeIds(params?.town, params?.village)
  3562. const pageNum = params?.pageNum || 1
  3563. const pageSize = params?.pageSize || 10
  3564. const res = await post<PageResult<Record<string, unknown>>>('/alerts/page', {
  3565. pageNum,
  3566. pageSize,
  3567. townId: townId || undefined,
  3568. villageId: villageId || undefined,
  3569. })
  3570. return {
  3571. list: (res?.list || []).map(mapAlert),
  3572. total: Number(res?.total || 0),
  3573. pageNum,
  3574. pageSize,
  3575. }
  3576. }
  3577. export async function fetchAlerts(): Promise<AlertItem[]> {
  3578. const page = await fetchAlertsPage({ pageNum: 1, pageSize: 100 })
  3579. return page.list
  3580. }
  3581. export async function handleAlert(payload: HandleAlertPayload | string, result?: string) {
  3582. const data: HandleAlertPayload =
  3583. typeof payload === 'string'
  3584. ? {
  3585. id: payload,
  3586. personName: '',
  3587. idCard: '',
  3588. town: '',
  3589. village: '',
  3590. bankCard: '',
  3591. bankName: '',
  3592. socialInsuranceCode: '',
  3593. heirName: '',
  3594. heirIdCard: '',
  3595. heirBankCard: '',
  3596. heirContact: '',
  3597. result,
  3598. }
  3599. : payload
  3600. // 人员字段由页面 savePerson 一次落库;此处只关告警,避免二次无锁覆盖
  3601. await post('/alerts/handle', {
  3602. alertId: data.id,
  3603. handleStatus: 'CLOSED',
  3604. handleResult: data.result || '已核实并处理',
  3605. remarks: data.remarks || undefined,
  3606. fileUrls: data.fileUrls || undefined,
  3607. handler: operatorOf(),
  3608. })
  3609. return { ok: true }
  3610. }
  3611. /** —— 报表 / 系统 —— */
  3612. export async function fetchReportBatches(): Promise<VillageBatch[]> {
  3613. return fetchVillageBatches()
  3614. }
  3615. /** —— 系统管理员运维 —— */
  3616. export async function listRegionsAdmin(params?: {
  3617. level?: string
  3618. parentId?: string
  3619. }): Promise<RegionRow[]> {
  3620. const data = await get<RegionRow[] | { list?: RegionRow[] }>(
  3621. '/regions',
  3622. {
  3623. level: params?.level,
  3624. parentId: params?.parentId,
  3625. },
  3626. { skipCache: true },
  3627. )
  3628. if (Array.isArray(data)) return data
  3629. if (data && typeof data === 'object' && Array.isArray(data.list)) return data.list
  3630. return []
  3631. }
  3632. export async function saveRegionAdmin(payload: {
  3633. id?: string
  3634. code: string
  3635. name: string
  3636. level: string
  3637. parentId?: string
  3638. entrustPlanCode?: string
  3639. defaultBank?: string
  3640. enabled?: boolean
  3641. remarks?: string
  3642. districtSharePercent?: number
  3643. townSharePercent?: number
  3644. }) {
  3645. await post('/regions', payload)
  3646. invalidateRegionCache()
  3647. return { ok: true as const }
  3648. }
  3649. export async function deleteRegionAdmin(id: string) {
  3650. await del(`/regions/${encodeURIComponent(id)}`)
  3651. invalidateRegionCache()
  3652. return { ok: true as const }
  3653. }
  3654. export async function pageAdminBatches(params?: {
  3655. payMonth?: string
  3656. townId?: string
  3657. villageId?: string
  3658. batchLevel?: string
  3659. status?: string
  3660. pageNum?: number
  3661. pageSize?: number
  3662. /** 默认 false;列表首屏先出表,再异步 true 补镇人数/金额 */
  3663. withTownTotals?: boolean
  3664. }): Promise<{
  3665. list: Array<Record<string, unknown>>
  3666. total: number
  3667. pageNum: number
  3668. pageSize: number
  3669. }> {
  3670. await ensureTowns()
  3671. const pageNum = params?.pageNum || 1
  3672. const pageSize = params?.pageSize || 10
  3673. const withTownTotals = params?.withTownTotals === true
  3674. const res = await post<PageResult<Record<string, unknown>>>('/batches/page', {
  3675. pageNum,
  3676. pageSize,
  3677. payMonth: params?.payMonth || undefined,
  3678. townId: params?.townId || undefined,
  3679. villageId: params?.villageId || undefined,
  3680. status: params?.status || undefined,
  3681. batchLevel: params?.batchLevel || undefined,
  3682. lite: true,
  3683. withTownTotals,
  3684. withDistrictTotals: withTownTotals,
  3685. })
  3686. const list = (res?.list || [])
  3687. .filter((b) => String(b.batchLevel || '') !== 'VILLAGE')
  3688. .map((b) => ({
  3689. id: String(b.id || ''),
  3690. batchNo: String(b.batchNo || b.payMonth || ''),
  3691. payMonth: String(b.payMonth || ''),
  3692. batchLevel: String(b.batchLevel || ''),
  3693. townId: String(b.townId || ''),
  3694. villageId: String(b.villageId || ''),
  3695. town: regionName(String(b.townId || '')),
  3696. village:
  3697. String(b.batchLevel || '') === 'DISTRICT'
  3698. ? '全区'
  3699. : String(b.batchLevel || '') === 'TOWN'
  3700. ? '本镇'
  3701. : regionName(String(b.villageId || '')),
  3702. peopleCount: Number(b.peopleCount || 0),
  3703. totalAmount: Number(b.totalAmount || 0),
  3704. status: String(b.status || ''),
  3705. currentStage: String(b.currentStage || ''),
  3706. }))
  3707. return { list, total: Number(res?.total || list.length), pageNum, pageSize }
  3708. }
  3709. export async function purgeBatchAdmin(batchId: string) {
  3710. await post(`/batches/${encodeURIComponent(batchId)}/purge`)
  3711. return { ok: true as const }
  3712. }
  3713. export async function deletePersonnelAdmin(personnelId: string) {
  3714. await del(`/personnel/${encodeURIComponent(personnelId)}`)
  3715. return { ok: true as const }
  3716. }
  3717. export async function deletePersonnelBatchAdmin(ids: string[]) {
  3718. const res = await post<Record<string, unknown>>('/personnel/batch-delete', { ids }, { timeout: 180000 })
  3719. return res
  3720. }
  3721. export async function clearAllPersonnelAdmin() {
  3722. const res = await post<Record<string, unknown>>(
  3723. '/personnel/clear-all',
  3724. { confirm: 'CLEAR_ALL_PERSONNEL' },
  3725. { timeout: 600000 },
  3726. )
  3727. return res
  3728. }
  3729. export async function updatePersonnelStatusAdmin(payload: {
  3730. personnelId: string
  3731. bizStatus: string
  3732. payThisMonth?: boolean
  3733. remarks?: string
  3734. }) {
  3735. const remarks = payload.remarks
  3736. await put('/personnel/status', { ...payload, remarks, allowDirect: true })
  3737. return { ok: true as const }
  3738. }
  3739. export async function initPersonnelStatusAdmin(payload: {
  3740. bizStatus: string
  3741. insuranceMatchResult: string
  3742. retireQualifyType?: string
  3743. payThisMonth?: boolean
  3744. townId?: string
  3745. villageId?: string
  3746. }) {
  3747. const res = await post<Record<string, unknown>>('/personnel/init-status', {
  3748. confirm: 'INIT_STATUS_CONFIRM',
  3749. operator: operatorOf(),
  3750. bizStatus: payload.bizStatus,
  3751. insuranceMatchResult: payload.insuranceMatchResult,
  3752. retireQualifyType: payload.retireQualifyType || 'UNSET',
  3753. payThisMonth: payload.payThisMonth,
  3754. townId: payload.townId || undefined,
  3755. villageId: payload.villageId || undefined,
  3756. }, { timeout: 1_200_000 })
  3757. return {
  3758. ok: true as const,
  3759. updatedCount: Number(res?.updatedCount || 0),
  3760. skippedArchived: Number(res?.skippedArchived || 0),
  3761. skippedSame: Number(res?.skippedSame || 0),
  3762. failCount: Number(res?.failCount || 0),
  3763. message: String(res?.message || '已初始化'),
  3764. }
  3765. }
  3766. /** 管理员:恢复初始化(清空批次/明细/流程审批/告警等;人员底数保留,资格与状态打回待提交) */
  3767. export async function factoryResetBusinessData() {
  3768. const res = await post<Record<string, unknown>>('/admin/factory-reset', {
  3769. confirm: 'RESET_CONFIRM',
  3770. operator: operatorOf(),
  3771. }, { timeout: 180000 })
  3772. return {
  3773. ok: true as const,
  3774. destroyedTotal: Number(res?.destroyedTotal || 0),
  3775. detail: res || {},
  3776. message: String(res?.message || '已恢复初始化'),
  3777. }
  3778. }
  3779. export async function fetchOpLogsPage(params?: {
  3780. pageNum?: number
  3781. pageSize?: number
  3782. operator?: string
  3783. operatorRole?: string
  3784. bizType?: string
  3785. action?: string
  3786. result?: string
  3787. operateTimeFrom?: number
  3788. operateTimeTo?: number
  3789. }) {
  3790. const pageNum = params?.pageNum || 1
  3791. const pageSize = params?.pageSize || 10
  3792. const res = await post<PageResult<Record<string, unknown>>>('/process-logs/page', {
  3793. pageNum,
  3794. pageSize,
  3795. excludeBizType: ADMIN_OP_LOG_EXCLUDE,
  3796. operator: params?.operator || undefined,
  3797. operatorRole: params?.operatorRole || undefined,
  3798. bizType: params?.bizType || undefined,
  3799. action: params?.action || undefined,
  3800. result: params?.result || undefined,
  3801. operateTimeFrom: params?.operateTimeFrom || undefined,
  3802. operateTimeTo: params?.operateTimeTo || undefined,
  3803. })
  3804. return {
  3805. list: (res?.list || []).map((r, idx) => mapAdminOpLog(r, idx)),
  3806. total: Number(res?.total || 0),
  3807. pageNum,
  3808. pageSize,
  3809. }
  3810. }
  3811. export async function fetchOpLogs(): Promise<OpLog[]> {
  3812. const page = await fetchOpLogsPage({ pageNum: 1, pageSize: 10 })
  3813. return page.list
  3814. }