Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -292,86 +292,91 @@ func GetProductSeriesMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
||||
defByID[d.ID] = d
|
||||
}
|
||||
|
||||
// Strict token->dim_id resolver (no inference/persistence). UI should not invent dim mappings.
|
||||
loadDimTokenIDsStrict := func(ctx context.Context, pg *sql.DB, column string, tokens []string) (map[string]int64, error) {
|
||||
out := map[string]int64{}
|
||||
if len(tokens) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
rows, err := pg.QueryContext(ctx, `
|
||||
SELECT token, dim_id
|
||||
FROM mk_dim_token_map
|
||||
WHERE dim_column=$1 AND token = ANY($2)
|
||||
`, column, pq.Array(tokens))
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var tok string
|
||||
var id int64
|
||||
if err := rows.Scan(&tok, &id); err != nil {
|
||||
return out, err
|
||||
}
|
||||
out[strings.TrimSpace(tok)] = id
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// Load all known (product_code, dim1, dim3_key) combos for products in this response.
|
||||
loadProductDimCombos := func(ctx context.Context, pg *sql.DB, productCodes []string) (map[string]struct{}, error) {
|
||||
out := map[string]struct{}{}
|
||||
if len(productCodes) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
rows, err := pg.QueryContext(ctx, `
|
||||
SELECT product_code, dim1, dim3_key
|
||||
FROM mk_mmitem_dim_combo
|
||||
WHERE product_code = ANY($1)
|
||||
`, pq.Array(productCodes))
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var code string
|
||||
var dim1, dim3Key int64
|
||||
if err := rows.Scan(&code, &dim1, &dim3Key); err != nil {
|
||||
return out, err
|
||||
}
|
||||
key := fmt.Sprintf("%s|%d|%d", strings.TrimSpace(code), dim1, dim3Key)
|
||||
out[key] = struct{}{}
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
codes := setToSortedSlice(codeSet)
|
||||
mmitemByCode, _ := loadMmitemIDs(ctx, pg, codes)
|
||||
dim1ByToken, _ := loadDimTokenIDs(ctx, pg, "dimval1", setToSortedSlice(colorSet))
|
||||
dim3ByToken, _ := loadDimTokenIDs(ctx, pg, "dimval3", setToSortedSlice(dim3Set))
|
||||
dim1ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval1", setToSortedSlice(colorSet))
|
||||
dim3ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval3", setToSortedSlice(dim3Set))
|
||||
combos, _ := loadProductDimCombos(ctx, pg, codes)
|
||||
existing, _ := loadProductSeriesAssignments(ctx, pg, codes)
|
||||
|
||||
// Per-request cache for per-mmitem dimval3 inference to avoid repeated dfblob scans.
|
||||
inferCache := map[string]int64{}
|
||||
inferDim3ForMmitem := func(mmitemID int64, token string) int64 {
|
||||
mmitemID = int64(mmitemID)
|
||||
tok := strings.ToUpper(normalizeDimParam(token))
|
||||
if mmitemID <= 0 || tok == "" {
|
||||
return 0
|
||||
}
|
||||
key := fmt.Sprintf("%d|%s", mmitemID, tok)
|
||||
if v, ok := inferCache[key]; ok {
|
||||
return v
|
||||
}
|
||||
// Use the same pattern approach as resolveDimvalFromFileNameToken, but scoped to src_id.
|
||||
patterns := buildNameLikePatterns(tok)
|
||||
if len(patterns) == 0 {
|
||||
inferCache[key] = 0
|
||||
return 0
|
||||
}
|
||||
var dimv string
|
||||
err := pg.QueryRowContext(ctx, `
|
||||
SELECT x.dimv
|
||||
FROM (
|
||||
SELECT COALESCE(dimval3::text, '') AS dimv, COUNT(*) AS cnt
|
||||
FROM dfblob
|
||||
WHERE src_table='mmitem'
|
||||
AND typ='img'
|
||||
AND src_id=$7
|
||||
AND COALESCE(dimval3::text, '') <> ''
|
||||
AND (
|
||||
UPPER(COALESCE(file_name,'')) LIKE $1 OR
|
||||
UPPER(COALESCE(file_name,'')) LIKE $2 OR
|
||||
UPPER(COALESCE(file_name,'')) LIKE $3 OR
|
||||
UPPER(COALESCE(file_name,'')) LIKE $4 OR
|
||||
UPPER(COALESCE(file_name,'')) LIKE $5 OR
|
||||
UPPER(COALESCE(file_name,'')) LIKE $6
|
||||
)
|
||||
GROUP BY COALESCE(dimval3::text, '')
|
||||
) x
|
||||
ORDER BY x.cnt DESC, x.dimv
|
||||
LIMIT 1
|
||||
`, patterns[0], patterns[1], patterns[2], patterns[3], patterns[4], patterns[5], mmitemID).Scan(&dimv)
|
||||
if err != nil {
|
||||
inferCache[key] = 0
|
||||
return 0
|
||||
}
|
||||
dimv = strings.TrimSpace(dimv)
|
||||
if dimv == "" || dimv == "0" {
|
||||
inferCache[key] = 0
|
||||
return 0
|
||||
}
|
||||
id, err := strconv.ParseInt(dimv, 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
inferCache[key] = 0
|
||||
return 0
|
||||
}
|
||||
inferCache[key] = id
|
||||
return id
|
||||
}
|
||||
|
||||
out := make([]productSeriesMappingRow, 0, len(grouped))
|
||||
for _, row := range grouped {
|
||||
row.MmitemID = mmitemByCode[row.ProductCode]
|
||||
row.Dim1ID = dim1ByToken[row.ColorCode]
|
||||
if row.Dim3Code != "" {
|
||||
// dimval3 can be ambiguous, but if mk_dim_token_map has a row we treat it as source of truth.
|
||||
// Strict: only accept explicit token map rows for dimval3.
|
||||
if v := dim3ByToken[row.Dim3Code]; v > 0 {
|
||||
row.Dim3ID = v
|
||||
} else if inferred := inferDim3ForMmitem(row.MmitemID, row.Dim3Code); inferred > 0 {
|
||||
row.Dim3ID = inferred
|
||||
}
|
||||
}
|
||||
// Only show variants that are also known/valid on the PG side (product_code + dim1 + dim3_key).
|
||||
// This prevents UI from listing stock variants that don't exist in PG's authoritative item-dim combos.
|
||||
row.MappingReady = row.MmitemID > 0 && row.Dim1ID > 0 && (row.Dim3Code == "" || row.Dim3ID > 0)
|
||||
dim3Key := int64(0)
|
||||
if row.Dim3Code != "" {
|
||||
dim3Key = row.Dim3ID
|
||||
}
|
||||
if row.MappingReady {
|
||||
_, ok := combos[fmt.Sprintf("%s|%d|%d", strings.TrimSpace(row.ProductCode), row.Dim1ID, dim3Key)]
|
||||
row.MappingReady = ok
|
||||
}
|
||||
if !row.MappingReady {
|
||||
row.MappingWarning = "PG urun veya varyant token eslesmesi bulunamadi"
|
||||
// Skip: do not surface "orphan" variants in the UI.
|
||||
continue
|
||||
}
|
||||
assignKey := assignmentKey(row.ProductCode, row.Dim1ID, row.Dim3ID)
|
||||
appendSeries := func(key string) {
|
||||
@@ -383,11 +388,6 @@ LIMIT 1
|
||||
}
|
||||
}
|
||||
appendSeries(assignKey)
|
||||
// Fallback: if we couldn't match dim3-specific assignments (dim3 token ambiguity / missing),
|
||||
// show the default (dim3 NULL) assignments for the same product+color.
|
||||
if len(row.SeriesIDs) == 0 && row.Dim3Code != "" && row.Dim1ID > 0 {
|
||||
appendSeries(assignmentKey(row.ProductCode, row.Dim1ID, 0))
|
||||
}
|
||||
sort.Slice(row.Series, func(i, j int) bool { return row.Series[i].Code < row.Series[j].Code })
|
||||
sort.Slice(row.SeriesIDs, func(i, j int) bool { return row.SeriesIDs[i] < row.SeriesIDs[j] })
|
||||
out = append(out, *row)
|
||||
@@ -416,6 +416,239 @@ LIMIT 1
|
||||
}
|
||||
}
|
||||
|
||||
func GetProductSeriesOrphanMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
f := readStockAttrFilters(r)
|
||||
limit := 0
|
||||
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
|
||||
if n, err := strconv.Atoi(raw); err == nil && n >= 0 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
search := firstNonEmpty(r.URL.Query().Get("q"), r.URL.Query().Get("code"), r.URL.Query().Get("product_code"))
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
msRows, err := db.MssqlDB.QueryContext(ctx, queries.GetProductSeriesStockRowsQuery,
|
||||
f.kategori,
|
||||
f.urunAnaGrubu,
|
||||
joinFilterValues(f.urunAltGrubu),
|
||||
joinFilterValues(f.renk),
|
||||
joinFilterValues(f.renk2),
|
||||
joinFilterValues(f.urunIcerigi),
|
||||
joinFilterValues(f.fit),
|
||||
joinFilterValues(f.drop),
|
||||
joinFilterValues(f.beden),
|
||||
strconv.Itoa(limit),
|
||||
search,
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, "Stok seri listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer msRows.Close()
|
||||
|
||||
grouped := map[string]*productSeriesMappingRow{}
|
||||
codeSet := map[string]struct{}{}
|
||||
colorSet := map[string]struct{}{}
|
||||
dim3Set := map[string]struct{}{}
|
||||
sizeSet := map[string]struct{}{}
|
||||
for msRows.Next() {
|
||||
var raw productSeriesStockRawRow
|
||||
if err := msRows.Scan(
|
||||
&raw.ProductCode,
|
||||
&raw.ProductDescription,
|
||||
&raw.ColorCode,
|
||||
&raw.ColorTitle,
|
||||
&raw.Dim3Code,
|
||||
&raw.SizeCode,
|
||||
&raw.Qty,
|
||||
&raw.UrunAnaGrubu,
|
||||
&raw.UrunAltGrubu,
|
||||
&raw.Marka,
|
||||
&raw.DropVal,
|
||||
&raw.Fit,
|
||||
&raw.UrunIcerigi,
|
||||
&raw.Kategori,
|
||||
); err != nil {
|
||||
http.Error(w, "Stok satiri okunamadi: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
raw.ProductCode = strings.TrimSpace(raw.ProductCode)
|
||||
raw.ColorCode = strings.TrimSpace(raw.ColorCode)
|
||||
raw.Dim3Code = strings.TrimSpace(raw.Dim3Code)
|
||||
raw.SizeCode = strings.TrimSpace(raw.SizeCode)
|
||||
key := productSeriesKey(raw.ProductCode, raw.ColorCode, raw.Dim3Code)
|
||||
row := grouped[key]
|
||||
if row == nil {
|
||||
row = &productSeriesMappingRow{
|
||||
RowKey: key,
|
||||
ProductCode: raw.ProductCode,
|
||||
ProductDescription: strings.TrimSpace(raw.ProductDescription),
|
||||
ColorCode: raw.ColorCode,
|
||||
ColorTitle: strings.TrimSpace(raw.ColorTitle),
|
||||
Dim3Code: raw.Dim3Code,
|
||||
UrunAnaGrubu: strings.TrimSpace(raw.UrunAnaGrubu),
|
||||
UrunAltGrubu: strings.TrimSpace(raw.UrunAltGrubu),
|
||||
Marka: strings.TrimSpace(raw.Marka),
|
||||
DropVal: strings.TrimSpace(raw.DropVal),
|
||||
Fit: strings.TrimSpace(raw.Fit),
|
||||
UrunIcerigi: strings.TrimSpace(raw.UrunIcerigi),
|
||||
Kategori: strings.TrimSpace(raw.Kategori),
|
||||
SizeQty: map[string]float64{},
|
||||
SeriesIDs: []int64{},
|
||||
Series: []productSeriesDefinition{},
|
||||
}
|
||||
grouped[key] = row
|
||||
codeSet[raw.ProductCode] = struct{}{}
|
||||
colorSet[raw.ColorCode] = struct{}{}
|
||||
if raw.Dim3Code != "" {
|
||||
dim3Set[raw.Dim3Code] = struct{}{}
|
||||
}
|
||||
}
|
||||
if raw.SizeCode != "" {
|
||||
row.SizeQty[raw.SizeCode] = raw.Qty
|
||||
sizeSet[raw.SizeCode] = struct{}{}
|
||||
}
|
||||
row.TotalQty += raw.Qty
|
||||
}
|
||||
if err := msRows.Err(); err != nil {
|
||||
http.Error(w, "Stok satirlari okunamadi: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
defs, err := listProductSeriesDefinitions(ctx, pg, true)
|
||||
if err != nil {
|
||||
http.Error(w, "Seri tanimlari alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Strict token map lookups (no inference).
|
||||
loadDimTokenIDsStrict := func(ctx context.Context, pg *sql.DB, column string, tokens []string) (map[string]int64, error) {
|
||||
out := map[string]int64{}
|
||||
if len(tokens) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
rows, err := pg.QueryContext(ctx, `
|
||||
SELECT token, dim_id
|
||||
FROM mk_dim_token_map
|
||||
WHERE dim_column=$1 AND token = ANY($2)
|
||||
`, column, pq.Array(tokens))
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var tok string
|
||||
var id int64
|
||||
if err := rows.Scan(&tok, &id); err != nil {
|
||||
return out, err
|
||||
}
|
||||
out[strings.TrimSpace(tok)] = id
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
loadProductDimCombos := func(ctx context.Context, pg *sql.DB, productCodes []string) (map[string]struct{}, error) {
|
||||
out := map[string]struct{}{}
|
||||
if len(productCodes) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
rows, err := pg.QueryContext(ctx, `
|
||||
SELECT product_code, dim1, dim3_key
|
||||
FROM mk_mmitem_dim_combo
|
||||
WHERE product_code = ANY($1)
|
||||
`, pq.Array(productCodes))
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var code string
|
||||
var dim1, dim3Key int64
|
||||
if err := rows.Scan(&code, &dim1, &dim3Key); err != nil {
|
||||
return out, err
|
||||
}
|
||||
key := fmt.Sprintf("%s|%d|%d", strings.TrimSpace(code), 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))
|
||||
combos, _ := loadProductDimCombos(ctx, pg, codes)
|
||||
|
||||
out := make([]productSeriesMappingRow, 0, len(grouped))
|
||||
for _, row := range grouped {
|
||||
if row.TotalQty <= 0 {
|
||||
continue
|
||||
}
|
||||
row.MmitemID = mmitemByCode[row.ProductCode]
|
||||
row.Dim1ID = dim1ByToken[row.ColorCode]
|
||||
if row.Dim3Code != "" {
|
||||
if v := dim3ByToken[row.Dim3Code]; v > 0 {
|
||||
row.Dim3ID = v
|
||||
}
|
||||
}
|
||||
|
||||
// Determine orphan reason.
|
||||
baseReady := row.MmitemID > 0 && row.Dim1ID > 0 && (row.Dim3Code == "" || row.Dim3ID > 0)
|
||||
dim3Key := int64(0)
|
||||
if row.Dim3Code != "" {
|
||||
dim3Key = row.Dim3ID
|
||||
}
|
||||
comboKey := fmt.Sprintf("%s|%d|%d", strings.TrimSpace(row.ProductCode), row.Dim1ID, dim3Key)
|
||||
_, comboOK := combos[comboKey]
|
||||
|
||||
row.MappingReady = false
|
||||
switch {
|
||||
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)"
|
||||
case row.Dim3Code != "" && row.Dim3ID <= 0:
|
||||
row.MappingWarning = "B2B'de dim3 token eslesmesi yok (mk_dim_token_map: dimval3)"
|
||||
case !comboOK:
|
||||
row.MappingWarning = "B2B'de varyant kombosu yok (mk_mmitem_dim_combo)"
|
||||
default:
|
||||
// Not an orphan; skip.
|
||||
if baseReady && comboOK {
|
||||
continue
|
||||
}
|
||||
row.MappingWarning = "B2B varyant eslesmesi bulunamadi"
|
||||
}
|
||||
|
||||
out = append(out, *row)
|
||||
}
|
||||
|
||||
productTotals := productSeriesTotalQtyByCode(out)
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
totalI := productTotals[out[i].ProductCode]
|
||||
totalJ := productTotals[out[j].ProductCode]
|
||||
if totalI != totalJ {
|
||||
return totalI > totalJ
|
||||
}
|
||||
if out[i].ProductCode != out[j].ProductCode {
|
||||
return out[i].ProductCode < out[j].ProductCode
|
||||
}
|
||||
if out[i].ColorCode != out[j].ColorCode {
|
||||
return out[i].ColorCode < out[j].ColorCode
|
||||
}
|
||||
return out[i].Dim3Code < out[j].Dim3Code
|
||||
})
|
||||
|
||||
writeJSON(w, productSeriesMappingsResponse{
|
||||
Rows: out,
|
||||
SizeColumns: sortSizeColumns(setToSortedSlice(sizeSet)),
|
||||
Definitions: defs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func productSeriesTotalQtyByCode(rows []productSeriesMappingRow) map[string]float64 {
|
||||
out := make(map[string]float64, len(rows))
|
||||
for _, row := range rows {
|
||||
|
||||
Reference in New Issue
Block a user