280 lines
7.3 KiB
Go
280 lines
7.3 KiB
Go
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",
|
|
})
|
|
}
|