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)/compliance/audit-checklist/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

776 lines
33 KiB
TypeScript

'use client'
/**
* Audit Checklist Page - 476+ Requirements Interactive Checklist
*
* Features:
* - Session management (create, start, complete)
* - Paginated checklist with search & filters
* - Sign-off workflow with digital signatures
* - Progress tracking with statistics
*/
import { useState, useEffect } from 'react'
import { PagePurpose } from '@/components/common/PagePurpose'
// Types
interface AuditSession {
id: string
name: string
auditor_name: string
auditor_email?: string
auditor_organization?: string
status: 'draft' | 'in_progress' | 'completed' | 'archived'
regulation_ids?: string[]
total_items: number
completed_items: number
compliant_count: number
non_compliant_count: number
completion_percentage: number
created_at: string
started_at?: string
completed_at?: string
}
interface ChecklistItem {
requirement_id: string
regulation_code: string
article: string
paragraph?: string
title: string
description?: string
current_result: string
notes?: string
is_signed: boolean
signed_at?: string
signed_by?: string
evidence_count: number
controls_mapped: number
implementation_status: string
priority: number
}
interface AuditStatistics {
total: number
compliant: number
compliant_with_notes: number
non_compliant: number
not_applicable: number
pending: number
completion_percentage: number
}
// Haupt-/Nebenabweichungen aus ISMS
interface FindingsData {
major_count: number // Hauptabweichungen (blockiert Zertifizierung)
minor_count: number // Nebenabweichungen (erfordert CAPA)
ofi_count: number // Verbesserungspotenziale
total: number
open_majors: number // Offene Hauptabweichungen
open_minors: number // Offene Nebenabweichungen
}
const RESULT_COLORS: Record<string, { bg: string; text: string; label: string }> = {
compliant: { bg: 'bg-green-100', text: 'text-green-700', label: 'Konform' },
compliant_notes: { bg: 'bg-green-50', text: 'text-green-600', label: 'Konform (mit Anm.)' },
non_compliant: { bg: 'bg-red-100', text: 'text-red-700', label: 'Nicht konform' },
not_applicable: { bg: 'bg-slate-100', text: 'text-slate-600', label: 'N/A' },
pending: { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'Ausstehend' },
}
export default function AuditChecklistPage() {
const [sessions, setSessions] = useState<AuditSession[]>([])
const [selectedSession, setSelectedSession] = useState<AuditSession | null>(null)
const [checklist, setChecklist] = useState<ChecklistItem[]>([])
const [statistics, setStatistics] = useState<AuditStatistics | null>(null)
const [loading, setLoading] = useState(true)
const [checklistLoading, setChecklistLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [findings, setFindings] = useState<FindingsData | null>(null)
// Filters
const [search, setSearch] = useState('')
const [statusFilter, setStatusFilter] = useState<string>('')
const [regulationFilter, setRegulationFilter] = useState<string>('')
const [page, setPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
// Modal states
const [showCreateModal, setShowCreateModal] = useState(false)
const [showSignOffModal, setShowSignOffModal] = useState(false)
const [selectedItem, setSelectedItem] = useState<ChecklistItem | null>(null)
// New session form
const [newSession, setNewSession] = useState({
name: '',
auditor_name: '',
auditor_email: '',
auditor_organization: '',
regulation_codes: [] as string[],
})
useEffect(() => {
loadSessions()
loadFindings()
}, [])
const loadFindings = async () => {
try {
const res = await fetch('/api/admin/compliance/isms/findings/summary')
if (res.ok) {
const data = await res.json()
setFindings(data)
}
} catch (err) {
console.error('Failed to load findings:', err)
}
}
useEffect(() => {
if (selectedSession) {
loadChecklist()
}
}, [selectedSession, page, statusFilter, regulationFilter, search])
const loadSessions = async () => {
setLoading(true)
try {
const res = await fetch('/api/admin/audit/sessions')
if (res.ok) {
const data = await res.json()
setSessions(data)
}
} catch (err) {
console.error('Failed to load sessions:', err)
setError('Sessions konnten nicht geladen werden')
} finally {
setLoading(false)
}
}
const loadChecklist = async () => {
if (!selectedSession) return
setChecklistLoading(true)
try {
const params = new URLSearchParams({
page: page.toString(),
page_size: '50',
})
if (statusFilter) params.set('status_filter', statusFilter)
if (regulationFilter) params.set('regulation_filter', regulationFilter)
if (search) params.set('search', search)
const res = await fetch(`/api/admin/compliance/audit/checklist/${selectedSession.id}?${params}`)
if (res.ok) {
const data = await res.json()
setChecklist(data.items || [])
setStatistics(data.statistics)
setTotalPages(data.pagination?.total_pages || 1)
}
} catch (err) {
console.error('Failed to load checklist:', err)
} finally {
setChecklistLoading(false)
}
}
const createSession = async () => {
try {
const res = await fetch('/api/admin/audit/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newSession),
})
if (res.ok) {
const session = await res.json()
setSessions([session, ...sessions])
setSelectedSession(session)
setShowCreateModal(false)
setNewSession({
name: '',
auditor_name: '',
auditor_email: '',
auditor_organization: '',
regulation_codes: [],
})
}
} catch (err) {
console.error('Failed to create session:', err)
}
}
const startSession = async (sessionId: string) => {
try {
const res = await fetch(`/api/admin/audit/sessions/${sessionId}/start`, {
method: 'PUT',
})
if (res.ok) {
loadSessions()
if (selectedSession?.id === sessionId) {
setSelectedSession({ ...selectedSession, status: 'in_progress' })
}
}
} catch (err) {
console.error('Failed to start session:', err)
}
}
const completeSession = async (sessionId: string) => {
try {
const res = await fetch(`/api/admin/audit/sessions/${sessionId}/complete`, {
method: 'PUT',
})
if (res.ok) {
loadSessions()
if (selectedSession?.id === sessionId) {
setSelectedSession({ ...selectedSession, status: 'completed' })
}
}
} catch (err) {
console.error('Failed to complete session:', err)
}
}
const signOffItem = async (result: string, notes: string, sign: boolean) => {
if (!selectedSession || !selectedItem) return
try {
const res = await fetch(
`/api/admin/compliance/audit/checklist/${selectedSession.id}/items/${selectedItem.requirement_id}`,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ result, notes, sign }),
}
)
if (res.ok) {
loadChecklist()
loadSessions()
setShowSignOffModal(false)
setSelectedItem(null)
}
} catch (err) {
console.error('Failed to sign off:', err)
}
}
const downloadPdf = async (sessionId: string) => {
window.open(`/api/admin/audit/sessions/${sessionId}/pdf`, '_blank')
}
return (
<div className="space-y-6">
<PagePurpose
title="Audit Checkliste"
purpose="Interaktive Checkliste mit 476+ Compliance-Anforderungen aus DSGVO, AI Act, CRA und BSI TR-03161. Erstellen Sie Audit-Sessions, bewerten Sie Anforderungen und generieren Sie Audit-Reports mit digitalen Signaturen."
audience={['Auditor', 'DSB', 'Compliance Officer']}
gdprArticles={['Art. 5 (Rechenschaftspflicht)', 'Art. 30 (Verzeichnis)', 'Art. 32 (Sicherheit)']}
architecture={{
services: ['Python Backend', 'PostgreSQL'],
databases: ['compliance_audit_sessions', 'compliance_audit_signoffs', 'compliance_requirements'],
}}
relatedPages={[
{ name: 'Compliance Hub', href: '/compliance/hub', description: 'Uebersicht & Dashboard' },
{ name: 'Audit Report', href: '/compliance/audit-report', description: 'PDF-Reports generieren' },
{ name: 'Controls', href: '/compliance/controls', description: 'Technische Massnahmen' },
]}
/>
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-700">{error}</p>
</div>
)}
{/* Haupt-/Nebenabweichungen Uebersicht */}
{findings && (
<div className="bg-white rounded-xl shadow-sm border p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-slate-900">Audit Findings (ISMS)</h2>
<span className={`px-3 py-1 text-sm rounded-full ${
findings.open_majors > 0
? 'bg-red-100 text-red-700'
: 'bg-green-100 text-green-700'
}`}>
{findings.open_majors > 0 ? 'Zertifizierung blockiert' : 'Zertifizierungsfaehig'}
</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<div className="text-center p-4 bg-red-50 rounded-lg border border-red-200">
<p className="text-3xl font-bold text-red-700">{findings.major_count}</p>
<p className="text-sm text-red-600 font-medium">Hauptabweichungen</p>
<p className="text-xs text-red-500 mt-1">(MAJOR)</p>
{findings.open_majors > 0 && (
<p className="text-xs text-red-700 mt-2 font-medium">
{findings.open_majors} offen
</p>
)}
</div>
<div className="text-center p-4 bg-orange-50 rounded-lg border border-orange-200">
<p className="text-3xl font-bold text-orange-700">{findings.minor_count}</p>
<p className="text-sm text-orange-600 font-medium">Nebenabweichungen</p>
<p className="text-xs text-orange-500 mt-1">(MINOR)</p>
{findings.open_minors > 0 && (
<p className="text-xs text-orange-700 mt-2 font-medium">
{findings.open_minors} offen
</p>
)}
</div>
<div className="text-center p-4 bg-blue-50 rounded-lg border border-blue-200">
<p className="text-3xl font-bold text-blue-700">{findings.ofi_count}</p>
<p className="text-sm text-blue-600 font-medium">Verbesserungen</p>
<p className="text-xs text-blue-500 mt-1">(OFI)</p>
</div>
<div className="text-center p-4 bg-slate-50 rounded-lg border border-slate-200">
<p className="text-3xl font-bold text-slate-700">{findings.total}</p>
<p className="text-sm text-slate-600 font-medium">Gesamt Findings</p>
</div>
<div className="text-center p-4 bg-purple-50 rounded-lg border border-purple-200">
<div className="flex flex-col items-center">
<svg className={`w-8 h-8 ${findings.open_majors === 0 ? 'text-green-500' : 'text-red-500'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
{findings.open_majors === 0 ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
)}
</svg>
<p className="text-sm text-purple-600 font-medium mt-2">Zertifizierung</p>
<p className={`text-xs mt-1 font-medium ${findings.open_majors === 0 ? 'text-green-600' : 'text-red-600'}`}>
{findings.open_majors === 0 ? 'Moeglich' : 'Blockiert'}
</p>
</div>
</div>
</div>
<div className="mt-4 p-3 bg-slate-50 rounded-lg">
<p className="text-xs text-slate-600">
<strong>Hauptabweichung (MAJOR):</strong> Signifikante Abweichung von Anforderungen - blockiert Zertifizierung bis zur Behebung.{' '}
<strong>Nebenabweichung (MINOR):</strong> Kleinere Abweichung - erfordert CAPA (Corrective Action) innerhalb 90 Tagen.
</p>
</div>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sessions Sidebar */}
<div className="lg:col-span-1 space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-slate-900">Audit Sessions</h2>
<button
onClick={() => setShowCreateModal(true)}
className="p-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
</button>
</div>
{loading ? (
<div className="flex justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
) : sessions.length === 0 ? (
<div className="text-center py-8 text-slate-500">
<p>Keine Sessions vorhanden</p>
<button
onClick={() => setShowCreateModal(true)}
className="mt-2 text-purple-600 hover:text-purple-700"
>
Erste Session erstellen
</button>
</div>
) : (
<div className="space-y-2">
{sessions.map((session) => (
<div
key={session.id}
onClick={() => setSelectedSession(session)}
className={`p-4 rounded-lg border cursor-pointer transition-colors ${
selectedSession?.id === session.id
? 'border-purple-500 bg-purple-50'
: 'border-slate-200 hover:border-slate-300'
}`}
>
<div className="flex items-center justify-between mb-2">
<span className={`px-2 py-0.5 text-xs rounded-full ${
session.status === 'completed' ? 'bg-green-100 text-green-700' :
session.status === 'in_progress' ? 'bg-blue-100 text-blue-700' :
session.status === 'archived' ? 'bg-slate-100 text-slate-700' :
'bg-yellow-100 text-yellow-700'
}`}>
{session.status === 'completed' ? 'Abgeschlossen' :
session.status === 'in_progress' ? 'In Bearbeitung' :
session.status === 'archived' ? 'Archiviert' : 'Entwurf'}
</span>
<span className="text-xs text-slate-500">{session.completion_percentage.toFixed(0)}%</span>
</div>
<h3 className="font-medium text-slate-900 truncate">{session.name}</h3>
<p className="text-sm text-slate-500">{session.auditor_name}</p>
<div className="mt-2 h-1.5 bg-slate-200 rounded-full overflow-hidden">
<div
className="h-full bg-purple-500"
style={{ width: `${session.completion_percentage}%` }}
/>
</div>
</div>
))}
</div>
)}
</div>
{/* Checklist Content */}
<div className="lg:col-span-3 space-y-4">
{!selectedSession ? (
<div className="bg-white rounded-xl shadow-sm border p-12 text-center">
<svg className="w-16 h-16 mx-auto text-slate-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<h3 className="text-lg font-medium text-slate-900">Waehlen Sie eine Session</h3>
<p className="text-slate-500 mt-2">Waehlen Sie eine Audit-Session aus der Liste oder erstellen Sie eine neue.</p>
</div>
) : (
<>
{/* Session Header */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-xl font-semibold text-slate-900">{selectedSession.name}</h2>
<p className="text-slate-500">{selectedSession.auditor_name} {selectedSession.auditor_organization && `- ${selectedSession.auditor_organization}`}</p>
</div>
<div className="flex gap-2">
{selectedSession.status === 'draft' && (
<button
onClick={() => startSession(selectedSession.id)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Starten
</button>
)}
{selectedSession.status === 'in_progress' && (
<button
onClick={() => completeSession(selectedSession.id)}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"
>
Abschliessen
</button>
)}
{selectedSession.status === 'completed' && (
<button
onClick={() => downloadPdf(selectedSession.id)}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
PDF Export
</button>
)}
</div>
</div>
{/* Statistics */}
{statistics && (
<div className="grid grid-cols-6 gap-4">
<div className="text-center p-3 bg-slate-50 rounded-lg">
<p className="text-2xl font-bold text-slate-900">{statistics.total}</p>
<p className="text-xs text-slate-500">Gesamt</p>
</div>
<div className="text-center p-3 bg-green-50 rounded-lg">
<p className="text-2xl font-bold text-green-700">{statistics.compliant + statistics.compliant_with_notes}</p>
<p className="text-xs text-green-600">Konform</p>
</div>
<div className="text-center p-3 bg-red-50 rounded-lg">
<p className="text-2xl font-bold text-red-700">{statistics.non_compliant}</p>
<p className="text-xs text-red-600">Nicht konform</p>
</div>
<div className="text-center p-3 bg-slate-50 rounded-lg">
<p className="text-2xl font-bold text-slate-700">{statistics.not_applicable}</p>
<p className="text-xs text-slate-500">N/A</p>
</div>
<div className="text-center p-3 bg-yellow-50 rounded-lg">
<p className="text-2xl font-bold text-yellow-700">{statistics.pending}</p>
<p className="text-xs text-yellow-600">Ausstehend</p>
</div>
<div className="text-center p-3 bg-purple-50 rounded-lg">
<p className="text-2xl font-bold text-purple-700">{statistics.completion_percentage.toFixed(0)}%</p>
<p className="text-xs text-purple-600">Fortschritt</p>
</div>
</div>
)}
</div>
{/* Filters */}
<div className="flex gap-4">
<input
type="text"
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1) }}
placeholder="Suche..."
className="flex-1 px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1) }}
className="px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Alle Status</option>
<option value="pending">Ausstehend</option>
<option value="compliant">Konform</option>
<option value="non_compliant">Nicht konform</option>
<option value="not_applicable">N/A</option>
</select>
</div>
{/* Checklist Table */}
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
{checklistLoading ? (
<div className="flex justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
) : checklist.length === 0 ? (
<div className="text-center py-12 text-slate-500">
Keine Eintraege gefunden
</div>
) : (
<table className="w-full">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Regulation</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Artikel</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Titel</th>
<th className="px-4 py-3 text-center text-xs font-medium text-slate-500 uppercase">Controls</th>
<th className="px-4 py-3 text-center text-xs font-medium text-slate-500 uppercase">Status</th>
<th className="px-4 py-3 text-center text-xs font-medium text-slate-500 uppercase">Aktion</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
{checklist.map((item) => {
const resultConfig = RESULT_COLORS[item.current_result] || RESULT_COLORS.pending
return (
<tr key={item.requirement_id} className="hover:bg-slate-50">
<td className="px-4 py-3">
<span className="font-mono text-sm text-purple-600">{item.regulation_code}</span>
</td>
<td className="px-4 py-3">
<span className="font-medium">{item.article}</span>
{item.paragraph && <span className="text-slate-500 text-sm"> {item.paragraph}</span>}
</td>
<td className="px-4 py-3">
<p className="text-sm text-slate-900 line-clamp-2">{item.title}</p>
</td>
<td className="px-4 py-3 text-center">
<span className={`px-2 py-1 text-xs rounded-full ${
item.controls_mapped > 0 ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500'
}`}>
{item.controls_mapped}
</span>
</td>
<td className="px-4 py-3 text-center">
<span className={`px-2 py-1 text-xs rounded-full ${resultConfig.bg} ${resultConfig.text}`}>
{resultConfig.label}
</span>
{item.is_signed && (
<svg className="w-4 h-4 text-green-600 inline-block ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)}
</td>
<td className="px-4 py-3 text-center">
<button
onClick={() => { setSelectedItem(item); setShowSignOffModal(true) }}
className="px-3 py-1 text-sm bg-purple-100 text-purple-700 rounded hover:bg-purple-200"
disabled={selectedSession.status !== 'in_progress' && selectedSession.status !== 'draft'}
>
Bewerten
</button>
</td>
</tr>
)
})}
</tbody>
</table>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="px-4 py-3 border-t flex items-center justify-between">
<button
onClick={() => setPage(Math.max(1, page - 1))}
disabled={page === 1}
className="px-3 py-1 border rounded disabled:opacity-50"
>
Zurueck
</button>
<span className="text-sm text-slate-500">
Seite {page} von {totalPages}
</span>
<button
onClick={() => setPage(Math.min(totalPages, page + 1))}
disabled={page === totalPages}
className="px-3 py-1 border rounded disabled:opacity-50"
>
Weiter
</button>
</div>
)}
</div>
</>
)}
</div>
</div>
{/* Create Session Modal */}
{showCreateModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl p-6 w-full max-w-md">
<h3 className="text-lg font-semibold mb-4">Neue Audit Session</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Name</label>
<input
type="text"
value={newSession.name}
onChange={(e) => setNewSession({ ...newSession, name: e.target.value })}
className="w-full px-3 py-2 border rounded-lg"
placeholder="z.B. Q1 2026 DSGVO Audit"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Auditor Name</label>
<input
type="text"
value={newSession.auditor_name}
onChange={(e) => setNewSession({ ...newSession, auditor_name: e.target.value })}
className="w-full px-3 py-2 border rounded-lg"
placeholder="Dr. Max Mustermann"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Organisation (optional)</label>
<input
type="text"
value={newSession.auditor_organization}
onChange={(e) => setNewSession({ ...newSession, auditor_organization: e.target.value })}
className="w-full px-3 py-2 border rounded-lg"
placeholder="TÜV Rheinland"
/>
</div>
</div>
<div className="flex justify-end gap-3 mt-6">
<button
onClick={() => setShowCreateModal(false)}
className="px-4 py-2 border rounded-lg"
>
Abbrechen
</button>
<button
onClick={createSession}
disabled={!newSession.name || !newSession.auditor_name}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
Erstellen
</button>
</div>
</div>
</div>
)}
{/* Sign Off Modal */}
{showSignOffModal && selectedItem && (
<SignOffModal
item={selectedItem}
onClose={() => { setShowSignOffModal(false); setSelectedItem(null) }}
onSignOff={signOffItem}
/>
)}
</div>
)
}
// Sign Off Modal Component
function SignOffModal({
item,
onClose,
onSignOff,
}: {
item: ChecklistItem
onClose: () => void
onSignOff: (result: string, notes: string, sign: boolean) => void
}) {
const [result, setResult] = useState(item.current_result === 'pending' ? '' : item.current_result)
const [notes, setNotes] = useState(item.notes || '')
const [sign, setSign] = useState(false)
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl p-6 w-full max-w-lg">
<h3 className="text-lg font-semibold mb-2">Anforderung bewerten</h3>
<p className="text-sm text-slate-500 mb-4">
{item.regulation_code} {item.article}: {item.title}
</p>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Bewertung</label>
<div className="grid grid-cols-2 gap-2">
{[
{ value: 'compliant', label: 'Konform', color: 'green' },
{ value: 'compliant_notes', label: 'Konform (mit Anm.)', color: 'green' },
{ value: 'non_compliant', label: 'Nicht konform', color: 'red' },
{ value: 'not_applicable', label: 'Nicht anwendbar', color: 'slate' },
].map((opt) => (
<button
key={opt.value}
onClick={() => setResult(opt.value)}
className={`p-3 rounded-lg border-2 text-left transition-colors ${
result === opt.value
? opt.color === 'green' ? 'border-green-500 bg-green-50' :
opt.color === 'red' ? 'border-red-500 bg-red-50' :
'border-slate-500 bg-slate-50'
: 'border-slate-200 hover:border-slate-300'
}`}
>
<span className="font-medium">{opt.label}</span>
</button>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Anmerkungen</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
rows={3}
placeholder="Optionale Anmerkungen..."
/>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="sign"
checked={sign}
onChange={(e) => setSign(e.target.checked)}
className="w-4 h-4 rounded"
/>
<label htmlFor="sign" className="text-sm text-slate-700">
Digitale Signatur erstellen (SHA-256)
</label>
</div>
</div>
<div className="flex justify-end gap-3 mt-6">
<button onClick={onClose} className="px-4 py-2 border rounded-lg">
Abbrechen
</button>
<button
onClick={() => onSignOff(result, notes, sign)}
disabled={!result}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
Speichern
</button>
</div>
</div>
</div>
)
}