'use client'; import React, { useState, useEffect } from 'react'; import { SafeIncidentDetail, SafeIncidentNote, IncidentSeverity, IncidentStatus, OperationsRole, } from './types'; import { OperationsIncidentModal, IncidentModalAction, } from './operations-incident-modal'; import { isValidStatusTransition } from '../../lib/ops-data'; interface OperationsIncidentDetailProps { caseReference: string; role: OperationsRole; onBack: () => void; } export function OperationsIncidentDetail({ caseReference, role, onBack, }: OperationsIncidentDetailProps) { const isOwner = role === 'owner'; const [incident, setIncident] = useState(null); const [notes, setNotes] = useState([]); const [isLoading, setIsLoading] = useState(true); const [errorMessage, setErrorMessage] = useState(null); // Note form state const [newNoteBody, setNewNoteBody] = useState(''); const [isAddingNote, setIsAddingNote] = useState(false); const [noteError, setNoteError] = useState(null); // Mutation state const [isMutating, setIsMutating] = useState(false); const [mutationSuccess, setMutationSuccess] = useState(null); // Confirmation Modal state const [modalOpen, setModalOpen] = useState(false); const [modalAction, setModalAction] = useState('close'); const [isSubmittingModal, setIsSubmittingModal] = useState(false); async function fetchIncidentData() { setIsLoading(true); setErrorMessage(null); try { const [detailRes, notesRes] = await Promise.all([ fetch(`/api/ops/incidents/${encodeURIComponent(caseReference)}`, { method: 'GET', headers: { accept: 'application/json' }, cache: 'no-store', }), fetch(`/api/ops/incidents/${encodeURIComponent(caseReference)}/notes`, { method: 'GET', headers: { accept: 'application/json' }, cache: 'no-store', }), ]); if (detailRes.status === 401 || notesRes.status === 401) { setErrorMessage('Your Operations session has expired.'); setIsLoading(false); return; } if (detailRes.status === 403 || notesRes.status === 403) { setErrorMessage('You do not have authority to view this incident.'); setIsLoading(false); return; } if (detailRes.status === 404) { setErrorMessage('Incident not found.'); setIsLoading(false); return; } if (!detailRes.ok) { setErrorMessage('Failed to load incident detail.'); setIsLoading(false); return; } const detailData = (await detailRes.json()) as SafeIncidentDetail; setIncident(detailData); if (notesRes.ok) { const notesData = await notesRes.json(); setNotes(Array.isArray(notesData?.notes) ? notesData.notes : []); } } catch { setErrorMessage('Unable to connect to Operations platform.'); } finally { setIsLoading(false); } } useEffect(() => { fetchIncidentData(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [caseReference]); // Handle Add Note async function handleAddNote(e: React.FormEvent) { e.preventDefault(); if (!newNoteBody.trim()) { setNoteError('Note body cannot be empty.'); return; } setIsAddingNote(true); setNoteError(null); try { const res = await fetch(`/api/ops/incidents/${encodeURIComponent(caseReference)}/notes`, { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json', }, body: JSON.stringify({ body: newNoteBody.trim() }), }); if (res.status === 401) { setNoteError('Your Operations session has expired.'); setIsAddingNote(false); return; } if (res.status === 403) { setNoteError('You do not have authority to add notes.'); setIsAddingNote(false); return; } if (res.status === 409) { setNoteError('Archived incidents cannot be modified.'); setIsAddingNote(false); return; } if (!res.ok) { setNoteError('Failed to append note.'); setIsAddingNote(false); return; } const addedNote = (await res.json()) as SafeIncidentNote; setNotes((prev) => [addedNote, ...prev]); setNewNoteBody(''); } catch { setNoteError('Unable to connect to Operations platform.'); } finally { setIsAddingNote(false); } } // Handle Severity Change async function handleSeverityChange(newSeverity: IncidentSeverity) { if (!incident || incident.severity === newSeverity || isMutating) return; setIsMutating(true); setMutationSuccess(null); setErrorMessage(null); try { const res = await fetch(`/api/ops/incidents/${encodeURIComponent(caseReference)}`, { method: 'PATCH', headers: { 'content-type': 'application/json', accept: 'application/json', }, body: JSON.stringify({ severity: newSeverity }), }); if (!res.ok) { const data = await res.json().catch(() => null); setErrorMessage(data?.detail ?? 'Failed to update severity.'); setIsMutating(false); return; } const updated = (await res.json()) as SafeIncidentDetail; setIncident(updated); setMutationSuccess(`Severity updated to ${newSeverity.toUpperCase()}`); setTimeout(() => setMutationSuccess(null), 4000); } catch { setErrorMessage('Unable to connect to Operations platform.'); } finally { setIsMutating(false); } } // Handle Status Transition via Select async function handleStatusChange(targetStatus: IncidentStatus) { if (!incident || incident.status === targetStatus || isMutating) return; // If target status is closed, route to modal to capture resolution if (targetStatus === 'closed') { setModalAction('close'); setModalOpen(true); return; } // If target status is archived, route to modal if (targetStatus === 'archived') { setModalAction('archive'); setModalOpen(true); return; } // If target status is resolved without existing resolution, prompt for modal if (targetStatus === 'resolved' && (!incident.resolution || !incident.resolution.trim())) { setModalAction('close'); setModalOpen(true); return; } setIsMutating(true); setMutationSuccess(null); setErrorMessage(null); try { const res = await fetch(`/api/ops/incidents/${encodeURIComponent(caseReference)}`, { method: 'PATCH', headers: { 'content-type': 'application/json', accept: 'application/json', }, body: JSON.stringify({ status: targetStatus }), }); if (!res.ok) { const data = await res.json().catch(() => null); setErrorMessage(data?.detail ?? 'Failed to update incident status.'); setIsMutating(false); return; } const updated = (await res.json()) as SafeIncidentDetail; setIncident(updated); setMutationSuccess(`Status transitioned to ${targetStatus.toUpperCase()}`); setTimeout(() => setMutationSuccess(null), 4000); } catch { setErrorMessage('Unable to connect to Operations platform.'); } finally { setIsMutating(false); } } // Handle Modal Confirmation async function handleModalConfirm(modalResolution?: string) { if (!incident || isSubmittingModal) return; setIsSubmittingModal(true); setErrorMessage(null); let patchPayload: Record = {}; if (modalAction === 'close') { patchPayload = { status: 'closed', resolution: modalResolution ?? '', }; } else if (modalAction === 'reopen') { patchPayload = { status: 'investigating' }; } else if (modalAction === 'archive') { patchPayload = { status: 'archived' }; } try { const res = await fetch(`/api/ops/incidents/${encodeURIComponent(caseReference)}`, { method: 'PATCH', headers: { 'content-type': 'application/json', accept: 'application/json', }, body: JSON.stringify(patchPayload), }); if (!res.ok) { const data = await res.json().catch(() => null); setErrorMessage(data?.detail ?? `Failed to perform ${modalAction} action.`); setIsSubmittingModal(false); setModalOpen(false); return; } const updated = (await res.json()) as SafeIncidentDetail; setIncident(updated); setModalOpen(false); setMutationSuccess(`Incident successfully ${modalAction === 'close' ? 'closed' : modalAction === 'reopen' ? 'reopened' : 'archived'}.`); setTimeout(() => setMutationSuccess(null), 4000); } catch { setErrorMessage('Unable to connect to Operations platform.'); } finally { setIsSubmittingModal(false); } } function formatDate(iso?: string | null) { if (!iso) return '—'; try { const d = new Date(iso); return d.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }); } catch { return iso; } } function getSeverityBadge(severity: IncidentSeverity) { switch (severity) { case 'critical': return ( Critical ); case 'high': return ( High ); case 'medium': return ( Medium ); case 'low': default: return ( Low ); } } function getStatusBadge(status: IncidentStatus) { switch (status) { case 'open': return ( Open ); case 'investigating': return ( Investigating ); case 'monitoring': return ( Monitoring ); case 'resolved': return ( Resolved ); case 'closed': return ( Closed ); case 'archived': default: return ( Archived ); } } if (isLoading) { return (
progress_activity Loading incident details...
); } if (!incident) { return (
{errorMessage ?? 'Incident not found.'}
); } const isArchived = incident.status === 'archived'; const isClosed = incident.status === 'closed'; return (
{/* Navigation & Actions Topbar */}
{incident.case_reference}
{/* Quick Modal Triggers */} {!isArchived && !isClosed && ( )} {isClosed && isOwner && ( )} {!isArchived && isOwner && ( )}
{/* Alerts */} {errorMessage && (
error {errorMessage}
)} {mutationSuccess && (
check_circle {mutationSuccess}
)} {/* Incident Header Card */}
{incident.case_reference}
{getSeverityBadge(incident.severity)} {getStatusBadge(incident.status)}

{incident.title}

{/* Mutation Selects (Active incidents only) */} {!isArchived && (
{/* Severity Selector */}
Severity
{/* Status Transition Selector */}
Transition Status
)}
{/* Metadata Grid */}
Created By {incident.created_by_identity}
Created At {formatDate(incident.created_at)}
Last Updated {formatDate(incident.updated_at)}
Closed {incident.closed_at ? formatDate(incident.closed_at) : '—'}
{/* Summary Card */}

Incident Summary

{incident.summary}
{/* Resolution Card (if present) */} {incident.resolution && (
task_alt

Resolution Record

{incident.resolution}
)} {/* Two Column Layout: Operational Timeline & Append-only Notes */}
{/* Append-only Notes Section */}
notes

Operator Notes

Append-Only ({notes.length})
{/* Add Note Form */} {!isArchived && (
{newNoteBody.length} / 4000