feat(api, agents): add agent node profile retrieval and update functionality
Implemented new API functions to fetch and update agent node profiles, enhancing the management capabilities for agent data. This addition improves the overall functionality of the admin agents console, allowing for better user interaction with agent profiles. Updated related types for improved type safety and clarity in the codebase.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsyncEffect } from "@/hooks/use-async-effect";
|
||||
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
||||
@@ -15,25 +15,18 @@ import {
|
||||
postAdminRunDrawRng,
|
||||
} from "@/api/admin-draws";
|
||||
import { postAdminRunDrawSettlement } from "@/api/admin-settlement";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
||||
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type { AdminDrawShowData } from "@/types/api/admin-draws";
|
||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import {
|
||||
drawResultSourceLabel,
|
||||
drawStatusLabel,
|
||||
hallPreviewDiffersFromDbStatus,
|
||||
} from "./draw-display";
|
||||
import { drawStatusLabel, hallPreviewDiffersFromDbStatus } from "./draw-display";
|
||||
import { DrawStatusBadge } from "./draw-status-badge";
|
||||
import {
|
||||
PRD_DRAW_REOPEN_MANAGE,
|
||||
@@ -42,12 +35,31 @@ import {
|
||||
PRD_PAYOUT_REVIEW,
|
||||
} from "./draw-prd";
|
||||
|
||||
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
type ScheduleStep = {
|
||||
key: string;
|
||||
label: string;
|
||||
at: string | null | undefined;
|
||||
};
|
||||
|
||||
function ScheduleTimeline({ steps }: { steps: ScheduleStep[] }) {
|
||||
const formatDt = useAdminDateTimeFormatter();
|
||||
|
||||
return (
|
||||
<div className="grid gap-1 sm:grid-cols-[10rem_1fr] sm:items-start">
|
||||
<Label className="text-muted-foreground">{label}</Label>
|
||||
<div className="text-sm">{children}</div>
|
||||
</div>
|
||||
<ol className="grid gap-3 sm:grid-cols-3">
|
||||
{steps.map((step, index) => (
|
||||
<li
|
||||
key={step.key}
|
||||
className={cn(
|
||||
"relative rounded-lg border bg-muted/20 px-3 py-2.5",
|
||||
index < steps.length - 1 &&
|
||||
"sm:after:absolute sm:after:top-1/2 sm:after:left-full sm:after:h-px sm:after:w-3 sm:after:-translate-y-1/2 sm:after:bg-border",
|
||||
)}
|
||||
>
|
||||
<p className="text-xs font-medium text-muted-foreground">{step.label}</p>
|
||||
<p className="mt-1 font-mono text-sm tabular-nums">{formatDt(step.at)}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,7 +74,6 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
||||
PRD_PAYOUT_MANAGE,
|
||||
PRD_PAYOUT_REVIEW,
|
||||
]);
|
||||
const formatDt = useAdminDateTimeFormatter();
|
||||
const [data, setData] = useState<AdminDrawShowData | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -105,6 +116,99 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
||||
void load();
|
||||
}, [idNum]);
|
||||
|
||||
const scheduleSteps = useMemo((): ScheduleStep[] => {
|
||||
if (!data) return [];
|
||||
const steps: ScheduleStep[] = [
|
||||
{ key: "start", label: t("startTime"), at: data.start_time },
|
||||
{ key: "close", label: t("closeTime"), at: data.close_time },
|
||||
{ key: "draw", label: t("plannedDraw"), at: data.draw_time },
|
||||
];
|
||||
if (data.cooling_end_time) {
|
||||
steps.push({
|
||||
key: "cooling",
|
||||
label: t("coolingEndTime"),
|
||||
at: data.cooling_end_time,
|
||||
});
|
||||
}
|
||||
return steps;
|
||||
}, [data, t]);
|
||||
|
||||
const availableActions = useMemo(() => {
|
||||
if (!data) return [];
|
||||
|
||||
type ActionDef = {
|
||||
key: string;
|
||||
label: string;
|
||||
variant: "outline" | "destructive";
|
||||
enabled: boolean;
|
||||
onConfirm: () => Promise<void>;
|
||||
confirmTitle: string;
|
||||
confirmDescription: string;
|
||||
confirmVariant?: "default" | "destructive";
|
||||
};
|
||||
|
||||
const defs: ActionDef[] = [];
|
||||
|
||||
if (canManageDraw) {
|
||||
defs.push(
|
||||
{
|
||||
key: "manualClose",
|
||||
label: t("manualClose"),
|
||||
variant: "outline",
|
||||
enabled: ["pending", "open"].includes(data.status),
|
||||
onConfirm: () => postAdminManualCloseDraw(idNum),
|
||||
confirmTitle: t("confirm.manualCloseTitle"),
|
||||
confirmDescription: t("confirm.manualCloseDescription"),
|
||||
},
|
||||
{
|
||||
key: "cancel",
|
||||
label: t("cancelBeforeDraw"),
|
||||
variant: "outline",
|
||||
enabled: ["pending", "open", "closing", "closed"].includes(data.status),
|
||||
onConfirm: () => postAdminCancelDraw(idNum),
|
||||
confirmTitle: t("confirm.cancelDrawTitle"),
|
||||
confirmDescription: t("confirm.cancelDrawDescription"),
|
||||
},
|
||||
{
|
||||
key: "rng",
|
||||
label: t("rngAutoGenerate"),
|
||||
variant: "outline",
|
||||
enabled: data.status === "closed",
|
||||
onConfirm: () => postAdminRunDrawRng(idNum),
|
||||
confirmTitle: t("confirm.rngDrawTitle"),
|
||||
confirmDescription: t("confirm.rngDrawDescription"),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (canReopenDraw) {
|
||||
defs.push({
|
||||
key: "reopen",
|
||||
label: t("cooldownReopen"),
|
||||
variant: "destructive",
|
||||
enabled: data.status === "cooldown",
|
||||
onConfirm: () => postAdminReopenDraw(idNum),
|
||||
confirmTitle: t("confirm.reopenTitle"),
|
||||
confirmDescription: t("confirm.reopenDescription"),
|
||||
confirmVariant: "destructive",
|
||||
});
|
||||
}
|
||||
|
||||
if (canRunSettlement) {
|
||||
defs.push({
|
||||
key: "settlement",
|
||||
label: t("runSettlement"),
|
||||
variant: "outline",
|
||||
enabled: data.status === "settling",
|
||||
onConfirm: () => postAdminRunDrawSettlement(idNum),
|
||||
confirmTitle: t("confirm.runSettlementTitle"),
|
||||
confirmDescription: t("confirm.runSettlementDescription"),
|
||||
});
|
||||
}
|
||||
|
||||
return defs.filter((d) => d.enabled);
|
||||
}, [canManageDraw, canReopenDraw, canRunSettlement, data, idNum, t]);
|
||||
|
||||
if (loading && !data) {
|
||||
return <AdminLoadingState minHeight="6rem" className="py-6" />;
|
||||
}
|
||||
@@ -113,164 +217,122 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
||||
return <p className="text-sm text-destructive">{error ?? t("states.noData", { ns: "common" })}</p>;
|
||||
}
|
||||
|
||||
const batch = data.result_batch_counts;
|
||||
const hasResultActivity = batch.total > 0 || batch.pending_review > 0 || batch.published > 0;
|
||||
const showActions =
|
||||
availableActions.length > 0 && (canManageDraw || canReopenDraw || canRunSettlement);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader className="border-b bg-muted/30 pb-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="text-xl">{data.draw_no}</CardTitle>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{t("drawDetail")}</p>
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<CardTitle className="font-mono text-xl tracking-tight">{data.draw_no}</CardTitle>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{t("detailSubtitle", {
|
||||
date: data.business_date,
|
||||
seq: data.sequence_no,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1 text-right">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<DrawStatusBadge
|
||||
status={data.status}
|
||||
label={drawStatusLabel(data.status, t)}
|
||||
/>
|
||||
{hallPreviewDiffersFromDbStatus(data.status, data.hall_preview_status) ? (
|
||||
<p className="flex flex-wrap items-center justify-end gap-2 text-sm text-muted-foreground">
|
||||
<span>{t("hallPreviewStatusLabel")}</span>
|
||||
<>
|
||||
<span className="text-xs text-muted-foreground">{t("hallPreviewStatusLabel")}</span>
|
||||
<DrawStatusBadge
|
||||
status={data.hall_preview_status}
|
||||
label={drawStatusLabel(data.hall_preview_status, t)}
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6 p-6 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<Field label={t("businessDate")}>{data.business_date}</Field>
|
||||
<Field label={t("sequenceNo")}>{data.sequence_no}</Field>
|
||||
<Field label={t("startTime")}>{formatDt(data.start_time)}</Field>
|
||||
<Field label={t("closeTime")}>{formatDt(data.close_time)}</Field>
|
||||
<Field label={t("plannedDraw")}>{formatDt(data.draw_time)}</Field>
|
||||
<Field label={t("coolingEndTime")}>{formatDt(data.cooling_end_time)}</Field>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<Field label={t("resultSource")}>{drawResultSourceLabel(data.result_source, t)}</Field>
|
||||
<Field label={t("currentResultVersion")}>{data.current_result_version}</Field>
|
||||
<Field label={t("settleVersion")}>{data.settle_version}</Field>
|
||||
<Field label={t("isReopened")}>{data.is_reopened ? t("yes") : t("no")}</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-muted/20 p-4">
|
||||
<p className="text-sm font-medium text-muted-foreground">{t("batchStats")}</p>
|
||||
<div className="mt-3 grid gap-3 text-sm">
|
||||
<div className="flex items-center justify-between rounded-lg bg-background px-3 py-2">
|
||||
<span>{t("batchTotal")}</span>
|
||||
<span className="font-semibold">{data.result_batch_counts.total}</span>
|
||||
<CardContent className="space-y-6 border-t pt-6">
|
||||
<section className="space-y-3">
|
||||
<h3 className="text-sm font-medium">{t("scheduleTitle")}</h3>
|
||||
<ScheduleTimeline steps={scheduleSteps} />
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h3 className="text-sm font-medium">{t("resultBatchesTitle")}</h3>
|
||||
{hasResultActivity ? (
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm">
|
||||
<span className="rounded-md bg-muted px-2.5 py-1">
|
||||
{t("batchSummaryTotal", { count: batch.total })}
|
||||
</span>
|
||||
{batch.pending_review > 0 ? (
|
||||
<Link
|
||||
href={`/admin/draws/${drawId}/review`}
|
||||
className="rounded-md bg-amber-500/15 px-2.5 py-1 font-medium text-amber-800 dark:text-amber-200"
|
||||
>
|
||||
{t("batchSummaryPending", { count: batch.pending_review })}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="rounded-md bg-muted px-2.5 py-1 text-muted-foreground">
|
||||
{t("batchSummaryPending", { count: 0 })}
|
||||
</span>
|
||||
)}
|
||||
{batch.published > 0 ? (
|
||||
<Link
|
||||
href={`/admin/draws/${drawId}/results`}
|
||||
className="rounded-md bg-emerald-500/15 px-2.5 py-1 font-medium text-emerald-800 dark:text-emerald-200"
|
||||
>
|
||||
{t("batchSummaryPublished", { count: batch.published })}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="rounded-md bg-muted px-2.5 py-1 text-muted-foreground">
|
||||
{t("batchSummaryPublished", { count: 0 })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-lg bg-background px-3 py-2 text-amber-600 dark:text-amber-400">
|
||||
<span>{t("pendingReview")}</span>
|
||||
<span className="font-semibold">{data.result_batch_counts.pending_review}</span>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("noResultBatchesYet")}{" "}
|
||||
<Link
|
||||
href={`/admin/draws/${drawId}/review`}
|
||||
className="font-medium text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{t("goToReviewTab")}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{showActions ? (
|
||||
<section className="space-y-3 border-t pt-6">
|
||||
<h3 className="text-sm font-medium">{t("drawActions")}</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{availableActions.map((action) => (
|
||||
<Button
|
||||
key={action.key}
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={action.variant}
|
||||
disabled={acting !== null}
|
||||
onClick={() =>
|
||||
requestConfirm({
|
||||
title: action.confirmTitle,
|
||||
description: action.confirmDescription,
|
||||
confirmVariant: action.confirmVariant,
|
||||
onConfirm: () => runAction(action.label, action.onConfirm),
|
||||
})
|
||||
}
|
||||
>
|
||||
{acting === action.label ? t("processing") : action.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-lg bg-background px-3 py-2 text-emerald-600 dark:text-emerald-400">
|
||||
<span>{t("published")}</span>
|
||||
<span className="font-semibold">{data.result_batch_counts.published}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href={`/admin/draws/${drawId}/finance`}
|
||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "mt-4 w-full")}
|
||||
>
|
||||
{t("viewFinance")}
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{(canManageDraw || canReopenDraw || canRunSettlement) ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{t("drawActions")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!canManageDraw || acting !== null || !["pending", "open"].includes(data.status)}
|
||||
onClick={() =>
|
||||
requestConfirm({
|
||||
title: t("confirm.manualCloseTitle"),
|
||||
description: t("confirm.manualCloseDescription"),
|
||||
onConfirm: () => runAction(t("manualClose"), () => postAdminManualCloseDraw(idNum)),
|
||||
})
|
||||
}
|
||||
>
|
||||
{acting === t("manualClose") ? t("processing") : t("manualClose")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!canManageDraw || acting !== null || !["pending", "open", "closing", "closed"].includes(data.status)}
|
||||
onClick={() =>
|
||||
requestConfirm({
|
||||
title: t("confirm.cancelDrawTitle"),
|
||||
description: t("confirm.cancelDrawDescription"),
|
||||
onConfirm: () => runAction(t("cancelDraw"), () => postAdminCancelDraw(idNum)),
|
||||
})
|
||||
}
|
||||
>
|
||||
{acting === t("cancelDraw") ? t("processing") : t("cancelBeforeDraw")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!canManageDraw || acting !== null || data.status !== "closed"}
|
||||
onClick={() =>
|
||||
requestConfirm({
|
||||
title: t("confirm.rngDrawTitle"),
|
||||
description: t("confirm.rngDrawDescription"),
|
||||
onConfirm: () => runAction(t("rngDraw"), () => postAdminRunDrawRng(idNum)),
|
||||
})
|
||||
}
|
||||
>
|
||||
{acting === t("rngDraw") ? t("generating") : t("rngAutoGenerate")}
|
||||
</Button>
|
||||
{canReopenDraw ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={acting !== null || data.status !== "cooldown"}
|
||||
onClick={() =>
|
||||
requestConfirm({
|
||||
title: t("confirm.reopenTitle"),
|
||||
description: t("confirm.reopenDescription"),
|
||||
confirmVariant: "destructive",
|
||||
onConfirm: () => runAction(t("reopen"), () => postAdminReopenDraw(idNum)),
|
||||
})
|
||||
}
|
||||
>
|
||||
{acting === t("reopen") ? t("processing") : t("cooldownReopen")}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!canRunSettlement || acting !== null || data.status !== "settling"}
|
||||
onClick={() =>
|
||||
requestConfirm({
|
||||
title: t("confirm.runSettlementTitle"),
|
||||
description: t("confirm.runSettlementDescription"),
|
||||
onConfirm: () => runAction(t("runSettlement"), () => postAdminRunDrawSettlement(idNum)),
|
||||
})
|
||||
}
|
||||
>
|
||||
{acting === t("runSettlement") ? t("processing") : t("runSettlement")}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
<ConfirmDialog />
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user