From 8682522212240aaf261ca78f41237511d08cb2ca Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sat, 9 May 2026 10:47:01 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Variantenmanagement=20=E2=80=94=20Sub-P?= =?UTF-8?q?rojekte=20mit=20GAP-Analyse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - parent_project_id auf iace_projects (DB + Go Struct) - POST/GET /variants + GET /variant-gap Endpoints - GAP-Analyse: Differenz Hazards/Massnahmen/Kategorien Frontend: - VariantPanel auf Projekt-Uebersicht - Variante erstellen Dialog - Sidebar-Anzeige (Variantenanzahl / Basis-Link) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../[projectId]/_components/VariantPanel.tsx | 253 ++++++++++++++++++ .../app/sdk/iace/[projectId]/page.tsx | 24 ++ admin-compliance/app/sdk/iace/layout.tsx | 45 +++- .../api/handlers/iace_handler_projects.go | 85 ++++++ ai-compliance-sdk/internal/app/routes.go | 3 + ai-compliance-sdk/internal/iace/models_api.go | 23 ++ .../internal/iace/models_entities.go | 1 + .../internal/iace/store_projects.go | 169 +++++++++++- 8 files changed, 592 insertions(+), 11 deletions(-) create mode 100644 admin-compliance/app/sdk/iace/[projectId]/_components/VariantPanel.tsx diff --git a/admin-compliance/app/sdk/iace/[projectId]/_components/VariantPanel.tsx b/admin-compliance/app/sdk/iace/[projectId]/_components/VariantPanel.tsx new file mode 100644 index 0000000..6db00e1 --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/_components/VariantPanel.tsx @@ -0,0 +1,253 @@ +'use client' + +import { useState, useEffect, useCallback } from 'react' +import Link from 'next/link' + +interface VariantProject { + id: string + machine_name: string + description?: string + status: string + hazard_count?: number + parent_project_id?: string +} + +interface VariantGapResponse { + base_project: { id: string; name: string; hazard_count: number; measure_count: number } + variant: { id: string; name: string; hazard_count: number; measure_count: number } + gap: { additional_hazards: number; additional_measures: number; categories_affected: string[] } +} + +interface Props { + projectId: string + parentProjectId?: string | null + parentProjectName?: string +} + +export function VariantPanel({ projectId, parentProjectId, parentProjectName }: Props) { + const [variants, setVariants] = useState([]) + const [gapMap, setGapMap] = useState>({}) + const [loading, setLoading] = useState(true) + const [showCreate, setShowCreate] = useState(false) + const [creating, setCreating] = useState(false) + const [name, setName] = useState('') + const [description, setDescription] = useState('') + + const fetchVariants = useCallback(async () => { + try { + const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/variants`) + if (!res.ok) { + setVariants([]) + return + } + const json = await res.json() + const list: VariantProject[] = json.variants || json.projects || [] + setVariants(list) + + // Fetch gap analysis for this project + const gapRes = await fetch(`/api/sdk/v1/iace/projects/${projectId}/variant-gap`) + if (gapRes.ok) { + const gapJson = await gapRes.json() + const gaps: Record = {} + // Could be a single gap or array — handle both + if (Array.isArray(gapJson)) { + for (const g of gapJson) { + gaps[g.variant?.id] = g + } + } else if (gapJson.variant) { + gaps[gapJson.variant.id] = gapJson + } + setGapMap(gaps) + } + } catch { + setVariants([]) + } finally { + setLoading(false) + } + }, [projectId]) + + useEffect(() => { + fetchVariants() + }, [fetchVariants]) + + async function handleCreate() { + if (!name.trim()) return + setCreating(true) + try { + const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/variants`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + machine_name: name.trim(), + description: description.trim(), + }), + }) + if (res.ok) { + setName('') + setDescription('') + setShowCreate(false) + fetchVariants() + } + } catch { + // silently handle + } finally { + setCreating(false) + } + } + + // If this project IS a variant, show link to base project + if (parentProjectId) { + return ( +
+
+
+ + + +
+
+

Variante

+

+ Dieses Projekt ist eine Variante des Basis-Projekts +

+
+ + {parentProjectName || 'Basis-Projekt'} + + + + +
+
+ ) + } + + if (loading) return null + if (variants.length === 0 && !showCreate) { + return ( +
+
+
+
+ + + +
+
+

Keine Varianten

+

Erstellen Sie Varianten fuer verschiedene Betriebsarten

+
+
+ +
+ {renderCreateDialog()} +
+ ) + } + + function renderCreateDialog() { + if (!showCreate) return null + return ( +
+

Neue Variante erstellen

+ setName(e.target.value)} + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white dark:bg-gray-800 dark:border-gray-600 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> +