import React, { useState, useEffect, useCallback } from 'react'; import { X, MessageSquare, Send, Loader, AlertCircle, Clock, Shield, Trash2, Calendar, FileText, Save } from 'lucide-react'; import ConfirmModal from '../ConfirmModal'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const TEAL = '#14B8A6'; const CATEGORY_COLORS = { 'Vulnerability Management': '#EF4444', 'Access & MFA': '#F59E0B', 'Logging & Monitoring': '#8B5CF6', 'End-of-Life OS': '#F97316', 'Decommissioned Assets': '#64748B', 'Asset Data Quality': '#64748B', 'Application Security': '#0EA5E9', 'Disaster Recovery': TEAL, 'Endpoint Protection': '#F97316', }; function categoryColor(category) { return CATEGORY_COLORS[category] || '#94A3B8'; } function MetricChip({ metricId, category, status }) { const color = status === 'resolved' ? '#64748B' : categoryColor(category); return ( {metricId} ); } export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded, onNavigate }) { const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [noteText, setNoteText] = useState(''); const [selectedMetrics, setSelectedMetrics] = useState([]); const [submitting, setSubmitting] = useState(false); const [noteError, setNoteError] = useState(null); const [pendingConfirm, setPendingConfirm] = useState(null); // Metadata fields const [resolutionDate, setResolutionDate] = useState(''); const [remediationPlan, setRemediationPlan] = useState(''); const [metaSaving, setMetaSaving] = useState(false); const [metaError, setMetaError] = useState(null); const handleSaveMetadata = async (fields) => { setMetaSaving(true); setMetaError(null); try { const res = await fetch(`${API_BASE}/compliance/items/${encodeURIComponent(hostname)}/metadata`, { method: 'PATCH', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(fields), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Failed to save metadata'); } catch (err) { setMetaError(err.message); } finally { setMetaSaving(false); } }; const fetchDetail = useCallback(async () => { setLoading(true); setError(null); try { const res = await fetch(`${API_BASE}/compliance/items/${encodeURIComponent(hostname)}`, { credentials: 'include' }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Failed to load device'); setDetail(data); // Default selected metrics to first active failing metric const firstActive = (data.metrics || []).find(m => m.status === 'active'); if (firstActive) setSelectedMetrics([firstActive.metric_id]); // Populate metadata fields setResolutionDate(data.resolution_date || ''); setRemediationPlan(data.remediation_plan || ''); } catch (err) { setError(err.message); } finally { setLoading(false); } }, [hostname]); useEffect(() => { fetchDetail(); }, [fetchDetail]); const handleAddNote = async () => { if (!noteText.trim() || selectedMetrics.length === 0) return; setSubmitting(true); setNoteError(null); try { const res = await fetch(`${API_BASE}/compliance/notes`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hostname, metric_ids: selectedMetrics, note: noteText.trim() }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Failed to save note'); setNoteText(''); await fetchDetail(); if (onNoteAdded) onNoteAdded(); } catch (err) { setNoteError(err.message); } finally { setSubmitting(false); } }; const handleDeleteNote = async (noteId, hasGroup) => { setPendingConfirm({ title: 'Delete Note', message: 'Delete this note?', confirmText: 'Delete', onConfirm: async () => { setPendingConfirm(null); try { const url = hasGroup ? `${API_BASE}/compliance/notes/${noteId}?group=true` : `${API_BASE}/compliance/notes/${noteId}`; const res = await fetch(url, { method: 'DELETE', credentials: 'include' }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Failed to delete note'); await fetchDetail(); if (onNoteAdded) onNoteAdded(); } catch (err) { setNoteError(err.message); } }, }); }; const activeMetrics = detail?.metrics?.filter(m => m.status === 'active') || []; const resolvedMetrics = detail?.metrics?.filter(m => m.status === 'resolved') || []; return ( <> {/* Backdrop */}
{/* Panel */}