feat(agents, config, dashboard, i18n): add agent line provision wizard, site deletion, and site dashboard with multi-language support

Added agent line provision wizard page with permission gating, replacing redirect placeholder. Introduced site deletion API and UI with confirmation dialog in integration sites management. Added new site-scoped dashboard panel showing bet metrics, P/L trends, active players, and quick links. Enhanced chart tooltip to support custom formatters and fix indicator color
This commit is contained in:
2026-06-12 20:47:53 +08:00
parent 24fd7c10bd
commit 6ea0a6feec
48 changed files with 1573 additions and 629 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { Copy, Download, Link2, Pencil, ShieldAlert } from "lucide-react";
import { Copy, Download, Link2, Pencil, ShieldAlert, Trash2 } from "lucide-react";
import { useCallback, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncEffect } from "@/hooks/use-async-effect";
@@ -8,6 +8,7 @@ import { useTranslationRef } from "@/hooks/use-translation-ref";
import { toast } from "sonner";
import {
deleteAdminIntegrationSite,
getAdminIntegrationSite,
getAdminIntegrationSiteExport,
getAdminIntegrationSites,
@@ -243,6 +244,8 @@ export function IntegrationSitesConsole({
const [editingId, setEditingId] = useState<number | null>(null);
const [form, setForm] = useState<FormState>(EMPTY_FORM);
const [rotateTarget, setRotateTarget] = useState<AdminIntegrationSiteRow | null>(null);
const [deleteTarget, setDeleteTarget] = useState<AdminIntegrationSiteRow | null>(null);
const [deleteBusy, setDeleteBusy] = useState(false);
const [rotateBusy, setRotateBusy] = useState(false);
const [secretsDialog, setSecretsDialog] = useState<{
siteCode: string;
@@ -388,6 +391,25 @@ export function IntegrationSitesConsole({
}
}
async function confirmDelete(): Promise<void> {
if (!deleteTarget || !canManage) return;
setDeleteBusy(true);
try {
await deleteAdminIntegrationSite(deleteTarget.id);
toast.success(t("integrationSites.deleteSuccess", { code: deleteTarget.code }));
secretsCacheRef.current.delete(deleteTarget.id);
setDeleteTarget(null);
await load();
} catch (error) {
toast.error(
error instanceof LotteryApiBizError ? error.message : t("integrationSites.deleteFailed"),
);
} finally {
setDeleteBusy(false);
}
}
function openConnectivity(row: AdminIntegrationSiteRow): void {
setConnectivityTarget(row);
setConnectivityPlayerId("10001");
@@ -519,7 +541,13 @@ export function IntegrationSitesConsole({
{loading ? (
<AdminLoadingState minHeight="8rem" label={t("integrationSites.loading")} />
) : items.length === 0 ? (
<AdminNoResourceState />
<AdminNoResourceState message={t("integrationSites.empty")}>
{canCreate ? (
<Button type="button" size="sm" onClick={openCreate}>
{t("integrationSites.create")}
</Button>
) : null}
</AdminNoResourceState>
) : (
<div className="overflow-x-auto">
<Table>
@@ -637,6 +665,14 @@ export function IntegrationSitesConsole({
hidden: !canManage,
onClick: () => setRotateTarget(row),
},
{
key: "delete",
label: t("integrationSites.delete"),
icon: Trash2,
destructive: true,
hidden: !canManage,
onClick: () => setDeleteTarget(row),
},
]}
/>
</TableCell>
@@ -850,6 +886,33 @@ export function IntegrationSitesConsole({
</DialogContent>
</Dialog>
<Dialog open={deleteTarget !== null} onOpenChange={(open) => !open && setDeleteTarget(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("integrationSites.deleteConfirmTitle")}</DialogTitle>
<DialogDescription>
{t("integrationSites.deleteConfirmDescription", {
code: deleteTarget?.code ?? "",
name: deleteTarget?.name ?? "",
})}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setDeleteTarget(null)}>
{t("integrationSites.cancel")}
</Button>
<Button
type="button"
variant="destructive"
disabled={deleteBusy}
onClick={() => void confirmDelete()}
>
{deleteBusy ? t("integrationSites.deleting") : t("integrationSites.deleteConfirm")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog
open={connectivityTarget !== null}
onOpenChange={(open) => {