'use client'; import { useEffect, useState, useCallback, useMemo } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from '@/components/ui/dialog'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { RefreshCw, CheckCircle2, AlertTriangle, WifiOff, GitCompare, Ticket, ChevronRight, ChevronDown, Sparkles, Loader2, } from 'lucide-react'; import { Skeleton } from '@/components/ui/skeleton'; // ── Types ───────────────────────────────────────────────────────────────────── type MatchStatus = 'both' | 'pulse_only' | 'datto_only' | 'offline_suppressed'; interface AtTicket { ticket_number: string; title: string; status: number | null; priority: number | null; created_at: string | null; completed_at: string | null; datto_source: string | null; } interface MatchRow { key: string; hostname: string | null; org_name: string | null; company_id: number | null; status: MatchStatus; pulse: { job_name: string; priority_level: string; hours_overdue: number; failure_category: string | null; opened_at: string; } | null; offline: { hours_offline: number; last_suppressed: string } | null; at_ticket_count: number; at_open_count: number; at_tickets: AtTicket[]; } interface ClientGroup { org_name: string | null; company_id: number | null; rows: MatchRow[]; counts: Record; totalAtTickets: number; totalAtOpen: number; } interface ComparisonData { period: string; summary: { pulse_open: number; datto_at_total: number; datto_at_open: number; both: number; pulse_only: number; datto_only: number; offline_suppressed: number; }; matches: MatchRow[]; } interface AnalysisResult { ticket_number: string; hostname: string; org_name: string; hours_offline: number | null; total_hours_logged: number; note_count: number; model: string; analysis: { would_suppress_correctly?: boolean; confidence?: string; work_summary?: string; reasoning?: string; recommendation?: string; raw?: string; }; } // ── Constants ───────────────────────────────────────────────────────────────── const PERIODS = [ { value: '1d', label: 'Last 24h' }, { value: '7d', label: 'Last 7d' }, { value: '14d', label: 'Last 14d' }, { value: '30d', label: 'Last 30d' }, ]; const CLOSED_STATUSES = [5, 29832279, 29832280]; const STATUS_CONFIG: Record = { both: { label: 'Both', badgeVariant: 'default', rowAccent: 'border-l-2 border-l-blue-500/50' }, pulse_only: { label: 'Pulse Only', badgeVariant: 'destructive', rowAccent: 'border-l-2 border-l-destructive/50' }, datto_only: { label: 'Datto/AT', badgeVariant: 'secondary', rowAccent: 'border-l-2 border-l-orange-400/50' }, offline_suppressed: { label: 'Offline', badgeVariant: 'outline', rowAccent: 'border-l-2 border-l-muted-foreground/40' }, }; // ── Helpers ─────────────────────────────────────────────────────────────────── function timeAgo(dateStr: string | null | undefined): string { if (!dateStr) return '—'; const diff = Date.now() - new Date(dateStr).getTime(); const h = Math.floor(diff / 3_600_000); if (h < 1) return 'Just now'; if (h < 24) return `${h}h ago`; return `${Math.floor(h / 24)}d ago`; } function atStatusOpen(status: number | null): boolean { return !CLOSED_STATUSES.includes(status ?? -1); } function atStatusLabel(status: number | null): string { const map: Record = { 1: 'New', 5: 'Complete', 8: 'In Progress', 47: 'Waiting' }; return map[status ?? -1] ?? (status != null ? `#${status}` : '?'); } function PriorityBadge({ level }: { level: string }) { const cls = level === 'critical' ? 'text-destructive border-destructive' : level === 'high' ? 'text-orange-500 border-orange-500' : 'text-muted-foreground border-muted-foreground/40'; return {level}; } // ── Analyze Dialog ──────────────────────────────────────────────────────────── function AnalyzeDialog({ ticket, hostname, orgName, hoursOffline, onClose, }: { ticket: AtTicket; hostname: string; orgName: string; hoursOffline?: number; onClose: () => void; }) { const [loading, setLoading] = useState(true); const [result, setResult] = useState(null); const [error, setError] = useState(null); useEffect(() => { fetch('/api/veeam/rpo-analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ at_ticket_number: ticket.ticket_number, hostname, org_name: orgName, hours_offline: hoursOffline, }), }) .then(r => r.json()) .then(d => { if (d.error) setError(d.error); else setResult(d); }) .catch(e => setError(e.message)) .finally(() => setLoading(false)); }, [ticket.ticket_number, hostname, orgName, hoursOffline]); const a = result?.analysis; const suppressed = a?.would_suppress_correctly; return ( RPO Suppression Analysis {ticket.ticket_number} · {hostname} · {orgName} {loading && (
Analyzing ticket work...
)} {error && (
{error}
)} {result && a && (
{/* Verdict */}
{suppressed ? : }
{suppressed ? 'Suppression would have been correct' : 'Human intervention was needed'}
Confidence: {a.confidence ?? 'unknown'} {result.total_hours_logged > 0 && ` · ${result.total_hours_logged.toFixed(2)}h logged`} {result.note_count > 0 && ` · ${result.note_count} note${result.note_count !== 1 ? 's' : ''}`}
{/* Work summary */} {a.work_summary && (
Work Summary

{a.work_summary}

)} {/* Reasoning */} {a.reasoning && (
Reasoning

{a.reasoning}

)} {/* Recommendation */} {a.recommendation && (

{a.recommendation}

)} {a.raw &&
{a.raw}
}
Model: {result.model}
)}
); } // ── AT Ticket Cell ──────────────────────────────────────────────────────────── function AtTicketCell({ tickets, ticketCount, hostname, orgName, hoursOffline }: { tickets: AtTicket[]; ticketCount: number; hostname: string | null; orgName: string | null; hoursOffline?: number; }) { const [analyzing, setAnalyzing] = useState(null); if (tickets.length === 0) return ; return ( <>
{tickets.map((t, i) => { const isOpen = atStatusOpen(t.status); const isClosed = !isOpen; return (
{t.ticket_number} {atStatusLabel(t.status)} {t.datto_source && ( {t.datto_source} )} {timeAgo(t.created_at)} {isClosed && hostname && ( )}
); })} {ticketCount > 5 && (
+{ticketCount - 5} more
)}
{analyzing && ( setAnalyzing(null)} /> )} ); } // ── Client Group Row ────────────────────────────────────────────────────────── function ClientGroupRow({ group, defaultOpen }: { group: ClientGroup; defaultOpen: boolean }) { const [open, setOpen] = useState(defaultOpen); const actionable = group.counts.both + group.counts.pulse_only; return ( <> {/* Group summary header — columns align with the detail table below */} setOpen(o => !o)} > {/* Device col: chevron + org name */}
{open ? : } {group.org_name ?? 'Unknown'}
{/* Match col: status pills */}
{group.counts.both > 0 && ( {group.counts.both} Both )} {group.counts.pulse_only > 0 && ( {group.counts.pulse_only} Pulse )} {group.counts.datto_only > 0 && ( {group.counts.datto_only} Datto )} {group.counts.offline_suppressed > 0 && ( {group.counts.offline_suppressed} Offline )}
{/* Pulse shadow col: total devices flagged */} {actionable > 0 ? {actionable} device{actionable !== 1 ? 's' : ''} need attention : {group.rows.length} device{group.rows.length !== 1 ? 's' : ''}} {/* AT tickets col: ticket count */} {group.totalAtTickets > 0 ? <>{group.totalAtTickets} ticket{group.totalAtTickets !== 1 ? 's' : ''}{group.totalAtOpen > 0 && · {group.totalAtOpen} open} : }
{/* Device detail rows */} {open && group.rows.map((row) => { const cfg = STATUS_CONFIG[row.status]; return ( {row.hostname ?? unknown} {cfg.label} {row.pulse ? ( <>
{row.pulse.hours_overdue}h overdue
{row.pulse.failure_category ?? '—'}
since {timeAgo(row.pulse.opened_at)}
) : row.offline ? ( offline {Math.round(row.offline.hours_offline)}h ) : ( )}
); })} ); } // ── Page ────────────────────────────────────────────────────────────────────── export default function VeeamComparisonPage() { const [data, setData] = useState(null); const [period, setPeriod] = useState('7d'); const [loading, setLoading] = useState(true); const [filter, setFilter] = useState('all'); const fetchData = useCallback(async () => { setLoading(true); try { const res = await fetch(`/api/veeam/rpo-comparison?period=${period}`); setData(await res.json()); } finally { setLoading(false); } }, [period]); useEffect(() => { fetchData(); }, [fetchData]); const groups = useMemo(() => { if (!data) return []; const filtered = data.matches.filter(m => filter === 'all' || m.status === filter); const byOrg = new Map(); for (const row of filtered) { const key = row.org_name ?? '(Unknown)'; if (!byOrg.has(key)) { byOrg.set(key, { org_name: row.org_name, company_id: row.company_id, rows: [], counts: { both: 0, pulse_only: 0, datto_only: 0, offline_suppressed: 0 }, totalAtTickets: 0, totalAtOpen: 0, }); } const g = byOrg.get(key)!; g.rows.push(row); g.counts[row.status]++; g.totalAtTickets += row.at_ticket_count; g.totalAtOpen += row.at_open_count; } return Array.from(byOrg.values()).sort((a, b) => { const aScore = (a.counts.both + a.counts.pulse_only) > 0 ? 1 : 0; const bScore = (b.counts.both + b.counts.pulse_only) > 0 ? 1 : 0; if (bScore !== aScore) return bScore - aScore; return (a.org_name ?? '').localeCompare(b.org_name ?? ''); }); }, [data, filter]); const filteredTotal = data?.matches.filter(m => filter === 'all' || m.status === filter).length ?? 0; return (
{/* Header */}

Veeam RPO — Shadow vs Datto/AT

What Pulse would ticket (shadow mode) vs what Datto RMM actually created in Autotask. Click Analyze on any closed ticket to evaluate suppression accuracy.

{PERIODS.map(p => ( ))}
{loading && !data ? (
{[...Array(4)].map((_, i) => )}
) : data && ( <> {/* Summary cards */}
{([ { key: 'both', label: 'Both Agree', icon: CheckCircle2, iconCls: 'text-blue-500', value: data.summary.both, sub: 'Pulse + Datto both flagged', valCls: '' }, { key: 'pulse_only', label: 'Pulse Only', icon: AlertTriangle,iconCls: 'text-destructive', value: data.summary.pulse_only, sub: 'No AT ticket from Datto', valCls: 'text-destructive' }, { key: 'datto_only', label: 'Datto / AT Only', icon: Ticket, iconCls: 'text-orange-500', value: data.summary.datto_only, sub: `${data.summary.datto_at_total} total · ${data.summary.datto_at_open} open`, valCls: 'text-orange-500' }, { key: 'offline_suppressed', label: 'Offline Suppressed',icon: WifiOff, iconCls: 'text-muted-foreground',value: data.summary.offline_suppressed, sub: 'Device offline — suppressed', valCls: '' }, ] as const).map(({ key, label, icon: Icon, iconCls, value, sub, valCls }) => ( setFilter(key as any)}> {label}
{value}

{sub}

))}
{/* Table */} setFilter(v as any)}> All {data.matches.length} Both ({data.summary.both}) Pulse Only ({data.summary.pulse_only}) Datto/AT ({data.summary.datto_only}) Offline ({data.summary.offline_suppressed})
Device Match Pulse Shadow Autotask Tickets {groups.length > 0 ? groups.map(group => ( 0} /> )) : ( {data.matches.length === 0 ? 'No data yet — RPO check must run at least once.' : 'No rows match this filter.'} )}
{groups.length > 0 && (

{groups.length} client{groups.length !== 1 ? 's' : ''} · {filteredTotal} device{filteredTotal !== 1 ? 's' : ''}

)}
)}
); }