diff --git a/admin-compliance/app/sdk/iace/[projectId]/clarifications/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/clarifications/page.tsx new file mode 100644 index 00000000..685396f4 --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/clarifications/page.tsx @@ -0,0 +1,316 @@ +'use client' + +import { useState, useEffect, useCallback } from 'react' +import { useParams } from 'next/navigation' + +type Clarification = { + id: string + question: string + source: string + category: 'manufacturer' | 'pattern_norm' | string + norm_references?: string[] + affected_hazard_ids: string[] + affected_hazard_names: string[] + status: 'open' | 'in_progress' | 'answered' | 'not_relevant' + answer?: 'ja' | 'nein' | 'teilweise' | '' + reasoning?: string + answered_by?: string + answered_at?: string +} + +type ListResponse = { + clarifications: Clarification[] + open_count: number + answered_count: number + total: number +} + +const CATEGORY_LABEL: Record = { + manufacturer: 'Hersteller', + pattern_norm: 'Norm / Pattern', +} + +const STATUS_LABEL: Record = { + open: 'Offen', + in_progress: 'In Klärung', + answered: 'Beantwortet', + not_relevant: 'Nicht relevant', +} + +const STATUS_COLOR: Record = { + open: 'bg-orange-100 text-orange-800', + in_progress: 'bg-yellow-100 text-yellow-800', + answered: 'bg-green-100 text-green-800', + not_relevant: 'bg-gray-100 text-gray-700', +} + +export default function ClarificationsPage() { + const params = useParams() + const projectId = params.projectId as string + + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [editing, setEditing] = useState(null) + const [filter, setFilter] = useState<'all' | 'open' | 'answered'>('open') + const [searchQuery, setSearchQuery] = useState('') + + const load = useCallback(async () => { + setLoading(true) + setError(null) + try { + const r = await fetch(`/api/sdk/v1/iace/projects/${projectId}/clarifications`) + if (!r.ok) throw new Error(`HTTP ${r.status}`) + const json: ListResponse = await r.json() + setData(json) + } catch (e) { + setError(e instanceof Error ? e.message : String(e)) + } finally { + setLoading(false) + } + }, [projectId]) + + useEffect(() => { + load() + }, [load]) + + const filtered = (data?.clarifications ?? []).filter(c => { + if (filter === 'open' && (c.status === 'answered' || c.status === 'not_relevant')) return false + if (filter === 'answered' && c.status !== 'answered' && c.status !== 'not_relevant') return false + if (searchQuery) { + const q = searchQuery.toLowerCase() + if (!c.question.toLowerCase().includes(q) && !c.source.toLowerCase().includes(q)) return false + } + return true + }) + + const groupedBySource: Record = {} + for (const c of filtered) { + const key = c.source + if (!groupedBySource[key]) groupedBySource[key] = [] + groupedBySource[key].push(c) + } + + return ( +
+
+
+

Klärungen mit dem Anlagenbauer

+

+ Standardisierte Prüffragen aus Norm- und Herstellerwissen. Eine Antwort gilt für alle referenzierten Gefährdungen. +

+
+ {data && ( +
+ + + +
+ )} +
+ +
+
+ {(['open', 'answered', 'all'] as const).map(f => ( + + ))} +
+ setSearchQuery(e.target.value)} + className="flex-1 max-w-sm border rounded px-3 py-1.5 text-sm" + /> +
+ + {loading &&
Lade Klärungen…
} + {error &&
Fehler: {error}
} + + {!loading && data && Object.keys(groupedBySource).length === 0 && ( +
Keine Klärungen für die aktuelle Auswahl.
+ )} + + {!loading && data && Object.entries(groupedBySource).map(([source, items]) => ( +
+

+ + {CATEGORY_LABEL[items[0].category] || items[0].category} + + {source} +

+
+ {items.map(c => ( +
+
+
+
{c.question}
+
+ Betrifft {c.affected_hazard_ids.length} Gefährdung + {c.affected_hazard_ids.length !== 1 ? 'en' : ''} + {c.affected_hazard_names.length > 0 && ( + — {c.affected_hazard_names.slice(0, 2).join('; ')}{c.affected_hazard_names.length > 2 ? `, +${c.affected_hazard_names.length - 2} weitere` : ''} + )} +
+ {c.norm_references && c.norm_references.length > 0 && ( +
+ Norm: {c.norm_references.join(' | ')} +
+ )} + {c.status === 'answered' && c.reasoning && ( +
+ Antwort ({c.answer}): {c.reasoning} + {c.answered_by && ( + — {c.answered_by}, {c.answered_at?.slice(0, 10)} + )} +
+ )} +
+
+ {STATUS_LABEL[c.status]} + +
+
+
+ ))} +
+
+ ))} + + {editing && ( + setEditing(null)} + onSaved={() => { + setEditing(null) + load() + }} + /> + )} +
+ ) +} + +function Badge({ color, label }: { color: string; label: string }) { + return {label} +} + +function AnswerModal({ + clarification, + projectId, + onClose, + onSaved, +}: { + clarification: Clarification + projectId: string + onClose: () => void + onSaved: () => void +}) { + const [status, setStatus] = useState(clarification.status) + const [answer, setAnswer] = useState<'ja' | 'nein' | 'teilweise' | ''>( + (clarification.answer as 'ja' | 'nein' | 'teilweise' | '') || '' + ) + const [reasoning, setReasoning] = useState(clarification.reasoning || '') + const [answeredBy, setAnsweredBy] = useState(clarification.answered_by || '') + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + + const save = async () => { + setSaving(true) + setError(null) + try { + const r = await fetch( + `/api/sdk/v1/iace/projects/${projectId}/clarifications/${encodeURIComponent(clarification.id)}/answer`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status, answer, reasoning, answered_by: answeredBy }), + } + ) + if (!r.ok) throw new Error(`HTTP ${r.status}`) + onSaved() + } catch (e) { + setError(e instanceof Error ? e.message : String(e)) + } finally { + setSaving(false) + } + } + + return ( +
+
e.stopPropagation()}> +
{clarification.source}
+
{clarification.question}
+ + +
+ {(['open', 'in_progress', 'answered', 'not_relevant'] as const).map(s => ( + + ))} +
+ + {(status === 'answered' || status === 'in_progress') && ( + <> + +
+ {(['ja', 'teilweise', 'nein'] as const).map(a => ( + + ))} +
+ + )} + + +