feat(settlement, admin): introduce new types and functions for downline share and settlement period hints
Added new types for downline share breakdown and settlement period open hints to enhance the agent settlement API. Updated the admin console components to support these new features, improving the user experience with better data presentation and interaction. Additionally, refined the date range field to accommodate new calendar markers and hints, ensuring a more intuitive interface for managing settlement periods.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -8,10 +9,12 @@ import { toast } from "sonner";
|
||||
|
||||
import { getSettlementPeriods, type SettlementPeriodRow } from "@/api/admin-agent-settlement";
|
||||
import { getAdminIntegrationSites } from "@/api/admin-integration-sites";
|
||||
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||
import { AgentBillDetail } from "@/modules/settlement/agent-bill-detail";
|
||||
import { SettlementCenterPeriodDetail } from "@/modules/settlement/settlement-center-period-detail";
|
||||
import {
|
||||
parseSettlementCenterView,
|
||||
settlementCenterListHref,
|
||||
settlementPeriodViewHref,
|
||||
type SettlementPeriodView,
|
||||
} from "@/modules/settlement/settlement-center-nav";
|
||||
@@ -25,7 +28,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
@@ -41,7 +44,8 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
const profile = useAdminProfile();
|
||||
const boundAgent = profile?.agent ?? null;
|
||||
|
||||
const { periodId: activePeriodId, view: activeView } = parseSettlementCenterView(
|
||||
const { siteId: siteFromUrl, periodId: activePeriodId, view: activeView } = parseSettlementCenterView(
|
||||
searchParams.get("site"),
|
||||
searchParams.get("period"),
|
||||
searchParams.get("view"),
|
||||
);
|
||||
@@ -50,6 +54,7 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
profile?.is_super_admin === true ||
|
||||
adminHasAnyPermission(profile?.permissions, [PRD_SETTLEMENT_AGENT_MANAGE]);
|
||||
const canManagePeriods = canOperateBills && boundAgent === null;
|
||||
const canFinanceAdjustments = canOperateBills && boundAgent === null;
|
||||
|
||||
const [siteOptions, setSiteOptions] = useState<SiteOption[]>([]);
|
||||
const [adminSiteId, setAdminSiteId] = useState<number | null>(null);
|
||||
@@ -59,10 +64,14 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
const [periodsReady, setPeriodsReady] = useState(false);
|
||||
const [detailBillId, setDetailBillId] = useState<number | null>(null);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const [periodLookupDone, setPeriodLookupDone] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (boundAgent?.admin_site_id) {
|
||||
const label = formatAdminSiteLabel(boundAgent.name, boundAgent.site_code ?? boundAgent.code);
|
||||
const label = formatAdminSiteLabel(
|
||||
boundAgent.admin_site_name,
|
||||
boundAgent.site_code ?? boundAgent.code,
|
||||
);
|
||||
setSiteOptions([{
|
||||
id: boundAgent.admin_site_id,
|
||||
label,
|
||||
@@ -81,11 +90,17 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
currency_code: site.currency_code ?? "NPR",
|
||||
}));
|
||||
setSiteOptions(options);
|
||||
if (adminSiteId === null && options[0]) {
|
||||
setAdminSiteId(options[0].id);
|
||||
}
|
||||
setAdminSiteId((current) => {
|
||||
if (siteFromUrl !== null && options.some((site) => site.id === siteFromUrl)) {
|
||||
return siteFromUrl;
|
||||
}
|
||||
if (current !== null && options.some((site) => site.id === current)) {
|
||||
return current;
|
||||
}
|
||||
return options[0]?.id ?? null;
|
||||
});
|
||||
});
|
||||
}, [adminSiteId, boundAgent]);
|
||||
}, [boundAgent, siteFromUrl]);
|
||||
|
||||
const siteId = adminSiteId ?? siteOptions[0]?.id ?? null;
|
||||
const selectedSite = siteOptions.find((s) => s.id === siteId) ?? null;
|
||||
@@ -95,6 +110,17 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
? siteOptions.filter((site) => site.label.toLowerCase().includes(siteKeyword.trim().toLowerCase()))
|
||||
: siteOptions;
|
||||
|
||||
const boundAgentIdentity =
|
||||
boundAgent !== null ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("boundAgentIdentity", {
|
||||
defaultValue: "经营身份:{{agent}} · 账号 {{username}}",
|
||||
agent: boundAgent.name || boundAgent.code,
|
||||
username: profile?.username ?? "—",
|
||||
})}
|
||||
</p>
|
||||
) : null;
|
||||
|
||||
const siteSelector =
|
||||
siteOptions.length > 0 && siteId !== null ? (
|
||||
<Popover open={sitePickerOpen} onOpenChange={setSitePickerOpen}>
|
||||
@@ -143,6 +169,7 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
setAdminSiteId(site.id);
|
||||
setSitePickerOpen(false);
|
||||
setSiteKeyword("");
|
||||
router.replace(settlementCenterListHref(site.id));
|
||||
}}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -164,6 +191,14 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
</Popover>
|
||||
) : null;
|
||||
|
||||
const headerActions =
|
||||
siteSelector !== null || boundAgentIdentity !== null ? (
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
{siteSelector}
|
||||
{boundAgentIdentity}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const loadPeriods = useCallback(async (): Promise<SettlementPeriodRow[]> => {
|
||||
if (siteId === null) {
|
||||
return [];
|
||||
@@ -191,22 +226,75 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
activePeriodId !== null ? (periods.find((row) => row.id === activePeriodId) ?? null) : null;
|
||||
|
||||
const openPeriodView = (periodId: number, view: SettlementPeriodView): void => {
|
||||
router.push(settlementPeriodViewHref(periodId, view));
|
||||
router.push(settlementPeriodViewHref(periodId, view, siteId));
|
||||
};
|
||||
|
||||
const isListMode = activePeriodId === null;
|
||||
|
||||
useEffect(() => {
|
||||
if (boundAgent !== null || siteId === null) {
|
||||
return;
|
||||
}
|
||||
if (isListMode) {
|
||||
if (siteFromUrl === siteId) {
|
||||
return;
|
||||
}
|
||||
router.replace(settlementCenterListHref(siteId));
|
||||
return;
|
||||
}
|
||||
if (activePeriodId !== null && siteFromUrl !== siteId) {
|
||||
router.replace(settlementPeriodViewHref(activePeriodId, activeView, siteId));
|
||||
}
|
||||
}, [activePeriodId, activeView, boundAgent, isListMode, router, siteFromUrl, siteId]);
|
||||
|
||||
useEffect(() => {
|
||||
setPeriodLookupDone(false);
|
||||
}, [activePeriodId, siteId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!periodsReady || activePeriodId === null || siteId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activePeriod !== null) {
|
||||
setPeriodLookupDone(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
void getSettlementPeriods().then((data) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = (data.items ?? []).find((row) => row.id === activePeriodId);
|
||||
if (match?.admin_site_id && match.admin_site_id !== siteId) {
|
||||
setAdminSiteId(match.admin_site_id);
|
||||
router.replace(settlementPeriodViewHref(activePeriodId, activeView, match.admin_site_id));
|
||||
return;
|
||||
}
|
||||
|
||||
setPeriodLookupDone(true);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [activePeriod, activePeriodId, activeView, periodsReady, router, siteId]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4">
|
||||
{siteId === null || !periodsReady ? (
|
||||
{siteId === null ? (
|
||||
<p className="text-sm text-muted-foreground">{t("empty.noSite", { defaultValue: "请选择站点。" })}</p>
|
||||
) : !periodsReady ? (
|
||||
<AdminLoadingState />
|
||||
) : isListMode ? (
|
||||
<SettlementPeriodWorkbench
|
||||
adminSiteId={siteId}
|
||||
currencyCode={currency}
|
||||
canManage={canManagePeriods}
|
||||
periods={periods}
|
||||
headerActions={siteSelector}
|
||||
headerActions={headerActions}
|
||||
onViewDetail={(id) => openPeriodView(id, "bills")}
|
||||
onReloadPeriods={loadPeriods}
|
||||
onPeriodOpened={() => {
|
||||
@@ -226,9 +314,21 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
}}
|
||||
/>
|
||||
) : activePeriod === null ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("periodDetail.notFound", { defaultValue: "账期不存在或已切换站点,请返回列表。" })}
|
||||
</p>
|
||||
!periodLookupDone ? (
|
||||
<AdminLoadingState />
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("periodDetail.notFound", { defaultValue: "账期不存在或已切换站点,请返回列表。" })}
|
||||
</p>
|
||||
<Link
|
||||
href={settlementCenterListHref(siteId)}
|
||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "w-fit")}
|
||||
>
|
||||
{t("periodDetail.back", { defaultValue: "返回账期列表" })}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<SettlementCenterPeriodDetail
|
||||
period={activePeriod}
|
||||
@@ -236,6 +336,7 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
adminSiteId={siteId}
|
||||
currencyCode={currency}
|
||||
canOperateBills={canOperateBills}
|
||||
boundAgentId={boundAgent?.id ?? null}
|
||||
refreshKey={refreshKey}
|
||||
onOpenBillDetail={setDetailBillId}
|
||||
/>
|
||||
@@ -243,7 +344,7 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
|
||||
<Dialog open={detailBillId !== null} onOpenChange={(open) => !open && setDetailBillId(null)}>
|
||||
<DialogContent
|
||||
className="grid !h-[min(92vh,980px)] !w-[calc(100vw-2rem)] !max-w-none sm:!w-[min(860px,calc(100vw-2rem))] sm:!max-w-[860px] grid-rows-[auto,minmax(0,1fr)] overflow-hidden p-0"
|
||||
className="grid !h-[min(92vh,980px)] !w-[calc(100vw-2rem)] !max-w-none sm:!w-[min(640px,calc(100vw-2rem))] sm:!max-w-[640px] grid-rows-[auto,minmax(0,1fr)] overflow-hidden p-0"
|
||||
>
|
||||
<DialogHeader className="border-b px-6 py-4">
|
||||
<DialogTitle>{t("actions.billDetail", { defaultValue: "账单详情" })}</DialogTitle>
|
||||
@@ -254,6 +355,8 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
billId={detailBillId}
|
||||
currencyCode={currency}
|
||||
canManage={canOperateBills}
|
||||
boundAgent={boundAgent}
|
||||
canFinanceAdjustments={canFinanceAdjustments}
|
||||
onUpdated={() => {
|
||||
void loadPeriods();
|
||||
setRefreshKey((n) => n + 1);
|
||||
|
||||
Reference in New Issue
Block a user