MotorSignal-App/website/components/ops/operations-incident-modal.tsx

194 lines
7.1 KiB
TypeScript

'use client';
import React, { useState, useEffect, useRef } from 'react';
export type IncidentModalAction = 'close' | 'reopen' | 'archive';
interface OperationsIncidentModalProps {
isOpen: boolean;
action: IncidentModalAction;
caseReference: string;
initialResolution?: string;
isSubmitting: boolean;
onConfirm: (resolution?: string) => void;
onClose: () => void;
}
export function OperationsIncidentModal({
isOpen,
action,
caseReference,
initialResolution = '',
isSubmitting,
onConfirm,
onClose,
}: OperationsIncidentModalProps) {
const [resolution, setResolution] = useState(initialResolution);
const [error, setError] = useState<string | null>(null);
const modalRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
setResolution(initialResolution);
setError(null);
}, [isOpen, initialResolution, action]);
useEffect(() => {
if (isOpen) {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && !isSubmitting) {
onClose();
}
};
window.addEventListener('keydown', handleKeyDown);
// Auto-focus appropriate element
setTimeout(() => {
if (action === 'close' && textareaRef.current) {
textareaRef.current.focus();
} else if (modalRef.current) {
modalRef.current.focus();
}
}, 50);
return () => window.removeEventListener('keydown', handleKeyDown);
}
}, [isOpen, isSubmitting, action, onClose]);
if (!isOpen) return null;
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (action === 'close') {
if (!resolution.trim()) {
setError('Resolution summary is required before closing this incident.');
return;
}
onConfirm(resolution.trim());
} else {
onConfirm();
}
}
const titles: Record<IncidentModalAction, string> = {
close: 'Close Incident',
reopen: 'Reopen Incident',
archive: 'Archive Incident',
};
const descriptions: Record<IncidentModalAction, string> = {
close: `Closing incident ${caseReference} requires an authoritative resolution record.`,
reopen: `Reopening incident ${caseReference} will transition status back to INVESTIGATING. (Owner authorization required)`,
archive: `Archiving incident ${caseReference} is terminal. No further mutations will be permitted. (Owner authorization required)`,
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm animate-in fade-in duration-150">
<div
ref={modalRef}
tabIndex={-1}
className="w-full max-w-md bg-surface-container-low border border-outline-variant/40 rounded-2xl p-6 shadow-2xl space-y-5 focus:outline-none"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
{/* Modal Header */}
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-primary-container text-[20px]">
{action === 'close' ? 'task_alt' : action === 'reopen' ? 'replay' : 'archive'}
</span>
<h2 id="modal-title" className="text-base font-headline-sm font-bold text-on-surface">
{titles[action]}
</h2>
</div>
<p className="text-xs font-mono font-bold text-primary-fixed-dim">
{caseReference}
</p>
</div>
<button
type="button"
onClick={onClose}
disabled={isSubmitting}
className="text-outline hover:text-on-surface p-1 rounded-lg hover:bg-surface-container transition-colors"
aria-label="Close modal"
>
<span className="material-symbols-outlined text-[18px]">close</span>
</button>
</div>
<p className="text-xs text-on-surface-variant leading-relaxed">
{descriptions[action]}
</p>
{error && (
<div className="p-3 rounded-lg bg-error/10 border border-error/30 text-error text-xs flex items-center gap-2">
<span className="material-symbols-outlined text-[16px]">error</span>
<span>{error}</span>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
{action === 'close' && (
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<label htmlFor="modal-resolution" className="block text-xs font-semibold text-on-surface">
Resolution Summary <span className="text-error">*</span>
</label>
<span className="text-[10px] font-mono text-outline">
{resolution.length} / 4000
</span>
</div>
<textarea
ref={textareaRef}
id="modal-resolution"
required
rows={4}
maxLength={4000}
value={resolution}
onChange={(e) => setResolution(e.target.value)}
placeholder="Explain the root cause and resolution applied..."
className="w-full bg-surface-container px-3.5 py-2 rounded-lg border border-outline-variant/40 text-xs text-on-surface placeholder:text-outline focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none resize-y"
/>
</div>
)}
{/* Modal Actions */}
<div className="flex items-center justify-end gap-3 pt-2">
<button
type="button"
onClick={onClose}
disabled={isSubmitting}
className="px-4 py-2 rounded-lg bg-surface-container border border-outline-variant/30 text-xs font-semibold text-on-surface-variant hover:text-on-surface transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-xs font-bold shadow-md transition-all focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none disabled:opacity-50 ${
action === 'archive'
? 'bg-error text-on-error hover:bg-error/90'
: 'bg-primary-container text-on-primary-container hover:bg-primary-container/90'
}`}
>
{isSubmitting && (
<span className="material-symbols-outlined text-[16px] animate-spin">
progress_activity
</span>
)}
<span>
{action === 'close'
? 'Confirm & Close'
: action === 'reopen'
? 'Confirm Reopen'
: 'Confirm Archive'}
</span>
</button>
</div>
</form>
</div>
</div>
);
}