Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -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/orders/export", "GET", "export", routes.OrderListExcelRoute(mssql)},
|
||||||
{"/api/order/check/{id}", "GET", "view", routes.OrderExistsHandler(mssql)},
|
{"/api/order/check/{id}", "GET", "view", routes.OrderExistsHandler(mssql)},
|
||||||
{"/api/order/validate", "POST", "insert", routes.ValidateOrderHandler(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/send-market-mail", "POST", "read", routes.SendOrderMarketMailHandler(pgDB, mssql, ml)},
|
||||||
{"/api/order-inventory", "GET", "view", http.HandlerFunc(routes.GetOrderInventoryHandler)},
|
{"/api/order-inventory", "GET", "view", http.HandlerFunc(routes.GetOrderInventoryHandler)},
|
||||||
{"/api/orderpricelistb2b", "GET", "view", routes.GetOrderPriceListB2BHandler(pgDB, mssql)},
|
{"/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",
|
"order", "view",
|
||||||
http.HandlerFunc(routes.GetProductImageContentHandler(pgDB)),
|
http.HandlerFunc(routes.GetProductImageContentHandler(pgDB)),
|
||||||
)
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/product-size-match/rules", "GET",
|
||||||
|
"order", "view",
|
||||||
|
wrapV3(routes.GetProductSizeMatchRulesHandler(pgDB)),
|
||||||
|
)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// ROLE MANAGEMENT
|
// ROLE MANAGEMENT
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -71,7 +72,7 @@ func SendOrderMarketMailHandler(pg *sql.DB, mssql *sql.DB, ml *mailer.GraphMaile
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pdfBytes, header, err := buildOrderPDFBytesForMail(mssql, orderID)
|
pdfBytes, header, err := buildOrderPDFBytesForMail(mssql, pg, orderID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "pdf build error: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "pdf build error: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -241,7 +242,7 @@ ORDER BY email
|
|||||||
return out, nil
|
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)
|
header, err := getOrderHeaderFromDB(db, orderID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
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()
|
pdf, err := newOrderPdf()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ type OrderLineRaw struct {
|
|||||||
LineDescription sql.NullString
|
LineDescription sql.NullString
|
||||||
UrunAnaGrubu sql.NullString
|
UrunAnaGrubu sql.NullString
|
||||||
UrunAltGrubu sql.NullString
|
UrunAltGrubu sql.NullString
|
||||||
|
YetiskinGarson sql.NullString
|
||||||
IsClosed sql.NullBool
|
IsClosed sql.NullBool
|
||||||
WithHoldingTaxType sql.NullString
|
WithHoldingTaxType sql.NullString
|
||||||
DOVCode sql.NullString
|
DOVCode sql.NullString
|
||||||
@@ -101,20 +102,21 @@ type OrderLineRaw struct {
|
|||||||
=========================================================== */
|
=========================================================== */
|
||||||
|
|
||||||
type PdfRow struct {
|
type PdfRow struct {
|
||||||
Model string
|
Model string
|
||||||
Color string
|
Color string
|
||||||
GroupMain string
|
GroupMain string
|
||||||
GroupSub string
|
GroupSub string
|
||||||
Description string
|
YetiskinGarson string
|
||||||
Category string
|
Description string
|
||||||
SizeQty map[string]int
|
Category string
|
||||||
TotalQty int
|
SizeQty map[string]int
|
||||||
Price float64
|
TotalQty int
|
||||||
Currency string
|
Price float64
|
||||||
Amount float64
|
Currency string
|
||||||
Termin string
|
Amount float64
|
||||||
IsClosed bool
|
Termin string
|
||||||
OrderLineIDs map[string]string
|
IsClosed bool
|
||||||
|
OrderLineIDs map[string]string
|
||||||
|
|
||||||
ClosedSizes map[string]bool // 🆕 her beden için IsClosed bilgisi
|
ClosedSizes map[string]bool // 🆕 her beden için IsClosed bilgisi
|
||||||
}
|
}
|
||||||
@@ -332,7 +334,140 @@ func parseNumericSize(v string) (int, bool) {
|
|||||||
return n, true
|
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)
|
ana = normalizeTextForMatchGo(ana)
|
||||||
alt = normalizeTextForMatchGo(alt)
|
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İŞKIN/GARSON") || strings.Contains(alt, "YETİŞKIN/GARSON") ||
|
||||||
strings.Contains(ana, "YETİŞKİN/GARSON") || strings.Contains(alt, "YETİŞKİN/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)
|
// ✅ Garson → yaş (ürün tipi fark etmeksizin)
|
||||||
if hasGarson {
|
if hasGarson {
|
||||||
return catYas
|
return catYas
|
||||||
@@ -618,6 +762,7 @@ func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) {
|
|||||||
L.LineDescription,
|
L.LineDescription,
|
||||||
P.ProductAtt01Desc,
|
P.ProductAtt01Desc,
|
||||||
P.ProductAtt02Desc,
|
P.ProductAtt02Desc,
|
||||||
|
P.ProductAtt44Desc,
|
||||||
L.IsClosed,
|
L.IsClosed,
|
||||||
L.WithHoldingTaxTypeCode,
|
L.WithHoldingTaxTypeCode,
|
||||||
L.DOVCode,
|
L.DOVCode,
|
||||||
@@ -656,6 +801,7 @@ func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) {
|
|||||||
&l.LineDescription,
|
&l.LineDescription,
|
||||||
&l.UrunAnaGrubu,
|
&l.UrunAnaGrubu,
|
||||||
&l.UrunAltGrubu,
|
&l.UrunAltGrubu,
|
||||||
|
&l.YetiskinGarson,
|
||||||
&l.IsClosed,
|
&l.IsClosed,
|
||||||
&l.WithHoldingTaxType,
|
&l.WithHoldingTaxType,
|
||||||
&l.DOVCode,
|
&l.DOVCode,
|
||||||
@@ -675,7 +821,7 @@ func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) {
|
|||||||
4) NORMALIZE + CATEGORY MAP
|
4) NORMALIZE + CATEGORY MAP
|
||||||
=========================================================== */
|
=========================================================== */
|
||||||
|
|
||||||
func normalizeOrderLinesForPdf(lines []OrderLineRaw) []PdfRow {
|
func normalizeOrderLinesForPdf(lines []OrderLineRaw, matchData *ProductSizeMatchResponse) []PdfRow {
|
||||||
type comboKey struct {
|
type comboKey struct {
|
||||||
Model, Color, Color2 string
|
Model, Color, Color2 string
|
||||||
}
|
}
|
||||||
@@ -701,16 +847,17 @@ func normalizeOrderLinesForPdf(lines []OrderLineRaw) []PdfRow {
|
|||||||
|
|
||||||
if _, ok := merged[key]; !ok {
|
if _, ok := merged[key]; !ok {
|
||||||
merged[key] = &PdfRow{
|
merged[key] = &PdfRow{
|
||||||
Model: model,
|
Model: model,
|
||||||
Color: displayColor,
|
Color: displayColor,
|
||||||
GroupMain: s64(raw.UrunAnaGrubu),
|
GroupMain: s64(raw.UrunAnaGrubu),
|
||||||
GroupSub: s64(raw.UrunAltGrubu),
|
GroupSub: s64(raw.UrunAltGrubu),
|
||||||
Description: s64(raw.LineDescription),
|
YetiskinGarson: s64(raw.YetiskinGarson),
|
||||||
SizeQty: make(map[string]int),
|
Description: s64(raw.LineDescription),
|
||||||
Currency: s64(raw.DocCurrencyCode),
|
SizeQty: make(map[string]int),
|
||||||
Price: f64(raw.Price),
|
Currency: s64(raw.DocCurrencyCode),
|
||||||
OrderLineIDs: make(map[string]string),
|
Price: f64(raw.Price),
|
||||||
ClosedSizes: make(map[string]bool), // 🆕
|
OrderLineIDs: make(map[string]string),
|
||||||
|
ClosedSizes: make(map[string]bool), // 🆕
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
row := merged[key]
|
row := merged[key]
|
||||||
@@ -751,7 +898,7 @@ func normalizeOrderLinesForPdf(lines []OrderLineRaw) []PdfRow {
|
|||||||
for s := range r.SizeQty {
|
for s := range r.SizeQty {
|
||||||
sizes = append(sizes, s)
|
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
|
r.Amount = float64(r.TotalQty) * r.Price
|
||||||
out = append(out, *r)
|
out = append(out, *r)
|
||||||
}
|
}
|
||||||
@@ -1555,7 +1702,7 @@ func renderOrderGrid(pdf *gofpdf.Fpdf, header *OrderHeader, rows []PdfRow, hasVa
|
|||||||
HTTP HANDLER → /api/order/pdf/{id}
|
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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
orderID := mux.Vars(r)["id"]
|
orderID := mux.Vars(r)["id"]
|
||||||
@@ -1612,7 +1759,30 @@ func OrderPDFHandler(db *sql.DB) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Normalize
|
// 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))
|
log.Printf("📄 OrderPDF normalized rows orderID=%s rowCount=%d", orderID, len(rows))
|
||||||
for i, rr := range rows {
|
for i, rr := range rows {
|
||||||
if i >= 30 {
|
if i >= 30 {
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import (
|
|||||||
"bssapp-backend/queries"
|
"bssapp-backend/queries"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -79,6 +81,16 @@ func GetOrderInventoryHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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))
|
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")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|||||||
106
svc/routes/product_size_match.go
Normal file
106
svc/routes/product_size_match.go
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -835,6 +835,7 @@ const $q = useQuasar()
|
|||||||
const orderStore = useOrderEntryStore()
|
const orderStore = useOrderEntryStore()
|
||||||
const orderentryStore = useOrderEntryStore()
|
const orderentryStore = useOrderEntryStore()
|
||||||
orderStore.initSchemaMap()
|
orderStore.initSchemaMap()
|
||||||
|
void orderStore.ensureProductSizeMatchRules()
|
||||||
|
|
||||||
const schemaSource = computed(() =>
|
const schemaSource = computed(() =>
|
||||||
Object.keys(orderStore?.schemaMap || {}).length
|
Object.keys(orderStore?.schemaMap || {}).length
|
||||||
@@ -1777,6 +1778,7 @@ watch(() => orderStore.replaceRouteSignal, async (id) => {
|
|||||||
|
|
||||||
/* -------------------- LIFECYCLE -------------------- */
|
/* -------------------- LIFECYCLE -------------------- */
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
await orderStore.ensureProductSizeMatchRules()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
/* ---------------- UI ---------------- */
|
/* ---------------- UI ---------------- */
|
||||||
@@ -2671,17 +2673,16 @@ async function onModelChange(modelCode) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
/* =======================================================
|
/* =======================================================
|
||||||
🔑 BEDEN GRUBU — TEK VE KESİN KARAR
|
🔑 BEDEN GRUBU — TEK VE KESİN KARAR (SQL kural tabanı)
|
||||||
- ÖNCE detectBedenGroup (accent/garson/yas kurallarını içerir)
|
|
||||||
- Sonra güvenli fallback
|
|
||||||
======================================================= */
|
======================================================= */
|
||||||
let bedenGrpKey = null
|
let bedenGrpKey = null
|
||||||
try {
|
try {
|
||||||
bedenGrpKey = detectBedenGroup(
|
bedenGrpKey = detectBedenGroup(
|
||||||
null,
|
null,
|
||||||
form.urunAnaGrubu,
|
form.urunAnaGrubu,
|
||||||
form.kategori || form.urunAltGrubu,
|
form.kategori || '',
|
||||||
form.yetiskinGarson || form.askiliyan
|
form.yetiskinGarson || form.askiliyan,
|
||||||
|
form.urunAltGrubu || ''
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('⚠️ detectBedenGroup hata:', e)
|
console.warn('⚠️ detectBedenGroup hata:', e)
|
||||||
@@ -2689,42 +2690,13 @@ async function onModelChange(modelCode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!bedenGrpKey) {
|
if (!bedenGrpKey) {
|
||||||
const anaNRaw = String(form.urunAnaGrubu || '')
|
$q.notify({
|
||||||
.normalize('NFD')
|
type: 'negative',
|
||||||
.replace(/[\u0300-\u036f]/g, '')
|
message: 'Beden grubu eşleşmesi bulunamadı (kategori/ana grup/alt grup).'
|
||||||
.toLowerCase()
|
})
|
||||||
.trim()
|
return
|
||||||
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'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Son fallback
|
|
||||||
if (!bedenGrpKey) bedenGrpKey = 'tak'
|
|
||||||
|
|
||||||
form.grpKey = bedenGrpKey
|
form.grpKey = bedenGrpKey
|
||||||
console.log('🧭 Editor grpKey set edildi →', bedenGrpKey)
|
console.log('🧭 Editor grpKey set edildi →', bedenGrpKey)
|
||||||
// ✅ Editor bedenleri hemen aç (UI seed) — schemaMap tek kaynak
|
// ✅ Editor bedenleri hemen aç (UI seed) — schemaMap tek kaynak
|
||||||
|
|||||||
@@ -875,7 +875,8 @@ function toggleAllDetails() {
|
|||||||
|
|
||||||
function buildLevel3Rows(grp3) {
|
function buildLevel3Rows(grp3) {
|
||||||
const byKey = new Map()
|
const byKey = new Map()
|
||||||
const gk = activeGrpKey.value || 'tak'
|
const gk = activeGrpKey.value
|
||||||
|
if (!gk) return []
|
||||||
|
|
||||||
for (const item of grp3.items || []) {
|
for (const item of grp3.items || []) {
|
||||||
const model = String(item.Urun_Kodu || '').trim()
|
const model = String(item.Urun_Kodu || '').trim()
|
||||||
@@ -1235,6 +1236,7 @@ async function fetchStockByAttributes() {
|
|||||||
if (!orderStore.schemaMap || !Object.keys(orderStore.schemaMap).length) {
|
if (!orderStore.schemaMap || !Object.keys(orderStore.schemaMap).length) {
|
||||||
orderStore.initSchemaMap()
|
orderStore.initSchemaMap()
|
||||||
}
|
}
|
||||||
|
await orderStore.ensureProductSizeMatchRules()
|
||||||
|
|
||||||
const res = await api.get('/product-stock-query-by-attributes', { params })
|
const res = await api.get('/product-stock-query-by-attributes', { params })
|
||||||
const list = Array.isArray(res?.data) ? res.data : []
|
const list = Array.isArray(res?.data) ? res.data : []
|
||||||
@@ -1249,14 +1251,27 @@ async function fetchStockByAttributes() {
|
|||||||
const grpKey = detectBedenGroup(
|
const grpKey = detectBedenGroup(
|
||||||
list.map((x) => x?.Beden || ''),
|
list.map((x) => x?.Beden || ''),
|
||||||
first?.URUN_ANA_GRUBU || '',
|
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
|
const schemaMap = Object.keys(orderStore.schemaMap || {}).length
|
||||||
? orderStore.schemaMap
|
? orderStore.schemaMap
|
||||||
: storeSchemaByKey
|
: storeSchemaByKey
|
||||||
activeGrpKey.value = grpKey || 'tak'
|
if (!grpKey || !schemaMap?.[grpKey]) {
|
||||||
activeSchema.value = schemaMap?.[grpKey] || storeSchemaByKey.tak
|
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
|
rawRows.value = list
|
||||||
productImageCache.value = {}
|
productImageCache.value = {}
|
||||||
@@ -1491,7 +1506,8 @@ function resetForm() {
|
|||||||
void loadFilterOptions(true)
|
void loadFilterOptions(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
|
await orderStore.ensureProductSizeMatchRules()
|
||||||
void loadFilterOptions(true)
|
void loadFilterOptions(true)
|
||||||
window.addEventListener('mousemove', onFullscreenMouseMove)
|
window.addEventListener('mousemove', onFullscreenMouseMove)
|
||||||
window.addEventListener('mouseup', onFullscreenMouseUp)
|
window.addEventListener('mouseup', onFullscreenMouseUp)
|
||||||
|
|||||||
@@ -850,7 +850,8 @@ function toggleAllDetails() {
|
|||||||
|
|
||||||
function buildLevel3Rows(grp3) {
|
function buildLevel3Rows(grp3) {
|
||||||
const byKey = new Map()
|
const byKey = new Map()
|
||||||
const gk = activeGrpKey.value || 'tak'
|
const gk = activeGrpKey.value
|
||||||
|
if (!gk) return []
|
||||||
|
|
||||||
for (const item of grp3.items || []) {
|
for (const item of grp3.items || []) {
|
||||||
const model = String(item.Urun_Kodu || '').trim()
|
const model = String(item.Urun_Kodu || '').trim()
|
||||||
@@ -1045,6 +1046,7 @@ async function fetchStockByCode() {
|
|||||||
if (!orderStore.schemaMap || !Object.keys(orderStore.schemaMap).length) {
|
if (!orderStore.schemaMap || !Object.keys(orderStore.schemaMap).length) {
|
||||||
orderStore.initSchemaMap()
|
orderStore.initSchemaMap()
|
||||||
}
|
}
|
||||||
|
await orderStore.ensureProductSizeMatchRules()
|
||||||
|
|
||||||
const res = await api.get('/product-stock-query', { params: { code } })
|
const res = await api.get('/product-stock-query', { params: { code } })
|
||||||
const list = Array.isArray(res?.data) ? res.data : []
|
const list = Array.isArray(res?.data) ? res.data : []
|
||||||
@@ -1059,14 +1061,27 @@ async function fetchStockByCode() {
|
|||||||
const grpKey = detectBedenGroup(
|
const grpKey = detectBedenGroup(
|
||||||
list.map((x) => x?.Beden || ''),
|
list.map((x) => x?.Beden || ''),
|
||||||
first?.URUN_ANA_GRUBU || '',
|
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
|
const schemaMap = Object.keys(orderStore.schemaMap || {}).length
|
||||||
? orderStore.schemaMap
|
? orderStore.schemaMap
|
||||||
: storeSchemaByKey
|
: storeSchemaByKey
|
||||||
activeGrpKey.value = grpKey || 'tak'
|
if (!grpKey || !schemaMap?.[grpKey]) {
|
||||||
activeSchema.value = schemaMap?.[grpKey] || storeSchemaByKey.tak
|
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
|
rawRows.value = list
|
||||||
productImageCache.value = {}
|
productImageCache.value = {}
|
||||||
@@ -1308,7 +1323,8 @@ onUnmounted(() => {
|
|||||||
productImageBlobUrls.value = []
|
productImageBlobUrls.value = []
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
|
await orderStore.ensureProductSizeMatchRules()
|
||||||
loadProductOptions()
|
loadProductOptions()
|
||||||
window.addEventListener('mousemove', onFullscreenMouseMove)
|
window.addEventListener('mousemove', onFullscreenMouseMove)
|
||||||
window.addEventListener('mouseup', onFullscreenMouseUp)
|
window.addEventListener('mouseup', onFullscreenMouseUp)
|
||||||
|
|||||||
@@ -57,6 +57,60 @@ export const schemaByKey = BEDEN_SCHEMA.reduce((m, g) => {
|
|||||||
return m
|
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 stockMap = ref({})
|
||||||
export const bedenStock = 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) {
|
getRowKey(row) {
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
@@ -923,6 +999,14 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
try {
|
try {
|
||||||
// geçici varsayım (sonra isClosed durumuna göre set edilecek)
|
// geçici varsayım (sonra isClosed durumuna göre set edilecek)
|
||||||
this.setMode?.('edit')
|
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
|
🔹 BACKEND — authoritative load
|
||||||
@@ -1774,6 +1858,13 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
form.tutar = Number((adet * Number(form.fiyat || 0)).toFixed(2))
|
form.tutar = Number((adet * Number(form.fiyat || 0)).toFixed(2))
|
||||||
|
|
||||||
const newRow = toSummaryRowFromForm(form)
|
const newRow = toSummaryRowFromForm(form)
|
||||||
|
if (!newRow) {
|
||||||
|
$q?.notify?.({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Beden grubu eşleşmesi bulunamadı.'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/* =======================================================
|
/* =======================================================
|
||||||
5️⃣ EDIT MODE (editingKey ZORUNLU)
|
5️⃣ EDIT MODE (editingKey ZORUNLU)
|
||||||
@@ -2327,14 +2418,15 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
detectBedenGroup(
|
detectBedenGroup(
|
||||||
Object.keys(srcMap || {}),
|
Object.keys(srcMap || {}),
|
||||||
raw.urunAnaGrubu || raw.UrunAnaGrubu || '',
|
raw.urunAnaGrubu || raw.UrunAnaGrubu || '',
|
||||||
raw.kategori || raw.Kategori || raw.urunAltGrubu || raw.UrunAltGrubu || '',
|
raw.kategori || raw.Kategori || '',
|
||||||
raw.yetiskinGarson ||
|
raw.yetiskinGarson ||
|
||||||
raw.YETISKIN_GARSON ||
|
raw.YETISKIN_GARSON ||
|
||||||
raw.YetiskinGarson ||
|
raw.YetiskinGarson ||
|
||||||
raw.AskiliYan ||
|
raw.AskiliYan ||
|
||||||
raw.ASKILIYAN ||
|
raw.ASKILIYAN ||
|
||||||
raw.askiliyan ||
|
raw.askiliyan ||
|
||||||
''
|
'',
|
||||||
|
raw.urunAltGrubu || raw.UrunAltGrubu || ''
|
||||||
) ||
|
) ||
|
||||||
'tak'
|
'tak'
|
||||||
|
|
||||||
@@ -2393,7 +2485,12 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
|
|
||||||
urunAnaGrubu: raw.UrunAnaGrubu || 'GENEL',
|
urunAnaGrubu: raw.UrunAnaGrubu || 'GENEL',
|
||||||
urunAltGrubu: raw.UrunAltGrubu || '',
|
urunAltGrubu: raw.UrunAltGrubu || '',
|
||||||
kategori: raw.Kategori || raw.UrunAltGrubu || '',
|
kategori:
|
||||||
|
raw.Kategori ||
|
||||||
|
raw.YETISKIN_GARSON ||
|
||||||
|
raw.YetiskinGarson ||
|
||||||
|
raw.yetiskinGarson ||
|
||||||
|
'',
|
||||||
yetiskinGarson:
|
yetiskinGarson:
|
||||||
raw.YETISKIN_GARSON ||
|
raw.YETISKIN_GARSON ||
|
||||||
raw.YetiskinGarson ||
|
raw.YetiskinGarson ||
|
||||||
@@ -2458,8 +2555,9 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
const grpKey = detectBedenGroup(
|
const grpKey = detectBedenGroup(
|
||||||
bedenList,
|
bedenList,
|
||||||
row.urunAnaGrubu,
|
row.urunAnaGrubu,
|
||||||
row.kategori || row.urunAltGrubu,
|
row.kategori || '',
|
||||||
row.yetiskinGarson
|
row.yetiskinGarson,
|
||||||
|
row.urunAltGrubu || ''
|
||||||
)
|
)
|
||||||
|
|
||||||
const cleanedMap = { ...row.__tmpMap }
|
const cleanedMap = { ...row.__tmpMap }
|
||||||
@@ -2655,11 +2753,36 @@ export const useOrderEntryStore = defineStore('orderentry', {
|
|||||||
// 🔸 GRUP ANAHTARI TESPİTİ
|
// 🔸 GRUP ANAHTARI TESPİTİ
|
||||||
// =======================================================
|
// =======================================================
|
||||||
activeGroupKeyForRow(row) {
|
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(
|
return detectBedenGroup(
|
||||||
null,
|
Array.from(bedenSet),
|
||||||
row?.urunAnaGrubu || '',
|
row?.urunAnaGrubu || '',
|
||||||
row?.kategori || row?.urunAltGrubu || '',
|
row?.kategori || '',
|
||||||
row?.YETISKIN_GARSON || row?.yetiskinGarson || ''
|
row?.YETISKIN_GARSON || row?.yetiskinGarson || '',
|
||||||
|
row?.urunAltGrubu || ''
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
/* =======================================================
|
/* =======================================================
|
||||||
@@ -3621,16 +3744,113 @@ export function normalizeBeden(v) {
|
|||||||
return normalizeBedenLabel(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
|
Size Group Detection
|
||||||
- Core logic aligned with backend detectBedenGroupGo
|
- Core logic aligned with backend detectBedenGroupGo
|
||||||
- Keeps frontend aksbir bucket for accessory lines
|
- 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
|
const list = Array.isArray(bedenList) && bedenList.length > 0
|
||||||
? bedenList.map(v => (v || '').toString().trim().toUpperCase())
|
? 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 rawAna = normalizeTextForMatch(urunAnaGrubu || '')
|
||||||
const rawKat = normalizeTextForMatch(urunKategori || '')
|
const rawKat = normalizeTextForMatch(urunKategori || '')
|
||||||
const rawYetiskinGarson = normalizeTextForMatch(yetiskinGarson || '')
|
const rawYetiskinGarson = normalizeTextForMatch(yetiskinGarson || '')
|
||||||
@@ -3672,6 +3892,12 @@ export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = ''
|
|||||||
return 'yas'
|
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
|
const hasGarson = hasGarsonSignal
|
||||||
if (hasGarson) return 'yas'
|
if (hasGarson) return 'yas'
|
||||||
|
|
||||||
@@ -3721,7 +3947,8 @@ export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = ''
|
|||||||
export function toSummaryRowFromForm(form) {
|
export function toSummaryRowFromForm(form) {
|
||||||
if (!form) return null
|
if (!form) return null
|
||||||
|
|
||||||
const grpKey = form.grpKey || 'tak'
|
const grpKey = form.grpKey
|
||||||
|
if (!grpKey) return null
|
||||||
const bedenMap = {}
|
const bedenMap = {}
|
||||||
|
|
||||||
const labels = Array.isArray(form.bedenLabels) ? form.bedenLabels : []
|
const labels = Array.isArray(form.bedenLabels) ? form.bedenLabels : []
|
||||||
|
|||||||
Reference in New Issue
Block a user