- Migration 029: local_partnumber_books, local_partnumber_book_items, vendor_spec TEXT column on local_configurations - Models: LocalPartnumberBook, LocalPartnumberBookItem, VendorSpec, VendorSpecItem with JSON Valuer/Scanner - Repository: PartnumberBookRepository (GetActiveBook, FindLotByPartnumber, SaveBook/Items, ListBooks, CountBookItems) - Service: VendorSpecResolver 3-step resolution (book → manual suggestion → unresolved) + AggregateLOTs with is_primary_pn qty logic - Sync: PullPartnumberBooks append-only pull from qt_partnumber_books - Handlers: VendorSpecHandler (GET/PUT/resolve/apply), PartnumberBooksHandler - Routes: /api/configs/:uuid/vendor-spec*, /api/partnumber-books, /api/sync/partnumber-books, /partnumber-books page - UI: 3 top-level tabs [Estimate][BOM вендора][Ценообразование]; Excel paste, PN resolution, inline LOT autocomplete, pricing table - Bible: 03-database.md updated, 09-vendor-spec.md added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
167 lines
4.9 KiB
Go
167 lines
4.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
|
"git.mchus.pro/mchus/quoteforge/internal/middleware"
|
|
"git.mchus.pro/mchus/quoteforge/internal/repository"
|
|
"git.mchus.pro/mchus/quoteforge/internal/services"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// VendorSpecHandler handles vendor BOM spec operations for a configuration.
|
|
type VendorSpecHandler struct {
|
|
localDB *localdb.LocalDB
|
|
}
|
|
|
|
func NewVendorSpecHandler(localDB *localdb.LocalDB) *VendorSpecHandler {
|
|
return &VendorSpecHandler{localDB: localDB}
|
|
}
|
|
|
|
// GetVendorSpec returns the vendor spec (BOM) for a configuration.
|
|
// GET /api/configs/:uuid/vendor-spec
|
|
func (h *VendorSpecHandler) GetVendorSpec(c *gin.Context) {
|
|
uuid := c.Param("uuid")
|
|
username := middleware.GetUsername(c)
|
|
|
|
var cfg localdb.LocalConfiguration
|
|
if err := h.localDB.DB().Where("uuid = ? AND original_username = ? AND is_active = 1", uuid, username).First(&cfg).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "configuration not found"})
|
|
return
|
|
}
|
|
|
|
spec := cfg.VendorSpec
|
|
if spec == nil {
|
|
spec = localdb.VendorSpec{}
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"vendor_spec": spec})
|
|
}
|
|
|
|
// PutVendorSpec saves (replaces) the vendor spec for a configuration.
|
|
// PUT /api/configs/:uuid/vendor-spec
|
|
func (h *VendorSpecHandler) PutVendorSpec(c *gin.Context) {
|
|
uuid := c.Param("uuid")
|
|
username := middleware.GetUsername(c)
|
|
|
|
var body struct {
|
|
VendorSpec []localdb.VendorSpecItem `json:"vendor_spec"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
var cfg localdb.LocalConfiguration
|
|
if err := h.localDB.DB().Where("uuid = ? AND original_username = ? AND is_active = 1", uuid, username).First(&cfg).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "configuration not found"})
|
|
return
|
|
}
|
|
|
|
// Assign sort_order if not set
|
|
for i := range body.VendorSpec {
|
|
if body.VendorSpec[i].SortOrder == 0 {
|
|
body.VendorSpec[i].SortOrder = (i + 1) * 10
|
|
}
|
|
}
|
|
|
|
spec := localdb.VendorSpec(body.VendorSpec)
|
|
if err := h.localDB.DB().Model(&cfg).Update("vendor_spec", spec).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"vendor_spec": spec})
|
|
}
|
|
|
|
// ResolveVendorSpec resolves vendor PN → LOT without modifying the cart.
|
|
// POST /api/configs/:uuid/vendor-spec/resolve
|
|
func (h *VendorSpecHandler) ResolveVendorSpec(c *gin.Context) {
|
|
uuid := c.Param("uuid")
|
|
username := middleware.GetUsername(c)
|
|
|
|
var cfg localdb.LocalConfiguration
|
|
if err := h.localDB.DB().Where("uuid = ? AND original_username = ? AND is_active = 1", uuid, username).First(&cfg).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "configuration not found"})
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
VendorSpec []localdb.VendorSpecItem `json:"vendor_spec"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
bookRepo := repository.NewPartnumberBookRepository(h.localDB.DB())
|
|
resolver := services.NewVendorSpecResolver(bookRepo)
|
|
|
|
resolved, err := resolver.Resolve(body.VendorSpec)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Also compute aggregated LOTs
|
|
book, _ := bookRepo.GetActiveBook()
|
|
aggregated, err := services.AggregateLOTs(resolved, book, bookRepo)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"resolved": resolved,
|
|
"aggregated": aggregated,
|
|
})
|
|
}
|
|
|
|
// ApplyVendorSpec applies the resolved BOM to the cart (Estimate items).
|
|
// POST /api/configs/:uuid/vendor-spec/apply
|
|
func (h *VendorSpecHandler) ApplyVendorSpec(c *gin.Context) {
|
|
uuid := c.Param("uuid")
|
|
username := middleware.GetUsername(c)
|
|
|
|
var body struct {
|
|
Items []struct {
|
|
LotName string `json:"lot_name"`
|
|
Quantity int `json:"quantity"`
|
|
UnitPrice float64 `json:"unit_price"`
|
|
} `json:"items"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
var cfg localdb.LocalConfiguration
|
|
if err := h.localDB.DB().Where("uuid = ? AND original_username = ? AND is_active = 1", uuid, username).First(&cfg).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "configuration not found"})
|
|
return
|
|
}
|
|
|
|
newItems := make(localdb.LocalConfigItems, 0, len(body.Items))
|
|
for _, it := range body.Items {
|
|
newItems = append(newItems, localdb.LocalConfigItem{
|
|
LotName: it.LotName,
|
|
Quantity: it.Quantity,
|
|
UnitPrice: it.UnitPrice,
|
|
})
|
|
}
|
|
|
|
itemsJSON, err := json.Marshal(newItems)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := h.localDB.DB().Model(&cfg).Update("items", string(itemsJSON)).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"items": newItems})
|
|
}
|