'use client'; import React, { useState, useEffect, useCallback } from 'react'; import { OperationsRole, SupportTicketSummary, SupportTicketDetail, SupportTicketType, SupportTicketSeverity, SupportTicketStatus, SupportDiagnosticBundle, } from './types'; import { SupportReportModal } from './support-report-modal'; interface OperationsSupportProps { role: OperationsRole; } type TabFilter = 'all' | 'bug_reports' | 'contact_requests' | 'in_progress' | 'resolved'; export function OperationsSupport({ role: _role }: OperationsSupportProps) { const [tickets, setTickets] = useState([]); const [totalCount, setTotalCount] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const [errorMessage, setErrorMessage] = useState(null); // Tab & Search filters const [activeTab, setActiveTab] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); const [appliedSearch, setAppliedSearch] = useState(''); // Ticket Detail state const [selectedTicketId, setSelectedTicketId] = useState(null); const [ticketDetail, setTicketDetail] = useState(null); const [isLoadingDetail, setIsLoadingDetail] = useState(false); const [detailError, setDetailError] = useState(null); // Status transition state const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); const [statusUpdateError, setStatusUpdateError] = useState(null); // Append-only Note form state const [newNoteBody, setNewNoteBody] = useState(''); const [isSubmittingNote, setIsSubmittingNote] = useState(false); const [noteError, setNoteError] = useState(null); // Diagnostics Inspection Modal state const [isDiagnosticsModalOpen, setIsDiagnosticsModalOpen] = useState(false); const [diagnosticsReason, setDiagnosticsReason] = useState(''); const [isLoadingDiagnostics, setIsLoadingDiagnostics] = useState(false); const [diagnosticsError, setDiagnosticsError] = useState(null); const [diagnosticBundle, setDiagnosticBundle] = useState(null); // Fetch tickets list (Manual only, zero automatic polling) const fetchTickets = useCallback( async (isManualRefresh = false) => { if (isManualRefresh) { setIsRefreshing(true); } else { setIsLoading(true); setErrorMessage(null); } const params = new URLSearchParams(); if (appliedSearch.trim()) { params.set('q', appliedSearch.trim()); } if (activeTab === 'bug_reports') { params.set('type_filter', 'bug_report'); } else if (activeTab === 'contact_requests') { params.set('type_filter', 'contact_request'); } else if (activeTab === 'in_progress') { params.set('status_filter', 'in_progress'); } else if (activeTab === 'resolved') { params.set('status_filter', 'resolved'); } try { const response = await fetch(`/api/ops/support/tickets?${params.toString()}`, { method: 'GET', headers: { accept: 'application/json' }, cache: 'no-store', }); if (response.status === 401) { setErrorMessage('Your Operations session has expired.'); return; } if (response.status === 403) { setErrorMessage('You do not have authority to view support tickets.'); return; } if (!response.ok) { setErrorMessage('Failed to load support tickets from platform.'); return; } const data = await response.json(); const items: SupportTicketSummary[] = Array.isArray(data?.items) ? data.items : []; setTickets(items); if (typeof data?.total === 'number') { setTotalCount(data.total); } else { setTotalCount(items.length); } } catch { setErrorMessage('Unable to connect to Operations platform.'); } finally { setIsLoading(false); setIsRefreshing(false); } }, [activeTab, appliedSearch] ); // Initial fetch on tab or applied search change useEffect(() => { fetchTickets(false); }, [fetchTickets]); // Fetch individual ticket detail const fetchTicketDetail = useCallback(async (ticketId: string) => { setIsLoadingDetail(true); setDetailError(null); setDiagnosticBundle(null); setDiagnosticsError(null); setNoteError(null); setStatusUpdateError(null); try { const response = await fetch(`/api/ops/support/tickets/${encodeURIComponent(ticketId)}`, { method: 'GET', headers: { accept: 'application/json' }, cache: 'no-store', }); if (response.status === 401) { setDetailError('Your Operations session has expired.'); return; } if (response.status === 403) { setDetailError('You do not have authority to view this ticket.'); return; } if (response.status === 404) { setDetailError('Support ticket not found.'); return; } if (!response.ok) { setDetailError('Failed to load ticket details.'); return; } const data = (await response.json()) as SupportTicketDetail; setTicketDetail(data); if (data.diagnostic_bundle) { setDiagnosticBundle(data.diagnostic_bundle); } } catch { setDetailError('Unable to connect to Operations platform.'); } finally { setIsLoadingDetail(false); } }, []); const handleSelectTicket = (ticketId: string) => { setSelectedTicketId(ticketId); void fetchTicketDetail(ticketId); }; const handleBackToList = () => { setSelectedTicketId(null); setTicketDetail(null); setDiagnosticBundle(null); setIsDiagnosticsModalOpen(false); setDiagnosticsReason(''); }; // Status transition action const handleUpdateStatus = async (newStatus: SupportTicketStatus) => { if (!selectedTicketId || isUpdatingStatus) return; setIsUpdatingStatus(true); setStatusUpdateError(null); try { const response = await fetch(`/api/ops/support/tickets/${encodeURIComponent(selectedTicketId)}`, { method: 'PATCH', headers: { 'content-type': 'application/json', accept: 'application/json', }, body: JSON.stringify({ status: newStatus }), }); if (response.status === 401) { setStatusUpdateError('Your Operations session has expired.'); return; } if (response.status === 403) { setStatusUpdateError('You do not have authority to update ticket status.'); return; } if (!response.ok) { const errData = await response.json().catch(() => null); setStatusUpdateError(errData?.detail ?? 'Failed to update ticket status.'); return; } const updated = (await response.json()) as SupportTicketDetail; setTicketDetail(updated); // Refresh list to update status badge in background void fetchTickets(false); } catch { setStatusUpdateError('Network error while updating ticket status.'); } finally { setIsUpdatingStatus(false); } }; // Append-only Note submission const handleAddNote = async (e: React.FormEvent) => { e.preventDefault(); if (!selectedTicketId || !newNoteBody.trim() || isSubmittingNote) return; setIsSubmittingNote(true); setNoteError(null); try { const response = await fetch(`/api/ops/support/tickets/${encodeURIComponent(selectedTicketId)}/notes`, { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json', }, body: JSON.stringify({ body: newNoteBody.trim() }), }); if (response.status === 401) { setNoteError('Your Operations session has expired.'); return; } if (response.status === 403) { setNoteError('You do not have authority to append notes.'); return; } if (!response.ok) { const errData = await response.json().catch(() => null); setNoteError(errData?.detail ?? 'Failed to append note.'); return; } setNewNoteBody(''); // Re-fetch detail to get updated notes list void fetchTicketDetail(selectedTicketId); } catch { setNoteError('Network error while saving note.'); } finally { setIsSubmittingNote(false); } }; // Inspect diagnostics with audited reason const handleInspectDiagnostics = async (e: React.FormEvent) => { e.preventDefault(); if (!selectedTicketId || diagnosticsReason.trim().length < 3 || isLoadingDiagnostics) return; setIsLoadingDiagnostics(true); setDiagnosticsError(null); try { const params = new URLSearchParams({ reason: diagnosticsReason.trim() }); const response = await fetch( `/api/ops/support/tickets/${encodeURIComponent(selectedTicketId)}/diagnostics?${params.toString()}`, { method: 'GET', headers: { accept: 'application/json' }, cache: 'no-store', } ); if (response.status === 401) { setDiagnosticsError('Your Operations session has expired.'); return; } if (response.status === 403) { setDiagnosticsError('You do not have authority to inspect diagnostics.'); return; } if (response.status === 404) { setDiagnosticsError('No diagnostic bundle found for this ticket.'); return; } if (response.status === 422) { setDiagnosticsError('Operational reason must be at least 3 characters.'); return; } if (!response.ok) { setDiagnosticsError('Failed to retrieve diagnostic bundle.'); return; } const bundle = (await response.json()) as SupportDiagnosticBundle; setDiagnosticBundle(bundle); setIsDiagnosticsModalOpen(false); setDiagnosticsReason(''); } catch { setDiagnosticsError('Network error while retrieving diagnostics.'); } finally { setIsLoadingDiagnostics(false); } }; // Badge render helpers const renderTypeBadge = (type: SupportTicketType) => { switch (type) { case 'bug_report': return ( bug_report Bug Report ); case 'contact_request': return ( mail Contact ); case 'telemetry_issue': return ( sensors Telemetry ); case 'general': default: return ( help_outline General ); } }; const renderSeverityBadge = (sev: SupportTicketSeverity) => { switch (sev) { case 'critical': return ( Critical ); case 'high': return ( High ); case 'medium': return ( Medium ); case 'low': default: return ( Low ); } }; const renderStatusBadge = (status: SupportTicketStatus) => { switch (status) { case 'open': return ( Open ); case 'in_progress': return ( In Progress ); case 'resolved': return ( Resolved ); case 'closed': default: return ( Closed ); } }; // If a ticket is selected, render Ticket Detail Pane if (selectedTicketId) { return (
{/* Detail Top Header */}

{ticketDetail ? ticketDetail.case_reference : 'Ticket Details'}

{/* Action Buttons */} {ticketDetail && (
{ticketDetail.status !== 'in_progress' && ( )} {ticketDetail.status !== 'resolved' && ( )} {ticketDetail.status !== 'closed' && ( )} {(ticketDetail.status === 'resolved' || ticketDetail.status === 'closed') && ( )}
)}
{statusUpdateError && (
{statusUpdateError}
)} {isLoadingDetail && (
Loading ticket details...
)} {detailError && (
{detailError}
)} {ticketDetail && !isLoadingDetail && (
{/* Main Content Column (2 cols) */}
{/* Ticket Overview Card */}
{renderTypeBadge(ticketDetail.ticket_type)} {renderSeverityBadge(ticketDetail.severity)} {renderStatusBadge(ticketDetail.status)}

{ticketDetail.subject}

{ticketDetail.description}

{/* Submitted Diagnostics Section */}
terminal

Submitted Diagnostics

{ticketDetail.has_diagnostics && !diagnosticBundle && ( )}
{!ticketDetail.has_diagnostics ? (

No diagnostic bundle was submitted with this support ticket.

) : !diagnosticBundle ? (
Diagnostic environment & telemetry logs available

Inspection requires audit justification to ensure rider telemetry privacy.

) : (
{/* Environment metadata grid */}
App Version {diagnosticBundle.app_version}
OS Version {diagnosticBundle.os_version}
Device Model {diagnosticBundle.device_model}
Submitted At {new Date(diagnosticBundle.submitted_at).toLocaleTimeString()}
{/* Sensor Status */}
Sensor Fusion & Hardware State {diagnosticBundle.sensor_status}
{/* Stack Trace */} {diagnosticBundle.stack_trace && (
Captured Exception Stack Trace
                          {diagnosticBundle.stack_trace}
                        
)} {/* Logs Snippet */} {diagnosticBundle.logs_snippet && (
Device Logs Snippet
                          {diagnosticBundle.logs_snippet}
                        
)}
)}
{/* Append-Only Internal Notes Section */}
note_stack

Internal Operator Notes ({ticketDetail.notes?.length ?? 0})

{/* Notes List */}
{!ticketDetail.notes || ticketDetail.notes.length === 0 ? (

No operator notes appended yet.

) : ( ticketDetail.notes.map((note) => (
{note.author_identity} {new Date(note.created_at).toLocaleString()}

{note.body}

)) )}
{/* Add Note Form */}