backend-lehrer (11 files): - llm_gateway/routes/schools.py (867 → 5), recording_api.py (848 → 6) - messenger_api.py (840 → 5), print_generator.py (824 → 5) - unit_analytics_api.py (751 → 5), classroom/routes/context.py (726 → 4) - llm_gateway/routes/edu_search_seeds.py (710 → 4) klausur-service (12 files): - ocr_labeling_api.py (845 → 4), metrics_db.py (833 → 4) - legal_corpus_api.py (790 → 4), page_crop.py (758 → 3) - mail/ai_service.py (747 → 4), github_crawler.py (767 → 3) - trocr_service.py (730 → 4), full_compliance_pipeline.py (723 → 4) - dsfa_rag_api.py (715 → 4), ocr_pipeline_auto.py (705 → 4) website (6 pages): - audit-checklist (867 → 8), content (806 → 6) - screen-flow (790 → 4), scraper (789 → 5) - zeugnisse (776 → 5), modules (745 → 4) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
225 lines
9.0 KiB
TypeScript
225 lines
9.0 KiB
TypeScript
'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 Link from 'next/link'
|
|
import AdminLayout from '@/components/admin/AdminLayout'
|
|
import { RESULT_STATUS, SESSION_STATUS } from './_components/types'
|
|
import { useAuditChecklist } from './_components/useAuditChecklist'
|
|
import SessionCard from './_components/SessionCard'
|
|
import AuditProgressBar from './_components/AuditProgressBar'
|
|
import ChecklistItemRow from './_components/ChecklistItemRow'
|
|
import CreateSessionModal from './_components/CreateSessionModal'
|
|
import SignOffModal from './_components/SignOffModal'
|
|
|
|
export default function AuditChecklistPage() {
|
|
const {
|
|
sessions, selectedSession, setSelectedSession,
|
|
filteredItems, statistics, regulations,
|
|
loading, loadingChecklist, error,
|
|
filterStatus, setFilterStatus,
|
|
filterRegulation, setFilterRegulation,
|
|
searchQuery, setSearchQuery,
|
|
showCreateModal, setShowCreateModal,
|
|
showSignOffModal, setShowSignOffModal,
|
|
selectedItem, setSelectedItem,
|
|
lang, handleSignOff, handleCreateSession,
|
|
} = useAuditChecklist()
|
|
|
|
// 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>
|
|
)}
|
|
|
|
<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>
|
|
|
|
{showCreateModal && (
|
|
<CreateSessionModal
|
|
regulations={regulations}
|
|
onClose={() => setShowCreateModal(false)}
|
|
onCreate={handleCreateSession}
|
|
/>
|
|
)}
|
|
</AdminLayout>
|
|
)
|
|
}
|
|
|
|
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">
|
|
<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>
|
|
|
|
{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>
|
|
|
|
{showSignOffModal && selectedItem && (
|
|
<SignOffModal
|
|
item={selectedItem}
|
|
auditorName={selectedSession?.auditor_name || ''}
|
|
onClose={() => {
|
|
setShowSignOffModal(false)
|
|
setSelectedItem(null)
|
|
}}
|
|
onSubmit={(result, notes, sign) => handleSignOff(selectedItem, result, notes, sign)}
|
|
/>
|
|
)}
|
|
</AdminLayout>
|
|
)
|
|
}
|