feat: add pricelist CSV export and improve description display
- Add CSV export functionality for pricelists with download button - Export includes all pricelist items with proper UTF-8 encoding - Support both warehouse and estimate pricelist sources - Remove description column from admin pricing tables - Show description as tooltip on row hover instead - Improve table layout by removing redundant column Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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, " | ")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user