Files
breakpilot-lehrer/website/app/admin/compliance/evidence/_components/EvidenceModals.tsx
Benjamin Admin 451365a312 [split-required] Split remaining 500-680 LOC files (final batch)
website (17 pages + 3 components):
- multiplayer/wizard, middleware/wizard+test-wizard, communication
- builds/wizard, staff-search, voice, sbom/wizard
- foerderantrag, mail/tasks, tools/communication, sbom
- compliance/evidence, uni-crawler, brandbook (already done)
- CollectionsTab, IngestionTab, RiskHeatmap

backend-lehrer (5 files):
- letters_api (641 → 2), certificates_api (636 → 2)
- alerts_agent/db/models (636 → 3)
- llm_gateway/communication_service (614 → 2)
- game/database already done in prior batch

klausur-service (2 files):
- hybrid_vocab_extractor (664 → 2)
- klausur-service/frontend: api.ts (620 → 3), EHUploadWizard (591 → 2)

voice-service (3 files):
- bqas/rag_judge (618 → 3), runner (529 → 2)
- enhanced_task_orchestrator (519 → 2)

studio-v2 (6 files):
- korrektur/[klausurId] (578 → 4), fairness (569 → 2)
- AlertsWizard (552 → 2), OnboardingWizard (513 → 2)
- korrektur/api.ts (506 → 3), geo-lernwelt (501 → 2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 08:56:45 +02:00

233 lines
8.0 KiB
TypeScript

import { useRef, useState } from 'react'
interface Control {
id: string
control_id: string
title: string
}
interface NewEvidenceData {
control_id: string
evidence_type: string
title: string
description: string
artifact_url: string
}
const EVIDENCE_TYPES = [
{ value: 'scan_report', label: 'Scan Report' },
{ value: 'policy_document', label: 'Policy Dokument' },
{ value: 'config_snapshot', label: 'Config Snapshot' },
{ value: 'test_result', label: 'Test Ergebnis' },
{ value: 'screenshot', label: 'Screenshot' },
{ value: 'external_link', label: 'Externer Link' },
{ value: 'manual_upload', label: 'Manueller Upload' },
]
function formatFileSize(bytes: number | null) {
if (!bytes) return '-'
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
interface UploadModalProps {
controls: Control[]
newEvidence: NewEvidenceData
setNewEvidence: (data: NewEvidenceData) => void
uploading: boolean
onUpload: (file: File) => void
onClose: () => void
}
export function UploadModal({
controls,
newEvidence,
setNewEvidence,
uploading,
onUpload,
onClose,
}: UploadModalProps) {
const fileInputRef = useRef<HTMLInputElement>(null)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Datei hochladen</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Control *</label>
<select
value={newEvidence.control_id}
onChange={(e) => setNewEvidence({ ...newEvidence, control_id: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
>
<option value="">Control auswaehlen...</option>
{controls.map((c) => (
<option key={c.id} value={c.control_id}>{c.control_id} - {c.title}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Typ</label>
<select
value={newEvidence.evidence_type}
onChange={(e) => setNewEvidence({ ...newEvidence, evidence_type: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
>
{EVIDENCE_TYPES.filter((t) => t.value !== 'external_link').map((t) => (
<option key={t.value} value={t.value}>{t.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Titel *</label>
<input
type="text"
value={newEvidence.title}
onChange={(e) => setNewEvidence({ ...newEvidence, title: e.target.value })}
placeholder="z.B. Semgrep Scan Report 2026-01"
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Beschreibung</label>
<textarea
value={newEvidence.description}
onChange={(e) => setNewEvidence({ ...newEvidence, description: e.target.value })}
rows={2}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Datei *</label>
<input
type="file"
ref={fileInputRef}
onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
/>
{selectedFile && (
<p className="mt-1 text-sm text-slate-500">
{selectedFile.name} ({formatFileSize(selectedFile.size)})
</p>
)}
</div>
</div>
<div className="flex justify-end gap-3 mt-6">
<button
onClick={onClose}
className="px-4 py-2 text-slate-600 hover:text-slate-800"
>
Abbrechen
</button>
<button
onClick={() => selectedFile && onUpload(selectedFile)}
disabled={uploading || !selectedFile}
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
{uploading ? 'Hochladen...' : 'Hochladen'}
</button>
</div>
</div>
</div>
)
}
interface LinkModalProps {
controls: Control[]
newEvidence: NewEvidenceData
setNewEvidence: (data: NewEvidenceData) => void
uploading: boolean
onSubmit: () => void
onClose: () => void
}
export function LinkModal({
controls,
newEvidence,
setNewEvidence,
uploading,
onSubmit,
onClose,
}: LinkModalProps) {
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Link/Quelle hinzufuegen</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Control *</label>
<select
value={newEvidence.control_id}
onChange={(e) => setNewEvidence({ ...newEvidence, control_id: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
>
<option value="">Control auswaehlen...</option>
{controls.map((c) => (
<option key={c.id} value={c.control_id}>{c.control_id} - {c.title}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Titel *</label>
<input
type="text"
value={newEvidence.title}
onChange={(e) => setNewEvidence({ ...newEvidence, title: e.target.value })}
placeholder="z.B. GitHub Branch Protection Settings"
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">URL *</label>
<input
type="url"
value={newEvidence.artifact_url}
onChange={(e) => setNewEvidence({ ...newEvidence, artifact_url: e.target.value })}
placeholder="https://github.com/..."
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Beschreibung</label>
<textarea
value={newEvidence.description}
onChange={(e) => setNewEvidence({ ...newEvidence, description: e.target.value })}
rows={2}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
/>
</div>
</div>
<div className="flex justify-end gap-3 mt-6">
<button
onClick={onClose}
className="px-4 py-2 text-slate-600 hover:text-slate-800"
>
Abbrechen
</button>
<button
onClick={onSubmit}
disabled={uploading}
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
{uploading ? 'Speichern...' : 'Hinzufuegen'}
</button>
</div>
</div>
</div>
)
}