/** * ApprovalQueueWidget - Human-in-the-Loop Approval Interface * * Displays pending requests requiring human approval. * Allows users to approve/reject operations in real-time. */ import React, { useState, useEffect } from 'react'; import { Shield, Check, X, Clock, AlertTriangle, RefreshCw, ChevronDown, ChevronUp, Info } from 'lucide-react'; import { cn } from '@/lib/utils'; interface ApprovalRequest { id: string; operation: string; description: string; riskLevel: 'low' | 'medium' | 'high' | 'critical'; requestedBy: string; requestedAt: number; expiresAt: number; status: 'pending' | 'approved' | 'rejected' | 'expired'; metadata: Record; } const API_BASE = 'http://localhost:3001/api'; export default function ApprovalQueueWidget() { const [requests, setRequests] = useState([]); const [loading, setLoading] = useState(true); const [expandedId, setExpandedId] = useState(null); const [processing, setProcessing] = useState(null); const fetchPending = async () => { try { const res = await fetch(`${API_BASE}/approvals/pending`); const data = await res.json(); if (data.success) { setRequests(data.requests); } } catch (error) { console.error('Failed to fetch approvals:', error); } finally { setLoading(false); } }; useEffect(() => { fetchPending(); const interval = setInterval(fetchPending, 5000); // Poll every 5s return () => clearInterval(interval); }, []); const handleApprove = async (requestId: string) => { setProcessing(requestId); try { const res = await fetch(`${API_BASE}/approvals/${requestId}/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ approvedBy: 'Dashboard User' }), }); const data = await res.json(); if (data.success) { await fetchPending(); // Refresh list } else { alert(`Approval failed: ${data.error}`); } } catch (error: any) { alert(`Error: ${error.message}`); } finally { setProcessing(null); } }; const handleReject = async (requestId: string) => { const reason = prompt('Enter rejection reason:'); if (!reason) return; setProcessing(requestId); try { const res = await fetch(`${API_BASE}/approvals/${requestId}/reject`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rejectedBy: 'Dashboard User', reason, }), }); const data = await res.json(); if (data.success) { await fetchPending(); } else { alert(`Rejection failed: ${data.error}`); } } catch (error: any) { alert(`Error: ${error.message}`); } finally { setProcessing(null); } }; const getRiskColor = (level: string) => { switch (level) { case 'critical': return 'text-red-400 bg-red-400/10 border-red-400/30'; case 'high': return 'text-orange-400 bg-orange-400/10 border-orange-400/30'; case 'medium': return 'text-yellow-400 bg-yellow-400/10 border-yellow-400/30'; case 'low': return 'text-green-400 bg-green-400/10 border-green-400/30'; default: return 'text-gray-400 bg-gray-400/10 border-gray-400/30'; } }; const getRiskIcon = (level: string) => { switch (level) { case 'critical': return ; case 'high': return ; default: return ; } }; const formatTimeRemaining = (expiresAt: number) => { const remaining = expiresAt - Date.now(); if (remaining < 0) return 'Expired'; const minutes = Math.floor(remaining / 60000); const seconds = Math.floor((remaining % 60000) / 1000); return `${minutes}m ${seconds}s`; }; return (
{/* Header */}
Approval Queue
{requests.length > 0 && ( {requests.length} pending )}
{/* Content */}
{loading ? (
) : requests.length === 0 ? (

No pending approvals

) : ( requests.map(request => { const isExpanded = expandedId === request.id; const timeRemaining = formatTimeRemaining(request.expiresAt); const isExpiringSoon = request.expiresAt - Date.now() < 60000; // < 1 min return (
{/* Header */}
{getRiskIcon(request.riskLevel)} {request.riskLevel} risk {request.operation}

{request.description}

{timeRemaining}
{request.requestedBy}
{/* Expanded details */} {isExpanded && (
{Object.entries(request.metadata).map(([key, value]) => (
{key}: {String(value)}
))}
)} {/* Action buttons */}
); }) )}
{/* Footer */}

🔐 Human-in-the-Loop: Ingen handlinger udført uden din godkendelse

); }