package handlers import ( "net/http" "sort" "strconv" "strings" "git.mchus.pro/mchus/quoteforge/internal/localdb" "git.mchus.pro/mchus/quoteforge/internal/models" "github.com/gin-gonic/gin" ) type PricelistHandler struct { localDB *localdb.LocalDB } func NewPricelistHandler(localDB *localdb.LocalDB) *PricelistHandler { return &PricelistHandler{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")) if page < 1 { page = 1 } if perPage < 1 { perPage = 20 } source := c.Query("source") activeOnly := c.DefaultQuery("active_only", "false") == "true" localPLs, err := h.localDB.GetLocalPricelists() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if source != "" { filtered := localPLs[:0] for _, lpl := range localPLs { if strings.EqualFold(lpl.Source, source) { filtered = append(filtered, lpl) } } localPLs = filtered } type pricelistWithCount struct { pricelist localdb.LocalPricelist itemCount int64 usageCount int } withCounts := make([]pricelistWithCount, 0, len(localPLs)) for _, lpl := range localPLs { itemCount := h.localDB.CountLocalPricelistItems(lpl.ID) if activeOnly && itemCount == 0 { continue } usageCount := 0 if lpl.IsUsed { usageCount = 1 } withCounts = append(withCounts, pricelistWithCount{ pricelist: lpl, itemCount: itemCount, usageCount: usageCount, }) } localPLs = localPLs[:0] for _, row := range withCounts { localPLs = append(localPLs, row.pricelist) } sort.SliceStable(localPLs, func(i, j int) bool { return localPLs[i].CreatedAt.After(localPLs[j].CreatedAt) }) total := len(localPLs) start := (page - 1) * perPage if start > total { start = total } end := start + perPage if end > total { end = total } pageSlice := localPLs[start:end] summaries := make([]map[string]interface{}, 0, len(pageSlice)) for _, lpl := range pageSlice { itemCount := int64(0) usageCount := 0 for _, row := range withCounts { if row.pricelist.ID == lpl.ID { itemCount = row.itemCount usageCount = row.usageCount break } } summaries = append(summaries, map[string]interface{}{ "id": lpl.ServerID, "source": lpl.Source, "version": lpl.Version, "created_by": "sync", "item_count": itemCount, "usage_count": usageCount, "is_active": true, "created_at": lpl.CreatedAt, "synced_from": "local", }) } c.JSON(http.StatusOK, gin.H{ "pricelists": summaries, "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 } localPL, err := h.localDB.GetLocalPricelistByServerID(uint(id)) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "pricelist not found"}) return } c.JSON(http.StatusOK, gin.H{ "id": localPL.ServerID, "source": localPL.Source, "version": localPL.Version, "created_by": "sync", "item_count": h.localDB.CountLocalPricelistItems(localPL.ID), "is_active": true, "created_at": localPL.CreatedAt, "synced_from": "local", }) } // 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") localPL, err := h.localDB.GetLocalPricelistByServerID(uint(id)) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "pricelist not found"}) return } if page < 1 { page = 1 } if perPage < 1 { perPage = 50 } var items []localdb.LocalPricelistItem dbq := h.localDB.DB().Model(&localdb.LocalPricelistItem{}).Where("pricelist_id = ?", localPL.ID) if strings.TrimSpace(search) != "" { dbq = dbq.Where("lot_name LIKE ?", "%"+strings.TrimSpace(search)+"%") } var total int64 if err := dbq.Count(&total).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } offset := (page - 1) * perPage if err := dbq.Order("lot_name").Offset(offset).Limit(perPage).Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } lotNames := make([]string, len(items)) for i, item := range items { lotNames[i] = item.LotName } type compRow struct { LotName string LotDescription string } var comps []compRow if len(lotNames) > 0 { h.localDB.DB().Table("local_components"). Select("lot_name, lot_description"). Where("lot_name IN ?", lotNames). Scan(&comps) } descMap := make(map[string]string, len(comps)) for _, c := range comps { descMap[c.LotName] = c.LotDescription } resultItems := make([]gin.H, 0, len(items)) for _, item := range items { resultItems = append(resultItems, gin.H{ "id": item.ID, "lot_name": item.LotName, "lot_description": descMap[item.LotName], "price": item.Price, "category": item.LotCategory, "available_qty": item.AvailableQty, "partnumbers": []string(item.Partnumbers), "partnumber_qtys": map[string]interface{}{}, "competitor_names": []string{}, "price_spread_pct": nil, }) } c.JSON(http.StatusOK, gin.H{ "source": localPL.Source, "items": resultItems, "total": total, "page": page, "per_page": perPage, }) } func (h *PricelistHandler) GetLotNames(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 } localPL, err := h.localDB.GetLocalPricelistByServerID(uint(id)) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "pricelist not found"}) return } items, err := h.localDB.GetLocalPricelistItems(localPL.ID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } lotNames := make([]string, 0, len(items)) for _, item := range items { lotNames = append(lotNames, item.LotName) } sort.Strings(lotNames) c.JSON(http.StatusOK, gin.H{ "lot_names": lotNames, "total": len(lotNames), }) } // GetLatest returns the most recent active pricelist. func (h *PricelistHandler) GetLatest(c *gin.Context) { source := c.DefaultQuery("source", string(models.PricelistSourceEstimate)) source = string(models.NormalizePricelistSource(source)) localPL, err := h.localDB.GetLatestLocalPricelistBySource(source) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "no pricelists available"}) return } c.JSON(http.StatusOK, gin.H{ "id": localPL.ServerID, "source": localPL.Source, "version": localPL.Version, "created_by": "sync", "item_count": h.localDB.CountLocalPricelistItems(localPL.ID), "is_active": true, "created_at": localPL.CreatedAt, "synced_from": "local", }) }