## Overview Removed the CurrentPrice and SyncedAt fields from local_components, transitioning to a pricelist-based pricing model where all prices are sourced from local_pricelist_items based on the configuration's selected pricelist. ## Changes ### Data Model Updates - **LocalComponent**: Now stores only metadata (LotName, LotDescription, Category, Model) - Removed: CurrentPrice, SyncedAt (both redundant) - Pricing is now exclusively sourced from local_pricelist_items - **LocalConfiguration**: Added pricelist selection fields - Added: WarehousePricelistID, CompetitorPricelistID - These complement the existing PricelistID (Estimate) ### Migrations - Added migration "drop_component_unused_fields" to remove CurrentPrice and SyncedAt columns - Added migration "add_warehouse_competitor_pricelists" to add new pricelist fields ### Component Sync - Removed current_price from MariaDB query - Removed CurrentPrice assignment in component creation - SyncComponentPrices now exclusively updates based on pricelist_items via quote calculation ### Quote Calculation - Added PricelistID field to QuoteRequest - Updated local-first path to use pricelist_items instead of component.CurrentPrice - Falls back to latest estimate pricelist if PricelistID not specified - Maintains offline-first behavior: local queries work without MariaDB ### Configuration Refresh - Removed fallback on component.CurrentPrice - Prices are only refreshed from local_pricelist_items - If price not found in pricelist, original price is preserved ### API Changes - Removed CurrentPrice from ComponentView - Components API no longer returns pricing information - Pricing is accessed via QuoteService or PricelistService ### Code Cleanup - Removed UpdateComponentPricesFromPricelist() method - Removed EnsureComponentPricesFromPricelists() method - Updated UnifiedRepository to remove offline pricing logic - Updated converters to remove CurrentPrice mapping ## Architecture Impact - Components = metadata store only - Prices = managed by pricelist system - Quote calculation = owns all pricing logic - Local-first behavior preserved: SQLite queries work offline, no MariaDB dependency ## Testing - Build successful - All code compiles without errors - Ready for migration testing with existing databases Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
109 lines
2.8 KiB
Go
109 lines
2.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
|
"git.mchus.pro/mchus/quoteforge/internal/models"
|
|
"git.mchus.pro/mchus/quoteforge/internal/repository"
|
|
"git.mchus.pro/mchus/quoteforge/internal/services"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type ComponentHandler struct {
|
|
componentService *services.ComponentService
|
|
localDB *localdb.LocalDB
|
|
}
|
|
|
|
func NewComponentHandler(componentService *services.ComponentService, localDB *localdb.LocalDB) *ComponentHandler {
|
|
return &ComponentHandler{
|
|
componentService: componentService,
|
|
localDB: localDB,
|
|
}
|
|
}
|
|
|
|
func (h *ComponentHandler) 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
|
|
}
|
|
|
|
filter := repository.ComponentFilter{
|
|
Category: c.Query("category"),
|
|
Search: c.Query("search"),
|
|
HasPrice: c.Query("has_price") == "true",
|
|
ExcludeHidden: c.Query("include_hidden") != "true", // По умолчанию скрытые не показываются
|
|
}
|
|
|
|
localFilter := localdb.ComponentFilter{
|
|
Category: filter.Category,
|
|
Search: filter.Search,
|
|
HasPrice: filter.HasPrice,
|
|
}
|
|
offset := (page - 1) * perPage
|
|
localComps, total, err := h.localDB.ListComponents(localFilter, offset, perPage)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
components := make([]services.ComponentView, len(localComps))
|
|
for i, lc := range localComps {
|
|
components[i] = services.ComponentView{
|
|
LotName: lc.LotName,
|
|
Description: lc.LotDescription,
|
|
Category: lc.Category,
|
|
CategoryName: lc.Category,
|
|
Model: lc.Model,
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, &services.ComponentListResult{
|
|
Components: components,
|
|
Total: total,
|
|
Page: page,
|
|
PerPage: perPage,
|
|
})
|
|
}
|
|
|
|
func (h *ComponentHandler) Get(c *gin.Context) {
|
|
lotName := c.Param("lot_name")
|
|
component, err := h.localDB.GetLocalComponent(lotName)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "component not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, services.ComponentView{
|
|
LotName: component.LotName,
|
|
Description: component.LotDescription,
|
|
Category: component.Category,
|
|
CategoryName: component.Category,
|
|
Model: component.Model,
|
|
})
|
|
}
|
|
|
|
func (h *ComponentHandler) GetCategories(c *gin.Context) {
|
|
codes, err := h.localDB.GetLocalComponentCategories()
|
|
if err == nil && len(codes) > 0 {
|
|
categories := make([]models.Category, 0, len(codes))
|
|
for _, code := range codes {
|
|
trimmed := strings.TrimSpace(code)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
categories = append(categories, models.Category{Code: trimmed, Name: trimmed})
|
|
}
|
|
c.JSON(http.StatusOK, categories)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.DefaultCategories)
|
|
}
|