194 lines
7.1 KiB
TypeScript
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>
|
|
);
|
|
}
|