Files
lotteryAdmin/src/modules/risk/risk-lock-logs-console.tsx
kang 4080f0b601 feat(ui): enhance table and admin components with improved layout and status display
- Updated global CSS to center-align table headers and cells, ensuring a consistent layout.
- Modified admin table components to replace switches with status badges for better clarity.
- Enhanced internationalization support by adding new strings for version actions and validation messages in multiple locales.
- Refactored configuration document screens to include version selection and improved user feedback on status changes.
2026-05-26 11:13:16 +08:00

222 lines
8.2 KiB
TypeScript

"use client";
import { useCallback, useEffect, useState } from "react";
import { useExportLabels } from "@/hooks/use-export-labels";
import { useTranslation } from "react-i18next";
import { getAdminRiskPoolLockLogs } from "@/api/admin-risk";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { riskActionTypeLabel, riskSourceReasonLabel } from "@/modules/risk/risk-display";
import { formatAdminMinorUnits } from "@/lib/money";
import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminRiskLockLogListData, AdminRiskLockLogRow } from "@/types/api/admin-risk";
const ACTION_ALL = "__all__";
function riskActionFilterLabel(
value: string,
t: (key: string) => string,
): string {
if (value === ACTION_ALL) {
return t("noLimit");
}
return riskActionTypeLabel(value, t);
}
export function RiskLockLogsConsole({ drawId }: { drawId: number }) {
const { t } = useTranslation(["risk", "common"]);
const exportLabels = useExportLabels("riskLockLogs");
useAdminCurrencyCatalog();
const playCodeLabel = useAdminPlayCodeLabel();
const formatDt = useAdminDateTimeFormatter();
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const [data, setData] = useState<AdminRiskLockLogListData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [draftNumber, setDraftNumber] = useState("");
const [appliedNumber, setAppliedNumber] = useState("");
const [draftAction, setDraftAction] = useState<string>(ACTION_ALL);
const [appliedAction, setAppliedAction] = useState<string>(ACTION_ALL);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
const d = await getAdminRiskPoolLockLogs(drawId, {
page,
per_page: perPage,
normalized_number: appliedNumber.trim() === "" ? undefined : appliedNumber.trim(),
action_type:
appliedAction === ACTION_ALL
? undefined
: (appliedAction as "lock" | "release"),
});
setData(d);
} catch (e) {
const msg =
e instanceof LotteryApiBizError ? e.message : t("loadLogsFailed");
setError(msg);
setData(null);
} finally {
setLoading(false);
}
}, [drawId, page, perPage, appliedAction, appliedNumber, t]);
useEffect(() => {
queueMicrotask(() => {
void load();
});
}, [load]);
return (
<Card className="admin-list-card">
<CardHeader className="admin-list-header">
<CardTitle className="admin-list-title">{t("lockLogsTitle")}</CardTitle>
</CardHeader>
<CardContent className="admin-list-content">
<div className="admin-list-toolbar">
<div className="admin-list-field">
<Label htmlFor="risk-log-number" className="sm:w-20 sm:shrink-0">
{t("number4d")}
</Label>
<Input
id="risk-log-number"
inputMode="numeric"
maxLength={4}
value={draftNumber}
className="w-full sm:w-32"
onChange={(e) => setDraftNumber(e.target.value.replace(/\D/g, "").slice(0, 4))}
placeholder={t("optional")}
/>
</div>
<div className="admin-list-field">
<Label htmlFor="risk-log-action" className="sm:w-20 sm:shrink-0">
{t("actionFilter")}
</Label>
<Select
modal={false}
value={draftAction}
onValueChange={(v) => {
if (v) setDraftAction(v);
}}
>
<SelectTrigger id="risk-log-action" size="sm" className="w-full sm:w-40">
<SelectValue>{riskActionFilterLabel(draftAction, t)}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value={ACTION_ALL}>{t("noLimit")}</SelectItem>
<SelectItem value="lock">{t("lock")}</SelectItem>
<SelectItem value="release">{t("release")}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="admin-list-actions">
<AdminTableExportButton
tableId={`risk-lock-logs-table-${drawId}`}
filename={exportLabels.filename}
sheetName={exportLabels.sheetName}
/>
<Button
type="button"
size="sm"
onClick={() => {
setAppliedNumber(draftNumber);
setAppliedAction(draftAction);
setPage(1);
}}
>
{t("applyFilter")}
</Button>
</div>
</div>
{error ? <p className="text-sm text-destructive">{error}</p> : null}
{loading && !data ? (
<p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>
) : (
<>
<div className="admin-table-shell">
<Table id={`risk-lock-logs-table-${drawId}`}>
<TableHeader>
<TableRow>
<TableHead>{t("time")}</TableHead>
<TableHead>{t("searchNumber")}</TableHead>
<TableHead>{t("action")}</TableHead>
<TableHead className="text-center">{t("amount")}</TableHead>
<TableHead>{t("source")}</TableHead>
<TableHead>{t("ticketNo")}</TableHead>
<TableHead>{t("playCode")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{(data?.items ?? []).map((row: AdminRiskLockLogRow) => (
<TableRow key={row.id}>
<TableCell className="whitespace-nowrap text-xs text-muted-foreground">
{row.created_at ? formatDt(row.created_at) : "—"}
</TableCell>
<TableCell className="font-mono text-sm">{row.normalized_number}</TableCell>
<TableCell className="text-sm">
{riskActionTypeLabel(row.action_type, t)}
</TableCell>
<TableCell className="text-center text-sm tabular-nums">
{formatAdminMinorUnits(row.amount, data?.currency_code ?? "NPR")}
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{riskSourceReasonLabel(row.source_reason, t)}
</TableCell>
<TableCell className="font-mono text-xs">{row.ticket_no ?? "—"}</TableCell>
<TableCell className="text-xs">{playCodeLabel(row.play_code)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{data ? (
<AdminListPaginationFooter
selectId={`risk-logs-${drawId}`}
total={data.meta.total}
page={data.meta.current_page}
lastPage={data.meta.last_page}
perPage={data.meta.per_page}
loading={loading}
onPerPageChange={(n) => {
setPerPage(n);
setPage(1);
}}
onPageChange={setPage}
/>
) : null}
</>
)}
</CardContent>
</Card>
);
}