'use client'; import { useState, useEffect, useMemo } from 'react'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Search, ChevronRight, ChevronDown, Server, Monitor, Mail, Package, Loader2, } from 'lucide-react'; interface ServiceLine { cs_id: number; contract_id: number; contract_name: string; line_name: string; unit_price: number | null; unit_cost: number | null; category: 'server' | 'workstation' | 'm365' | 'other'; } interface CoverageRow { company_id: number; company_name: string; contracted: { servers: number; workstations: number; m365: number; other: number }; deployed: { servers: number; workstations: number; other: number }; lines: ServiceLine[]; } const CATEGORY_COLORS: Record = { server: 'bg-blue-500/10 text-blue-600 dark:text-blue-400', workstation: 'bg-purple-500/10 text-purple-600 dark:text-purple-400', m365: 'bg-amber-500/10 text-amber-600 dark:text-amber-400', other: 'bg-muted text-muted-foreground', }; const CATEGORY_LABELS: Record = { server: 'Server', workstation: 'Workstation', m365: 'M365', other: 'Other', }; function DeltaBadge({ contracted, deployed }: { contracted: number; deployed: number }) { const delta = deployed - contracted; if (contracted === 0 && deployed === 0) return ; if (delta === 0) return ; if (delta > 0) return ( +{delta} ); return ( {delta} ); } function CountCell({ contracted, deployed, }: { contracted: number; deployed: number; }) { const delta = deployed - contracted; const hasData = contracted > 0 || deployed > 0; if (!hasData) return ; const color = delta === 0 ? 'text-green-600 dark:text-green-400' : delta > 0 ? 'text-amber-600 dark:text-amber-400' : 'text-red-600 dark:text-red-400'; return ( {deployed}/{contracted} ); } function ClientRow({ row }: { row: CoverageRow }) { const [expanded, setExpanded] = useState(false); const hasAnyData = row.contracted.servers + row.contracted.workstations + row.contracted.m365 + row.deployed.servers + row.deployed.workstations > 0; // Group service lines by contract const byContract = useMemo(() => { const map = new Map(); for (const l of row.lines) { if (!map.has(l.contract_id)) { map.set(l.contract_id, { contract_name: l.contract_name, lines: [] }); } map.get(l.contract_id)!.lines.push(l); } return [...map.values()]; }, [row.lines]); return ( <> setExpanded((v) => !v)} > {/* Expand toggle + Client */}
{expanded ? ( ) : ( )} {row.company_name}
{/* Servers deployed/contracted */} {/* Workstations deployed/contracted */} {/* M365 contracted (no Veeam deployed count for M365) */} {row.contracted.m365 > 0 ? ( {row.contracted.m365} ) : ( )} {/* Total service lines */} {row.lines.length}
{/* Expanded detail rows */} {expanded && (
{byContract.length === 0 ? (

No contract service lines found.

) : ( byContract.map((contract) => { const backupLines = contract.lines.filter( (l) => l.category === 'server' || l.category === 'workstation' ); if (backupLines.length === 0) return null; return (

{contract.contract_name}

{backupLines.map((line) => ( ))}
Service Category
{line.category === 'server' ? ( ) : ( )} {line.line_name} {CATEGORY_LABELS[line.category]}
); }) )}
)} ); } export function ContractCoverageTable() { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [filter, setFilter] = useState<'all' | 'gap' | 'over' | 'matched'>('all'); useEffect(() => { fetch('/api/veeam/contract-coverage') .then((r) => r.json()) .then((d) => setRows(d.rows ?? [])) .catch(console.error) .finally(() => setLoading(false)); }, []); const filtered = useMemo(() => { return rows.filter((r) => { const matchesSearch = r.company_name.toLowerCase().includes(search.toLowerCase()); if (!matchesSearch) return false; if (filter === 'all') return true; const serverDelta = r.deployed.servers - r.contracted.servers; const wsDelta = r.deployed.workstations - r.contracted.workstations; if (filter === 'gap') return serverDelta < 0 || wsDelta < 0; if (filter === 'over') return serverDelta > 0 || wsDelta > 0; if (filter === 'matched') return ( r.contracted.servers > 0 || r.contracted.workstations > 0 ? serverDelta === 0 && wsDelta === 0 : false ); return true; }); }, [rows, search, filter]); const FILTERS: { key: typeof filter; label: string }[] = [ { key: 'all', label: 'All' }, { key: 'gap', label: 'Under-deployed' }, { key: 'over', label: 'Over-deployed' }, { key: 'matched', label: 'Matched' }, ]; return (
{/* Toolbar */}
setSearch(e.target.value)} className="pl-9 h-8 text-sm" />
{FILTERS.map((f) => ( ))}
{filtered.length} clients
{/* Legend */}
Counts: deployed / contracted Matched Over-deployed Gap
{/* Table */}
{loading ? (
) : ( Client
Servers
Workstations
M365
Lines
{filtered.length === 0 ? ( No clients match the current filter ) : ( filtered.map((row) => ) )}
)}
); }