'use client'; import { useState, useEffect, useCallback } from 'react'; import { Button } from '@/components/ui/button'; import { Send, RefreshCw, CheckCircle2, XCircle, Clock, Loader2, ChevronDown, ChevronUp, BarChart3, Brain, Calendar, CalendarDays, CalendarRange, MessageSquare, Bell, Globe, ExternalLink, } from 'lucide-react'; interface DigestConfig { daily_enabled: boolean; weekly_enabled: boolean; monthly_enabled: boolean; daily_cron: string; weekly_cron: string; monthly_cron: string; llm_provider: string; llm_model: string; include_noise_analysis: boolean; include_sla_analysis: boolean; include_resource_analysis: boolean; include_client_analysis: boolean; include_recommendations: boolean; channel_ids: number[]; } interface NotificationChannel { id: number; name: string; channel_type: 'teams' | 'telegram' | 'ntfy' | 'webhook'; config: Record; is_active: boolean; } interface DigestReport { id: number; period_type: string; period_start: string; period_end: string; generated_at: string; stats: any; llm_analysis: string | null; delivery_status: Record; tokens_used: number | null; processing_time_ms: number | null; } function fmtDate(d: string | null) { if (!d) return 'Never'; const date = new Date(d); const diff = Date.now() - date.getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return 'Just now'; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; return `${Math.floor(hrs / 24)}d ago`; } function ChannelIcon({ type }: { type: string }) { if (type === 'teams') return ; if (type === 'telegram') return ; if (type === 'ntfy') return ; return ; } function PeriodIcon({ period }: { period: string }) { if (period === 'daily') return ; if (period === 'weekly') return ; return ; } export default function TicketDigestPage() { const [config, setConfig] = useState(null); const [channels, setChannels] = useState([]); const [history, setHistory] = useState([]); const [loading, setLoading] = useState(true); const [generating, setGenerating] = useState(null); const [toast, setToast] = useState<{ msg: string; ok: boolean } | null>(null); const [expandedReport, setExpandedReport] = useState(null); const [previewData, setPreviewData] = useState(null); const [previewPeriod, setPreviewPeriod] = useState(null); const [previewLoading, setPreviewLoading] = useState(false); const showToast = useCallback((msg: string, ok: boolean) => { setToast({ msg, ok }); setTimeout(() => setToast(null), 4000); }, []); const loadData = useCallback(async () => { try { const res = await fetch('/api/reports/ticket-digest'); const data = await res.json(); setConfig(data.config); setChannels(data.channels || []); setHistory(data.history || []); } catch (e) { showToast('Failed to load data', false); } finally { setLoading(false); } }, [showToast]); useEffect(() => { loadData(); }, [loadData]); const generateReport = async (period: string) => { setGenerating(period); try { const res = await fetch('/api/reports/ticket-digest', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ period }), }); const data = await res.json(); if (data.success) { showToast(`${period} digest generated (${data.processingTimeMs}ms)`, true); loadData(); } else { showToast(data.error || 'Generation failed', false); } } catch (e) { showToast('Network error', false); } finally { setGenerating(null); } }; const loadPreview = async (period: string) => { if (previewPeriod === period) { setPreviewPeriod(null); setPreviewData(null); return; } setPreviewLoading(true); setPreviewPeriod(period); try { const res = await fetch(`/api/reports/ticket-digest?preview=${period}`); const data = await res.json(); setPreviewData(data.stats); } catch { showToast('Failed to load preview', false); } finally { setPreviewLoading(false); } }; const updateConfig = async (updates: Partial) => { try { const res = await fetch('/api/reports/ticket-digest/config', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates), }); const data = await res.json(); setConfig(data.config); showToast('Config updated', true); } catch { showToast('Failed to update config', false); } }; const toggleChannelId = async (id: number) => { if (!config) return; const current = config.channel_ids || []; const updated = current.includes(id) ? current.filter(c => c !== id) : [...current, id]; await updateConfig({ channel_ids: updated }); }; if (loading) return (
); return (
{/* Toast */} {toast && (
{toast.msg}
)} {/* Header */}

Ticket Digest Reports

LLM-analyzed ticket reports delivered to Teams โ€” daily, weekly, and monthly

{/* Generate Reports */}

Generate Report

{(['daily', 'weekly', 'monthly'] as const).map(p => (
{p}
))}
{/* Preview */} {previewPeriod && previewData && (

{previewPeriod} Preview โ€” {previewData.period?.label}

{previewData.overview?.total_created ?? 0}
Created
{previewData.overview?.total_resolved ?? 0}
Resolved
{previewData.overview?.avg_resolution_hours ?? 'โ€”'}h
Avg Resolve
{(previewData.overview?.total_hours_worked ?? 0).toFixed(1)}h
Hours Worked
{previewData.noise_candidates?.length > 0 && (

๐Ÿ” Noise Candidates ({previewData.noise_candidates.length})

{previewData.noise_candidates.slice(0, 10).map((n: any, i: number) => (
{n.title} {n.count}ร— ยท {n.source_label}
))}
)} {previewData.top_clients?.length > 0 && (

๐Ÿข Top Clients

{previewData.top_clients.slice(0, 5).map((c: any, i: number) => (
{c.company_name} {c.ticket_count} tickets ยท {c.hours_worked.toFixed(1)}h
))}
)}
)}
{/* Analysis Sections Config */} {config && (

Analysis Sections

{([ { key: 'include_noise_analysis', label: '๐Ÿ” Noise & Automation' }, { key: 'include_sla_analysis', label: 'โฑ๏ธ SLA & Response Times' }, { key: 'include_resource_analysis', label: '๐Ÿ‘ฅ Team Workload' }, { key: 'include_client_analysis', label: '๐Ÿข Client Spotlight' }, { key: 'include_recommendations', label: '๐Ÿ’ก Recommendations' }, ] as const).map(({ key, label }) => ( ))}
)} {/* Notification Channels */}

Delivery Channels

Select which notification channels receive these reports. Manage channels in Notification Channels.

Manage Channels
{channels.length === 0 ? (

No notification channels configured yet.{' '} Add one here.

) : (
{channels.map(ch => { const selected = config?.channel_ids?.includes(ch.id) ?? false; return ( ); })}
)} {config && (config.channel_ids?.length ?? 0) === 0 && channels.length > 0 && (

โš ๏ธ No channels selected โ€” reports will be generated but not delivered.

)}
{/* History */}

Report History

{history.length === 0 ? (

No reports generated yet. Generate your first report above.

) : (
{history.map(report => { const isExpanded = expandedReport === report.id; const ov = report.stats?.overview; return (
{isExpanded && report.llm_analysis && (
{report.llm_analysis}
{report.delivery_status && Object.keys(report.delivery_status).length > 0 && (
Delivery: {Object.entries(report.delivery_status).map(([whId, st]) => ( #{whId}: {(st as any).success ? 'OK' : (st as any).error || 'Failed'} ))}
)}
)}
); })}
)}
); }