Files
QuoteForge/internal/handlers/export.go
Mikhail Chusavitin 8f596cec68 fix: standardize CSV export filename format to use project name
Unified export filename format across both ExportCSV and ExportConfigCSV:
- Format: YYYY-MM-DD (project_name) config_name BOM.csv
- Use PriceUpdatedAt if available, otherwise CreatedAt
- Extract project name from ProjectUUID for ExportCSV via projectService
- Pass project_uuid from frontend to backend in export request
- Add projectUUID and projectName state variables to track project context

This ensures consistent naming whether exporting from form or project view,
and uses most recent price update timestamp in filename.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-09 17:22:51 +03:00

169 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
import (
"fmt"
"net/http"
"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"`
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
filename := fmt.Sprintf("%s (%s) %s BOM.csv", exportDate.Format("2006-01-02"), projectName, req.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
}
}
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,
Items: items,
Total: total,
Notes: req.Notes,
CreatedAt: time.Now(),
}
}
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
}
}