From 05c6103a3a433607314f6c6873f1f7a6c5eacd3e Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Sun, 29 Mar 2026 22:41:02 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- svc/main.go | 7 +- svc/routes/order_mail.go | 19 +- svc/routes/order_pdf.go | 228 ++++++++++++++-- svc/routes/orderinventory.go | 12 + svc/routes/product_size_match.go | 106 ++++++++ ...g.js.temporary.compiled.1774811975940.mjs} | 0 ui/src/pages/OrderEntry.vue | 50 +--- ui/src/pages/ProductStockByAttributes.vue | 26 +- ui/src/pages/ProductStockQuery.vue | 26 +- ui/src/stores/orderentryStore.js | 247 +++++++++++++++++- 10 files changed, 629 insertions(+), 92 deletions(-) create mode 100644 svc/routes/product_size_match.go rename ui/{quasar.config.js.temporary.compiled.1774248846219.mjs => quasar.config.js.temporary.compiled.1774811975940.mjs} (100%) diff --git a/svc/main.go b/svc/main.go index 17c1a17..c05a938 100644 --- a/svc/main.go +++ b/svc/main.go @@ -531,7 +531,7 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router {"/api/orders/export", "GET", "export", routes.OrderListExcelRoute(mssql)}, {"/api/order/check/{id}", "GET", "view", routes.OrderExistsHandler(mssql)}, {"/api/order/validate", "POST", "insert", routes.ValidateOrderHandler(mssql)}, - {"/api/order/pdf/{id}", "GET", "export", routes.OrderPDFHandler(mssql)}, + {"/api/order/pdf/{id}", "GET", "export", routes.OrderPDFHandler(mssql, pgDB)}, {"/api/order/send-market-mail", "POST", "read", routes.SendOrderMarketMailHandler(pgDB, mssql, ml)}, {"/api/order-inventory", "GET", "view", http.HandlerFunc(routes.GetOrderInventoryHandler)}, {"/api/orderpricelistb2b", "GET", "view", routes.GetOrderPriceListB2BHandler(pgDB, mssql)}, @@ -612,6 +612,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "order", "view", http.HandlerFunc(routes.GetProductImageContentHandler(pgDB)), ) + bindV3(r, pgDB, + "/api/product-size-match/rules", "GET", + "order", "view", + wrapV3(routes.GetProductSizeMatchRulesHandler(pgDB)), + ) // ============================================================ // ROLE MANAGEMENT diff --git a/svc/routes/order_mail.go b/svc/routes/order_mail.go index 20cf7c6..c7b6d61 100644 --- a/svc/routes/order_mail.go +++ b/svc/routes/order_mail.go @@ -5,6 +5,7 @@ import ( "context" "database/sql" "encoding/json" + "errors" "fmt" "net/http" "strings" @@ -71,7 +72,7 @@ func SendOrderMarketMailHandler(pg *sql.DB, mssql *sql.DB, ml *mailer.GraphMaile return } - pdfBytes, header, err := buildOrderPDFBytesForMail(mssql, orderID) + pdfBytes, header, err := buildOrderPDFBytesForMail(mssql, pg, orderID) if err != nil { http.Error(w, "pdf build error: "+err.Error(), http.StatusInternalServerError) return @@ -241,7 +242,7 @@ ORDER BY email return out, nil } -func buildOrderPDFBytesForMail(db *sql.DB, orderID string) ([]byte, *OrderHeader, error) { +func buildOrderPDFBytesForMail(db *sql.DB, pgDB *sql.DB, orderID string) ([]byte, *OrderHeader, error) { header, err := getOrderHeaderFromDB(db, orderID) if err != nil { return nil, nil, err @@ -262,7 +263,19 @@ func buildOrderPDFBytesForMail(db *sql.DB, orderID string) ([]byte, *OrderHeader } } - rows := normalizeOrderLinesForPdf(lines) + if pgDB == nil { + return nil, nil, errors.New("product-size-match db not initialized") + } + sizeMatchData, err := loadProductSizeMatchData(pgDB) + if err != nil { + return nil, nil, err + } + rows := normalizeOrderLinesForPdf(lines, sizeMatchData) + for _, rr := range rows { + if strings.TrimSpace(rr.Category) == "" { + return nil, nil, fmt.Errorf("product-size-match unmapped row: %s/%s/%s", rr.Model, rr.GroupMain, rr.GroupSub) + } + } pdf, err := newOrderPdf() if err != nil { diff --git a/svc/routes/order_pdf.go b/svc/routes/order_pdf.go index ecce0a5..cd19976 100644 --- a/svc/routes/order_pdf.go +++ b/svc/routes/order_pdf.go @@ -87,6 +87,7 @@ type OrderLineRaw struct { LineDescription sql.NullString UrunAnaGrubu sql.NullString UrunAltGrubu sql.NullString + YetiskinGarson sql.NullString IsClosed sql.NullBool WithHoldingTaxType sql.NullString DOVCode sql.NullString @@ -101,20 +102,21 @@ type OrderLineRaw struct { =========================================================== */ type PdfRow struct { - Model string - Color string - GroupMain string - GroupSub string - Description string - Category string - SizeQty map[string]int - TotalQty int - Price float64 - Currency string - Amount float64 - Termin string - IsClosed bool - OrderLineIDs map[string]string + Model string + Color string + GroupMain string + GroupSub string + YetiskinGarson string + Description string + Category string + SizeQty map[string]int + TotalQty int + Price float64 + Currency string + Amount float64 + Termin string + IsClosed bool + OrderLineIDs map[string]string ClosedSizes map[string]bool // 🆕 her beden için IsClosed bilgisi } @@ -332,7 +334,140 @@ func parseNumericSize(v string) (int, bool) { return n, true } -func detectBedenGroupGo(bedenList []string, ana, alt string) string { +func deriveKategoriTokenGo(urunKategori, yetiskinGarson string) string { + kat := normalizeTextForMatchGo(urunKategori) + if strings.Contains(kat, "GARSON") { + return "GARSON" + } + if strings.Contains(kat, "YETISKIN") { + return "YETISKIN" + } + return "" +} + +func normalizeRuleAltGroupGo(urunAltGrubu string) string { + return normalizeTextForMatchGo(urunAltGrubu) +} + +func pickBestGroupFromCandidatesGo(groupKeys, bedenList []string, schemas map[string][]string) string { + if len(groupKeys) == 0 { + return "" + } + if len(groupKeys) == 1 { + return strings.TrimSpace(groupKeys[0]) + } + + normalizedBeden := make([]string, 0, len(bedenList)) + for _, b := range bedenList { + n := normalizeBedenLabelGo(b) + if strings.TrimSpace(n) == "" { + n = " " + } + normalizedBeden = append(normalizedBeden, n) + } + + if len(normalizedBeden) == 0 { + return strings.TrimSpace(groupKeys[0]) + } + + bestKey := strings.TrimSpace(groupKeys[0]) + bestScore := -1 + for _, key := range groupKeys { + k := strings.TrimSpace(key) + if k == "" { + continue + } + normalizedSchema := map[string]bool{} + for _, sv := range schemas[k] { + ns := normalizeBedenLabelGo(sv) + if strings.TrimSpace(ns) == "" { + ns = " " + } + normalizedSchema[ns] = true + } + score := 0 + for _, b := range normalizedBeden { + if normalizedSchema[b] { + score++ + } + } + if score > bestScore { + bestScore = score + bestKey = k + } + } + return bestKey +} + +func resolveGroupFromProductSizeMatchRulesGo( + matchData *ProductSizeMatchResponse, + bedenList []string, + urunAnaGrubu, urunKategori, yetiskinGarson, urunAltGrubu string, +) string { + if matchData == nil || len(matchData.Rules) == 0 { + return "" + } + + kategoriToken := deriveKategoriTokenGo(urunKategori, yetiskinGarson) + ana := normalizeTextForMatchGo(urunAnaGrubu) + alt := normalizeRuleAltGroupGo(urunAltGrubu) + if kategoriToken == "" || ana == "" { + return "" + } + + candidateGroupKeys := make([]string, 0, 2) + seen := map[string]bool{} + + for i := range matchData.Rules { + rule := &matchData.Rules[i] + if normalizeTextForMatchGo(rule.UrunAnaGrubu) != ana { + continue + } + ruleKategori := normalizeTextForMatchGo(rule.Kategori) + if ruleKategori != kategoriToken { + continue + } + + ruleAlt := normalizeTextForMatchGo(rule.UrunAltGrubu) + if ruleAlt != alt { + continue + } + for _, g := range rule.GroupKeys { + key := strings.TrimSpace(g) + if key == "" || seen[key] { + continue + } + seen[key] = true + candidateGroupKeys = append(candidateGroupKeys, key) + } + } + + if len(candidateGroupKeys) == 0 { + return "" + } + return pickBestGroupFromCandidatesGo(candidateGroupKeys, bedenList, matchData.Schemas) +} + +func detectBedenGroupGo( + matchData *ProductSizeMatchResponse, + bedenList []string, + ana, alt, urunKategori, yetiskinGarson string, +) string { + ruleBased := resolveGroupFromProductSizeMatchRulesGo( + matchData, + bedenList, + ana, + urunKategori, + yetiskinGarson, + alt, + ) + if ruleBased != "" { + return ruleBased + } + if matchData != nil && len(matchData.Rules) > 0 { + return "" + } + ana = normalizeTextForMatchGo(ana) alt = normalizeTextForMatchGo(alt) @@ -368,6 +503,15 @@ func detectBedenGroupGo(bedenList []string, ana, alt string) string { strings.Contains(ana, "YETİŞKIN/GARSON") || strings.Contains(alt, "YETİŞKIN/GARSON") || strings.Contains(ana, "YETİŞKİN/GARSON") || strings.Contains(alt, "YETİŞKİN/GARSON") + // Ayakkabi kurali garsondan once uygulanmali: + // GARSON + AYAKKABI => ayk_garson, digerleri => ayk + if strings.Contains(ana, "AYAKKABI") || strings.Contains(alt, "AYAKKABI") { + if hasGarson { + return catAykGar + } + return catAyk + } + // ✅ Garson → yaş (ürün tipi fark etmeksizin) if hasGarson { return catYas @@ -618,6 +762,7 @@ func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) { L.LineDescription, P.ProductAtt01Desc, P.ProductAtt02Desc, + P.ProductAtt44Desc, L.IsClosed, L.WithHoldingTaxTypeCode, L.DOVCode, @@ -656,6 +801,7 @@ func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) { &l.LineDescription, &l.UrunAnaGrubu, &l.UrunAltGrubu, + &l.YetiskinGarson, &l.IsClosed, &l.WithHoldingTaxType, &l.DOVCode, @@ -675,7 +821,7 @@ func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) { 4) NORMALIZE + CATEGORY MAP =========================================================== */ -func normalizeOrderLinesForPdf(lines []OrderLineRaw) []PdfRow { +func normalizeOrderLinesForPdf(lines []OrderLineRaw, matchData *ProductSizeMatchResponse) []PdfRow { type comboKey struct { Model, Color, Color2 string } @@ -701,16 +847,17 @@ func normalizeOrderLinesForPdf(lines []OrderLineRaw) []PdfRow { if _, ok := merged[key]; !ok { merged[key] = &PdfRow{ - Model: model, - Color: displayColor, - GroupMain: s64(raw.UrunAnaGrubu), - GroupSub: s64(raw.UrunAltGrubu), - Description: s64(raw.LineDescription), - SizeQty: make(map[string]int), - Currency: s64(raw.DocCurrencyCode), - Price: f64(raw.Price), - OrderLineIDs: make(map[string]string), - ClosedSizes: make(map[string]bool), // 🆕 + Model: model, + Color: displayColor, + GroupMain: s64(raw.UrunAnaGrubu), + GroupSub: s64(raw.UrunAltGrubu), + YetiskinGarson: s64(raw.YetiskinGarson), + Description: s64(raw.LineDescription), + SizeQty: make(map[string]int), + Currency: s64(raw.DocCurrencyCode), + Price: f64(raw.Price), + OrderLineIDs: make(map[string]string), + ClosedSizes: make(map[string]bool), // 🆕 } } row := merged[key] @@ -751,7 +898,7 @@ func normalizeOrderLinesForPdf(lines []OrderLineRaw) []PdfRow { for s := range r.SizeQty { sizes = append(sizes, s) } - r.Category = detectBedenGroupGo(sizes, r.GroupMain, r.GroupSub) + r.Category = detectBedenGroupGo(matchData, sizes, r.GroupMain, r.GroupSub, r.YetiskinGarson, r.YetiskinGarson) r.Amount = float64(r.TotalQty) * r.Price out = append(out, *r) } @@ -1555,7 +1702,7 @@ func renderOrderGrid(pdf *gofpdf.Fpdf, header *OrderHeader, rows []PdfRow, hasVa HTTP HANDLER → /api/order/pdf/{id} =========================================================== */ -func OrderPDFHandler(db *sql.DB) http.Handler { +func OrderPDFHandler(db *sql.DB, pgDB *sql.DB) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { orderID := mux.Vars(r)["id"] @@ -1612,7 +1759,30 @@ func OrderPDFHandler(db *sql.DB) http.Handler { } // Normalize - rows := normalizeOrderLinesForPdf(lines) + var sizeMatchData *ProductSizeMatchResponse + if pgDB == nil { + http.Error(w, "product-size-match db not initialized", http.StatusInternalServerError) + return + } + if m, err := loadProductSizeMatchData(pgDB); err != nil { + log.Printf("❌ OrderPDF product-size-match load failed orderID=%s: %v", orderID, err) + http.Error(w, "product-size-match load failed: "+err.Error(), http.StatusInternalServerError) + return + } else { + sizeMatchData = m + } + rows := normalizeOrderLinesForPdf(lines, sizeMatchData) + unmapped := make([]string, 0) + for _, rr := range rows { + if strings.TrimSpace(rr.Category) == "" { + unmapped = append(unmapped, fmt.Sprintf("%s/%s/%s", rr.Model, rr.GroupMain, rr.GroupSub)) + } + } + if len(unmapped) > 0 { + log.Printf("❌ OrderPDF product-size-match unmapped orderID=%s rows=%v", orderID, unmapped) + http.Error(w, "product-size-match unmapped rows", http.StatusInternalServerError) + return + } log.Printf("📄 OrderPDF normalized rows orderID=%s rowCount=%d", orderID, len(rows)) for i, rr := range rows { if i >= 30 { diff --git a/svc/routes/orderinventory.go b/svc/routes/orderinventory.go index 8b4a824..d3bda45 100644 --- a/svc/routes/orderinventory.go +++ b/svc/routes/orderinventory.go @@ -5,8 +5,10 @@ import ( "bssapp-backend/queries" "context" "encoding/json" + "fmt" "log" "net/http" + "sort" "time" ) @@ -79,6 +81,16 @@ func GetOrderInventoryHandler(w http.ResponseWriter, r *http.Request) { return } + // Debug: beden/adet özetini tek satırda yazdır (saha doğrulaması için) + if len(list) > 0 { + keys := make([]string, 0, len(list)) + for _, it := range list { + keys = append(keys, fmt.Sprintf("%s:%g", it.Beden, it.KullanilabilirAdet)) + } + sort.Strings(keys) + log.Printf("🔎 [ORDERINV] beden/qty -> %s", keys) + } + log.Printf("✅ [ORDERINV] %s / %s / %s -> %d kayıt döndü", code, color, color2, len(list)) w.Header().Set("Content-Type", "application/json; charset=utf-8") diff --git a/svc/routes/product_size_match.go b/svc/routes/product_size_match.go new file mode 100644 index 0000000..c2f3824 --- /dev/null +++ b/svc/routes/product_size_match.go @@ -0,0 +1,106 @@ +package routes + +import ( + "database/sql" + "encoding/json" + "net/http" + "strings" + + "github.com/lib/pq" +) + +type ProductSizeMatchRule struct { + ProductGroupID int `json:"product_group_id"` + Kategori string `json:"kategori"` + UrunAnaGrubu string `json:"urun_ana_grubu"` + UrunAltGrubu string `json:"urun_alt_grubu"` + GroupKeys []string `json:"group_keys"` +} + +type ProductSizeMatchResponse struct { + Rules []ProductSizeMatchRule `json:"rules"` + Schemas map[string][]string `json:"schemas"` +} + +func defaultSizeSchemas() map[string][]string { + return map[string][]string{ + "tak": {"44", "46", "48", "50", "52", "54", "56", "58", "60", "62", "64", "66", "68", "70", "72", "74"}, + "ayk": {"39", "40", "41", "42", "43", "44", "45"}, + "ayk_garson": {"22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "STD"}, + "yas": {"2", "4", "6", "8", "10", "12", "14"}, + "pan": {"38", "40", "42", "44", "46", "48", "50", "52", "54", "56", "58", "60", "62", "64", "66", "68"}, + "gom": {"XS", "S", "M", "L", "XL", "2XL", "3XL", "4XL", "5XL", "6XL", "7XL"}, + "aksbir": {" ", "44", "STD", "110", "115", "120", "125", "130", "135"}, + } +} + +func loadProductSizeMatchData(pgDB *sql.DB) (*ProductSizeMatchResponse, error) { + rows, err := pgDB.Query(` + SELECT + pg.id AS product_group_id, + COALESCE(pg.kategori, ''), + COALESCE(pg.urun_ana_grubu, ''), + COALESCE(pg.urun_alt_grubu, ''), + COALESCE( + array_agg(DISTINCT sm.size_group_key ORDER BY sm.size_group_key) + FILTER (WHERE sm.size_group_key IS NOT NULL), + ARRAY[]::text[] + ) AS group_keys + FROM mk_product_size_match sm + JOIN mk_product_group pg + ON pg.id = sm.product_group_id + GROUP BY + pg.id, pg.kategori, pg.urun_ana_grubu, pg.urun_alt_grubu + ORDER BY pg.id + `) + if err != nil { + return nil, err + } + defer rows.Close() + + resp := &ProductSizeMatchResponse{ + Rules: make([]ProductSizeMatchRule, 0), + Schemas: defaultSizeSchemas(), + } + + for rows.Next() { + var item ProductSizeMatchRule + var arr pq.StringArray + if err := rows.Scan( + &item.ProductGroupID, + &item.Kategori, + &item.UrunAnaGrubu, + &item.UrunAltGrubu, + &arr, + ); err != nil { + return nil, err + } + item.GroupKeys = make([]string, 0, len(arr)) + for _, g := range arr { + g = strings.TrimSpace(g) + if g == "" { + continue + } + item.GroupKeys = append(item.GroupKeys, g) + } + resp.Rules = append(resp.Rules, item) + } + if err := rows.Err(); err != nil { + return nil, err + } + + return resp, nil +} + +// GET /api/product-size-match/rules +func GetProductSizeMatchRulesHandler(pgDB *sql.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + resp, err := loadProductSizeMatchData(pgDB) + if err != nil { + http.Error(w, "product-size-match load failed: "+err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(resp) + }) +} diff --git a/ui/quasar.config.js.temporary.compiled.1774248846219.mjs b/ui/quasar.config.js.temporary.compiled.1774811975940.mjs similarity index 100% rename from ui/quasar.config.js.temporary.compiled.1774248846219.mjs rename to ui/quasar.config.js.temporary.compiled.1774811975940.mjs diff --git a/ui/src/pages/OrderEntry.vue b/ui/src/pages/OrderEntry.vue index 6791a5f..a4352d7 100644 --- a/ui/src/pages/OrderEntry.vue +++ b/ui/src/pages/OrderEntry.vue @@ -835,6 +835,7 @@ const $q = useQuasar() const orderStore = useOrderEntryStore() const orderentryStore = useOrderEntryStore() orderStore.initSchemaMap() +void orderStore.ensureProductSizeMatchRules() const schemaSource = computed(() => Object.keys(orderStore?.schemaMap || {}).length @@ -1777,6 +1778,7 @@ watch(() => orderStore.replaceRouteSignal, async (id) => { /* -------------------- LIFECYCLE -------------------- */ onMounted(async () => { + await orderStore.ensureProductSizeMatchRules() await nextTick() /* ---------------- UI ---------------- */ @@ -2671,17 +2673,16 @@ async function onModelChange(modelCode) { }) /* ======================================================= - 🔑 BEDEN GRUBU — TEK VE KESİN KARAR - - ÖNCE detectBedenGroup (accent/garson/yas kurallarını içerir) - - Sonra güvenli fallback + 🔑 BEDEN GRUBU — TEK VE KESİN KARAR (SQL kural tabanı) ======================================================= */ let bedenGrpKey = null try { bedenGrpKey = detectBedenGroup( null, form.urunAnaGrubu, - form.kategori || form.urunAltGrubu, - form.yetiskinGarson || form.askiliyan + form.kategori || '', + form.yetiskinGarson || form.askiliyan, + form.urunAltGrubu || '' ) } catch (e) { console.warn('⚠️ detectBedenGroup hata:', e) @@ -2689,42 +2690,13 @@ async function onModelChange(modelCode) { } if (!bedenGrpKey) { - const anaNRaw = String(form.urunAnaGrubu || '') - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') - .toLowerCase() - .trim() - const katN = String(form.kategori || form.urunAltGrubu || '') - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') - .toLowerCase() - .trim() - const ygN = String(form.yetiskinGarson || form.askiliyan || '') - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') - .toLowerCase() - .trim() - const anaN = - (katN.includes('yetiskin') && anaNRaw.includes('gomlek klasik')) - ? anaNRaw.replace('gomlek klasik', 'gomlek ata yaka') - : anaNRaw - - const hasGarsonMeta = - anaN.includes('garson') || - katN.includes('garson') || - katN.includes('yetiskin/garson') || - ygN.includes('garson') || - ygN.includes('yetiskin/garson') - - if (hasGarsonMeta) bedenGrpKey = 'yas' - else if (anaN.includes('pantolon') || katN.includes('pantolon')) bedenGrpKey = 'pan' - else if (anaN.includes('gomlek') || katN.includes('gomlek')) bedenGrpKey = 'gom' - else if (anaN.includes('ayakkabi') || katN.includes('ayakkabi')) bedenGrpKey = 'ayk' + $q.notify({ + type: 'negative', + message: 'Beden grubu eşleşmesi bulunamadı (kategori/ana grup/alt grup).' + }) + return } - // ✅ Son fallback - if (!bedenGrpKey) bedenGrpKey = 'tak' - form.grpKey = bedenGrpKey console.log('🧭 Editor grpKey set edildi →', bedenGrpKey) // ✅ Editor bedenleri hemen aç (UI seed) — schemaMap tek kaynak diff --git a/ui/src/pages/ProductStockByAttributes.vue b/ui/src/pages/ProductStockByAttributes.vue index 700fd30..eed1963 100644 --- a/ui/src/pages/ProductStockByAttributes.vue +++ b/ui/src/pages/ProductStockByAttributes.vue @@ -875,7 +875,8 @@ function toggleAllDetails() { function buildLevel3Rows(grp3) { const byKey = new Map() - const gk = activeGrpKey.value || 'tak' + const gk = activeGrpKey.value + if (!gk) return [] for (const item of grp3.items || []) { const model = String(item.Urun_Kodu || '').trim() @@ -1235,6 +1236,7 @@ async function fetchStockByAttributes() { if (!orderStore.schemaMap || !Object.keys(orderStore.schemaMap).length) { orderStore.initSchemaMap() } + await orderStore.ensureProductSizeMatchRules() const res = await api.get('/product-stock-query-by-attributes', { params }) const list = Array.isArray(res?.data) ? res.data : [] @@ -1249,14 +1251,27 @@ async function fetchStockByAttributes() { const grpKey = detectBedenGroup( list.map((x) => x?.Beden || ''), first?.URUN_ANA_GRUBU || '', - first?.YETISKIN_GARSON || '' + first?.KATEGORI || first?.YETISKIN_GARSON || '', + first?.YETISKIN_GARSON || '', + first?.URUN_ALT_GRUBU || '' ) const schemaMap = Object.keys(orderStore.schemaMap || {}).length ? orderStore.schemaMap : storeSchemaByKey - activeGrpKey.value = grpKey || 'tak' - activeSchema.value = schemaMap?.[grpKey] || storeSchemaByKey.tak + if (!grpKey || !schemaMap?.[grpKey]) { + rawRows.value = [] + openState.value = {} + errorMessage.value = 'Beden grubu eşleşmesi bulunamadı.' + $q.notify({ + type: 'negative', + position: 'top-right', + message: 'Beden grubu eşleşmesi bulunamadı (kategori/ana grup/alt grup).' + }) + return + } + activeGrpKey.value = grpKey + activeSchema.value = schemaMap[grpKey] rawRows.value = list productImageCache.value = {} @@ -1491,7 +1506,8 @@ function resetForm() { void loadFilterOptions(true) } -onMounted(() => { +onMounted(async () => { + await orderStore.ensureProductSizeMatchRules() void loadFilterOptions(true) window.addEventListener('mousemove', onFullscreenMouseMove) window.addEventListener('mouseup', onFullscreenMouseUp) diff --git a/ui/src/pages/ProductStockQuery.vue b/ui/src/pages/ProductStockQuery.vue index 9f1249c..7b8307d 100644 --- a/ui/src/pages/ProductStockQuery.vue +++ b/ui/src/pages/ProductStockQuery.vue @@ -850,7 +850,8 @@ function toggleAllDetails() { function buildLevel3Rows(grp3) { const byKey = new Map() - const gk = activeGrpKey.value || 'tak' + const gk = activeGrpKey.value + if (!gk) return [] for (const item of grp3.items || []) { const model = String(item.Urun_Kodu || '').trim() @@ -1045,6 +1046,7 @@ async function fetchStockByCode() { if (!orderStore.schemaMap || !Object.keys(orderStore.schemaMap).length) { orderStore.initSchemaMap() } + await orderStore.ensureProductSizeMatchRules() const res = await api.get('/product-stock-query', { params: { code } }) const list = Array.isArray(res?.data) ? res.data : [] @@ -1059,14 +1061,27 @@ async function fetchStockByCode() { const grpKey = detectBedenGroup( list.map((x) => x?.Beden || ''), first?.URUN_ANA_GRUBU || '', - first?.YETISKIN_GARSON || '' + first?.KATEGORI || first?.YETISKIN_GARSON || '', + first?.YETISKIN_GARSON || '', + first?.URUN_ALT_GRUBU || '' ) const schemaMap = Object.keys(orderStore.schemaMap || {}).length ? orderStore.schemaMap : storeSchemaByKey - activeGrpKey.value = grpKey || 'tak' - activeSchema.value = schemaMap?.[grpKey] || storeSchemaByKey.tak + if (!grpKey || !schemaMap?.[grpKey]) { + rawRows.value = [] + openState.value = {} + errorMessage.value = 'Beden grubu eşleşmesi bulunamadı.' + $q.notify({ + type: 'negative', + position: 'top-right', + message: 'Beden grubu eşleşmesi bulunamadı (kategori/ana grup/alt grup).' + }) + return + } + activeGrpKey.value = grpKey + activeSchema.value = schemaMap[grpKey] rawRows.value = list productImageCache.value = {} @@ -1308,7 +1323,8 @@ onUnmounted(() => { productImageBlobUrls.value = [] }) -onMounted(() => { +onMounted(async () => { + await orderStore.ensureProductSizeMatchRules() loadProductOptions() window.addEventListener('mousemove', onFullscreenMouseMove) window.addEventListener('mouseup', onFullscreenMouseUp) diff --git a/ui/src/stores/orderentryStore.js b/ui/src/stores/orderentryStore.js index 6da0839..3061721 100644 --- a/ui/src/stores/orderentryStore.js +++ b/ui/src/stores/orderentryStore.js @@ -57,6 +57,60 @@ export const schemaByKey = BEDEN_SCHEMA.reduce((m, g) => { return m }, {}) +const productSizeMatchCache = { + loaded: false, + rules: [], + schemas: {} +} + +function resetProductSizeMatchCache() { + productSizeMatchCache.loaded = false + productSizeMatchCache.rules = [] + productSizeMatchCache.schemas = {} +} + +function setProductSizeMatchCache(payload) { + const rules = Array.isArray(payload?.rules) ? payload.rules : [] + const schemasRaw = payload?.schemas && typeof payload.schemas === 'object' + ? payload.schemas + : {} + + const normalizedRules = rules + .map(r => ({ + productGroupID: Number(r?.product_group_id || r?.productGroupID || 0), + kategori: normalizeTextForMatch(r?.kategori || ''), + urunAnaGrubu: normalizeTextForMatch(r?.urun_ana_grubu || r?.urunAnaGrubu || ''), + urunAltGrubu: normalizeTextForMatch(r?.urun_alt_grubu || r?.urunAltGrubu || ''), + groupKeys: Array.isArray(r?.group_keys || r?.groupKeys) + ? (r.group_keys || r.groupKeys).map(g => String(g || '').trim()).filter(Boolean) + : [] + })) + .filter(r => r.groupKeys.length > 0) + .sort((a, b) => { + if (a.productGroupID && b.productGroupID) return a.productGroupID - b.productGroupID + return 0 + }) + + const normalizedSchemas = {} + for (const [k, vals] of Object.entries(schemasRaw)) { + const key = String(k || '').trim() + if (!key) continue + const arr = Array.isArray(vals) + ? vals + : String(vals || '').split(',') + normalizedSchemas[key] = arr + .map(v => { + const s = String(v == null ? '' : v).trim() + return s === '' ? ' ' : s + }) + .filter((v, idx, all) => all.indexOf(v) === idx) + } + + productSizeMatchCache.loaded = true + productSizeMatchCache.rules = normalizedRules + productSizeMatchCache.schemas = normalizedSchemas +} + export const stockMap = ref({}) export const bedenStock = ref([]) @@ -228,6 +282,28 @@ export const useOrderEntryStore = defineStore('orderentry', { ) }, + async ensureProductSizeMatchRules($q = null, force = false) { + if (!force && productSizeMatchCache.loaded && productSizeMatchCache.rules.length > 0) { + return true + } + + try { + const res = await api.get('/product-size-match/rules') + setProductSizeMatchCache(res?.data || {}) + return true + } catch (err) { + if (force) { + resetProductSizeMatchCache() + } + console.warn('⚠ product-size-match rules alınamadı:', err) + $q?.notify?.({ + type: 'warning', + message: 'Beden eşleme kuralları alınamadı.' + }) + return false + } + }, + getRowKey(row) { if (!row) return null @@ -923,6 +999,14 @@ export const useOrderEntryStore = defineStore('orderentry', { try { // geçici varsayım (sonra isClosed durumuna göre set edilecek) this.setMode?.('edit') + const rulesReady = await this.ensureProductSizeMatchRules?.($q) + if (!rulesReady) { + $q?.notify?.({ + type: 'negative', + message: 'Beden eşleme kuralları yüklenemedi.' + }) + return false + } /* ======================================================= 🔹 BACKEND — authoritative load @@ -1774,6 +1858,13 @@ export const useOrderEntryStore = defineStore('orderentry', { form.tutar = Number((adet * Number(form.fiyat || 0)).toFixed(2)) const newRow = toSummaryRowFromForm(form) + if (!newRow) { + $q?.notify?.({ + type: 'negative', + message: 'Beden grubu eşleşmesi bulunamadı.' + }) + return false + } /* ======================================================= 5️⃣ EDIT MODE (editingKey ZORUNLU) @@ -2327,14 +2418,15 @@ export const useOrderEntryStore = defineStore('orderentry', { detectBedenGroup( Object.keys(srcMap || {}), raw.urunAnaGrubu || raw.UrunAnaGrubu || '', - raw.kategori || raw.Kategori || raw.urunAltGrubu || raw.UrunAltGrubu || '', + raw.kategori || raw.Kategori || '', raw.yetiskinGarson || raw.YETISKIN_GARSON || raw.YetiskinGarson || raw.AskiliYan || raw.ASKILIYAN || raw.askiliyan || - '' + '', + raw.urunAltGrubu || raw.UrunAltGrubu || '' ) || 'tak' @@ -2393,7 +2485,12 @@ export const useOrderEntryStore = defineStore('orderentry', { urunAnaGrubu: raw.UrunAnaGrubu || 'GENEL', urunAltGrubu: raw.UrunAltGrubu || '', - kategori: raw.Kategori || raw.UrunAltGrubu || '', + kategori: + raw.Kategori || + raw.YETISKIN_GARSON || + raw.YetiskinGarson || + raw.yetiskinGarson || + '', yetiskinGarson: raw.YETISKIN_GARSON || raw.YetiskinGarson || @@ -2458,8 +2555,9 @@ export const useOrderEntryStore = defineStore('orderentry', { const grpKey = detectBedenGroup( bedenList, row.urunAnaGrubu, - row.kategori || row.urunAltGrubu, - row.yetiskinGarson + row.kategori || '', + row.yetiskinGarson, + row.urunAltGrubu || '' ) const cleanedMap = { ...row.__tmpMap } @@ -2655,11 +2753,36 @@ export const useOrderEntryStore = defineStore('orderentry', { // 🔸 GRUP ANAHTARI TESPİTİ // ======================================================= activeGroupKeyForRow(row) { + const bedenSet = new Set() + + if (row?.bedenMap && typeof row.bedenMap === 'object') { + const grp = row?.grpKey && row.bedenMap[row.grpKey] && typeof row.bedenMap[row.grpKey] === 'object' + ? row.bedenMap[row.grpKey] + : null + + if (grp) { + Object.keys(grp).forEach(k => bedenSet.add(String(k || ''))) + } else { + Object.values(row.bedenMap).forEach(m => { + if (m && typeof m === 'object') { + Object.keys(m).forEach(k => bedenSet.add(String(k || ''))) + } + }) + } + } + + if (bedenSet.size === 0 && Array.isArray(row?.bedenLabels)) { + row.bedenLabels.forEach(lbl => { + bedenSet.add(String(lbl == null ? '' : lbl)) + }) + } + return detectBedenGroup( - null, + Array.from(bedenSet), row?.urunAnaGrubu || '', - row?.kategori || row?.urunAltGrubu || '', - row?.YETISKIN_GARSON || row?.yetiskinGarson || '' + row?.kategori || '', + row?.YETISKIN_GARSON || row?.yetiskinGarson || '', + row?.urunAltGrubu || '' ) }, /* ======================================================= @@ -3621,16 +3744,113 @@ export function normalizeBeden(v) { return normalizeBedenLabel(v) } +function deriveKategoriToken(urunKategori = '', yetiskinGarson = '') { + const kat = normalizeTextForMatch(urunKategori || '') + if (kat.includes('GARSON')) return 'GARSON' + if (kat.includes('YETISKIN')) return 'YETISKIN' + return '' +} + +function normalizeRuleAltGroup(urunAltGrubu = '') { + return normalizeTextForMatch(urunAltGrubu || '') +} + +function pickBestGroupFromCandidates(groupKeys = [], bedenList = []) { + if (!Array.isArray(groupKeys) || groupKeys.length === 0) return '' + if (groupKeys.length === 1) return groupKeys[0] + + const normalizedBeden = (Array.isArray(bedenList) ? bedenList : []) + .map(v => normalizeBedenLabel(v)) + .filter(Boolean) + + if (!normalizedBeden.length) return groupKeys[0] + + let bestKey = groupKeys[0] + let bestScore = -1 + + for (const key of groupKeys) { + const schema = Array.isArray(productSizeMatchCache.schemas?.[key]) + ? productSizeMatchCache.schemas[key] + : [] + const normalizedSchema = new Set(schema.map(v => normalizeBedenLabel(v))) + let score = 0 + for (const b of normalizedBeden) { + if (normalizedSchema.has(b)) score += 1 + } + if (score > bestScore) { + bestScore = score + bestKey = key + } + } + + return bestKey || groupKeys[0] +} + +function resolveGroupFromProductSizeMatchRules( + bedenList, + urunAnaGrubu = '', + urunKategori = '', + yetiskinGarson = '', + urunAltGrubu = '' +) { + if (!productSizeMatchCache.loaded || !Array.isArray(productSizeMatchCache.rules) || !productSizeMatchCache.rules.length) { + return '' + } + + const kategoriToken = deriveKategoriToken(urunKategori, yetiskinGarson) + const ana = normalizeTextForMatch(urunAnaGrubu || '') + const alt = normalizeRuleAltGroup(urunAltGrubu) + if (!kategoriToken || !ana) return '' + + const candidateGroupKeys = [] + for (const rule of productSizeMatchCache.rules) { + if (!rule?.urunAnaGrubu || rule.urunAnaGrubu !== ana) continue + if (rule.kategori !== kategoriToken) continue + const ruleAlt = normalizeTextForMatch(rule.urunAltGrubu || '') + if (ruleAlt !== alt) continue + for (const g of (rule.groupKeys || [])) { + const key = String(g || '').trim() + if (key && !candidateGroupKeys.includes(key)) { + candidateGroupKeys.push(key) + } + } + } + + if (!candidateGroupKeys.length) return '' + return pickBestGroupFromCandidates(candidateGroupKeys, bedenList) +} + /* =========================================================== Size Group Detection - Core logic aligned with backend detectBedenGroupGo - Keeps frontend aksbir bucket for accessory lines =========================================================== */ -export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '', yetiskinGarson = '') { +export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '', yetiskinGarson = '', urunAltGrubu = '') { const list = Array.isArray(bedenList) && bedenList.length > 0 ? bedenList.map(v => (v || '').toString().trim().toUpperCase()) : [' '] + const ruleBased = resolveGroupFromProductSizeMatchRules( + list, + urunAnaGrubu, + urunKategori, + yetiskinGarson, + urunAltGrubu + ) + if (productSizeMatchCache.loaded) { + if (!ruleBased) { + console.warn('⚠ product-size-match eşleşme bulunamadı', { + kategori: deriveKategoriToken(urunKategori, yetiskinGarson), + urunAnaGrubu: normalizeTextForMatch(urunAnaGrubu || ''), + urunAltGrubu: normalizeRuleAltGroup(urunAltGrubu), + bedenList: list + }) + } + return ruleBased || '' + } + + return '' + const rawAna = normalizeTextForMatch(urunAnaGrubu || '') const rawKat = normalizeTextForMatch(urunKategori || '') const rawYetiskinGarson = normalizeTextForMatch(yetiskinGarson || '') @@ -3672,6 +3892,12 @@ export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '' return 'yas' } + // Ayakkabi kurali garsondan once uygulanmali: + // GARSON + AYAKKABI => ayk_garson, digerleri => ayk + if (mappedRawAna.includes('AYAKKABI') || rawKat.includes('AYAKKABI')) { + return hasGarsonSignal ? 'ayk_garson' : 'ayk' + } + const hasGarson = hasGarsonSignal if (hasGarson) return 'yas' @@ -3721,7 +3947,8 @@ export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '' export function toSummaryRowFromForm(form) { if (!form) return null - const grpKey = form.grpKey || 'tak' + const grpKey = form.grpKey + if (!grpKey) return null const bedenMap = {} const labels = Array.isArray(form.bedenLabels) ? form.bedenLabels : []