backend-lehrer (5 files): - alerts_agent/db/repository.py (992 → 5), abitur_docs_api.py (956 → 3) - teacher_dashboard_api.py (951 → 3), services/pdf_service.py (916 → 3) - mail/mail_db.py (987 → 6) klausur-service (5 files): - legal_templates_ingestion.py (942 → 3), ocr_pipeline_postprocess.py (929 → 4) - ocr_pipeline_words.py (876 → 3), ocr_pipeline_ocr_merge.py (616 → 2) - KorrekturPage.tsx (956 → 6) website (5 pages): - mail (985 → 9), edu-search (958 → 8), mac-mini (950 → 7) - ocr-labeling (946 → 7), audit-workspace (871 → 4) studio-v2 (5 files + 1 deleted): - page.tsx (946 → 5), MessagesContext.tsx (925 → 4) - korrektur (914 → 6), worksheet-cleanup (899 → 6) - useVocabWorksheet.ts (888 → 3) - Deleted dead page-original.tsx (934 LOC) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
171 lines
6.4 KiB
TypeScript
171 lines
6.4 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Audit Workspace - Collaborative Audit Management
|
|
*
|
|
* Features:
|
|
* - View all requirements with original text from regulations
|
|
* - Document implementation details for each requirement
|
|
* - Link to source documents (PDFs, EUR-Lex)
|
|
* - Track audit status (pending, in_review, approved, rejected)
|
|
* - Add code references and evidence
|
|
* - Auditor notes and sign-off
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import Link from 'next/link'
|
|
import AdminLayout from '@/components/admin/AdminLayout'
|
|
import type { Regulation, Requirement, RequirementUpdate } from './types'
|
|
import RequirementList from './_components/RequirementList'
|
|
import RequirementDetailPanel from './_components/RequirementDetailPanel'
|
|
|
|
export default function AuditWorkspacePage() {
|
|
const [regulations, setRegulations] = useState<Regulation[]>([])
|
|
const [requirements, setRequirements] = useState<Requirement[]>([])
|
|
const [selectedRegulation, setSelectedRegulation] = useState<string | null>(null)
|
|
const [selectedRequirement, setSelectedRequirement] = useState<Requirement | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [saving, setSaving] = useState(false)
|
|
const [filterAuditStatus, setFilterAuditStatus] = useState<string>('all')
|
|
const [filterImplStatus, setFilterImplStatus] = useState<string>('all')
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|
|
|
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
|
|
|
|
useEffect(() => { loadRegulations() }, [])
|
|
|
|
useEffect(() => {
|
|
if (selectedRegulation) loadRequirements(selectedRegulation)
|
|
}, [selectedRegulation])
|
|
|
|
const loadRegulations = async () => {
|
|
try {
|
|
const res = await fetch(`${BACKEND_URL}/api/v1/compliance/regulations`)
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
setRegulations(data.regulations || [])
|
|
if (data.regulations?.length > 0) setSelectedRegulation(data.regulations[0].code)
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load regulations:', err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const loadRequirements = async (regCode: string) => {
|
|
try {
|
|
const res = await fetch(`${BACKEND_URL}/api/v1/compliance/regulations/${regCode}/requirements`)
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
setRequirements(data.requirements || [])
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load requirements:', err)
|
|
}
|
|
}
|
|
|
|
const updateRequirement = async (reqId: string, updates: RequirementUpdate) => {
|
|
setSaving(true)
|
|
try {
|
|
const res = await fetch(`${BACKEND_URL}/api/v1/compliance/requirements/${reqId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(updates),
|
|
})
|
|
if (res.ok) {
|
|
setRequirements(prev => prev.map(r => r.id === reqId ? { ...r, ...updates } : r))
|
|
if (selectedRequirement?.id === reqId) {
|
|
setSelectedRequirement({ ...selectedRequirement, ...updates })
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to update requirement:', err)
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
const filteredRequirements = requirements.filter(req => {
|
|
if (filterAuditStatus !== 'all' && req.audit_status !== filterAuditStatus) return false
|
|
if (filterImplStatus !== 'all' && req.implementation_status !== filterImplStatus) return false
|
|
if (searchQuery) {
|
|
const query = searchQuery.toLowerCase()
|
|
return (
|
|
req.title.toLowerCase().includes(query) ||
|
|
req.article.toLowerCase().includes(query) ||
|
|
req.requirement_text?.toLowerCase().includes(query)
|
|
)
|
|
}
|
|
return true
|
|
})
|
|
|
|
const currentRegulation = regulations.find(r => r.code === selectedRegulation)
|
|
|
|
const stats = {
|
|
total: requirements.length,
|
|
verified: requirements.filter(r => r.implementation_status === 'verified').length,
|
|
approved: requirements.filter(r => r.audit_status === 'approved').length,
|
|
pending: requirements.filter(r => r.audit_status === 'pending').length,
|
|
}
|
|
|
|
return (
|
|
<AdminLayout title="Audit Workspace" description="Gemeinsam mit Pruefern arbeiten">
|
|
{/* Header with back link */}
|
|
<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>
|
|
|
|
<div className="flex items-center gap-4">
|
|
<div className="text-sm text-slate-500">
|
|
{stats.approved}/{stats.total} genehmigt | {stats.verified}/{stats.total} verifiziert
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-12 gap-6">
|
|
<RequirementList
|
|
regulations={regulations}
|
|
selectedRegulation={selectedRegulation}
|
|
setSelectedRegulation={setSelectedRegulation}
|
|
filteredRequirements={filteredRequirements}
|
|
selectedRequirement={selectedRequirement}
|
|
setSelectedRequirement={setSelectedRequirement}
|
|
searchQuery={searchQuery}
|
|
setSearchQuery={setSearchQuery}
|
|
filterAuditStatus={filterAuditStatus}
|
|
setFilterAuditStatus={setFilterAuditStatus}
|
|
filterImplStatus={filterImplStatus}
|
|
setFilterImplStatus={setFilterImplStatus}
|
|
/>
|
|
|
|
{/* Right Panel */}
|
|
<div className="col-span-8">
|
|
{selectedRequirement ? (
|
|
<RequirementDetailPanel
|
|
requirement={selectedRequirement}
|
|
regulation={currentRegulation}
|
|
onUpdate={(updates) => updateRequirement(selectedRequirement.id, updates)}
|
|
saving={saving}
|
|
/>
|
|
) : (
|
|
<div className="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 12h6m-6 4h6m2 5H7a2 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>
|
|
<p className="text-slate-500">Waehlen Sie eine Anforderung aus der Liste</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</AdminLayout>
|
|
)
|
|
}
|