import React, { useState, useEffect, useCallback } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { AppLayout } from '../../components/layout/AppLayout';
import {
getRevenueLeaks,
scanLeaks,
getRemediation,
applyRemediation,
getResolutionHistory,
getRecurringLeaks,
getResolutionStats,
getSeverityRules,
updateSeverityRules,
getAlertSettings,
updateAlertSettings,
resolveRevenueLeak,
getLocationRollup,
getLocationComparison,
runLocationDetector,
} from '../../api/revenueLeaks';
import type {
RevenueLeak,
LeakSeverity,
ScanResult,
RemediationResponse,
RemediationSuggestion,
ResolutionHistoryResponse,
ResolutionEntry,
RecurringLeaksResponse,
RecurringLeakGroup,
StatsResponse,
SeverityRulesResponse,
SeverityRule,
AlertSettingsResponse,
LocationLocationsResponse,
LocationComparisonResponse,
LocationDetectorResponse,
} from '../../types';
import {
AlertTriangle,
Search,
Filter,
CheckCircle,
AlertCircle,
Eye,
X,
RefreshCw,
Zap,
Repeat,
BarChart3,
Settings,
ChevronRight,
Wrench,
Clock,
TrendingDown,
TrendingUp,
Shield,
Bell,
Play,
Loader2,
MapPin,
Building2,
AlertOctagon,
Activity,
} from 'lucide-react';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const severityColors: Record<LeakSeverity, string> = {
critical: 'bg-red-100 text-red-700 border-red-200',
high: 'bg-orange-100 text-orange-700 border-orange-200',
medium: 'bg-amber-100 text-amber-700 border-amber-200',
low: 'bg-blue-100 text-blue-700 border-blue-200',
};
const severityDot: Record<LeakSeverity, string> = {
critical: 'bg-red-500',
high: 'bg-orange-500',
medium: 'bg-amber-500',
low: 'bg-blue-500',
};
type TabKey = 'leaks' | 'recurring' | 'locations' | 'stats' | 'settings';
const TABS: { key: TabKey; label: string; icon: React.ElementType }[] = [
{ key: 'leaks', label: 'Leaks', icon: AlertTriangle },
{ key: 'recurring', label: 'Recurring', icon: Repeat },
{ key: 'locations', label: 'Locations', icon: Building2 },
{ key: 'stats', label: 'Stats', icon: BarChart3 },
{ key: 'settings', label: 'Settings', icon: Settings },
];
// ---------------------------------------------------------------------------
// Helper components
// ---------------------------------------------------------------------------
function SeverityBadge({ severity, size = 'md' }: { severity: LeakSeverity; size?: 'sm' | 'md' }) {
const sizeClass = size === 'sm' ? 'px-1.5 py-0 text-[11px]' : 'px-2 py-0.5 text-xs';
return (
<span className={`inline-flex items-center gap-1 rounded-full border font-medium ${severityColors[severity]} ${sizeClass}`}>
<span className={`inline-block h-1.5 w-1.5 rounded-full ${severityDot[severity]}`} />
{severity.charAt(0).toUpperCase() + severity.slice(1)}
</span>
);
}
function StatCard({ label, value, sub, color }: { label: string; value: string; sub?: string; color: string }) {
return (
<div className="rounded-xl border border-surface-200 bg-white p-5">
<p className="text-sm font-medium text-surface-500">{label}</p>
<p className={`mt-2 text-3xl font-bold ${color}`}>{value}</p>
{sub && <p className="mt-1 text-xs text-surface-400">{sub}</p>}
</div>
);
}
function EmptyState({ icon: Icon, title, description }: { icon: React.ElementType; title: string; description: string }) {
return (
<div className="flex flex-col items-center justify-center rounded-xl border border-surface-200 bg-white py-16">
<Icon className="h-12 w-12 text-surface-300" />
<p className="mt-4 text-sm font-medium text-surface-700">{title}</p>
<p className="mt-1 text-sm text-surface-500">{description}</p>
</div>
);
}
function formatDateTime(iso: string | null) {
if (!iso) return '—';
const d = new Date(iso);
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) +
' ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
}
// ---------------------------------------------------------------------------
// Leak Drawer (slide-over)
// ---------------------------------------------------------------------------
function LeakDrawer({
leak,
companyId,
onClose,
onRefresh,
}: {
leak: RevenueLeak;
companyId: string;
onClose: () => void;
onRefresh: () => void;
}) {
const [remediation, setRemediation] = useState<RemediationResponse | null>(null);
const [history, setHistory] = useState<ResolutionHistoryResponse | null>(null);
const [loading, setLoading] = useState<string | null>(null);
const [applying, setApplying] = useState<number | null>(null);
const [resolving, setResolving] = useState(false);
const [resolutionNote, setResolutionNote] = useState('');
const [showResolve, setShowResolve] = useState(false);
const loadDetails = useCallback(async () => {
try {
setLoading('details');
const [rem, hist] = await Promise.all([
getRemediation(companyId, leak.id),
getResolutionHistory(companyId, leak.id),
]);
setRemediation(rem);
setHistory(hist);
} catch {
// silent — drawer still useful with partial data
} finally {
setLoading(null);
}
}, [companyId, leak.id]);
useEffect(() => {
loadDetails();
}, [loadDetails]);
const handleApply = async (suggestion: RemediationSuggestion) => {
try {
setApplying(suggestion.index);
await applyRemediation(companyId, leak.id, suggestion.index);
await loadDetails();
} catch {
// silent
} finally {
setApplying(null);
}
};
const handleResolve = async () => {
try {
setResolving(true);
await resolveRevenueLeak(companyId, leak.id, resolutionNote || undefined);
onRefresh();
onClose();
} catch {
// silent
} finally {
setResolving(false);
}
};
const effortColor: Record<string, string> = {
low: 'text-emerald-600',
medium: 'text-amber-600',
high: 'text-red-600',
};
return (
<div className="fixed inset-0 z-50 flex justify-end" role="dialog">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/30" onClick={onClose} />
{/* Drawer panel */}
<div className="relative z-10 flex w-full max-w-lg flex-col bg-white shadow-2xl">
{/* Header */}
<div className="flex items-center justify-between border-b border-surface-200 px-6 py-4">
<div className="flex items-center gap-3">
<SeverityBadge severity={leak.severity} />
<h2 className="text-lg font-semibold text-surface-900">{leak.source}</h2>
</div>
<button
onClick={onClose}
className="rounded-lg p-1.5 text-surface-400 hover:bg-surface-100"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Body */}
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-6">
{/* Leak details */}
<div className="space-y-3">
<p className="text-sm text-surface-700">{leak.description || 'No description.'}</p>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<span className="text-surface-500">Detected:</span>
<p className="font-medium text-surface-800">{formatDateTime(leak.detected_at)}</p>
</div>
<div>
<span className="text-surface-500">Est. Loss:</span>
<p className="font-medium text-red-600">
{leak.estimated_loss ? `$${leak.estimated_loss.toLocaleString()}` : 'Unknown'}
</p>
</div>
<div>
<span className="text-surface-500">Detector:</span>
<p className="font-mono text-xs text-surface-600">
{leak.metadata_json?.detector_id || '—'}
</p>
</div>
<div>
<span className="text-surface-500">Status:</span>
<p className="font-medium">
{leak.resolved ? (
<span className="text-emerald-600">✓ Resolved {formatDateTime(leak.resolved_at)}</span>
) : (
<span className="text-red-600">Active</span>
)}
</p>
</div>
</div>
</div>
{/* Resolve button */}
{!leak.resolved && !showResolve && (
<button
onClick={() => setShowResolve(true)}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-emerald-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-emerald-700 transition-colors"
>
<CheckCircle className="h-4 w-4" />
Mark as Resolved
</button>
)}
{/* Resolve form */}
{!leak.resolved && showResolve && (
<div className="space-y-3 rounded-lg border border-emerald-200 bg-emerald-50 p-4">
<label className="block text-sm font-medium text-surface-700">Resolution Notes (optional)</label>
<textarea
value={resolutionNote}
onChange={(e) => setResolutionNote(e.target.value)}
rows={3}
className="w-full rounded-lg border border-surface-300 py-2 px-3 text-sm outline-none focus:border-brand-500 resize-none"
placeholder="What did you do to fix this?"
/>
<div className="flex gap-2">
<button
onClick={handleResolve}
disabled={resolving}
className="flex-1 rounded-lg bg-emerald-600 px-4 py-2 text-sm font-medium text-white hover:bg-emerald-700 disabled:opacity-50"
>
{resolving ? (
<span className="inline-flex items-center gap-2"><Loader2 className="h-4 w-4 animate-spin" /> Saving...</span>
) : 'Confirm Resolve'}
</button>
<button
onClick={() => setShowResolve(false)}
className="rounded-lg border border-surface-300 px-4 py-2 text-sm font-medium text-surface-700 hover:bg-surface-50"
>
Cancel
</button>
</div>
</div>
)}
{/* Remediation suggestions */}
<div>
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-surface-800">
<Wrench className="h-4 w-4 text-surface-500" />
Remediation Suggestions
</h3>
{loading === 'details' ? (
<div className="flex items-center gap-2 py-4 text-sm text-surface-500">
<Loader2 className="h-4 w-4 animate-spin" /> Loading suggestions...
</div>
) : remediation?.suggestions && remediation.suggestions.length > 0 ? (
<div className="space-y-3">
{remediation.suggestions.map((s) => (
<div key={s.index} className="rounded-lg border border-surface-200 bg-white p-4">
<div className="mb-2 flex items-start justify-between gap-2">
<h4 className="text-sm font-semibold text-surface-800">{s.title}</h4>
<span className={`shrink-0 text-xs font-medium ${effortColor[s.effort]}`}>
{s.effort} effort · {s.impact_score >= 0 ? '+' : ''}{s.impact_score}% impact
</span>
</div>
<p className="mb-3 text-xs text-surface-600">{s.description}</p>
{s.impact_score >= 0 && (
<div className="mb-3 h-1.5 w-full rounded-full bg-surface-100">
<div
className={`h-1.5 rounded-full ${s.impact_score > 50 ? 'bg-emerald-500' : s.impact_score > 25 ? 'bg-amber-500' : 'bg-blue-500'}`}
style={{ width: `${Math.min(s.impact_score, 100)}%` }}
/>
</div>
)}
{!leak.resolved && (
<button
onClick={() => handleApply(s)}
disabled={applying === s.index}
className="inline-flex items-center gap-1 rounded-md bg-brand-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-brand-700 disabled:opacity-50 transition-colors"
>
{applying === s.index ? (
<><Loader2 className="h-3 w-3 animate-spin" /> Applying...</>
) : (
<><Zap className="h-3 w-3" /> Apply Fix</>
)}
</button>
)}
</div>
))}
</div>
) : (
<p className="text-sm text-surface-500">No suggestions available.</p>
)}
</div>
{/* Resolution history */}
<div>
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-surface-800">
<Clock className="h-4 w-4 text-surface-500" />
Resolution History
</h3>
{loading === 'details' ? (
<div className="flex items-center gap-2 py-4 text-sm text-surface-500">
<Loader2 className="h-4 w-4 animate-spin" /> Loading history...
</div>
) : history?.resolution_history && history.resolution_history.length > 0 ? (
<div className="space-y-2">
{history.resolution_history.map((entry, i) => (
<div key={i} className="flex items-start gap-3 rounded-lg bg-surface-50 p-3">
<div className="mt-0.5 h-2 w-2 rounded-full bg-emerald-500" />
<div className="min-w-0 flex-1">
<p className="text-sm text-surface-700">
{entry.resolution_notes || 'Resolved'}
</p>
<p className="text-xs text-surface-400">
{formatDateTime(entry.resolved_at)} · by {typeof entry.resolved_by === 'string' ? entry.resolved_by : `user ${entry.resolved_by}`}
</p>
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-surface-500">No resolution history.</p>
)}
</div>
</div>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Recurring Tab
// ---------------------------------------------------------------------------
function RecurringTab({ companyId }: { companyId: string }) {
const [recurring, setRecurring] = useState<RecurringLeakGroup[]>([]);
const [loading, setLoading] = useState(true);
const [days, setDays] = useState(90);
const load = useCallback(async () => {
try {
setLoading(true);
const data = await getRecurringLeaks(companyId, days);
setRecurring(data.recurring_leaks || []);
} catch {
setRecurring([]);
} finally {
setLoading(false);
}
}, [companyId, days]);
useEffect(() => { load(); }, [load]);
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-surface-700">
Leaks detected multiple times
</h3>
<select
value={days}
onChange={(e) => setDays(Number(e.target.value))}
className="rounded-lg border border-surface-300 py-2 px-3 text-sm outline-none focus:border-brand-500 min-h-[44px]"
>
<option value={30}>Last 30 days</option>
<option value={60}>Last 60 days</option>
<option value={90}>Last 90 days</option>
<option value={180}>Last 180 days</option>
</select>
</div>
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-5 w-5 animate-spin text-brand-600" />
<span className="ml-3 text-sm text-surface-500">Loading recurring leaks…</span>
</div>
) : recurring.length === 0 ? (
<EmptyState icon={Repeat} title="No recurring leaks" description="Good news — no leaks have been detected repeatedly in this period." />
) : (
<div className="overflow-x-auto rounded-xl border border-surface-200 bg-white">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-surface-200 bg-surface-50">
<th className="px-4 py-3 text-left font-medium text-surface-600">Source</th>
<th className="px-4 py-3 text-left font-medium text-surface-600">Detector</th>
<th className="px-4 py-3 text-center font-medium text-surface-600">Count</th>
<th className="px-4 py-3 text-left font-medium text-surface-600">Severity</th>
<th className="px-4 py-3 text-right font-medium text-surface-600">Total Loss</th>
<th className="px-4 py-3 text-right font-medium text-surface-600">First / Last</th>
</tr>
</thead>
<tbody>
{recurring.map((group, i) => (
<tr key={i} className="border-b border-surface-100 last:border-0 hover:bg-surface-50">
<td className="px-4 py-3 font-medium text-surface-900">{group.source}</td>
<td className="px-4 py-3">
<code className="rounded bg-surface-100 px-1.5 py-0.5 text-xs text-surface-600">
{group.detector_id}
</code>
</td>
<td className="px-4 py-3 text-center">
<span className="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-semibold text-red-700">
{group.count}×
</span>
</td>
<td className="px-4 py-3">
<SeverityBadge severity={group.severity} size="sm" />
</td>
<td className="px-4 py-3 text-right font-medium text-red-600">
${group.total_loss?.toLocaleString() || '—'}
</td>
<td className="px-4 py-3 text-right text-xs text-surface-500">
<div>{formatDateTime(group.first_detected)}</div>
<div>{formatDateTime(group.last_detected)}</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// Locations Tab (P4 Multi-Location Rollup)
// ---------------------------------------------------------------------------
function LocationsTab() {
const [rollup, setRollup] = useState<LocationLocationsResponse | null>(null);
const [comparison, setComparison] = useState<LocationComparisonResponse | null>(null);
const [detector, setDetector] = useState<LocationDetectorResponse | null>(null);
const [loading, setLoading] = useState(true);
const [loadingDetector, setLoadingDetector] = useState(false);
const [days, setDays] = useState(90);
const [subTab, setSubTab] = useState<'rollup' | 'comparison' | 'detector'>('rollup');
const loadRollup = useCallback(async () => {
try {
setLoading(true);
const data = await getLocationRollup(days);
setRollup(data);
} catch {
setRollup(null);
} finally {
setLoading(false);
}
}, [days]);
const loadComparison = useCallback(async () => {
try {
setLoading(true);
const data = await getLocationComparison(days);
setComparison(data);
} catch {
setComparison(null);
} finally {
setLoading(false);
}
}, [days]);
const handleRunDetector = useCallback(async () => {
try {
setLoadingDetector(true);
const data = await runLocationDetector({ period_days: days });
setDetector(data);
} catch {
setDetector(null);
} finally {
setLoadingDetector(false);
}
}, [days]);
useEffect(() => {
loadRollup();
}, [loadRollup]);
useEffect(() => {
if (subTab === 'comparison') loadComparison();
}, [subTab, loadComparison]);
const subTabs = [
{ key: 'rollup' as const, label: 'Rollup', icon: Building2 },
{ key: 'comparison' as const, label: 'Comparison', icon: BarChart3 },
{ key: 'detector' as const, label: 'Detector', icon: AlertOctagon },
];
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-5 w-5 animate-spin text-brand-600" />
<span className="ml-3 text-sm text-surface-500">Loading location analytics…</span>
</div>
);
}
return (
<div className="space-y-4">
{/* Header */}
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h3 className="text-sm font-medium text-surface-700">Multi-Location Revenue Analytics</h3>
<p className="text-xs text-surface-500 mt-0.5">
Revenue concentration, variance alerts, and cross-location benchmarks
</p>
</div>
<select
value={days}
onChange={(e) => { setDays(Number(e.target.value)); setSubTab('rollup'); }}
className="rounded-lg border border-surface-300 py-2 px-3 text-sm outline-none focus:border-brand-500 min-h-[44px]"
>
<option value={30}>Last 30 days</option>
<option value={60}>Last 60 days</option>
<option value={90}>Last 90 days</option>
<option value={180}>Last 180 days</option>
</select>
</div>
{/* Sub-tabs */}
<div className="flex gap-1 rounded-lg bg-surface-100 p-1">
{subTabs.map(({ key, label, icon: Icon }) => (
<button
key={key}
onClick={() => setSubTab(key)}
className={`flex flex-1 items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors min-h-[44px] ${
subTab === key
? 'bg-white text-surface-900 shadow-sm'
: 'text-surface-600 hover:text-surface-800'
}`}
>
<Icon className="h-4 w-4" />
{label}
</button>
))}
</div>
{/* Rollup sub-tab */}
{subTab === 'rollup' && (
<div className="space-y-4">
{rollup === null ? (
<EmptyState
icon={Building2}
title="No location data"
description="Location revenue data will appear once QuickBooks or other sources are connected."
/>
) : (
<>
{/* Summary stats */}
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
<StatCard
label="Locations"
value={String(rollup.locations.total_locations)}
color="text-surface-800"
/>
<StatCard
label="Total Revenue"
value={`$${rollup.locations.total_revenue.toLocaleString()}`}
color="text-emerald-600"
/>
<StatCard
label="Avg / Location"
value={`$${Math.round(rollup.locations.avg_revenue_per_location).toLocaleString()}`}
color="text-surface-800"
/>
<StatCard
label="Max Variance"
value={`${rollup.locations.max_variance_pct.toFixed(1)}%`}
color={rollup.locations.max_variance_pct > 50 ? 'text-red-600' : 'text-emerald-600'}
/>
</div>
{/* Alerts */}
{rollup.alerts.length > 0 && (
<div className="space-y-2">
{rollup.alerts.map((alert, i) => {
const alertColor: Record<string, string> = {
critical: 'bg-red-50 border-red-200 text-red-800',
high: 'bg-orange-50 border-orange-200 text-orange-800',
medium: 'bg-amber-50 border-amber-200 text-amber-800',
low: 'bg-blue-50 border-blue-200 text-blue-800',
};
const alertIcon: Record<string, React.ElementType> = {
concentration: AlertOctagon,
underperformer: TrendingDown,
variance: Activity,
};
const AlertIcon = alertIcon[alert.type] || AlertOctagon;
return (
<div
key={i}
className={`flex items-start gap-3 rounded-lg border p-4 ${alertColor[alert.severity]}`}
>
<AlertIcon className="mt-0.5 h-5 w-5 shrink-0" />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="inline-flex items-center rounded-full bg-black/10 px-2 py-0.5 text-xs font-medium uppercase">
{alert.type}
</span>
<SeverityBadge severity={alert.severity} size="sm" />
</div>
<p className="mt-1 text-sm font-medium">{alert.location_name}</p>
<p className="text-sm opacity-90">{alert.message}</p>
</div>
</div>
);
})}
</div>
)}
{/* Breakdown table */}
{rollup.breakdown.length > 0 && (
<div className="overflow-x-auto rounded-xl border border-surface-200 bg-white">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-surface-200 bg-surface-50">
<th className="px-4 py-3 text-left font-medium text-surface-600">Location</th>
<th className="px-4 py-3 text-right font-medium text-surface-600">Revenue</th>
<th className="px-4 py-3 text-right font-medium text-surface-600">Transactions</th>
<th className="px-4 py-3 text-right font-medium text-surface-600">Avg Transaction</th>
<th className="px-4 py-3 text-right font-medium text-surface-600">% of Total</th>
</tr>
</thead>
<tbody>
{rollup.breakdown.map((loc, i) => (
<tr key={i} className="border-b border-surface-100 last:border-0 hover:bg-surface-50">
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<MapPin className="h-4 w-4 text-surface-400" />
<span className="font-medium text-surface-900">{loc.location_name}</span>
</div>
<code className="text-[10px] text-surface-400">{loc.location_id}</code>
</td>
<td className="px-4 py-3 text-right font-medium text-emerald-700">
${loc.revenue.toLocaleString()}
</td>
<td className="px-4 py-3 text-right text-surface-600">
{loc.transaction_count}
</td>
<td className="px-4 py-3 text-right text-surface-600">
${Math.round(loc.avg_transaction).toLocaleString()}
</td>
<td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-2">
<div className="h-1.5 w-16 rounded-full bg-surface-100">
<div
className={`h-1.5 rounded-full ${loc.pct_of_total > 50 ? 'bg-red-500' : loc.pct_of_total > 30 ? 'bg-amber-500' : 'bg-emerald-500'}`}
style={{ width: `${Math.min(loc.pct_of_total, 100)}%` }}
/>
</div>
<span className="text-surface-700 font-medium w-12 text-right">{loc.pct_of_total.toFixed(1)}%</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</>
)}
</div>
)}
{/* Comparison sub-tab */}
{subTab === 'comparison' && (
<div className="space-y-4">
{comparison === null ? (
<EmptyState
icon={BarChart3}
title="No comparison data"
description="Compare your locations side-by-side."
/>
) : (
<>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
<StatCard label="Locations" value={String(comparison.comparison.total_locations)} color="text-surface-800" />
<StatCard label="Total Revenue" value={`$${comparison.comparison.total_revenue.toLocaleString()}`} color="text-emerald-600" />
<StatCard label="Avg Revenue" value={`$${Math.round(comparison.comparison.avg_revenue).toLocaleString()}`} color="text-surface-800" />
<StatCard label="Median Revenue" value={`$${Math.round(comparison.comparison.median_revenue).toLocaleString()}`} color="text-surface-800" />
</div>
<div className="overflow-x-auto rounded-xl border border-surface-200 bg-white">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-surface-200 bg-surface-50">
<th className="px-4 py-3 text-center font-medium text-surface-600 w-16">#</th>
<th className="px-4 py-3 text-left font-medium text-surface-600">Location</th>
<th className="px-4 py-3 text-right font-medium text-surface-600">Revenue</th>
<th className="px-4 py-3 text-right font-medium text-surface-600">vs Average</th>
<th className="px-4 py-3 text-right font-medium text-surface-600">vs Median</th>
<th className="px-4 py-3 text-right font-medium text-surface-600">% of Total</th>
</tr>
</thead>
<tbody>
{comparison.locations.map((loc, i) => {
const vsAvgIcon = loc.vs_avg_pct >= 100 ? TrendingUp : TrendingDown;
const vsAvgColor = loc.vs_avg_pct >= 100 ? 'text-emerald-600' : 'text-red-600';
const VsAvgIcon = vsAvgIcon;
return (
<tr key={i} className="border-b border-surface-100 last:border-0 hover:bg-surface-50">
<td className="px-4 py-3 text-center">
<span className={`inline-flex h-6 w-6 items-center justify-center rounded-full text-xs font-bold ${
loc.ranking === 1 ? 'bg-amber-100 text-amber-700' : 'bg-surface-100 text-surface-600'
}`}>
{loc.ranking}
</span>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<MapPin className="h-4 w-4 text-surface-400" />
<span className="font-medium text-surface-900">{loc.location_name}</span>
</div>
</td>
<td className="px-4 py-3 text-right font-medium text-emerald-700">
${loc.revenue.toLocaleString()}
</td>
<td className={`px-4 py-3 text-right font-medium ${vsAvgColor}`}>
<span className="inline-flex items-center gap-1">
<VsAvgIcon className="h-3 w-3" />
{(loc.vs_avg_pct - 100) >= 0 ? '+' : ''}{(loc.vs_avg_pct - 100).toFixed(1)}%
</span>
</td>
<td className={`px-4 py-3 text-right font-medium ${
loc.vs_median_pct >= 100 ? 'text-emerald-600' : 'text-red-600'
}`}>
{(loc.vs_median_pct - 100) >= 0 ? '+' : ''}{(loc.vs_median_pct - 100).toFixed(1)}%
</td>
<td className="px-4 py-3 text-right text-surface-700 font-medium">
{loc.pct_of_total.toFixed(1)}%
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</>
)}
</div>
)}
{/* Detector sub-tab */}
{subTab === 'detector' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-sm text-surface-600">
Run the multi-location rollup detector to find concentration and variance issues.
</p>
<button
onClick={handleRunDetector}
disabled={loadingDetector}
className="inline-flex items-center gap-2 rounded-lg bg-brand-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-700 disabled:opacity-50 transition-colors min-h-[44px]"
>
{loadingDetector ? (
<><Loader2 className="h-4 w-4 animate-spin" /> Running...</>
) : (
<><Play className="h-4 w-4" /> Run Detector</>
)}
</button>
</div>
{detector === null ? (
<EmptyState
icon={AlertOctagon}
title="No detector results yet"
description="Click 'Run Detector' to analyze location concentration and variance."
/>
) : (
<>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
<StatCard
label="Detector"
value={detector.detector}
color="text-surface-800"
/>
<StatCard
label="Candidates"
value={String(detector.candidates_found)}
color={detector.candidates_found > 0 ? 'text-red-600' : 'text-emerald-600'}
/>
<StatCard
label="Est. Exposure"
value={`$${detector.candidates.reduce((s, c) => s + (c.estimated_loss || 0), 0).toLocaleString()}`}
color="text-red-600"
/>
</div>
{detector.candidates.length > 0 ? (
<div className="space-y-3">
{detector.candidates.map((c, i) => {
const alertColor: Record<string, string> = {
critical: 'bg-red-50 border-red-200 text-red-800',
high: 'bg-orange-50 border-orange-200 text-orange-800',
medium: 'bg-amber-50 border-amber-200 text-amber-800',
low: 'bg-blue-50 border-blue-200 text-blue-800',
};
return (
<div
key={c.dedupe_key}
className={`rounded-lg border p-4 ${alertColor[c.severity]}`}
>
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2">
<SeverityBadge severity={c.severity} size="sm" />
<span className="text-xs font-medium uppercase opacity-70">{c.type}</span>
</div>
{c.estimated_loss && (
<span className="text-sm font-semibold">
${c.estimated_loss.toLocaleString()}
</span>
)}
</div>
<p className="mt-1 text-sm font-medium">{c.location_name}</p>
<p className="mt-0.5 text-sm opacity-90">{c.message}</p>
</div>
);
})}
</div>
) : (
<div className="rounded-xl border border-emerald-200 bg-emerald-50 p-6 text-center">
<CheckCircle className="mx-auto h-10 w-10 text-emerald-600" />
<p className="mt-2 text-sm font-medium text-emerald-800">No issues found</p>
<p className="text-sm text-emerald-600">All locations are performing within expected parameters.</p>
</div>
)}
</>
)}
</div>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// Stats Tab
// ---------------------------------------------------------------------------
function StatsTab({ companyId }: { companyId: string }) {
const [stats, setStats] = useState<{ days: number; stats: any } | null>(null);
const [loading, setLoading] = useState(true);
const [days, setDays] = useState(30);
const load = useCallback(async () => {
try {
setLoading(true);
const data = await getResolutionStats(companyId, days);
setStats(data);
} catch {
setStats(null);
} finally {
setLoading(false);
}
}, [companyId, days]);
useEffect(() => { load(); }, [load]);
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-5 w-5 animate-spin text-brand-600" />
<span className="ml-3 text-sm text-surface-500">Loading stats…</span>
</div>
);
}
const s = stats?.stats;
if (!s) {
return <EmptyState icon={BarChart3} title="No stats available" description="Try connecting your integrations to generate data." />;
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-surface-700">Resolution performance</h3>
<select
value={days}
onChange={(e) => setDays(Number(e.target.value))}
className="rounded-lg border border-surface-300 py-2 px-3 text-sm outline-none focus:border-brand-500 min-h-[44px]"
>
<option value={7}>Last 7 days</option>
<option value={14}>Last 14 days</option>
<option value={30}>Last 30 days</option>
<option value={90}>Last 90 days</option>
</select>
</div>
{/* Stats grid */}
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
<StatCard label="Resolved" value={String(s.total_resolved || 0)} color="text-emerald-600" />
<StatCard label="Avg Resolution" value={`${(s.avg_resolution_time_hours || 0).toFixed(1)}h`} color="text-surface-800" />
<StatCard label="Recurring" value={`${s.recurring_count || 0}`} sub={`${(s.recurring_pct || 0).toFixed(0)}% of total`} color="text-red-600" />
<StatCard label="Period" value={`${stats?.days || days}d`} color="text-surface-800" />
</div>
{/* Severity breakdown */}
{s.by_severity && (
<div className="rounded-xl border border-surface-200 bg-white p-5">
<h4 className="mb-4 text-sm font-semibold text-surface-800">Resolutions by Severity</h4>
<div className="space-y-3">
{(['critical', 'high', 'medium', 'low'] as LeakSeverity[]).map((sev) => {
const count = (s.by_severity[sev] as number) || 0;
const total = s.total_resolved || 1;
const pct = Math.round((count / total) * 100);
return (
<div key={sev} className="flex items-center gap-3">
<SeverityBadge severity={sev} size="sm" />
<div className="flex-1">
<div className="h-2 w-full rounded-full bg-surface-100">
<div
className={`h-2 rounded-full transition-all ${severityDot[sev]}`}
style={{ width: `${pct}%` }}
/>
</div>
</div>
<span className="w-8 text-right text-sm font-medium text-surface-700">{count}</span>
</div>
);
})}
</div>
</div>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// Settings Tab
// ---------------------------------------------------------------------------
function SettingsTab({ companyId }: { companyId: string }) {
const [alertData, setAlertData] = useState<AlertSettingsResponse | null>(null);
const [severityData, setSeverityData] = useState<SeverityRule[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const load = useCallback(async () => {
try {
setLoading(true);
const [alerts, severity] = await Promise.all([
getAlertSettings(companyId),
getSeverityRules(companyId),
]);
setAlertData(alerts);
setSeverityData(Array.isArray(severity.severity_rules) ? severity.severity_rules : []);
} catch {
// silent
} finally {
setLoading(false);
}
}, [companyId]);
useEffect(() => { load(); }, [load]);
const handleSaveAlerts = async () => {
if (!alertData) return;
try {
setSaving(true);
const res = await updateAlertSettings(companyId, alertData.alert_settings);
setAlertData(res);
} catch {
// silent
} finally {
setSaving(false);
}
};
const handleSaveSeverity = async () => {
try {
setSaving(true);
const res = await updateSeverityRules(companyId, severityData);
setSeverityData(Array.isArray(res.severity_rules) ? res.severity_rules : []);
} catch {
// silent
} finally {
setSaving(false);
}
};
const updateAlertSetting = (key: keyof any, value: any) => {
setAlertData((prev) =>
prev ? { ...prev, alert_settings: { ...prev.alert_settings, [key]: value } } : prev
);
};
const updateSeverityRule = (idx: number, key: keyof SeverityRule, value: any) => {
setSeverityData((prev) => {
const next = [...prev];
next[idx] = { ...next[idx], [key]: value };
return next;
});
};
const addSeverityRule = () => {
setSeverityData((prev) => [
...prev,
{ min_loss: 0, min_recurrence: 1, severity: 'medium' as LeakSeverity },
]);
};
const removeSeverityRule = (idx: number) => {
setSeverityData((prev) => prev.filter((_, i) => i !== idx));
};
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-5 w-5 animate-spin text-brand-600" />
<span className="ml-3 text-sm text-surface-500">Loading settings…</span>
</div>
);
}
return (
<div className="space-y-6">
{/* Alert Settings */}
<div className="rounded-xl border border-surface-200 bg-white p-5">
<h4 className="mb-4 flex items-center gap-2 text-sm font-semibold text-surface-800">
<Bell className="h-4 w-4 text-surface-500" />
Slack Alerts
</h4>
{alertData?.slack_connected === false && (
<div className="mb-4 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-700">
No Slack connector found. Connect Slack in <strong>Integrations</strong> first.
</div>
)}
<div className="space-y-4">
{/* Enabled toggle */}
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-surface-700">Enable Alerts</p>
<p className="text-xs text-surface-500">Send leak alerts to Slack</p>
</div>
<button
onClick={() => updateAlertSetting('enabled', !alertData?.alert_settings.enabled)}
className={`relative h-6 w-11 rounded-full transition-colors ${
alertData?.alert_settings.enabled ? 'bg-brand-600' : 'bg-surface-300'
}`}
>
<span
className={`absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform ${
alertData?.alert_settings.enabled ? 'translate-x-5' : 'translate-x-0.5'
}`}
/>
</button>
</div>
{/* Channel */}
<div>
<label className="block text-sm font-medium text-surface-700">Slack Channel</label>
<input
type="text"
value={alertData?.alert_settings.slack_channel || ''}
onChange={(e) => updateAlertSetting('slack_channel', e.target.value)}
placeholder="#revenue-alerts"
className="mt-1 w-full rounded-lg border border-surface-300 py-2 px-3 text-sm outline-none focus:border-brand-500"
/>
</div>
{/* Slack workspace */}
{alertData?.slack_workspaces && alertData.slack_workspaces.length > 0 && (
<div>
<label className="block text-sm font-medium text-surface-700">Workspace</label>
<select
value={alertData.alert_settings.slack_connector_id || ''}
onChange={(e) => updateAlertSetting('slack_connector_id', e.target.value)}
className="mt-1 w-full rounded-lg border border-surface-300 py-2 px-3 text-sm outline-none focus:border-brand-500 min-h-[44px]"
>
<option value="">Select workspace...</option>
{alertData.slack_workspaces.map((w) => (
<option key={w.id} value={w.id}>{w.workspace}</option>
))}
</select>
</div>
)}
{/* Severity threshold */}
<div>
<label className="block text-sm font-medium text-surface-700">Minimum Severity Threshold</label>
<select
value={alertData?.alert_settings.severity_threshold || 'high'}
onChange={(e) => updateAlertSetting('severity_threshold', e.target.value)}
className="mt-1 w-full rounded-lg border border-surface-300 py-2 px-3 text-sm outline-none focus:border-brand-500 min-h-[44px]"
>
<option value="critical">Critical only</option>
<option value="high">High and above</option>
<option value="medium">Medium and above</option>
<option value="low">All</option>
</select>
</div>
<button
onClick={handleSaveAlerts}
disabled={saving}
className="rounded-lg bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700 disabled:opacity-50 transition-colors"
>
{saving ? 'Saving...' : 'Save Alert Settings'}
</button>
</div>
</div>
{/* Severity Rules */}
<div className="rounded-xl border border-surface-200 bg-white p-5">
<div className="mb-4 flex items-center justify-between">
<h4 className="flex items-center gap-2 text-sm font-semibold text-surface-800">
<Shield className="h-4 w-4 text-surface-500" />
Severity Rules
</h4>
<button
onClick={addSeverityRule}
className="rounded-lg border border-surface-300 px-3 py-1.5 text-xs font-medium text-surface-700 hover:bg-surface-50"
>
+ Add Rule
</button>
</div>
{severityData.length === 0 ? (
<p className="text-sm text-surface-500">No custom severity rules. Add one to override default severity assignment.</p>
) : (
<div className="space-y-3">
{severityData.map((rule, idx) => (
<div key={idx} className="flex items-center gap-3 rounded-lg border border-surface-200 p-3">
<div className="flex-1 grid grid-cols-2 gap-2 sm:grid-cols-4">
<input
type="text"
value={rule.detector_id || ''}
onChange={(e) => updateSeverityRule(idx, 'detector_id', e.target.value)}
placeholder="Detector ID"
className="rounded-md border border-surface-300 py-1.5 px-2 text-xs outline-none focus:border-brand-500"
/>
<input
type="text"
value={rule.source || ''}
onChange={(e) => updateSeverityRule(idx, 'source', e.target.value)}
placeholder="Source"
className="rounded-md border border-surface-300 py-1.5 px-2 text-xs outline-none focus:border-brand-500"
/>
<input
type="number"
value={rule.min_loss ?? ''}
onChange={(e) => updateSeverityRule(idx, 'min_loss', e.target.value ? Number(e.target.value) : undefined)}
placeholder="Min loss $"
className="rounded-md border border-surface-300 py-1.5 px-2 text-xs outline-none focus:border-brand-500"
/>
<select
value={rule.severity}
onChange={(e) => updateSeverityRule(idx, 'severity', e.target.value)}
className="rounded-md border border-surface-300 py-1.5 px-2 text-xs outline-none focus:border-brand-500 min-h-[32px]"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
<button
onClick={() => removeSeverityRule(idx)}
className="rounded p-1 text-surface-400 hover:bg-red-50 hover:text-red-600"
>
<X className="h-4 w-4" />
</button>
</div>
))}
<button
onClick={handleSaveSeverity}
disabled={saving}
className="rounded-lg bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700 disabled:opacity-50 transition-colors"
>
{saving ? 'Saving...' : 'Save Severity Rules'}
</button>
</div>
)}
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Main Page
// ---------------------------------------------------------------------------
export function RevenueLeaksPage() {
const { user } = useAuth();
const companyId = user?.tenantId as string;
const [activeTab, setActiveTab] = useState<TabKey>('leaks');
const [leaks, setLeaks] = useState<RevenueLeak[]>([]);
const [loading, setLoading] = useState(true);
const [scanning, setScanning] = useState(false);
const [scanResult, setScanResult] = useState<ScanResult | null>(null);
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState('');
const [filterSeverity, setFilterSeverity] = useState<'all' | LeakSeverity>('all');
const [filterResolved, setFilterResolved] = useState<'all' | 'active' | 'resolved'>('all');
const [selectedLeak, setSelectedLeak] = useState<RevenueLeak | null>(null);
const loadLeaks = useCallback(async () => {
if (!companyId) return;
try {
setLoading(true);
const data = await getRevenueLeaks(companyId);
setLeaks(data);
setError(null);
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to load revenue leaks');
} finally {
setLoading(false);
}
}, [companyId]);
useEffect(() => {
loadLeaks();
}, [loadLeaks]);
const handleScan = async () => {
if (!companyId) return;
try {
setScanning(true);
setError(null);
const result = await scanLeaks(companyId);
setScanResult(result);
await loadLeaks();
// Show scan result briefly
setTimeout(() => setScanResult(null), 5000);
} catch (e) {
setError(e instanceof Error ? e.message : 'Scan failed');
} finally {
setScanning(false);
}
};
// Filter logic
const filtered = leaks
.filter((l) => filterSeverity === 'all' || l.severity === filterSeverity)
.filter((l) => {
if (filterResolved === 'active') return !l.resolved;
if (filterResolved === 'resolved') return l.resolved;
return true;
})
.filter((l) =>
search === '' ||
l.source.toLowerCase().includes(search.toLowerCase()) ||
l.description.toLowerCase().includes(search.toLowerCase())
);
const activeCount = leaks.filter((l) => !l.resolved).length;
const totalLoss = leaks
.filter((l) => !l.resolved && l.estimated_loss)
.reduce((sum, l) => sum + (l.estimated_loss ?? 0), 0);
return (
<AppLayout title="Revenue Leaks">
<div data-testid="page-content" className="space-y-6">
{/* Scan result notification */}
{scanResult && (
<div className="rounded-xl border border-brand-200 bg-brand-50 p-4 animate-pulse">
<div className="flex items-center gap-3">
<Zap className="h-5 w-5 text-brand-600" />
<div>
<p className="text-sm font-semibold text-brand-800">
Scan complete — {scanResult.new_leaks} new leak{scanResult.new_leaks !== 1 ? 's' : ''} found
</p>
<p className="text-xs text-brand-600">
{scanResult.scanned} detectors scanned · {scanResult.total_leaks} total leaks
</p>
</div>
<button onClick={() => setScanResult(null)} className="ml-auto text-brand-400 hover:text-brand-600">
<X className="h-4 w-4" />
</button>
</div>
</div>
)}
{/* Summary Cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<StatCard label="Active Leaks" value={String(activeCount)} sub={`${leaks.length} total`} color="text-surface-900" />
<StatCard label="Estimated Monthly Loss" value={`$${totalLoss.toLocaleString()}`} color="text-red-600" />
<StatCard label="Resolved" value={String(leaks.length - activeCount)} color="text-emerald-600" />
</div>
{/* Tabs */}
<div className="flex gap-1 rounded-lg bg-surface-100 p-1">
{TABS.map(({ key, label, icon: Icon }) => (
<button
key={key}
onClick={() => setActiveTab(key)}
className={`flex flex-1 items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors min-h-[44px] ${
activeTab === key
? 'bg-white text-surface-900 shadow-sm'
: 'text-surface-600 hover:text-surface-800'
}`}
>
<Icon className="h-4 w-4" />
{label}
</button>
))}
</div>
{/* Error */}
{error && (
<div className="rounded-xl border border-red-200 bg-red-50 p-4">
<p className="text-sm font-medium text-red-800">{error}</p>
<button onClick={() => setError(null)} className="mt-1 text-sm text-red-600 underline">
Dismiss
</button>
</div>
)}
{/* Tab content */}
{activeTab === 'leaks' && (
<div className="space-y-4">
{/* Toolbar */}
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2">
<div className="relative flex-1 sm:flex-initial">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-surface-400" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search leaks..."
className="w-full rounded-lg border border-surface-300 py-2 pl-9 pr-4 text-sm outline-none focus:border-brand-500 focus:ring-2 focus:ring-brand-100 sm:w-64"
/>
</div>
<select
value={filterSeverity}
onChange={(e) => setFilterSeverity(e.target.value as 'all' | LeakSeverity)}
className="rounded-lg border border-surface-300 py-2 px-3 text-sm outline-none focus:border-brand-500 min-h-[44px]"
>
<option value="all">All Severities</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<select
value={filterResolved}
onChange={(e) => setFilterResolved(e.target.value as 'all' | 'active' | 'resolved')}
className="rounded-lg border border-surface-300 py-2 px-3 text-sm outline-none focus:border-brand-500 min-h-[44px]"
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="resolved">Resolved</option>
</select>
</div>
<button
onClick={handleScan}
disabled={scanning}
className="inline-flex items-center gap-2 rounded-lg bg-brand-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-700 disabled:opacity-50 transition-colors min-h-[44px]"
>
{scanning ? (
<><Loader2 className="h-4 w-4 animate-spin" /> Scanning...</>
) : (
<><Play className="h-4 w-4" /> Run Scan</>
)}
</button>
</div>
{/* Leaks list */}
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-5 w-5 animate-spin text-brand-600" />
<span className="ml-3 text-sm text-surface-500">Loading revenue leaks…</span>
</div>
) : filtered.length === 0 ? (
<EmptyState
icon={AlertTriangle}
title={search || filterSeverity !== 'all' || filterResolved !== 'all' ? 'No leaks match your filters' : 'No revenue leaks detected'}
description={
search || filterSeverity !== 'all' || filterResolved !== 'all'
? 'Try adjusting your filters or run a new scan'
: 'Run a scan to detect revenue leaks from your connected integrations'
}
/>
) : (
<div className="overflow-hidden rounded-xl border border-surface-200 bg-white">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-surface-200 bg-surface-50">
<th className="px-4 py-3 text-left font-medium text-surface-600">Source</th>
<th className="px-4 py-3 text-left font-medium text-surface-600">Severity</th>
<th className="px-4 py-3 text-right font-medium text-surface-600">Est. Loss</th>
<th className="px-4 py-3 text-left font-medium text-surface-600">Status</th>
<th className="px-4 py-3 text-left font-medium text-surface-600">Detected</th>
<th className="px-4 py-3 text-right font-medium text-surface-600 w-12"></th>
</tr>
</thead>
<tbody>
{filtered.map((leak) => (
<tr
key={leak.id}
onClick={() => setSelectedLeak(leak)}
className="cursor-pointer border-b border-surface-100 last:border-0 hover:bg-brand-50/50 transition-colors"
>
<td className="px-4 py-3">
<div className="font-medium text-surface-900">{leak.source}</div>
{leak.description && (
<div className="max-w-xs truncate text-xs text-surface-500">{leak.description}</div>
)}
{leak.metadata_json?.detector_id && (
<code className="mt-0.5 block font-mono text-[10px] text-surface-400">
{leak.metadata_json.detector_id}
</code>
)}
</td>
<td className="px-4 py-3">
<SeverityBadge severity={leak.severity} size="sm" />
</td>
<td className="px-4 py-3 text-right">
{leak.estimated_loss ? (
<span className="font-medium text-red-600">${leak.estimated_loss.toLocaleString()}</span>
) : (
<span className="text-surface-400">—</span>
)}
</td>
<td className="px-4 py-3">
{leak.resolved ? (
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700">
<CheckCircle className="h-3 w-3" /> Resolved
</span>
) : (
<span className="inline-flex items-center gap-1 rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700">
<AlertCircle className="h-3 w-3" /> Active
</span>
)}
</td>
<td className="px-4 py-3 text-xs text-surface-500">
{formatDateTime(leak.detected_at)}
</td>
<td className="px-4 py-3 text-right">
<ChevronRight className="h-4 w-4 text-surface-400" />
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="border-t border-surface-200 bg-surface-50 px-4 py-2 text-xs text-surface-500">
{filtered.length} of {leaks.length} leaks · Click a row for details
</div>
</div>
)}
</div>
)}
{activeTab === 'recurring' && <RecurringTab companyId={companyId} />}
{activeTab === 'locations' && <LocationsTab />}
{activeTab === 'stats' && <StatsTab companyId={companyId} />}
{activeTab === 'settings' && <SettingsTab companyId={companyId} />}
{/* Leak detail drawer */}
{selectedLeak && (
<LeakDrawer
leak={selectedLeak}
companyId={companyId}
onClose={() => setSelectedLeak(null)}
onRefresh={loadLeaks}
/>
)}
</div>
</AppLayout>
);
}