diff --git a/cmd/pfs/main.go b/cmd/pfs/main.go index 45b1418..ba33943 100644 --- a/cmd/pfs/main.go +++ b/cmd/pfs/main.go @@ -565,6 +565,7 @@ func setupRouter(cfg *config.Config, configPath string, connMgr *db.ConnectionMa pricelists.GET("/:id", pricelistHandler.Get) pricelists.GET("/:id/items", pricelistHandler.GetItems) pricelists.GET("/:id/lots", pricelistHandler.GetLotNames) + pricelists.GET("/:id/export-csv", pricelistHandler.ExportCSV) pricelists.POST("", pricelistHandler.Create) pricelists.POST("/create-with-progress", pricelistHandler.CreateWithProgress) pricelists.PATCH("/:id/active", pricelistHandler.SetActive) diff --git a/internal/handlers/pricelist.go b/internal/handlers/pricelist.go index 205e335..cb46071 100644 --- a/internal/handlers/pricelist.go +++ b/internal/handlers/pricelist.go @@ -1,7 +1,9 @@ package handlers import ( + "encoding/csv" "errors" + "fmt" "io" "net/http" "strconv" @@ -239,3 +241,160 @@ func (h *PricelistHandler) GetLatest(c *gin.Context) { } c.JSON(http.StatusOK, pl) } + +func (h *PricelistHandler) ExportCSV(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid pricelist ID"}) + return + } + + // Get pricelist info + pl, err := h.service.GetByID(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "pricelist not found"}) + return + } + + // Get all items (no pagination) + items, _, err := h.service.GetItems(uint(id), 1, 999999, "") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Set response headers for CSV download + filename := fmt.Sprintf("pricelist_%s.csv", pl.Version) + c.Header("Content-Type", "text/csv; charset=utf-8") + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) + + // Create CSV writer + writer := csv.NewWriter(c.Writer) + defer writer.Flush() + + // Write UTF-8 BOM for Excel compatibility + c.Writer.Write([]byte{0xEF, 0xBB, 0xBF}) + + // Determine if warehouse source + isWarehouse := strings.ToLower(pl.Source) == "warehouse" + + // Write CSV header + var header []string + if isWarehouse { + header = []string{"Артикул", "Категория", "Описание", "Доступно", "Partnumbers", "Цена, $", "Настройки"} + } else { + header = []string{"Артикул", "Категория", "Описание", "Цена, $", "Настройки"} + } + if err := writer.Write(header); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write CSV header"}) + return + } + + // Write items + for _, item := range items { + row := make([]string, 0, len(header)) + + // Артикул + row = append(row, item.LotName) + + // Категория + category := item.Category + if category == "" { + category = "-" + } + row = append(row, category) + + // Описание + description := item.LotDescription + if description == "" { + description = "-" + } + row = append(row, description) + + if isWarehouse { + // Доступно + qty := "-" + if item.AvailableQty != nil { + qty = fmt.Sprintf("%.3f", *item.AvailableQty) + } + row = append(row, qty) + + // Partnumbers + partnumbers := "-" + if len(item.Partnumbers) > 0 { + partnumbers = strings.Join(item.Partnumbers, ", ") + } + row = append(row, partnumbers) + } + + // Цена + row = append(row, fmt.Sprintf("%.2f", item.Price)) + + // Настройки + settings := formatPriceSettings(item) + row = append(row, settings) + + if err := writer.Write(row); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write CSV row"}) + return + } + } +} + +func formatPriceSettings(item models.PricelistItem) string { + var settings []string + + hasManualPrice := item.ManualPrice != nil && *item.ManualPrice > 0 + hasMeta := item.MetaPrices != "" + method := strings.ToLower(item.PriceMethod) + + // Method indicator + if hasManualPrice { + settings = append(settings, "РУЧН") + } else if method == "average" { + settings = append(settings, "Сред") + } else if method == "weighted_median" { + settings = append(settings, "Взвеш. мед") + } else { + settings = append(settings, "Мед") + } + + // Period (only if not manual price) + if !hasManualPrice { + period := item.PricePeriodDays + switch period { + case 7: + settings = append(settings, "1н") + case 30: + settings = append(settings, "1м") + case 90: + settings = append(settings, "3м") + case 365: + settings = append(settings, "1г") + case 0: + settings = append(settings, "все") + default: + settings = append(settings, fmt.Sprintf("%dд", period)) + } + } + + // Coefficient + if item.PriceCoefficient != 0 { + coef := item.PriceCoefficient + if coef > 0 { + settings = append(settings, fmt.Sprintf("+%.0f%%", coef)) + } else { + settings = append(settings, fmt.Sprintf("%.0f%%", coef)) + } + } + + // Meta article indicator + if hasMeta { + settings = append(settings, "МЕТА") + } + + if len(settings) == 0 { + return "-" + } + return strings.Join(settings, " | ") +} diff --git a/web/templates/admin_pricing.html b/web/templates/admin_pricing.html index c74bce4..2eb1399 100644 --- a/web/templates/admin_pricing.html +++ b/web/templates/admin_pricing.html @@ -684,7 +684,6 @@ function renderLots(lots, total) { html += '
| Артикул | '; html += 'Категория | '; - html += 'Описание | '; html += 'Популярность | '; html += 'Кол-во котировок | '; html += 'Цена | '; @@ -741,7 +738,7 @@ function renderComponents(components, total) { components.forEach((c, idx) => { const price = c.current_price ? '$' + parseFloat(c.current_price).toLocaleString('en-US', {minimumFractionDigits: 2}) : '—'; const category = c.category ? c.category.code : '—'; - const desc = c.lot && c.lot.lot_description ? c.lot.lot_description : '—'; + const desc = c.lot && c.lot.lot_description ? c.lot.lot_description : ''; const quoteCount = c.quote_count || 0; const popularity = c.popularity_score ? c.popularity_score.toFixed(2) : '0.00'; const isHidden = c.is_hidden || quoteCount === 0; @@ -807,10 +804,9 @@ function renderComponents(components, total) { settingsHtml = settings.join(' | '); } - html += '
|---|---|---|---|---|---|
| ' + escapeHtml(c.lot_name) + ' | '; html += '' + escapeHtml(category) + ' | '; - html += '' + escapeHtml(desc) + ' | '; html += '' + popularity + ' | '; html += '' + quoteCount + ' | '; html += '' + price + ' | '; diff --git a/web/templates/pricelist_detail.html b/web/templates/pricelist_detail.html index da2fded..73fc38e 100644 --- a/web/templates/pricelist_detail.html +++ b/web/templates/pricelist_detail.html @@ -2,13 +2,21 @@ {{define "content"}}