import { useEffect, useState, useMemo } from 'react' import { useNavigate } from 'react-router-dom' import { useAppPath } from '@core/hooks/useAppPath' import { apiFetch } from '@core/lib/api' import { useAuth } from '@core/contexts/AuthContext' import { Input } from '@core/components/ui/input' import { Badge } from '@core/components/ui/badge' import { Button } from '@core/components/ui/button' import { Skeleton } from '@core/components/ui/skeleton' import { Tabs, TabsList, TabsTrigger } from '@core/components/ui/tabs' import { ScrollArea } from '@core/components/ui/scroll-area' import { Headphones, Clock, ChevronRight, List, LayoutGrid, Filter, AlertCircle, User, Timer } from 'lucide-react' interface Ticket { id: string title: string status?: string priority?: string description?: string created_at: string account_id?: string data?: { aim_confidence_threshold?: number aim_confidence_at_response?: number aim_escalation_reason?: string aim_human_assignee_id?: string status?: string } } // 3-Factor Priority Score Calculation interface PriorityScore { total: number urgency: number risk: number staleness: number explanation: string } function calculatePriorityScore(ticket: Ticket): PriorityScore { // Urgency: Plan tier (core=1, custom=2, enterprise=3) - use priority as proxy const tierWeights: Record = { low: 1, medium: 2, high: 3, urgent: 3 } const urgency = tierWeights[ticket.priority || 'low'] || 1 // Risk: Escalation reason const escalationReason = ticket.data?.aim_escalation_reason let risk = 2 // default medium if (escalationReason === 'thumbs_down') risk = 3 else if (escalationReason === 'low_confidence') risk = 2 else if (escalationReason === 'customer_request') risk = 1 // Staleness: Hours since creation (capped at 72h) const hoursSince = Math.min( Math.floor((Date.now() - new Date(ticket.created_at).getTime()) / 3600000), 72 ) const staleness = Math.ceil(hoursSince / 24) // 0-3 scale // Weighted composite const total = Math.round((urgency * 0.4 + risk * 0.35 + staleness * 0.25) / 3 * 100) const explanationParts: string[] = [] if (urgency >= 3) explanationParts.push('High urgency') if (risk >= 3) explanationParts.push('Negative feedback') if (staleness >= 2) explanationParts.push(`${staleness}d stale`) return { total, urgency, risk, staleness, explanation: explanationParts.join(', ') || 'Standard priority' } } const STATUS_BADGE: Record = { open: 'bg-blue-100 text-blue-700', ai_responding: 'bg-purple-100 text-purple-700', human_assigned: 'bg-amber-100 text-amber-700', in_progress: 'bg-amber-100 text-amber-700', resolved: 'bg-green-100 text-green-700', closed: 'bg-muted text-muted-foreground', } function ageLabel(created_at: string) { const diff = Date.now() - new Date(created_at).getTime() const hours = Math.floor(diff / 3600000) if (hours < 1) return 'just now' if (hours < 24) return `${hours}h ago` return `${Math.floor(hours / 24)}d ago` } type FilterType = 'all' | 'mine' | 'unassigned' | 'high_priority' type ViewType = 'list' | 'kanban' // Kanban columns based on AI-first workflow const KANBAN_COLUMNS = [ { id: 'open', label: 'New', color: 'border-blue-200' }, { id: 'to_customer', label: 'To Customer', color: 'border-cyan-200' }, { id: 'ai_responding', label: 'AI Responding', color: 'border-purple-200' }, { id: 'human_assigned', label: 'Human Assigned', color: 'border-amber-200' }, { id: 'in_progress', label: 'In Progress', color: 'border-orange-200' }, { id: 'resolved', label: 'Resolved', color: 'border-green-200' }, { id: 'closed', label: 'Closed', color: 'border-gray-200' }, ] export default function SupportPage() { const navigate = useNavigate() const appPath = useAppPath() const { user } = useAuth() const [tickets, setTickets] = useState([]) const [loading, setLoading] = useState(true) const [search, setSearch] = useState('') const [filter, setFilter] = useState('all') const [view, setView] = useState('list') const [myWatchedIds, setMyWatchedIds] = useState>(new Set()) const [showAllStatuses, setShowAllStatuses] = useState(false) const [showClosed, setShowClosed] = useState(false) useEffect(() => { apiFetch('/api/admin-data?action=list&entity=items&type_slug=support_ticket&limit=500') .then(r => r.json()) .then(j => setTickets(Array.isArray(j?.data) ? j.data : Array.isArray(j) ? j : [])) .catch(() => setTickets([])) .finally(() => setLoading(false)) }, []) // Fetch watched ticket IDs for current user useEffect(() => { if (!user?.id) return apiFetch(`/api/admin-data?action=list&entity=watchers&target_type=item&person_id=${user.id}&limit=500`) .then(r => r.json()) .then(j => { const watchers = Array.isArray(j?.data) ? j.data : Array.isArray(j) ? j : [] setMyWatchedIds(new Set(watchers.map((w: any) => w.target_id))) }) .catch(() => setMyWatchedIds(new Set())) }, [user?.id]) // Calculate priority scores for all tickets const ticketsWithScores = useMemo(() => { return tickets.map(t => ({ ...t, score: calculatePriorityScore(t), effectiveStatus: t.data?.status || t.status })) }, [tickets]) const filtered = ticketsWithScores.filter(t => { const matchesSearch = !search || t.title.toLowerCase().includes(search.toLowerCase()) const matchesFilter = filter === 'all' || (filter === 'high_priority' && t.score.total >= 70) || (filter === 'unassigned' && !t.data?.aim_human_assignee_id) || (filter === 'mine' && myWatchedIds.has(t.id)) return matchesSearch && matchesFilter }) // Sort by priority score descending, then by age const sorted = [...filtered].sort((a, b) => { if (b.score.total !== a.score.total) return b.score.total - a.score.total return new Date(a.created_at).getTime() - new Date(b.created_at).getTime() }) const counts = { all: tickets.length, new: tickets.filter(t => (t.data?.status || t.status) === 'open').length, ai_responding: tickets.filter(t => (t.data?.status || t.status) === 'ai_responding').length, human_assigned: tickets.filter(t => (t.data?.status || t.status) === 'human_assigned').length, resolved: tickets.filter(t => (t.data?.status || t.status) === 'resolved').length, high_priority: ticketsWithScores.filter(t => t.score.total >= 70).length, } // Kanban view: group by status with toggle filters const kanbanGroups = useMemo(() => { const groups: Record = {} KANBAN_COLUMNS.forEach(col => { let columnTickets = sorted.filter(t => t.effectiveStatus === col.id) // Apply showClosed toggle (hide closed if toggle is off) if (!showClosed && col.id === 'closed') { columnTickets = [] } groups[col.id] = columnTickets }) // Apply showAllStatuses toggle (hide empty columns if toggle is off) if (!showAllStatuses) { Object.keys(groups).forEach(key => { if (groups[key].length === 0) { delete groups[key] } }) } return groups }, [sorted, showAllStatuses, showClosed]) const renderListView = () => (
{loading ? (
{Array.from({ length: 5 }).map((_, i) => )}
) : sorted.length === 0 ? (

{search ? 'No tickets match your search.' : 'No tickets found.'}

) : ( {sorted.map(ticket => ( navigate(appPath(`/support/${ticket.id}`))} className="hover:bg-accent/50 cursor-pointer transition-colors" > ))}
Score Subject Status Why Time Waiting
= 70 ? 'destructive' : ticket.score.total >= 50 ? 'default' : 'secondary'} className="font-mono" > {ticket.score.total}

{ticket.title}

{ticket.description &&

{ticket.description}

}
{ticket.effectiveStatus?.replace('_', ' ')} {ticket.score.explanation} {ageLabel(ticket.created_at)}
)}
) const renderKanbanView = () => (
{/* Kanban Controls */}
setShowAllStatuses(e.target.checked)} className="h-4 w-4 text-primary border-gray-300 rounded focus:ring-primary" />
setShowClosed(e.target.checked)} className="h-4 w-4 text-primary border-gray-300 rounded focus:ring-primary" />
{/* Kanban Board */}
{Object.entries(kanbanGroups).map(([columnId, tickets]) => { const column = KANBAN_COLUMNS.find(col => col.id === columnId) if (!column) return null return (
{column.label} {tickets?.length || 0}
{tickets?.map(ticket => (
navigate(appPath(`/support/${ticket.id}`))} className="bg-background border rounded-lg p-3 cursor-pointer hover:shadow-sm transition-shadow" >

{ticket.title}

= 70 ? 'destructive' : 'secondary'} className="text-xs font-mono shrink-0" > {ticket.score.total}
{ageLabel(ticket.created_at)} {ticket.data?.aim_human_assignee_id && ( Assigned )}
))} {(!tickets || tickets.length === 0) && (
No tickets
)}
) })}
) return (

Support Queue

{tickets.length} tickets · {counts.new} new · {counts.human_assigned} need human attention

setView(v as ViewType)}> List Kanban
setSearch(e.target.value)} placeholder="Search tickets…" className="w-72" /> setFilter(v as FilterType)}> All {counts.all} High Priority {counts.high_priority} Unassigned My Tickets
{view === 'list' ? renderListView() : renderKanbanView()}
) }