'use client' /** * Audit Checklist Page - Sprint 3 Feature * * Ermoeglicht Auditoren: * - Audit-Sessions zu erstellen und zu verwalten * - Checklisten-Items durchzuarbeiten * - Sign-offs mit digitaler Signatur (SHA-256) zu erstellen * - Fortschritt und Statistiken zu verfolgen */ import { useState, useEffect, useCallback } from 'react' import Link from 'next/link' import AdminLayout from '@/components/admin/AdminLayout' import { getTerm, Language } from '@/lib/compliance-i18n' // Types based on backend schemas interface AuditSession { id: string name: string description: string | null auditor_name: string auditor_email: string | null status: 'draft' | 'in_progress' | 'completed' | 'archived' regulation_ids: string[] | null total_items: number completed_items: number created_at: string started_at: string | null completed_at: string | null } interface AuditChecklistItem { requirement_id: string regulation_code: string article: string title: string current_result: 'compliant' | 'compliant_notes' | 'non_compliant' | 'not_applicable' | 'pending' notes: string | null is_signed: boolean signed_at: string | null signed_by: string | null evidence_count: number controls_mapped: number } interface AuditStatistics { total: number compliant: number compliant_with_notes: number non_compliant: number not_applicable: number pending: number completion_percentage: number } interface Regulation { id: string code: string name: string requirement_count: number } // Result status configuration const RESULT_STATUS = { pending: { label: 'Ausstehend', labelEn: 'Pending', color: 'bg-slate-100 text-slate-700', icon: '○' }, compliant: { label: 'Konform', labelEn: 'Compliant', color: 'bg-green-100 text-green-700', icon: '✓' }, compliant_notes: { label: 'Konform (Anm.)', labelEn: 'Compliant w/ Notes', color: 'bg-yellow-100 text-yellow-700', icon: '⚠' }, non_compliant: { label: 'Nicht konform', labelEn: 'Non-Compliant', color: 'bg-red-100 text-red-700', icon: '✗' }, not_applicable: { label: 'N/A', labelEn: 'N/A', color: 'bg-slate-50 text-slate-500', icon: '−' }, } const SESSION_STATUS = { draft: { label: 'Entwurf', color: 'bg-slate-100 text-slate-700' }, in_progress: { label: 'In Bearbeitung', color: 'bg-blue-100 text-blue-700' }, completed: { label: 'Abgeschlossen', color: 'bg-green-100 text-green-700' }, archived: { label: 'Archiviert', color: 'bg-slate-50 text-slate-500' }, } export default function AuditChecklistPage() { const [sessions, setSessions] = useState([]) const [selectedSession, setSelectedSession] = useState(null) const [checklistItems, setChecklistItems] = useState([]) const [statistics, setStatistics] = useState(null) const [regulations, setRegulations] = useState([]) const [loading, setLoading] = useState(true) const [loadingChecklist, setLoadingChecklist] = useState(false) const [error, setError] = useState(null) // Filters const [filterStatus, setFilterStatus] = useState('all') const [filterRegulation, setFilterRegulation] = useState('all') const [searchQuery, setSearchQuery] = useState('') // Modal states const [showCreateModal, setShowCreateModal] = useState(false) const [showSignOffModal, setShowSignOffModal] = useState(false) const [selectedItem, setSelectedItem] = useState(null) // Language const [lang] = useState('de') const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000' // Load sessions const loadSessions = useCallback(async () => { try { const res = await fetch(`${BACKEND_URL}/api/v1/compliance/audit/sessions`) if (res.ok) { const data = await res.json() setSessions(data.sessions || []) } else { console.error('Failed to load sessions:', res.status) } } catch (err) { console.error('Failed to load sessions:', err) setError('Verbindung zum Backend fehlgeschlagen') } finally { setLoading(false) } }, [BACKEND_URL]) // Load regulations for filter const loadRegulations = useCallback(async () => { try { const res = await fetch(`${BACKEND_URL}/api/v1/compliance/regulations`) if (res.ok) { const data = await res.json() setRegulations(data.regulations || []) } } catch (err) { console.error('Failed to load regulations:', err) } }, [BACKEND_URL]) // Load checklist for selected session const loadChecklist = useCallback(async (sessionId: string) => { setLoadingChecklist(true) try { const res = await fetch(`${BACKEND_URL}/api/v1/compliance/audit/checklist/${sessionId}`) if (res.ok) { const data = await res.json() setChecklistItems(data.items || []) setStatistics(data.statistics || null) // Update session with latest data if (data.session) { setSelectedSession(data.session) } } else { console.error('Failed to load checklist:', res.status) } } catch (err) { console.error('Failed to load checklist:', err) } finally { setLoadingChecklist(false) } }, [BACKEND_URL]) useEffect(() => { loadSessions() loadRegulations() }, [loadSessions, loadRegulations]) useEffect(() => { if (selectedSession) { loadChecklist(selectedSession.id) } }, [selectedSession, loadChecklist]) // Filter checklist items const filteredItems = checklistItems.filter(item => { if (filterStatus !== 'all' && item.current_result !== filterStatus) return false if (filterRegulation !== 'all' && item.regulation_code !== filterRegulation) return false if (searchQuery) { const query = searchQuery.toLowerCase() return ( item.title.toLowerCase().includes(query) || item.article.toLowerCase().includes(query) || item.regulation_code.toLowerCase().includes(query) ) } return true }) // Handle sign-off const handleSignOff = async ( item: AuditChecklistItem, result: AuditChecklistItem['current_result'], notes: string, sign: boolean ) => { if (!selectedSession) return try { const res = await fetch( `${BACKEND_URL}/api/v1/compliance/audit/checklist/${selectedSession.id}/items/${item.requirement_id}/sign-off`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ result, notes, sign }), } ) if (res.ok) { // Reload checklist to get updated data await loadChecklist(selectedSession.id) setShowSignOffModal(false) setSelectedItem(null) } else { const err = await res.json() alert(`Fehler: ${err.detail || 'Sign-off fehlgeschlagen'}`) } } catch (err) { console.error('Sign-off failed:', err) alert('Netzwerkfehler bei Sign-off') } } // Session list view when no session is selected if (!selectedSession && !loading) { return (
Zurueck zu Compliance
{error && (
{error}
)} {/* Session Cards */}
{sessions.length === 0 ? (

Keine Audit-Sessions

Erstellen Sie eine neue Audit-Session um zu beginnen.

) : ( sessions.map(session => ( setSelectedSession(session)} /> )) )}
{/* Create Session Modal */} {showCreateModal && ( setShowCreateModal(false)} onCreate={async (data) => { try { const res = await fetch(`${BACKEND_URL}/api/v1/compliance/audit/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) if (res.ok) { await loadSessions() setShowCreateModal(false) } else { const err = await res.json() alert(`Fehler: ${err.detail || 'Session konnte nicht erstellt werden'}`) } } catch (err) { console.error('Create session failed:', err) alert('Netzwerkfehler') } }} /> )}
) } // Loading state if (loading) { return (
) } // Checklist view for selected session return ( {/* Header */}
{SESSION_STATUS[selectedSession?.status || 'draft'].label}
{/* Progress Bar */} {statistics && ( )} {/* Filters */}
setSearchQuery(e.target.value)} placeholder="Artikel, Titel..." className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
{/* Checklist Items */}
{loadingChecklist ? (

Checkliste wird geladen...

) : filteredItems.length === 0 ? (

Keine Eintraege gefunden

) : (
{filteredItems.map((item, index) => ( { setSelectedItem(item) setShowSignOffModal(true) }} lang={lang} /> ))}
)}
{/* Sign-off Modal */} {showSignOffModal && selectedItem && ( { setShowSignOffModal(false) setSelectedItem(null) }} onSubmit={(result, notes, sign) => handleSignOff(selectedItem, result, notes, sign)} /> )}
) } // Session Card Component function SessionCard({ session, onClick }: { session: AuditSession; onClick: () => void }) { const completionPercent = session.total_items > 0 ? Math.round((session.completed_items / session.total_items) * 100) : 0 return ( ) } // Progress Bar Component function AuditProgressBar({ statistics, lang }: { statistics: AuditStatistics; lang: Language }) { const segments = [ { key: 'compliant', count: statistics.compliant, color: 'bg-green-500', label: 'Konform' }, { key: 'compliant_with_notes', count: statistics.compliant_with_notes, color: 'bg-yellow-500', label: 'Mit Anm.' }, { key: 'non_compliant', count: statistics.non_compliant, color: 'bg-red-500', label: 'Nicht konform' }, { key: 'not_applicable', count: statistics.not_applicable, color: 'bg-slate-300', label: 'N/A' }, { key: 'pending', count: statistics.pending, color: 'bg-slate-100', label: 'Offen' }, ] return (

Fortschritt

{Math.round(statistics.completion_percentage)}%
{/* Stacked Progress Bar */}
{segments.map(seg => ( seg.count > 0 && (
) ))}
{/* Legend */}
{segments.map(seg => (
{seg.label}: {seg.count}
))}
) } // Checklist Item Row Component function ChecklistItemRow({ item, index, onSignOff, lang, }: { item: AuditChecklistItem index: number onSignOff: () => void lang: Language }) { const status = RESULT_STATUS[item.current_result] return (
{/* Status Icon */}
{status.icon}
{/* Content */}
{index}. {item.regulation_code} {item.article} {item.is_signed && ( Signiert )}

{item.title}

{item.notes && (

"{item.notes}"

)}
{item.controls_mapped} Controls {item.evidence_count} Nachweise {item.signed_at && ( Signiert: {new Date(item.signed_at).toLocaleDateString('de-DE')} )}
{/* Actions */}
{lang === 'de' ? status.label : status.labelEn}
) } // Create Session Modal function CreateSessionModal({ regulations, onClose, onCreate, }: { regulations: Regulation[] onClose: () => void onCreate: (data: { name: string; description?: string; auditor_name: string; auditor_email?: string; regulation_codes?: string[] }) => void }) { const [name, setName] = useState('') const [description, setDescription] = useState('') const [auditorName, setAuditorName] = useState('') const [auditorEmail, setAuditorEmail] = useState('') const [selectedRegs, setSelectedRegs] = useState([]) const [submitting, setSubmitting] = useState(false) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!name || !auditorName) return setSubmitting(true) await onCreate({ name, description: description || undefined, auditor_name: auditorName, auditor_email: auditorEmail || undefined, regulation_codes: selectedRegs.length > 0 ? selectedRegs : undefined, }) setSubmitting(false) } return (

Neue Audit-Session

setName(e.target.value)} placeholder="z.B. Q1 2026 Compliance Audit" className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500" required />