package handlers import ( "fmt" "net/http" "strings" "time" "git.mchus.pro/mchus/quoteforge/internal/middleware" "git.mchus.pro/mchus/quoteforge/internal/models" "git.mchus.pro/mchus/quoteforge/internal/services" "github.com/gin-gonic/gin" ) type ExportHandler struct { exportService *services.ExportService configService services.ConfigurationGetter projectService *services.ProjectService } func NewExportHandler( exportService *services.ExportService, configService services.ConfigurationGetter, projectService *services.ProjectService, ) *ExportHandler { return &ExportHandler{ exportService: exportService, configService: configService, projectService: projectService, } } type ExportRequest struct { Name string `json:"name" binding:"required"` ProjectName string `json:"project_name"` ProjectUUID string `json:"project_uuid"` Article string `json:"article"` ServerCount int `json:"server_count"` PricelistID *uint `json:"pricelist_id"` Items []struct { LotName string `json:"lot_name" binding:"required"` Quantity int `json:"quantity" binding:"required,min=1"` UnitPrice float64 `json:"unit_price"` } `json:"items" binding:"required,min=1"` Notes string `json:"notes"` } func (h *ExportHandler) ExportCSV(c *gin.Context) { var req ExportRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } data := h.buildExportData(&req) // Validate before streaming (can return JSON error) if len(data.Configs) == 0 || len(data.Configs[0].Items) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "no items to export"}) return } // Get project code for filename projectCode := req.ProjectName // legacy field: may contain code from frontend if projectCode == "" && req.ProjectUUID != "" { username := middleware.GetUsername(c) if project, err := h.projectService.GetByUUID(req.ProjectUUID, username); err == nil && project != nil { projectCode = project.Code } } if projectCode == "" { projectCode = req.Name } // Set headers before streaming exportDate := data.CreatedAt articleSegment := sanitizeFilenameSegment(req.Article) if articleSegment == "" { articleSegment = "BOM" } filename := fmt.Sprintf("%s (%s) %s %s.csv", exportDate.Format("2006-01-02"), projectCode, req.Name, articleSegment) c.Header("Content-Type", "text/csv; charset=utf-8") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) // Stream CSV (cannot return JSON after this point) if err := h.exportService.ToCSV(c.Writer, data); err != nil { c.Error(err) // Log only return } } // buildExportData converts an ExportRequest into a ProjectExportData using a temporary Configuration model // so that ExportService.ConfigToExportData can resolve categories via localDB. func (h *ExportHandler) buildExportData(req *ExportRequest) *services.ProjectExportData { configItems := make(models.ConfigItems, len(req.Items)) for i, item := range req.Items { configItems[i] = models.ConfigItem{ LotName: item.LotName, Quantity: item.Quantity, UnitPrice: item.UnitPrice, } } serverCount := req.ServerCount if serverCount < 1 { serverCount = 1 } cfg := &models.Configuration{ Article: req.Article, ServerCount: serverCount, PricelistID: req.PricelistID, Items: configItems, CreatedAt: time.Now(), } return h.exportService.ConfigToExportData(cfg) } func sanitizeFilenameSegment(value string) string { if strings.TrimSpace(value) == "" { return "" } replacer := strings.NewReplacer( "/", "_", "\\", "_", ":", "_", "*", "_", "?", "_", "\"", "_", "<", "_", ">", "_", "|", "_", ) return strings.TrimSpace(replacer.Replace(value)) } func (h *ExportHandler) ExportConfigCSV(c *gin.Context) { username := middleware.GetUsername(c) uuid := c.Param("uuid") // Get config before streaming (can return JSON error) config, err := h.configService.GetByUUID(uuid, username) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } data := h.exportService.ConfigToExportData(config) // Validate before streaming (can return JSON error) if len(data.Configs) == 0 || len(data.Configs[0].Items) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "no items to export"}) return } // Get project code for filename projectCode := config.Name // fallback: use config name if no project if config.ProjectUUID != nil && *config.ProjectUUID != "" { if project, err := h.projectService.GetByUUID(*config.ProjectUUID, username); err == nil && project != nil { projectCode = project.Code } } // Set headers before streaming // Use price update time if available, otherwise creation time exportDate := config.CreatedAt if config.PriceUpdatedAt != nil { exportDate = *config.PriceUpdatedAt } filename := fmt.Sprintf("%s (%s) %s BOM.csv", exportDate.Format("2006-01-02"), projectCode, config.Name) c.Header("Content-Type", "text/csv; charset=utf-8") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) // Stream CSV (cannot return JSON after this point) if err := h.exportService.ToCSV(c.Writer, data); err != nil { c.Error(err) // Log only return } } // ExportProjectCSV exports all active configurations of a project as a single CSV file. func (h *ExportHandler) ExportProjectCSV(c *gin.Context) { username := middleware.GetUsername(c) projectUUID := c.Param("uuid") project, err := h.projectService.GetByUUID(projectUUID, username) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } result, err := h.projectService.ListConfigurations(projectUUID, username, "active") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if len(result.Configs) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "no configurations to export"}) return } data := h.exportService.ProjectToExportData(result.Configs) // Filename: YYYY-MM-DD (ProjectCode) BOM.csv filename := fmt.Sprintf("%s (%s) BOM.csv", time.Now().Format("2006-01-02"), project.Code) c.Header("Content-Type", "text/csv; charset=utf-8") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) if err := h.exportService.ToCSV(c.Writer, data); err != nil { c.Error(err) return } }