ui: add B2B olmayan stok (orphans) page

This commit is contained in:
M_Kececi
2026-06-24 23:27:42 +03:00
parent aa100973b3
commit 3095c06cbf
2 changed files with 127 additions and 80 deletions

View File

@@ -1149,13 +1149,34 @@ func productSeriesResolvePGVariant(ctx context.Context, pg *sql.DB, productCode,
}
return 0, 0, sql.NullInt64{}, false, err
}
dim1ID, ok, err := productSeriesResolveDimTokenID(ctx, pg, "dimval1", colorCode, mmitemID)
if err != nil {
// Authoritative dim1 resolver: for this installation, "color_code" tokens correspond to dfgrp.code on
// the dim values referenced by mmitem_dim.val1. Do NOT rely on mk_dim_token_map for dimval1 because
// it can be polluted and conflate tokens.
var dim1ID int64
{
tok := strings.ToUpper(strings.TrimSpace(colorCode))
if tok == "" || tok == "0" {
return 0, 0, sql.NullInt64{}, false, nil
}
if err := pg.QueryRowContext(ctx, `
SELECT md.val1
FROM mmitem_dim md
JOIN dfgrp d ON d.id = md.val1
WHERE md.mmitem_id = $1
AND md.is_active = TRUE
AND UPPER(BTRIM(d.code)) = $2
GROUP BY md.val1
LIMIT 1
`, mmitemID, tok).Scan(&dim1ID); err != nil {
if err == sql.ErrNoRows {
return 0, 0, sql.NullInt64{}, false, nil
}
return 0, 0, sql.NullInt64{}, false, err
}
if !ok || dim1ID <= 0 {
if dim1ID <= 0 {
return 0, 0, sql.NullInt64{}, false, nil
}
}
var dim3ID sql.NullInt64
if strings.TrimSpace(dim3Code) != "" {
id, ok, err := productSeriesResolveDimTokenID(ctx, pg, "dimval3", dim3Code, mmitemID)

View File

@@ -318,42 +318,10 @@ WHERE dim_column=$1 AND token = ANY($2)
return out, rows.Err()
}
// Load authoritative combos from mmitem_dim (mmitem_id + val1(color) + val3(dim3 if any)).
// We do NOT rely on mk_mmitem_dim_combo here because that cache may be empty/stale.
loadProductDimCombos := func(ctx context.Context, pg *sql.DB, mmitemIDs []int64) (map[string]struct{}, error) {
out := map[string]struct{}{}
if len(mmitemIDs) == 0 {
return out, nil
}
rows, err := pg.QueryContext(ctx, `
SELECT
mmitem_id,
val1 AS dim1,
CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 ELSE 0 END AS dim3_key
FROM mmitem_dim
WHERE mmitem_id = ANY($1)
AND is_active = TRUE
AND val1 IS NOT NULL
GROUP BY mmitem_id, val1, CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 ELSE 0 END
`, pq.Array(mmitemIDs))
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var mmitemID, dim1, dim3Key int64
if err := rows.Scan(&mmitemID, &dim1, &dim3Key); err != nil {
return out, err
}
key := fmt.Sprintf("%d|%d|%d", mmitemID, dim1, dim3Key)
out[key] = struct{}{}
}
return out, rows.Err()
}
// Authoritative dims/combos are resolved via mmitem_dim + dfgrp.code (see helpers below).
codes := setToSortedSlice(codeSet)
mmitemByCode, _ := loadMmitemIDs(ctx, pg, codes)
dim1ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval1", setToSortedSlice(colorSet))
dim3ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval3", setToSortedSlice(dim3Set))
mmitemIDs := make([]int64, 0, len(mmitemByCode))
for _, c := range codes {
@@ -361,7 +329,13 @@ GROUP BY mmitem_id, val1, CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 E
mmitemIDs = append(mmitemIDs, id)
}
}
combos, _ := loadProductDimCombos(ctx, pg, mmitemIDs)
// Normalize color tokens to UPPER/TRIM to match dfgrp.code.
colorTokens := setToSortedSlice(colorSet)
for i := range colorTokens {
colorTokens[i] = strings.ToUpper(strings.TrimSpace(colorTokens[i]))
}
dim1ByMmitemToken, _ := loadDim1ByMmitemAndToken(ctx, pg, mmitemIDs, colorTokens)
combos, _ := loadProductDimCombosFromMmitemDim(ctx, pg, mmitemIDs)
existing, _ := loadProductSeriesAssignments(ctx, pg, codes)
// Per-request cache for per-mmitem dimval3 inference to avoid repeated dfblob scans.
@@ -425,7 +399,7 @@ LIMIT 1
out := make([]productSeriesMappingRow, 0, len(grouped))
for _, row := range grouped {
row.MmitemID = mmitemByCode[row.ProductCode]
row.Dim1ID = dim1ByToken[row.ColorCode]
row.Dim1ID = dim1ByMmitemToken[fmt.Sprintf("%d|%s", row.MmitemID, strings.ToUpper(strings.TrimSpace(row.ColorCode)))]
if row.Dim3Code != "" {
// Prefer token map; fallback to per-item inference (do not persist).
if v := dim3ByToken[row.Dim3Code]; v > 0 {
@@ -621,40 +595,8 @@ WHERE dim_column=$1 AND token = ANY($2)
return out, rows.Err()
}
loadProductDimCombos := func(ctx context.Context, pg *sql.DB, mmitemIDs []int64) (map[string]struct{}, error) {
out := map[string]struct{}{}
if len(mmitemIDs) == 0 {
return out, nil
}
rows, err := pg.QueryContext(ctx, `
SELECT
mmitem_id,
val1 AS dim1,
CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 ELSE 0 END AS dim3_key
FROM mmitem_dim
WHERE mmitem_id = ANY($1)
AND is_active = TRUE
AND val1 IS NOT NULL
GROUP BY mmitem_id, val1, CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 ELSE 0 END
`, pq.Array(mmitemIDs))
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var mmitemID, dim1, dim3Key int64
if err := rows.Scan(&mmitemID, &dim1, &dim3Key); err != nil {
return out, err
}
key := fmt.Sprintf("%d|%d|%d", mmitemID, dim1, dim3Key)
out[key] = struct{}{}
}
return out, rows.Err()
}
codes := setToSortedSlice(codeSet)
mmitemByCode, _ := loadMmitemIDs(ctx, pg, codes)
dim1ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval1", setToSortedSlice(colorSet))
dim3ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval3", setToSortedSlice(dim3Set))
mmitemIDs := make([]int64, 0, len(mmitemByCode))
for _, c := range codes {
@@ -662,7 +604,12 @@ GROUP BY mmitem_id, val1, CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 E
mmitemIDs = append(mmitemIDs, id)
}
}
combos, _ := loadProductDimCombos(ctx, pg, mmitemIDs)
colorTokens := setToSortedSlice(colorSet)
for i := range colorTokens {
colorTokens[i] = strings.ToUpper(strings.TrimSpace(colorTokens[i]))
}
dim1ByMmitemToken, _ := loadDim1ByMmitemAndToken(ctx, pg, mmitemIDs, colorTokens)
combos, _ := loadProductDimCombosFromMmitemDim(ctx, pg, mmitemIDs)
// Per-request cache for per-mmitem dimval3 inference (dfblob scan).
inferCache := map[string]int64{}
@@ -728,7 +675,7 @@ LIMIT 1
continue
}
row.MmitemID = mmitemByCode[row.ProductCode]
row.Dim1ID = dim1ByToken[row.ColorCode]
row.Dim1ID = dim1ByMmitemToken[fmt.Sprintf("%d|%s", row.MmitemID, strings.ToUpper(strings.TrimSpace(row.ColorCode)))]
if row.Dim3Code != "" {
if v := dim3ByToken[row.Dim3Code]; v > 0 {
row.Dim3ID = v
@@ -750,7 +697,7 @@ LIMIT 1
case row.MmitemID <= 0:
row.MappingWarning = "B2B'de urun yok (mmitem)"
case row.Dim1ID <= 0:
row.MappingWarning = "B2B'de renk token eslesmesi yok (mk_dim_token_map: dimval1)"
row.MappingWarning = "B2B'de renk bu urunde yok (mmitem_dim/dfgrp.code)"
case row.Dim3Code != "" && row.Dim3ID <= 0:
row.MappingWarning = "B2B'de dim3 token eslesmesi yok (mk_dim_token_map: dimval3)"
case !comboOK:
@@ -798,6 +745,68 @@ func productSeriesTotalQtyByCode(rows []productSeriesMappingRow) map[string]floa
return out
}
func loadDim1ByMmitemAndToken(ctx context.Context, pg *sql.DB, mmitemIDs []int64, colorTokens []string) (map[string]int64, error) {
out := map[string]int64{}
if len(mmitemIDs) == 0 || len(colorTokens) == 0 {
return out, nil
}
rows, err := pg.QueryContext(ctx, `
SELECT md.mmitem_id, UPPER(BTRIM(d.code)) AS code, md.val1
FROM mmitem_dim md
JOIN dfgrp d ON d.id = md.val1
WHERE md.mmitem_id = ANY($1)
AND md.is_active = TRUE
AND UPPER(BTRIM(d.code)) = ANY($2)
GROUP BY md.mmitem_id, UPPER(BTRIM(d.code)), md.val1
`, pq.Array(mmitemIDs), pq.Array(colorTokens))
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var mmid, dim1 int64
var code string
if err := rows.Scan(&mmid, &code, &dim1); err != nil {
return out, err
}
key := fmt.Sprintf("%d|%s", mmid, strings.TrimSpace(code))
if _, ok := out[key]; !ok && dim1 > 0 {
out[key] = dim1
}
}
return out, rows.Err()
}
func loadProductDimCombosFromMmitemDim(ctx context.Context, pg *sql.DB, mmitemIDs []int64) (map[string]struct{}, error) {
out := map[string]struct{}{}
if len(mmitemIDs) == 0 {
return out, nil
}
rows, err := pg.QueryContext(ctx, `
SELECT
mmitem_id,
val1 AS dim1,
CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 ELSE 0 END AS dim3_key
FROM mmitem_dim
WHERE mmitem_id = ANY($1)
AND is_active = TRUE
AND val1 IS NOT NULL
GROUP BY mmitem_id, val1, CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 ELSE 0 END
`, pq.Array(mmitemIDs))
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var mmitemID, dim1, dim3Key int64
if err := rows.Scan(&mmitemID, &dim1, &dim3Key); err != nil {
return out, err
}
out[fmt.Sprintf("%d|%d|%d", mmitemID, dim1, dim3Key)] = struct{}{}
}
return out, rows.Err()
}
type saveProductSeriesMappingsRequest struct {
Items []struct {
ProductCode string `json:"product_code"`
@@ -855,11 +864,28 @@ func PostProductSeriesMappingsSaveHandler(pg *sql.DB) http.HandlerFunc {
http.Error(w, "PG urun bulunamadi: "+code, http.StatusBadRequest)
return
}
dim1ID, err := resolveDimTokenIDTx(ctx, tx, "dimval1", color)
if err != nil || dim1ID <= 0 {
// Authoritative dim1 resolver: only allow saving against a color that exists for this product in mmitem_dim.
var dim1ID int64
{
tok := strings.ToUpper(strings.TrimSpace(color))
if tok == "" || tok == "0" {
http.Error(w, "Renk token eslesmesi bulunamadi: "+color, http.StatusBadRequest)
return
}
if err := tx.QueryRowContext(ctx, `
SELECT md.val1
FROM mmitem_dim md
JOIN dfgrp d ON d.id = md.val1
WHERE md.mmitem_id = $1
AND md.is_active = TRUE
AND UPPER(BTRIM(d.code)) = $2
GROUP BY md.val1
LIMIT 1
`, mmitemID, tok).Scan(&dim1ID); err != nil || dim1ID <= 0 {
http.Error(w, "Renk bu urunde yok: "+color, http.StatusBadRequest)
return
}
}
var dim3ID sql.NullInt64
if dim3Token != "" {
id, err := resolveDimTokenIDTx(ctx, tx, "dimval3", dim3Token)