import React, { useState, useEffect, useCallback } from 'react'; import { X, Loader, AlertCircle, Send, RefreshCw } from 'lucide-react'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; // --------------------------------------------------------------------------- // Styles — matches dark theme tactical intelligence aesthetic // --------------------------------------------------------------------------- const STYLES = { overlay: { position: 'fixed', inset: 0, zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', }, backdrop: { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(4px)', }, content: { position: 'relative', background: 'linear-gradient(135deg, #1E293B, #0F172A)', border: '1px solid rgba(14, 165, 233, 0.25)', borderRadius: '16px', padding: '2rem', width: '90%', maxWidth: '560px', maxHeight: '85vh', overflowY: 'auto', zIndex: 101, }, header: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: '1.25rem', }, title: { fontFamily: 'monospace', fontSize: '0.8rem', fontWeight: 700, color: '#A855F7', textTransform: 'uppercase', letterSpacing: '0.1em', }, subtitle: { fontFamily: 'monospace', fontSize: '0.7rem', color: '#94A3B8', marginTop: '0.35rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '400px', }, closeBtn: { background: 'none', border: 'none', color: '#64748B', cursor: 'pointer', padding: '0.25rem', borderRadius: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center', }, textarea: { width: '100%', boxSizing: 'border-box', minHeight: '100px', background: 'rgba(15, 23, 42, 0.8)', border: '1px solid rgba(14, 165, 233, 0.2)', borderRadius: '8px', padding: '0.75rem', color: '#F8FAFC', fontSize: '0.85rem', fontFamily: 'monospace', resize: 'vertical', outline: 'none', }, charCounter: { fontFamily: 'monospace', fontSize: '0.6rem', color: '#64748B', textAlign: 'right', marginTop: '0.25rem', }, submitBtn: { padding: '0.5rem 1rem', borderRadius: '8px', border: '1px solid rgba(168, 85, 247, 0.4)', background: 'rgba(168, 85, 247, 0.15)', color: '#C084FC', cursor: 'pointer', fontSize: '0.8rem', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: '0.4rem', transition: 'all 0.2s', }, submitBtnDisabled: { opacity: 0.4, cursor: 'not-allowed', }, noteItem: { padding: '0.75rem', marginBottom: '0.5rem', background: 'rgba(14, 165, 233, 0.04)', border: '1px solid rgba(14, 165, 233, 0.1)', borderRadius: '8px', }, noteMeta: { fontFamily: 'monospace', fontSize: '0.6rem', color: '#64748B', marginBottom: '0.35rem', display: 'flex', gap: '0.5rem', }, noteText: { fontFamily: 'monospace', fontSize: '0.75rem', color: '#CBD5E1', lineHeight: 1.5, whiteSpace: 'pre-wrap', wordBreak: 'break-word', }, error: { display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 0.75rem', background: 'rgba(239, 68, 68, 0.08)', border: '1px solid rgba(239, 68, 68, 0.2)', borderRadius: '0.5rem', marginBottom: '0.75rem', }, errorText: { fontFamily: 'monospace', fontSize: '0.7rem', color: '#FCA5A5', }, emptyState: { textAlign: 'center', padding: '1.5rem 0', fontFamily: 'monospace', fontSize: '0.7rem', color: '#475569', }, divider: { height: '1px', background: 'rgba(14, 165, 233, 0.1)', margin: '1rem 0', }, retryBtn: { padding: '0.4rem 0.75rem', borderRadius: '6px', border: '1px solid rgba(14, 165, 233, 0.3)', background: 'rgba(14, 165, 233, 0.1)', color: '#7DD3FC', cursor: 'pointer', fontSize: '0.7rem', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: '0.3rem', marginTop: '0.5rem', }, }; // --------------------------------------------------------------------------- // RemediationModal — add and view remediation notes for a queue item // --------------------------------------------------------------------------- export default function RemediationModal({ item, onClose, onNoteAdded }) { const [notes, setNotes] = useState([]); const [newNoteText, setNewNoteText] = useState(''); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [fetchError, setFetchError] = useState(null); // --------------------------------------------------------------------------- // Fetch existing notes on mount // --------------------------------------------------------------------------- const fetchNotes = useCallback(async () => { setLoading(true); setFetchError(null); try { const res = await fetch(`${API_BASE}/ivanti/todo-queue/${item.id}/notes`, { credentials: 'include', }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || `HTTP ${res.status}`); } const data = await res.json(); setNotes(data); } catch (e) { setFetchError(e.message || 'Failed to load notes.'); } finally { setLoading(false); } }, [item.id]); useEffect(() => { fetchNotes(); }, [fetchNotes]); // --------------------------------------------------------------------------- // Submit new note // --------------------------------------------------------------------------- const handleSubmit = useCallback(async () => { if (!newNoteText.trim() || saving) return; setSaving(true); setError(null); try { const res = await fetch(`${API_BASE}/ivanti/todo-queue/${item.id}/notes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ note_text: newNoteText }), }); const data = await res.json(); if (!res.ok) { throw new Error(data.error || `HTTP ${res.status}`); } // Prepend new note to list (most recent first) setNotes((prev) => [data, ...prev]); setNewNoteText(''); if (onNoteAdded) onNoteAdded(); } catch (e) { setError(e.message || 'Failed to save note.'); } finally { setSaving(false); } }, [newNoteText, saving, item.id, onNoteAdded]); // --------------------------------------------------------------------------- // Format date as YYYY-MM-DD // --------------------------------------------------------------------------- const formatDate = (dateStr) => { try { const d = new Date(dateStr); return d.toISOString().slice(0, 10); } catch { return '—'; } }; const canSubmit = newNoteText.trim().length > 0 && !saving; const remaining = 5000 - newNoteText.length; return (
{/* Header */}
Remediation Notes
{item.finding_title || item.finding_id}
ID: {item.finding_id}
{/* New note input */}