feat: Variantenmanagement — Sub-Projekte mit GAP-Analyse

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) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-09 10:47:01 +02:00
parent 2143840ee7
commit 8682522212
8 changed files with 592 additions and 11 deletions
@@ -308,3 +308,88 @@ func (h *IACEHandler) InitFromProfile(c *gin.Context) {
"regulations_triggered": triggeredRegulations,
})
}
// ============================================================================
// Variant Management
// ============================================================================
// CreateVariant handles POST /projects/:id/variants
// Creates a new variant sub-project linked to the given base project.
func (h *IACEHandler) CreateVariant(c *gin.Context) {
parentID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
return
}
// Verify parent project exists
parent, err := h.store.GetProject(c.Request.Context(), parentID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if parent == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "parent project not found"})
return
}
var req iace.CreateProjectRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Force the parent link
req.ParentProjectID = &parentID
project, err := h.store.CreateProject(c.Request.Context(), parent.TenantID, req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"project": project})
}
// ListVariants handles GET /projects/:id/variants
// Returns all variant sub-projects for a given base project.
func (h *IACEHandler) ListVariants(c *gin.Context) {
parentID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
return
}
variants, err := h.store.ListVariants(c.Request.Context(), parentID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if variants == nil {
variants = []iace.Project{}
}
c.JSON(http.StatusOK, iace.ProjectListResponse{
Projects: variants,
Total: len(variants),
})
}
// GetVariantGap handles GET /projects/:id/variant-gap
// Returns a gap analysis comparing a variant against its base project.
func (h *IACEHandler) GetVariantGap(c *gin.Context) {
variantID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
return
}
gap, err := h.store.GetVariantGap(c.Request.Context(), variantID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gap)
}