595 lines
25 KiB
TypeScript
595 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 { 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>
|
|
<p className="mt-1 text-base font-semibold">
|
|
{statusOn ? t("enabled") : t("disabled")}
|
|
</p>
|
|
</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>
|
|
);
|
|
}
|