Add stock pricelist admin flow with mapping placeholders and warehouse details
This commit is contained in:
@@ -1,17 +1,20 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/repository"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/services"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/services/alerts"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/services/pricing"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -41,12 +44,14 @@ func calculateAverage(prices []float64) float64 {
|
||||
}
|
||||
|
||||
type PricingHandler struct {
|
||||
db *gorm.DB
|
||||
pricingService *pricing.Service
|
||||
alertService *alerts.Service
|
||||
componentRepo *repository.ComponentRepository
|
||||
priceRepo *repository.PriceRepository
|
||||
statsRepo *repository.StatsRepository
|
||||
db *gorm.DB
|
||||
pricingService *pricing.Service
|
||||
alertService *alerts.Service
|
||||
componentRepo *repository.ComponentRepository
|
||||
priceRepo *repository.PriceRepository
|
||||
statsRepo *repository.StatsRepository
|
||||
stockImportService *services.StockImportService
|
||||
dbUsername string
|
||||
}
|
||||
|
||||
func NewPricingHandler(
|
||||
@@ -56,14 +61,18 @@ func NewPricingHandler(
|
||||
componentRepo *repository.ComponentRepository,
|
||||
priceRepo *repository.PriceRepository,
|
||||
statsRepo *repository.StatsRepository,
|
||||
stockImportService *services.StockImportService,
|
||||
dbUsername string,
|
||||
) *PricingHandler {
|
||||
return &PricingHandler{
|
||||
db: db,
|
||||
pricingService: pricingService,
|
||||
alertService: alertService,
|
||||
componentRepo: componentRepo,
|
||||
priceRepo: priceRepo,
|
||||
statsRepo: statsRepo,
|
||||
db: db,
|
||||
pricingService: pricingService,
|
||||
alertService: alertService,
|
||||
componentRepo: componentRepo,
|
||||
priceRepo: priceRepo,
|
||||
statsRepo: statsRepo,
|
||||
stockImportService: stockImportService,
|
||||
dbUsername: dbUsername,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -936,3 +945,167 @@ func expandMetaPricesWithCache(metaPrices, excludeLot string, allLotNames []stri
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *PricingHandler) ImportStockLog(c *gin.Context) {
|
||||
if h.stockImportService == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "Импорт склада доступен только в онлайн режиме",
|
||||
"offline": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
fileHeader, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "file is required"})
|
||||
return
|
||||
}
|
||||
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to open uploaded file"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read uploaded file"})
|
||||
return
|
||||
}
|
||||
modTime := time.Now()
|
||||
if statter, ok := file.(interface{ Stat() (os.FileInfo, error) }); ok {
|
||||
if st, statErr := statter.Stat(); statErr == nil {
|
||||
modTime = st.ModTime()
|
||||
}
|
||||
}
|
||||
|
||||
flusher, ok := c.Writer.(http.Flusher)
|
||||
if !ok {
|
||||
result, impErr := h.stockImportService.Import(fileHeader.Filename, content, modTime, h.dbUsername, nil)
|
||||
if impErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": impErr.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "completed",
|
||||
"rows_total": result.RowsTotal,
|
||||
"valid_rows": result.ValidRows,
|
||||
"inserted": result.Inserted,
|
||||
"deleted": result.Deleted,
|
||||
"unmapped": result.Unmapped,
|
||||
"conflicts": result.Conflicts,
|
||||
"fallback_matches": result.FallbackMatches,
|
||||
"parse_errors": result.ParseErrors,
|
||||
"import_date": result.ImportDate.Format("2006-01-02"),
|
||||
"warehouse_pricelist_id": result.WarehousePLID,
|
||||
"warehouse_pricelist_version": result.WarehousePLVer,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Header("X-Accel-Buffering", "no")
|
||||
|
||||
send := func(p gin.H) {
|
||||
c.SSEvent("progress", p)
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
send(gin.H{"status": "starting", "message": "Запуск импорта"})
|
||||
_, impErr := h.stockImportService.Import(fileHeader.Filename, content, modTime, h.dbUsername, func(p services.StockImportProgress) {
|
||||
send(gin.H{
|
||||
"status": p.Status,
|
||||
"message": p.Message,
|
||||
"current": p.Current,
|
||||
"total": p.Total,
|
||||
"rows_total": p.RowsTotal,
|
||||
"valid_rows": p.ValidRows,
|
||||
"inserted": p.Inserted,
|
||||
"deleted": p.Deleted,
|
||||
"unmapped": p.Unmapped,
|
||||
"conflicts": p.Conflicts,
|
||||
"fallback_matches": p.FallbackMatches,
|
||||
"parse_errors": p.ParseErrors,
|
||||
"import_date": p.ImportDate,
|
||||
"warehouse_pricelist_id": p.PricelistID,
|
||||
"warehouse_pricelist_version": p.PricelistVer,
|
||||
})
|
||||
})
|
||||
if impErr != nil {
|
||||
send(gin.H{"status": "error", "message": impErr.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PricingHandler) ListStockMappings(c *gin.Context) {
|
||||
if h.stockImportService == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "Сопоставления доступны только в онлайн режиме",
|
||||
"offline": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "50"))
|
||||
search := c.Query("search")
|
||||
|
||||
rows, total, err := h.stockImportService.ListMappings(page, perPage, search)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"items": rows,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"per_page": perPage,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *PricingHandler) UpsertStockMapping(c *gin.Context) {
|
||||
if h.stockImportService == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "Сопоставления доступны только в онлайн режиме",
|
||||
"offline": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Partnumber string `json:"partnumber" binding:"required"`
|
||||
LotName string `json:"lot_name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.stockImportService.UpsertMapping(req.Partnumber, req.LotName, req.Description); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "mapping saved"})
|
||||
}
|
||||
|
||||
func (h *PricingHandler) DeleteStockMapping(c *gin.Context) {
|
||||
if h.stockImportService == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "Сопоставления доступны только в онлайн режиме",
|
||||
"offline": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
partnumber := c.Param("partnumber")
|
||||
deleted, err := h.stockImportService.DeleteMapping(partnumber)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"deleted": deleted})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user