import React, { useCallback, useEffect, useMemo, useState, useRef } from "react"; import { Box, Paper, Typography, Button, Stack, Alert, FormControl, InputLabel, MenuItem, Select, CircularProgress, Tooltip } from "@mui/material"; import { ContentCopy as CopyIcon, DeleteOutline as DeleteIcon, Refresh as RefreshIcon, Key as KeyIcon, } from "@mui/icons-material"; import { AgGridReact } from "ag-grid-react"; import { ModuleRegistry, AllCommunityModule, themeQuartz } from "ag-grid-community"; // IMPORTANT: Do NOT import global AG Grid CSS here to avoid overriding other pages. // We rely on the project's existing CSS and themeQuartz class name like other MagicUI pages. ModuleRegistry.registerModules([AllCommunityModule]); // Match the palette used on other pages (see Site_List / Device_List) const MAGIC_UI = { shellBg: "radial-gradient(120% 120% at 0% 0%, rgba(76, 186, 255, 0.16), transparent 55%), " + "radial-gradient(120% 120% at 100% 0%, rgba(214, 130, 255, 0.18), transparent 60%), #040711", panelBg: "linear-gradient(135deg, rgba(10, 16, 31, 0.98) 0%, rgba(6, 10, 24, 0.94) 60%, rgba(15, 6, 26, 0.96) 100%)", panelBorder: "rgba(148, 163, 184, 0.35)", textBright: "#e2e8f0", textMuted: "#94a3b8", accentA: "#7dd3fc", accentB: "#c084fc", }; // Generate a scoped Quartz theme class (same pattern as other pages) const gridTheme = themeQuartz.withParams({ accentColor: "#8b5cf6", backgroundColor: "#070b1a", browserColorScheme: "dark", fontFamily: { googleFont: "IBM Plex Sans" }, foregroundColor: "#f4f7ff", headerFontSize: 13, }); const themeClassName = gridTheme.themeName || "ag-theme-quartz"; const TTL_PRESETS = [ { value: 1, label: "1 hour" }, { value: 3, label: "3 hours" }, { value: 6, label: "6 hours" }, { value: 12, label: "12 hours" }, { value: 24, label: "24 hours" }, ]; const determineStatus = (record) => { if (!record) return "expired"; const maxUses = Number.isFinite(record?.max_uses) ? record.max_uses : 1; const useCount = Number.isFinite(record?.use_count) ? record.use_count : 0; if (useCount >= Math.max(1, maxUses || 1)) return "used"; if (!record.expires_at) return "expired"; const expires = new Date(record.expires_at); if (Number.isNaN(expires.getTime())) return "expired"; return expires.getTime() > Date.now() ? "active" : "expired"; }; const formatDateTime = (value) => { if (!value) return "—"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString(); }; const maskCode = (code) => { if (!code) return "—"; const parts = code.split("-"); if (parts.length <= 1) { const prefix = code.slice(0, 4); return `${prefix}${"•".repeat(Math.max(0, code.length - prefix.length))}`; } return parts .map((part, idx) => (idx === 0 || idx === parts.length - 1 ? part : "•".repeat(part.length))) .join("-"); }; export default function EnrollmentCodes() { const [codes, setCodes] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [feedback, setFeedback] = useState(null); const [statusFilter, setStatusFilter] = useState("all"); const [ttlHours, setTtlHours] = useState(6); const [generating, setGenerating] = useState(false); const [maxUses, setMaxUses] = useState(2); const gridRef = useRef(null); const fetchCodes = useCallback(async () => { setLoading(true); setError(""); try { const query = statusFilter === "all" ? "" : `?status=${encodeURIComponent(statusFilter)}`; const resp = await fetch(`/api/admin/enrollment-codes${query}`, { credentials: "include" }); if (!resp.ok) { const body = await resp.json().catch(() => ({})); throw new Error(body.error || `Request failed (${resp.status})`); } const data = await resp.json(); setCodes(Array.isArray(data.codes) ? data.codes : []); } catch (err) { setError(err.message || "Unable to load codes"); } finally { setLoading(false); } }, [statusFilter]); useEffect(() => { fetchCodes(); }, [fetchCodes]); const handleGenerate = useCallback(async () => { setGenerating(true); try { const resp = await fetch("/api/admin/enrollment-codes", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ttl_hours: ttlHours, max_uses: maxUses }), }); if (!resp.ok) { const body = await resp.json().catch(() => ({})); throw new Error(body.error || `Request failed (${resp.status})`); } await fetchCodes(); setFeedback({ type: "success", message: "New installer code created" }); } catch (err) { setFeedback({ type: "error", message: err.message }); } finally { setGenerating(false); } }, [ttlHours, maxUses, fetchCodes]); const handleCopy = (code) => { if (!code) return; try { if (navigator.clipboard?.writeText) { navigator.clipboard.writeText(code); setFeedback({ type: "success", message: "Code copied to clipboard" }); } } catch (_) {} }; const handleDelete = async (id) => { if (!id) return; if (!window.confirm("Delete this installer code?")) return; try { const resp = await fetch(`/api/admin/enrollment-codes/${id}`, { method: "DELETE", credentials: "include", }); if (!resp.ok) { const body = await resp.json().catch(() => ({})); throw new Error(body.error || `Request failed (${resp.status})`); } await fetchCodes(); setFeedback({ type: "success", message: "Code deleted" }); } catch (err) { setFeedback({ type: "error", message: err.message }); } }; const columns = useMemo(() => [ { headerName: "Status", field: "status", cellRenderer: (params) => { const status = determineStatus(params.data); const color = status === "active" ? "#34d399" : status === "used" ? "#7dd3fc" : "#fbbf24"; return {status}; }, minWidth: 100 }, { headerName: "Installer Code", field: "code", cellRenderer: (params) => ( {maskCode(params.value)} ), minWidth: 340 }, { headerName: "Expires At", field: "expires_at", valueFormatter: p => formatDateTime(p.value) }, { headerName: "Created By", field: "created_by_user_id" }, { headerName: "Usage", valueGetter: (p) => `${p.data.use_count || 0} / ${p.data.max_uses || 1}`, cellStyle: { fontFamily: "monospace" }, width: 120 }, { headerName: "Last Used", field: "last_used_at", valueFormatter: p => formatDateTime(p.value) }, { headerName: "Used By GUID", field: "used_by_guid" }, { headerName: "Actions", cellRenderer: (params) => { const record = params.data; const disableDelete = (record.use_count || 0) !== 0; return ( ); }, width: 160 } ], []); const defaultColDef = useMemo(() => ({ sortable: true, filter: true, resizable: true, flex: 1, minWidth: 140, }), []); return ( {/* Hero header */} Enrollment Installer Codes {/* Controls */} Status Duration Allowed Uses {feedback && ( setFeedback(null)}> {feedback.message} )} {error && ( {error} )} {/* Grid wrapper — all overrides are SCOPED to this instance via inline CSS vars */} ); }