Some checks failed
lotteryadmin CI / build (push) Has been cancelled
Updated the player management components to improve error handling and display for various operations, including ticket loading and transaction retrieval. Introduced new status badges for better visual representation of player statuses. Enhanced localization support for error messages and improved the overall user experience in the admin console. Refactored components for better maintainability and clarity.
598 lines
25 KiB
TypeScript
598 lines
25 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useAsyncEffect } from "@/hooks/use-async-effect";
|
|
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
|
|
|
import {
|
|
getAdminJackpotPoolAdjustments,
|
|
getAdminJackpotPools,
|
|
postAdminJackpotManualBurst,
|
|
postAdminJackpotPoolAdjustment,
|
|
putAdminJackpotPool,
|
|
} from "@/api/admin-jackpot";
|
|
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
|
import { formatAdminMinorDecimal, parseAdminMajorToMinor } from "@/lib/money";
|
|
import { PRD_JACKPOT_MANAGE, PRD_JACKPOT_MANUAL_BURST } from "@/lib/admin-prd";
|
|
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
|
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
|
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
|
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 { Switch } from "@/components/ui/switch";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { toast } from "sonner";
|
|
import { useAdminProfile } from "@/stores/admin-session";
|
|
import { LotteryApiBizError } from "@/types/api/errors";
|
|
import type { AdminJackpotPoolAdjustmentRow, AdminJackpotPoolRow } from "@/types/api/admin-jackpot";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
|
|
import {
|
|
formatRatioAsPercent,
|
|
percentUiToRatio,
|
|
ratioToPercentUi,
|
|
} from "@/lib/admin-rate-percent";
|
|
|
|
type Draft = {
|
|
contribution_rate: string;
|
|
trigger_threshold: string;
|
|
payout_rate: string;
|
|
force_trigger_draw_gap: string;
|
|
min_bet_amount: string;
|
|
combo_trigger_play_codes: string;
|
|
status: string;
|
|
manual_burst_draw_id: string;
|
|
};
|
|
|
|
type AdjustmentDraft = {
|
|
direction: "increase" | "decrease";
|
|
amount: string;
|
|
reason: string;
|
|
};
|
|
|
|
function toDraft(p: AdminJackpotPoolRow): Draft {
|
|
return {
|
|
contribution_rate: ratioToPercentUi(p.contribution_rate),
|
|
trigger_threshold: formatAdminMinorDecimal(p.trigger_threshold, p.currency_code),
|
|
payout_rate: ratioToPercentUi(p.payout_rate),
|
|
force_trigger_draw_gap: String(p.force_trigger_draw_gap),
|
|
min_bet_amount: formatAdminMinorDecimal(p.min_bet_amount, p.currency_code),
|
|
combo_trigger_play_codes: p.combo_trigger_play_codes.join(","),
|
|
status: String(p.status),
|
|
manual_burst_draw_id: "",
|
|
};
|
|
}
|
|
|
|
type JackpotPoolsConsoleProps = {
|
|
/** 嵌入运营配置单页时去掉外层脚手架与重复标题 */
|
|
embedded?: boolean;
|
|
};
|
|
|
|
export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsoleProps) {
|
|
const { t } = useTranslation(["jackpot", "common"]);
|
|
const tRef = useTranslationRef(["jackpot", "common"]);
|
|
const profile = useAdminProfile();
|
|
const canManageJackpot = adminHasAnyPermission(profile?.permissions, [PRD_JACKPOT_MANAGE]);
|
|
const canManualBurst = adminHasAnyPermission(profile?.permissions, [PRD_JACKPOT_MANUAL_BURST]);
|
|
const { request: requestConfirm, ConfirmDialog: ConfirmActionDialog } = useConfirmAction();
|
|
const [items, setItems] = useState<AdminJackpotPoolRow[]>([]);
|
|
const [drafts, setDrafts] = useState<Record<number, Draft>>({});
|
|
const [loading, setLoading] = useState(true);
|
|
const [savingId, setSavingId] = useState<number | null>(null);
|
|
const [burstingId, setBurstingId] = useState<number | null>(null);
|
|
const [confirmBurstPoolId, setConfirmBurstPoolId] = useState<number | null>(null);
|
|
const [adjustmentDrafts, setAdjustmentDrafts] = useState<Record<number, AdjustmentDraft>>({});
|
|
const [adjustmentRows, setAdjustmentRows] = useState<Record<number, AdminJackpotPoolAdjustmentRow[]>>({});
|
|
const [adjustingId, setAdjustingId] = useState<number | null>(null);
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await getAdminJackpotPools();
|
|
setItems(res.items);
|
|
const d: Record<number, Draft> = {};
|
|
const adjDrafts: Record<number, AdjustmentDraft> = {};
|
|
const adjRows: Record<number, AdminJackpotPoolAdjustmentRow[]> = {};
|
|
for (const p of res.items) {
|
|
d[p.id] = toDraft(p);
|
|
adjDrafts[p.id] = { direction: "increase", amount: "", reason: "" };
|
|
try {
|
|
const ledger = await getAdminJackpotPoolAdjustments(p.id, { per_page: 5 });
|
|
adjRows[p.id] = ledger.items;
|
|
} catch {
|
|
adjRows[p.id] = [];
|
|
}
|
|
}
|
|
setDrafts(d);
|
|
setAdjustmentDrafts(adjDrafts);
|
|
setAdjustmentRows(adjRows);
|
|
} catch (e) {
|
|
toast.error(e instanceof LotteryApiBizError ? e.message : tRef.current("loadFailed"));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useAsyncEffect(() => {
|
|
void load();
|
|
}, []);
|
|
|
|
const updateDraft = (id: number, patch: Partial<Draft>) => {
|
|
setDrafts((prev) => ({
|
|
...prev,
|
|
[id]: { ...prev[id], ...patch },
|
|
}));
|
|
};
|
|
|
|
const updateAdjustmentDraft = (id: number, patch: Partial<AdjustmentDraft>) => {
|
|
setAdjustmentDrafts((prev) => ({
|
|
...prev,
|
|
[id]: { ...(prev[id] ?? { direction: "increase", amount: "", reason: "" }), ...patch },
|
|
}));
|
|
};
|
|
|
|
const save = async (p: AdminJackpotPoolRow) => {
|
|
const d = drafts[p.id];
|
|
if (!d) return;
|
|
setSavingId(p.id);
|
|
try {
|
|
await putAdminJackpotPool(p.id, {
|
|
contribution_rate: percentUiToRatio(d.contribution_rate),
|
|
trigger_threshold: parseAdminMajorToMinor(d.trigger_threshold, p.currency_code) ?? 0,
|
|
payout_rate: percentUiToRatio(d.payout_rate),
|
|
force_trigger_draw_gap: Number.parseInt(d.force_trigger_draw_gap, 10),
|
|
min_bet_amount: parseAdminMajorToMinor(d.min_bet_amount, p.currency_code) ?? 0,
|
|
combo_trigger_play_codes: d.combo_trigger_play_codes
|
|
.split(",")
|
|
.map((v) => v.trim().toLowerCase())
|
|
.filter(Boolean),
|
|
status: Number.parseInt(d.status, 10),
|
|
});
|
|
toast.success(t("saveSuccess"));
|
|
await load();
|
|
} catch (e) {
|
|
toast.error(e instanceof LotteryApiBizError ? e.message : t("saveFailed"));
|
|
} finally {
|
|
setSavingId(null);
|
|
}
|
|
};
|
|
|
|
const submitAdjustment = async (p: AdminJackpotPoolRow) => {
|
|
const adj = adjustmentDrafts[p.id];
|
|
if (!adj) return;
|
|
const minor = parseAdminMajorToMinor(adj.amount, p.currency_code);
|
|
if (minor === null || minor <= 0) {
|
|
toast.error(t("adjustmentAmountInvalid"));
|
|
return;
|
|
}
|
|
const trimmedReason = adj.reason.trim();
|
|
if (trimmedReason.length < 3) {
|
|
toast.error(t("adjustmentReasonRequired"));
|
|
return;
|
|
}
|
|
const amountDelta = adj.direction === "decrease" ? -minor : minor;
|
|
|
|
setAdjustingId(p.id);
|
|
try {
|
|
const res = await postAdminJackpotPoolAdjustment(p.id, {
|
|
amount_delta: amountDelta,
|
|
reason: trimmedReason,
|
|
});
|
|
toast.success(t("adjustmentSuccess"));
|
|
setItems((prev) =>
|
|
prev.map((row) =>
|
|
row.id === p.id ? { ...row, current_amount: res.pool.current_amount } : row,
|
|
),
|
|
);
|
|
updateAdjustmentDraft(p.id, { amount: "", reason: "" });
|
|
const ledger = await getAdminJackpotPoolAdjustments(p.id, { per_page: 5 });
|
|
setAdjustmentRows((prev) => ({ ...prev, [p.id]: ledger.items }));
|
|
} catch (e) {
|
|
toast.error(e instanceof LotteryApiBizError ? e.message : t("adjustmentFailed"));
|
|
} finally {
|
|
setAdjustingId(null);
|
|
}
|
|
};
|
|
|
|
const manualBurst = async (p: AdminJackpotPoolRow) => {
|
|
const d = drafts[p.id];
|
|
if (!d) return;
|
|
const drawRef = d.manual_burst_draw_id.trim();
|
|
if (drawRef.length === 0) {
|
|
toast.error(t("invalidDrawId"));
|
|
return;
|
|
}
|
|
|
|
setBurstingId(p.id);
|
|
try {
|
|
const res = await postAdminJackpotManualBurst(p.id, { draw_id: drawRef });
|
|
toast.success(
|
|
`${t("manualBurstSuccess")} · ${res.draw_no} · ${res.winner_count} ${t("winnerCount")}`,
|
|
);
|
|
await load();
|
|
} catch (e) {
|
|
toast.error(e instanceof LotteryApiBizError ? e.message : t("manualBurstFailed"));
|
|
} finally {
|
|
setBurstingId(null);
|
|
setConfirmBurstPoolId(null);
|
|
}
|
|
};
|
|
|
|
const poolList = (
|
|
<div className={embedded ? "space-y-4" : "space-y-8"}>
|
|
{loading ? <AdminLoadingState minHeight="6rem" className="py-6" /> : null}
|
|
{!loading && items.length === 0 ? (
|
|
<AdminNoResourceState />
|
|
) : null}
|
|
{items.map((p) => {
|
|
const d = drafts[p.id] ?? toDraft(p);
|
|
const adj = adjustmentDrafts[p.id] ?? { direction: "increase", amount: "", reason: "" };
|
|
const ledger = adjustmentRows[p.id] ?? [];
|
|
const currentAmount = formatAdminMinorDecimal(p.current_amount, p.currency_code);
|
|
const triggerThreshold = formatAdminMinorDecimal(p.trigger_threshold, p.currency_code);
|
|
const minBetAmount = formatAdminMinorDecimal(p.min_bet_amount, p.currency_code);
|
|
const statusOn = d.status === "1";
|
|
return (
|
|
<div
|
|
key={p.id}
|
|
className="space-y-3 rounded-xl border border-border/60 bg-background p-3 shadow-sm"
|
|
>
|
|
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
<div>
|
|
<h3 className="text-base font-semibold">{p.currency_code}</h3>
|
|
<p className="text-muted-foreground text-xs">{t("configTitle")}</p>
|
|
</div>
|
|
<p className="text-muted-foreground text-sm font-medium">
|
|
{t("displayBalance", { amount: currentAmount })}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-4">
|
|
<div className="rounded-lg border border-border/60 bg-muted/20 p-2.5">
|
|
<p className="text-muted-foreground text-xs">{t("currentAmount")}</p>
|
|
<p className="mt-1 text-2xl font-semibold leading-none tracking-tight">
|
|
{currentAmount}
|
|
</p>
|
|
</div>
|
|
<div className="rounded-lg border border-border/60 bg-muted/20 p-2.5">
|
|
<p className="text-muted-foreground text-xs">{t("status")}</p>
|
|
<div className="mt-1">
|
|
<AdminStatusBadge status={statusOn ? "enabled" : "disabled"}>
|
|
{statusOn ? t("enabled") : t("disabled")}
|
|
</AdminStatusBadge>
|
|
</div>
|
|
</div>
|
|
<div className="rounded-lg border border-border/60 bg-muted/20 p-2.5">
|
|
<p className="text-muted-foreground text-xs">{t("payoutRate")}</p>
|
|
<p className="mt-1 text-lg font-semibold">
|
|
{formatRatioAsPercent(percentUiToRatio(d.payout_rate))}
|
|
</p>
|
|
</div>
|
|
<div className="rounded-lg border border-border/60 bg-muted/20 p-2.5">
|
|
<p className="text-muted-foreground text-xs">{t("forceTriggerGap")}</p>
|
|
<p className="mt-1 text-lg font-semibold">{d.force_trigger_draw_gap}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-3 xl:grid-cols-12">
|
|
<div className="space-y-3 xl:col-span-8">
|
|
{canManageJackpot ? (
|
|
<div className="space-y-2 rounded-lg border border-border/60 bg-background p-3">
|
|
<p className="text-sm font-medium">{t("balanceAdjustmentTitle")}</p>
|
|
<p className="text-muted-foreground text-xs">{t("balanceAdjustmentHint")}</p>
|
|
<div className="grid gap-2 sm:grid-cols-2">
|
|
<div className="space-y-1.5">
|
|
<Label>{t("adjustmentDirection")}</Label>
|
|
<Select
|
|
value={adj.direction}
|
|
onValueChange={(value: "increase" | "decrease" | null) => {
|
|
if (value === null) return;
|
|
updateAdjustmentDraft(p.id, { direction: value });
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-full min-w-0 sm:max-w-[12rem]">
|
|
<SelectValue>
|
|
{(value) =>
|
|
value === "increase"
|
|
? t("adjustmentIncrease")
|
|
: value === "decrease"
|
|
? t("adjustmentDecrease")
|
|
: value != null
|
|
? String(value)
|
|
: t("adjustmentIncrease")
|
|
}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="increase">{t("adjustmentIncrease")}</SelectItem>
|
|
<SelectItem value="decrease">{t("adjustmentDecrease")}</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor={`adj-amt-${p.id}`}>{t("adjustmentAmount")}</Label>
|
|
<Input
|
|
id={`adj-amt-${p.id}`}
|
|
className="font-mono"
|
|
value={adj.amount}
|
|
placeholder={t("adjustmentAmountPlaceholder")}
|
|
onChange={(e) => updateAdjustmentDraft(p.id, { amount: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5 sm:col-span-2">
|
|
<Label htmlFor={`adj-reason-${p.id}`}>{t("adjustmentReason")}</Label>
|
|
<Textarea
|
|
id={`adj-reason-${p.id}`}
|
|
rows={1}
|
|
value={adj.reason}
|
|
placeholder={t("adjustmentReasonPlaceholder")}
|
|
onChange={(e) => updateAdjustmentDraft(p.id, { reason: e.target.value })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-end">
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
disabled={adjustingId === p.id}
|
|
onClick={() =>
|
|
requestConfirm({
|
|
title: t("confirmAdjustmentTitle"),
|
|
description: t("confirmAdjustmentDescription"),
|
|
confirmLabel: t("confirm.confirmSave", { ns: "common" }),
|
|
onConfirm: () => submitAdjustment(p),
|
|
})
|
|
}
|
|
>
|
|
{adjustingId === p.id ? t("processing") : t("submitAdjustment")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
<fieldset
|
|
disabled={!canManageJackpot}
|
|
className="grid gap-2 rounded-lg border border-border/60 bg-background p-3 sm:grid-cols-2"
|
|
>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor={`th-${p.id}`}>{t("triggerThreshold")}</Label>
|
|
<Input
|
|
id={`th-${p.id}`}
|
|
className="font-mono"
|
|
value={d.trigger_threshold}
|
|
placeholder={t("triggerThresholdPlaceholder")}
|
|
onChange={(e) => updateDraft(p.id, { trigger_threshold: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor={`min-${p.id}`}>{t("minBetAmount")}</Label>
|
|
<Input
|
|
id={`min-${p.id}`}
|
|
className="font-mono"
|
|
value={d.min_bet_amount}
|
|
placeholder={t("minBetAmountPlaceholder")}
|
|
onChange={(e) => updateDraft(p.id, { min_bet_amount: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor={`pr-${p.id}`}>{t("payoutRate")}</Label>
|
|
<Input
|
|
id={`pr-${p.id}`}
|
|
type="number"
|
|
min={0}
|
|
max={100}
|
|
step="0.01"
|
|
className="font-mono"
|
|
value={d.payout_rate}
|
|
placeholder={t("payoutRatePlaceholder")}
|
|
onChange={(e) => updateDraft(p.id, { payout_rate: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor={`gap-${p.id}`}>{t("forceTriggerGap")}</Label>
|
|
<Input
|
|
id={`gap-${p.id}`}
|
|
className="font-mono"
|
|
value={d.force_trigger_draw_gap}
|
|
placeholder={t("forceTriggerGapPlaceholder")}
|
|
onChange={(e) => updateDraft(p.id, { force_trigger_draw_gap: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor={`cr-${p.id}`}>{t("contributionRate")}</Label>
|
|
<Input
|
|
id={`cr-${p.id}`}
|
|
type="number"
|
|
min={0}
|
|
max={100}
|
|
step="0.01"
|
|
className="font-mono"
|
|
value={d.contribution_rate}
|
|
placeholder={t("contributionRatePlaceholder")}
|
|
onChange={(e) => updateDraft(p.id, { contribution_rate: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor={`combo-${p.id}`}>{t("comboTriggerPlays")}</Label>
|
|
<Input
|
|
id={`combo-${p.id}`}
|
|
className="font-mono"
|
|
value={d.combo_trigger_play_codes}
|
|
placeholder={t("comboTriggerPlaysPlaceholder")}
|
|
onChange={(e) => updateDraft(p.id, { combo_trigger_play_codes: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-between gap-3 rounded-lg border border-border/60 px-3 py-2 sm:col-span-2">
|
|
<Label htmlFor={`status-${p.id}`} className="text-sm font-medium">
|
|
{t("status")}
|
|
</Label>
|
|
<Switch
|
|
id={`status-${p.id}`}
|
|
checked={statusOn}
|
|
disabled={!canManageJackpot}
|
|
aria-label={t("status")}
|
|
onCheckedChange={(checked) =>
|
|
updateDraft(p.id, { status: checked ? "1" : "0" })
|
|
}
|
|
/>
|
|
</div>
|
|
{canManageJackpot ? (
|
|
<div className="flex justify-end sm:col-span-2">
|
|
<Button
|
|
type="button"
|
|
disabled={savingId === p.id}
|
|
onClick={() =>
|
|
requestConfirm({
|
|
title: t("confirmSavePoolTitle"),
|
|
description: t("confirmSavePoolDescription"),
|
|
confirmLabel: t("confirm.confirmSave", { ns: "common" }),
|
|
onConfirm: () => save(p),
|
|
})
|
|
}
|
|
>
|
|
{savingId === p.id ? t("saving") : t("save")}
|
|
</Button>
|
|
</div>
|
|
) : null}
|
|
</fieldset>
|
|
</div>
|
|
|
|
<div className="space-y-3 xl:col-span-4">
|
|
<div className="rounded-lg border border-border/60 bg-background p-3">
|
|
<div className="mb-2 flex items-center justify-between gap-2">
|
|
<p className="text-sm font-medium">{t("recentAdjustments")}</p>
|
|
<span className="text-muted-foreground text-xs">{ledger.length}</span>
|
|
</div>
|
|
{ledger.length > 0 ? (
|
|
<ul className="max-h-60 space-y-2 overflow-y-auto pr-1">
|
|
{ledger.map((row) => (
|
|
<li key={row.id} className="rounded-md border border-border/60 bg-muted/20 p-2">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<span className="font-mono text-xs">{row.adjustment_no}</span>
|
|
<span className="text-sm font-semibold">
|
|
{row.amount_delta > 0 ? "+" : ""}
|
|
{formatAdminMinorDecimal(row.amount_delta, p.currency_code)}
|
|
</span>
|
|
</div>
|
|
<p className="text-muted-foreground mt-1 line-clamp-2 text-xs">{row.reason}</p>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<p className="text-muted-foreground text-xs">—</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="rounded-lg border border-border/60 bg-background p-3">
|
|
<p className="text-muted-foreground text-xs">{t("triggerThreshold")}</p>
|
|
<p className="mt-1 text-lg font-semibold">{triggerThreshold}</p>
|
|
<p className="text-muted-foreground mt-2 text-xs">{t("minBetAmount")}</p>
|
|
<p className="mt-1 text-lg font-semibold">{minBetAmount}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{canManualBurst ? (
|
|
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-3">
|
|
<p className="mb-1 text-sm font-medium text-destructive">{t("manualBurst")}</p>
|
|
<p className="mb-2 text-xs text-muted-foreground">{t("manualBurstHint")}</p>
|
|
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-end">
|
|
<div className="min-w-0 flex-1 space-y-1.5 sm:max-w-xs">
|
|
<Label htmlFor={`burst-draw-${p.id}`}>{t("manualBurstDrawId")}</Label>
|
|
<Input
|
|
id={`burst-draw-${p.id}`}
|
|
className="font-mono"
|
|
value={d.manual_burst_draw_id}
|
|
onChange={(e) => updateDraft(p.id, { manual_burst_draw_id: e.target.value })}
|
|
/>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
className="shrink-0 sm:ml-auto"
|
|
disabled={burstingId === p.id}
|
|
onClick={() => setConfirmBurstPoolId(p.id)}
|
|
>
|
|
{burstingId === p.id ? t("processing") : t("manualBurst")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
|
|
const confirmPool = confirmBurstPoolId !== null ? items.find((p) => p.id === confirmBurstPoolId) : null;
|
|
const confirmDraft = confirmPool ? drafts[confirmPool.id] : null;
|
|
|
|
const confirmDialog = (
|
|
<Dialog open={confirmBurstPoolId !== null} onOpenChange={(open) => !open && setConfirmBurstPoolId(null)}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>{t("manualBurstConfirmTitle")}</DialogTitle>
|
|
<DialogDescription>
|
|
{t("manualBurstConfirmDescription", {
|
|
drawId: confirmDraft?.manual_burst_draw_id ?? "—",
|
|
})}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button type="button" variant="outline" onClick={() => setConfirmBurstPoolId(null)}>
|
|
{t("cancel")}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
disabled={confirmPool === undefined || burstingId !== null}
|
|
onClick={() => confirmPool && void manualBurst(confirmPool)}
|
|
>
|
|
{t("manualBurstConfirm")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
|
|
if (embedded) {
|
|
return (
|
|
<>
|
|
{poolList}
|
|
{confirmDialog}
|
|
<ConfirmActionDialog />
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ModuleScaffold>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">{t("configTitle")}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>{poolList}</CardContent>
|
|
</Card>
|
|
{confirmDialog}
|
|
<ConfirmActionDialog />
|
|
</ModuleScaffold>
|
|
);
|
|
}
|