Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-02-20 11:20:53 +03:00
parent d9c527d13f
commit 32d0c38ab9
11 changed files with 1264 additions and 122 deletions

View File

@@ -0,0 +1,27 @@
/* Indexes for order validate performance */
IF NOT EXISTS (
SELECT 1
FROM sys.indexes
WHERE name = 'IX_trOrderLine_OrderHeader_ItemCode'
AND object_id = OBJECT_ID('dbo.trOrderLine')
)
BEGIN
CREATE NONCLUSTERED INDEX IX_trOrderLine_OrderHeader_ItemCode
ON dbo.trOrderLine (OrderHeaderID, ItemCode)
INCLUDE (ItemTypeCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, LineDescription, SortOrder, OrderLineID);
END
GO
IF NOT EXISTS (
SELECT 1
FROM sys.indexes
WHERE name = 'IX_prItemVariant_Combo'
AND object_id = OBJECT_ID('dbo.prItemVariant')
)
BEGIN
CREATE NONCLUSTERED INDEX IX_prItemVariant_Combo
ON dbo.prItemVariant (ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code)
INCLUDE (PLU);
END
GO

View File

@@ -441,6 +441,9 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
{"/api/orders/list", "GET", "view", routes.OrderListRoute(mssql)},
{"/api/orders/production-list", "GET", "update", routes.OrderProductionListRoute(mssql)},
{"/api/orders/production-items/{id}", "GET", "view", routes.OrderProductionItemsRoute(mssql)},
{"/api/orders/production-items/{id}/insert-missing", "POST", "update", routes.OrderProductionInsertMissingRoute(mssql)},
{"/api/orders/production-items/{id}/validate", "POST", "update", routes.OrderProductionValidateRoute(mssql)},
{"/api/orders/production-items/{id}/apply", "POST", "update", routes.OrderProductionApplyRoute(mssql)},
{"/api/orders/close-ready", "GET", "update", routes.OrderCloseReadyListRoute(mssql)},
{"/api/orders/bulk-close", "POST", "update", routes.OrderBulkCloseRoute(mssql)},
{"/api/orders/export", "GET", "export", routes.OrderListExcelRoute(mssql)},

View File

@@ -7,6 +7,10 @@ type OrderProductionItem struct {
OrderHeaderID string `json:"OrderHeaderID"`
OrderLineID string `json:"OrderLineID"`
ItemTypeCode int16 `json:"ItemTypeCode"`
OldDim1 string `json:"OldDim1"`
OldDim3 string `json:"OldDim3"`
OldItemCode string `json:"OldItemCode"`
OldColor string `json:"OldColor"`
OldDim2 string `json:"OldDim2"`
@@ -16,4 +20,6 @@ type OrderProductionItem struct {
NewColor string `json:"NewColor"`
NewDim2 string `json:"NewDim2"`
NewDesc string `json:"NewDesc"`
IsVariantMissing bool `json:"IsVariantMissing"`
}

View File

@@ -0,0 +1,24 @@
package models
type OrderProductionUpdateLine struct {
OrderLineID string `json:"OrderLineID"`
NewItemCode string `json:"NewItemCode"`
NewColor string `json:"NewColor"`
NewDim2 string `json:"NewDim2"`
NewDesc string `json:"NewDesc"`
}
type OrderProductionUpdatePayload struct {
Lines []OrderProductionUpdateLine `json:"lines"`
InsertMissing bool `json:"insertMissing"`
}
type OrderProductionMissingVariant struct {
OrderLineID string `json:"OrderLineID"`
ItemTypeCode int16 `json:"ItemTypeCode"`
ItemCode string `json:"ItemCode"`
ColorCode string `json:"ColorCode"`
ItemDim1Code string `json:"ItemDim1Code"`
ItemDim2Code string `json:"ItemDim2Code"`
ItemDim3Code string `json:"ItemDim3Code"`
}

View File

@@ -2,6 +2,8 @@ package queries
import (
"database/sql"
"bssapp-backend/models"
)
// ========================================================
@@ -12,20 +14,209 @@ func GetOrderProductionItems(mssql *sql.DB, orderHeaderID string) (*sql.Rows, er
SELECT
CAST(l.OrderHeaderID AS NVARCHAR(50)) AS OrderHeaderID,
CAST(l.OrderLineID AS NVARCHAR(50)) AS OrderLineID,
l.ItemTypeCode AS ItemTypeCode,
ISNULL(l.ItemDim1Code,'') AS OldDim1,
ISNULL(l.ItemDim3Code,'') AS OldDim3,
-- Şimdilik eski alanlar boş döndürülür
CAST('' AS NVARCHAR(50)) AS OldItemCode,
CAST('' AS NVARCHAR(50)) AS OldColor,
CAST('' AS NVARCHAR(50)) AS OldDim2,
CAST('' AS NVARCHAR(250)) AS OldDesc,
ISNULL(l.ItemCode,'') AS OldItemCode,
ISNULL(l.ColorCode,'') AS OldColor,
ISNULL(l.ItemDim2Code,'') AS OldDim2,
ISNULL(l.LineDescription,'') AS OldDesc,
ISNULL(l.ItemCode,'') AS NewItemCode,
ISNULL(l.ColorCode,'') AS NewColor,
ISNULL(l.ItemDim2Code,'') AS NewDim2,
ISNULL(l.LineDescription,'') AS NewDesc
CAST('' AS NVARCHAR(60)) AS NewItemCode,
CAST('' AS NVARCHAR(30)) AS NewColor,
CAST('' AS NVARCHAR(30)) AS NewDim2,
CAST('' AS NVARCHAR(250)) AS NewDesc,
CAST(0 AS bit) AS IsVariantMissing
FROM dbo.trOrderLine l
WHERE l.OrderHeaderID = @p1
AND ISNULL(l.ItemCode,'') LIKE 'U%'
ORDER BY l.SortOrder, l.OrderLineID
`, orderHeaderID)
}
// ========================================================
// 📌 InsertMissingProductionVariants — eksik prItemVariant ekler
// ========================================================
func InsertMissingProductionVariants(mssql *sql.DB, orderHeaderID string, username string) (int64, error) {
query := `
;WITH Missing AS (
SELECT DISTINCT
l.ItemTypeCode,
l.ItemCode,
l.ColorCode,
l.ItemDim1Code,
l.ItemDim2Code,
l.ItemDim3Code
FROM dbo.trOrderLine l
LEFT JOIN dbo.prItemVariant pv
ON pv.ItemTypeCode = l.ItemTypeCode
AND pv.ItemCode = l.ItemCode
AND pv.ColorCode = l.ColorCode
AND ISNULL(pv.ItemDim1Code,'') = ISNULL(l.ItemDim1Code,'')
AND ISNULL(pv.ItemDim2Code,'') = ISNULL(l.ItemDim2Code,'')
AND ISNULL(pv.ItemDim3Code,'') = ISNULL(l.ItemDim3Code,'')
WHERE l.OrderHeaderID = @p1
AND ISNULL(l.ItemCode,'') LIKE 'U%'
AND pv.ItemCode IS NULL
),
MaxPlu AS (
SELECT ISNULL(MAX(PLU),0) AS BasePlu
FROM dbo.prItemVariant WITH (UPDLOCK, HOLDLOCK)
)
INSERT INTO dbo.prItemVariant (
ItemTypeCode,
ItemCode,
ColorCode,
ItemDim1Code,
ItemDim2Code,
ItemDim3Code,
PLU,
CreatedUserName,
CreatedDate,
LastUpdatedUserName,
LastUpdatedDate
)
SELECT
m.ItemTypeCode,
m.ItemCode,
m.ColorCode,
m.ItemDim1Code,
m.ItemDim2Code,
m.ItemDim3Code,
mp.BasePlu + ROW_NUMBER() OVER (ORDER BY m.ItemCode, m.ColorCode, m.ItemDim1Code, m.ItemDim2Code, m.ItemDim3Code),
@p2,
GETDATE(),
@p2,
GETDATE()
FROM Missing m
CROSS JOIN MaxPlu mp;
`
res, err := mssql.Exec(query, orderHeaderID, username)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
// ========================================================
// OrderProductionUpdate - variant kontrolu ve guncelleme
// ========================================================
func GetOrderLineDims(mssql *sql.DB, orderHeaderID string, orderLineID string) (int16, string, string, string, error) {
var itemTypeCode int16
var dim1 string
var dim2 string
var dim3 string
err := mssql.QueryRow(`
SELECT
ItemTypeCode,
ISNULL(ItemDim1Code,'') AS ItemDim1Code,
ISNULL(ItemDim2Code,'') AS ItemDim2Code,
ISNULL(ItemDim3Code,'') AS ItemDim3Code
FROM dbo.trOrderLine
WHERE OrderHeaderID = @p1 AND OrderLineID = @p2
`, orderHeaderID, orderLineID).Scan(&itemTypeCode, &dim1, &dim2, &dim3)
return itemTypeCode, dim1, dim2, dim3, err
}
func VariantExists(mssql *sql.DB, itemTypeCode int16, itemCode string, colorCode string, dim1 string, dim2 string, dim3 string) (bool, error) {
var exists int
err := mssql.QueryRow(`
SELECT TOP 1 1
FROM dbo.prItemVariant
WHERE ItemTypeCode = @p1
AND ItemCode = @p2
AND ColorCode = @p3
AND ISNULL(ItemDim1Code,'') = ISNULL(@p4,'')
AND ISNULL(ItemDim2Code,'') = ISNULL(@p5,'')
AND ISNULL(ItemDim3Code,'') = ISNULL(@p6,'')
`, itemTypeCode, itemCode, colorCode, dim1, dim2, dim3).Scan(&exists)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
func InsertMissingVariantsTx(tx *sql.Tx, missing []models.OrderProductionMissingVariant, username string) (int64, error) {
if len(missing) == 0 {
return 0, nil
}
var basePlu int64
if err := tx.QueryRow(`
SELECT ISNULL(MAX(PLU),0) AS BasePlu
FROM dbo.prItemVariant WITH (UPDLOCK, HOLDLOCK)
`).Scan(&basePlu); err != nil {
return 0, err
}
var inserted int64
for i, v := range missing {
plu := basePlu + int64(i) + 1
res, err := tx.Exec(`
IF NOT EXISTS (
SELECT 1
FROM dbo.prItemVariant
WHERE ItemTypeCode = @p1
AND ItemCode = @p2
AND ColorCode = @p3
AND ISNULL(ItemDim1Code,'') = ISNULL(@p4,'')
AND ISNULL(ItemDim2Code,'') = ISNULL(@p5,'')
AND ISNULL(ItemDim3Code,'') = ISNULL(@p6,'')
)
INSERT INTO dbo.prItemVariant (
ItemTypeCode,
ItemCode,
ColorCode,
ItemDim1Code,
ItemDim2Code,
ItemDim3Code,
PLU,
CreatedUserName,
CreatedDate,
LastUpdatedUserName,
LastUpdatedDate
)
VALUES (
@p1, @p2, @p3, @p4, @p5, @p6,
@p7, @p8, GETDATE(), @p8, GETDATE()
);
`, v.ItemTypeCode, v.ItemCode, v.ColorCode, v.ItemDim1Code, v.ItemDim2Code, v.ItemDim3Code, plu, username)
if err != nil {
return inserted, err
}
if rows, err := res.RowsAffected(); err == nil {
inserted += rows
}
}
return inserted, nil
}
func UpdateOrderLinesTx(tx *sql.Tx, orderHeaderID string, lines []models.OrderProductionUpdateLine, username string) (int64, error) {
var updated int64
for _, line := range lines {
res, err := tx.Exec(`
UPDATE dbo.trOrderLine
SET
ItemCode = @p1,
ColorCode = @p2,
ItemDim2Code = @p3,
LineDescription = COALESCE(NULLIF(@p4,''), LineDescription),
LastUpdatedUserName = @p5,
LastUpdatedDate = GETDATE()
WHERE OrderHeaderID = @p6 AND OrderLineID = @p7
`, line.NewItemCode, line.NewColor, line.NewDim2, line.NewDesc, username, orderHeaderID, line.OrderLineID)
if err != nil {
return updated, err
}
if rows, err := res.RowsAffected(); err == nil {
updated += rows
}
}
return updated, nil
}

View File

@@ -1,12 +1,15 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/models"
"bssapp-backend/queries"
"database/sql"
"encoding/json"
"errors"
"log"
"net/http"
"strings"
"github.com/gorilla/mux"
)
@@ -40,6 +43,9 @@ func OrderProductionItemsRoute(mssql *sql.DB) http.Handler {
if err := rows.Scan(
&o.OrderHeaderID,
&o.OrderLineID,
&o.ItemTypeCode,
&o.OldDim1,
&o.OldDim3,
&o.OldItemCode,
&o.OldColor,
&o.OldDim2,
@@ -48,6 +54,7 @@ func OrderProductionItemsRoute(mssql *sql.DB) http.Handler {
&o.NewColor,
&o.NewDim2,
&o.NewDesc,
&o.IsVariantMissing,
); err != nil {
log.Printf("⚠️ SCAN HATASI: %v", err)
continue
@@ -64,3 +71,224 @@ func OrderProductionItemsRoute(mssql *sql.DB) http.Handler {
}
})
}
// ======================================================
// 📌 OrderProductionInsertMissingRoute — eksik varyantları ekler
// ======================================================
func OrderProductionInsertMissingRoute(mssql *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadı", http.StatusBadRequest)
return
}
claims, _ := auth.GetClaimsFromContext(r.Context())
username := ""
if claims != nil {
username = claims.Username
}
if username == "" {
username = "system"
}
affected, err := queries.InsertMissingProductionVariants(mssql, id, username)
if err != nil {
log.Printf("❌ INSERT varyant hatası: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
resp := map[string]any{
"inserted": affected,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("❌ encode error: %v", err)
}
})
}
// ======================================================
// OrderProductionValidateRoute - yeni model varyant kontrolu
// ======================================================
func OrderProductionValidateRoute(mssql *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadi", http.StatusBadRequest)
return
}
var payload models.OrderProductionUpdatePayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "Gecersiz istek", http.StatusBadRequest)
return
}
if err := validateUpdateLines(payload.Lines); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
missing, err := buildMissingVariants(mssql, id, payload.Lines)
if err != nil {
log.Printf("❌ validate error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
resp := map[string]any{
"missingCount": len(missing),
"missing": missing,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("❌ encode error: %v", err)
}
})
}
// ======================================================
// OrderProductionApplyRoute - yeni model varyant guncelleme
// ======================================================
func OrderProductionApplyRoute(mssql *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadi", http.StatusBadRequest)
return
}
var payload models.OrderProductionUpdatePayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "Gecersiz istek", http.StatusBadRequest)
return
}
if err := validateUpdateLines(payload.Lines); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
missing, err := buildMissingVariants(mssql, id, payload.Lines)
if err != nil {
log.Printf("❌ apply validate error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
if len(missing) > 0 && !payload.InsertMissing {
w.WriteHeader(http.StatusConflict)
_ = json.NewEncoder(w).Encode(map[string]any{
"missingCount": len(missing),
"missing": missing,
"message": "Eksik varyantlar var",
})
return
}
claims, _ := auth.GetClaimsFromContext(r.Context())
username := ""
if claims != nil {
username = claims.Username
}
if strings.TrimSpace(username) == "" {
username = "system"
}
tx, err := mssql.Begin()
if err != nil {
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
defer tx.Rollback()
var inserted int64
if payload.InsertMissing {
inserted, err = queries.InsertMissingVariantsTx(tx, missing, username)
if err != nil {
log.Printf("❌ insert missing error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
}
updated, err := queries.UpdateOrderLinesTx(tx, id, payload.Lines, username)
if err != nil {
log.Printf("❌ update order lines error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
if err := tx.Commit(); err != nil {
log.Printf("❌ commit error: %v", err)
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
return
}
resp := map[string]any{
"updated": updated,
"inserted": inserted,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("❌ encode error: %v", err)
}
})
}
func buildMissingVariants(mssql *sql.DB, orderHeaderID string, lines []models.OrderProductionUpdateLine) ([]models.OrderProductionMissingVariant, error) {
missing := make([]models.OrderProductionMissingVariant, 0)
for _, line := range lines {
lineID := strings.TrimSpace(line.OrderLineID)
newItem := strings.TrimSpace(line.NewItemCode)
newColor := strings.TrimSpace(line.NewColor)
newDim2 := strings.TrimSpace(line.NewDim2)
if lineID == "" || newItem == "" || newColor == "" {
continue
}
itemTypeCode, dim1, _, dim3, err := queries.GetOrderLineDims(mssql, orderHeaderID, lineID)
if err != nil {
return nil, err
}
exists, err := queries.VariantExists(mssql, itemTypeCode, newItem, newColor, dim1, newDim2, dim3)
if err != nil {
return nil, err
}
if !exists {
missing = append(missing, models.OrderProductionMissingVariant{
OrderLineID: lineID,
ItemTypeCode: itemTypeCode,
ItemCode: newItem,
ColorCode: newColor,
ItemDim1Code: dim1,
ItemDim2Code: newDim2,
ItemDim3Code: dim3,
})
}
}
return missing, nil
}
func validateUpdateLines(lines []models.OrderProductionUpdateLine) error {
for _, line := range lines {
if strings.TrimSpace(line.OrderLineID) == "" {
return errors.New("OrderLineID zorunlu")
}
if strings.TrimSpace(line.NewItemCode) == "" {
return errors.New("Yeni urun kodu zorunlu")
}
if strings.TrimSpace(line.NewColor) == "" {
return errors.New("Yeni renk kodu zorunlu")
}
}
return nil
}

View File

@@ -0,0 +1,230 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/db"
"database/sql"
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"github.com/gorilla/mux"
)
type ProductionUpdateLine struct {
OrderLineID string `json:"OrderLineID"`
ItemTypeCode int16 `json:"ItemTypeCode"`
ItemCode string `json:"ItemCode"`
ColorCode string `json:"ColorCode"`
ItemDim1Code string `json:"ItemDim1Code"`
ItemDim2Code string `json:"ItemDim2Code"`
ItemDim3Code string `json:"ItemDim3Code"`
LineDescription string `json:"LineDescription"`
}
type ProductionUpdateRequest struct {
Lines []ProductionUpdateLine `json:"lines"`
InsertMissing bool `json:"insertMissing"`
}
type MissingVariant struct {
ItemTypeCode int16 `json:"ItemTypeCode"`
ItemCode string `json:"ItemCode"`
ColorCode string `json:"ColorCode"`
ItemDim1Code string `json:"ItemDim1Code"`
ItemDim2Code string `json:"ItemDim2Code"`
ItemDim3Code string `json:"ItemDim3Code"`
}
// ======================================================
// 📌 OrderProductionUpdateRoute — U ürün satırlarını güncelle
// ======================================================
func OrderProductionUpdateRoute(mssql *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadı", http.StatusBadRequest)
return
}
var req ProductionUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Geçersiz JSON", http.StatusBadRequest)
return
}
if len(req.Lines) == 0 {
http.Error(w, "Satır bulunamadı", http.StatusBadRequest)
return
}
claims, _ := auth.GetClaimsFromContext(r.Context())
username := ""
if claims != nil {
username = claims.Username
}
if username == "" {
username = "system"
}
tx, err := db.MssqlDB.Begin()
if err != nil {
http.Error(w, "İşlem başlatılamadı", http.StatusInternalServerError)
return
}
defer tx.Rollback()
// 1) Eksik varyantları kontrol et
missingMap := make(map[string]MissingVariant)
checkStmt, err := tx.Prepare(`
SELECT TOP 1 1
FROM dbo.prItemVariant
WHERE ItemTypeCode = @p1
AND ItemCode = @p2
AND ColorCode = @p3
AND ISNULL(ItemDim1Code,'') = ISNULL(@p4,'')
AND ISNULL(ItemDim2Code,'') = ISNULL(@p5,'')
AND ISNULL(ItemDim3Code,'') = ISNULL(@p6,'')
`)
if err != nil {
http.Error(w, "Varyant kontrolü hazırlanamadı", http.StatusInternalServerError)
return
}
defer checkStmt.Close()
for _, ln := range req.Lines {
if strings.TrimSpace(ln.ItemCode) == "" {
http.Error(w, "Yeni model kodu boş", http.StatusBadRequest)
return
}
row := checkStmt.QueryRow(
ln.ItemTypeCode,
ln.ItemCode,
ln.ColorCode,
ln.ItemDim1Code,
ln.ItemDim2Code,
ln.ItemDim3Code,
)
var ok int
if err := row.Scan(&ok); err != nil {
if errors.Is(err, sql.ErrNoRows) {
key := strings.Join([]string{
ln.ItemCode, ln.ColorCode, ln.ItemDim1Code, ln.ItemDim2Code, ln.ItemDim3Code,
}, "|")
missingMap[key] = MissingVariant{
ItemTypeCode: ln.ItemTypeCode,
ItemCode: ln.ItemCode,
ColorCode: ln.ColorCode,
ItemDim1Code: ln.ItemDim1Code,
ItemDim2Code: ln.ItemDim2Code,
ItemDim3Code: ln.ItemDim3Code,
}
continue
}
http.Error(w, "Varyant kontrolü hatası", http.StatusInternalServerError)
return
}
}
if len(missingMap) > 0 && !req.InsertMissing {
missing := make([]MissingVariant, 0, len(missingMap))
for _, v := range missingMap {
missing = append(missing, v)
}
w.WriteHeader(http.StatusConflict)
_ = json.NewEncoder(w).Encode(map[string]any{
"missing": missing,
})
return
}
// 2) Eksikleri ekle (gerekirse)
if len(missingMap) > 0 {
// PLU üretimi (max + row_number)
var basePlu int64
if err := tx.QueryRow(`SELECT ISNULL(MAX(PLU),0) FROM dbo.prItemVariant WITH (UPDLOCK, HOLDLOCK)`).Scan(&basePlu); err != nil {
http.Error(w, "PLU alınamadı", http.StatusInternalServerError)
return
}
now := time.Now()
i := int64(0)
for _, v := range missingMap {
i++
if _, err := tx.Exec(`
INSERT INTO dbo.prItemVariant
(
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code,
PLU, CreatedUserName, CreatedDate, LastUpdatedUserName, LastUpdatedDate
)
VALUES
(@p1,@p2,@p3,@p4,@p5,@p6,@p7,@p8,@p9,@p8,@p9)
`,
v.ItemTypeCode,
v.ItemCode,
v.ColorCode,
v.ItemDim1Code,
v.ItemDim2Code,
v.ItemDim3Code,
basePlu+i,
username,
now,
); err != nil {
http.Error(w, "Varyant insert hatası", http.StatusInternalServerError)
return
}
}
}
// 3) trOrderLine güncelle
updStmt, err := tx.Prepare(`
UPDATE dbo.trOrderLine
SET
ItemCode = @p1,
ColorCode = @p2,
ItemDim2Code = @p3,
LineDescription = @p4,
LastUpdatedUserName = @p5,
LastUpdatedDate = @p6
WHERE OrderHeaderID = @p7
AND OrderLineID = @p8
`)
if err != nil {
http.Error(w, "Update hazırlığı başarısız", http.StatusInternalServerError)
return
}
defer updStmt.Close()
now := time.Now()
for _, ln := range req.Lines {
if _, err := updStmt.Exec(
ln.ItemCode,
ln.ColorCode,
ln.ItemDim2Code,
ln.LineDescription,
username,
now,
id,
ln.OrderLineID,
); err != nil {
http.Error(w, "Satır güncelleme hatası", http.StatusInternalServerError)
return
}
}
if err := tx.Commit(); err != nil {
http.Error(w, "Commit hatası", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{
"status": "ok",
"updated": len(req.Lines),
})
})
}

View File

@@ -1,24 +1,177 @@
<template>
<q-page class="q-pa-md">
<div class="text-h6 text-weight-bold">Üretime Verilen Ürünler</div>
<div class="text-caption text-grey-7 q-mt-xs">
OrderHeaderID: {{ orderHeaderID || '-' }}
<div class="row items-center justify-between">
<div>
<div class="text-h6 text-weight-bold">Uretime Verilen Urunleri Guncelle</div>
<div class="text-caption text-grey-7 q-mt-xs">
OrderHeaderID: {{ orderHeaderID || '-' }}
</div>
</div>
<q-btn
color="primary"
icon="refresh"
label="Yenile"
:loading="store.loading"
@click="refreshAll"
/>
</div>
<div class="filter-bar row q-col-gutter-md q-mt-md">
<div class="col-5">
<q-input
:model-value="cariLabel"
label="Cari Secimi"
filled
dense
readonly
/>
</div>
<div class="col-2">
<q-input
:model-value="header?.OrderNumber || ''"
label="Siparis No"
filled
dense
readonly
/>
</div>
<div class="col-2">
<q-input
:model-value="formatDate(header?.OrderDate)"
label="Olusturulma Tarihi"
filled
dense
readonly
/>
</div>
<div class="col-2">
<q-input
:model-value="formatDate(header?.AverageDueDate)"
label="Tahmini Termin Tarihi"
filled
dense
readonly
/>
</div>
</div>
<q-table
<q-table
class="q-mt-md"
flat
bordered
dense
separator="cell"
row-key="OrderLineID"
:rows="store.items"
:rows="rows"
:columns="columns"
:loading="store.loading"
no-data-label="Üretime verilecek ürün bulunamadı"
no-data-label="Uretime verilecek urun bulunamadi"
:rows-per-page-options="[0]"
hide-bottom
/>
>
<template #body-cell-actions="props">
<q-td :props="props" class="text-center">
<q-btn
color="primary"
icon="save"
flat
round
dense
:loading="rowSavingId === props.row.OrderLineID"
@click="onRowSubmit(props.row)"
>
<q-tooltip>Satiri Guncelle</q-tooltip>
</q-btn>
</q-td>
</template>
<template #body-cell-NewItemCode="props">
<q-td :props="props">
<q-input
v-model="props.row.NewItemCode"
dense
filled
label="Yeni Urun"
@update:model-value="val => onNewItemChange(props.row, val)"
>
<template #append>
<q-icon name="arrow_drop_down" class="cursor-pointer" />
</template>
<q-menu
anchor="bottom left"
self="top left"
fit
>
<div class="q-pa-sm" style="min-width:260px">
<q-input
v-model="productSearch"
dense
filled
debounce="200"
placeholder="Urun ara..."
/>
<q-list class="q-mt-xs" bordered separator>
<q-item
v-for="opt in filteredProducts"
:key="opt.ProductCode"
clickable
@click="onSelectProduct(props.row, opt.ProductCode)"
>
<q-item-section>{{ opt.ProductCode }}</q-item-section>
</q-item>
</q-list>
</div>
</q-menu>
</q-input>
</q-td>
</template>
<template #body-cell-NewColor="props">
<q-td :props="props">
<q-select
v-model="props.row.NewColor"
:options="getColorOptions(props.row)"
option-label="colorLabel"
option-value="color_code"
emit-value
map-options
use-input
dense
filled
label="Yeni Renk"
@update:model-value="() => onNewColorChange(props.row)"
/>
</q-td>
</template>
<template #body-cell-NewDim2="props">
<q-td :props="props">
<q-select
v-model="props.row.NewDim2"
:options="getSecondColorOptions(props.row)"
option-label="item_dim2_code"
option-value="item_dim2_code"
emit-value
map-options
use-input
dense
filled
label="Yeni 2. Renk"
/>
</q-td>
</template>
<template #body-cell-NewDesc="props">
<q-td :props="props">
<q-input
v-model="props.row.NewDesc"
dense
filled
label="Yeni Aciklama"
/>
</q-td>
</template>
</q-table>
<q-banner v-if="store.error" class="bg-red text-white q-mt-sm">
Hata: {{ store.error }}
@@ -27,31 +180,193 @@
</template>
<script setup>
import { computed, onMounted, watch } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useQuasar } from 'quasar'
import { useOrderProductionItemStore } from 'src/stores/OrderProductionItemStore'
const route = useRoute()
const $q = useQuasar()
const store = useOrderProductionItemStore()
const orderHeaderID = computed(() => String(route.params.orderHeaderID || '').trim())
const header = computed(() => store.header || {})
const cariLabel = computed(() => {
const code = header.value?.CurrAccCode || ''
const name = header.value?.CurrAccDescription || ''
if (!code && !name) return ''
if (!name) return code
return `${code} - ${name}`
})
const rows = ref([])
const productOptions = ref([])
const productSearch = ref('')
const rowSavingId = ref('')
const columns = [
{ name: 'OldItemCode', label: 'Eski Ürün Kodu', field: 'OldItemCode', align: 'left', sortable: true, style: 'min-width:140px;white-space:nowrap', headerStyle: 'min-width:140px;white-space:nowrap' },
{ name: 'OldColor', label: 'Eski Ürün Rengi', field: 'OldColor', align: 'left', sortable: true, style: 'min-width:120px;white-space:nowrap', headerStyle: 'min-width:120px;white-space:nowrap' },
{ name: 'OldItemCode', label: 'Eski Urun Kodu', field: 'OldItemCode', align: 'left', sortable: true, style: 'min-width:140px;white-space:nowrap', headerStyle: 'min-width:140px;white-space:nowrap' },
{ name: 'OldColor', label: 'Eski Urun Rengi', field: 'OldColor', align: 'left', sortable: true, style: 'min-width:120px;white-space:nowrap', headerStyle: 'min-width:120px;white-space:nowrap' },
{ name: 'OldDim2', label: 'Eski 2. Renk', field: 'OldDim2', align: 'left', sortable: true, style: 'min-width:110px;white-space:nowrap', headerStyle: 'min-width:110px;white-space:nowrap' },
{ name: 'OldDesc', label: 'Eski Açıklama', field: 'OldDesc', align: 'left', sortable: false, style: 'min-width:180px;white-space:nowrap', headerStyle: 'min-width:180px;white-space:nowrap' },
{ name: 'NewItemCode', label: 'Yeni Ürün Kodu', field: 'NewItemCode', align: 'left', sortable: true, style: 'min-width:140px;white-space:nowrap', headerStyle: 'min-width:140px;white-space:nowrap' },
{ name: 'NewColor', label: 'Yeni Ürün Rengi', field: 'NewColor', align: 'left', sortable: true, style: 'min-width:120px;white-space:nowrap', headerStyle: 'min-width:120px;white-space:nowrap' },
{ name: 'NewDim2', label: 'Yeni 2. Renk', field: 'NewDim2', align: 'left', sortable: true, style: 'min-width:110px;white-space:nowrap', headerStyle: 'min-width:110px;white-space:nowrap' },
{ name: 'NewDesc', label: 'Yeni Açıklama', field: 'NewDesc', align: 'left', sortable: false, style: 'min-width:180px;white-space:nowrap', headerStyle: 'min-width:180px;white-space:nowrap' }
{ name: 'OldDesc', label: 'Eski Aciklama', field: 'OldDesc', align: 'left', sortable: false, style: 'min-width:180px;white-space:nowrap', headerStyle: 'min-width:180px;white-space:nowrap' },
{ name: 'NewItemCode', label: 'Yeni Urun Kodu', field: 'NewItemCode', align: 'left', sortable: false, style: 'min-width:190px;', headerStyle: 'min-width:190px;' },
{ name: 'NewColor', label: 'Yeni Urun Rengi', field: 'NewColor', align: 'left', sortable: false, style: 'min-width:160px;', headerStyle: 'min-width:160px;' },
{ name: 'NewDim2', label: 'Yeni 2. Renk', field: 'NewDim2', align: 'left', sortable: false, style: 'min-width:160px;', headerStyle: 'min-width:160px;' },
{ name: 'NewDesc', label: 'Yeni Aciklama', field: 'NewDesc', align: 'left', sortable: false, style: 'min-width:220px;', headerStyle: 'min-width:220px;' },
{ name: 'actions', label: '', field: 'actions', align: 'center', sortable: false, style: 'width:60px;', headerStyle: 'width:60px;' }
]
onMounted(() => {
store.fetchItems(orderHeaderID.value)
onMounted(async () => {
await refreshAll()
})
watch(orderHeaderID, (id) => {
store.fetchItems(id)
watch(orderHeaderID, async (id) => {
await refreshAll()
})
watch(
() => store.items,
(items) => {
rows.value = (items || []).map(item => ({
...item,
NewItemCode: '',
NewColor: '',
NewDim2: '',
NewDesc: ''
}))
},
{ immediate: true }
)
watch(
() => store.products,
(products) => {
productOptions.value = products || []
},
{ immediate: true }
)
function formatDate (val) {
if (!val) return ''
const text = String(val)
return text.length >= 10 ? text.slice(0, 10) : text
}
const filteredProducts = computed(() => {
const needle = String(productSearch.value || '').toLowerCase()
if (!needle) return productOptions.value.slice(0, 50)
return productOptions.value.filter(p =>
String(p?.ProductCode || '').toLowerCase().includes(needle)
).slice(0, 50)
})
function onSelectProduct (row, code) {
productSearch.value = ''
onNewItemChange(row, code)
}
function onNewItemChange (row, val) {
const next = String(val || '').trim()
if (next && !isValidModelCode(next)) {
$q.notify({ type: 'negative', message: 'Model kodu formati gecersiz. Ornek: S000-DMY00001' })
row.NewItemCode = ''
row.NewColor = ''
row.NewDim2 = ''
return
}
row.NewItemCode = next ? next.toUpperCase() : ''
row.NewColor = ''
row.NewDim2 = ''
if (row.NewItemCode) {
store.fetchColors(row.NewItemCode)
}
}
function onNewColorChange (row) {
row.NewDim2 = ''
if (row.NewItemCode && row.NewColor) {
store.fetchSecondColors(row.NewItemCode, row.NewColor)
}
}
function getColorOptions (row) {
const code = row?.NewItemCode || ''
const list = store.colorOptionsByCode[code] || []
return list.map(c => ({
...c,
colorLabel: `${c.color_code} - ${c.color_description || ''}`.trim()
}))
}
function getSecondColorOptions (row) {
const code = row?.NewItemCode || ''
const color = row?.NewColor || ''
const key = `${code}::${color}`
return store.secondColorOptionsByKey[key] || []
}
function isValidModelCode (value) {
const text = String(value || '').trim().toUpperCase()
return /^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$/.test(text)
}
function buildPayloadLines () {
return rows.value.map(r => ({
OrderLineID: r.OrderLineID,
NewItemCode: String(r.NewItemCode || '').trim(),
NewColor: String(r.NewColor || '').trim(),
NewDim2: String(r.NewDim2 || '').trim(),
NewDesc: String(r.NewDesc || '').trim()
}))
}
async function refreshAll () {
await store.fetchHeader(orderHeaderID.value)
await store.fetchItems(orderHeaderID.value)
await store.fetchProducts()
}
async function onRowSubmit (row) {
const line = {
OrderLineID: row.OrderLineID,
NewItemCode: String(row.NewItemCode || '').trim(),
NewColor: String(row.NewColor || '').trim(),
NewDim2: String(row.NewDim2 || '').trim(),
NewDesc: String(row.NewDesc || '').trim()
}
if (!line.NewItemCode || !line.NewColor) {
$q.notify({ type: 'negative', message: 'Yeni urun ve renk zorunludur.' })
return
}
rowSavingId.value = row.OrderLineID
try {
const validate = await store.validateUpdates(orderHeaderID.value, [line])
const missingCount = validate?.missingCount || 0
if (missingCount > 0) {
const missingList = (validate?.missing || []).map(v => (
`${v.ItemCode} / ${v.ColorCode} / ${v.ItemDim1Code} / ${v.ItemDim2Code}`
))
$q.dialog({
title: 'Eksik Varyantlar',
message: `Eksik varyant bulundu: ${missingCount}<br><br>${missingList.join('<br>')}`,
html: true,
ok: { label: 'Ekle ve Guncelle', color: 'primary' },
cancel: { label: 'Vazgec', flat: true }
}).onOk(async () => {
await store.applyUpdates(orderHeaderID.value, [line], true)
await store.fetchItems(orderHeaderID.value)
})
return
}
await store.applyUpdates(orderHeaderID.value, [line], false)
await store.fetchItems(orderHeaderID.value)
} catch (err) {
$q.notify({ type: 'negative', message: 'Islem basarisiz.' })
} finally {
rowSavingId.value = ''
}
}
</script>

View File

@@ -5,11 +5,35 @@ import api from 'src/services/api'
export const useOrderProductionItemStore = defineStore('orderproductionitems', {
state: () => ({
items: [],
header: null,
products: [],
colorOptionsByCode: {},
secondColorOptionsByKey: {},
loading: false,
saving: false,
error: null
}),
actions: {
async fetchHeader (orderHeaderID) {
if (!orderHeaderID) {
this.header = null
return
}
this.loading = true
this.error = null
try {
const res = await api.get(`/order/get/${encodeURIComponent(orderHeaderID)}`)
this.header = res?.data?.header || null
} catch (err) {
this.header = null
this.error = err?.response?.data || err?.message || 'Siparis bilgisi alinamadi'
} finally {
this.loading = false
}
},
async fetchItems (orderHeaderID) {
if (!orderHeaderID) {
this.items = []
@@ -25,10 +49,99 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', {
this.items = Array.isArray(data) ? data : []
} catch (err) {
this.items = []
this.error = err?.response?.data || err?.message || 'Liste alınamadı'
this.error = err?.response?.data || err?.message || 'Liste alinamadi'
} finally {
this.loading = false
}
},
async fetchProducts () {
this.error = null
try {
const res = await api.get('/products')
const data = res?.data
this.products = Array.isArray(data) ? data : []
} catch (err) {
this.products = []
this.error = err?.response?.data || err?.message || 'Urun listesi alinamadi'
}
},
async fetchColors (productCode) {
const code = String(productCode || '').trim()
if (!code) return []
if (this.colorOptionsByCode[code]) {
return this.colorOptionsByCode[code]
}
try {
const res = await api.get('/product-colors', { params: { code } })
const data = res?.data
const list = Array.isArray(data) ? data : []
this.colorOptionsByCode[code] = list
return list
} catch (err) {
this.error = err?.response?.data || err?.message || 'Renk listesi alinamadi'
return []
}
},
async fetchSecondColors (productCode, colorCode) {
const code = String(productCode || '').trim()
const color = String(colorCode || '').trim()
if (!code || !color) return []
const key = `${code}::${color}`
if (this.secondColorOptionsByKey[key]) {
return this.secondColorOptionsByKey[key]
}
try {
const res = await api.get('/product-secondcolor', { params: { code, color } })
const data = res?.data
const list = Array.isArray(data) ? data : []
this.secondColorOptionsByKey[key] = list
return list
} catch (err) {
this.error = err?.response?.data || err?.message || '2. renk listesi alinamadi'
return []
}
},
async validateUpdates (orderHeaderID, lines) {
if (!orderHeaderID) return { missingCount: 0, missing: [] }
this.saving = true
this.error = null
try {
const res = await api.post(
`/orders/production-items/${encodeURIComponent(orderHeaderID)}/validate`,
{ lines }
)
return res?.data || { missingCount: 0, missing: [] }
} catch (err) {
this.error = err?.response?.data || err?.message || 'Kontrol basarisiz'
throw err
} finally {
this.saving = false
}
},
async applyUpdates (orderHeaderID, lines, insertMissing) {
if (!orderHeaderID) return { updated: 0, inserted: 0 }
this.saving = true
this.error = null
try {
const res = await api.post(
`/orders/production-items/${encodeURIComponent(orderHeaderID)}/apply`,
{ lines, insertMissing }
)
return res?.data || { updated: 0, inserted: 0 }
} catch (err) {
this.error = err?.response?.data || err?.message || 'Guncelleme basarisiz'
throw err
} finally {
this.saving = false
}
}
}
})

View File

@@ -1701,14 +1701,15 @@ export const useOrderEntryStore = defineStore('orderentry', {
: {}
/* ===== SAME COMBO → UPDATE ===== */
if (sameCombo) {
rows[idx] = {
...prev,
...newRow,
id: prev.id,
OrderLineID: prev.OrderLineID || null,
lineIdMap: preservedLineIdMap
}
if (sameCombo) {
rows[idx] = {
...prev,
...newRow,
_dirty: true,
id: prev.id,
OrderLineID: prev.OrderLineID || null,
lineIdMap: preservedLineIdMap
}
this.summaryRows = rows
this.orders = rows
@@ -1761,12 +1762,13 @@ export const useOrderEntryStore = defineStore('orderentry', {
comboLineIds: { ...(prev.comboLineIds || {}) }
}
const insertedRow = {
...newRow,
id: crypto.randomUUID(),
OrderLineID: null,
lineIdMap: {}
}
const insertedRow = {
...newRow,
_dirty: true,
id: crypto.randomUUID(),
OrderLineID: null,
lineIdMap: {}
}
rows.splice(idx, 1, insertedRow)
@@ -1835,13 +1837,14 @@ export const useOrderEntryStore = defineStore('orderentry', {
const price = Number(newRow?.fiyat ?? prev?.fiyat ?? 0)
const totalTutar = Number((totalAdet * price).toFixed(2))
rows[dupIdx] = {
...prev,
...newRow,
rows[dupIdx] = {
...prev,
...newRow,
_dirty: true,
// kritik korumalar
id: prev.id,
OrderLineID: prev.OrderLineID || null,
// kritik korumalar
id: prev.id,
OrderLineID: prev.OrderLineID || null,
lineIdMap: { ...(prev.lineIdMap || {}) },
// MERGED bedenMap
@@ -1869,12 +1872,13 @@ export const useOrderEntryStore = defineStore('orderentry', {
}
// dup yoksa (veya dup delete satırıydı) → yeni satır
rows.push({
...newRow,
id: newRow.id || crypto.randomUUID(),
OrderLineID: null,
lineIdMap: { ...(newRow.lineIdMap || {}) }
})
rows.push({
...newRow,
_dirty: true,
id: newRow.id || crypto.randomUUID(),
OrderLineID: null,
lineIdMap: { ...(newRow.lineIdMap || {}) }
})
this.summaryRows = rows
this.orders = rows
@@ -2633,7 +2637,12 @@ export const useOrderEntryStore = defineStore('orderentry', {
// 🧪 PRE-VALIDATE — prItemVariant ön kontrol
// - invalid varsa CREATE/UPDATE ÇALIŞMAZ
// =======================================================
const v = await api.post('/order/validate', { header, lines })
const linesToValidate =
isNew
? lines
: lines.filter(l => l._deleteSignal === true || l._dirty === true || !l.OrderLineID)
const v = await api.post('/order/validate', { header, lines: linesToValidate })
const invalid = v?.data?.invalid || []
if (invalid.length > 0) {
@@ -2969,10 +2978,12 @@ export const useOrderEntryStore = defineStore('orderentry', {
// ComboKey stabil kalsın diye bedenKey kullan
const comboKey = buildComboKey(row, bedenKey)
const makeLine = () => ({
OrderLineID: orderLineId || '',
ClientKey: makeLineClientKey(row, grpKey, bedenKey),
ComboKey: comboKey,
const makeLine = () => ({
OrderLineID: orderLineId || '',
ClientKey: makeLineClientKey(row, grpKey, bedenKey),
ComboKey: comboKey,
_dirty: row?._dirty === true,
_deleteSignal: isDeleteSignal === true,
SortOrder: 0,
ItemTypeCode: 1,
@@ -3040,26 +3051,30 @@ export const useOrderEntryStore = defineStore('orderentry', {
DOVCode: ''
})
const existing = lineByCombo.get(comboKey)
const existing = lineByCombo.get(comboKey)
if (!existing) {
const ln = makeLine()
lineByCombo.set(comboKey, ln)
lines.push(ln)
return
}
if (!existing) {
const ln = makeLine()
lineByCombo.set(comboKey, ln)
lines.push(ln)
return
}
/* DELETE */
if (isDeleteSignal) {
if (orderLineId && !existing.OrderLineID) {
existing.OrderLineID = orderLineId
if (isDeleteSignal) {
if (orderLineId && !existing.OrderLineID) {
existing.OrderLineID = orderLineId
}
existing._deleteSignal = true
existing.Qty1 = 0
return
}
existing.Qty1 = 0
return
}
/* MERGE */
existing.Qty1 += qty
/* MERGE */
existing.Qty1 += qty
if (row?._dirty === true) {
existing._dirty = true
}
if (this.mode === 'edit' && orderLineId && !existing.OrderLineID) {
existing.OrderLineID = orderLineId
@@ -3346,66 +3361,56 @@ export function normalizeBeden(v) {
- Keeps frontend aksbir bucket for accessory lines
=========================================================== */
export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '') {
const list = Array.isArray(bedenList) ? bedenList : []
const ana = normalizeTextForMatch(urunAnaGrubu)
const alt = normalizeTextForMatch(urunKategori)
const list = Array.isArray(bedenList) && bedenList.length > 0
? bedenList.map(v => (v || '').toString().trim().toUpperCase())
: [' ']
// Frontend compatibility: accessory-only products should stay in aksbir.
const accessoryGroups = [
'AKSESUAR', 'KRAVAT', 'PAPYON', 'KEMER', 'CORAP',
'FULAR', 'MENDIL', 'KASKOL', 'ASKI', 'YAKA', 'KOL DUGMESI'
const ana = (urunAnaGrubu || '')
.toUpperCase()
.trim()
.replace(/\(.*?\)/g, '')
.replace(/[^A-ZÇĞİÖŞÜ0-9\s]/g, '')
.replace(/\s+/g, ' ')
const kat = (urunKategori || '').toUpperCase().trim()
// 🔸 Aksesuar ise "aksbir"
const aksesuarGruplari = [
'AKSESUAR','KRAVAT','PAPYON','KEMER','CORAP','ÇORAP',
'FULAR','MENDIL','MENDİL','KASKOL','ASKI',
'YAKA','KOL DUGMESI','KOL DÜĞMESİ'
]
const clothingGroups = ['GOMLEK', 'CEKET', 'PANTOLON', 'MONT', 'YELEK', 'TAKIM', 'TSHIRT']
const giyimGruplari = ['GÖMLEK','CEKET','PANTOLON','MONT','YELEK','TAKIM','TSHIRT','TİŞÖRT']
// 🔸 Pantolon özel durumu
if (
accessoryGroups.some(g => ana.includes(g) || alt.includes(g)) &&
!clothingGroups.some(g => ana.includes(g))
) {
return 'aksbir'
aksesuarGruplari.some(g => ana.includes(g) || kat.includes(g)) &&
!giyimGruplari.some(g => ana.includes(g))
) return 'aksbir'
if (ana.includes('PANTOLON') && kat.includes('YETİŞKİN')) return 'pan'
// 🔸 Tamamen numerik (örneğin 39-44 arası) → ayakkabı
const allNumeric = list.every(v => /^\d+$/.test(v))
if (allNumeric) {
const nums = list.map(v => parseInt(v, 10)).filter(Boolean)
const diffs = nums.slice(1).map((v, i) => v - nums[i])
if (diffs.every(d => d === 1) && nums[0] >= 35 && nums[0] <= 46) return 'ayk'
}
if (ana.includes('AYAKKABI') || alt.includes('AYAKKABI')) {
return 'ayk'
}
// 🔸 Yaş grubu (çocuk/garson)
if (kat.includes('GARSON') || kat.includes('ÇOCUK')) return 'yas'
let hasYasNumeric = false
let hasAykNumeric = false
let hasPanNumeric = false
// 🔸 Harfli beden varsa doğrudan "gom" (gömlek, üst giyim)
const harfliBedenler = ['XS','S','M','L','XL','XXL','3XL','4XL']
if (list.some(b => harfliBedenler.includes(b))) return 'gom'
for (const raw of list) {
const b = safeTrimUpperJs(raw)
switch (b) {
case 'XS':
case 'S':
case 'M':
case 'L':
case 'XL':
case '2XL':
case '3XL':
case '4XL':
case '5XL':
case '6XL':
case '7XL':
return 'gom'
}
const n = parseNumericSizeJs(b)
if (n == null) continue
if (n >= 2 && n <= 14) hasYasNumeric = true
if (n >= 39 && n <= 45) hasAykNumeric = true
if (n >= 38 && n <= 68) hasPanNumeric = true
}
if (hasAykNumeric) return 'ayk'
if (ana.includes('PANTOLON')) return 'pan'
if (hasPanNumeric) return 'pan'
if (alt.includes('COCUK') || alt.includes('GARSON')) return 'yas'
if (hasYasNumeric) return 'yas'
// 🔸 Varsayılan: takım elbise
return 'tak'
}
export function toSummaryRowFromForm(form) {
if (!form) return null