package handlers import ( "fmt" "net/http" "strings" "time" "git.mchus.pro/mchus/quoteforge/internal/middleware" "git.mchus.pro/mchus/quoteforge/internal/services" "github.com/gin-gonic/gin" ) type ExportHandler struct { exportService *services.ExportService configService services.ConfigurationGetter componentService *services.ComponentService projectService *services.ProjectService } func NewExportHandler( exportService *services.ExportService, configService services.ConfigurationGetter, componentService *services.ComponentService, projectService *services.ProjectService, ) *ExportHandler { return &ExportHandler{ exportService: exportService, configService: configService, componentService: componentService, 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"` 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.Items) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "no items to export"}) return } // Get project name if available projectName := req.ProjectName if projectName == "" && req.ProjectUUID != "" { // Try to load project name from database username := middleware.GetUsername(c) if project, err := h.projectService.GetByUUID(req.ProjectUUID, username); err == nil && project != nil { projectName = project.Name } } if projectName == "" { projectName = 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"), projectName, 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 } } func (h *ExportHandler) buildExportData(req *ExportRequest) *services.ExportData { items := make([]services.ExportItem, len(req.Items)) var total float64 for i, item := range req.Items { itemTotal := item.UnitPrice * float64(item.Quantity) // Получаем информацию о компоненте для заполнения категории и описания componentView, err := h.componentService.GetByLotName(item.LotName) if err != nil { // Если не удалось получить информацию о компоненте, используем только основные данные items[i] = services.ExportItem{ LotName: item.LotName, Quantity: item.Quantity, UnitPrice: item.UnitPrice, TotalPrice: itemTotal, } } else { items[i] = services.ExportItem{ LotName: item.LotName, Description: componentView.Description, Category: componentView.Category, Quantity: item.Quantity, UnitPrice: item.UnitPrice, TotalPrice: itemTotal, } } total += itemTotal } return &services.ExportData{ Name: req.Name, Article: req.Article, Items: items, Total: total, Notes: req.Notes, CreatedAt: time.Now(), } } 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, h.componentService) // Validate before streaming (can return JSON error) if len(data.Items) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "no items to export"}) return } // Get project name if configuration belongs to a project projectName := 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 { projectName = project.Name } } // 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"), projectName, 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 } }