fix: Verifikation — Suchfeld statt 654 Mini-Kacheln + Lazy-Load

- SuggestEvidenceModal: Suchfeld + max 20 Ergebnisse statt alle Kacheln
- Verification page: Mitigations nur on-demand laden (nicht beim Seitenstart)
- Deutlich schnellerer Seitenaufbau

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-07 18:33:21 +02:00
parent 969658261f
commit 78d7273b82
2 changed files with 43 additions and 17 deletions
@@ -14,6 +14,12 @@ export function SuggestEvidenceModal({
const [selectedMitigation, setSelectedMitigation] = useState<string>('') const [selectedMitigation, setSelectedMitigation] = useState<string>('')
const [suggested, setSuggested] = useState<SuggestedEvidence[]>([]) const [suggested, setSuggested] = useState<SuggestedEvidence[]>([])
const [loadingSuggestions, setLoadingSuggestions] = useState(false) const [loadingSuggestions, setLoadingSuggestions] = useState(false)
const [search, setSearch] = useState('')
const filtered = search.trim()
? mitigations.filter(m => (m.title || '').toLowerCase().includes(search.toLowerCase()))
: mitigations
const displayed = filtered.slice(0, 20) // Show max 20 at a time
async function handleSelectMitigation(mitigationId: string) { async function handleSelectMitigation(mitigationId: string) {
setSelectedMitigation(mitigationId) setSelectedMitigation(mitigationId)
@@ -41,22 +47,35 @@ export function SuggestEvidenceModal({
</svg> </svg>
</button> </button>
</div> </div>
<p className="text-sm text-gray-500 mb-3"> <p className="text-sm text-gray-500 mb-2">
Waehlen Sie eine Massnahme, um passende Nachweismethoden vorgeschlagen zu bekommen. Waehlen Sie eine Massnahme ({mitigations.length} gesamt). Suchen Sie nach Name:
</p> </p>
<div className="flex flex-wrap gap-2"> <input
{mitigations.map(m => ( type="text" value={search} onChange={e => setSearch(e.target.value)}
placeholder="Massnahme suchen..."
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg mb-3 focus:ring-2 focus:ring-purple-400 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
<div className="max-h-[200px] overflow-auto space-y-1">
{displayed.map(m => (
<button <button
key={m.id} onClick={() => handleSelectMitigation(m.id)} key={m.id} onClick={() => handleSelectMitigation(m.id)}
className={`px-3 py-1.5 text-xs rounded-lg border transition-colors ${ className={`w-full text-left px-3 py-2 text-xs rounded-lg border transition-colors ${
selectedMitigation === m.id selectedMitigation === m.id
? 'border-purple-400 bg-purple-50 text-purple-700 font-medium' ? 'border-purple-400 bg-purple-50 text-purple-700 font-medium'
: 'border-gray-200 bg-white text-gray-700 hover:border-purple-300' : 'border-gray-100 bg-white text-gray-700 hover:border-purple-300 hover:bg-gray-50'
}`} }`}
> >
{m.title} {m.title || '(Ohne Titel)'}
</button> </button>
))} ))}
{filtered.length > 20 && (
<p className="text-xs text-gray-400 text-center py-1">
{filtered.length - 20} weitere Suchbegriff eingeben um zu filtern
</p>
)}
{filtered.length === 0 && (
<p className="text-xs text-gray-400 text-center py-2">Keine Massnahmen gefunden</p>
)}
</div> </div>
</div> </div>
<div className="flex-1 overflow-auto p-6"> <div className="flex-1 overflow-auto p-6">
@@ -23,18 +23,25 @@ export default function VerificationPage() {
async function fetchData() { async function fetchData() {
try { try {
const [verRes, hazRes, mitRes] = await Promise.all([ // Only load verifications initially — hazards/mitigations loaded on demand
fetch(`/api/sdk/v1/iace/projects/${projectId}/verifications`), const verRes = await fetch(`/api/sdk/v1/iace/projects/${projectId}/verifications`)
fetch(`/api/sdk/v1/iace/projects/${projectId}/hazards`),
fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations`),
])
if (verRes.ok) { const j = await verRes.json(); setItems(j.verifications || j || []) } if (verRes.ok) { const j = await verRes.json(); setItems(j.verifications || j || []) }
if (hazRes.ok) { const j = await hazRes.json(); setHazards((j.hazards || j || []).map((h: { id: string; name: string }) => ({ id: h.id, name: h.name }))) }
if (mitRes.ok) { const j = await mitRes.json(); setMitigations((j.mitigations || j || []).map((m: { id: string; title: string }) => ({ id: m.id, title: m.title }))) }
} catch (err) { console.error('Failed to fetch data:', err) } } catch (err) { console.error('Failed to fetch data:', err) }
finally { setLoading(false) } finally { setLoading(false) }
} }
async function loadMitigationsIfNeeded() {
if (mitigations.length > 0) return
try {
const mitRes = await fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations`)
if (mitRes.ok) {
const j = await mitRes.json()
const mits = (j.mitigations || j || []).map((m: Record<string, string>) => ({ id: m.id, title: m.title || m.name || '' }))
setMitigations(mits)
}
} catch { /* ignore */ }
}
async function handleSubmit(data: VerificationFormData) { async function handleSubmit(data: VerificationFormData) {
try { try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/verifications`, { const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/verifications`, {
@@ -89,8 +96,8 @@ export default function VerificationPage() {
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">Nachweisfuehrung fuer alle Schutzmassnahmen und Sicherheitsanforderungen.</p> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">Nachweisfuehrung fuer alle Schutzmassnahmen und Sicherheitsanforderungen.</p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{mitigations.length > 0 && ( {true && (
<button onClick={() => setShowSuggest(true)} className="flex items-center gap-2 px-3 py-2 border border-green-300 text-green-700 rounded-lg hover:bg-green-50 transition-colors text-sm"> <button onClick={async () => { await loadMitigationsIfNeeded(); setShowSuggest(true) }} className="flex items-center gap-2 px-3 py-2 border border-green-300 text-green-700 rounded-lg hover:bg-green-50 transition-colors text-sm">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg> </svg>
@@ -147,7 +154,7 @@ export default function VerificationPage() {
</p> </p>
<div className="mt-6 flex items-center justify-center gap-3"> <div className="mt-6 flex items-center justify-center gap-3">
{mitigations.length > 0 && ( {mitigations.length > 0 && (
<button onClick={() => setShowSuggest(true)} className="px-6 py-3 border border-green-300 text-green-700 rounded-lg hover:bg-green-50 transition-colors"> <button onClick={async () => { await loadMitigationsIfNeeded(); setShowSuggest(true) }} className="px-6 py-3 border border-green-300 text-green-700 rounded-lg hover:bg-green-50 transition-colors">
Nachweise vorschlagen Nachweise vorschlagen
</button> </button>
)} )}