Implement warehouse/lot pricing updates and configurator performance fixes
This commit is contained in:
@@ -45,8 +45,15 @@ func (h *PricelistHandler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// If offline (empty list), fallback to local pricelists
|
||||
if total == 0 && h.localDB != nil {
|
||||
isOffline := false
|
||||
if v, ok := c.Get("is_offline"); ok {
|
||||
if b, ok := v.(bool); ok {
|
||||
isOffline = b
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to local pricelists only in explicit offline mode.
|
||||
if isOffline && total == 0 && h.localDB != nil {
|
||||
localPLs, err := h.localDB.GetLocalPricelists()
|
||||
if err == nil && len(localPLs) > 0 {
|
||||
if source != "" {
|
||||
@@ -338,6 +345,26 @@ func (h *PricelistHandler) GetItems(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
lotNames, err := h.service.GetLotNames(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"lot_names": lotNames,
|
||||
"total": len(lotNames),
|
||||
})
|
||||
}
|
||||
|
||||
// CanWrite returns whether the current user can create pricelists
|
||||
func (h *PricelistHandler) CanWrite(c *gin.Context) {
|
||||
canWrite, debugInfo := h.service.CanWriteDebug()
|
||||
|
||||
@@ -997,6 +997,7 @@ func (h *PricingHandler) ImportStockLog(c *gin.Context) {
|
||||
"conflicts": result.Conflicts,
|
||||
"fallback_matches": result.FallbackMatches,
|
||||
"parse_errors": result.ParseErrors,
|
||||
"qty_parse_errors": result.QtyParseErrors,
|
||||
"ignored": result.Ignored,
|
||||
"mapping_suggestions": result.MappingSuggestions,
|
||||
"import_date": result.ImportDate.Format("2006-01-02"),
|
||||
@@ -1031,6 +1032,7 @@ func (h *PricingHandler) ImportStockLog(c *gin.Context) {
|
||||
"conflicts": p.Conflicts,
|
||||
"fallback_matches": p.FallbackMatches,
|
||||
"parse_errors": p.ParseErrors,
|
||||
"qty_parse_errors": p.QtyParseErrors,
|
||||
"ignored": p.Ignored,
|
||||
"mapping_suggestions": p.MappingSuggestions,
|
||||
"import_date": p.ImportDate,
|
||||
@@ -1183,6 +1185,231 @@ func (h *PricingHandler) DeleteStockIgnoreRule(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"deleted": deleted})
|
||||
}
|
||||
|
||||
type LotTableRow struct {
|
||||
LotName string `json:"lot_name"`
|
||||
LotDescription string `json:"lot_description"`
|
||||
Category string `json:"category"`
|
||||
Partnumbers []string `json:"partnumbers"`
|
||||
Popularity float64 `json:"popularity"`
|
||||
EstimateCount int64 `json:"estimate_count"`
|
||||
StockQty *float64 `json:"stock_qty"`
|
||||
}
|
||||
|
||||
func (h *PricingHandler) ListLotsTable(c *gin.Context) {
|
||||
if h.db == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "Список LOT доступен только в онлайн режиме",
|
||||
"offline": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "50"))
|
||||
search := strings.TrimSpace(c.Query("search"))
|
||||
sortFieldParam := c.DefaultQuery("sort", "lot_name")
|
||||
sortDirParam := strings.ToUpper(c.DefaultQuery("dir", "asc"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if perPage < 1 || perPage > 200 {
|
||||
perPage = 50
|
||||
}
|
||||
if sortDirParam != "ASC" && sortDirParam != "DESC" {
|
||||
sortDirParam = "ASC"
|
||||
}
|
||||
|
||||
type lotRow struct {
|
||||
LotName string `gorm:"column:lot_name"`
|
||||
LotDescription string `gorm:"column:lot_description"`
|
||||
CategoryCode *string `gorm:"column:category_code"`
|
||||
Popularity *float64 `gorm:"column:popularity_score"`
|
||||
}
|
||||
|
||||
baseQuery := h.db.Table("lot").
|
||||
Select("lot.lot_name, lot.lot_description, qt_categories.code as category_code, qt_lot_metadata.popularity_score").
|
||||
Joins("LEFT JOIN qt_lot_metadata ON qt_lot_metadata.lot_name = lot.lot_name").
|
||||
Joins("LEFT JOIN qt_categories ON qt_categories.id = qt_lot_metadata.category_id")
|
||||
|
||||
if search != "" {
|
||||
baseQuery = baseQuery.Where("lot.lot_name LIKE ? OR lot.lot_description LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := baseQuery.Count(&total).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
allowedDBSorts := map[string]string{
|
||||
"lot_name": "lot.lot_name",
|
||||
"category": "qt_categories.code",
|
||||
"popularity_score": "qt_lot_metadata.popularity_score",
|
||||
}
|
||||
needsComputedSort := sortFieldParam == "estimate_count" || sortFieldParam == "stock_qty"
|
||||
|
||||
var rows []lotRow
|
||||
rowsQuery := baseQuery.Session(&gorm.Session{})
|
||||
if needsComputedSort {
|
||||
rowsQuery = rowsQuery.Order("lot.lot_name ASC")
|
||||
} else {
|
||||
orderCol, ok := allowedDBSorts[sortFieldParam]
|
||||
if !ok {
|
||||
orderCol = "lot.lot_name"
|
||||
}
|
||||
rowsQuery = rowsQuery.Order(orderCol + " " + sortDirParam).Order("lot.lot_name ASC")
|
||||
rowsQuery = rowsQuery.Offset((page - 1) * perPage).Limit(perPage)
|
||||
}
|
||||
if err := rowsQuery.Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"lots": []LotTableRow{},
|
||||
"total": total,
|
||||
"page": page,
|
||||
"per_page": perPage,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Collect lot names for batch subqueries
|
||||
lotNames := make([]string, len(rows))
|
||||
for i, r := range rows {
|
||||
lotNames[i] = r.LotName
|
||||
}
|
||||
|
||||
type countRow struct {
|
||||
Lot string `gorm:"column:lot"`
|
||||
Count int64 `gorm:"column:cnt"`
|
||||
}
|
||||
var estimateCounts []countRow
|
||||
if err := h.db.Raw("SELECT lot, COUNT(*) as cnt FROM lot_log WHERE lot IN ? GROUP BY lot", lotNames).Scan(&estimateCounts).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
estimateMap := make(map[string]int64, len(estimateCounts))
|
||||
for _, ec := range estimateCounts {
|
||||
estimateMap[ec.Lot] = ec.Count
|
||||
}
|
||||
|
||||
type stockRow struct {
|
||||
LotName string `gorm:"column:lot_name"`
|
||||
Qty *float64 `gorm:"column:total_qty"`
|
||||
}
|
||||
var stockRows []stockRow
|
||||
if err := h.db.Raw(`
|
||||
SELECT lp.lot_name, SUM(sl.qty) as total_qty
|
||||
FROM stock_log sl
|
||||
INNER JOIN lot_partnumbers lp ON LOWER(TRIM(lp.partnumber)) = LOWER(TRIM(sl.partnumber))
|
||||
INNER JOIN (SELECT MAX(date) as max_date FROM stock_log) md ON sl.date = md.max_date
|
||||
WHERE lp.lot_name IN ?
|
||||
GROUP BY lp.lot_name
|
||||
`, lotNames).Scan(&stockRows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
stockMap := make(map[string]*float64, len(stockRows))
|
||||
for _, sr := range stockRows {
|
||||
qty := sr.Qty
|
||||
stockMap[sr.LotName] = qty
|
||||
}
|
||||
|
||||
type pnRow struct {
|
||||
LotName string `gorm:"column:lot_name"`
|
||||
Partnumber string `gorm:"column:partnumber"`
|
||||
}
|
||||
var pnRows []pnRow
|
||||
if err := h.db.Raw("SELECT lot_name, partnumber FROM lot_partnumbers WHERE lot_name IN ? ORDER BY lot_name, partnumber", lotNames).Scan(&pnRows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
pnMap := make(map[string][]string, len(pnRows))
|
||||
for _, pn := range pnRows {
|
||||
pnMap[pn.LotName] = append(pnMap[pn.LotName], pn.Partnumber)
|
||||
}
|
||||
|
||||
result := make([]LotTableRow, len(rows))
|
||||
for i, r := range rows {
|
||||
cat := ""
|
||||
if r.CategoryCode != nil {
|
||||
cat = *r.CategoryCode
|
||||
}
|
||||
pop := 0.0
|
||||
if r.Popularity != nil {
|
||||
pop = *r.Popularity
|
||||
}
|
||||
result[i] = LotTableRow{
|
||||
LotName: r.LotName,
|
||||
LotDescription: r.LotDescription,
|
||||
Category: cat,
|
||||
Partnumbers: pnMap[r.LotName],
|
||||
Popularity: pop,
|
||||
EstimateCount: estimateMap[r.LotName],
|
||||
StockQty: stockMap[r.LotName],
|
||||
}
|
||||
if result[i].Partnumbers == nil {
|
||||
result[i].Partnumbers = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
if needsComputedSort {
|
||||
sort.SliceStable(result, func(i, j int) bool {
|
||||
if sortFieldParam == "estimate_count" {
|
||||
if result[i].EstimateCount == result[j].EstimateCount {
|
||||
if sortDirParam == "DESC" {
|
||||
return result[i].LotName > result[j].LotName
|
||||
}
|
||||
return result[i].LotName < result[j].LotName
|
||||
}
|
||||
if sortDirParam == "DESC" {
|
||||
return result[i].EstimateCount > result[j].EstimateCount
|
||||
}
|
||||
return result[i].EstimateCount < result[j].EstimateCount
|
||||
}
|
||||
qi := 0.0
|
||||
if result[i].StockQty != nil {
|
||||
qi = *result[i].StockQty
|
||||
}
|
||||
qj := 0.0
|
||||
if result[j].StockQty != nil {
|
||||
qj = *result[j].StockQty
|
||||
}
|
||||
if qi == qj {
|
||||
if sortDirParam == "DESC" {
|
||||
return result[i].LotName > result[j].LotName
|
||||
}
|
||||
return result[i].LotName < result[j].LotName
|
||||
}
|
||||
if sortDirParam == "DESC" {
|
||||
return qi > qj
|
||||
}
|
||||
return qi < qj
|
||||
})
|
||||
|
||||
start := (page - 1) * perPage
|
||||
if start >= len(result) {
|
||||
result = []LotTableRow{}
|
||||
} else {
|
||||
end := start + perPage
|
||||
if end > len(result) {
|
||||
end = len(result)
|
||||
}
|
||||
result = result[start:end]
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"lots": result,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"per_page": perPage,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *PricingHandler) ListLots(c *gin.Context) {
|
||||
if h.db == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@@ -364,10 +365,27 @@ func (h *SyncHandler) GetPendingChanges(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// SyncInfoResponse represents sync information
|
||||
// SyncInfoResponse represents sync information for the modal
|
||||
type SyncInfoResponse struct {
|
||||
LastSyncAt *time.Time `json:"last_sync_at"`
|
||||
IsOnline bool `json:"is_online"`
|
||||
// Connection
|
||||
DBHost string `json:"db_host"`
|
||||
DBUser string `json:"db_user"`
|
||||
DBName string `json:"db_name"`
|
||||
|
||||
// Status
|
||||
IsOnline bool `json:"is_online"`
|
||||
LastSyncAt *time.Time `json:"last_sync_at"`
|
||||
|
||||
// Statistics
|
||||
LotCount int64 `json:"lot_count"`
|
||||
LotLogCount int64 `json:"lot_log_count"`
|
||||
ConfigCount int64 `json:"config_count"`
|
||||
ProjectCount int64 `json:"project_count"`
|
||||
|
||||
// Pending changes
|
||||
PendingChanges []localdb.PendingChange `json:"pending_changes"`
|
||||
|
||||
// Errors
|
||||
ErrorCount int `json:"error_count"`
|
||||
Errors []SyncError `json:"errors,omitempty"`
|
||||
}
|
||||
@@ -392,31 +410,44 @@ func (h *SyncHandler) GetInfo(c *gin.Context) {
|
||||
// Check online status by pinging MariaDB
|
||||
isOnline := h.checkOnline()
|
||||
|
||||
// Get DB connection info
|
||||
var dbHost, dbUser, dbName string
|
||||
if settings, err := h.localDB.GetSettings(); err == nil {
|
||||
dbHost = settings.Host + ":" + fmt.Sprintf("%d", settings.Port)
|
||||
dbUser = settings.User
|
||||
dbName = settings.Database
|
||||
}
|
||||
|
||||
// Get sync times
|
||||
lastPricelistSync := h.localDB.GetLastSyncTime()
|
||||
|
||||
// Get MariaDB counts (if online)
|
||||
var lotCount, lotLogCount int64
|
||||
if isOnline {
|
||||
if mariaDB, err := h.connMgr.GetDB(); err == nil {
|
||||
mariaDB.Table("lot").Count(&lotCount)
|
||||
mariaDB.Table("lot_log").Count(&lotLogCount)
|
||||
}
|
||||
}
|
||||
|
||||
// Get local counts
|
||||
configCount := h.localDB.CountConfigurations()
|
||||
projectCount := h.localDB.CountProjects()
|
||||
|
||||
// Get error count (only changes with LastError != "")
|
||||
errorCount := int(h.localDB.CountErroredChanges())
|
||||
|
||||
// Get recent errors (last 10)
|
||||
// Get pending changes
|
||||
changes, err := h.localDB.GetPendingChanges()
|
||||
if err != nil {
|
||||
slog.Error("failed to get pending changes for sync info", "error", err)
|
||||
// Even if we can't get changes, we can still return the error count
|
||||
c.JSON(http.StatusOK, SyncInfoResponse{
|
||||
LastSyncAt: lastPricelistSync,
|
||||
IsOnline: isOnline,
|
||||
ErrorCount: errorCount,
|
||||
Errors: []SyncError{}, // Return empty errors list
|
||||
})
|
||||
return
|
||||
changes = []localdb.PendingChange{}
|
||||
}
|
||||
|
||||
var errors []SyncError
|
||||
var syncErrors []SyncError
|
||||
for _, change := range changes {
|
||||
// Check if there's a last error and it's not empty
|
||||
if change.LastError != "" {
|
||||
errors = append(errors, SyncError{
|
||||
syncErrors = append(syncErrors, SyncError{
|
||||
Timestamp: change.CreatedAt,
|
||||
Message: change.LastError,
|
||||
})
|
||||
@@ -424,15 +455,23 @@ func (h *SyncHandler) GetInfo(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Limit to last 10 errors
|
||||
if len(errors) > 10 {
|
||||
errors = errors[:10]
|
||||
if len(syncErrors) > 10 {
|
||||
syncErrors = syncErrors[:10]
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, SyncInfoResponse{
|
||||
LastSyncAt: lastPricelistSync,
|
||||
IsOnline: isOnline,
|
||||
ErrorCount: errorCount,
|
||||
Errors: errors,
|
||||
DBHost: dbHost,
|
||||
DBUser: dbUser,
|
||||
DBName: dbName,
|
||||
IsOnline: isOnline,
|
||||
LastSyncAt: lastPricelistSync,
|
||||
LotCount: lotCount,
|
||||
LotLogCount: lotLogCount,
|
||||
ConfigCount: configCount,
|
||||
ProjectCount: projectCount,
|
||||
PendingChanges: changes,
|
||||
ErrorCount: errorCount,
|
||||
Errors: syncErrors,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user