import { useMemo, useState } from "react"; import { ApiError } from "../../api/client"; import type { SupportCode } from "../../api/types"; import { useAuth } from "../../auth/AuthContext"; import { PageHeader } from "../../components/layout/PageHeader"; import { PlusIcon, RefreshIcon, SearchIcon, TrashIcon } from "../../components/layout/icons"; import { Badge } from "../../components/ui/Badge"; import { Button } from "../../components/ui/Button"; import { Input } from "../../components/ui/Input"; import { Panel } from "../../components/ui/Panel"; import { EmptyState, ErrorState } from "../../components/ui/States"; import { codeLabel, codeTone } from "../../components/ui/status"; import { Table, type Column } from "../../components/ui/Table"; import { TableSkeleton } from "../../components/ui/TableSkeleton"; import { absoluteTime, relativeTime } from "../../lib/time"; import { CancelCodeDialog } from "./CancelCodeDialog"; import { GenerateCodeModal } from "./GenerateCodeModal"; import { useSupportCodes } from "./hooks"; import "./codes.css"; /** A code is still cancellable only while it is pending or connected. */ function canCancel(status: string): boolean { return status === "pending" || status === "connected"; } export function SupportCodesPage() { const { user } = useAuth(); const codesQuery = useSupportCodes(); const [filter, setFilter] = useState(""); const [generating, setGenerating] = useState(false); const [cancelFor, setCancelFor] = useState(null); const { data } = codesQuery; const codes = useMemo(() => data ?? [], [data]); // Newest first: the in-memory map the server returns has no guaranteed order, // and the code a tech just generated should be at the top where they expect // it. const sorted = useMemo( () => [...codes].sort( (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), ), [codes], ); const filtered = useMemo(() => { const q = filter.trim().toLowerCase(); if (!q) return sorted; return sorted.filter( (c) => c.code.toLowerCase().includes(q) || c.created_by.toLowerCase().includes(q) || (c.client_machine?.toLowerCase().includes(q) ?? false), ); }, [sorted, filter]); const pendingCount = useMemo( () => codes.filter((c) => c.status === "pending").length, [codes], ); const columns: Column[] = [ { key: "code", header: "Code", render: (c) => {c.code}, }, { key: "status", header: "Status", render: (c) => ( {codeLabel(c.status)} ), }, { key: "bound", header: "Bound to", render: (c) => c.client_machine || c.client_name ? (
{c.client_machine ?? c.client_name} {c.client_machine && c.client_name && ( {c.client_name} )}
) : ( Not redeemed ), }, { key: "created_by", header: "Created by", render: (c) => {c.created_by}, }, { key: "created", header: "Created", render: (c) => ( {relativeTime(c.created_at)} ), }, { key: "actions", header: "", cellClass: "dt__actions", render: (c) => { const cancellable = canCancel(c.status); return ( e.stopPropagation()}> ); }, }, ]; return (
} />
setFilter(e.target.value)} aria-label="Filter support codes" />
{pendingCount} awaiting redeem ยท{" "} {codes.length} active
{codesQuery.isLoading ? ( <> Loading support codes ) : codesQuery.isError ? ( void codesQuery.refetch()}> Retry } /> ) : filtered.length === 0 ? ( filter ? ( setFilter("")}> Clear filter } /> ) : ( setGenerating(true)}> Generate code } /> ) ) : ( c.code} /> )} setGenerating(false)} /> setCancelFor(null)} /> ); }