"use client"; import { Plus } from "lucide-react"; import { useEffect, useMemo, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { getSettlementPeriodOpenHints, postSettlementPeriod, postSettlementPeriodClose, type SettlementPeriodCloseResult, type SettlementPeriodOpenHints, type SettlementPeriodRow, } from "@/api/admin-agent-settlement"; import { AdminDateRangeField } from "@/components/admin/admin-date-range-field"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminPageCard } from "@/components/admin/admin-page-card"; import { SettlementPeriodsTable } from "@/modules/settlement/settlement-periods-table"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { formatSettlementPeriodSpan, isSettlementLocalDateRangeValid, localDateRangeToUtcPeriodBounds, settlementRangeOverlapsOccupiedDates, utcStorageDateToLocalFormYmd, utcStorageDatesToLocalMarks, } from "@/lib/agent-settlement-period-range"; import { settlementPeriodStatusLabel } from "@/modules/settlement/settlement-status-label"; import { LotteryApiBizError } from "@/types/api/errors"; type PeriodStatusFilter = "all" | "open" | "closed" | "completed"; const STATUS_FILTER_OPTIONS: PeriodStatusFilter[] = ["all", "open", "closed", "completed"]; type SettlementPeriodWorkbenchProps = { adminSiteId: number; currencyCode: string; canManage: boolean; periods: SettlementPeriodRow[]; headerActions?: ReactNode; onViewDetail: (periodId: number) => void; onReloadPeriods: () => Promise; onPeriodOpened?: (periodId: number) => void; onPeriodClosed?: (result: SettlementPeriodCloseResult) => void; }; export function SettlementPeriodWorkbench({ adminSiteId, currencyCode, canManage, periods, headerActions, onViewDetail, onReloadPeriods, onPeriodOpened, onPeriodClosed, }: SettlementPeriodWorkbenchProps): React.ReactElement { const { t } = useTranslation(["settlementCenter", "agents", "common"]); const [draftStatus, setDraftStatus] = useState("all"); const [appliedStatus, setAppliedStatus] = useState("all"); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(10); const [openDialogOpen, setOpenDialogOpen] = useState(false); const [customStart, setCustomStart] = useState(""); const [customEnd, setCustomEnd] = useState(""); const [openHints, setOpenHints] = useState(null); const [hintsLoading, setHintsLoading] = useState(false); const [busy, setBusy] = useState(false); const [reloading, setReloading] = useState(false); const [closeDialogOpen, setCloseDialogOpen] = useState(false); const [closeTarget, setCloseTarget] = useState(null); const openPeriod = useMemo( () => periods.find((row) => row.status === "open") ?? null, [periods], ); const filteredPeriods = useMemo(() => { let list = [...periods]; if (appliedStatus !== "all") { list = list.filter((row) => row.status === appliedStatus); } return list.sort((a, b) => b.id - a.id); }, [periods, appliedStatus]); const lastPage = Math.max(1, Math.ceil(filteredPeriods.length / perPage)); const pagedPeriods = useMemo(() => { const start = (page - 1) * perPage; return filteredPeriods.slice(start, start + perPage); }, [filteredPeriods, page, perPage]); useEffect(() => { setPage(1); }, [appliedStatus, perPage]); useEffect(() => { if (page > lastPage) { setPage(lastPage); } }, [page, lastPage]); const calendarMarkers = useMemo(() => { if (openHints === null) { return undefined; } return { occupiedPeriod: utcStorageDatesToLocalMarks(openHints.occupied_period_dates), pendingActivity: utcStorageDatesToLocalMarks(openHints.pending_activity_dates), unpaidBill: utcStorageDatesToLocalMarks(openHints.unpaid_bill_dates), }; }, [openHints]); const occupiedLocalDates = useMemo( () => utcStorageDatesToLocalMarks(openHints?.occupied_period_dates ?? []), [openHints], ); const selectedRangeOverlapsOccupied = useMemo(() => { if (!customStart.trim() || !customEnd.trim()) { return false; } return settlementRangeOverlapsOccupiedDates( customStart.trim(), customEnd.trim(), occupiedLocalDates, ); }, [customEnd, customStart, occupiedLocalDates]); const statusFilterLabel = (value: PeriodStatusFilter): string => { if (value === "all") { return t("filters.statusAll", { defaultValue: "全部" }); } return settlementPeriodStatusLabel(value, t); }; async function openWithRange(startYmd: string, endYmd: string): Promise { if (!canManage) { return; } if (!isSettlementLocalDateRangeValid(startYmd, endYmd)) { toast.error( t("agents:settlementPeriods.invalidRange", { defaultValue: "结束日期不能早于开始日期", }), ); return; } if (settlementRangeOverlapsOccupiedDates(startYmd, endYmd, occupiedLocalDates)) { toast.error( t("agents:settlementPeriods.overlapsOccupied", { defaultValue: "所选范围与已有账期重叠,请避开灰色删除线的日期。", }), ); return; } const bounds = localDateRangeToUtcPeriodBounds(startYmd, endYmd); setBusy(true); try { const row = await postSettlementPeriod({ admin_site_id: adminSiteId, period_start: bounds.period_start, period_end: bounds.period_end, }); await onReloadPeriods(); onPeriodOpened?.(row.id); setOpenDialogOpen(false); setCustomStart(""); setCustomEnd(""); setOpenHints(null); toast.success(t("agents:settlementPeriods.opened", { defaultValue: "账期已开启" })); } catch (err: unknown) { toast.error( err instanceof LotteryApiBizError ? err.message : t("agents:settlementPeriods.openFailed", { defaultValue: "开账失败" }), ); } finally { setBusy(false); } } async function openCustom(): Promise { if (!customStart.trim() || !customEnd.trim()) { toast.error(t("agents:settlementPeriods.datesRequired", { defaultValue: "请填写账期起止" })); return; } await openWithRange(customStart.trim(), customEnd.trim()); } async function loadOpenHints(): Promise { setHintsLoading(true); try { const hints = await getSettlementPeriodOpenHints({ admin_site_id: adminSiteId }); setOpenHints(hints); setCustomStart(""); setCustomEnd(""); if (hints.suggested_start && hints.suggested_end) { const from = utcStorageDateToLocalFormYmd(hints.suggested_start); const to = utcStorageDateToLocalFormYmd(hints.suggested_end); const occupied = utcStorageDatesToLocalMarks(hints.occupied_period_dates); if (!settlementRangeOverlapsOccupiedDates(from, to, occupied)) { setCustomStart(from); setCustomEnd(to); } } } catch (err: unknown) { setOpenHints(null); toast.error( err instanceof LotteryApiBizError ? err.message : t("agents:settlementPeriods.hintsFailed", { defaultValue: "无法加载开账建议" }), ); } finally { setHintsLoading(false); } } function handleOpenDialog(open: boolean): void { setOpenDialogOpen(open); if (open) { void loadOpenHints(); return; } setCustomStart(""); setCustomEnd(""); setOpenHints(null); } function requestClose(row: SettlementPeriodRow): void { setCloseTarget(row); setCloseDialogOpen(true); } async function confirmClose(): Promise { if (!closeTarget) { return; } setBusy(true); try { const result = await postSettlementPeriodClose(closeTarget.id); const items = await onReloadPeriods(); setCloseDialogOpen(false); setCloseTarget(null); onPeriodClosed?.(result); const stillThere = items.find((row) => row.id === closeTarget.id); if (stillThere?.status === "closed") { toast.success(t("agents:settlementPeriods.closed", { defaultValue: "账期已关账,账单已生成" })); } } catch (err: unknown) { toast.error( err instanceof LotteryApiBizError ? err.message : t("agents:settlementPeriods.closeFailed", { defaultValue: "关账失败" }), ); } finally { setBusy(false); } } async function handleRefresh(): Promise { setReloading(true); try { await onReloadPeriods(); } finally { setReloading(false); } } function applyFilters(): void { setAppliedStatus(draftStatus); setPage(1); } function resetFilters(): void { setDraftStatus("all"); setAppliedStatus("all"); setPage(1); } const shareCount = closeTarget?.pipeline?.share_ledger_count ?? 0; const unsettledCount = closeTarget?.pipeline?.unsettled_ticket_count ?? 0; const cardDescription = canManage ? t("subtitle", { defaultValue: "账期关账、账单确认与收付登记" }) : t("periodTable.readOnlyHint", { defaultValue: "绑定代理账号不可开/关账期,仅可查看与收付。", }); const openPeriodHiddenByFilter = openPeriod !== null && appliedStatus !== "all" && appliedStatus !== "open" && !filteredPeriods.some((row) => row.id === openPeriod.id); const tableEmptyMessage = useMemo(() => { if (periods.length === 0) { if (!canManage) { return t("periodTable.emptyReadOnly", { defaultValue: "暂无账期记录。" }); } return t("periodTable.emptyOpenHint", { defaultValue: "暂无账期,请点击工具栏「开账」创建。", }); } if (openPeriodHiddenByFilter) { return t("periodTable.emptyFilteredOpen", { defaultValue: "当前筛选未包含进行中的账期,请选「全部」或「进行中」。", }); } return t("periodTable.emptyFiltered", { defaultValue: "筛选结果为空,请重置筛选。" }); }, [canManage, openPeriodHiddenByFilter, periods.length, t]); return ( <>
{canManage && openPeriod ? ( ) : null} {canManage && !openPeriod ? ( ) : null}
{canManage && openPeriod ? (

{t("periodTable.hasOpen", { defaultValue: "已有进行中账期 {{range}},须先关账才能开新期。", range: formatSettlementPeriodSpan(openPeriod.period_start, openPeriod.period_end), })}

) : null} { setPerPage(next); setPage(1); }} onPageChange={setPage} />
{t("period.openTitle", { defaultValue: "开账" })} {t("agents:settlementPeriods.openDesc", { defaultValue: "选择账期起止日期;灰色删除线为已有账期不可再开,琥珀点为待入账,右上角红点为未结清。", })}
{ setCustomStart(from); setCustomEnd(to); }} />
{t("agents:settlementPeriods.markerOccupied", { defaultValue: "已有账期" })} {t("agents:settlementPeriods.markerPending", { defaultValue: "待入账流水" })} {t("agents:settlementPeriods.markerUnpaid", { defaultValue: "未结清账期" })}
{selectedRangeOverlapsOccupied ? (

{t("agents:settlementPeriods.overlapsOccupied", { defaultValue: "所选范围与已有账期重叠,请避开灰色删除线的日期。", })}

) : null}
{ setCloseDialogOpen(open); if (!open) { setCloseTarget(null); } }} > {t("period.closeDialogTitle", { defaultValue: "确认关账" })} {closeTarget ? t("period.closeDialogDesc", { defaultValue: "将汇总 {{range}} 内的流水并生成账单。", range: formatSettlementPeriodSpan( closeTarget.period_start, closeTarget.period_end, ), }) : null} {closeTarget ? (
  • {shareCount > 0 ? t("period.closeDialogShare", { defaultValue: "流水 {{count}} 笔", count: shareCount, }) : t("period.closeDialogEmpty", { defaultValue: "本期暂无占成流水,关账后不会生成账单。", })}
  • {unsettledCount > 0 ? (
  • {t("period.closeDialogUnsettled", { defaultValue: "仍有 {{count}} 笔注单未结算", count: unsettledCount, })}
  • ) : null}
  • {t("period.closeDialogIrreversible", { defaultValue: "关账后不可撤销,差错请通过调账或冲正处理。", })}
) : null}
); }