package handlers import ( "context" "fmt" "io" "net/http" "os" "strconv" "time" "git.mchus.pro/mchus/priceforge/internal/services" "git.mchus.pro/mchus/priceforge/internal/tasks" "github.com/gin-gonic/gin" ) func (h *PricingHandler) ImportStockLog(c *gin.Context) { if h.stockImportService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{ "error": "Импорт склада доступен только в онлайн режиме", "offline": true, }) return } fileHeader, err := c.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "file is required"}) return } if fileHeader.Size > maxStockImportFileSize { c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "file too large (max 25MB)"}) return } // Read create_pricelist parameter createPricelistStr := c.PostForm("create_pricelist") createPricelist := createPricelistStr == "true" file, err := fileHeader.Open() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "failed to open uploaded file"}) return } defer file.Close() content, err := io.ReadAll(io.LimitReader(file, maxStockImportFileSize+1)) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read uploaded file"}) return } if int64(len(content)) > maxStockImportFileSize { c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "file too large (max 25MB)"}) return } modTime := time.Now() if statter, ok := file.(interface{ Stat() (os.FileInfo, error) }); ok { if st, statErr := statter.Stat(); statErr == nil { modTime = st.ModTime() } } filename := fileHeader.Filename taskID := h.taskManager.Submit(tasks.TaskTypeStockImport, func(ctx context.Context, progressCb func(int, string)) (map[string]interface{}, error) { result, impErr := h.stockImportService.Import(filename, content, modTime, h.dbUsername, createPricelist, func(p services.StockImportProgress) { // 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 impErr != nil { return nil, impErr } // Send final completion message var message string if result.WarehousePLID > 0 { message = fmt.Sprintf("Импорт завершён: добавлено %d позиций, создан прайслист %s", result.Inserted, result.WarehousePLVer) } else { message = fmt.Sprintf("Импорт завершён: добавлено %d позиций", result.Inserted) } progressCb(100, message) return map[string]interface{}{ "rows_total": result.RowsTotal, "valid_rows": result.ValidRows, "inserted": result.Inserted, "deleted": result.Deleted, "unmapped": result.Unmapped, "conflicts": result.Conflicts, "auto_mapped": result.AutoMapped, "parse_errors": result.ParseErrors, "qty_parse_errors": result.QtyParseErrors, "ignored": result.Ignored, "import_date": result.ImportDate.Format("2006-01-02"), "warehouse_pricelist_id": result.WarehousePLID, "warehouse_pricelist_version": result.WarehousePLVer, }, nil }) fmt.Printf("[StockImport] Task submitted: task_id=%s, filename=%s, size=%d bytes, create_pricelist=%v\n", taskID, filename, len(content), createPricelist) c.JSON(http.StatusOK, gin.H{"task_id": taskID}) } func (h *PricingHandler) ListStockMappings(c *gin.Context) { if h.stockImportService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{ "error": "Сопоставления доступны только в онлайн режиме", "offline": true, }) return } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "50")) search := c.Query("search") rows, total, err := h.stockImportService.ListMappings(page, perPage, search) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "items": rows, "total": total, "page": page, "per_page": perPage, }) } func (h *PricingHandler) UpsertStockMapping(c *gin.Context) { if h.stockImportService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{ "error": "Сопоставления доступны только в онлайн режиме", "offline": true, }) return } var req struct { Partnumber string `json:"partnumber" binding:"required"` LotName string `json:"lot_name"` Description string `json:"description"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.stockImportService.UpsertMapping(req.Partnumber, req.LotName, req.Description); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "mapping saved"}) } func (h *PricingHandler) DeleteStockMapping(c *gin.Context) { if h.stockImportService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{ "error": "Сопоставления доступны только в онлайн режиме", "offline": true, }) return } partnumber := c.Param("partnumber") deleted, err := h.stockImportService.DeleteMapping(partnumber) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"deleted": deleted}) } func (h *PricingHandler) ListStockIgnoreRules(c *gin.Context) { if h.stockImportService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{ "error": "Правила игнорирования доступны только в онлайн режиме", "offline": true, }) return } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "50")) rows, total, err := h.stockImportService.ListIgnoreRules(page, perPage) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "items": rows, "total": total, "page": page, "per_page": perPage, }) } func (h *PricingHandler) UpsertStockIgnoreRule(c *gin.Context) { if h.stockImportService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{ "error": "Правила игнорирования доступны только в онлайн режиме", "offline": true, }) return } var req struct { Target string `json:"target" binding:"required"` MatchType string `json:"match_type" binding:"required"` Pattern string `json:"pattern" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.stockImportService.UpsertIgnoreRule(req.Target, req.MatchType, req.Pattern); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "ignore rule saved"}) } func (h *PricingHandler) DeleteStockIgnoreRule(c *gin.Context) { if h.stockImportService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{ "error": "Правила игнорирования доступны только в онлайн режиме", "offline": true, }) return } id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil || id == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } deleted, err := h.stockImportService.DeleteIgnoreRule(uint(id)) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"deleted": deleted}) }