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>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:26 +01:00
commit 5a31f52310
1224 changed files with 425430 additions and 0 deletions

View File

@@ -0,0 +1,867 @@
'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>
)
}