refactor: update agent API schemas, standardize UI text styling, and enhance settlement credit ledger components
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -24,16 +25,14 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Button } 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";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
|
||||
type SiteOption = { id: number; label: string; currency_code: string };
|
||||
type SiteOption = { id: number; label: string; code: string; currency_code: string };
|
||||
|
||||
export function SettlementCenterShell(): React.ReactElement {
|
||||
const { t } = useTranslation(["settlementCenter", "common"]);
|
||||
@@ -54,6 +53,8 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
|
||||
const [siteOptions, setSiteOptions] = useState<SiteOption[]>([]);
|
||||
const [adminSiteId, setAdminSiteId] = useState<number | null>(null);
|
||||
const [sitePickerOpen, setSitePickerOpen] = useState(false);
|
||||
const [siteKeyword, setSiteKeyword] = useState("");
|
||||
const [periods, setPeriods] = useState<SettlementPeriodRow[]>([]);
|
||||
const [periodsReady, setPeriodsReady] = useState(false);
|
||||
const [detailBillId, setDetailBillId] = useState<number | null>(null);
|
||||
@@ -62,7 +63,12 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
useEffect(() => {
|
||||
if (boundAgent?.admin_site_id) {
|
||||
const label = formatAdminSiteLabel(boundAgent.name, boundAgent.site_code ?? boundAgent.code);
|
||||
setSiteOptions([{ id: boundAgent.admin_site_id, label, currency_code: "NPR" }]);
|
||||
setSiteOptions([{
|
||||
id: boundAgent.admin_site_id,
|
||||
label,
|
||||
code: boundAgent.site_code ?? boundAgent.code ?? "",
|
||||
currency_code: "NPR",
|
||||
}]);
|
||||
setAdminSiteId(boundAgent.admin_site_id);
|
||||
return;
|
||||
}
|
||||
@@ -71,6 +77,7 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
const options = (sites.items ?? []).map((site) => ({
|
||||
id: site.id,
|
||||
label: formatAdminSiteLabel(site.name, site.code),
|
||||
code: site.code,
|
||||
currency_code: site.currency_code ?? "NPR",
|
||||
}));
|
||||
setSiteOptions(options);
|
||||
@@ -81,8 +88,81 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
}, [adminSiteId, boundAgent]);
|
||||
|
||||
const siteId = adminSiteId ?? siteOptions[0]?.id ?? null;
|
||||
const siteLabel = siteOptions.find((s) => s.id === siteId)?.label ?? null;
|
||||
const selectedSite = siteOptions.find((s) => s.id === siteId) ?? null;
|
||||
const siteLabel = selectedSite?.label ?? null;
|
||||
const currency = siteOptions.find((s) => s.id === siteId)?.currency_code ?? "NPR";
|
||||
const filteredSites = siteKeyword.trim().toLowerCase()
|
||||
? siteOptions.filter((site) => site.label.toLowerCase().includes(siteKeyword.trim().toLowerCase()))
|
||||
: siteOptions;
|
||||
|
||||
const siteSelector =
|
||||
siteOptions.length > 0 && siteId !== null ? (
|
||||
<Popover open={sitePickerOpen} onOpenChange={setSitePickerOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 w-[240px] justify-between gap-2 bg-background px-3 font-normal"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate text-left">{siteLabel ?? "—"}</span>
|
||||
<span className="shrink-0 text-xs text-muted-foreground">
|
||||
{selectedSite?.code ?? ""}
|
||||
</span>
|
||||
<ChevronDown className="size-4 shrink-0 text-muted-foreground" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-[320px] p-0">
|
||||
<div className="border-b border-border/60 p-3">
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
value={siteKeyword}
|
||||
onChange={(event) => setSiteKeyword(event.target.value)}
|
||||
placeholder={t("siteSearch", { defaultValue: "搜索站点名称" })}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="max-h-72">
|
||||
<div className="p-2">
|
||||
{filteredSites.map((site) => {
|
||||
const active = site.id === siteId;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={site.id}
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex w-full items-center gap-3 rounded-md px-3 py-2 text-left transition-colors",
|
||||
active ? "bg-primary/10 text-primary" : "hover:bg-muted/70",
|
||||
)}
|
||||
onClick={() => {
|
||||
setAdminSiteId(site.id);
|
||||
setSitePickerOpen(false);
|
||||
setSiteKeyword("");
|
||||
}}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium text-foreground">{site.label}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">{site.code}</div>
|
||||
</div>
|
||||
{active ? <Check className="size-4 shrink-0" /> : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{filteredSites.length === 0 ? (
|
||||
<div className="px-3 py-6 text-center text-sm text-muted-foreground">
|
||||
{t("common:states.empty", { defaultValue: "暂无数据" })}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : null;
|
||||
|
||||
const loadPeriods = useCallback(async (): Promise<SettlementPeriodRow[]> => {
|
||||
if (siteId === null) {
|
||||
@@ -118,41 +198,6 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold tracking-tight">
|
||||
{t("title", { defaultValue: "结算中心" })}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{isListMode
|
||||
? t("subtitleList", { defaultValue: "账期列表:开账、关账,从行操作进入账单与报表。" })
|
||||
: t("subtitle", { defaultValue: "账期关账、账单确认与收付登记" })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{siteOptions.length >= 1 && siteId !== null ? (
|
||||
<Select
|
||||
value={String(siteId)}
|
||||
onValueChange={(v) => {
|
||||
setAdminSiteId(Number(v));
|
||||
setPeriodsReady(false);
|
||||
router.push("/admin/settlement-center");
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-9 w-[220px]">
|
||||
<SelectValue>{siteLabel}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{siteOptions.map((site) => (
|
||||
<SelectItem key={site.id} value={String(site.id)}>
|
||||
{site.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{siteId === null || !periodsReady ? (
|
||||
<p className="text-sm text-muted-foreground">{t("empty.noSite", { defaultValue: "请选择站点。" })}</p>
|
||||
) : isListMode ? (
|
||||
@@ -161,6 +206,7 @@ export function SettlementCenterShell(): React.ReactElement {
|
||||
currencyCode={currency}
|
||||
canManage={canManagePeriods}
|
||||
periods={periods}
|
||||
headerActions={siteSelector}
|
||||
onViewDetail={(id) => openPeriodView(id, "bills")}
|
||||
onReloadPeriods={loadPeriods}
|
||||
onPeriodOpened={() => {
|
||||
|
||||
Reference in New Issue
Block a user