A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
741 lines
19 KiB
Go
741 lines
19 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/roadmap"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// RoadmapHandlers handles roadmap-related HTTP requests
|
|
type RoadmapHandlers struct {
|
|
store *roadmap.Store
|
|
parser *roadmap.Parser
|
|
}
|
|
|
|
// NewRoadmapHandlers creates new roadmap handlers
|
|
func NewRoadmapHandlers(store *roadmap.Store) *RoadmapHandlers {
|
|
return &RoadmapHandlers{
|
|
store: store,
|
|
parser: roadmap.NewParser(),
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Roadmap CRUD
|
|
// ============================================================================
|
|
|
|
// CreateRoadmap creates a new roadmap
|
|
// POST /sdk/v1/roadmaps
|
|
func (h *RoadmapHandlers) CreateRoadmap(c *gin.Context) {
|
|
var req roadmap.CreateRoadmapRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
tenantID := rbac.GetTenantID(c)
|
|
userID := rbac.GetUserID(c)
|
|
|
|
r := &roadmap.Roadmap{
|
|
TenantID: tenantID,
|
|
Title: req.Title,
|
|
Description: req.Description,
|
|
AssessmentID: req.AssessmentID,
|
|
PortfolioID: req.PortfolioID,
|
|
StartDate: req.StartDate,
|
|
TargetDate: req.TargetDate,
|
|
Status: "draft",
|
|
CreatedBy: userID,
|
|
}
|
|
|
|
if err := h.store.CreateRoadmap(c.Request.Context(), r); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, roadmap.CreateRoadmapResponse{Roadmap: *r})
|
|
}
|
|
|
|
// ListRoadmaps lists roadmaps for the tenant
|
|
// GET /sdk/v1/roadmaps
|
|
func (h *RoadmapHandlers) ListRoadmaps(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
|
|
filters := &roadmap.RoadmapFilters{
|
|
Status: c.Query("status"),
|
|
Limit: 50,
|
|
}
|
|
|
|
if assessmentID := c.Query("assessment_id"); assessmentID != "" {
|
|
if id, err := uuid.Parse(assessmentID); err == nil {
|
|
filters.AssessmentID = &id
|
|
}
|
|
}
|
|
|
|
roadmaps, err := h.store.ListRoadmaps(c.Request.Context(), tenantID, filters)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"roadmaps": roadmaps,
|
|
"total": len(roadmaps),
|
|
})
|
|
}
|
|
|
|
// GetRoadmap retrieves a roadmap by ID
|
|
// GET /sdk/v1/roadmaps/:id
|
|
func (h *RoadmapHandlers) GetRoadmap(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
|
return
|
|
}
|
|
|
|
r, err := h.store.GetRoadmap(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if r == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "roadmap not found"})
|
|
return
|
|
}
|
|
|
|
// Get items
|
|
items, err := h.store.ListItems(c.Request.Context(), id, nil)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Get stats
|
|
stats, _ := h.store.GetRoadmapStats(c.Request.Context(), id)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"roadmap": r,
|
|
"items": items,
|
|
"stats": stats,
|
|
})
|
|
}
|
|
|
|
// UpdateRoadmap updates a roadmap
|
|
// PUT /sdk/v1/roadmaps/:id
|
|
func (h *RoadmapHandlers) UpdateRoadmap(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
|
return
|
|
}
|
|
|
|
r, err := h.store.GetRoadmap(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if r == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "roadmap not found"})
|
|
return
|
|
}
|
|
|
|
var req roadmap.CreateRoadmapRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
r.Title = req.Title
|
|
r.Description = req.Description
|
|
r.AssessmentID = req.AssessmentID
|
|
r.PortfolioID = req.PortfolioID
|
|
r.StartDate = req.StartDate
|
|
r.TargetDate = req.TargetDate
|
|
|
|
if err := h.store.UpdateRoadmap(c.Request.Context(), r); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"roadmap": r})
|
|
}
|
|
|
|
// DeleteRoadmap deletes a roadmap
|
|
// DELETE /sdk/v1/roadmaps/:id
|
|
func (h *RoadmapHandlers) DeleteRoadmap(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
|
return
|
|
}
|
|
|
|
if err := h.store.DeleteRoadmap(c.Request.Context(), id); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "roadmap deleted"})
|
|
}
|
|
|
|
// GetRoadmapStats returns statistics for a roadmap
|
|
// GET /sdk/v1/roadmaps/:id/stats
|
|
func (h *RoadmapHandlers) GetRoadmapStats(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
|
return
|
|
}
|
|
|
|
stats, err := h.store.GetRoadmapStats(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, stats)
|
|
}
|
|
|
|
// ============================================================================
|
|
// RoadmapItem CRUD
|
|
// ============================================================================
|
|
|
|
// CreateItem creates a new roadmap item
|
|
// POST /sdk/v1/roadmaps/:id/items
|
|
func (h *RoadmapHandlers) CreateItem(c *gin.Context) {
|
|
roadmapID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid roadmap ID"})
|
|
return
|
|
}
|
|
|
|
var input roadmap.RoadmapItemInput
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
item := &roadmap.RoadmapItem{
|
|
RoadmapID: roadmapID,
|
|
Title: input.Title,
|
|
Description: input.Description,
|
|
Category: input.Category,
|
|
Priority: input.Priority,
|
|
Status: input.Status,
|
|
ControlID: input.ControlID,
|
|
RegulationRef: input.RegulationRef,
|
|
GapID: input.GapID,
|
|
EffortDays: input.EffortDays,
|
|
AssigneeName: input.AssigneeName,
|
|
Department: input.Department,
|
|
PlannedStart: input.PlannedStart,
|
|
PlannedEnd: input.PlannedEnd,
|
|
Notes: input.Notes,
|
|
}
|
|
|
|
if err := h.store.CreateItem(c.Request.Context(), item); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update roadmap progress
|
|
h.store.UpdateRoadmapProgress(c.Request.Context(), roadmapID)
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"item": item})
|
|
}
|
|
|
|
// ListItems lists items for a roadmap
|
|
// GET /sdk/v1/roadmaps/:id/items
|
|
func (h *RoadmapHandlers) ListItems(c *gin.Context) {
|
|
roadmapID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid roadmap ID"})
|
|
return
|
|
}
|
|
|
|
filters := &roadmap.RoadmapItemFilters{
|
|
SearchQuery: c.Query("search"),
|
|
Limit: 100,
|
|
}
|
|
|
|
if status := c.Query("status"); status != "" {
|
|
filters.Status = roadmap.ItemStatus(status)
|
|
}
|
|
if priority := c.Query("priority"); priority != "" {
|
|
filters.Priority = roadmap.ItemPriority(priority)
|
|
}
|
|
if category := c.Query("category"); category != "" {
|
|
filters.Category = roadmap.ItemCategory(category)
|
|
}
|
|
if controlID := c.Query("control_id"); controlID != "" {
|
|
filters.ControlID = controlID
|
|
}
|
|
|
|
items, err := h.store.ListItems(c.Request.Context(), roadmapID, filters)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"items": items,
|
|
"total": len(items),
|
|
})
|
|
}
|
|
|
|
// GetItem retrieves a roadmap item
|
|
// GET /sdk/v1/roadmap-items/:id
|
|
func (h *RoadmapHandlers) GetItem(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
|
return
|
|
}
|
|
|
|
item, err := h.store.GetItem(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if item == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "item not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"item": item})
|
|
}
|
|
|
|
// UpdateItem updates a roadmap item
|
|
// PUT /sdk/v1/roadmap-items/:id
|
|
func (h *RoadmapHandlers) UpdateItem(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
|
return
|
|
}
|
|
|
|
item, err := h.store.GetItem(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if item == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "item not found"})
|
|
return
|
|
}
|
|
|
|
var input roadmap.RoadmapItemInput
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update fields
|
|
item.Title = input.Title
|
|
item.Description = input.Description
|
|
if input.Category != "" {
|
|
item.Category = input.Category
|
|
}
|
|
if input.Priority != "" {
|
|
item.Priority = input.Priority
|
|
}
|
|
if input.Status != "" {
|
|
item.Status = input.Status
|
|
}
|
|
item.ControlID = input.ControlID
|
|
item.RegulationRef = input.RegulationRef
|
|
item.GapID = input.GapID
|
|
item.EffortDays = input.EffortDays
|
|
item.AssigneeName = input.AssigneeName
|
|
item.Department = input.Department
|
|
item.PlannedStart = input.PlannedStart
|
|
item.PlannedEnd = input.PlannedEnd
|
|
item.Notes = input.Notes
|
|
|
|
if err := h.store.UpdateItem(c.Request.Context(), item); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update roadmap progress
|
|
h.store.UpdateRoadmapProgress(c.Request.Context(), item.RoadmapID)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"item": item})
|
|
}
|
|
|
|
// UpdateItemStatus updates just the status of a roadmap item
|
|
// PATCH /sdk/v1/roadmap-items/:id/status
|
|
func (h *RoadmapHandlers) UpdateItemStatus(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Status roadmap.ItemStatus `json:"status"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
item, err := h.store.GetItem(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if item == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "item not found"})
|
|
return
|
|
}
|
|
|
|
item.Status = req.Status
|
|
|
|
// Set actual dates
|
|
now := time.Now().UTC()
|
|
if req.Status == roadmap.ItemStatusInProgress && item.ActualStart == nil {
|
|
item.ActualStart = &now
|
|
}
|
|
if req.Status == roadmap.ItemStatusCompleted && item.ActualEnd == nil {
|
|
item.ActualEnd = &now
|
|
}
|
|
|
|
if err := h.store.UpdateItem(c.Request.Context(), item); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update roadmap progress
|
|
h.store.UpdateRoadmapProgress(c.Request.Context(), item.RoadmapID)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"item": item})
|
|
}
|
|
|
|
// DeleteItem deletes a roadmap item
|
|
// DELETE /sdk/v1/roadmap-items/:id
|
|
func (h *RoadmapHandlers) DeleteItem(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
|
return
|
|
}
|
|
|
|
item, err := h.store.GetItem(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if item == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "item not found"})
|
|
return
|
|
}
|
|
|
|
roadmapID := item.RoadmapID
|
|
|
|
if err := h.store.DeleteItem(c.Request.Context(), id); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update roadmap progress
|
|
h.store.UpdateRoadmapProgress(c.Request.Context(), roadmapID)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "item deleted"})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Import Workflow
|
|
// ============================================================================
|
|
|
|
// UploadImport handles file upload for import
|
|
// POST /sdk/v1/roadmaps/import/upload
|
|
func (h *RoadmapHandlers) UploadImport(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
userID := rbac.GetUserID(c)
|
|
|
|
// Get file from form
|
|
file, header, err := c.Request.FormFile("file")
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "file is required"})
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
// Read file content
|
|
buf := bytes.Buffer{}
|
|
if _, err := io.Copy(&buf, file); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read file"})
|
|
return
|
|
}
|
|
|
|
// Detect format
|
|
format := roadmap.ImportFormat("")
|
|
filename := header.Filename
|
|
contentType := header.Header.Get("Content-Type")
|
|
|
|
// Create import job
|
|
job := &roadmap.ImportJob{
|
|
TenantID: tenantID,
|
|
Filename: filename,
|
|
FileSize: header.Size,
|
|
ContentType: contentType,
|
|
Status: "pending",
|
|
CreatedBy: userID,
|
|
}
|
|
|
|
if err := h.store.CreateImportJob(c.Request.Context(), job); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Parse the file
|
|
job.Status = "parsing"
|
|
h.store.UpdateImportJob(c.Request.Context(), job)
|
|
|
|
result, err := h.parser.ParseFile(buf.Bytes(), filename, contentType)
|
|
if err != nil {
|
|
job.Status = "failed"
|
|
job.ErrorMessage = err.Error()
|
|
h.store.UpdateImportJob(c.Request.Context(), job)
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "failed to parse file",
|
|
"detail": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Update job with parsed data
|
|
job.Status = "parsed"
|
|
job.Format = format
|
|
job.TotalRows = result.TotalRows
|
|
job.ValidRows = result.ValidRows
|
|
job.InvalidRows = result.InvalidRows
|
|
job.ParsedItems = result.Items
|
|
|
|
h.store.UpdateImportJob(c.Request.Context(), job)
|
|
|
|
c.JSON(http.StatusOK, roadmap.ImportParseResponse{
|
|
JobID: job.ID,
|
|
Status: job.Status,
|
|
TotalRows: result.TotalRows,
|
|
ValidRows: result.ValidRows,
|
|
InvalidRows: result.InvalidRows,
|
|
Items: result.Items,
|
|
ColumnMap: buildColumnMap(result.Columns),
|
|
})
|
|
}
|
|
|
|
// GetImportJob returns the status of an import job
|
|
// GET /sdk/v1/roadmaps/import/:jobId
|
|
func (h *RoadmapHandlers) GetImportJob(c *gin.Context) {
|
|
jobID, err := uuid.Parse(c.Param("jobId"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid job ID"})
|
|
return
|
|
}
|
|
|
|
job, err := h.store.GetImportJob(c.Request.Context(), jobID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if job == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "import job not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"job": job,
|
|
"items": job.ParsedItems,
|
|
})
|
|
}
|
|
|
|
// ConfirmImport confirms and executes the import
|
|
// POST /sdk/v1/roadmaps/import/:jobId/confirm
|
|
func (h *RoadmapHandlers) ConfirmImport(c *gin.Context) {
|
|
jobID, err := uuid.Parse(c.Param("jobId"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid job ID"})
|
|
return
|
|
}
|
|
|
|
var req roadmap.ImportConfirmRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
job, err := h.store.GetImportJob(c.Request.Context(), jobID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if job == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "import job not found"})
|
|
return
|
|
}
|
|
|
|
if job.Status != "parsed" {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "job is not ready for confirmation",
|
|
"status": job.Status,
|
|
})
|
|
return
|
|
}
|
|
|
|
tenantID := rbac.GetTenantID(c)
|
|
userID := rbac.GetUserID(c)
|
|
|
|
// Create or use existing roadmap
|
|
var roadmapID uuid.UUID
|
|
if req.RoadmapID != nil {
|
|
roadmapID = *req.RoadmapID
|
|
// Verify roadmap exists
|
|
r, err := h.store.GetRoadmap(c.Request.Context(), roadmapID)
|
|
if err != nil || r == nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "roadmap not found"})
|
|
return
|
|
}
|
|
} else {
|
|
// Create new roadmap
|
|
title := req.RoadmapTitle
|
|
if title == "" {
|
|
title = "Imported Roadmap - " + job.Filename
|
|
}
|
|
|
|
r := &roadmap.Roadmap{
|
|
TenantID: tenantID,
|
|
Title: title,
|
|
Status: "active",
|
|
CreatedBy: userID,
|
|
}
|
|
|
|
if err := h.store.CreateRoadmap(c.Request.Context(), r); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
roadmapID = r.ID
|
|
}
|
|
|
|
// Determine which rows to import
|
|
selectedRows := make(map[int]bool)
|
|
if len(req.SelectedRows) > 0 {
|
|
for _, row := range req.SelectedRows {
|
|
selectedRows[row] = true
|
|
}
|
|
}
|
|
|
|
// Convert parsed items to roadmap items
|
|
var items []roadmap.RoadmapItem
|
|
var importedCount, skippedCount int
|
|
|
|
for i, parsed := range job.ParsedItems {
|
|
// Skip invalid items
|
|
if !parsed.IsValid {
|
|
skippedCount++
|
|
continue
|
|
}
|
|
|
|
// Skip unselected rows if selection was specified
|
|
if len(selectedRows) > 0 && !selectedRows[parsed.RowNumber] {
|
|
skippedCount++
|
|
continue
|
|
}
|
|
|
|
item := roadmap.RoadmapItem{
|
|
RoadmapID: roadmapID,
|
|
Title: parsed.Data.Title,
|
|
Description: parsed.Data.Description,
|
|
Category: parsed.Data.Category,
|
|
Priority: parsed.Data.Priority,
|
|
Status: parsed.Data.Status,
|
|
ControlID: parsed.Data.ControlID,
|
|
RegulationRef: parsed.Data.RegulationRef,
|
|
GapID: parsed.Data.GapID,
|
|
EffortDays: parsed.Data.EffortDays,
|
|
AssigneeName: parsed.Data.AssigneeName,
|
|
Department: parsed.Data.Department,
|
|
PlannedStart: parsed.Data.PlannedStart,
|
|
PlannedEnd: parsed.Data.PlannedEnd,
|
|
Notes: parsed.Data.Notes,
|
|
SourceRow: parsed.RowNumber,
|
|
SourceFile: job.Filename,
|
|
SortOrder: i,
|
|
}
|
|
|
|
// Apply auto-mappings if requested
|
|
if req.ApplyMappings {
|
|
if parsed.MatchedControl != "" {
|
|
item.ControlID = parsed.MatchedControl
|
|
}
|
|
if parsed.MatchedRegulation != "" {
|
|
item.RegulationRef = parsed.MatchedRegulation
|
|
}
|
|
if parsed.MatchedGap != "" {
|
|
item.GapID = parsed.MatchedGap
|
|
}
|
|
}
|
|
|
|
// Set defaults
|
|
if item.Status == "" {
|
|
item.Status = roadmap.ItemStatusPlanned
|
|
}
|
|
if item.Priority == "" {
|
|
item.Priority = roadmap.ItemPriorityMedium
|
|
}
|
|
if item.Category == "" {
|
|
item.Category = roadmap.ItemCategoryTechnical
|
|
}
|
|
|
|
items = append(items, item)
|
|
importedCount++
|
|
}
|
|
|
|
// Bulk create items
|
|
if len(items) > 0 {
|
|
if err := h.store.BulkCreateItems(c.Request.Context(), items); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Update roadmap progress
|
|
h.store.UpdateRoadmapProgress(c.Request.Context(), roadmapID)
|
|
|
|
// Update job status
|
|
now := time.Now().UTC()
|
|
job.Status = "completed"
|
|
job.RoadmapID = &roadmapID
|
|
job.ImportedItems = importedCount
|
|
job.CompletedAt = &now
|
|
h.store.UpdateImportJob(c.Request.Context(), job)
|
|
|
|
c.JSON(http.StatusOK, roadmap.ImportConfirmResponse{
|
|
RoadmapID: roadmapID,
|
|
ImportedItems: importedCount,
|
|
SkippedItems: skippedCount,
|
|
Message: "Import completed successfully",
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
func buildColumnMap(columns []roadmap.DetectedColumn) map[string]string {
|
|
result := make(map[string]string)
|
|
for _, col := range columns {
|
|
if col.MappedTo != "" {
|
|
result[col.Header] = col.MappedTo
|
|
}
|
|
}
|
|
return result
|
|
}
|