package handlers import ( "context" "encoding/csv" "errors" "fmt" "io" "net/http" "strconv" "strings" "git.mchus.pro/mchus/priceforge/internal/models" "git.mchus.pro/mchus/priceforge/internal/services/pricelist" "git.mchus.pro/mchus/priceforge/internal/tasks" "github.com/gin-gonic/gin" ) type PricelistHandler struct { service *pricelist.Service dbUser string taskManager *tasks.Manager } func NewPricelistHandler(service *pricelist.Service, dbUser string, taskManager *tasks.Manager) *PricelistHandler { return &PricelistHandler{service: service, dbUser: dbUser, taskManager: taskManager} } func (h *PricelistHandler) List(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20")) source := c.Query("source") activeOnly := c.DefaultQuery("active_only", "false") == "true" var ( list []models.PricelistSummary total int64 err error ) if activeOnly { list, total, err = h.service.ListActiveBySource(page, perPage, source) } else { list, total, err = h.service.ListBySource(page, perPage, source) } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"pricelists": list, "total": total, "page": page, "per_page": perPage}) } func (h *PricelistHandler) Get(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 } pl, err := h.service.GetByID(uint(id)) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "pricelist not found"}) return } c.JSON(http.StatusOK, pl) } func (h *PricelistHandler) Create(c *gin.Context) { canWrite, debugInfo := h.service.CanWriteDebug() if !canWrite { c.JSON(http.StatusForbidden, gin.H{"error": "pricelist write is not allowed", "debug": debugInfo}) return } var req struct { Source string `json:"source"` Items []struct { LotName string `json:"lot_name"` Price float64 `json:"price"` } `json:"items"` } if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } source := string(models.NormalizePricelistSource(req.Source)) createdBy := h.dbUser if strings.TrimSpace(createdBy) == "" { createdBy = "unknown" } items := make([]pricelist.CreateItemInput, 0, len(req.Items)) for _, item := range req.Items { items = append(items, pricelist.CreateItemInput{LotName: item.LotName, Price: item.Price}) } pl, err := h.service.CreateForSourceWithProgress(createdBy, source, items, nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, pl) } func (h *PricelistHandler) CreateWithProgress(c *gin.Context) { canWrite, debugInfo := h.service.CanWriteDebug() if !canWrite { c.JSON(http.StatusForbidden, gin.H{"error": "pricelist write is not allowed", "debug": debugInfo}) return } var req struct { Source string `json:"source"` Items []struct { LotName string `json:"lot_name"` Price float64 `json:"price"` } `json:"items"` } if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } source := string(models.NormalizePricelistSource(req.Source)) createdBy := h.dbUser if strings.TrimSpace(createdBy) == "" { createdBy = "unknown" } items := make([]pricelist.CreateItemInput, 0, len(req.Items)) for _, item := range req.Items { items = append(items, pricelist.CreateItemInput{LotName: item.LotName, Price: item.Price}) } taskID := h.taskManager.Submit(tasks.TaskTypePricelistCreate, func(ctx context.Context, progressCb func(int, string)) (map[string]interface{}, error) { pl, err := h.service.CreateForSourceWithProgress(createdBy, source, items, func(p pricelist.CreateProgress) { // Convert service progress to task progress var progress int if p.Total > 0 { progress = int(float64(p.Current) / float64(p.Total) * 100) } progressCb(progress, p.Message) }) if err != nil { return nil, err } // Send final completion message progressCb(100, fmt.Sprintf("Прайслист создан: %s из источника %s", pl.Version, pl.Source)) return map[string]interface{}{ "pricelist_id": pl.ID, "pricelist_version": pl.Version, "source": pl.Source, }, nil }) c.JSON(http.StatusOK, gin.H{"task_id": taskID}) } func (h *PricelistHandler) Delete(c *gin.Context) { canWrite, debugInfo := h.service.CanWriteDebug() if !canWrite { c.JSON(http.StatusForbidden, gin.H{"error": "pricelist write is not allowed", "debug": debugInfo}) return } id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid pricelist ID"}) return } if err := h.service.Delete(uint(id)); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "pricelist deleted"}) } func (h *PricelistHandler) SetActive(c *gin.Context) { canWrite, debugInfo := h.service.CanWriteDebug() if !canWrite { c.JSON(http.StatusForbidden, gin.H{"error": "pricelist write is not allowed", "debug": debugInfo}) return } id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid pricelist ID"}) return } var req struct { IsActive bool `json:"is_active"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.service.SetActive(uint(id), req.IsActive); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "updated", "is_active": req.IsActive}) } func (h *PricelistHandler) GetItems(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 } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "50")) search := c.Query("search") items, total, err := h.service.GetItems(uint(id), page, perPage, search) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"items": items, "total": total, "page": page, "per_page": perPage}) } func (h *PricelistHandler) GetLotNames(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 } lotNames, err := h.service.GetLotNames(uint(id)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"lot_names": lotNames, "total": len(lotNames)}) } func (h *PricelistHandler) CanWrite(c *gin.Context) { canWrite, debugInfo := h.service.CanWriteDebug() c.JSON(http.StatusOK, gin.H{"can_write": canWrite, "debug": debugInfo}) } func (h *PricelistHandler) GetLatest(c *gin.Context) { source := string(models.NormalizePricelistSource(c.DefaultQuery("source", string(models.PricelistSourceEstimate)))) pl, err := h.service.GetLatestActiveBySource(source) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "no pricelists available"}) return } 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 } // 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)) // Write UTF-8 BOM for Excel compatibility c.Writer.Write([]byte{0xEF, 0xBB, 0xBF}) // Create CSV writer with semicolon separator writer := csv.NewWriter(c.Writer) writer.Comma = ';' defer writer.Flush() // 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.String(http.StatusInternalServerError, "Failed to write CSV header") return } // Stream items in batches to avoid loading everything into memory err = h.service.StreamItemsForExport(uint(id), 500, func(items []models.PricelistItem) error { for _, item := range items { row := make([]string, 0, len(header)) // Артикул row = append(row, item.LotName) // Категория category := "-" if item.LotCategory != nil && *item.LotCategory != "" { category = *item.LotCategory } 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 { return err } } // Flush after each batch writer.Flush() return nil }) if err != nil { // Already started writing, can't return JSON error c.String(http.StatusInternalServerError, "Export failed: %v", err) 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, " | ") }