Files
breakpilot-lehrer/website/app/admin/compliance/audit-checklist/page.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

868 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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'
/**
* 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<AuditSession[]>([])
const [selectedSession, setSelectedSession] = useState<AuditSession | null>(null)
const [checklistItems, setChecklistItems] = useState<AuditChecklistItem[]>([])
const [statistics, setStatistics] = useState<AuditStatistics | null>(null)
const [regulations, setRegulations] = useState<Regulation[]>([])
const [loading, setLoading] = useState(true)
const [loadingChecklist, setLoadingChecklist] = useState(false)
const [error, setError] = useState<string | null>(null)
// Filters
const [filterStatus, setFilterStatus] = useState<string>('all')
const [filterRegulation, setFilterRegulation] = useState<string>('all')
const [searchQuery, setSearchQuery] = useState('')
// Modal states
const [showCreateModal, setShowCreateModal] = useState(false)
const [showSignOffModal, setShowSignOffModal] = useState(false)
const [selectedItem, setSelectedItem] = useState<AuditChecklistItem | null>(null)
// Language
const [lang] = useState<Language>('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 (
<AdminLayout title="Audit Checkliste" description="Strukturierte Pruefungen durchfuehren">
<div className="mb-6 flex items-center justify-between">
<Link
href="/admin/compliance"
className="text-sm text-slate-600 hover:text-slate-900 flex items-center gap-1"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Zurueck zu Compliance
</Link>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 flex items-center gap-2"
>
<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>
Neue Audit-Session
</button>
</div>
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
{error}
</div>
)}
{/* Session Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{sessions.length === 0 ? (
<div className="col-span-full bg-white rounded-lg border border-slate-200 p-8 text-center">
<svg className="w-12 h-12 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 2m-6 9l2 2 4-4" />
</svg>
<h3 className="text-lg font-medium text-slate-700 mb-2">Keine Audit-Sessions</h3>
<p className="text-slate-500 mb-4">Erstellen Sie eine neue Audit-Session um zu beginnen.</p>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700"
>
Erste Session erstellen
</button>
</div>
) : (
sessions.map(session => (
<SessionCard
key={session.id}
session={session}
onClick={() => setSelectedSession(session)}
/>
))
)}
</div>
{/* Create Session Modal */}
{showCreateModal && (
<CreateSessionModal
regulations={regulations}
onClose={() => 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')
}
}}
/>
)}
</AdminLayout>
)
}
// Loading state
if (loading) {
return (
<AdminLayout title="Audit Checkliste" description="Laden...">
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
</div>
</AdminLayout>
)
}
// Checklist view for selected session
return (
<AdminLayout title={selectedSession?.name || 'Audit Checkliste'} description="Pruefung durchfuehren">
{/* Header */}
<div className="mb-6 flex items-center justify-between">
<button
onClick={() => setSelectedSession(null)}
className="text-sm text-slate-600 hover:text-slate-900 flex items-center gap-1"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Zurueck zur Uebersicht
</button>
<div className="flex items-center gap-3">
<span className={`px-3 py-1 text-sm font-medium rounded-full ${SESSION_STATUS[selectedSession?.status || 'draft'].color}`}>
{SESSION_STATUS[selectedSession?.status || 'draft'].label}
</span>
<button
className="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 flex items-center gap-2"
onClick={() => {/* Export PDF */}}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Export PDF
</button>
</div>
</div>
{/* Progress Bar */}
{statistics && (
<AuditProgressBar statistics={statistics} lang={lang} />
)}
{/* Filters */}
<div className="bg-white rounded-lg border border-slate-200 p-4 mb-6">
<div className="flex flex-wrap gap-4">
<div className="flex-1 min-w-[200px]">
<label className="block text-xs font-medium text-slate-500 mb-1">Suche</label>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Artikel, Titel..."
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">Verordnung</label>
<select
value={filterRegulation}
onChange={(e) => setFilterRegulation(e.target.value)}
className="px-3 py-2 border border-slate-300 rounded-lg text-sm"
>
<option value="all">Alle</option>
{regulations.map(reg => (
<option key={reg.code} value={reg.code}>{reg.code}</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">Status</label>
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="px-3 py-2 border border-slate-300 rounded-lg text-sm"
>
<option value="all">Alle</option>
{Object.entries(RESULT_STATUS).map(([key, { label }]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
</div>
</div>
</div>
{/* Checklist Items */}
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
{loadingChecklist ? (
<div className="p-8 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto"></div>
<p className="mt-2 text-slate-500">Checkliste wird geladen...</p>
</div>
) : filteredItems.length === 0 ? (
<div className="p-8 text-center">
<p className="text-slate-500">Keine Eintraege gefunden</p>
</div>
) : (
<div className="divide-y divide-slate-100">
{filteredItems.map((item, index) => (
<ChecklistItemRow
key={item.requirement_id}
item={item}
index={index + 1}
onSignOff={() => {
setSelectedItem(item)
setShowSignOffModal(true)
}}
lang={lang}
/>
))}
</div>
)}
</div>
{/* Sign-off Modal */}
{showSignOffModal && selectedItem && (
<SignOffModal
item={selectedItem}
auditorName={selectedSession?.auditor_name || ''}
onClose={() => {
setShowSignOffModal(false)
setSelectedItem(null)
}}
onSubmit={(result, notes, sign) => handleSignOff(selectedItem, result, notes, sign)}
/>
)}
</AdminLayout>
)
}
// 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 (
<button
onClick={onClick}
className="bg-white rounded-lg border border-slate-200 p-4 text-left hover:shadow-md hover:border-primary-300 transition-all"
>
<div className="flex items-start justify-between mb-3">
<h3 className="font-semibold text-slate-900">{session.name}</h3>
<span className={`px-2 py-0.5 text-xs font-medium rounded ${SESSION_STATUS[session.status].color}`}>
{SESSION_STATUS[session.status].label}
</span>
</div>
{session.description && (
<p className="text-sm text-slate-500 mb-3 line-clamp-2">{session.description}</p>
)}
<div className="mb-3">
<div className="flex justify-between text-xs text-slate-500 mb-1">
<span>{session.completed_items} / {session.total_items} Punkte</span>
<span>{completionPercent}%</span>
</div>
<div className="h-2 bg-slate-100 rounded-full overflow-hidden">
<div
className="h-full bg-primary-500 transition-all"
style={{ width: `${completionPercent}%` }}
/>
</div>
</div>
<div className="flex items-center justify-between text-xs text-slate-500">
<span>Auditor: {session.auditor_name}</span>
<span>{new Date(session.created_at).toLocaleDateString('de-DE')}</span>
</div>
</button>
)
}
// 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 (
<div className="bg-white rounded-lg border border-slate-200 p-4 mb-6">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-slate-900">Fortschritt</h3>
<span className="text-2xl font-bold text-primary-600">
{Math.round(statistics.completion_percentage)}%
</span>
</div>
{/* Stacked Progress Bar */}
<div className="h-4 bg-slate-100 rounded-full overflow-hidden flex">
{segments.map(seg => (
seg.count > 0 && (
<div
key={seg.key}
className={`${seg.color} transition-all`}
style={{ width: `${(seg.count / statistics.total) * 100}%` }}
title={`${seg.label}: ${seg.count}`}
/>
)
))}
</div>
{/* Legend */}
<div className="flex flex-wrap gap-4 mt-3">
{segments.map(seg => (
<div key={seg.key} className="flex items-center gap-1.5 text-sm">
<span className={`w-3 h-3 rounded ${seg.color}`} />
<span className="text-slate-600">{seg.label}:</span>
<span className="font-medium text-slate-900">{seg.count}</span>
</div>
))}
</div>
</div>
)
}
// 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 (
<div className="p-4 hover:bg-slate-50 transition-colors">
<div className="flex items-start gap-4">
{/* Status Icon */}
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-lg ${status.color}`}>
{status.icon}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-medium text-slate-500">{index}.</span>
<span className="font-mono text-sm text-primary-600">{item.regulation_code}</span>
<span className="font-mono text-sm text-slate-700">{item.article}</span>
{item.is_signed && (
<span className="px-1.5 py-0.5 text-xs bg-green-100 text-green-700 rounded flex items-center gap-1">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
Signiert
</span>
)}
</div>
<h4 className="text-sm font-medium text-slate-900 mb-1">{item.title}</h4>
{item.notes && (
<p className="text-xs text-slate-500 italic mb-2">"{item.notes}"</p>
)}
<div className="flex items-center gap-4 text-xs text-slate-500">
<span>{item.controls_mapped} Controls</span>
<span>{item.evidence_count} Nachweise</span>
{item.signed_at && (
<span>Signiert: {new Date(item.signed_at).toLocaleDateString('de-DE')}</span>
)}
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2">
<span className={`px-2 py-1 text-xs font-medium rounded ${status.color}`}>
{lang === 'de' ? status.label : status.labelEn}
</span>
<button
onClick={onSignOff}
className="p-2 text-slate-400 hover:text-primary-600 hover:bg-primary-50 rounded-lg transition-colors"
title="Sign-off"
>
<svg className="w-5 h-5" 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>
</button>
</div>
</div>
</div>
)
}
// 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<string[]>([])
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 (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl max-w-lg w-full mx-4 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
<h2 className="text-lg font-semibold text-slate-900">Neue Audit-Session</h2>
<button onClick={onClose} className="text-slate-400 hover:text-slate-600">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Name der Pruefung *
</label>
<input
type="text"
value={name}
onChange={(e) => 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
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Beschreibung
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Optionale Beschreibung..."
className="w-full px-3 py-2 border border-slate-300 rounded-lg"
rows={2}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Auditor Name *
</label>
<input
type="text"
value={auditorName}
onChange={(e) => setAuditorName(e.target.value)}
placeholder="Dr. Max Mustermann"
className="w-full px-3 py-2 border border-slate-300 rounded-lg"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
E-Mail
</label>
<input
type="email"
value={auditorEmail}
onChange={(e) => setAuditorEmail(e.target.value)}
placeholder="auditor@example.com"
className="w-full px-3 py-2 border border-slate-300 rounded-lg"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Verordnungen (optional - leer = alle)
</label>
<div className="flex flex-wrap gap-2 p-2 border border-slate-300 rounded-lg max-h-32 overflow-y-auto">
{regulations.map(reg => (
<label key={reg.code} className="flex items-center gap-1.5">
<input
type="checkbox"
checked={selectedRegs.includes(reg.code)}
onChange={(e) => {
if (e.target.checked) {
setSelectedRegs([...selectedRegs, reg.code])
} else {
setSelectedRegs(selectedRegs.filter(r => r !== reg.code))
}
}}
className="rounded"
/>
<span className="text-sm text-slate-700">{reg.code}</span>
</label>
))}
</div>
</div>
<div className="flex justify-end gap-3 pt-4">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-slate-600 hover:text-slate-800"
>
Abbrechen
</button>
<button
type="submit"
disabled={!name || !auditorName || submitting}
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
{submitting ? 'Erstelle...' : 'Session erstellen'}
</button>
</div>
</form>
</div>
</div>
)
}
// Sign-off Modal
function SignOffModal({
item,
auditorName,
onClose,
onSubmit,
}: {
item: AuditChecklistItem
auditorName: string
onClose: () => void
onSubmit: (result: AuditChecklistItem['current_result'], notes: string, sign: boolean) => void
}) {
const [result, setResult] = useState<AuditChecklistItem['current_result']>(item.current_result)
const [notes, setNotes] = useState(item.notes || '')
const [sign, setSign] = useState(false)
const [submitting, setSubmitting] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setSubmitting(true)
await onSubmit(result, notes, sign)
setSubmitting(false)
}
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl max-w-lg w-full mx-4 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-200">
<h2 className="text-lg font-semibold text-slate-900">Auditor Sign-off</h2>
<p className="text-sm text-slate-500 mt-1">
{item.regulation_code} {item.article} - {item.title}
</p>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Pruefungsergebnis
</label>
<div className="space-y-2">
{Object.entries(RESULT_STATUS).map(([key, { label, color }]) => (
<label key={key} className="flex items-center gap-3 p-2 rounded-lg hover:bg-slate-50 cursor-pointer">
<input
type="radio"
name="result"
value={key}
checked={result === key}
onChange={() => setResult(key as typeof result)}
className="w-4 h-4"
/>
<span className={`px-2 py-0.5 text-sm font-medium rounded ${color}`}>
{label}
</span>
</label>
))}
</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)}
placeholder="Optionale Anmerkungen zur Pruefung..."
className="w-full px-3 py-2 border border-slate-300 rounded-lg"
rows={3}
/>
</div>
<div className="p-3 bg-amber-50 border border-amber-200 rounded-lg">
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
checked={sign}
onChange={(e) => setSign(e.target.checked)}
className="mt-1 rounded"
/>
<div>
<span className="font-medium text-amber-800">Digital signieren</span>
<p className="text-sm text-amber-700 mt-0.5">
Erstellt eine SHA-256 Signatur des Ergebnisses. Diese Aktion kann nicht rueckgaengig gemacht werden.
</p>
</div>
</label>
</div>
<div className="text-sm text-slate-500 pt-2 border-t border-slate-100">
<p>Auditor: <span className="font-medium text-slate-700">{auditorName}</span></p>
<p>Datum: <span className="font-medium text-slate-700">{new Date().toLocaleString('de-DE')}</span></p>
</div>
<div className="flex justify-end gap-3 pt-4">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-slate-600 hover:text-slate-800"
>
Abbrechen
</button>
<button
type="submit"
disabled={submitting}
className={`px-4 py-2 rounded-lg disabled:opacity-50 ${
sign
? 'bg-amber-600 text-white hover:bg-amber-700'
: 'bg-primary-600 text-white hover:bg-primary-700'
}`}
>
{submitting ? 'Speichere...' : sign ? 'Signieren & Speichern' : 'Speichern'}
</button>
</div>
</form>
</div>
</div>
)
}