|
|
@@ -0,0 +1,1681 @@
|
|
|
+/**
|
|
|
+ * sjnmtybt-server 真实接口实现(对齐新前端页面流程与既有函数签名)
|
|
|
+ * 页面仍调用 @/api/biz;useMock=false 时走到本模块。
|
|
|
+ */
|
|
|
+import { del, get, post, put } from '@/utils/request'
|
|
|
+import { getAppConfig } from '@/utils/appConfig'
|
|
|
+import { buildDashboardByRole } from '@/utils/buildDashboard'
|
|
|
+import type {
|
|
|
+ AccountItem,
|
|
|
+ AlertItem,
|
|
|
+ Approval,
|
|
|
+ DashboardData,
|
|
|
+ DeathRecord,
|
|
|
+ DiskFailPerson,
|
|
|
+ DiskRecord,
|
|
|
+ HandleAlertPayload,
|
|
|
+ MonthlyBatch,
|
|
|
+ OpLog,
|
|
|
+ Person,
|
|
|
+ SpecialBiz,
|
|
|
+ SubsidyStandard,
|
|
|
+ VillageBatch,
|
|
|
+ VillageMonthlyBatch,
|
|
|
+ AdjustReissuePerson,
|
|
|
+ BatchFlowOverview,
|
|
|
+ ProcessLogItem,
|
|
|
+} from '@/types/biz'
|
|
|
+import {
|
|
|
+ ensureTowns,
|
|
|
+ ensureVillages,
|
|
|
+ regionName,
|
|
|
+ resolveScopeIds,
|
|
|
+ townIdOf,
|
|
|
+ type RegionRow,
|
|
|
+} from './regionStore'
|
|
|
+import {
|
|
|
+ approvalIdBatch,
|
|
|
+ approvalIdSpecial,
|
|
|
+ flowStageLabel,
|
|
|
+ mapAlert,
|
|
|
+ mapDisk,
|
|
|
+ mapPerson,
|
|
|
+ mapProcessLog,
|
|
|
+ mapReturnFail,
|
|
|
+ mapSpecial,
|
|
|
+ mapStandard,
|
|
|
+ mapVillageBatch,
|
|
|
+ mapVillageMonthly,
|
|
|
+ monthToPayMonth,
|
|
|
+ parseApprovalId,
|
|
|
+ personIdStr,
|
|
|
+ specialTypeToCode,
|
|
|
+ isPayMonthLike,
|
|
|
+} from './mappers'
|
|
|
+
|
|
|
+type PageResult<T> = { list?: T[]; total?: number; pageNum?: number; pageSize?: number }
|
|
|
+
|
|
|
+async function pageAll<T>(
|
|
|
+ fetcher: (pageNum: number, pageSize: number) => Promise<PageResult<T> | null | undefined>,
|
|
|
+ pageSize = 100,
|
|
|
+ maxPages = 30,
|
|
|
+): Promise<T[]> {
|
|
|
+ const all: T[] = []
|
|
|
+ for (let pageNum = 1; pageNum <= maxPages; pageNum++) {
|
|
|
+ const res = await fetcher(pageNum, pageSize)
|
|
|
+ const list = res?.list || []
|
|
|
+ all.push(...list)
|
|
|
+ if (list.length < pageSize || all.length >= Number(res?.total || 0)) break
|
|
|
+ }
|
|
|
+ return all
|
|
|
+}
|
|
|
+
|
|
|
+function currentPayMonth() {
|
|
|
+ return monthToPayMonth(getAppConfig().currentMonth)
|
|
|
+}
|
|
|
+
|
|
|
+function operatorOf() {
|
|
|
+ const cfg = getAppConfig()
|
|
|
+ const role = cfg.roles.find((r) => r.key === cfg.currentRole)
|
|
|
+ return role?.label || cfg.currentRole || '联调'
|
|
|
+}
|
|
|
+
|
|
|
+type MonthStatRow = {
|
|
|
+ payMonth?: string
|
|
|
+ peopleCount?: number
|
|
|
+ totalAmount?: number | string
|
|
|
+ actualAmount?: number | string
|
|
|
+ failCount?: number
|
|
|
+ batchCount?: number
|
|
|
+}
|
|
|
+
|
|
|
+type RegionDashRow = {
|
|
|
+ payMonth?: string
|
|
|
+ townId?: string
|
|
|
+ villageId?: string
|
|
|
+ grain?: string
|
|
|
+ personnelCount?: number
|
|
|
+ currentMonth?: MonthStatRow
|
|
|
+ series?: MonthStatRow[]
|
|
|
+}
|
|
|
+
|
|
|
+type BatchStatRow = {
|
|
|
+ townId?: string
|
|
|
+ villageId?: string
|
|
|
+ shouldPayCount?: number
|
|
|
+ shouldPayAmount?: number | string
|
|
|
+ actualPayAmount?: number | string
|
|
|
+}
|
|
|
+
|
|
|
+function formatPayMonthLabel(ym?: string) {
|
|
|
+ const s = String(ym || '')
|
|
|
+ if (s.length === 6) return `${s.slice(0, 4)}-${s.slice(4)}`
|
|
|
+ return s || '-'
|
|
|
+}
|
|
|
+
|
|
|
+function seriesToHalfYearCharts(series: MonthStatRow[] | undefined, months = 6) {
|
|
|
+ const list = Array.isArray(series) ? series.slice(-months) : []
|
|
|
+ return {
|
|
|
+ people: list.map((m) => ({
|
|
|
+ name: formatPayMonthLabel(m.payMonth),
|
|
|
+ value: Number(m.peopleCount || 0),
|
|
|
+ })),
|
|
|
+ amount: list.map((m) => ({
|
|
|
+ name: formatPayMonthLabel(m.payMonth),
|
|
|
+ value: Number(m.totalAmount || 0),
|
|
|
+ })),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 本批次各村发放人数/金额(镇级工作台) */
|
|
|
+async function buildTownVillageChartsFromApi(townId: string, payMonth: string) {
|
|
|
+ await ensureVillages(townId)
|
|
|
+ const batches = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
|
|
|
+ post<PageResult<Record<string, unknown>>>('/batches/page', {
|
|
|
+ pageNum,
|
|
|
+ pageSize,
|
|
|
+ townId,
|
|
|
+ payMonth,
|
|
|
+ batchLevel: 'VILLAGE',
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ const peopleMap = new Map<string, number>()
|
|
|
+ const amountMap = new Map<string, number>()
|
|
|
+ for (const b of batches) {
|
|
|
+ if (String(b.status || '') === 'ARCHIVED') continue
|
|
|
+ const vName = regionName(String(b.villageId || '')) || String(b.villageId || '未知村')
|
|
|
+ peopleMap.set(vName, (peopleMap.get(vName) || 0) + Number(b.peopleCount || 0))
|
|
|
+ amountMap.set(vName, (amountMap.get(vName) || 0) + Number(b.totalAmount || 0))
|
|
|
+ }
|
|
|
+ const names = Array.from(new Set([...peopleMap.keys(), ...amountMap.keys()])).sort((a, b) =>
|
|
|
+ a.localeCompare(b, 'zh-CN'),
|
|
|
+ )
|
|
|
+ return {
|
|
|
+ people: names.map((name) => ({ name, value: peopleMap.get(name) || 0 })),
|
|
|
+ amount: names.map((name) => ({ name, value: amountMap.get(name) || 0 })),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** —— 工作台 —— */
|
|
|
+export async function fetchDashboard(params: {
|
|
|
+ roleKey: string
|
|
|
+ currentMonth: string
|
|
|
+}): Promise<DashboardData> {
|
|
|
+ await ensureTowns()
|
|
|
+ const role =
|
|
|
+ getAppConfig().roles.find((r) => r.key === params.roleKey) || getAppConfig().roles[0]
|
|
|
+ const { townId, villageId } = await resolveScopeIds(role.town, role.village)
|
|
|
+ if (townId) await ensureVillages(townId)
|
|
|
+ const payMonth = monthToPayMonth(params.currentMonth)
|
|
|
+ const base = buildDashboardByRole(role, params.currentMonth)
|
|
|
+ base.batchInfo.batchNo = payMonth
|
|
|
+ base.batchInfo.scope =
|
|
|
+ role.village && role.town
|
|
|
+ ? `${role.town}/${role.village}`
|
|
|
+ : role.town
|
|
|
+ ? role.town
|
|
|
+ : role.allData
|
|
|
+ ? townId
|
|
|
+ ? regionName(townId) || role.town || '全区'
|
|
|
+ : '全区'
|
|
|
+ : base.batchInfo.scope
|
|
|
+
|
|
|
+ try {
|
|
|
+ const [summary, region] = await Promise.all([
|
|
|
+ get<Record<string, unknown>>('/dashboard/summary', {
|
|
|
+ payMonth,
|
|
|
+ townId: townId || undefined,
|
|
|
+ }).catch(() => null),
|
|
|
+ get<RegionDashRow>('/dashboard/region-stats', {
|
|
|
+ payMonth,
|
|
|
+ townId: townId || undefined,
|
|
|
+ villageId: villageId || undefined,
|
|
|
+ months: 6,
|
|
|
+ }).catch(() => null),
|
|
|
+ ])
|
|
|
+
|
|
|
+ const halfYear = seriesToHalfYearCharts(region?.series, 6)
|
|
|
+ const cur = region?.currentMonth
|
|
|
+ let regionPeople = Number(cur?.peopleCount ?? 0)
|
|
|
+ let regionAmount = Number(cur?.totalAmount ?? 0)
|
|
|
+ const pendingAlert = Number(summary?.pendingAlertCount || 0)
|
|
|
+
|
|
|
+ // 批次有人数但金额未回填时,用本月村/镇批次再算一遍(避免金额柱图全 0)
|
|
|
+ if ((regionPeople > 0 && regionAmount <= 0) || (villageId && regionPeople <= 0)) {
|
|
|
+ try {
|
|
|
+ const monthBatches = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
|
|
|
+ post<PageResult<Record<string, unknown>>>('/batches/page', {
|
|
|
+ pageNum,
|
|
|
+ pageSize,
|
|
|
+ payMonth,
|
|
|
+ townId: townId || undefined,
|
|
|
+ villageId: villageId || undefined,
|
|
|
+ batchLevel: villageId ? 'VILLAGE' : undefined,
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ let p = 0
|
|
|
+ let a = 0
|
|
|
+ for (const b of monthBatches) {
|
|
|
+ if (String(b.status || '') === 'ARCHIVED') continue
|
|
|
+ p += Number(b.peopleCount || 0)
|
|
|
+ a += Number(b.totalAmount || 0)
|
|
|
+ }
|
|
|
+ if (p > 0) regionPeople = p
|
|
|
+ if (a > 0) regionAmount = a
|
|
|
+ } catch {
|
|
|
+ /* ignore */
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (regionPeople <= 0) regionPeople = Number(summary?.totalPeople || 0)
|
|
|
+ if (regionAmount <= 0) regionAmount = Number(summary?.totalAmount || 0)
|
|
|
+
|
|
|
+ // 把本月校正写回近半年序列最后一柱
|
|
|
+ if (halfYear.people.length) {
|
|
|
+ const lastIdx = halfYear.people.length - 1
|
|
|
+ if (regionPeople > 0) halfYear.people[lastIdx].value = regionPeople
|
|
|
+ if (regionAmount > 0) halfYear.amount[lastIdx].value = regionAmount
|
|
|
+ }
|
|
|
+
|
|
|
+ // 汇总卡片:优先区划统计(含村级),保证切换镇/村后数字一致
|
|
|
+ base.stats.amount = regionAmount
|
|
|
+ base.stats.alert = pendingAlert
|
|
|
+ base.stats.normal = Number(region?.personnelCount ?? regionPeople)
|
|
|
+ base.batchInfo.peopleCount = regionPeople
|
|
|
+ base.batchInfo.amount = regionAmount
|
|
|
+ base.batchInfo.alertCount = pendingAlert
|
|
|
+
|
|
|
+ // 村合作社:近半年图表用真实序列(不再用 mock 骨架)
|
|
|
+ if (role.village || role.key === 'village') {
|
|
|
+ base.villageHalfYearPeopleChart = halfYear.people
|
|
|
+ base.villageHalfYearAmountChart = halfYear.amount
|
|
|
+ base.townAmountChart = halfYear.amount
|
|
|
+ base.batchCompareChart = halfYear.amount.map((i, idx) => ({
|
|
|
+ ...i,
|
|
|
+ count: halfYear.people[idx]?.value,
|
|
|
+ }))
|
|
|
+ }
|
|
|
+
|
|
|
+ // 镇级:本批次各村柱状图
|
|
|
+ if (townId && !villageId && (role.key === 'town' || role.key === 'social' || role.key === 'leader')) {
|
|
|
+ try {
|
|
|
+ const villageCharts = await buildTownVillageChartsFromApi(townId, payMonth)
|
|
|
+ base.townVillagePeopleChart = villageCharts.people
|
|
|
+ base.townVillageAmountChart = villageCharts.amount
|
|
|
+ base.townAmountChart = villageCharts.amount
|
|
|
+ } catch {
|
|
|
+ /* 保留骨架 */
|
|
|
+ }
|
|
|
+ // 镇级也可看近半年趋势(右侧/其他图用到时)
|
|
|
+ if (halfYear.people.length) {
|
|
|
+ base.batchCompareChart = halfYear.amount.map((i, idx) => ({
|
|
|
+ ...i,
|
|
|
+ count: halfYear.people[idx]?.value,
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 区级:近半年 + 各街镇(来自 summary.townStats)
|
|
|
+ if (role.allData && base.districtOverview) {
|
|
|
+ base.districtOverview.halfYearPeopleChart = halfYear.people
|
|
|
+ base.districtOverview.halfYearAmountChart = halfYear.amount
|
|
|
+ base.districtOverview.peopleCount = regionPeople
|
|
|
+ base.districtOverview.amount = regionAmount
|
|
|
+ const townStats = (summary?.townStats as BatchStatRow[] | undefined) || []
|
|
|
+ if (townStats.length) {
|
|
|
+ const byTown = new Map<string, { people: number; amount: number }>()
|
|
|
+ for (const s of townStats) {
|
|
|
+ const name = regionName(String(s.townId || '')) || String(s.townId || '未知')
|
|
|
+ const curT = byTown.get(name) || { people: 0, amount: 0 }
|
|
|
+ curT.people += Number(s.shouldPayCount || 0)
|
|
|
+ curT.amount += Number(s.shouldPayAmount || 0)
|
|
|
+ byTown.set(name, curT)
|
|
|
+ }
|
|
|
+ const names = Array.from(byTown.keys()).sort((a, b) => a.localeCompare(b, 'zh-CN'))
|
|
|
+ base.districtOverview.townCount = names.length
|
|
|
+ base.districtOverview.townPeopleChart = names.map((n) => ({
|
|
|
+ name: n,
|
|
|
+ value: byTown.get(n)?.people || 0,
|
|
|
+ }))
|
|
|
+ base.districtOverview.townAmountChart = names.map((n) => ({
|
|
|
+ name: n,
|
|
|
+ value: byTown.get(n)?.amount || 0,
|
|
|
+ }))
|
|
|
+ base.districtOverview.townSummary = names.map((n) => ({
|
|
|
+ town: n,
|
|
|
+ villageCount: 0,
|
|
|
+ peopleCount: byTown.get(n)?.people || 0,
|
|
|
+ amount: byTown.get(n)?.amount || 0,
|
|
|
+ withLandCount: 0,
|
|
|
+ withoutLandCount: 0,
|
|
|
+ qualifiedCount: byTown.get(n)?.people || 0,
|
|
|
+ }))
|
|
|
+ base.townAmountChart = base.districtOverview.townAmountChart
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return base
|
|
|
+ } catch {
|
|
|
+ return base
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** —— 人员 / 村批 —— */
|
|
|
+export async function fetchVillageBatches(): Promise<VillageBatch[]> {
|
|
|
+ await ensureTowns()
|
|
|
+ const cfg = getAppConfig()
|
|
|
+ const role = cfg.roles.find((r) => r.key === cfg.currentRole)
|
|
|
+ const { townId, villageId } = await resolveScopeIds(role?.town, role?.village)
|
|
|
+ if (townId) await ensureVillages(townId)
|
|
|
+ const payMonth = currentPayMonth()
|
|
|
+
|
|
|
+ // 进「人员新增录入」即确保本村本月草稿批(对齐后端流程:ensure-current-month)
|
|
|
+ if (townId && villageId) {
|
|
|
+ await post('/batches/ensure-current-month', undefined, {
|
|
|
+ params: { townId, villageId, payMonth },
|
|
|
+ }).catch(() => undefined)
|
|
|
+ }
|
|
|
+
|
|
|
+ const list = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
|
|
|
+ post<PageResult<Record<string, unknown>>>('/batches/page', {
|
|
|
+ pageNum,
|
|
|
+ pageSize,
|
|
|
+ townId: townId || undefined,
|
|
|
+ villageId: villageId || undefined,
|
|
|
+ batchLevel: 'VILLAGE',
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ const mapped = list.map(mapVillageBatch)
|
|
|
+ // 合法发放月优先靠前;脏业务号(如 214808)沉底
|
|
|
+ return mapped.sort((a, b) => {
|
|
|
+ const ak = isPayMonthLike(a.batchNo) ? a.batchNo : `0${a.batchNo}`
|
|
|
+ const bk = isPayMonthLike(b.batchNo) ? b.batchNo : `0${b.batchNo}`
|
|
|
+ if (ak !== bk) return bk.localeCompare(ak)
|
|
|
+ return String(b.id).localeCompare(String(a.id))
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+export async function fetchPeople(params?: {
|
|
|
+ batchNo?: string
|
|
|
+ town?: string
|
|
|
+ village?: string
|
|
|
+}): Promise<Person[]> {
|
|
|
+ await ensureTowns()
|
|
|
+ const cfg = getAppConfig()
|
|
|
+ const role = cfg.roles.find((r) => r.key === cfg.currentRole)
|
|
|
+ const town = params?.town || role?.town
|
|
|
+ const village = params?.village || role?.village
|
|
|
+ const { townId, villageId } = await resolveScopeIds(town, village)
|
|
|
+ if (townId) await ensureVillages(townId)
|
|
|
+
|
|
|
+ const list = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
|
|
|
+ post<PageResult<Record<string, unknown>>>('/personnel/page', {
|
|
|
+ pageNum,
|
|
|
+ pageSize,
|
|
|
+ townId: townId || undefined,
|
|
|
+ villageId: villageId || undefined,
|
|
|
+ payMonth: params?.batchNo || undefined,
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ return list.map((p) => mapPerson(p, params?.batchNo || currentPayMonth()))
|
|
|
+}
|
|
|
+
|
|
|
+export async function savePerson(payload: Partial<Person>) {
|
|
|
+ const cfg = getAppConfig()
|
|
|
+ const role = cfg.roles.find((r) => r.key === cfg.currentRole)
|
|
|
+ const town = payload.town || role?.town
|
|
|
+ const village = payload.village || role?.village
|
|
|
+ const { townId, villageId } = await resolveScopeIds(town, village)
|
|
|
+ if (!townId || !villageId) throw new Error('请先选择街镇与村居')
|
|
|
+ await post('/personnel', {
|
|
|
+ name: payload.name,
|
|
|
+ idNumber: payload.idCard,
|
|
|
+ townId,
|
|
|
+ villageId,
|
|
|
+ bankCardNumber: payload.bankCard,
|
|
|
+ bankName: payload.bankName,
|
|
|
+ socialInsuranceCode: payload.socialInsuranceCode,
|
|
|
+ monthlyStandard: payload.standard,
|
|
|
+ enjoyStartMonth: monthToPayMonth(payload.startMonth) || currentPayMonth(),
|
|
|
+ source: '村级录入',
|
|
|
+ remarks: payload.remark,
|
|
|
+ })
|
|
|
+ // 确保本月村批存在
|
|
|
+ await post('/batches/ensure-current-month', undefined, {
|
|
|
+ params: { townId, villageId, payMonth: currentPayMonth() },
|
|
|
+ }).catch(() => undefined)
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+export async function submitVillageBatch(batchId: string) {
|
|
|
+ await post(`/batches/${encodeURIComponent(batchId)}/submit`)
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+/** 更新批次纳入人员(草稿/退回可改) */
|
|
|
+export async function updateBatchMembers(batchId: string, personnelIds: string[]) {
|
|
|
+ await put(
|
|
|
+ `/batches/${encodeURIComponent(batchId)}/members`,
|
|
|
+ {
|
|
|
+ personnelIds,
|
|
|
+ operator: operatorOf(),
|
|
|
+ },
|
|
|
+ { timeout: 60000 },
|
|
|
+ )
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+/** 批次已纳入人员 ID(发放明细) */
|
|
|
+export async function fetchBatchMemberIds(batchId: string): Promise<string[]> {
|
|
|
+ if (!batchId) return []
|
|
|
+ const list = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
|
|
|
+ post<PageResult<Record<string, unknown>>>('/payments/details/page', {
|
|
|
+ pageNum,
|
|
|
+ pageSize,
|
|
|
+ batchId,
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ return list.map((d) => String(d.personnelId || '')).filter(Boolean)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 本批次人员明细:只返回发放明细中的人(不是全村人员库)。
|
|
|
+ * 草稿/退回批会先走 ensure-current-month,剔除暂停/停止/死亡等默认不应在册人员。
|
|
|
+ */
|
|
|
+export async function fetchBatchMemberPeople(
|
|
|
+ batchId: string,
|
|
|
+ opts?: { batchNo?: string; town?: string; village?: string; pruneDefaults?: boolean },
|
|
|
+): Promise<Person[]> {
|
|
|
+ if (!batchId) return []
|
|
|
+ await ensureTowns()
|
|
|
+ const town = opts?.town
|
|
|
+ const village = opts?.village
|
|
|
+ if (opts?.pruneDefaults !== false && town && village) {
|
|
|
+ const { townId, villageId } = await resolveScopeIds(town, village)
|
|
|
+ if (townId && villageId) {
|
|
|
+ await post('/batches/ensure-current-month', undefined, {
|
|
|
+ params: {
|
|
|
+ townId,
|
|
|
+ villageId,
|
|
|
+ payMonth: opts?.batchNo || currentPayMonth(),
|
|
|
+ },
|
|
|
+ }).catch(() => undefined)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const details = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
|
|
|
+ post<PageResult<Record<string, unknown>>>('/payments/details/page', {
|
|
|
+ pageNum,
|
|
|
+ pageSize,
|
|
|
+ batchId,
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ if (!details.length) return []
|
|
|
+
|
|
|
+ const roster = await fetchPeople({ town, village }).catch(() => [] as Person[])
|
|
|
+ const byId = new Map(roster.map((p) => [String(p.id), p]))
|
|
|
+ const batchNo = opts?.batchNo || currentPayMonth()
|
|
|
+
|
|
|
+ const result: Person[] = []
|
|
|
+ for (const d of details) {
|
|
|
+ const pid = String(d.personnelId || '')
|
|
|
+ if (!pid) continue
|
|
|
+ const base = byId.get(pid)
|
|
|
+ if (base) {
|
|
|
+ result.push({
|
|
|
+ ...base,
|
|
|
+ batchNo,
|
|
|
+ standard: Number(d.monthlyAmount ?? base.standard ?? 0),
|
|
|
+ bankCard: String(d.bankCardNumber || base.bankCard || ''),
|
|
|
+ remark: String(d.remarks || base.remark || ''),
|
|
|
+ })
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ result.push(
|
|
|
+ mapPerson(
|
|
|
+ {
|
|
|
+ id: pid,
|
|
|
+ name: d.name,
|
|
|
+ idNumber: d.idNumber,
|
|
|
+ townId: d.townId,
|
|
|
+ villageId: d.villageId,
|
|
|
+ bankCardNumber: d.bankCardNumber,
|
|
|
+ monthlyStandard: d.monthlyAmount,
|
|
|
+ remarks: d.remarks,
|
|
|
+ bizStatus: 'NORMAL',
|
|
|
+ },
|
|
|
+ batchNo,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ }
|
|
|
+ return result
|
|
|
+}
|
|
|
+
|
|
|
+/** 批次详情(原始 BatchVO → VillageBatch) */
|
|
|
+export async function fetchBatchDetail(batchId: string): Promise<VillageBatch | null> {
|
|
|
+ if (!batchId) return null
|
|
|
+ await ensureTowns()
|
|
|
+ const raw = await get<Record<string, unknown>>(`/batches/${encodeURIComponent(batchId)}`)
|
|
|
+ if (!raw) return null
|
|
|
+ if (raw.townId) await ensureVillages(String(raw.townId))
|
|
|
+ return mapVillageBatch(raw)
|
|
|
+}
|
|
|
+
|
|
|
+/** 批次流程总览(可执行动作 + 阶段说明) */
|
|
|
+export async function fetchBatchOverview(batchId: string): Promise<BatchFlowOverview | null> {
|
|
|
+ if (!batchId) return null
|
|
|
+ try {
|
|
|
+ const raw = await get<Record<string, unknown>>(
|
|
|
+ `/workflow/batches/${encodeURIComponent(batchId)}/overview`,
|
|
|
+ )
|
|
|
+ if (!raw) return null
|
|
|
+ const logs = Array.isArray(raw.recentLogs)
|
|
|
+ ? (raw.recentLogs as Record<string, unknown>[]).map(mapProcessLog)
|
|
|
+ : []
|
|
|
+ return {
|
|
|
+ batchId: String(raw.batchId || batchId),
|
|
|
+ batchNo: String(raw.batchNo || ''),
|
|
|
+ currentStage: flowStageLabel(String(raw.currentStage || '')),
|
|
|
+ status: String(raw.status || ''),
|
|
|
+ stageDescription: raw.stageDescription ? String(raw.stageDescription) : undefined,
|
|
|
+ availableActions: Array.isArray(raw.availableActions)
|
|
|
+ ? (raw.availableActions as string[])
|
|
|
+ : [],
|
|
|
+ recentLogs: logs,
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 批次流程时间线 */
|
|
|
+export async function fetchBatchTimeline(batchId: string): Promise<ProcessLogItem[]> {
|
|
|
+ if (!batchId) return []
|
|
|
+ try {
|
|
|
+ const res = await get<PageResult<Record<string, unknown>> | Record<string, unknown>[]>(
|
|
|
+ `/process-logs/batches/${encodeURIComponent(batchId)}/timeline`,
|
|
|
+ )
|
|
|
+ const list = Array.isArray(res) ? res : res?.list || []
|
|
|
+ return list
|
|
|
+ .map(mapProcessLog)
|
|
|
+ .sort((a, b) => String(b.operateTime || '').localeCompare(String(a.operateTime || '')))
|
|
|
+ } catch {
|
|
|
+ return []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 补充流程备注 */
|
|
|
+export async function addBatchProcessNote(payload: {
|
|
|
+ batchId: string
|
|
|
+ opinion: string
|
|
|
+ fileUrls?: string
|
|
|
+}) {
|
|
|
+ await post('/process-logs/note', {
|
|
|
+ batchId: payload.batchId,
|
|
|
+ opinion: payload.opinion,
|
|
|
+ fileUrls: payload.fileUrls,
|
|
|
+ operator: operatorOf(),
|
|
|
+ })
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+/** —— 居保 —— */
|
|
|
+/** 人工确认需要 batchId:记录最近一次打开的居保批次 */
|
|
|
+let lastInsuranceBatchId = ''
|
|
|
+
|
|
|
+export async function fetchInsuranceBatches(): Promise<VillageBatch[]> {
|
|
|
+ const all = await fetchVillageBatches()
|
|
|
+ return all.filter((b) => b.stage === '居保状态匹配' || b.stage === '比对结果已生成')
|
|
|
+}
|
|
|
+
|
|
|
+export async function fetchInsuranceResults(params?: {
|
|
|
+ batchId?: string
|
|
|
+ batchNo?: string
|
|
|
+ town?: string
|
|
|
+ village?: string
|
|
|
+}): Promise<Person[]> {
|
|
|
+ let batchId = params?.batchId || lastInsuranceBatchId
|
|
|
+ if (!batchId) {
|
|
|
+ const batches = await fetchVillageBatches()
|
|
|
+ const hit = batches.find(
|
|
|
+ (b) =>
|
|
|
+ (!params?.batchNo || b.batchNo === params.batchNo) &&
|
|
|
+ (!params?.town || b.town === params.town) &&
|
|
|
+ (!params?.village || b.village === params.village) &&
|
|
|
+ (b.stage === '居保状态匹配' ||
|
|
|
+ b.stage === '比对结果已生成' ||
|
|
|
+ b.stage === '分管领导审批'),
|
|
|
+ )
|
|
|
+ batchId = hit?.id || ''
|
|
|
+ }
|
|
|
+ if (!batchId) return []
|
|
|
+ lastInsuranceBatchId = batchId
|
|
|
+ try {
|
|
|
+ const rows = await get<Record<string, unknown>[]>(
|
|
|
+ `/insurance-match/batches/${encodeURIComponent(batchId)}/results`,
|
|
|
+ )
|
|
|
+ if (!Array.isArray(rows) || !rows.length) return []
|
|
|
+ return rows.map((r) =>
|
|
|
+ mapPerson(
|
|
|
+ {
|
|
|
+ id: r.personnelId || r.id,
|
|
|
+ name: r.name || r.personnelName,
|
|
|
+ idNumber: r.idNumber,
|
|
|
+ townId: r.townId,
|
|
|
+ villageId: r.villageId,
|
|
|
+ insuranceMatchResult: r.matchResult || r.insuranceMatchResult || r.result,
|
|
|
+ bizStatus: r.bizStatus || 'PENDING_INSURANCE_MATCH',
|
|
|
+ remarks: r.remarks || r.failReason || r.message,
|
|
|
+ },
|
|
|
+ params?.batchNo,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ } catch {
|
|
|
+ return []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export async function triggerInsuranceMatch(batchIds?: string | string[]) {
|
|
|
+ const ids = batchIds == null ? [] : Array.isArray(batchIds) ? batchIds : [batchIds]
|
|
|
+ let count = 0
|
|
|
+ for (const batchId of ids) {
|
|
|
+ lastInsuranceBatchId = batchId
|
|
|
+ const res = await post<{ nextStatus?: string; failCount?: number }>(
|
|
|
+ '/insurance-match/trigger',
|
|
|
+ { batchId, operator: operatorOf() },
|
|
|
+ )
|
|
|
+ // 全员成功时后端已进领导审;仍补齐明细。有 FAIL 时不要假装已生成可提交清单
|
|
|
+ if (!res?.failCount) {
|
|
|
+ await post(`/payments/batches/${encodeURIComponent(batchId)}/generate`).catch(() => undefined)
|
|
|
+ }
|
|
|
+ count += 1
|
|
|
+ }
|
|
|
+ return { ok: true, count }
|
|
|
+}
|
|
|
+
|
|
|
+export async function confirmInsuranceManual(personId: number | string, batchId?: string) {
|
|
|
+ const bid = batchId || lastInsuranceBatchId
|
|
|
+ if (!bid) {
|
|
|
+ throw new Error('请先打开村居比对结果后再人工确认')
|
|
|
+ }
|
|
|
+ lastInsuranceBatchId = bid
|
|
|
+ await post('/insurance-match/manual-confirm', {
|
|
|
+ personnelId: personIdStr(personId),
|
|
|
+ batchId: bid,
|
|
|
+ pass: true,
|
|
|
+ operator: operatorOf(),
|
|
|
+ })
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+/** 居保完成后提交分管领导(推进批次状态,非仅生成明细) */
|
|
|
+export async function submitToLeader(batchId: string) {
|
|
|
+ await post(`/batches/${encodeURIComponent(batchId)}/submit-leader`)
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+export async function exportCompareList(_batchId?: string) {
|
|
|
+ return {
|
|
|
+ ok: false as const,
|
|
|
+ message: '比对清单文件导出接口尚未对接,请在「查看比对结果」中核对名单',
|
|
|
+ fileName: '',
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export async function returnBatchToTown(batchId: string, reason?: string) {
|
|
|
+ await post('/batches/town-approve', {
|
|
|
+ bizId: batchId,
|
|
|
+ action: 'REJECT',
|
|
|
+ opinion: reason || '居保匹配异常,退回镇经发中心',
|
|
|
+ operator: operatorOf(),
|
|
|
+ })
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+/** —— 审批 —— */
|
|
|
+export async function fetchApprovals(params?: {
|
|
|
+ tab?: string
|
|
|
+ node?: string
|
|
|
+ town?: string
|
|
|
+ batchNo?: string
|
|
|
+ bizTypeIncludes?: string
|
|
|
+}): Promise<Approval[]> {
|
|
|
+ await ensureTowns()
|
|
|
+ const { townId } = await resolveScopeIds(params?.town, undefined)
|
|
|
+ const list: Approval[] = []
|
|
|
+
|
|
|
+ // 镇经发:待初审村批
|
|
|
+ if (!params?.node || params.node.includes('镇经发')) {
|
|
|
+ const townPending = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
|
|
|
+ post<PageResult<Record<string, unknown>>>('/batches/page', {
|
|
|
+ pageNum,
|
|
|
+ pageSize,
|
|
|
+ townId: townId || undefined,
|
|
|
+ batchLevel: 'VILLAGE',
|
|
|
+ status: 'PENDING_TOWN_APPROVE',
|
|
|
+ batchNo: params?.batchNo,
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ for (const b of townPending) {
|
|
|
+ const vb = mapVillageBatch(b)
|
|
|
+ list.push({
|
|
|
+ id: approvalIdBatch(vb.id),
|
|
|
+ batchId: vb.id,
|
|
|
+ bizType: '月度批次人员申请',
|
|
|
+ batchNo: vb.batchNo,
|
|
|
+ town: vb.town,
|
|
|
+ village: vb.village,
|
|
|
+ title: `${vb.village}${vb.month}批次人员申请`,
|
|
|
+ applicant: `${vb.village}合作社`,
|
|
|
+ node: '镇经发中心初审',
|
|
|
+ status: '待审批',
|
|
|
+ createdAt: vb.submittedAt || '',
|
|
|
+ peopleCount: vb.peopleCount,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 分管领导:待审月度清单
|
|
|
+ if (!params?.node || params.node.includes('分管领导')) {
|
|
|
+ const leaderPending = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
|
|
|
+ post<PageResult<Record<string, unknown>>>('/batches/page', {
|
|
|
+ pageNum,
|
|
|
+ pageSize,
|
|
|
+ townId: townId || undefined,
|
|
|
+ batchLevel: 'VILLAGE',
|
|
|
+ status: 'PENDING_LEADER_APPROVE',
|
|
|
+ batchNo: params?.batchNo,
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ for (const b of leaderPending) {
|
|
|
+ const vb = mapVillageBatch(b)
|
|
|
+ list.push({
|
|
|
+ id: approvalIdBatch(vb.id),
|
|
|
+ batchId: vb.id,
|
|
|
+ bizType: '月补贴清单确认',
|
|
|
+ batchNo: vb.batchNo,
|
|
|
+ town: vb.town,
|
|
|
+ village: vb.village,
|
|
|
+ title: `${vb.town}${vb.village}月补贴清单提交分管领导`,
|
|
|
+ applicant: `${vb.town}社区事务受理服务中心`,
|
|
|
+ node: '分管领导线上审批',
|
|
|
+ status: '待审批',
|
|
|
+ createdAt: vb.submittedAt || '',
|
|
|
+ peopleCount: vb.peopleCount,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 特殊业务待审
|
|
|
+ const specials = await get<PageResult<Record<string, unknown>>>('/special-biz/page', {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 200,
|
|
|
+ approveStatus: '待分管领导审批',
|
|
|
+ })
|
|
|
+ for (const s of specials?.list || []) {
|
|
|
+ const sp = mapSpecial(s)
|
|
|
+ if (params?.town && sp.town && sp.town !== params.town) continue
|
|
|
+ list.push({
|
|
|
+ id: approvalIdSpecial(sp.id),
|
|
|
+ bizType: sp.type,
|
|
|
+ batchNo: sp.batchNo,
|
|
|
+ town: sp.town,
|
|
|
+ village: sp.village,
|
|
|
+ title: `${sp.personName || sp.town}${sp.type}申请`,
|
|
|
+ applicant: `${sp.town}社区事务受理服务中心`,
|
|
|
+ node: '分管领导线上审批',
|
|
|
+ status: '待审批',
|
|
|
+ createdAt: sp.createdAt,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let out = list
|
|
|
+ if (params?.tab === 'pending') out = out.filter((a) => a.status === '待审批')
|
|
|
+ if (params?.bizTypeIncludes) {
|
|
|
+ out = out.filter((a) => a.bizType.includes(params.bizTypeIncludes!))
|
|
|
+ }
|
|
|
+ return out
|
|
|
+}
|
|
|
+
|
|
|
+export async function reviewPersonQualify(payload: {
|
|
|
+ personId: number | string
|
|
|
+ retireQualifyType: '有土地退养' | '无土地有资格'
|
|
|
+}) {
|
|
|
+ const id = personIdStr(payload.personId)
|
|
|
+ const detail = await get<Record<string, unknown>>(`/personnel/${encodeURIComponent(id)}`)
|
|
|
+ const tip =
|
|
|
+ payload.retireQualifyType === '无土地有资格'
|
|
|
+ ? '无承包地,按政策认定具备退养补助资格;资格已核验'
|
|
|
+ : '有土地退养;资格已核验'
|
|
|
+ await post('/personnel', {
|
|
|
+ id,
|
|
|
+ name: detail?.name,
|
|
|
+ idNumber: detail?.idNumber,
|
|
|
+ townId: detail?.townId,
|
|
|
+ villageId: detail?.villageId,
|
|
|
+ bankCardNumber: detail?.bankCardNumber,
|
|
|
+ bankName: detail?.bankName,
|
|
|
+ socialInsuranceCode: detail?.socialInsuranceCode,
|
|
|
+ monthlyStandard: detail?.monthlyStandard,
|
|
|
+ enjoyStartMonth: detail?.enjoyStartMonth,
|
|
|
+ source: detail?.source,
|
|
|
+ remarks: [detail?.remarks, tip].filter(Boolean).join(';'),
|
|
|
+ })
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+export async function decideApproval(payload: {
|
|
|
+ id: string
|
|
|
+ pass: boolean
|
|
|
+ opinion?: string
|
|
|
+}) {
|
|
|
+ const parsed = parseApprovalId(payload.id)
|
|
|
+ if (!parsed) return { ok: false }
|
|
|
+ const body = {
|
|
|
+ bizId: parsed.bizId,
|
|
|
+ action: payload.pass ? 'PASS' : 'REJECT',
|
|
|
+ opinion: payload.opinion || (payload.pass ? '同意' : '退回修改'),
|
|
|
+ operator: operatorOf(),
|
|
|
+ }
|
|
|
+ if (parsed.kind === 'batch') {
|
|
|
+ // 按批次当前状态选接口
|
|
|
+ const batch = await get<Record<string, unknown>>(`/batches/${encodeURIComponent(parsed.bizId)}`)
|
|
|
+ const st = String(batch?.status || '')
|
|
|
+ if (st === 'PENDING_TOWN_APPROVE') {
|
|
|
+ await post('/batches/town-approve', body)
|
|
|
+ } else {
|
|
|
+ await post('/batches/leader-approve', body)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ await post('/special-biz/leader-approve', body)
|
|
|
+ }
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+/** —— 状态维护 / 标准 —— */
|
|
|
+export async function fetchStatusBatches(): Promise<VillageBatch[]> {
|
|
|
+ const all = await fetchVillageBatches()
|
|
|
+ return all.filter((b) =>
|
|
|
+ ['分管领导审批', '待线下发放', '回盘信息确认'].includes(b.stage),
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+const standardIdMap = new Map<number, string>()
|
|
|
+
|
|
|
+export async function fetchStandards(town?: string): Promise<SubsidyStandard[]> {
|
|
|
+ await ensureTowns()
|
|
|
+ const townId = town ? townIdOf(town) : undefined
|
|
|
+ // 后端返回 PageResult,不是裸数组
|
|
|
+ const page = await get<PageResult<Record<string, unknown>>>('/amount-standards', {
|
|
|
+ townId: townId || undefined,
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 200,
|
|
|
+ })
|
|
|
+ const rows = Array.isArray(page) ? page : page?.list || []
|
|
|
+ const list = rows.map((r) => {
|
|
|
+ const m = mapStandard(r)
|
|
|
+ standardIdMap.set(m.id, String(r.id))
|
|
|
+ return m
|
|
|
+ })
|
|
|
+ return list.sort((a, b) => (b.year !== a.year ? b.year - a.year : b.date.localeCompare(a.date)))
|
|
|
+}
|
|
|
+
|
|
|
+export async function createStandard(payload: {
|
|
|
+ town: string
|
|
|
+ year: number
|
|
|
+ monthly: number
|
|
|
+ funeral: number
|
|
|
+ date: string
|
|
|
+ reason: string
|
|
|
+}) {
|
|
|
+ const townId = townIdOf(payload.town)
|
|
|
+ if (!townId) return { ok: false as const, message: '未识别街镇' }
|
|
|
+ await post('/amount-standards/yearly-adjust', {
|
|
|
+ townId,
|
|
|
+ monthlyAmount: payload.monthly,
|
|
|
+ funeralAmount: payload.funeral,
|
|
|
+ effectiveMonth: `${payload.year}01`,
|
|
|
+ adjustReason: payload.reason,
|
|
|
+ operator: operatorOf(),
|
|
|
+ }).catch(async () => {
|
|
|
+ await post('/amount-standards', {
|
|
|
+ townId,
|
|
|
+ standardType: 'YEARLY_ADJUST',
|
|
|
+ amount: payload.monthly,
|
|
|
+ funeralAmount: payload.funeral,
|
|
|
+ effectiveTime: payload.date,
|
|
|
+ adjustReason: payload.reason,
|
|
|
+ operator: operatorOf(),
|
|
|
+ })
|
|
|
+ })
|
|
|
+ return { ok: true as const }
|
|
|
+}
|
|
|
+
|
|
|
+export async function updateStandard(payload: {
|
|
|
+ id: number
|
|
|
+ monthly: number
|
|
|
+ funeral: number
|
|
|
+ date: string
|
|
|
+ reason: string
|
|
|
+}) {
|
|
|
+ const realId = standardIdMap.get(payload.id)
|
|
|
+ await post('/amount-standards', {
|
|
|
+ id: realId,
|
|
|
+ amount: payload.monthly,
|
|
|
+ funeralAmount: payload.funeral,
|
|
|
+ effectiveTime: payload.date,
|
|
|
+ adjustReason: payload.reason,
|
|
|
+ operator: operatorOf(),
|
|
|
+ })
|
|
|
+ return { ok: true as const }
|
|
|
+}
|
|
|
+
|
|
|
+export async function updateDeathStatus(deathId: string) {
|
|
|
+ // deathId 可能是人员 ID 或告警 ID
|
|
|
+ try {
|
|
|
+ await put('/personnel/status', {
|
|
|
+ personnelId: deathId,
|
|
|
+ bizStatus: 'STOPPED',
|
|
|
+ payThisMonth: false,
|
|
|
+ remarks: '已完成死亡状态更新,停止发放',
|
|
|
+ operator: operatorOf(),
|
|
|
+ })
|
|
|
+ } catch {
|
|
|
+ /* ignore */
|
|
|
+ }
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+export async function applyPersonStatusAction(payload: {
|
|
|
+ personId: number | string
|
|
|
+ action: string
|
|
|
+ amount?: number
|
|
|
+ reason?: string
|
|
|
+}) {
|
|
|
+ const personnelId = personIdStr(payload.personId)
|
|
|
+ const map: Record<string, string> = {
|
|
|
+ 停止: 'STOP',
|
|
|
+ '暂停/停止': 'STOP',
|
|
|
+ 暂停: 'PAUSE',
|
|
|
+ 恢复: 'RESUME',
|
|
|
+ 丧葬: 'FUNERAL',
|
|
|
+ 死亡状态更新: 'FUNERAL',
|
|
|
+ 补发: 'SUPPLEMENT',
|
|
|
+ 调标补发: 'STANDARD_ADJUST',
|
|
|
+ }
|
|
|
+ const bizType = map[payload.action]
|
|
|
+ if (bizType === 'STOP' || bizType === 'PAUSE' || bizType === 'RESUME' || bizType === 'FUNERAL' || bizType === 'SUPPLEMENT') {
|
|
|
+ await post('/special-biz', {
|
|
|
+ personnelId,
|
|
|
+ bizType,
|
|
|
+ reason: payload.reason || payload.action,
|
|
|
+ amount: payload.amount,
|
|
|
+ operator: operatorOf(),
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+/** —— 特殊业务 —— */
|
|
|
+export async function fetchSpecials(): Promise<SpecialBiz[]> {
|
|
|
+ await ensureTowns()
|
|
|
+ const res = await get<PageResult<Record<string, unknown>>>('/special-biz/page', {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 200,
|
|
|
+ })
|
|
|
+ return (res?.list || []).map(mapSpecial)
|
|
|
+}
|
|
|
+
|
|
|
+export async function fetchDeaths(): Promise<DeathRecord[]> {
|
|
|
+ await ensureTowns()
|
|
|
+ const list = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
|
|
|
+ post<PageResult<Record<string, unknown>>>('/personnel/page', {
|
|
|
+ pageNum,
|
|
|
+ pageSize,
|
|
|
+ bizStatus: 'FUNERAL',
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ return list.map((p) => ({
|
|
|
+ id: String(p.id || ''),
|
|
|
+ personName: String(p.name || ''),
|
|
|
+ idCard: String(p.idNumber || ''),
|
|
|
+ town: regionName(String(p.townId || '')),
|
|
|
+ village: regionName(String(p.villageId || '')),
|
|
|
+ deathDate: String(p.deathDate || ''),
|
|
|
+ heirName: p.heirName ? String(p.heirName) : '',
|
|
|
+ heirIdCard: p.heirIdNumber ? String(p.heirIdNumber) : '',
|
|
|
+ heirBankCard: p.heirBankCard ? String(p.heirBankCard) : '',
|
|
|
+ heirContact: p.heirPhone ? String(p.heirPhone) : '',
|
|
|
+ source: String(p.source || '社区事务受理服务中心手工录入'),
|
|
|
+ status: '待死亡状态更新',
|
|
|
+ createdAt: '',
|
|
|
+ }))
|
|
|
+}
|
|
|
+
|
|
|
+export async function createDeathRecord(payload: {
|
|
|
+ personName: string
|
|
|
+ idCard: string
|
|
|
+ town: string
|
|
|
+ village: string
|
|
|
+ deathDate: string
|
|
|
+ heirName?: string
|
|
|
+ heirIdCard?: string
|
|
|
+ heirBankCard?: string
|
|
|
+ heirContact?: string
|
|
|
+}) {
|
|
|
+ const { townId, villageId } = await resolveScopeIds(payload.town, payload.village)
|
|
|
+ // 先按身份证找人
|
|
|
+ const found = await post<PageResult<Record<string, unknown>>>('/personnel/page', {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 5,
|
|
|
+ idNumber: payload.idCard,
|
|
|
+ townId: townId || undefined,
|
|
|
+ })
|
|
|
+ let personnelId = found?.list?.[0]?.id ? String(found.list[0].id) : ''
|
|
|
+ if (!personnelId) {
|
|
|
+ const created = await post<Record<string, unknown>>('/personnel', {
|
|
|
+ name: payload.personName,
|
|
|
+ idNumber: payload.idCard,
|
|
|
+ townId,
|
|
|
+ villageId,
|
|
|
+ deathDate: payload.deathDate,
|
|
|
+ heirName: payload.heirName,
|
|
|
+ heirIdNumber: payload.heirIdCard,
|
|
|
+ heirBankCard: payload.heirBankCard,
|
|
|
+ heirPhone: payload.heirContact,
|
|
|
+ source: '死亡名单',
|
|
|
+ remarks: '死亡人员信息录入',
|
|
|
+ })
|
|
|
+ personnelId = String(created?.id || '')
|
|
|
+ } else {
|
|
|
+ await put('/personnel/status', {
|
|
|
+ personnelId,
|
|
|
+ bizStatus: 'FUNERAL',
|
|
|
+ payThisMonth: false,
|
|
|
+ remarks: `死亡日期 ${payload.deathDate}`,
|
|
|
+ operator: operatorOf(),
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return { ok: true, id: personnelId }
|
|
|
+}
|
|
|
+
|
|
|
+export async function createSpecialBiz(payload: {
|
|
|
+ type: string
|
|
|
+ personName: string
|
|
|
+ town: string
|
|
|
+ village: string
|
|
|
+ reason: string
|
|
|
+ amount: number
|
|
|
+ heirName?: string
|
|
|
+}) {
|
|
|
+ const { townId, villageId } = await resolveScopeIds(payload.town, payload.village)
|
|
|
+ const bizType = specialTypeToCode(payload.type)
|
|
|
+ let personnelId: string | undefined
|
|
|
+ if (bizType !== 'STANDARD_ADJUST') {
|
|
|
+ const people = await post<PageResult<Record<string, unknown>>>('/personnel/page', {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ name: payload.personName,
|
|
|
+ townId: townId || undefined,
|
|
|
+ villageId: villageId || undefined,
|
|
|
+ })
|
|
|
+ personnelId = people?.list?.[0]?.id ? String(people.list[0].id) : undefined
|
|
|
+ if (!personnelId) throw new Error(`未找到人员:${payload.personName}`)
|
|
|
+ }
|
|
|
+ const created = await post<Record<string, unknown>>('/special-biz', {
|
|
|
+ personnelId,
|
|
|
+ bizType,
|
|
|
+ reason: payload.reason,
|
|
|
+ amount: payload.amount,
|
|
|
+ townId: townId || undefined,
|
|
|
+ heirName: payload.heirName,
|
|
|
+ operator: operatorOf(),
|
|
|
+ })
|
|
|
+ return { ok: true, id: String(created?.id || '') }
|
|
|
+}
|
|
|
+
|
|
|
+export async function exportSpecialSummary(type?: string) {
|
|
|
+ return {
|
|
|
+ ok: true,
|
|
|
+ fileName: type
|
|
|
+ ? `${currentPayMonth()}-${type}汇总文件.xlsx`
|
|
|
+ : '特殊业务汇总文件.xlsx',
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export async function triggerAdjustReissue(town?: string) {
|
|
|
+ const cfg = getAppConfig()
|
|
|
+ const role = cfg.roles.find((r) => r.key === cfg.currentRole)
|
|
|
+ const townName = town || role?.town || '叶榭镇'
|
|
|
+ const { townId } = await resolveScopeIds(townName)
|
|
|
+ if (!townId) return { ok: false, message: '未识别街镇', batchNo: currentPayMonth(), count: 0, villageCount: 0 }
|
|
|
+ const created = await post<Record<string, unknown>>('/special-biz', {
|
|
|
+ bizType: 'STANDARD_ADJUST',
|
|
|
+ townId,
|
|
|
+ reason: '年度调标差额补发',
|
|
|
+ operator: operatorOf(),
|
|
|
+ })
|
|
|
+ const related = String(created?.relatedBatchIds || '')
|
|
|
+ const villageCount = related ? related.split(/[,,]/).filter(Boolean).length : 0
|
|
|
+ return {
|
|
|
+ ok: true,
|
|
|
+ batchNo: String(created?.batchNo || currentPayMonth()),
|
|
|
+ villageCount,
|
|
|
+ count: Number(created?.amount ? 0 : 0) || villageCount,
|
|
|
+ message: undefined as string | undefined,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 从备注解析旧/新月标(批次或明细备注:旧月标1280|新月标2113) */
|
|
|
+function parseOldNewMonthly(text?: string): { oldMonthly: number; newMonthly: number } | null {
|
|
|
+ const s = String(text || '')
|
|
|
+ const oldM = s.match(/旧月标\s*([0-9]+(?:\.[0-9]+)?)/)
|
|
|
+ const newM = s.match(/新月标\s*([0-9]+(?:\.[0-9]+)?)/)
|
|
|
+ if (!oldM || !newM) return null
|
|
|
+ const oldMonthly = Number(oldM[1])
|
|
|
+ const newMonthly = Number(newM[1])
|
|
|
+ if (!(newMonthly > 0) || !(oldMonthly >= 0)) return null
|
|
|
+ return { oldMonthly, newMonthly }
|
|
|
+}
|
|
|
+
|
|
|
+/** 从明细备注解析应发月数与单月差额 */
|
|
|
+function parseAdjustMonthsDiff(text?: string): { months: number; diff: number } | null {
|
|
|
+ const s = String(text || '')
|
|
|
+ const m = s.match(/应发\s*(\d+)\s*月\s*[×xX*]\s*差额\s*([0-9]+(?:\.[0-9]+)?)/)
|
|
|
+ if (!m) return null
|
|
|
+ return { months: Number(m[1]), diff: Number(m[2]) }
|
|
|
+}
|
|
|
+
|
|
|
+/** 从金额标准表取当前新标 + 上一档旧标 */
|
|
|
+async function loadTownAdjustStandards(townId: string): Promise<{
|
|
|
+ oldMonthly: number
|
|
|
+ newMonthly: number
|
|
|
+} | null> {
|
|
|
+ try {
|
|
|
+ const page = await get<PageResult<Record<string, unknown>>>('/amount-standards', {
|
|
|
+ townId,
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 100,
|
|
|
+ })
|
|
|
+ const list = (page?.list || []).filter((r) => {
|
|
|
+ const t = String(r.standardType || '')
|
|
|
+ return (t === 'YEARLY_ADJUST' || t === 'MONTHLY') && Number(r.amount || 0) > 0
|
|
|
+ })
|
|
|
+ if (!list.length) return null
|
|
|
+ list.sort((a, b) => Number(b.effectiveTime || 0) - Number(a.effectiveTime || 0))
|
|
|
+ const cur =
|
|
|
+ list.find((r) => r.current === true || r.current === 'true') || list[0]
|
|
|
+ const curAmt = Number(cur.amount || 0)
|
|
|
+ const curEff = Number(cur.effectiveTime || 0)
|
|
|
+ const prev = list.find((r) => {
|
|
|
+ if (String(r.id) === String(cur.id)) return false
|
|
|
+ const amt = Number(r.amount || 0)
|
|
|
+ if (!(amt > 0)) return false
|
|
|
+ const eff = Number(r.effectiveTime || 0)
|
|
|
+ return !curEff || !eff || eff < curEff
|
|
|
+ })
|
|
|
+ if (!prev) return null
|
|
|
+ const oldMonthly = Number(prev.amount || 0)
|
|
|
+ if (!(curAmt > oldMonthly)) return null
|
|
|
+ return { oldMonthly, newMonthly: curAmt }
|
|
|
+ } catch {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export async function fetchAdjustReissueRoster(params?: {
|
|
|
+ town?: string
|
|
|
+ batchNo?: string
|
|
|
+ village?: string
|
|
|
+}): Promise<AdjustReissuePerson[]> {
|
|
|
+ await ensureTowns()
|
|
|
+ const { townId, villageId } = await resolveScopeIds(params?.town, params?.village)
|
|
|
+ const townStandards = townId ? await loadTownAdjustStandards(townId) : null
|
|
|
+
|
|
|
+ // 调标村批一般为 YYYY99 或备注含调标;按发放明细补发额组装
|
|
|
+ const batches = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
|
|
|
+ post<PageResult<Record<string, unknown>>>('/batches/page', {
|
|
|
+ pageNum,
|
|
|
+ pageSize,
|
|
|
+ townId: townId || undefined,
|
|
|
+ villageId: villageId || undefined,
|
|
|
+ batchLevel: 'VILLAGE',
|
|
|
+ batchNo: params?.batchNo,
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ const adjustBatches = batches.filter((b) => {
|
|
|
+ const no = String(b.batchNo || b.payMonth || '')
|
|
|
+ const remarks = String(b.remarks || '')
|
|
|
+ const id = String(b.id || '')
|
|
|
+ return (
|
|
|
+ no.endsWith('99') ||
|
|
|
+ id.includes('99') ||
|
|
|
+ remarks.includes('YEARLY_ADJUST') ||
|
|
|
+ remarks.includes('调标') ||
|
|
|
+ Number(b.supplementAmount || 0) > 0
|
|
|
+ )
|
|
|
+ })
|
|
|
+ const roster: AdjustReissuePerson[] = []
|
|
|
+ for (const b of adjustBatches) {
|
|
|
+ if (params?.village && regionName(String(b.villageId || '')) !== params.village) continue
|
|
|
+ const batchBasis =
|
|
|
+ parseOldNewMonthly(String(b.remarks || '')) || townStandards
|
|
|
+ const details = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
|
|
|
+ post<PageResult<Record<string, unknown>>>('/payments/details/page', {
|
|
|
+ pageNum,
|
|
|
+ pageSize,
|
|
|
+ batchId: b.id,
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ for (const d of details) {
|
|
|
+ const amt = Number(d.supplementAmount ?? d.totalAmount ?? 0)
|
|
|
+ if (!amt) continue
|
|
|
+ const detailBasis = parseOldNewMonthly(String(d.remarks || ''))
|
|
|
+ const monthsDiff = parseAdjustMonthsDiff(String(d.remarks || ''))
|
|
|
+ let newStandard = 0
|
|
|
+ let oldStandard = 0
|
|
|
+
|
|
|
+ if (detailBasis) {
|
|
|
+ oldStandard = detailBasis.oldMonthly
|
|
|
+ newStandard = detailBasis.newMonthly
|
|
|
+ } else if (batchBasis) {
|
|
|
+ oldStandard = batchBasis.oldMonthly
|
|
|
+ newStandard = batchBasis.newMonthly
|
|
|
+ } else if (monthsDiff && monthsDiff.diff > 0) {
|
|
|
+ // 仅有差额时:明细月标若为新标则反推旧标;否则用差额反推
|
|
|
+ const monthly = Number(d.monthlyAmount || 0)
|
|
|
+ if (monthly > monthsDiff.diff) {
|
|
|
+ newStandard = monthly
|
|
|
+ oldStandard = Math.max(0, monthly - monthsDiff.diff)
|
|
|
+ } else {
|
|
|
+ newStandard = monthsDiff.diff
|
|
|
+ oldStandard = 0
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const monthly = Number(d.monthlyAmount || 0)
|
|
|
+ if (monthly > 0) {
|
|
|
+ // 历史脏数据曾把月标写成 0;若已有新标则用差额反推
|
|
|
+ newStandard = monthly
|
|
|
+ const months = monthsDiff?.months || 0
|
|
|
+ const unitDiff = months > 0 ? amt / months : 0
|
|
|
+ oldStandard = Math.max(0, monthly - (unitDiff || 0))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 仍缺新标时:用「补发额 / 月数 + 旧标」补全
|
|
|
+ if (newStandard <= 0 && oldStandard > 0 && monthsDiff?.months) {
|
|
|
+ newStandard = oldStandard + amt / monthsDiff.months
|
|
|
+ }
|
|
|
+ if (oldStandard <= 0 && newStandard > 0 && monthsDiff?.months) {
|
|
|
+ oldStandard = Math.max(0, newStandard - amt / monthsDiff.months)
|
|
|
+ }
|
|
|
+ if (newStandard <= 0 && monthsDiff?.diff) {
|
|
|
+ newStandard = (oldStandard || 0) + monthsDiff.diff
|
|
|
+ }
|
|
|
+ if (oldStandard <= 0 && newStandard > 0 && monthsDiff?.diff) {
|
|
|
+ oldStandard = Math.max(0, newStandard - monthsDiff.diff)
|
|
|
+ }
|
|
|
+
|
|
|
+ roster.push({
|
|
|
+ id: String(d.id || ''),
|
|
|
+ batchNo: String(b.batchNo || b.payMonth || params?.batchNo || ''),
|
|
|
+ personId: String(d.personnelId || '') as unknown as number,
|
|
|
+ name: String(d.name || ''),
|
|
|
+ idCard: String(d.idNumber || ''),
|
|
|
+ town: regionName(String(d.townId || b.townId || '')),
|
|
|
+ village: regionName(String(d.villageId || b.villageId || '')),
|
|
|
+ oldStandard: Number(oldStandard.toFixed(2)),
|
|
|
+ newStandard: Number(newStandard.toFixed(2)),
|
|
|
+ amount: amt,
|
|
|
+ status: '待提交',
|
|
|
+ remark: '年度调标差额补发',
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return roster
|
|
|
+}
|
|
|
+
|
|
|
+/** —— 月补贴 —— */
|
|
|
+export async function fetchVillageMonthlyBatches(town?: string): Promise<VillageMonthlyBatch[]> {
|
|
|
+ await ensureTowns()
|
|
|
+ const cfg = getAppConfig()
|
|
|
+ const role = cfg.roles.find((r) => r.key === cfg.currentRole)
|
|
|
+ const { townId } = await resolveScopeIds(town || role?.town)
|
|
|
+ if (townId) await ensureVillages(townId)
|
|
|
+ const list = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
|
|
|
+ post<PageResult<Record<string, unknown>>>('/batches/page', {
|
|
|
+ pageNum,
|
|
|
+ pageSize,
|
|
|
+ townId: townId || undefined,
|
|
|
+ batchLevel: 'VILLAGE',
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ const mapped = list
|
|
|
+ .map(mapVillageMonthly)
|
|
|
+ .filter((b) =>
|
|
|
+ ['待提交', '待审核', '已审核', '已出盘', '已回盘'].includes(b.status),
|
|
|
+ )
|
|
|
+ // 后端 PENDING_STATUS_MAINTAIN / 匹配后未进领导审的也可视为待提交
|
|
|
+ const extra = list
|
|
|
+ .filter((r) => {
|
|
|
+ const st = String(r.status || '')
|
|
|
+ return st === 'PENDING_STATUS_MAINTAIN' || st === 'PENDING_LEADER_APPROVE' ||
|
|
|
+ st === 'READY_EXPORT' || st === 'EXPORTED' || st === 'RETURNED' || st === 'ARCHIVED'
|
|
|
+ })
|
|
|
+ .map(mapVillageMonthly)
|
|
|
+ const byId = new Map<string, VillageMonthlyBatch>()
|
|
|
+ ;[...mapped, ...extra].forEach((x) => byId.set(x.id, x))
|
|
|
+ return [...byId.values()]
|
|
|
+}
|
|
|
+
|
|
|
+export async function fetchMonthlyBatches(): Promise<MonthlyBatch[]> {
|
|
|
+ const villages = await fetchVillageMonthlyBatches()
|
|
|
+ const map = new Map<string, MonthlyBatch>()
|
|
|
+ for (const v of villages) {
|
|
|
+ const key = `${v.batchNo}|${v.town}`
|
|
|
+ const cur = map.get(key)
|
|
|
+ if (!cur) {
|
|
|
+ map.set(key, {
|
|
|
+ id: `MB${v.batchNo}${townIdOf(v.town)}`,
|
|
|
+ batchNo: v.batchNo,
|
|
|
+ month: v.month,
|
|
|
+ town: v.town,
|
|
|
+ peopleCount: v.peopleCount,
|
|
|
+ normalAmount: v.normalAmount,
|
|
|
+ extraAmount: v.extraAmount,
|
|
|
+ deductAmount: v.deductAmount,
|
|
|
+ totalAmount: v.totalAmount,
|
|
|
+ status: v.status,
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ cur.peopleCount += v.peopleCount
|
|
|
+ cur.normalAmount += v.normalAmount
|
|
|
+ cur.extraAmount += v.extraAmount
|
|
|
+ cur.deductAmount += v.deductAmount
|
|
|
+ cur.totalAmount += v.totalAmount
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return [...map.values()]
|
|
|
+}
|
|
|
+
|
|
|
+export async function calculateMonthly() {
|
|
|
+ await post('/batches/ensure-current-month-all', undefined, {
|
|
|
+ params: { payMonth: currentPayMonth() },
|
|
|
+ }).catch(() => undefined)
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+export async function confirmMonthly(id: string) {
|
|
|
+ await post('/payments/export-disk', { batchId: id, bypassDeadline: true }).catch(() => undefined)
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+export async function exportMonthlyFile(id?: string) {
|
|
|
+ if (!id) {
|
|
|
+ return { ok: false as const, message: '缺少批次ID', fileName: '' }
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const res = await post<Record<string, unknown>>('/payments/export-disk', {
|
|
|
+ batchId: id,
|
|
|
+ bypassDeadline: true,
|
|
|
+ })
|
|
|
+ return {
|
|
|
+ ok: true as const,
|
|
|
+ fileName: String(res?.fileName || res?.exportDiskNo || `${currentPayMonth()}-月补贴发放文件`),
|
|
|
+ message: String(res?.offlineTip || ''),
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ return {
|
|
|
+ ok: false as const,
|
|
|
+ fileName: '',
|
|
|
+ message: e instanceof Error ? e.message : '导出失败',
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export async function issueVillageMonthlyDisk(batchId: string) {
|
|
|
+ const res = await post<Record<string, unknown>>('/payments/export-disk', {
|
|
|
+ batchId,
|
|
|
+ bypassDeadline: true,
|
|
|
+ })
|
|
|
+ return {
|
|
|
+ ok: true as const,
|
|
|
+ fileName: String(res?.fileName || res?.exportDiskNo || `${currentPayMonth()}-月补贴发放文件`),
|
|
|
+ message: String(res?.offlineTip || ''),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export async function confirmVillageMonthlyReturn(payload: {
|
|
|
+ batchId: string
|
|
|
+ failCount?: number
|
|
|
+}) {
|
|
|
+ await post('/return-disk/confirm', {
|
|
|
+ batchId: payload.batchId,
|
|
|
+ manualAllSuccess: !payload.failCount,
|
|
|
+ operator: operatorOf(),
|
|
|
+ })
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+/** —— 回盘 —— */
|
|
|
+export async function fetchDisks(town?: string): Promise<DiskRecord[]> {
|
|
|
+ await ensureTowns()
|
|
|
+ const { townId } = await resolveScopeIds(town)
|
|
|
+ const list = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
|
|
|
+ post<PageResult<Record<string, unknown>>>('/batches/page', {
|
|
|
+ pageNum,
|
|
|
+ pageSize,
|
|
|
+ townId: townId || undefined,
|
|
|
+ batchLevel: 'VILLAGE',
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ return list
|
|
|
+ .filter((b) => ['EXPORTED', 'RETURNED', 'ARCHIVED'].includes(String(b.status || '')))
|
|
|
+ .map(mapDisk)
|
|
|
+ .sort((a, b) => b.batchNo.localeCompare(a.batchNo))
|
|
|
+}
|
|
|
+
|
|
|
+export async function exportDisk() {
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+export async function confirmReturnDisk(payload?: { id?: string; failCount?: number }) {
|
|
|
+ if (!payload?.id) return { ok: false }
|
|
|
+ await post('/return-disk/confirm', {
|
|
|
+ batchId: payload.id,
|
|
|
+ manualAllSuccess: !payload.failCount,
|
|
|
+ operator: operatorOf(),
|
|
|
+ })
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+export async function uploadReturnDiskFile(payload: { id: string; file: File }) {
|
|
|
+ const form = new FormData()
|
|
|
+ form.append('file', payload.file)
|
|
|
+ const uploaded = await post<Record<string, unknown>>(
|
|
|
+ `/return-disk/upload?batchId=${encodeURIComponent(payload.id)}`,
|
|
|
+ form as unknown as Record<string, unknown>,
|
|
|
+ )
|
|
|
+ // 上传解析后确认回盘(对齐旧前端:导入+确认)
|
|
|
+ const confirmed = await post<Record<string, unknown>>('/return-disk/confirm', {
|
|
|
+ batchId: payload.id,
|
|
|
+ fileName: payload.file.name,
|
|
|
+ operator: operatorOf(),
|
|
|
+ }).catch(() => uploaded)
|
|
|
+ return {
|
|
|
+ ok: true as const,
|
|
|
+ failCount: Number(confirmed?.failCount ?? uploaded?.failCount ?? 0),
|
|
|
+ alertCount: Number(confirmed?.alertCount ?? 0),
|
|
|
+ message: String(confirmed?.message || uploaded?.message || '回盘文件已处理'),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export async function fetchDiskFailPeople(diskId: string): Promise<DiskFailPerson[]> {
|
|
|
+ const details = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
|
|
|
+ post<PageResult<Record<string, unknown>>>('/return-disk/details/page', {
|
|
|
+ pageNum,
|
|
|
+ pageSize,
|
|
|
+ batchId: diskId,
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ return details
|
|
|
+ .filter((d) => d.success === false)
|
|
|
+ .map((d) => mapReturnFail(d, diskId))
|
|
|
+}
|
|
|
+
|
|
|
+export async function confirmDiskFailPaid(failPersonId: string) {
|
|
|
+ // 纳入待补发/已确认:走待补发台账 reissue 或仅标记
|
|
|
+ try {
|
|
|
+ await post('/return-arrears/reissue', {
|
|
|
+ arrearsIds: [failPersonId],
|
|
|
+ operator: operatorOf(),
|
|
|
+ })
|
|
|
+ } catch {
|
|
|
+ /* 明细 ID 可能不是台账 ID,忽略 */
|
|
|
+ }
|
|
|
+ return { ok: true as const }
|
|
|
+}
|
|
|
+
|
|
|
+export async function archiveToNextMonth(id?: string) {
|
|
|
+ if (id) await post(`/dashboard/archive/${encodeURIComponent(id)}`)
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+/** —— 告警 —— */
|
|
|
+export async function fetchAlerts(): Promise<AlertItem[]> {
|
|
|
+ await ensureTowns()
|
|
|
+ const list = await pageAll<Record<string, unknown>>((pageNum, pageSize) =>
|
|
|
+ post<PageResult<Record<string, unknown>>>('/alerts/page', { pageNum, pageSize }),
|
|
|
+ )
|
|
|
+ return list.map(mapAlert)
|
|
|
+}
|
|
|
+
|
|
|
+export async function handleAlert(payload: HandleAlertPayload | string, result?: string) {
|
|
|
+ const data: HandleAlertPayload =
|
|
|
+ typeof payload === 'string'
|
|
|
+ ? {
|
|
|
+ id: payload,
|
|
|
+ personName: '',
|
|
|
+ idCard: '',
|
|
|
+ town: '',
|
|
|
+ village: '',
|
|
|
+ bankCard: '',
|
|
|
+ bankName: '',
|
|
|
+ socialInsuranceCode: '',
|
|
|
+ heirName: '',
|
|
|
+ heirIdCard: '',
|
|
|
+ heirBankCard: '',
|
|
|
+ heirContact: '',
|
|
|
+ result,
|
|
|
+ }
|
|
|
+ : payload
|
|
|
+
|
|
|
+ await post('/alerts/handle', {
|
|
|
+ alertId: data.id,
|
|
|
+ handleStatus: 'CLOSED',
|
|
|
+ handleResult: data.result || '已核实并处理',
|
|
|
+ handler: operatorOf(),
|
|
|
+ })
|
|
|
+
|
|
|
+ // 同步人员信息(已有字段范围内)
|
|
|
+ if (data.idCard) {
|
|
|
+ const found = await post<PageResult<Record<string, unknown>>>('/personnel/page', {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 5,
|
|
|
+ idNumber: data.idCard,
|
|
|
+ })
|
|
|
+ const p = found?.list?.[0]
|
|
|
+ if (p?.id) {
|
|
|
+ const { townId, villageId } = await resolveScopeIds(data.town, data.village)
|
|
|
+ await post('/personnel', {
|
|
|
+ id: p.id,
|
|
|
+ name: data.personName || p.name,
|
|
|
+ idNumber: data.idCard || p.idNumber,
|
|
|
+ townId: townId || p.townId,
|
|
|
+ villageId: villageId || p.villageId,
|
|
|
+ bankCardNumber: data.bankCard || p.bankCardNumber,
|
|
|
+ bankName: data.bankName || p.bankName,
|
|
|
+ socialInsuranceCode: data.socialInsuranceCode || p.socialInsuranceCode,
|
|
|
+ heirName: data.heirName || p.heirName,
|
|
|
+ heirIdNumber: data.heirIdCard || p.heirIdNumber,
|
|
|
+ heirBankCard: data.heirBankCard || p.heirBankCard,
|
|
|
+ heirPhone: data.heirContact || p.heirPhone,
|
|
|
+ monthlyStandard: p.monthlyStandard,
|
|
|
+ enjoyStartMonth: p.enjoyStartMonth,
|
|
|
+ source: p.source,
|
|
|
+ remarks: p.remarks,
|
|
|
+ }).catch(() => undefined)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return { ok: true }
|
|
|
+}
|
|
|
+
|
|
|
+/** —— 报表 / 系统 —— */
|
|
|
+export async function fetchReportBatches(): Promise<VillageBatch[]> {
|
|
|
+ return fetchVillageBatches()
|
|
|
+}
|
|
|
+
|
|
|
+/** —— 系统管理员运维 —— */
|
|
|
+export async function listRegionsAdmin(params?: {
|
|
|
+ level?: string
|
|
|
+ parentId?: string
|
|
|
+}): Promise<RegionRow[]> {
|
|
|
+ const data = await get<RegionRow[]>('/regions', {
|
|
|
+ level: params?.level,
|
|
|
+ parentId: params?.parentId,
|
|
|
+ })
|
|
|
+ return data || []
|
|
|
+}
|
|
|
+
|
|
|
+export async function saveRegionAdmin(payload: {
|
|
|
+ id?: string
|
|
|
+ code: string
|
|
|
+ name: string
|
|
|
+ level: string
|
|
|
+ parentId?: string
|
|
|
+ entrustPlanCode?: string
|
|
|
+ defaultBank?: string
|
|
|
+ enabled?: boolean
|
|
|
+ remarks?: string
|
|
|
+}) {
|
|
|
+ await post('/regions', payload)
|
|
|
+ return { ok: true as const }
|
|
|
+}
|
|
|
+
|
|
|
+export async function deleteRegionAdmin(id: string) {
|
|
|
+ await del(`/regions/${encodeURIComponent(id)}`)
|
|
|
+ return { ok: true as const }
|
|
|
+}
|
|
|
+
|
|
|
+export async function pageAdminBatches(params?: {
|
|
|
+ payMonth?: string
|
|
|
+ townId?: string
|
|
|
+ villageId?: string
|
|
|
+ batchLevel?: string
|
|
|
+ status?: string
|
|
|
+}) {
|
|
|
+ await ensureTowns()
|
|
|
+ const res = await post<PageResult<Record<string, unknown>>>('/batches/page', {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 500,
|
|
|
+ payMonth: params?.payMonth || undefined,
|
|
|
+ townId: params?.townId || undefined,
|
|
|
+ villageId: params?.villageId || undefined,
|
|
|
+ batchLevel: params?.batchLevel || undefined,
|
|
|
+ status: params?.status || undefined,
|
|
|
+ })
|
|
|
+ return (res?.list || []).map((b) => ({
|
|
|
+ id: String(b.id || ''),
|
|
|
+ batchNo: String(b.batchNo || b.payMonth || ''),
|
|
|
+ payMonth: String(b.payMonth || ''),
|
|
|
+ batchLevel: String(b.batchLevel || 'VILLAGE'),
|
|
|
+ townId: String(b.townId || ''),
|
|
|
+ villageId: String(b.villageId || ''),
|
|
|
+ town: regionName(String(b.townId || '')),
|
|
|
+ village: regionName(String(b.villageId || '')),
|
|
|
+ peopleCount: Number(b.peopleCount || 0),
|
|
|
+ totalAmount: Number(b.totalAmount || 0),
|
|
|
+ status: String(b.status || ''),
|
|
|
+ currentStage: String(b.currentStage || ''),
|
|
|
+ }))
|
|
|
+}
|
|
|
+
|
|
|
+export async function purgeBatchAdmin(batchId: string) {
|
|
|
+ await post(`/batches/${encodeURIComponent(batchId)}/purge`)
|
|
|
+ return { ok: true as const }
|
|
|
+}
|
|
|
+
|
|
|
+export async function deletePersonnelAdmin(personnelId: string) {
|
|
|
+ await del(`/personnel/${encodeURIComponent(personnelId)}`)
|
|
|
+ return { ok: true as const }
|
|
|
+}
|
|
|
+
|
|
|
+export async function updatePersonnelStatusAdmin(payload: {
|
|
|
+ personnelId: string
|
|
|
+ bizStatus: string
|
|
|
+ payThisMonth?: boolean
|
|
|
+ remarks?: string
|
|
|
+}) {
|
|
|
+ await put('/personnel/status', payload)
|
|
|
+ return { ok: true as const }
|
|
|
+}
|
|
|
+
|
|
|
+export async function fetchAccounts(): Promise<AccountItem[]> {
|
|
|
+ // 后端无账号管理接口:返回空,页面可空态展示
|
|
|
+ return []
|
|
|
+}
|
|
|
+
|
|
|
+export async function fetchOpLogs(): Promise<OpLog[]> {
|
|
|
+ const res = await post<PageResult<Record<string, unknown>>>('/process-logs/page', {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 100,
|
|
|
+ })
|
|
|
+ return (res?.list || []).map((r, idx) => ({
|
|
|
+ id: idx + 1,
|
|
|
+ time: String(r.operateTime || r.createTime || ''),
|
|
|
+ user: String(r.operator || ''),
|
|
|
+ role: String(r.operatorRole || ''),
|
|
|
+ module: String(r.bizType || r.module || '流程'),
|
|
|
+ action: String(r.action || r.opinion || ''),
|
|
|
+ result: String(r.result || '成功'),
|
|
|
+ }))
|
|
|
+}
|