feat(admin): add draw management features including create, update, delete, and batch delete functionalities
- Implemented API functions for creating, updating, and deleting draws. - Enhanced the admin draws console with UI components for managing draws. - Added internationalization support for new draw management actions and messages.
This commit is contained in:
134
src/modules/draws/draw-create-dialog.tsx
Normal file
134
src/modules/draws/draw-create-dialog.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { postAdminCreateDraw } from "@/api/admin-draws";
|
||||
import { AdminDateTimeField } from "@/components/admin/admin-datetime-field";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { LOTTERY_SCHEDULE_TIMEZONE } from "@/lib/lottery-schedule-timezone";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
|
||||
type DrawCreateDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
scheduleTimezone?: string;
|
||||
onCreated: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
function resetFormState(): {
|
||||
drawTime: string;
|
||||
closeTime: string;
|
||||
startTime: string;
|
||||
drawNo: string;
|
||||
} {
|
||||
return { drawTime: "", closeTime: "", startTime: "", drawNo: "" };
|
||||
}
|
||||
|
||||
export function DrawCreateDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
scheduleTimezone,
|
||||
onCreated,
|
||||
}: DrawCreateDialogProps) {
|
||||
const { t } = useTranslation(["draws", "common"]);
|
||||
const [form, setForm] = useState(resetFormState);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setForm(resetFormState());
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
async function submit(): Promise<void> {
|
||||
if (!form.drawTime.trim()) {
|
||||
toast.error(t("createDraw.drawTimeRequired"));
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await postAdminCreateDraw({
|
||||
draw_time: form.drawTime.trim(),
|
||||
close_time: form.closeTime.trim() || undefined,
|
||||
start_time: form.startTime.trim() || undefined,
|
||||
draw_no: form.drawNo.trim() || undefined,
|
||||
});
|
||||
toast.success(t("createDraw.success"));
|
||||
setForm(resetFormState());
|
||||
onOpenChange(false);
|
||||
await onCreated();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("createDraw.failed"));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent showCloseButton className="max-w-lg gap-4">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("createDraw.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("createDraw.description", { tz: "Local" })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<AdminDateTimeField
|
||||
id="draw-create-draw-time"
|
||||
label={t("drawTime")}
|
||||
value={form.drawTime}
|
||||
onChange={(drawTime) => setForm((prev) => ({ ...prev, drawTime }))}
|
||||
required
|
||||
/>
|
||||
<AdminDateTimeField
|
||||
id="draw-create-close-time"
|
||||
label={t("closeTime")}
|
||||
value={form.closeTime}
|
||||
onChange={(closeTime) => setForm((prev) => ({ ...prev, closeTime }))}
|
||||
optional
|
||||
/>
|
||||
<AdminDateTimeField
|
||||
id="draw-create-start-time"
|
||||
label={t("startTime")}
|
||||
value={form.startTime}
|
||||
onChange={(startTime) => setForm((prev) => ({ ...prev, startTime }))}
|
||||
optional
|
||||
/>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="draw-create-draw-no">{t("drawNo")}</Label>
|
||||
<Input
|
||||
id="draw-create-draw-no"
|
||||
placeholder="20260526-008"
|
||||
value={form.drawNo}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, drawNo: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t("createDraw.hint")}</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
{t("actions.cancel", { ns: "common" })}
|
||||
</Button>
|
||||
<Button type="button" disabled={saving} onClick={() => void submit()}>
|
||||
{saving ? t("createDraw.saving") : t("createDraw.submit")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
146
src/modules/draws/draw-edit-dialog.tsx
Normal file
146
src/modules/draws/draw-edit-dialog.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { putAdminUpdateDraw } from "@/api/admin-draws";
|
||||
import { AdminDateTimeField } from "@/components/admin/admin-datetime-field";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { formatAdminInstant } from "@/lib/admin-datetime";
|
||||
import { getAdminRequestLocale } from "@/lib/admin-locale";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type { AdminDrawListItem } from "@/types/api/admin-draws";
|
||||
|
||||
type DrawEditDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
draw: AdminDrawListItem | null;
|
||||
scheduleTimezone?: string;
|
||||
onSaved: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
function isoToScheduleValue(iso: string | null): string {
|
||||
return formatAdminInstant(iso, {
|
||||
locale: getAdminRequestLocale(),
|
||||
});
|
||||
}
|
||||
|
||||
export function DrawEditDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
draw,
|
||||
scheduleTimezone,
|
||||
onSaved,
|
||||
}: DrawEditDialogProps) {
|
||||
const { t } = useTranslation(["draws", "common"]);
|
||||
const [drawTime, setDrawTime] = useState("");
|
||||
const [closeTime, setCloseTime] = useState("");
|
||||
const [startTime, setStartTime] = useState("");
|
||||
const [drawNo, setDrawNo] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || draw == null) {
|
||||
return;
|
||||
}
|
||||
setDrawTime(isoToScheduleValue(draw.draw_time));
|
||||
setCloseTime(isoToScheduleValue(draw.close_time));
|
||||
setStartTime(isoToScheduleValue(draw.start_time));
|
||||
setDrawNo(draw.draw_no);
|
||||
}, [open, draw]);
|
||||
|
||||
async function submit(): Promise<void> {
|
||||
if (draw == null) {
|
||||
return;
|
||||
}
|
||||
if (!drawTime.trim()) {
|
||||
toast.error(t("createDraw.drawTimeRequired"));
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await putAdminUpdateDraw(draw.id, {
|
||||
draw_time: drawTime.trim(),
|
||||
close_time: closeTime.trim() || undefined,
|
||||
start_time: startTime.trim() || undefined,
|
||||
draw_no: drawNo.trim() || undefined,
|
||||
});
|
||||
toast.success(t("editDraw.success"));
|
||||
onOpenChange(false);
|
||||
await onSaved();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("editDraw.failed"));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent showCloseButton className="max-w-lg gap-4">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("editDraw.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("editDraw.description", {
|
||||
tz: "Local",
|
||||
drawNo: draw?.draw_no ?? "",
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<AdminDateTimeField
|
||||
id="draw-edit-draw-time"
|
||||
label={t("drawTime")}
|
||||
value={drawTime}
|
||||
onChange={setDrawTime}
|
||||
required
|
||||
/>
|
||||
<AdminDateTimeField
|
||||
id="draw-edit-close-time"
|
||||
label={t("closeTime")}
|
||||
value={closeTime}
|
||||
onChange={setCloseTime}
|
||||
optional
|
||||
/>
|
||||
<AdminDateTimeField
|
||||
id="draw-edit-start-time"
|
||||
label={t("startTime")}
|
||||
value={startTime}
|
||||
onChange={setStartTime}
|
||||
optional
|
||||
/>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="draw-edit-draw-no">{t("drawNo")}</Label>
|
||||
<Input
|
||||
id="draw-edit-draw-no"
|
||||
value={drawNo}
|
||||
onChange={(e) => setDrawNo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t("createDraw.hint")}</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
{t("actions.cancel", { ns: "common" })}
|
||||
</Button>
|
||||
<Button type="button" disabled={saving || draw == null} onClick={() => void submit()}>
|
||||
{saving ? t("editDraw.saving") : t("editDraw.submit")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
25
src/modules/draws/draw-list-actions.ts
Normal file
25
src/modules/draws/draw-list-actions.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { AdminDrawListItem } from "@/types/api/admin-draws";
|
||||
|
||||
const CANCELLABLE_STATUSES = new Set(["pending", "open", "closing", "closed"]);
|
||||
|
||||
export function drawHasNoBets(row: AdminDrawListItem): boolean {
|
||||
return (row.total_bet_minor ?? 0) === 0;
|
||||
}
|
||||
|
||||
export function canEditDrawRow(row: AdminDrawListItem): boolean {
|
||||
if (!drawHasNoBets(row)) {
|
||||
return false;
|
||||
}
|
||||
return row.status === "pending" || row.status === "open";
|
||||
}
|
||||
|
||||
export function canDeleteDrawRow(row: AdminDrawListItem): boolean {
|
||||
return row.status === "pending" && drawHasNoBets(row);
|
||||
}
|
||||
|
||||
export function canCancelDrawRow(row: AdminDrawListItem): boolean {
|
||||
if (!drawHasNoBets(row)) {
|
||||
return false;
|
||||
}
|
||||
return CANCELLABLE_STATUSES.has(row.status);
|
||||
}
|
||||
@@ -5,13 +5,22 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { getAdminDraws, postAdminGenerateDrawPlan } from "@/api/admin-draws";
|
||||
import {
|
||||
deleteAdminDraw,
|
||||
getAdminDraws,
|
||||
postAdminBatchDestroyDraws,
|
||||
postAdminCancelDraw,
|
||||
postAdminGenerateDrawPlan,
|
||||
} from "@/api/admin-draws";
|
||||
import { formatAdminInstant } from "@/lib/admin-datetime";
|
||||
import { getAdminRequestLocale } from "@/lib/admin-locale";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -28,7 +37,13 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
import { DrawCreateDialog } from "./draw-create-dialog";
|
||||
import { DrawEditDialog } from "./draw-edit-dialog";
|
||||
import {
|
||||
canCancelDrawRow,
|
||||
canDeleteDrawRow,
|
||||
canEditDrawRow,
|
||||
} from "./draw-list-actions";
|
||||
import { formatAdminMinorUnits } from "@/lib/money";
|
||||
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||
import { useExportLabels } from "@/hooks/use-export-labels";
|
||||
@@ -72,12 +87,18 @@ export function DrawsIndexConsole() {
|
||||
const { t } = useTranslation(["draws", "common"]);
|
||||
const exportLabels = useExportLabels("drawsList");
|
||||
useAdminCurrencyCatalog();
|
||||
const formatDt = useAdminDateTimeFormatter();
|
||||
const defaultCurrency = "NPR";
|
||||
const profile = useAdminProfile();
|
||||
const canManageDraw = adminHasAnyPermission(profile?.permissions, [PRD_DRAW_RESULT_MANAGE]);
|
||||
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
|
||||
const [data, setData] = useState<AdminDrawListData | null>(null);
|
||||
const formatDt = useCallback(
|
||||
(iso: string | null | undefined) =>
|
||||
formatAdminInstant(iso, {
|
||||
locale: getAdminRequestLocale(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [draftDrawNo, setDraftDrawNo] = useState("");
|
||||
@@ -87,6 +108,10 @@ export function DrawsIndexConsole() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState<number>(10);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editDraw, setEditDraw] = useState<AdminDrawListItem | null>(null);
|
||||
const [selectedDrawIds, setSelectedDrawIds] = useState<Set<number>>(new Set());
|
||||
const [batchDeleting, setBatchDeleting] = useState(false);
|
||||
|
||||
const drawStatusTriggerLabel = useMemo(
|
||||
() =>
|
||||
@@ -149,25 +174,114 @@ export function DrawsIndexConsole() {
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [load]);
|
||||
|
||||
const handleSelectAll = useCallback((checked: boolean) => {
|
||||
if (checked && data) {
|
||||
const deletableIds = data.items
|
||||
.filter((row) => canDeleteDrawRow(row))
|
||||
.map((row) => row.id);
|
||||
setSelectedDrawIds(new Set(deletableIds));
|
||||
} else {
|
||||
setSelectedDrawIds(new Set());
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const handleSelectRow = useCallback((drawId: number, checked: boolean) => {
|
||||
setSelectedDrawIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (checked) {
|
||||
next.add(drawId);
|
||||
} else {
|
||||
next.delete(drawId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isAllSelected = useMemo(() => {
|
||||
if (!data) return false;
|
||||
const deletableIds = data.items.filter((row) => canDeleteDrawRow(row)).map((row) => row.id);
|
||||
return deletableIds.length > 0 && deletableIds.every((id) => selectedDrawIds.has(id));
|
||||
}, [data, selectedDrawIds]);
|
||||
|
||||
const isSomeSelected = useMemo(() => {
|
||||
if (!data) return false;
|
||||
const deletableIds = data.items.filter((row) => canDeleteDrawRow(row)).map((row) => row.id);
|
||||
return deletableIds.some((id) => selectedDrawIds.has(id));
|
||||
}, [data, selectedDrawIds]);
|
||||
|
||||
async function handleBatchDelete(): Promise<void> {
|
||||
if (selectedDrawIds.size === 0) return;
|
||||
|
||||
setBatchDeleting(true);
|
||||
try {
|
||||
const result = await postAdminBatchDestroyDraws(Array.from(selectedDrawIds));
|
||||
|
||||
if (result.failed.length > 0) {
|
||||
toast.error(
|
||||
t("batchDelete.partialFailed", {
|
||||
success: result.success.length,
|
||||
failed: result.failed.length,
|
||||
}),
|
||||
);
|
||||
result.failed.forEach((f) => {
|
||||
console.error(`Failed to delete draw ${f.id}: ${f.reason}`);
|
||||
});
|
||||
} else {
|
||||
toast.success(
|
||||
t("batchDelete.success", { count: result.success.length }),
|
||||
);
|
||||
}
|
||||
|
||||
setSelectedDrawIds(new Set());
|
||||
await load();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("batchDelete.failed"));
|
||||
} finally {
|
||||
setBatchDeleting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="admin-list-card">
|
||||
<CardHeader className="admin-list-header flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<CardTitle className="admin-list-title">{t("statusListTitle")}</CardTitle>
|
||||
{canManageDraw ? (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
requestConfirm({
|
||||
title: t("confirm.generatePlanTitle"),
|
||||
description: t("confirm.generatePlanDescription"),
|
||||
onConfirm: () => generatePlan(),
|
||||
})
|
||||
}
|
||||
disabled={generating}
|
||||
>
|
||||
{generating ? t("generating") : t("generatePlan")}
|
||||
</Button>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setCreateOpen(true)}>
|
||||
{t("createDraw.open")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
requestConfirm({
|
||||
title: t("confirm.generatePlanTitle"),
|
||||
description: t("confirm.generatePlanDescription"),
|
||||
onConfirm: () => generatePlan(),
|
||||
})
|
||||
}
|
||||
disabled={generating}
|
||||
>
|
||||
{generating ? t("generating") : t("generatePlan")}
|
||||
</Button>
|
||||
{selectedDrawIds.size > 0 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() =>
|
||||
requestConfirm({
|
||||
title: t("batchDelete.confirmTitle"),
|
||||
description: t("batchDelete.confirmDescription", { count: selectedDrawIds.size }),
|
||||
confirmVariant: "destructive",
|
||||
onConfirm: () => handleBatchDelete(),
|
||||
})
|
||||
}
|
||||
disabled={batchDeleting}
|
||||
>
|
||||
{batchDeleting ? t("batchDelete.deleting") : t("batchDelete.action", { count: selectedDrawIds.size })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</CardHeader>
|
||||
<CardContent className="admin-list-content">
|
||||
@@ -247,6 +361,18 @@ export function DrawsIndexConsole() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data?.schedule ? (
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
<p>
|
||||
{t("scheduleTimezoneHint", {
|
||||
tz: "Local",
|
||||
interval: data.schedule.interval_minutes,
|
||||
})}
|
||||
</p>
|
||||
{canManageDraw ? <p>{t("listActionsHint")}</p> : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
) : null}
|
||||
@@ -255,6 +381,15 @@ export function DrawsIndexConsole() {
|
||||
<Table id="draws-index-table">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-12">
|
||||
{canManageDraw && data && data.items.some((row) => canDeleteDrawRow(row)) ? (
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={(checked) => handleSelectAll(checked === true)}
|
||||
indeterminate={isSomeSelected && !isAllSelected}
|
||||
/>
|
||||
) : null}
|
||||
</TableHead>
|
||||
<TableHead>{t("drawNo")}</TableHead>
|
||||
<TableHead>{t("startTime")}</TableHead>
|
||||
<TableHead>{t("closeTime")}</TableHead>
|
||||
@@ -269,19 +404,27 @@ export function DrawsIndexConsole() {
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-muted-foreground">
|
||||
<TableCell colSpan={10} className="text-muted-foreground">
|
||||
{t("states.loading", { ns: "common" })}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : data === null || data.items.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-muted-foreground">
|
||||
<TableCell colSpan={10} className="text-muted-foreground">
|
||||
{t("states.noData", { ns: "common" })}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
data.items.map((row: AdminDrawListItem) => (
|
||||
<TableRow key={row.id}>
|
||||
<TableCell className="w-12">
|
||||
{canManageDraw && canDeleteDrawRow(row) ? (
|
||||
<Checkbox
|
||||
checked={selectedDrawIds.has(row.id)}
|
||||
onCheckedChange={(checked) => handleSelectRow(row.id, checked === true)}
|
||||
/>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">{row.draw_no}</TableCell>
|
||||
<TableCell className="text-sm">{formatDt(row.start_time)}</TableCell>
|
||||
<TableCell className="text-sm">{formatDt(row.close_time)}</TableCell>
|
||||
@@ -313,12 +456,85 @@ export function DrawsIndexConsole() {
|
||||
: "—"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Link
|
||||
href={`/admin/draws/${row.id}`}
|
||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
|
||||
>
|
||||
{t("viewDetails")}
|
||||
</Link>
|
||||
<div className="flex flex-wrap items-center justify-end gap-1.5">
|
||||
<Link
|
||||
href={`/admin/draws/${row.id}`}
|
||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
|
||||
>
|
||||
{t("viewDetails")}
|
||||
</Link>
|
||||
{canManageDraw && canEditDrawRow(row) ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setEditDraw(row)}
|
||||
>
|
||||
{t("editDraw.action")}
|
||||
</Button>
|
||||
) : null}
|
||||
{canManageDraw && canDeleteDrawRow(row) ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
requestConfirm({
|
||||
title: t("deleteDraw.title"),
|
||||
description: t("deleteDraw.description", { drawNo: row.draw_no }),
|
||||
confirmVariant: "destructive",
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await deleteAdminDraw(row.id);
|
||||
toast.success(t("deleteDraw.success"));
|
||||
await load();
|
||||
} catch (e) {
|
||||
toast.error(
|
||||
e instanceof LotteryApiBizError
|
||||
? e.message
|
||||
: t("deleteDraw.failed"),
|
||||
);
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("deleteDraw.action")}
|
||||
</Button>
|
||||
) : null}
|
||||
{canManageDraw &&
|
||||
canCancelDrawRow(row) &&
|
||||
!canDeleteDrawRow(row) ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
requestConfirm({
|
||||
title: t("cancelFromList.title"),
|
||||
description: t("cancelFromList.description", {
|
||||
drawNo: row.draw_no,
|
||||
}),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await postAdminCancelDraw(row.id);
|
||||
toast.success(t("actionSuccess", { name: t("cancelDraw") }));
|
||||
await load();
|
||||
} catch (e) {
|
||||
toast.error(
|
||||
e instanceof LotteryApiBizError
|
||||
? e.message
|
||||
: t("actionFailed", { name: t("cancelDraw") }),
|
||||
);
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("cancelFromList.action")}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
@@ -344,6 +560,25 @@ export function DrawsIndexConsole() {
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{canManageDraw ? (
|
||||
<DrawCreateDialog
|
||||
open={createOpen}
|
||||
onOpenChange={setCreateOpen}
|
||||
onCreated={load}
|
||||
/>
|
||||
) : null}
|
||||
{canManageDraw ? (
|
||||
<DrawEditDialog
|
||||
open={editDraw != null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setEditDraw(null);
|
||||
}
|
||||
}}
|
||||
draw={editDraw}
|
||||
onSaved={load}
|
||||
/>
|
||||
) : null}
|
||||
<ConfirmDialog />
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user