'use client' /** * Escalation Queue Page * * DSB Review & Approval Workflow for UCCA Assessments * Implements E0-E3 escalation levels with SLA tracking * * API: /sdk/v1/ucca/escalations */ import { useEffect, useState, useCallback } from 'react' import { PagePurpose } from '@/components/common/PagePurpose' // Types interface Escalation { id: string tenant_id: string assessment_id: string escalation_level: 'E0' | 'E1' | 'E2' | 'E3' escalation_reason: string assigned_to?: string assigned_role?: string assigned_at?: string status: 'pending' | 'assigned' | 'in_review' | 'approved' | 'rejected' | 'returned' reviewer_id?: string reviewer_notes?: string reviewed_at?: string decision?: 'approve' | 'reject' | 'modify' | 'escalate' decision_notes?: string decision_at?: string conditions?: string[] created_at: string updated_at: string due_date?: string notification_sent: boolean // Joined fields assessment_title?: string assessment_feasibility?: string assessment_risk_score?: number assessment_domain?: string } interface EscalationHistory { id: string escalation_id: string action: string old_status?: string new_status?: string old_level?: string new_level?: string actor_id: string actor_role?: string notes?: string created_at: string } interface EscalationStats { total_pending: number total_in_review: number total_approved: number total_rejected: number by_level: Record overdue_sla: number approaching_sla: number avg_resolution_hours: number } interface DSBPoolMember { id: string tenant_id: string user_id: string user_name: string user_email: string role: string is_active: boolean max_concurrent_reviews: number current_reviews: number created_at: string updated_at: string } // Constants const LEVEL_CONFIG = { E0: { label: 'Auto-Approve', color: 'bg-green-100 text-green-800', description: 'Automatische Freigabe' }, E1: { label: 'Team-Lead', color: 'bg-blue-100 text-blue-800', description: 'Team-Lead Review erforderlich' }, E2: { label: 'DSB', color: 'bg-yellow-100 text-yellow-800', description: 'DSB-Konsultation erforderlich' }, E3: { label: 'DSB + Legal', color: 'bg-red-100 text-red-800', description: 'DSB + Rechtsabteilung erforderlich' }, } const STATUS_CONFIG = { pending: { label: 'Ausstehend', color: 'bg-gray-100 text-gray-800' }, assigned: { label: 'Zugewiesen', color: 'bg-blue-100 text-blue-800' }, in_review: { label: 'In Prüfung', color: 'bg-yellow-100 text-yellow-800' }, approved: { label: 'Genehmigt', color: 'bg-green-100 text-green-800' }, rejected: { label: 'Abgelehnt', color: 'bg-red-100 text-red-800' }, returned: { label: 'Zurückgegeben', color: 'bg-orange-100 text-orange-800' }, } export default function EscalationsPage() { const [escalations, setEscalations] = useState([]) const [stats, setStats] = useState(null) const [dsbPool, setDsbPool] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // Filters const [statusFilter, setStatusFilter] = useState('pending') const [levelFilter, setLevelFilter] = useState('') const [myReviewsOnly, setMyReviewsOnly] = useState(false) // Selected escalation for detail view const [selectedEscalation, setSelectedEscalation] = useState(null) const [escalationHistory, setEscalationHistory] = useState([]) // Decision modal const [showDecisionModal, setShowDecisionModal] = useState(false) const [decisionForm, setDecisionForm] = useState({ decision: 'approve' as 'approve' | 'reject' | 'modify' | 'escalate', decision_notes: '', conditions: [] as string[], }) const [newCondition, setNewCondition] = useState('') // DSB Pool modal const [showDSBPoolModal, setShowDSBPoolModal] = useState(false) const [newMember, setNewMember] = useState({ user_id: '', user_name: '', user_email: '', role: 'dsb', max_concurrent_reviews: 10, }) // Load data useEffect(() => { loadEscalations() loadStats() loadDSBPool() }, [statusFilter, levelFilter, myReviewsOnly]) async function loadEscalations() { setLoading(true) setError(null) try { const params = new URLSearchParams() if (statusFilter && statusFilter !== 'all') params.append('status', statusFilter) if (levelFilter) params.append('level', levelFilter) if (myReviewsOnly) params.append('my_reviews', 'true') const res = await fetch(`/sdk/v1/ucca/escalations?${params}`, { headers: { 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', 'X-User-ID': localStorage.getItem('bp_user_id') || '', } }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = await res.json() setEscalations(data.escalations || []) } catch (err) { console.error('Failed to load escalations:', err) setError('Fehler beim Laden der Eskalationen') } finally { setLoading(false) } } async function loadStats() { try { const res = await fetch('/sdk/v1/ucca/escalations/stats', { headers: { 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', 'X-User-ID': localStorage.getItem('bp_user_id') || '', } }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = await res.json() setStats(data) } catch (err) { console.error('Failed to load stats:', err) } } async function loadDSBPool() { try { const res = await fetch('/sdk/v1/ucca/dsb-pool', { headers: { 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', 'X-User-ID': localStorage.getItem('bp_user_id') || '', } }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = await res.json() setDsbPool(data.members || []) } catch (err) { console.error('Failed to load DSB pool:', err) } } async function loadEscalationDetail(id: string) { try { const res = await fetch(`/sdk/v1/ucca/escalations/${id}`, { headers: { 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', 'X-User-ID': localStorage.getItem('bp_user_id') || '', } }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = await res.json() setSelectedEscalation(data.escalation) setEscalationHistory(data.history || []) } catch (err) { console.error('Failed to load escalation detail:', err) } } async function startReview(id: string) { try { const res = await fetch(`/sdk/v1/ucca/escalations/${id}/review`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', 'X-User-ID': localStorage.getItem('bp_user_id') || '', } }) if (!res.ok) throw new Error(`HTTP ${res.status}`) loadEscalations() if (selectedEscalation?.id === id) { loadEscalationDetail(id) } } catch (err) { console.error('Failed to start review:', err) alert('Fehler beim Starten der Prüfung') } } async function submitDecision() { if (!selectedEscalation) return try { const res = await fetch(`/sdk/v1/ucca/escalations/${selectedEscalation.id}/decide`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', 'X-User-ID': localStorage.getItem('bp_user_id') || '', }, body: JSON.stringify(decisionForm) }) if (!res.ok) throw new Error(`HTTP ${res.status}`) setShowDecisionModal(false) setDecisionForm({ decision: 'approve', decision_notes: '', conditions: [] }) loadEscalations() loadStats() setSelectedEscalation(null) } catch (err) { console.error('Failed to submit decision:', err) alert('Fehler beim Speichern der Entscheidung') } } async function assignEscalation(escalationId: string, userId: string) { try { const res = await fetch(`/sdk/v1/ucca/escalations/${escalationId}/assign`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', 'X-User-ID': localStorage.getItem('bp_user_id') || '', }, body: JSON.stringify({ assigned_to: userId }) }) if (!res.ok) throw new Error(`HTTP ${res.status}`) loadEscalations() if (selectedEscalation?.id === escalationId) { loadEscalationDetail(escalationId) } } catch (err) { console.error('Failed to assign escalation:', err) alert('Fehler bei der Zuweisung') } } async function addDSBPoolMember() { try { const res = await fetch('/sdk/v1/ucca/dsb-pool', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', 'X-User-ID': localStorage.getItem('bp_user_id') || '', }, body: JSON.stringify(newMember) }) if (!res.ok) throw new Error(`HTTP ${res.status}`) setShowDSBPoolModal(false) setNewMember({ user_id: '', user_name: '', user_email: '', role: 'dsb', max_concurrent_reviews: 10 }) loadDSBPool() } catch (err) { console.error('Failed to add DSB pool member:', err) alert('Fehler beim Hinzufügen') } } function addCondition() { if (newCondition.trim()) { setDecisionForm(prev => ({ ...prev, conditions: [...prev.conditions, newCondition.trim()] })) setNewCondition('') } } function removeCondition(index: number) { setDecisionForm(prev => ({ ...prev, conditions: prev.conditions.filter((_, i) => i !== index) })) } function formatDate(dateStr: string) { return new Date(dateStr).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) } function isOverdue(dueDate?: string) { if (!dueDate) return false return new Date(dueDate) < new Date() } function getTimeRemaining(dueDate?: string) { if (!dueDate) return null const now = new Date() const due = new Date(dueDate) const diff = due.getTime() - now.getTime() if (diff < 0) return 'Überfällig' const hours = Math.floor(diff / (1000 * 60 * 60)) if (hours < 24) return `${hours}h verbleibend` const days = Math.floor(hours / 24) return `${days}d verbleibend` } return (
{/* Header */}

Eskalations-Queue

DSB Review & Freigabe-Workflow für UCCA Assessments

{/* Stats Cards */} {stats && (
{stats.total_pending}
Ausstehend
{stats.total_in_review}
In Prüfung
{stats.total_approved}
Genehmigt
{stats.total_rejected}
Abgelehnt
{stats.overdue_sla}
SLA überschritten
{stats.approaching_sla}
SLA gefährdet
)} {/* Level Distribution */} {stats && stats.by_level && (

Verteilung nach Eskalationsstufe

{Object.entries(LEVEL_CONFIG).map(([level, config]) => (
{level} {stats.by_level[level] || 0}
))}
)} {/* Filters */}
setMyReviewsOnly(e.target.checked)} className="rounded border-gray-300" />
{/* Error */} {error && (
{error}
)} {/* Main Content */}
{/* Escalation List */}
{loading ? (
Laden...
) : escalations.length === 0 ? (
Keine Eskalationen gefunden
) : ( escalations.map((esc) => (
loadEscalationDetail(esc.id)} className={`bg-white rounded-lg border p-4 cursor-pointer hover:border-violet-300 transition-colors ${ selectedEscalation?.id === esc.id ? 'border-violet-500 ring-2 ring-violet-200' : '' } ${isOverdue(esc.due_date) && esc.status !== 'approved' && esc.status !== 'rejected' ? 'border-red-300 bg-red-50' : ''}`} >
{esc.escalation_level} {STATUS_CONFIG[esc.status].label} {esc.due_date && ( {getTimeRemaining(esc.due_date)} )}

{esc.assessment_title || `Assessment ${esc.assessment_id.slice(0, 8)}`}

{esc.escalation_reason}

Erstellt: {formatDate(esc.created_at)} {esc.assessment_risk_score !== undefined && ( Risk: {esc.assessment_risk_score}/100 )} {esc.assessment_domain && ( Domain: {esc.assessment_domain} )}
{esc.status === 'pending' && ( )}
)) )}
{/* Detail Panel */}
{selectedEscalation ? (

Detail

{/* Status & Level */}
{selectedEscalation.escalation_level} - {LEVEL_CONFIG[selectedEscalation.escalation_level].label} {STATUS_CONFIG[selectedEscalation.status].label}
{/* Reason */}
Grund
{selectedEscalation.escalation_reason}
{/* SLA */} {selectedEscalation.due_date && (
SLA Deadline
{formatDate(selectedEscalation.due_date)} {isOverdue(selectedEscalation.due_date) && ' (Überfällig!)'}
)} {/* Assignment */}
Zugewiesen an
{selectedEscalation.assigned_to ? (
{selectedEscalation.assigned_role || 'Unbekannt'}
) : (
)}
{/* Decision */} {selectedEscalation.decision && (
Entscheidung
{selectedEscalation.decision === 'approve' && '✅ Genehmigt'} {selectedEscalation.decision === 'reject' && '❌ Abgelehnt'} {selectedEscalation.decision === 'modify' && '🔄 Änderungen erforderlich'} {selectedEscalation.decision === 'escalate' && '⬆️ Eskaliert'}
{selectedEscalation.decision_notes && (
{selectedEscalation.decision_notes}
)} {selectedEscalation.conditions && selectedEscalation.conditions.length > 0 && (
Auflagen:
    {selectedEscalation.conditions.map((c, i) => (
  • {c}
  • ))}
)}
)} {/* Actions */} {(selectedEscalation.status === 'assigned' || selectedEscalation.status === 'in_review') && (
)} {/* History */} {escalationHistory.length > 0 && (
Verlauf
{escalationHistory.map((h) => (
{h.action}
{h.notes &&
{h.notes}
}
{formatDate(h.created_at)}
))}
)} {/* Link to Assessment */}
) : (
Wählen Sie eine Eskalation aus, um Details zu sehen
)}
{/* Decision Modal */} {showDecisionModal && selectedEscalation && (

Entscheidung für {selectedEscalation.escalation_level}