From 32d0c38ab9d906402e4c3f8018b19f4550656e44 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Fri, 20 Feb 2026 11:20:53 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- scripts/sql/add_indexes_order_validate.sql | 27 ++ svc/main.go | 3 + svc/models/orderproductionitem.go | 6 + svc/models/orderproductionupdate.go | 24 ++ svc/queries/orderproduction_items.go | 209 ++++++++++- svc/routes/orderproductionitems.go | 228 +++++++++++ svc/routes/orderproductionupdate.go | 230 ++++++++++++ ...g.js.temporary.compiled.1771572283583.mjs} | 0 ui/src/pages/OrderProductionUpdate.vue | 353 +++++++++++++++++- ui/src/stores/OrderProductionItemStore.js | 115 +++++- ui/src/stores/orderentryStore.js | 191 +++++----- 11 files changed, 1264 insertions(+), 122 deletions(-) create mode 100644 scripts/sql/add_indexes_order_validate.sql create mode 100644 svc/models/orderproductionupdate.go create mode 100644 svc/routes/orderproductionupdate.go rename ui/{quasar.config.js.temporary.compiled.1771513237059.mjs => quasar.config.js.temporary.compiled.1771572283583.mjs} (100%) diff --git a/scripts/sql/add_indexes_order_validate.sql b/scripts/sql/add_indexes_order_validate.sql new file mode 100644 index 0000000..c573fcb --- /dev/null +++ b/scripts/sql/add_indexes_order_validate.sql @@ -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 diff --git a/svc/main.go b/svc/main.go index 2be629a..95301da 100644 --- a/svc/main.go +++ b/svc/main.go @@ -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)}, diff --git a/svc/models/orderproductionitem.go b/svc/models/orderproductionitem.go index 328de1c..9c86bec 100644 --- a/svc/models/orderproductionitem.go +++ b/svc/models/orderproductionitem.go @@ -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"` } diff --git a/svc/models/orderproductionupdate.go b/svc/models/orderproductionupdate.go new file mode 100644 index 0000000..74539bd --- /dev/null +++ b/svc/models/orderproductionupdate.go @@ -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"` +} diff --git a/svc/queries/orderproduction_items.go b/svc/queries/orderproduction_items.go index 74aa58f..e95b2a2 100644 --- a/svc/queries/orderproduction_items.go +++ b/svc/queries/orderproduction_items.go @@ -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 +} diff --git a/svc/routes/orderproductionitems.go b/svc/routes/orderproductionitems.go index ac195cd..7886dee 100644 --- a/svc/routes/orderproductionitems.go +++ b/svc/routes/orderproductionitems.go @@ -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 +} diff --git a/svc/routes/orderproductionupdate.go b/svc/routes/orderproductionupdate.go new file mode 100644 index 0000000..cbb564a --- /dev/null +++ b/svc/routes/orderproductionupdate.go @@ -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), + }) + }) +} diff --git a/ui/quasar.config.js.temporary.compiled.1771513237059.mjs b/ui/quasar.config.js.temporary.compiled.1771572283583.mjs similarity index 100% rename from ui/quasar.config.js.temporary.compiled.1771513237059.mjs rename to ui/quasar.config.js.temporary.compiled.1771572283583.mjs diff --git a/ui/src/pages/OrderProductionUpdate.vue b/ui/src/pages/OrderProductionUpdate.vue index 3ac51fd..71d5058 100644 --- a/ui/src/pages/OrderProductionUpdate.vue +++ b/ui/src/pages/OrderProductionUpdate.vue @@ -1,24 +1,177 @@ diff --git a/ui/src/stores/OrderProductionItemStore.js b/ui/src/stores/OrderProductionItemStore.js index 3a96083..60644a1 100644 --- a/ui/src/stores/OrderProductionItemStore.js +++ b/ui/src/stores/OrderProductionItemStore.js @@ -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 + } } } }) diff --git a/ui/src/stores/orderentryStore.js b/ui/src/stores/orderentryStore.js index 1a452c7..5f440a6 100644 --- a/ui/src/stores/orderentryStore.js +++ b/ui/src/stores/orderentryStore.js @@ -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