diff --git a/cmd/qfs/main.go b/cmd/qfs/main.go index 6bf6bf2..34d74cd 100644 --- a/cmd/qfs/main.go +++ b/cmd/qfs/main.go @@ -695,7 +695,7 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect // Handlers componentHandler := handlers.NewComponentHandler(componentService, local) quoteHandler := handlers.NewQuoteHandler(quoteService) - exportHandler := handlers.NewExportHandler(exportService, configService, componentService) + exportHandler := handlers.NewExportHandler(exportService, configService, componentService, projectService) pricelistHandler := handlers.NewPricelistHandler(local) syncHandler, err := handlers.NewSyncHandler(local, syncService, connMgr, templatesPath, backgroundSyncInterval) if err != nil { diff --git a/internal/handlers/export.go b/internal/handlers/export.go index 7b9ab50..86159a6 100644 --- a/internal/handlers/export.go +++ b/internal/handlers/export.go @@ -14,24 +14,28 @@ 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"` - Items []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"` @@ -54,12 +58,22 @@ func (h *ExportHandler) ExportCSV(c *gin.Context) { return } - // Set headers before streaming + // 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 } - filename := fmt.Sprintf("%s (%s) %s BOM.csv", time.Now().Format("2006-01-02"), 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)) @@ -128,9 +142,21 @@ func (h *ExportHandler) ExportConfigCSV(c *gin.Context) { 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 - // For config export, use config name for both project and quotation name - filename := fmt.Sprintf("%s (%s) %s BOM.csv", config.CreatedAt.Format("2006-01-02"), config.Name, config.Name) + // 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)) diff --git a/internal/handlers/export_test.go b/internal/handlers/export_test.go index df40019..74be502 100644 --- a/internal/handlers/export_test.go +++ b/internal/handlers/export_test.go @@ -39,6 +39,7 @@ func TestExportCSV_Success(t *testing.T) { exportSvc, &mockConfigService{}, mockComponentService, + nil, ) // Create JSON request body @@ -113,6 +114,7 @@ func TestExportCSV_InvalidRequest(t *testing.T) { exportSvc, &mockConfigService{}, &services.ComponentService{}, + nil, ) // Create invalid request (missing required field) @@ -146,6 +148,7 @@ func TestExportCSV_EmptyItems(t *testing.T) { exportSvc, &mockConfigService{}, &services.ComponentService{}, + nil, ) // Create request with empty items array - should fail binding validation @@ -187,6 +190,7 @@ func TestExportConfigCSV_Success(t *testing.T) { exportSvc, &mockConfigService{config: mockConfig}, &services.ComponentService{}, + nil, ) // Create HTTP request @@ -236,6 +240,7 @@ func TestExportConfigCSV_NotFound(t *testing.T) { exportSvc, &mockConfigService{err: errors.New("config not found")}, &services.ComponentService{}, + nil, ) req, _ := http.NewRequest("GET", "/api/configs/nonexistent-uuid/export", nil) @@ -280,6 +285,7 @@ func TestExportConfigCSV_EmptyItems(t *testing.T) { exportSvc, &mockConfigService{config: mockConfig}, &services.ComponentService{}, + nil, ) req, _ := http.NewRequest("GET", "/api/configs/test-uuid/export", nil) diff --git a/web/templates/index.html b/web/templates/index.html index 6938929..6a3d346 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -326,6 +326,8 @@ let ASSIGNED_CATEGORIES = Object.values(TAB_CONFIG) // State let configUUID = '{{.ConfigUUID}}'; let configName = ''; +let projectUUID = ''; +let projectName = ''; let currentTab = 'base'; let allComponents = []; let cart = []; @@ -609,6 +611,7 @@ document.addEventListener('DOMContentLoaded', async function() { const config = await resp.json(); configName = config.name; + projectUUID = config.project_uuid || ''; document.getElementById('config-name').textContent = config.name; document.getElementById('save-buttons').classList.remove('hidden'); @@ -1795,7 +1798,7 @@ async function exportCSV() { const resp = await fetch('/api/export/csv', { method: 'POST', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({items: exportItems, name: configName}) + body: JSON.stringify({items: exportItems, name: configName, project_uuid: projectUUID}) }); const blob = await resp.blob(); @@ -2048,7 +2051,7 @@ async function exportCSVWithCustomPrice() { const resp = await fetch('/api/export/csv', { method: 'POST', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({items: adjustedCart, name: configName}) + body: JSON.stringify({items: adjustedCart, name: configName, project_uuid: projectUUID}) }); const blob = await resp.blob();