This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/app/(admin)/dsgvo/escalations/page.tsx
BreakPilot Dev 660295e218 fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit
restores all missing components:

- Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education,
  infrastructure, communication, development, onboarding, rbac
- SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen,
  vendor-compliance, tom-generator, dsr, and more
- Developer portal (25 pages): API docs, SDK guides, frameworks
- All components, lib files, hooks, and types
- Updated package.json with all dependencies

The issue was caused by incomplete initial repository state - the full
admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2
but was never fully synced to the main admin-v2 directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 23:40:15 -08:00

865 lines
34 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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<string, number>
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<Escalation[]>([])
const [stats, setStats] = useState<EscalationStats | null>(null)
const [dsbPool, setDsbPool] = useState<DSBPoolMember[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Filters
const [statusFilter, setStatusFilter] = useState<string>('pending')
const [levelFilter, setLevelFilter] = useState<string>('')
const [myReviewsOnly, setMyReviewsOnly] = useState(false)
// Selected escalation for detail view
const [selectedEscalation, setSelectedEscalation] = useState<Escalation | null>(null)
const [escalationHistory, setEscalationHistory] = useState<EscalationHistory[]>([])
// 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 (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Eskalations-Queue</h1>
<p className="text-gray-600 mt-1">DSB Review & Freigabe-Workflow für UCCA Assessments</p>
</div>
<button
onClick={() => setShowDSBPoolModal(true)}
className="px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors"
>
DSB-Pool verwalten
</button>
</div>
<PagePurpose
title="Eskalations-Queue"
purpose="Verwaltung von Eskalationen aus dem Advisory Board. DSB und Team-Leads prüfen risikoreiche Use-Cases (E1-E3) und erteilen Freigaben oder Ablehnungen mit Auflagen."
audience={['DSB', 'Team-Leads', 'Legal']}
gdprArticles={['Art. 5', 'Art. 22', 'Art. 35', 'Art. 36']}
/>
{/* Stats Cards */}
{stats && (
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
<div className="bg-white rounded-lg border p-4">
<div className="text-2xl font-bold text-gray-900">{stats.total_pending}</div>
<div className="text-sm text-gray-600">Ausstehend</div>
</div>
<div className="bg-white rounded-lg border p-4">
<div className="text-2xl font-bold text-yellow-600">{stats.total_in_review}</div>
<div className="text-sm text-gray-600">In Prüfung</div>
</div>
<div className="bg-white rounded-lg border p-4">
<div className="text-2xl font-bold text-green-600">{stats.total_approved}</div>
<div className="text-sm text-gray-600">Genehmigt</div>
</div>
<div className="bg-white rounded-lg border p-4">
<div className="text-2xl font-bold text-red-600">{stats.total_rejected}</div>
<div className="text-sm text-gray-600">Abgelehnt</div>
</div>
<div className="bg-white rounded-lg border p-4">
<div className="text-2xl font-bold text-red-600">{stats.overdue_sla}</div>
<div className="text-sm text-gray-600">SLA überschritten</div>
</div>
<div className="bg-white rounded-lg border p-4">
<div className="text-2xl font-bold text-orange-600">{stats.approaching_sla}</div>
<div className="text-sm text-gray-600">SLA gefährdet</div>
</div>
</div>
)}
{/* Level Distribution */}
{stats && stats.by_level && (
<div className="bg-white rounded-lg border p-4">
<h3 className="font-medium text-gray-900 mb-3">Verteilung nach Eskalationsstufe</h3>
<div className="flex gap-4">
{Object.entries(LEVEL_CONFIG).map(([level, config]) => (
<div key={level} className="flex items-center gap-2">
<span className={`px-2 py-1 rounded text-xs font-medium ${config.color}`}>
{level}
</span>
<span className="text-gray-600">{stats.by_level[level] || 0}</span>
</div>
))}
</div>
</div>
)}
{/* Filters */}
<div className="bg-white rounded-lg border p-4">
<div className="flex flex-wrap gap-4 items-center">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="border rounded-lg px-3 py-2 text-sm"
>
<option value="all">Alle</option>
<option value="pending">Ausstehend</option>
<option value="assigned">Zugewiesen</option>
<option value="in_review">In Prüfung</option>
<option value="approved">Genehmigt</option>
<option value="rejected">Abgelehnt</option>
<option value="returned">Zurückgegeben</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Level</label>
<select
value={levelFilter}
onChange={(e) => setLevelFilter(e.target.value)}
className="border rounded-lg px-3 py-2 text-sm"
>
<option value="">Alle</option>
<option value="E1">E1 - Team-Lead</option>
<option value="E2">E2 - DSB</option>
<option value="E3">E3 - DSB + Legal</option>
</select>
</div>
<div className="flex items-center gap-2 pt-6">
<input
type="checkbox"
id="myReviews"
checked={myReviewsOnly}
onChange={(e) => setMyReviewsOnly(e.target.checked)}
className="rounded border-gray-300"
/>
<label htmlFor="myReviews" className="text-sm text-gray-700">Nur meine Zuweisungen</label>
</div>
<div className="ml-auto pt-6">
<button
onClick={loadEscalations}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
>
Aktualisieren
</button>
</div>
</div>
</div>
{/* Error */}
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
{error}
</div>
)}
{/* Main Content */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Escalation List */}
<div className="lg:col-span-2 space-y-4">
{loading ? (
<div className="bg-white rounded-lg border p-8 text-center text-gray-500">
Laden...
</div>
) : escalations.length === 0 ? (
<div className="bg-white rounded-lg border p-8 text-center text-gray-500">
Keine Eskalationen gefunden
</div>
) : (
escalations.map((esc) => (
<div
key={esc.id}
onClick={() => 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' : ''}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className={`px-2 py-1 rounded text-xs font-medium ${LEVEL_CONFIG[esc.escalation_level].color}`}>
{esc.escalation_level}
</span>
<span className={`px-2 py-1 rounded text-xs font-medium ${STATUS_CONFIG[esc.status].color}`}>
{STATUS_CONFIG[esc.status].label}
</span>
{esc.due_date && (
<span className={`text-xs ${isOverdue(esc.due_date) ? 'text-red-600 font-medium' : 'text-gray-500'}`}>
{getTimeRemaining(esc.due_date)}
</span>
)}
</div>
<h3 className="font-medium text-gray-900">
{esc.assessment_title || `Assessment ${esc.assessment_id.slice(0, 8)}`}
</h3>
<p className="text-sm text-gray-600 mt-1 line-clamp-2">{esc.escalation_reason}</p>
<div className="flex items-center gap-4 mt-2 text-xs text-gray-500">
<span>Erstellt: {formatDate(esc.created_at)}</span>
{esc.assessment_risk_score !== undefined && (
<span>Risk: {esc.assessment_risk_score}/100</span>
)}
{esc.assessment_domain && (
<span>Domain: {esc.assessment_domain}</span>
)}
</div>
</div>
{esc.status === 'pending' && (
<button
onClick={(e) => {
e.stopPropagation()
startReview(esc.id)
}}
className="px-3 py-1 text-sm bg-violet-100 text-violet-700 rounded hover:bg-violet-200 transition-colors"
>
Review starten
</button>
)}
</div>
</div>
))
)}
</div>
{/* Detail Panel */}
<div className="lg:col-span-1">
{selectedEscalation ? (
<div className="bg-white rounded-lg border p-4 sticky top-6 space-y-4">
<h3 className="font-semibold text-gray-900">Detail</h3>
{/* Status & Level */}
<div className="flex gap-2">
<span className={`px-2 py-1 rounded text-xs font-medium ${LEVEL_CONFIG[selectedEscalation.escalation_level].color}`}>
{selectedEscalation.escalation_level} - {LEVEL_CONFIG[selectedEscalation.escalation_level].label}
</span>
<span className={`px-2 py-1 rounded text-xs font-medium ${STATUS_CONFIG[selectedEscalation.status].color}`}>
{STATUS_CONFIG[selectedEscalation.status].label}
</span>
</div>
{/* Reason */}
<div>
<div className="text-sm font-medium text-gray-700">Grund</div>
<div className="text-sm text-gray-600 mt-1">{selectedEscalation.escalation_reason}</div>
</div>
{/* SLA */}
{selectedEscalation.due_date && (
<div>
<div className="text-sm font-medium text-gray-700">SLA Deadline</div>
<div className={`text-sm mt-1 ${isOverdue(selectedEscalation.due_date) ? 'text-red-600 font-medium' : 'text-gray-600'}`}>
{formatDate(selectedEscalation.due_date)}
{isOverdue(selectedEscalation.due_date) && ' (Überfällig!)'}
</div>
</div>
)}
{/* Assignment */}
<div>
<div className="text-sm font-medium text-gray-700">Zugewiesen an</div>
{selectedEscalation.assigned_to ? (
<div className="text-sm text-gray-600 mt-1">
{selectedEscalation.assigned_role || 'Unbekannt'}
</div>
) : (
<div className="mt-2">
<select
onChange={(e) => {
if (e.target.value) {
assignEscalation(selectedEscalation.id, e.target.value)
}
}}
className="w-full border rounded px-2 py-1 text-sm"
defaultValue=""
>
<option value="">Reviewer auswählen...</option>
{dsbPool.filter(m => m.is_active).map(member => (
<option key={member.user_id} value={member.user_id}>
{member.user_name} ({member.role}) - {member.current_reviews}/{member.max_concurrent_reviews}
</option>
))}
</select>
</div>
)}
</div>
{/* Decision */}
{selectedEscalation.decision && (
<div>
<div className="text-sm font-medium text-gray-700">Entscheidung</div>
<div className="text-sm text-gray-600 mt-1">
{selectedEscalation.decision === 'approve' && '✅ Genehmigt'}
{selectedEscalation.decision === 'reject' && '❌ Abgelehnt'}
{selectedEscalation.decision === 'modify' && '🔄 Änderungen erforderlich'}
{selectedEscalation.decision === 'escalate' && '⬆️ Eskaliert'}
</div>
{selectedEscalation.decision_notes && (
<div className="text-sm text-gray-500 mt-1">{selectedEscalation.decision_notes}</div>
)}
{selectedEscalation.conditions && selectedEscalation.conditions.length > 0 && (
<div className="mt-2">
<div className="text-xs font-medium text-gray-700">Auflagen:</div>
<ul className="list-disc list-inside text-xs text-gray-600">
{selectedEscalation.conditions.map((c, i) => (
<li key={i}>{c}</li>
))}
</ul>
</div>
)}
</div>
)}
{/* Actions */}
{(selectedEscalation.status === 'assigned' || selectedEscalation.status === 'in_review') && (
<div className="pt-4 border-t">
<button
onClick={() => setShowDecisionModal(true)}
className="w-full px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors"
>
Entscheidung treffen
</button>
</div>
)}
{/* History */}
{escalationHistory.length > 0 && (
<div className="pt-4 border-t">
<div className="text-sm font-medium text-gray-700 mb-2">Verlauf</div>
<div className="space-y-2 max-h-48 overflow-y-auto">
{escalationHistory.map((h) => (
<div key={h.id} className="text-xs border-l-2 border-gray-200 pl-2">
<div className="text-gray-900">{h.action}</div>
{h.notes && <div className="text-gray-500">{h.notes}</div>}
<div className="text-gray-400">{formatDate(h.created_at)}</div>
</div>
))}
</div>
</div>
)}
{/* Link to Assessment */}
<div className="pt-4 border-t">
<a
href={`/dsgvo/advisory-board?assessment=${selectedEscalation.assessment_id}`}
className="text-sm text-violet-600 hover:text-violet-800"
>
Assessment anzeigen
</a>
</div>
</div>
) : (
<div className="bg-gray-50 rounded-lg border border-dashed p-8 text-center text-gray-500">
Wählen Sie eine Eskalation aus, um Details zu sehen
</div>
)}
</div>
</div>
{/* Decision Modal */}
{showDecisionModal && selectedEscalation && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-lg mx-4">
<h3 className="text-lg font-semibold mb-4">Entscheidung für {selectedEscalation.escalation_level}</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Entscheidung</label>
<select
value={decisionForm.decision}
onChange={(e) => setDecisionForm(prev => ({ ...prev, decision: e.target.value as any }))}
className="w-full border rounded-lg px-3 py-2"
>
<option value="approve"> Genehmigen</option>
<option value="reject"> Ablehnen</option>
<option value="modify">🔄 Änderungen erforderlich</option>
<option value="escalate"> Weiter eskalieren</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Begründung</label>
<textarea
value={decisionForm.decision_notes}
onChange={(e) => setDecisionForm(prev => ({ ...prev, decision_notes: e.target.value }))}
rows={3}
className="w-full border rounded-lg px-3 py-2"
placeholder="Begründung für die Entscheidung..."
/>
</div>
{(decisionForm.decision === 'approve' || decisionForm.decision === 'modify') && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Auflagen (optional)</label>
<div className="flex gap-2 mb-2">
<input
type="text"
value={newCondition}
onChange={(e) => setNewCondition(e.target.value)}
placeholder="Neue Auflage eingeben..."
className="flex-1 border rounded-lg px-3 py-2 text-sm"
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), addCondition())}
/>
<button
onClick={addCondition}
className="px-3 py-2 bg-gray-100 rounded-lg hover:bg-gray-200"
>
+
</button>
</div>
{decisionForm.conditions.length > 0 && (
<ul className="space-y-1">
{decisionForm.conditions.map((c, i) => (
<li key={i} className="flex items-center justify-between bg-gray-50 px-2 py-1 rounded text-sm">
<span>{c}</span>
<button
onClick={() => removeCondition(i)}
className="text-red-500 hover:text-red-700"
>
×
</button>
</li>
))}
</ul>
)}
</div>
)}
</div>
<div className="flex justify-end gap-2 mt-6">
<button
onClick={() => setShowDecisionModal(false)}
className="px-4 py-2 text-gray-600 hover:text-gray-800"
>
Abbrechen
</button>
<button
onClick={submitDecision}
className="px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700"
>
Entscheidung speichern
</button>
</div>
</div>
</div>
)}
{/* DSB Pool Modal */}
{showDSBPoolModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-2xl mx-4">
<h3 className="text-lg font-semibold mb-4">DSB-Pool verwalten</h3>
{/* Current Members */}
<div className="mb-6">
<h4 className="text-sm font-medium text-gray-700 mb-2">Aktuelle Mitglieder</h4>
{dsbPool.length === 0 ? (
<p className="text-sm text-gray-500">Keine Mitglieder im Pool</p>
) : (
<div className="border rounded-lg divide-y">
{dsbPool.map(member => (
<div key={member.id} className="p-3 flex items-center justify-between">
<div>
<div className="font-medium">{member.user_name}</div>
<div className="text-sm text-gray-500">{member.user_email}</div>
</div>
<div className="flex items-center gap-4">
<span className={`px-2 py-1 rounded text-xs ${
member.role === 'dsb' ? 'bg-violet-100 text-violet-800' :
member.role === 'team_lead' ? 'bg-blue-100 text-blue-800' :
'bg-gray-100 text-gray-800'
}`}>
{member.role}
</span>
<span className="text-sm text-gray-600">
{member.current_reviews}/{member.max_concurrent_reviews} Reviews
</span>
<span className={`w-2 h-2 rounded-full ${member.is_active ? 'bg-green-500' : 'bg-gray-300'}`} />
</div>
</div>
))}
</div>
)}
</div>
{/* Add New Member */}
<div className="border-t pt-4">
<h4 className="text-sm font-medium text-gray-700 mb-2">Neues Mitglied hinzufügen</h4>
<div className="grid grid-cols-2 gap-4">
<input
type="text"
value={newMember.user_name}
onChange={(e) => setNewMember(prev => ({ ...prev, user_name: e.target.value }))}
placeholder="Name"
className="border rounded-lg px-3 py-2"
/>
<input
type="email"
value={newMember.user_email}
onChange={(e) => setNewMember(prev => ({ ...prev, user_email: e.target.value }))}
placeholder="E-Mail"
className="border rounded-lg px-3 py-2"
/>
<select
value={newMember.role}
onChange={(e) => setNewMember(prev => ({ ...prev, role: e.target.value }))}
className="border rounded-lg px-3 py-2"
>
<option value="dsb">DSB</option>
<option value="deputy_dsb">Stellv. DSB</option>
<option value="team_lead">Team-Lead</option>
<option value="legal">Legal</option>
</select>
<input
type="number"
value={newMember.max_concurrent_reviews}
onChange={(e) => setNewMember(prev => ({ ...prev, max_concurrent_reviews: parseInt(e.target.value) || 10 }))}
placeholder="Max Reviews"
className="border rounded-lg px-3 py-2"
/>
</div>
<button
onClick={addDSBPoolMember}
disabled={!newMember.user_name || !newMember.user_email}
className="mt-4 px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Hinzufügen
</button>
</div>
<div className="flex justify-end mt-6">
<button
onClick={() => setShowDSBPoolModal(false)}
className="px-4 py-2 text-gray-600 hover:text-gray-800"
>
Schließen
</button>
</div>
</div>
</div>
)}
</div>
)
}