feat(admin, i18n): enhance reports, draws, config, and player workflows
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { CalendarRange, Eye, ShieldAlert, UserRound } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -18,7 +19,7 @@ import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admi
|
||||
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
||||
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
|
||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -71,6 +72,18 @@ function itemStatusLabel(status: string, t: (key: string) => string): string {
|
||||
return t("itemMatched");
|
||||
case "pending_check":
|
||||
return t("itemPendingCheck");
|
||||
case "stale_processing":
|
||||
return t("itemStaleProcessing");
|
||||
case "pending_reconcile":
|
||||
return t("itemPendingReconcile");
|
||||
case "missing_wallet_txn":
|
||||
return t("itemMissingWalletTxn");
|
||||
case "unexpected_wallet_txn":
|
||||
return t("itemUnexpectedWalletTxn");
|
||||
case "missing_refund":
|
||||
return t("itemMissingRefund");
|
||||
case "missing_reversal":
|
||||
return t("itemMissingReversal");
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
@@ -85,6 +98,82 @@ function reconcileTypeLabel(type: string, t: (key: string) => string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function itemResolutionLabel(
|
||||
row: Pick<AdminReconcileItemsData["items"][number], "resolved_at" | "is_resolved">,
|
||||
t: (key: string) => string,
|
||||
): string {
|
||||
return row.is_resolved === true || row.resolved_at ? t("itemResolved") : t("itemUnresolved");
|
||||
}
|
||||
|
||||
function itemResolutionTone(row: Pick<AdminReconcileItemsData["items"][number], "resolved_at" | "is_resolved">): "success" | "warning" {
|
||||
return row.is_resolved === true || row.resolved_at ? "success" : "warning";
|
||||
}
|
||||
|
||||
function itemDiagnosisLabel(status: string, t: (key: string) => string): string {
|
||||
switch (status) {
|
||||
case "stale_processing":
|
||||
return t("diagnosisStaleProcessing");
|
||||
case "pending_reconcile":
|
||||
return t("diagnosisPendingReconcile");
|
||||
case "missing_wallet_txn":
|
||||
return t("diagnosisMissingWalletTxn");
|
||||
case "unexpected_wallet_txn":
|
||||
return t("diagnosisUnexpectedWalletTxn");
|
||||
case "missing_refund":
|
||||
return t("diagnosisMissingRefund");
|
||||
case "missing_reversal":
|
||||
return t("diagnosisMissingReversal");
|
||||
case "matched":
|
||||
return t("diagnosisMatched");
|
||||
default:
|
||||
return t("diagnosisPendingCheck");
|
||||
}
|
||||
}
|
||||
|
||||
function itemSuggestedAction(
|
||||
row: Pick<AdminReconcileItemsData["items"][number], "status" | "resolved_at" | "is_resolved" | "current_transfer_status">,
|
||||
t: (key: string, opts?: Record<string, unknown>) => string,
|
||||
): string {
|
||||
if (row.is_resolved === true || row.resolved_at) {
|
||||
return t("actionResolved", {
|
||||
status: row.current_transfer_status ? itemTransferStatusLabel(row.current_transfer_status, t) : t("statusCompleted"),
|
||||
});
|
||||
}
|
||||
|
||||
const status = row.status;
|
||||
switch (status) {
|
||||
case "stale_processing":
|
||||
return t("actionStaleProcessing");
|
||||
case "pending_reconcile":
|
||||
return t("actionPendingReconcile");
|
||||
case "missing_wallet_txn":
|
||||
return t("actionMissingWalletTxn");
|
||||
case "unexpected_wallet_txn":
|
||||
return t("actionUnexpectedWalletTxn");
|
||||
case "missing_refund":
|
||||
return t("actionMissingRefund");
|
||||
case "missing_reversal":
|
||||
return t("actionMissingReversal");
|
||||
case "matched":
|
||||
return t("actionMatched");
|
||||
default:
|
||||
return t("actionPendingCheck");
|
||||
}
|
||||
}
|
||||
|
||||
function itemTransferStatusLabel(status: string, t: (key: string) => string): string {
|
||||
switch (status) {
|
||||
case "success":
|
||||
return t("transferStatusSuccess");
|
||||
case "reversed":
|
||||
return t("transferStatusReversed");
|
||||
case "manually_processed":
|
||||
return t("transferStatusManual");
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
function getJobSummaryValue(summary: Record<string, unknown> | null | undefined, key: string): number {
|
||||
const raw = summary?.[key];
|
||||
return typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
|
||||
@@ -507,8 +596,8 @@ export function ReconcileConsole(): React.ReactElement {
|
||||
<AdminRowActionsMenu
|
||||
actions={[
|
||||
{
|
||||
key: "view",
|
||||
label: t("view"),
|
||||
key: "view-details",
|
||||
label: t("viewDetails"),
|
||||
icon: Eye,
|
||||
onClick: () => {
|
||||
setSelectedId(row.id);
|
||||
@@ -609,16 +698,20 @@ export function ReconcileConsole(): React.ReactElement {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-20">{t("table.id", { ns: "common" })}</TableHead>
|
||||
<TableHead>{t("sideARef")}</TableHead>
|
||||
<TableHead>{t("sideBRef")}</TableHead>
|
||||
<TableHead className="text-right">{t("differenceAmount")}</TableHead>
|
||||
<TableHead>{t("status")}</TableHead>
|
||||
<TableHead>{t("detectedAt")}</TableHead>
|
||||
<TableHead className="min-w-[10rem]">{t("sideARef")}</TableHead>
|
||||
<TableHead className="min-w-[10rem]">{t("sideBRef")}</TableHead>
|
||||
<TableHead className="w-28 text-right">{t("differenceAmount")}</TableHead>
|
||||
<TableHead className="w-32">{t("itemResult")}</TableHead>
|
||||
<TableHead className="min-w-[16rem] whitespace-normal leading-snug">{t("diagnosis")}</TableHead>
|
||||
<TableHead className="min-w-[16rem] whitespace-normal leading-snug">{t("suggestedAction")}</TableHead>
|
||||
<TableHead className="w-28">{t("processingStatus")}</TableHead>
|
||||
<TableHead className="w-32">{t("quickAccess")}</TableHead>
|
||||
<TableHead className="w-36">{t("detectedAt")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.items.length === 0 ? (
|
||||
<AdminTableNoResourceRow colSpan={6} />
|
||||
<AdminTableNoResourceRow colSpan={10} />
|
||||
) : (
|
||||
items.items.map((r) => (
|
||||
<TableRow
|
||||
@@ -628,10 +721,10 @@ export function ReconcileConsole(): React.ReactElement {
|
||||
r.status === "matched" && "bg-emerald-500/5",
|
||||
)}
|
||||
>
|
||||
<TableCell>{r.id}</TableCell>
|
||||
<TableCell className="font-mono text-xs">{r.side_a_ref ?? "—"}</TableCell>
|
||||
<TableCell className="font-mono text-xs">{r.side_b_ref ?? "—"}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
<TableCell className="align-top">{r.id}</TableCell>
|
||||
<TableCell className="align-top font-mono text-xs break-all">{r.side_a_ref ?? "—"}</TableCell>
|
||||
<TableCell className="align-top font-mono text-xs break-all">{r.side_b_ref ?? "—"}</TableCell>
|
||||
<TableCell className="align-top text-right tabular-nums">
|
||||
<span
|
||||
className={cn(
|
||||
r.difference_amount !== 0 ? "font-medium text-amber-700" : "text-muted-foreground",
|
||||
@@ -640,12 +733,46 @@ export function ReconcileConsole(): React.ReactElement {
|
||||
{r.difference_amount}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCell className="align-top">
|
||||
<AdminStatusBadge status={r.status}>
|
||||
{itemStatusLabel(r.status, t)}
|
||||
</AdminStatusBadge>
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap font-mono text-[11px] text-muted-foreground">
|
||||
<TableCell className="align-top min-w-[16rem] max-w-[18rem] whitespace-normal break-words text-xs leading-6 text-muted-foreground">
|
||||
{itemDiagnosisLabel(r.status, t)}
|
||||
</TableCell>
|
||||
<TableCell className="align-top min-w-[16rem] max-w-[18rem] whitespace-normal break-words text-xs leading-6">
|
||||
{itemSuggestedAction(r, t)}
|
||||
</TableCell>
|
||||
<TableCell className="align-top">
|
||||
<AdminStatusBadge status={r.resolved_at ? "resolved" : "unresolved"} tone={itemResolutionTone(r)}>
|
||||
{itemResolutionLabel(r, t)}
|
||||
</AdminStatusBadge>
|
||||
</TableCell>
|
||||
<TableCell className="align-top min-w-[10rem]">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{r.side_a_ref ? (
|
||||
<Link
|
||||
href={`/admin/wallet/transfer-orders?transfer_no=${encodeURIComponent(r.side_a_ref)}`}
|
||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-8")}
|
||||
>
|
||||
{t("openTransferOrder")}
|
||||
</Link>
|
||||
) : null}
|
||||
{r.side_b_ref ? (
|
||||
<Link
|
||||
href={`/admin/wallet/transactions?txn_no=${encodeURIComponent(r.side_b_ref)}`}
|
||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-8")}
|
||||
>
|
||||
{t("openWalletTxn")}
|
||||
</Link>
|
||||
) : null}
|
||||
{!r.side_a_ref && !r.side_b_ref ? (
|
||||
<span className="text-xs text-muted-foreground">—</span>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="align-top whitespace-nowrap font-mono text-[11px] text-muted-foreground">
|
||||
{formatTs(r.created_at)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
Reference in New Issue
Block a user