Extract components and hooks into _components/ and _hooks/ subdirectories to reduce each page.tsx to under 500 LOC (was 1545/1383/1316). Final line counts: evidence=213, process-tasks=304, hazards=157. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
126 lines
5.8 KiB
TypeScript
126 lines
5.8 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import type { DisplayEvidence } from './EvidenceTypes'
|
|
|
|
export function ReviewModal({ evidence, onClose, onSuccess }: {
|
|
evidence: DisplayEvidence
|
|
onClose: () => void
|
|
onSuccess: () => void
|
|
}) {
|
|
const [confidenceLevel, setConfidenceLevel] = useState(evidence.confidenceLevel || 'E1')
|
|
const [truthStatus, setTruthStatus] = useState(evidence.truthStatus || 'uploaded')
|
|
const [reviewedBy, setReviewedBy] = useState('')
|
|
const [submitting, setSubmitting] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const handleSubmit = async () => {
|
|
if (!reviewedBy.trim()) { setError('Bitte E-Mail-Adresse angeben'); return }
|
|
setSubmitting(true)
|
|
setError(null)
|
|
try {
|
|
const res = await fetch(`/api/sdk/v1/compliance/evidence/${evidence.id}/review`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ confidence_level: confidenceLevel, truth_status: truthStatus, reviewed_by: reviewedBy }),
|
|
})
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ detail: 'Review fehlgeschlagen' }))
|
|
throw new Error(typeof err.detail === 'string' ? err.detail : JSON.stringify(err.detail))
|
|
}
|
|
onSuccess()
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
|
} finally {
|
|
setSubmitting(false)
|
|
}
|
|
}
|
|
|
|
const confidenceLevels = [
|
|
{ value: 'E0', label: 'E0 — Generiert' },
|
|
{ value: 'E1', label: 'E1 — Manuell' },
|
|
{ value: 'E2', label: 'E2 — Intern validiert' },
|
|
{ value: 'E3', label: 'E3 — System-beobachtet' },
|
|
{ value: 'E4', label: 'E4 — Extern auditiert' },
|
|
]
|
|
|
|
const truthStatuses = [
|
|
{ value: 'generated', label: 'Generiert' },
|
|
{ value: 'uploaded', label: 'Hochgeladen' },
|
|
{ value: 'observed', label: 'Beobachtet' },
|
|
{ value: 'validated', label: 'Validiert' },
|
|
{ value: 'audited', label: 'Auditiert' },
|
|
]
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
|
|
<div className="bg-white rounded-2xl shadow-xl w-full max-w-lg mx-4 p-6" onClick={e => e.stopPropagation()}>
|
|
<h2 className="text-xl font-bold text-gray-900 mb-4">Evidence Reviewen</h2>
|
|
<p className="text-sm text-gray-500 mb-4">{evidence.name}</p>
|
|
|
|
<div className="mb-4 p-3 bg-gray-50 rounded-lg text-sm space-y-1">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">Aktuelles Confidence-Level:</span>
|
|
<span className="font-medium">{evidence.confidenceLevel || '—'}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-500">Aktueller Truth-Status:</span>
|
|
<span className="font-medium">{evidence.truthStatus || '—'}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Neues Confidence-Level</label>
|
|
<select value={confidenceLevel} onChange={e => setConfidenceLevel(e.target.value)}
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
|
{confidenceLevels.map(l => <option key={l.value} value={l.value}>{l.label}</option>)}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Neuer Truth-Status</label>
|
|
<select value={truthStatus} onChange={e => setTruthStatus(e.target.value)}
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
|
{truthStatuses.map(s => <option key={s.value} value={s.value}>{s.label}</option>)}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Reviewer (E-Mail)</label>
|
|
<input type="email" value={reviewedBy} onChange={e => setReviewedBy(e.target.value)}
|
|
placeholder="reviewer@unternehmen.de"
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
|
|
</div>
|
|
|
|
{evidence.requiresFourEyes && evidence.approvalStatus !== 'approved' && (
|
|
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
|
<div className="flex items-start gap-2">
|
|
<svg className="w-5 h-5 text-yellow-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
<div className="text-sm text-yellow-800">
|
|
<p className="font-medium">4-Augen-Prinzip aktiv</p>
|
|
<p>Dieser Nachweis erfordert eine zusaetzliche Freigabe durch einen zweiten Reviewer.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">{error}</div>
|
|
)}
|
|
|
|
<div className="flex justify-end gap-3">
|
|
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
|
|
Abbrechen
|
|
</button>
|
|
<button onClick={handleSubmit} disabled={submitting}
|
|
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50">
|
|
{submitting ? 'Wird gespeichert...' : 'Review abschliessen'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|