package handlers import ( "fmt" "net/http" "strconv" "git.mchus.pro/mchus/quoteforge/internal/localdb" "git.mchus.pro/mchus/quoteforge/internal/services/pricelist" "github.com/gin-gonic/gin" ) type PricelistHandler struct { service *pricelist.Service localDB *localdb.LocalDB } func NewPricelistHandler(service *pricelist.Service, localDB *localdb.LocalDB) *PricelistHandler { return &PricelistHandler{service: service, localDB: localDB} } // List returns all pricelists with pagination func (h *PricelistHandler) List(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20")) activeOnly := c.DefaultQuery("active_only", "false") == "true" var ( pricelists any total int64 err error ) if activeOnly { pricelists, total, err = h.service.ListActive(page, perPage) } else { pricelists, total, err = h.service.List(page, perPage) } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // If offline (empty list), fallback to local pricelists if total == 0 && h.localDB != nil { localPLs, err := h.localDB.GetLocalPricelists() if err == nil && len(localPLs) > 0 { // Convert to PricelistSummary format summaries := make([]map[string]interface{}, len(localPLs)) for i, lpl := range localPLs { summaries[i] = map[string]interface{}{ "id": lpl.ServerID, "version": lpl.Version, "created_by": "sync", "item_count": 0, // Not tracked "usage_count": 0, // Not tracked in local "is_active": true, "created_at": lpl.CreatedAt, "synced_from": "local", } } c.JSON(http.StatusOK, gin.H{ "pricelists": summaries, "total": len(summaries), "page": page, "per_page": perPage, "offline": true, }) return } } c.JSON(http.StatusOK, gin.H{ "pricelists": pricelists, "total": total, "page": page, "per_page": perPage, }) } // Get returns a single pricelist by ID func (h *PricelistHandler) Get(c *gin.Context) { idStr := c.Param("id") id, err := strconv.ParseUint(idStr, 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) } // Create creates a new pricelist from current prices 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 } // Get the database username as the creator createdBy := h.localDB.GetDBUser() if createdBy == "" { createdBy = "unknown" } pl, err := h.service.CreateFromCurrentPrices(createdBy) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, pl) } // CreateWithProgress creates a pricelist and streams progress updates over SSE. 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 } createdBy := h.localDB.GetDBUser() if createdBy == "" { createdBy = "unknown" } c.Header("Content-Type", "text/event-stream") c.Header("Cache-Control", "no-cache") c.Header("Connection", "keep-alive") c.Header("X-Accel-Buffering", "no") flusher, ok := c.Writer.(http.Flusher) if !ok { pl, err := h.service.CreateFromCurrentPrices(createdBy) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, pl) return } sendProgress := func(payload gin.H) { c.SSEvent("progress", payload) flusher.Flush() } sendProgress(gin.H{"current": 0, "total": 4, "status": "starting", "message": "Запуск..."}) pl, err := h.service.CreateFromCurrentPricesWithProgress(createdBy, func(p pricelist.CreateProgress) { sendProgress(gin.H{ "current": p.Current, "total": p.Total, "status": p.Status, "message": p.Message, "updated": p.Updated, "errors": p.Errors, "lot_name": p.LotName, }) }) if err != nil { sendProgress(gin.H{ "current": 0, "total": 4, "status": "error", "message": fmt.Sprintf("Ошибка: %v", err), }) return } sendProgress(gin.H{ "current": 4, "total": 4, "status": "completed", "message": "Готово", "pricelist": pl, }) } // Delete deletes a pricelist by ID 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 } idStr := c.Param("id") id, err := strconv.ParseUint(idStr, 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"}) } // SetActive toggles active flag on a pricelist. 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 } idStr := c.Param("id") id, err := strconv.ParseUint(idStr, 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}) } // GetItems returns items for a pricelist with pagination func (h *PricelistHandler) GetItems(c *gin.Context) { idStr := c.Param("id") id, err := strconv.ParseUint(idStr, 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, }) } // CanWrite returns whether the current user can create pricelists func (h *PricelistHandler) CanWrite(c *gin.Context) { canWrite, debugInfo := h.service.CanWriteDebug() c.JSON(http.StatusOK, gin.H{"can_write": canWrite, "debug": debugInfo}) } // GetLatest returns the most recent active pricelist func (h *PricelistHandler) GetLatest(c *gin.Context) { // Try to get from server first pl, err := h.service.GetLatestActive() if err != nil { // If offline or no server pricelists, try to get from local cache if h.localDB == nil { c.JSON(http.StatusNotFound, gin.H{"error": "no database available"}) return } localPL, localErr := h.localDB.GetLatestLocalPricelist() if localErr != nil { // No local pricelists either c.JSON(http.StatusNotFound, gin.H{ "error": "no pricelists available", "local_error": localErr.Error(), }) return } // Return local pricelist c.JSON(http.StatusOK, gin.H{ "id": localPL.ServerID, "version": localPL.Version, "created_by": "sync", "item_count": 0, // Not tracked in local pricelists "is_active": true, "created_at": localPL.CreatedAt, "synced_from": "local", }) return } c.JSON(http.StatusOK, pl) }