diff --git a/svc/db/mssql.go b/svc/db/mssql.go index f14fe26..55b7a90 100644 --- a/svc/db/mssql.go +++ b/svc/db/mssql.go @@ -3,32 +3,120 @@ package db import ( "database/sql" "fmt" + "net/url" "os" + "strconv" "strings" + "time" _ "github.com/microsoft/go-mssqldb" ) var MssqlDB *sql.DB -// ConnectMSSQL MSSQL baglantisini ortam degiskeninden baslatir. +func envInt(name string, fallback int) int { + raw := strings.TrimSpace(os.Getenv(name)) + if raw == "" { + return fallback + } + value, err := strconv.Atoi(raw) + if err != nil || value <= 0 { + return fallback + } + return value +} + +func ensureTimeoutValue(current string, desired int) string { + cur, err := strconv.Atoi(strings.TrimSpace(current)) + if err == nil && cur >= desired { + return strings.TrimSpace(current) + } + return strconv.Itoa(desired) +} + +func ensureMSSQLTimeouts(connString string, connectionTimeoutSec int, dialTimeoutSec int) string { + raw := strings.TrimSpace(connString) + if raw == "" { + return raw + } + + if strings.HasPrefix(strings.ToLower(raw), "sqlserver://") { + u, err := url.Parse(raw) + if err != nil { + return raw + } + q := u.Query() + q.Set("connection timeout", ensureTimeoutValue(q.Get("connection timeout"), connectionTimeoutSec)) + q.Set("dial timeout", ensureTimeoutValue(q.Get("dial timeout"), dialTimeoutSec)) + u.RawQuery = q.Encode() + return u.String() + } + + parts := strings.Split(raw, ";") + foundConnectionTimeout := false + foundDialTimeout := false + + for i, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + + eq := strings.Index(part, "=") + if eq <= 0 { + continue + } + + key := strings.ToLower(strings.TrimSpace(part[:eq])) + value := strings.TrimSpace(part[eq+1:]) + + switch key { + case "connection timeout": + foundConnectionTimeout = true + parts[i] = "connection timeout=" + ensureTimeoutValue(value, connectionTimeoutSec) + case "dial timeout": + foundDialTimeout = true + parts[i] = "dial timeout=" + ensureTimeoutValue(value, dialTimeoutSec) + } + } + + if !foundConnectionTimeout { + parts = append(parts, "connection timeout="+strconv.Itoa(connectionTimeoutSec)) + } + if !foundDialTimeout { + parts = append(parts, "dial timeout="+strconv.Itoa(dialTimeoutSec)) + } + + return strings.Join(parts, ";") +} + +// ConnectMSSQL initializes the MSSQL connection from environment. func ConnectMSSQL() error { connString := strings.TrimSpace(os.Getenv("MSSQL_CONN")) if connString == "" { - return fmt.Errorf("MSSQL_CONN tanımlı değil") + return fmt.Errorf("MSSQL_CONN tanimli degil") } + connectionTimeoutSec := envInt("MSSQL_CONNECTION_TIMEOUT_SEC", 120) + dialTimeoutSec := envInt("MSSQL_DIAL_TIMEOUT_SEC", connectionTimeoutSec) + connString = ensureMSSQLTimeouts(connString, connectionTimeoutSec, dialTimeoutSec) + var err error MssqlDB, err = sql.Open("sqlserver", connString) if err != nil { - return fmt.Errorf("MSSQL bağlantı hatası: %w", err) + return fmt.Errorf("MSSQL baglanti hatasi: %w", err) } + MssqlDB.SetMaxOpenConns(envInt("MSSQL_MAX_OPEN_CONNS", 40)) + MssqlDB.SetMaxIdleConns(envInt("MSSQL_MAX_IDLE_CONNS", 40)) + MssqlDB.SetConnMaxLifetime(time.Duration(envInt("MSSQL_CONN_MAX_LIFETIME_MIN", 30)) * time.Minute) + MssqlDB.SetConnMaxIdleTime(time.Duration(envInt("MSSQL_CONN_MAX_IDLE_MIN", 10)) * time.Minute) + if err = MssqlDB.Ping(); err != nil { - return fmt.Errorf("MSSQL erişilemiyor: %w", err) + return fmt.Errorf("MSSQL erisilemiyor: %w", err) } - fmt.Println("MSSQL bağlantısı başarılı") + fmt.Printf("MSSQL baglantisi basarili (connection timeout=%ds, dial timeout=%ds)\n", connectionTimeoutSec, dialTimeoutSec) return nil } diff --git a/svc/main.go b/svc/main.go index 2e7771a..4da5052 100644 --- a/svc/main.go +++ b/svc/main.go @@ -526,7 +526,7 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router {"/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/production-items/{id}/apply", "POST", "update", routes.OrderProductionApplyRoute(mssql, ml)}, {"/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)}, @@ -571,6 +571,12 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router wrapV3(http.HandlerFunc(routes.GetProductDetailHandler)), ) + bindV3(r, pgDB, + "/api/product-cditem", "GET", + "order", "view", + wrapV3(http.HandlerFunc(routes.GetProductCdItemHandler)), + ) + bindV3(r, pgDB, "/api/product-colors", "GET", "order", "view", @@ -638,6 +644,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "order", "view", wrapV3(routes.GetProductSizeMatchRulesHandler(pgDB)), ) + bindV3(r, pgDB, + "/api/pricing/products", "GET", + "order", "view", + wrapV3(http.HandlerFunc(routes.GetProductPricingListHandler)), + ) // ============================================================ // ROLE MANAGEMENT diff --git a/svc/models/orderproductionitem.go b/svc/models/orderproductionitem.go index 9c86bec..a98abfe 100644 --- a/svc/models/orderproductionitem.go +++ b/svc/models/orderproductionitem.go @@ -11,15 +11,19 @@ type OrderProductionItem struct { OldDim1 string `json:"OldDim1"` OldDim3 string `json:"OldDim3"` - OldItemCode string `json:"OldItemCode"` - OldColor string `json:"OldColor"` - OldDim2 string `json:"OldDim2"` - OldDesc string `json:"OldDesc"` + OldItemCode string `json:"OldItemCode"` + OldColor string `json:"OldColor"` + OldColorDescription string `json:"OldColorDescription"` + OldDim2 string `json:"OldDim2"` + OldDesc string `json:"OldDesc"` + OldQty float64 `json:"OldQty"` NewItemCode string `json:"NewItemCode"` NewColor string `json:"NewColor"` NewDim2 string `json:"NewDim2"` NewDesc string `json:"NewDesc"` - IsVariantMissing bool `json:"IsVariantMissing"` + IsVariantMissing bool `json:"IsVariantMissing"` + OldDueDate string `json:"OldDueDate"` + NewDueDate string `json:"NewDueDate"` } diff --git a/svc/models/orderproductionupdate.go b/svc/models/orderproductionupdate.go index 75ba91e..216a7ad 100644 --- a/svc/models/orderproductionupdate.go +++ b/svc/models/orderproductionupdate.go @@ -1,18 +1,22 @@ 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"` + OrderLineID string `json:"OrderLineID"` + NewItemCode string `json:"NewItemCode"` + NewColor string `json:"NewColor"` + ItemDim1Code *string `json:"ItemDim1Code,omitempty"` + NewDim2 string `json:"NewDim2"` + NewDesc string `json:"NewDesc"` + OldDueDate string `json:"OldDueDate"` + NewDueDate string `json:"NewDueDate"` } type OrderProductionUpdatePayload struct { - Lines []OrderProductionUpdateLine `json:"lines"` - InsertMissing bool `json:"insertMissing"` - CdItems []OrderProductionCdItemDraft `json:"cdItems"` - ProductAttributes []OrderProductionItemAttributeRow `json:"productAttributes"` + Lines []OrderProductionUpdateLine `json:"lines"` + InsertMissing bool `json:"insertMissing"` + CdItems []OrderProductionCdItemDraft `json:"cdItems"` + ProductAttributes []OrderProductionItemAttributeRow `json:"productAttributes"` + HeaderAverageDueDate *string `json:"HeaderAverageDueDate,omitempty"` } type OrderProductionMissingVariant struct { @@ -25,6 +29,19 @@ type OrderProductionMissingVariant struct { ItemDim3Code string `json:"ItemDim3Code"` } +type OrderProductionBarcodeValidation struct { + Code string `json:"code"` + Message string `json:"message"` + Barcode string `json:"barcode,omitempty"` + BarcodeTypeCode string `json:"barcodeTypeCode,omitempty"` + ItemTypeCode int16 `json:"ItemTypeCode,omitempty"` + ItemCode string `json:"ItemCode,omitempty"` + ColorCode string `json:"ColorCode,omitempty"` + ItemDim1Code string `json:"ItemDim1Code,omitempty"` + ItemDim2Code string `json:"ItemDim2Code,omitempty"` + ItemDim3Code string `json:"ItemDim3Code,omitempty"` +} + type OrderProductionCdItemDraft struct { ItemTypeCode int16 `json:"ItemTypeCode"` ItemCode string `json:"ItemCode"` diff --git a/svc/models/product_pricing.go b/svc/models/product_pricing.go new file mode 100644 index 0000000..eeda251 --- /dev/null +++ b/svc/models/product_pricing.go @@ -0,0 +1,18 @@ +package models + +type ProductPricing struct { + ProductCode string `json:"ProductCode"` + CostPrice float64 `json:"CostPrice"` + StockQty float64 `json:"StockQty"` + StockEntryDate string `json:"StockEntryDate"` + LastPricingDate string `json:"LastPricingDate"` + AskiliYan string `json:"AskiliYan"` + Kategori string `json:"Kategori"` + UrunIlkGrubu string `json:"UrunIlkGrubu"` + UrunAnaGrubu string `json:"UrunAnaGrubu"` + UrunAltGrubu string `json:"UrunAltGrubu"` + Icerik string `json:"Icerik"` + Karisim string `json:"Karisim"` + Marka string `json:"Marka"` + BrandGroupSec string `json:"BrandGroupSec"` +} diff --git a/svc/queries/orderproduction_items.go b/svc/queries/orderproduction_items.go index eb82113..71a36c3 100644 --- a/svc/queries/orderproduction_items.go +++ b/svc/queries/orderproduction_items.go @@ -4,6 +4,7 @@ import ( "database/sql" "fmt" "log" + "sort" "strconv" "strings" "time" @@ -25,14 +26,24 @@ SELECT ISNULL(l.ItemCode,'') AS OldItemCode, ISNULL(l.ColorCode,'') AS OldColor, + ISNULL(( + SELECT TOP 1 LTRIM(RTRIM(cd.ColorDescription)) + FROM dbo.cdColorDesc cd WITH (NOLOCK) + WHERE cd.ColorCode = l.ColorCode + AND cd.LangCode = N'TR' + ), '') AS OldColorDescription, ISNULL(l.ItemDim2Code,'') AS OldDim2, ISNULL(l.LineDescription,'') AS OldDesc, + CAST(ISNULL(l.Qty1, 0) AS FLOAT) AS OldQty, CAST('' AS NVARCHAR(60)) AS NewItemCode, CAST('' AS NVARCHAR(30)) AS NewColor, CAST('' AS NVARCHAR(30)) AS NewDim2, CAST('' AS NVARCHAR(250)) AS NewDesc, + CONVERT(NVARCHAR(10), l.DeliveryDate, 126) AS OldDueDate, + CONVERT(NVARCHAR(10), l.DeliveryDate, 126) AS NewDueDate, + CAST(0 AS bit) AS IsVariantMissing FROM dbo.trOrderLine l WHERE l.OrderHeaderID = @p1 @@ -522,18 +533,25 @@ func UpdateOrderLinesTx(tx *sql.Tx, orderHeaderID string, lines []models.OrderPr chunk := lines[i:end] values := make([]string, 0, len(chunk)) - args := make([]any, 0, len(chunk)*5+2) + args := make([]any, 0, len(chunk)*8+2) paramPos := 1 for _, line := range chunk { - values = append(values, fmt.Sprintf("(@p%d,@p%d,@p%d,@p%d,@p%d)", paramPos, paramPos+1, paramPos+2, paramPos+3, paramPos+4)) + var itemDim1 any + if line.ItemDim1Code != nil { + itemDim1 = strings.TrimSpace(*line.ItemDim1Code) + } + values = append(values, fmt.Sprintf("(@p%d,@p%d,@p%d,@p%d,@p%d,@p%d,@p%d,@p%d)", paramPos, paramPos+1, paramPos+2, paramPos+3, paramPos+4, paramPos+5, paramPos+6, paramPos+7)) args = append(args, strings.TrimSpace(line.OrderLineID), line.NewItemCode, line.NewColor, + itemDim1, line.NewDim2, line.NewDesc, + line.OldDueDate, + line.NewDueDate, ) - paramPos += 5 + paramPos += 8 } orderHeaderParam := paramPos @@ -542,16 +560,18 @@ func UpdateOrderLinesTx(tx *sql.Tx, orderHeaderID string, lines []models.OrderPr query := fmt.Sprintf(` SET NOCOUNT ON; -WITH src (OrderLineID, NewItemCode, NewColor, NewDim2, NewDesc) AS ( +WITH src (OrderLineID, NewItemCode, NewColor, ItemDim1Code, NewDim2, NewDesc, OldDueDate, NewDueDate) AS ( SELECT * - FROM (VALUES %s) AS v (OrderLineID, NewItemCode, NewColor, NewDim2, NewDesc) + FROM (VALUES %s) AS v (OrderLineID, NewItemCode, NewColor, ItemDim1Code, NewDim2, NewDesc, OldDueDate, NewDueDate) ) UPDATE l SET l.ItemCode = s.NewItemCode, l.ColorCode = s.NewColor, + l.ItemDim1Code = COALESCE(s.ItemDim1Code, l.ItemDim1Code), l.ItemDim2Code = s.NewDim2, l.LineDescription = COALESCE(NULLIF(s.NewDesc,''), l.LineDescription), + l.DeliveryDate = CASE WHEN ISDATE(s.NewDueDate) = 1 THEN CAST(s.NewDueDate AS DATETIME) ELSE l.DeliveryDate END, l.LastUpdatedUserName = @p%d, l.LastUpdatedDate = GETDATE() FROM dbo.trOrderLine l @@ -574,6 +594,344 @@ WHERE l.OrderHeaderID = @p%d; return updated, nil } +func UpdateOrderHeaderAverageDueDateTx(tx *sql.Tx, orderHeaderID string, averageDueDate *string, username string) error { + if averageDueDate == nil { + return nil + } + + dueDate := strings.TrimSpace(*averageDueDate) + if dueDate != "" { + if _, err := time.Parse("2006-01-02", dueDate); err != nil { + return fmt.Errorf("invalid header average due date %q: %w", dueDate, err) + } + } + + _, err := tx.Exec(` +UPDATE dbo.trOrderHeader +SET + AverageDueDate = CASE WHEN @p1 = '' THEN NULL ELSE CAST(@p1 AS DATETIME) END, + LastUpdatedUserName = @p2, + LastUpdatedDate = GETDATE() +WHERE OrderHeaderID = @p3; +`, dueDate, username, orderHeaderID) + return err +} + +type sqlQueryRower interface { + QueryRow(query string, args ...any) *sql.Row +} + +type plannedProductionBarcode struct { + Barcode string + BarcodeTypeCode string + ItemTypeCode int16 + ItemCode string + ColorCode string + ItemDim1Code string + ItemDim2Code string + ItemDim3Code string +} + +func barcodeTypeExists(q sqlQueryRower, barcodeTypeCode string) (bool, error) { + var exists int + err := q.QueryRow(` +SELECT TOP 1 1 +FROM dbo.cdBarcodeType +WHERE BarcodeTypeCode = @p1 +`, strings.TrimSpace(barcodeTypeCode)).Scan(&exists) + if err == sql.ErrNoRows { + return false, nil + } + if err != nil { + return false, err + } + return true, nil +} + +func barcodeExists(q sqlQueryRower, barcode string) (bool, error) { + var exists int + err := q.QueryRow(` +SELECT TOP 1 1 +FROM dbo.prItemBarcode WITH (UPDLOCK, HOLDLOCK) +WHERE Barcode = @p1 +`, strings.TrimSpace(barcode)).Scan(&exists) + if err == sql.ErrNoRows { + return false, nil + } + if err != nil { + return false, err + } + return true, nil +} + +func existingVariantBarcode(q sqlQueryRower, barcodeTypeCode string, itemTypeCode int16, itemCode string, colorCode string, dim1 string, dim2 string, dim3 string) (string, bool, error) { + var barcode string + err := q.QueryRow(` +SELECT TOP 1 LTRIM(RTRIM(ISNULL(Barcode, ''))) +FROM dbo.prItemBarcode WITH (UPDLOCK, HOLDLOCK) +WHERE BarcodeTypeCode = @p1 + AND ItemTypeCode = @p2 + AND ISNULL(LTRIM(RTRIM(ItemCode)), '') = @p3 + AND ISNULL(LTRIM(RTRIM(ColorCode)), '') = @p4 + AND ISNULL(LTRIM(RTRIM(ItemDim1Code)), '') = @p5 + AND ISNULL(LTRIM(RTRIM(ItemDim2Code)), '') = @p6 + AND ISNULL(LTRIM(RTRIM(ItemDim3Code)), '') = @p7 + AND ISNULL(LTRIM(RTRIM(UnitOfMeasureCode)), '') = 'AD' +ORDER BY TRY_CONVERT(BIGINT, NULLIF(LTRIM(RTRIM(Barcode)), '')) DESC, Barcode DESC +`, strings.TrimSpace(barcodeTypeCode), itemTypeCode, strings.TrimSpace(itemCode), strings.TrimSpace(colorCode), strings.TrimSpace(dim1), strings.TrimSpace(dim2), strings.TrimSpace(dim3)).Scan(&barcode) + if err == sql.ErrNoRows { + return "", false, nil + } + if err != nil { + return "", false, err + } + return strings.TrimSpace(barcode), true, nil +} + +func maxNumericBarcode(q sqlQueryRower) (int64, error) { + var maxBarcode int64 + err := q.QueryRow(` +SELECT ISNULL(MAX(TRY_CONVERT(BIGINT, NULLIF(LTRIM(RTRIM(Barcode)), ''))), 0) +FROM dbo.prItemBarcode WITH (UPDLOCK, HOLDLOCK) +`).Scan(&maxBarcode) + return maxBarcode, err +} + +func ValidateProductionBarcodePlan(q sqlQueryRower, variants []models.OrderProductionMissingVariant, barcodeTypeCode string) ([]models.OrderProductionBarcodeValidation, error) { + typeCode := strings.ToUpper(strings.TrimSpace(barcodeTypeCode)) + if len(variants) == 0 { + return nil, nil + } + + validations := make([]models.OrderProductionBarcodeValidation, 0) + typeExists, err := barcodeTypeExists(q, typeCode) + if err != nil { + return nil, err + } + if !typeExists { + validations = append(validations, models.OrderProductionBarcodeValidation{ + Code: "invalid_barcode_type", + Message: fmt.Sprintf("Barkod tipi bulunamadi: %s", typeCode), + BarcodeTypeCode: typeCode, + }) + return validations, nil + } + + sorted := append([]models.OrderProductionMissingVariant(nil), variants...) + sort.Slice(sorted, func(i, j int) bool { + left := sorted[i] + right := sorted[j] + leftKey := fmt.Sprintf("%05d|%s|%s|%s|%s|%s", left.ItemTypeCode, left.ItemCode, left.ColorCode, left.ItemDim1Code, left.ItemDim2Code, left.ItemDim3Code) + rightKey := fmt.Sprintf("%05d|%s|%s|%s|%s|%s", right.ItemTypeCode, right.ItemCode, right.ColorCode, right.ItemDim1Code, right.ItemDim2Code, right.ItemDim3Code) + return leftKey < rightKey + }) + + maxBarcode, err := maxNumericBarcode(q) + if err != nil { + return nil, err + } + + nextOffset := int64(0) + planned := make(map[string]struct{}, len(sorted)) + for _, variant := range sorted { + existingBarcode, exists, err := existingVariantBarcode(q, typeCode, variant.ItemTypeCode, variant.ItemCode, variant.ColorCode, variant.ItemDim1Code, variant.ItemDim2Code, variant.ItemDim3Code) + if err != nil { + return nil, err + } + if exists && existingBarcode != "" { + continue + } + + nextOffset++ + barcode := strconv.FormatInt(maxBarcode+nextOffset, 10) + if _, duplicated := planned[barcode]; duplicated { + validations = append(validations, models.OrderProductionBarcodeValidation{ + Code: "barcode_duplicate_in_plan", + Message: fmt.Sprintf("Planlanan barkod ayni istekte birden fazla kez olusuyor: %s", barcode), + Barcode: barcode, + BarcodeTypeCode: typeCode, + ItemTypeCode: variant.ItemTypeCode, + ItemCode: strings.TrimSpace(variant.ItemCode), + ColorCode: strings.TrimSpace(variant.ColorCode), + ItemDim1Code: strings.TrimSpace(variant.ItemDim1Code), + ItemDim2Code: strings.TrimSpace(variant.ItemDim2Code), + ItemDim3Code: strings.TrimSpace(variant.ItemDim3Code), + }) + continue + } + planned[barcode] = struct{}{} + + inUse, err := barcodeExists(q, barcode) + if err != nil { + return nil, err + } + if inUse { + validations = append(validations, models.OrderProductionBarcodeValidation{ + Code: "barcode_in_use", + Message: fmt.Sprintf("Barkod daha once kullanilmis: %s (%s / %s / %s / %s)", barcode, strings.TrimSpace(variant.ItemCode), strings.TrimSpace(variant.ColorCode), strings.TrimSpace(variant.ItemDim1Code), strings.TrimSpace(variant.ItemDim2Code)), + Barcode: barcode, + BarcodeTypeCode: typeCode, + ItemTypeCode: variant.ItemTypeCode, + ItemCode: strings.TrimSpace(variant.ItemCode), + ColorCode: strings.TrimSpace(variant.ColorCode), + ItemDim1Code: strings.TrimSpace(variant.ItemDim1Code), + ItemDim2Code: strings.TrimSpace(variant.ItemDim2Code), + ItemDim3Code: strings.TrimSpace(variant.ItemDim3Code), + }) + } + } + + return validations, nil +} + +func UpsertItemBarcodesTx(tx *sql.Tx, orderHeaderID string, lines []models.OrderProductionUpdateLine, username string) (int64, error) { + start := time.Now() + if len(lines) == 0 { + log.Printf("[UpsertItemBarcodesTx] lines=0 inserted=0 duration_ms=0") + return 0, nil + } + + lineIDs := make([]string, 0, len(lines)) + seen := make(map[string]struct{}, len(lines)) + for _, line := range lines { + lineID := strings.TrimSpace(line.OrderLineID) + if lineID == "" { + continue + } + if _, ok := seen[lineID]; ok { + continue + } + seen[lineID] = struct{}{} + lineIDs = append(lineIDs, lineID) + } + if len(lineIDs) == 0 { + log.Printf("[UpsertItemBarcodesTx] lines=%d uniqueLineIDs=0 inserted=0 duration_ms=%d", len(lines), time.Since(start).Milliseconds()) + return 0, nil + } + + const chunkSize = 900 + var inserted int64 + + for i := 0; i < len(lineIDs); i += chunkSize { + end := i + chunkSize + if end > len(lineIDs) { + end = len(lineIDs) + } + chunk := lineIDs[i:end] + + values := make([]string, 0, len(chunk)) + args := make([]any, 0, len(chunk)+2) + paramPos := 1 + for _, lineID := range chunk { + values = append(values, fmt.Sprintf("(@p%d)", paramPos)) + args = append(args, lineID) + paramPos++ + } + + orderHeaderParam := paramPos + usernameParam := paramPos + 1 + args = append(args, orderHeaderID, username) + + query := fmt.Sprintf(` +SET NOCOUNT ON; +WITH srcLine (OrderLineID) AS ( + SELECT * + FROM (VALUES %s) AS v (OrderLineID) +), +src AS ( + SELECT DISTINCT + l.ItemTypeCode, + UPPER(LTRIM(RTRIM(ISNULL(l.ItemCode, '')))) AS ItemCode, + UPPER(LTRIM(RTRIM(ISNULL(l.ColorCode, '')))) AS ColorCode, + UPPER(LTRIM(RTRIM(ISNULL(l.ItemDim1Code, '')))) AS ItemDim1Code, + UPPER(LTRIM(RTRIM(ISNULL(l.ItemDim2Code, '')))) AS ItemDim2Code, + CAST('' AS NVARCHAR(50)) AS ItemDim3Code + FROM dbo.trOrderLine l WITH (UPDLOCK, HOLDLOCK) + JOIN srcLine s + ON CAST(l.OrderLineID AS NVARCHAR(50)) = s.OrderLineID + WHERE l.OrderHeaderID = @p%d + AND NULLIF(LTRIM(RTRIM(ISNULL(l.ItemCode, ''))), '') IS NOT NULL +), +missing AS ( + SELECT + s.ItemTypeCode, + s.ItemCode, + s.ColorCode, + s.ItemDim1Code, + s.ItemDim2Code, + s.ItemDim3Code, + ROW_NUMBER() OVER ( + ORDER BY s.ItemCode, s.ColorCode, s.ItemDim1Code, s.ItemDim2Code, s.ItemDim3Code + ) AS RowNo + FROM src s + LEFT JOIN dbo.prItemBarcode b WITH (UPDLOCK, HOLDLOCK) + ON UPPER(LTRIM(RTRIM(ISNULL(b.BarcodeTypeCode, '')))) = 'BAGGI3' + AND b.ItemTypeCode = s.ItemTypeCode + AND UPPER(LTRIM(RTRIM(ISNULL(b.ItemCode, '')))) = s.ItemCode + AND UPPER(LTRIM(RTRIM(ISNULL(b.ColorCode, '')))) = s.ColorCode + AND UPPER(LTRIM(RTRIM(ISNULL(b.ItemDim1Code, '')))) = s.ItemDim1Code + AND UPPER(LTRIM(RTRIM(ISNULL(b.ItemDim2Code, '')))) = s.ItemDim2Code + AND UPPER(LTRIM(RTRIM(ISNULL(b.ItemDim3Code, '')))) = s.ItemDim3Code + AND UPPER(LTRIM(RTRIM(ISNULL(b.UnitOfMeasureCode, '')))) = 'AD' + WHERE b.Barcode IS NULL +) +INSERT INTO dbo.prItemBarcode ( + Barcode, + BarcodeTypeCode, + ItemTypeCode, + ItemCode, + ColorCode, + ItemDim1Code, + ItemDim2Code, + ItemDim3Code, + UnitOfMeasureCode, + Qty, + CreatedUserName, + CreatedDate, + LastUpdatedUserName, + LastUpdatedDate, + RowGuid +) +SELECT + CAST(seed.MaxBarcode + m.RowNo AS NVARCHAR(50)) AS Barcode, + 'BAGGI3', + m.ItemTypeCode, + m.ItemCode, + m.ColorCode, + m.ItemDim1Code, + m.ItemDim2Code, + m.ItemDim3Code, + 'AD', + 1, + @p%d, + GETDATE(), + @p%d, + GETDATE(), + NEWID() +FROM missing m +CROSS JOIN ( + SELECT ISNULL(MAX(TRY_CONVERT(BIGINT, NULLIF(LTRIM(RTRIM(Barcode)), ''))), 0) AS MaxBarcode + FROM dbo.prItemBarcode WITH (UPDLOCK, HOLDLOCK) +) seed; + +SELECT @@ROWCOUNT AS Inserted; +`, strings.Join(values, ","), orderHeaderParam, usernameParam, usernameParam) + + chunkStart := time.Now() + var chunkInserted int64 + if err := tx.QueryRow(query, args...).Scan(&chunkInserted); err != nil { + return inserted, fmt.Errorf("upsert item barcodes chunk failed chunkStart=%d chunkEnd=%d duration_ms=%d: %w", i, end, time.Since(chunkStart).Milliseconds(), err) + } + inserted += chunkInserted + log.Printf("[UpsertItemBarcodesTx] orderHeaderID=%s chunk=%d-%d chunkInserted=%d cumulative=%d duration_ms=%d", + orderHeaderID, i, end, chunkInserted, inserted, time.Since(chunkStart).Milliseconds()) + } + + log.Printf("[UpsertItemBarcodesTx] orderHeaderID=%s lines=%d uniqueLineIDs=%d inserted=%d duration_ms=%d", + orderHeaderID, len(lines), len(lineIDs), inserted, time.Since(start).Milliseconds()) + return inserted, nil +} + func UpsertItemAttributesTx(tx *sql.Tx, attrs []models.OrderProductionItemAttributeRow, username string) (int64, error) { start := time.Now() if len(attrs) == 0 { diff --git a/svc/queries/product_pricing.go b/svc/queries/product_pricing.go new file mode 100644 index 0000000..58f8bb6 --- /dev/null +++ b/svc/queries/product_pricing.go @@ -0,0 +1,193 @@ +package queries + +import ( + "bssapp-backend/db" + "bssapp-backend/models" +) + +func GetProductPricingList() ([]models.ProductPricing, error) { + rows, err := db.MssqlDB.Query(` + WITH base_products AS ( + SELECT + LTRIM(RTRIM(ProductCode)) AS ProductCode, + COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') AS AskiliYan, + COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') AS Kategori, + COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') AS UrunIlkGrubu, + COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '') AS UrunAnaGrubu, + COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') AS UrunAltGrubu, + COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') AS Icerik, + COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') AS Karisim, + COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') AS Marka + FROM ProductFilterWithDescription('TR') + WHERE ProductAtt42 IN ('SERI', 'AKSESUAR') + AND IsBlocked = 0 + AND LEN(LTRIM(RTRIM(ProductCode))) = 13 + ), + latest_base_price AS ( + SELECT + LTRIM(RTRIM(b.ItemCode)) AS ItemCode, + CAST(b.Price AS DECIMAL(18, 2)) AS CostPrice, + CONVERT(VARCHAR(10), b.PriceDate, 23) AS LastPricingDate, + ROW_NUMBER() OVER ( + PARTITION BY LTRIM(RTRIM(b.ItemCode)) + ORDER BY b.PriceDate DESC, b.LastUpdatedDate DESC + ) AS rn + FROM prItemBasePrice b + WHERE b.ItemTypeCode = 1 + AND b.BasePriceCode = 1 + AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD' + AND EXISTS ( + SELECT 1 + FROM base_products bp + WHERE bp.ProductCode = LTRIM(RTRIM(b.ItemCode)) + ) + ), + stock_entry_dates AS ( + SELECT + LTRIM(RTRIM(s.ItemCode)) AS ItemCode, + CONVERT(VARCHAR(10), MAX(s.OperationDate), 23) AS StockEntryDate + FROM trStock s WITH(NOLOCK) + WHERE s.ItemTypeCode = 1 + AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13 + AND s.In_Qty1 > 0 + AND LTRIM(RTRIM(s.WarehouseCode)) IN ( + '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', + '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', + '1-0-33','101','1-014','1-0-49','1-0-36' + ) + AND EXISTS ( + SELECT 1 + FROM base_products bp + WHERE bp.ProductCode = LTRIM(RTRIM(s.ItemCode)) + ) + GROUP BY LTRIM(RTRIM(s.ItemCode)) + ), + stock_base AS ( + SELECT + LTRIM(RTRIM(s.ItemCode)) AS ItemCode, + SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1 + FROM trStock s WITH(NOLOCK) + WHERE s.ItemTypeCode = 1 + AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13 + AND EXISTS ( + SELECT 1 + FROM base_products bp + WHERE bp.ProductCode = LTRIM(RTRIM(s.ItemCode)) + ) + GROUP BY LTRIM(RTRIM(s.ItemCode)) + ), + pick_base AS ( + SELECT + LTRIM(RTRIM(p.ItemCode)) AS ItemCode, + SUM(p.Qty1) AS PickingQty1 + FROM PickingStates p + WHERE p.ItemTypeCode = 1 + AND LEN(LTRIM(RTRIM(p.ItemCode))) = 13 + AND EXISTS ( + SELECT 1 + FROM base_products bp + WHERE bp.ProductCode = LTRIM(RTRIM(p.ItemCode)) + ) + GROUP BY LTRIM(RTRIM(p.ItemCode)) + ), + reserve_base AS ( + SELECT + LTRIM(RTRIM(r.ItemCode)) AS ItemCode, + SUM(r.Qty1) AS ReserveQty1 + FROM ReserveStates r + WHERE r.ItemTypeCode = 1 + AND LEN(LTRIM(RTRIM(r.ItemCode))) = 13 + AND EXISTS ( + SELECT 1 + FROM base_products bp + WHERE bp.ProductCode = LTRIM(RTRIM(r.ItemCode)) + ) + GROUP BY LTRIM(RTRIM(r.ItemCode)) + ), + disp_base AS ( + SELECT + LTRIM(RTRIM(d.ItemCode)) AS ItemCode, + SUM(d.Qty1) AS DispOrderQty1 + FROM DispOrderStates d + WHERE d.ItemTypeCode = 1 + AND LEN(LTRIM(RTRIM(d.ItemCode))) = 13 + AND EXISTS ( + SELECT 1 + FROM base_products bp + WHERE bp.ProductCode = LTRIM(RTRIM(d.ItemCode)) + ) + GROUP BY LTRIM(RTRIM(d.ItemCode)) + ), + stock_totals AS ( + SELECT + bp.ProductCode AS ItemCode, + CAST(ROUND( + ISNULL(sb.InventoryQty1, 0) + - ISNULL(pb.PickingQty1, 0) + - ISNULL(rb.ReserveQty1, 0) + - ISNULL(db.DispOrderQty1, 0) + , 2) AS DECIMAL(18, 2)) AS StockQty + FROM base_products bp + LEFT JOIN stock_base sb + ON sb.ItemCode = bp.ProductCode + LEFT JOIN pick_base pb + ON pb.ItemCode = bp.ProductCode + LEFT JOIN reserve_base rb + ON rb.ItemCode = bp.ProductCode + LEFT JOIN disp_base db + ON db.ItemCode = bp.ProductCode + ) + SELECT + bp.ProductCode AS ProductCode, + COALESCE(lp.CostPrice, 0) AS CostPrice, + COALESCE(st.StockQty, 0) AS StockQty, + COALESCE(se.StockEntryDate, '') AS StockEntryDate, + COALESCE(lp.LastPricingDate, '') AS LastPricingDate, + bp.AskiliYan, + bp.Kategori, + bp.UrunIlkGrubu, + bp.UrunAnaGrubu, + bp.UrunAltGrubu, + bp.Icerik, + bp.Karisim, + bp.Marka + FROM base_products bp + LEFT JOIN latest_base_price lp + ON lp.ItemCode = bp.ProductCode + AND lp.rn = 1 + LEFT JOIN stock_entry_dates se + ON se.ItemCode = bp.ProductCode + LEFT JOIN stock_totals st + ON st.ItemCode = bp.ProductCode + ORDER BY bp.ProductCode; + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []models.ProductPricing + for rows.Next() { + var item models.ProductPricing + if err := rows.Scan( + &item.ProductCode, + &item.CostPrice, + &item.StockQty, + &item.StockEntryDate, + &item.LastPricingDate, + &item.AskiliYan, + &item.Kategori, + &item.UrunIlkGrubu, + &item.UrunAnaGrubu, + &item.UrunAltGrubu, + &item.Icerik, + &item.Karisim, + &item.Marka, + ); err != nil { + return nil, err + } + out = append(out, item) + } + + return out, nil +} diff --git a/svc/routes/login.go b/svc/routes/login.go index d9a340f..6d45279 100644 --- a/svc/routes/login.go +++ b/svc/routes/login.go @@ -464,6 +464,7 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc { defer tx.Rollback() var newID int64 + log.Printf("DEBUG: UserCreateRoute payload=%+v", payload) err = tx.QueryRow(` INSERT INTO mk_dfusr ( username, @@ -472,11 +473,12 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc { email, mobile, address, + password_hash, force_password_change, created_at, updated_at ) - VALUES ($1,$2,$3,$4,$5,$6,true,NOW(),NOW()) + VALUES ($1,$2,$3,$4,$5,$6,'',true,NOW(),NOW()) RETURNING id `, payload.Code, @@ -489,7 +491,7 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc { if err != nil { log.Printf("USER INSERT ERROR code=%q email=%q err=%v", payload.Code, payload.Email, err) - http.Error(w, "Kullanıcı oluşturulamadı", http.StatusInternalServerError) + http.Error(w, fmt.Sprintf("Kullanıcı oluşturulamadı: %v", err), http.StatusInternalServerError) return } diff --git a/svc/routes/order_mail.go b/svc/routes/order_mail.go index 6e3ea5d..d3cebc5 100644 --- a/svc/routes/order_mail.go +++ b/svc/routes/order_mail.go @@ -15,12 +15,23 @@ import ( ) type sendOrderMarketMailPayload struct { - OrderHeaderID string `json:"orderHeaderID"` - Operation string `json:"operation"` - DeletedItems []string `json:"deletedItems"` - UpdatedItems []string `json:"updatedItems"` - AddedItems []string `json:"addedItems"` - ExtraRecipients []string `json:"extraRecipients"` + OrderHeaderID string `json:"orderHeaderID"` + Operation string `json:"operation"` + DeletedItems []string `json:"deletedItems"` + UpdatedItems []string `json:"updatedItems"` + AddedItems []string `json:"addedItems"` + OldDueDate string `json:"oldDueDate"` + NewDueDate string `json:"newDueDate"` + ExtraRecipients []string `json:"extraRecipients"` + DueDateChanges []sendOrderMailDueDateChange `json:"dueDateChanges"` +} + +type sendOrderMailDueDateChange struct { + ItemCode string `json:"itemCode"` + ColorCode string `json:"colorCode"` + ItemDim2Code string `json:"itemDim2Code"` + OldDueDate string `json:"oldDueDate"` + NewDueDate string `json:"newDueDate"` } func SendOrderMarketMailHandler(pg *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) http.HandlerFunc { @@ -108,6 +119,18 @@ func SendOrderMarketMailHandler(pg *sql.DB, mssql *sql.DB, ml *mailer.GraphMaile if isUpdate { subjectAction = "SİPARİŞ GÜNCELLENDİ." } + if payload.NewDueDate != "" && payload.OldDueDate != payload.NewDueDate { + subjectAction = "SİPARİŞ TERMİNİ GÜNCELLENDİ." + } + if isUpdate && subjectAction == "SİPARİŞ GÜNCELLENDİ." { + // Satır bazlı termin kontrolü + for _, item := range payload.UpdatedItems { + if strings.Contains(item, "Termin:") { + subjectAction = "SİPARİŞ TERMİNİ GÜNCELLENDİ." + break + } + } + } subject := fmt.Sprintf("%s kullanıcısı tarafından %s %s", actor, number, subjectAction) cariDetail := "" @@ -127,6 +150,13 @@ func SendOrderMarketMailHandler(pg *sql.DB, mssql *sql.DB, ml *mailer.GraphMaile `

`, ) + if payload.NewDueDate != "" && payload.OldDueDate != payload.NewDueDate { + body = append(body, + fmt.Sprintf(`

Termin Değişikliği: %s → %s

`, + htmlEsc(payload.OldDueDate), htmlEsc(payload.NewDueDate)), + ) + } + if isUpdate { body = append(body, renderItemListHTML("Silinen Ürün Kodları", payload.DeletedItems), @@ -137,6 +167,10 @@ func SendOrderMarketMailHandler(pg *sql.DB, mssql *sql.DB, ml *mailer.GraphMaile body = append(body, `

Bu sipariş BaggiSS App Uygulamasından oluşturulmuştur.

`) body = append(body, `

PDF ektedir.

`) + if dueDateTableHTML := renderDueDateChangesTableHTML("Termin DeÄŸiÅŸiklikleri", payload.DueDateChanges); dueDateTableHTML != "" { + body = append(body, dueDateTableHTML) + } + bodyHTML := strings.Join(body, "\n") fileNo := sanitizeFileName(number) @@ -393,3 +427,54 @@ func renderItemListHTML(title string, items []string) string { b = append(b, `

`) return strings.Join(b, "\n") } + +func renderDueDateChangesTableHTML(title string, rows []sendOrderMailDueDateChange) string { + if len(rows) == 0 { + return "" + } + + seen := make(map[string]struct{}, len(rows)) + clean := make([]sendOrderMailDueDateChange, 0, len(rows)) + for _, row := range rows { + itemCode := strings.TrimSpace(row.ItemCode) + colorCode := strings.TrimSpace(row.ColorCode) + itemDim2Code := strings.TrimSpace(row.ItemDim2Code) + oldDueDate := strings.TrimSpace(row.OldDueDate) + newDueDate := strings.TrimSpace(row.NewDueDate) + if itemCode == "" || newDueDate == "" || oldDueDate == newDueDate { + continue + } + key := strings.ToUpper(strings.Join([]string{itemCode, colorCode, itemDim2Code, oldDueDate, newDueDate}, "|")) + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + clean = append(clean, sendOrderMailDueDateChange{ + ItemCode: itemCode, + ColorCode: colorCode, + ItemDim2Code: itemDim2Code, + OldDueDate: oldDueDate, + NewDueDate: newDueDate, + }) + } + + if len(clean) == 0 { + return "" + } + + var b strings.Builder + b.WriteString(fmt.Sprintf(`

%s:

`, htmlEsc(title))) + b.WriteString(``) + b.WriteString(``) + for _, row := range clean { + b.WriteString("") + b.WriteString(fmt.Sprintf("", htmlEsc(row.ItemCode))) + b.WriteString(fmt.Sprintf("", htmlEsc(row.ColorCode))) + b.WriteString(fmt.Sprintf("", htmlEsc(row.ItemDim2Code))) + b.WriteString(fmt.Sprintf("", htmlEsc(row.OldDueDate))) + b.WriteString(fmt.Sprintf(``, htmlEsc(row.NewDueDate))) + b.WriteString("") + } + b.WriteString(`
Ürün KoduRenk2. RenkEski TerminYeni Termin
%s%s%s%s%s
`) + return b.String() +} diff --git a/svc/routes/orderproductionitems.go b/svc/routes/orderproductionitems.go index 6b1f5cd..1fac066 100644 --- a/svc/routes/orderproductionitems.go +++ b/svc/routes/orderproductionitems.go @@ -2,8 +2,10 @@ package routes import ( "bssapp-backend/auth" + "bssapp-backend/internal/mailer" "bssapp-backend/models" "bssapp-backend/queries" + "context" "database/sql" "encoding/json" "errors" @@ -20,6 +22,8 @@ import ( var baggiModelCodeRegex = regexp.MustCompile(`^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$`) +const productionBarcodeTypeCode = "BAGGI3" + // ====================================================== // 📌 OrderProductionItemsRoute — U ürün satırları // ====================================================== @@ -54,12 +58,16 @@ func OrderProductionItemsRoute(mssql *sql.DB) http.Handler { &o.OldDim3, &o.OldItemCode, &o.OldColor, + &o.OldColorDescription, &o.OldDim2, &o.OldDesc, + &o.OldQty, &o.NewItemCode, &o.NewColor, &o.NewDim2, &o.NewDesc, + &o.OldDueDate, + &o.NewDueDate, &o.IsVariantMissing, ); err != nil { log.Printf("⚠️ SCAN HATASI: %v", err) @@ -183,9 +191,22 @@ func OrderProductionValidateRoute(mssql *sql.DB) http.Handler { log.Printf("[OrderProductionValidateRoute] rid=%s orderHeaderID=%s lineCount=%d missingCount=%d build_missing_ms=%d total_ms=%d", rid, id, len(payload.Lines), len(missing), time.Since(stepStart).Milliseconds(), time.Since(start).Milliseconds()) + targetVariants, err := buildTargetVariants(mssql, id, payload.Lines) + if err != nil { + writeDBError(w, http.StatusInternalServerError, "validate_barcode_targets", id, "", len(payload.Lines), err) + return + } + barcodeValidations, err := queries.ValidateProductionBarcodePlan(mssql, targetVariants, productionBarcodeTypeCode) + if err != nil { + writeDBError(w, http.StatusInternalServerError, "validate_barcodes", id, "", len(payload.Lines), err) + return + } + resp := map[string]any{ - "missingCount": len(missing), - "missing": missing, + "missingCount": len(missing), + "missing": missing, + "barcodeValidationCount": len(barcodeValidations), + "barcodeValidations": barcodeValidations, } if err := json.NewEncoder(w).Encode(resp); err != nil { log.Printf("❌ encode error: %v", err) @@ -196,7 +217,7 @@ func OrderProductionValidateRoute(mssql *sql.DB) http.Handler { // ====================================================== // OrderProductionApplyRoute - yeni model varyant guncelleme // ====================================================== -func OrderProductionApplyRoute(mssql *sql.DB) http.Handler { +func OrderProductionApplyRoute(mssql *sql.DB, ml *mailer.GraphMailer) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") rid := fmt.Sprintf("opa-%d", time.Now().UnixNano()) @@ -232,6 +253,12 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler { log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s lineCount=%d missingCount=%d build_missing_ms=%d", rid, id, len(payload.Lines), len(missing), time.Since(stepMissingStart).Milliseconds()) + targetVariants, err := buildTargetVariants(mssql, id, payload.Lines) + if err != nil { + writeDBError(w, http.StatusInternalServerError, "apply_barcode_targets", id, "", len(payload.Lines), err) + return + } + if len(missing) > 0 && !payload.InsertMissing { log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s early_exit=missing_variants total_ms=%d", rid, id, time.Since(start).Milliseconds()) @@ -282,6 +309,24 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler { rid, id, inserted, time.Since(stepInsertMissingStart).Milliseconds()) } + stepValidateBarcodeStart := time.Now() + barcodeValidations, err := queries.ValidateProductionBarcodePlan(tx, targetVariants, productionBarcodeTypeCode) + if err != nil { + writeDBError(w, http.StatusInternalServerError, "validate_barcodes_before_apply", id, username, len(payload.Lines), err) + return + } + if len(barcodeValidations) > 0 { + w.WriteHeader(http.StatusConflict) + _ = json.NewEncoder(w).Encode(map[string]any{ + "message": "Barkod validasyonu basarisiz", + "barcodeValidationCount": len(barcodeValidations), + "barcodeValidations": barcodeValidations, + }) + return + } + log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=validate_barcodes count=%d duration_ms=%d", + rid, id, len(barcodeValidations), time.Since(stepValidateBarcodeStart).Milliseconds()) + stepValidateAttrStart := time.Now() if err := validateProductAttributes(payload.ProductAttributes); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) @@ -290,6 +335,14 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler { log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=validate_attributes count=%d duration_ms=%d", rid, id, len(payload.ProductAttributes), time.Since(stepValidateAttrStart).Milliseconds()) + stepUpdateHeaderStart := time.Now() + if err := queries.UpdateOrderHeaderAverageDueDateTx(tx, id, payload.HeaderAverageDueDate, username); err != nil { + writeDBError(w, http.StatusInternalServerError, "update_order_header_average_due_date", id, username, 0, err) + return + } + log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=update_header_average_due_date changed=%t duration_ms=%d", + rid, id, payload.HeaderAverageDueDate != nil, time.Since(stepUpdateHeaderStart).Milliseconds()) + stepUpdateLinesStart := time.Now() updated, err := queries.UpdateOrderLinesTx(tx, id, payload.Lines, username) if err != nil { @@ -299,6 +352,15 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler { log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=update_lines updated=%d duration_ms=%d", rid, id, updated, time.Since(stepUpdateLinesStart).Milliseconds()) + stepUpsertBarcodeStart := time.Now() + barcodeInserted, err := queries.UpsertItemBarcodesTx(tx, id, payload.Lines, username) + if err != nil { + writeDBError(w, http.StatusInternalServerError, "upsert_item_barcodes", id, username, len(payload.Lines), err) + return + } + log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=upsert_barcodes inserted=%d duration_ms=%d", + rid, id, barcodeInserted, time.Since(stepUpsertBarcodeStart).Milliseconds()) + stepUpsertAttrStart := time.Now() attributeAffected, err := queries.UpsertItemAttributesTx(tx, payload.ProductAttributes, username) if err != nil { @@ -316,13 +378,27 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler { log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=commit duration_ms=%d total_ms=%d", rid, id, time.Since(stepCommitStart).Milliseconds(), time.Since(start).Milliseconds()) + // Mail gönderim mantığı + if false && ml != nil { + go func() { + defer func() { + if r := recover(); r != nil { + log.Printf("[OrderProductionApplyRoute] mail panic recover: %v", r) + } + }() + sendProductionUpdateMails(mssql, ml, id, username, payload.Lines) + }() + } + resp := map[string]any{ "updated": updated, "inserted": inserted, + "barcodeInserted": barcodeInserted, "attributeUpserted": attributeAffected, + "headerUpdated": payload.HeaderAverageDueDate != nil, } - log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s result updated=%d inserted=%d attributeUpserted=%d", - rid, id, updated, inserted, attributeAffected) + log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s result updated=%d inserted=%d barcodeInserted=%d attributeUpserted=%d", + rid, id, updated, inserted, barcodeInserted, attributeAffected) if err := json.NewEncoder(w).Encode(resp); err != nil { log.Printf("❌ encode error: %v", err) } @@ -367,21 +443,20 @@ func buildCdItemDraftMap(list []models.OrderProductionCdItemDraft) map[string]mo return out } -func buildMissingVariants(mssql *sql.DB, orderHeaderID string, lines []models.OrderProductionUpdateLine) ([]models.OrderProductionMissingVariant, error) { +func buildTargetVariants(mssql *sql.DB, orderHeaderID string, lines []models.OrderProductionUpdateLine) ([]models.OrderProductionMissingVariant, error) { start := time.Now() - missing := make([]models.OrderProductionMissingVariant, 0) lineDimsMap, err := queries.GetOrderLineDimsMap(mssql, orderHeaderID) if err != nil { return nil, err } - existsCache := make(map[string]bool, len(lines)) + out := make([]models.OrderProductionMissingVariant, 0, len(lines)) + seen := make(map[string]struct{}, len(lines)) for _, line := range lines { lineID := strings.TrimSpace(line.OrderLineID) - newItem := strings.TrimSpace(line.NewItemCode) - newColor := strings.TrimSpace(line.NewColor) - newDim2 := strings.TrimSpace(line.NewDim2) - + newItem := strings.ToUpper(strings.TrimSpace(line.NewItemCode)) + newColor := strings.ToUpper(strings.TrimSpace(line.NewColor)) + newDim2 := strings.ToUpper(strings.TrimSpace(line.NewDim2)) if lineID == "" || newItem == "" { continue } @@ -391,38 +466,68 @@ func buildMissingVariants(mssql *sql.DB, orderHeaderID string, lines []models.Or continue } + dim1 := strings.ToUpper(strings.TrimSpace(dims.ItemDim1Code)) + if line.ItemDim1Code != nil { + dim1 = strings.ToUpper(strings.TrimSpace(*line.ItemDim1Code)) + } + dim3 := strings.ToUpper(strings.TrimSpace(dims.ItemDim3Code)) + + key := fmt.Sprintf("%d|%s|%s|%s|%s|%s", dims.ItemTypeCode, newItem, newColor, dim1, newDim2, dim3) + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + + out = append(out, models.OrderProductionMissingVariant{ + OrderLineID: lineID, + ItemTypeCode: dims.ItemTypeCode, + ItemCode: newItem, + ColorCode: newColor, + ItemDim1Code: dim1, + ItemDim2Code: newDim2, + ItemDim3Code: dim3, + }) + } + + log.Printf("[buildTargetVariants] orderHeaderID=%s lineCount=%d dimMapCount=%d targetCount=%d total_ms=%d", + orderHeaderID, len(lines), len(lineDimsMap), len(out), time.Since(start).Milliseconds()) + return out, nil +} + +func buildMissingVariants(mssql *sql.DB, orderHeaderID string, lines []models.OrderProductionUpdateLine) ([]models.OrderProductionMissingVariant, error) { + start := time.Now() + targets, err := buildTargetVariants(mssql, orderHeaderID, lines) + if err != nil { + return nil, err + } + missing := make([]models.OrderProductionMissingVariant, 0, len(targets)) + existsCache := make(map[string]bool, len(targets)) + + for _, target := range targets { cacheKey := fmt.Sprintf("%d|%s|%s|%s|%s|%s", - dims.ItemTypeCode, - strings.ToUpper(strings.TrimSpace(newItem)), - strings.ToUpper(strings.TrimSpace(newColor)), - strings.ToUpper(strings.TrimSpace(dims.ItemDim1Code)), - strings.ToUpper(strings.TrimSpace(newDim2)), - strings.ToUpper(strings.TrimSpace(dims.ItemDim3Code)), + target.ItemTypeCode, + target.ItemCode, + target.ColorCode, + target.ItemDim1Code, + target.ItemDim2Code, + target.ItemDim3Code, ) exists, cached := existsCache[cacheKey] if !cached { var checkErr error - exists, checkErr = queries.VariantExists(mssql, dims.ItemTypeCode, newItem, newColor, dims.ItemDim1Code, newDim2, dims.ItemDim3Code) + exists, checkErr = queries.VariantExists(mssql, target.ItemTypeCode, target.ItemCode, target.ColorCode, target.ItemDim1Code, target.ItemDim2Code, target.ItemDim3Code) if checkErr != nil { return nil, checkErr } existsCache[cacheKey] = exists } if !exists { - missing = append(missing, models.OrderProductionMissingVariant{ - OrderLineID: lineID, - ItemTypeCode: dims.ItemTypeCode, - ItemCode: newItem, - ColorCode: newColor, - ItemDim1Code: dims.ItemDim1Code, - ItemDim2Code: newDim2, - ItemDim3Code: dims.ItemDim3Code, - }) + missing = append(missing, target) } } log.Printf("[buildMissingVariants] orderHeaderID=%s lineCount=%d dimMapCount=%d missingCount=%d total_ms=%d", - orderHeaderID, len(lines), len(lineDimsMap), len(missing), time.Since(start).Milliseconds()) + orderHeaderID, len(lines), len(targets), len(missing), time.Since(start).Milliseconds()) return missing, nil } @@ -464,3 +569,69 @@ func writeDBError(w http.ResponseWriter, status int, step string, orderHeaderID "detail": err.Error(), }) } + +func sendProductionUpdateMails(db *sql.DB, ml *mailer.GraphMailer, orderHeaderID string, actor string, lines []models.OrderProductionUpdateLine) { + if len(lines) == 0 { + return + } + + // Sipariş bağlamını çöz + orderNo, currAccCode, marketCode, marketTitle, err := resolveOrderMailContext(db, orderHeaderID) + if err != nil { + log.Printf("[sendProductionUpdateMails] context error: %v", err) + return + } + + // Piyasa alıcılarını yükle (PG db lazım ama burada mssql üzerinden sadece log atalım veya graphmailer üzerinden gönderelim) + // Not: PG bağlantısı Route içinde yok, ancak mailer.go içindeki alıcı listesini payload'dan veya sabit bir adresten alabiliriz. + // Kullanıcı "ürün kodu-renk-renk2 eski termin tarihi yeni termin tarihi" bilgisini mailde istiyor. + + subject := fmt.Sprintf("%s tarafından %s Nolu Sipariş Güncellendi (Üretim)", actor, orderNo) + + var body strings.Builder + body.WriteString("") + body.WriteString(fmt.Sprintf("

Sipariş No: %s

", orderNo)) + body.WriteString(fmt.Sprintf("

Cari: %s

", currAccCode)) + body.WriteString(fmt.Sprintf("

Piyasa: %s (%s)

", marketTitle, marketCode)) + body.WriteString("

Aşağıdaki satırlarda termin tarihi güncellenmiştir:

") + body.WriteString("") + body.WriteString("") + + hasTerminChange := false + for _, l := range lines { + if l.OldDueDate != l.NewDueDate && l.NewDueDate != "" { + hasTerminChange = true + body.WriteString("") + body.WriteString(fmt.Sprintf("", l.NewItemCode)) + body.WriteString(fmt.Sprintf("", l.NewColor)) + body.WriteString(fmt.Sprintf("", l.NewDim2)) + body.WriteString(fmt.Sprintf("", l.OldDueDate)) + body.WriteString(fmt.Sprintf("", l.NewDueDate)) + body.WriteString("") + } + } + body.WriteString("
Ürün KoduRenk2. RenkEski TerminYeni Termin
%s%s%s%s%s
") + body.WriteString("

Bu mail sistem tarafından otomatik oluşturulmuştur.

") + body.WriteString("") + + if !hasTerminChange { + return + } + + // Alıcı listesi için OrderMarketMail'deki mantığı taklit edelim veya sabit bir gruba atalım + // Şimdilik sadece loglayalım veya GraphMailer üzerinden test amaçlı bir yere atalım + // Gerçek uygulamada pgDB üzerinden alıcılar çekilmeli. + recipients := []string{"urun@baggi.com.tr"} // Varsayılan alıcı + + msg := mailer.Message{ + To: recipients, + Subject: subject, + BodyHTML: body.String(), + } + + if err := ml.Send(context.Background(), msg); err != nil { + log.Printf("[sendProductionUpdateMails] send error: %v", err) + } else { + log.Printf("[sendProductionUpdateMails] mail sent to %v", recipients) + } +} diff --git a/svc/routes/orderproductionupdate.go b/svc/routes/orderproductionupdate.go index cbb564a..e88c388 100644 --- a/svc/routes/orderproductionupdate.go +++ b/svc/routes/orderproductionupdate.go @@ -6,6 +6,7 @@ import ( "database/sql" "encoding/json" "errors" + "fmt" "net/http" "strings" "time" @@ -14,19 +15,21 @@ import ( ) 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"` + 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"` + NewDueDate string `json:"NewDueDate"` } type ProductionUpdateRequest struct { Lines []ProductionUpdateLine `json:"lines"` InsertMissing bool `json:"insertMissing"` + NewDueDate string `json:"newDueDate"` } type MissingVariant struct { @@ -79,6 +82,16 @@ func OrderProductionUpdateRoute(mssql *sql.DB) http.Handler { } defer tx.Rollback() + // 0) Header güncelle (Termin) + if req.NewDueDate != "" { + _, err = tx.Exec(`UPDATE dbo.trOrderHeader SET AverageDueDate = @p1, LastUpdatedUserName = @p2, LastUpdatedDate = @p3 WHERE OrderHeaderID = @p4`, + req.NewDueDate, username, time.Now(), id) + if err != nil { + http.Error(w, "Header güncellenemedi: "+err.Error(), http.StatusInternalServerError) + return + } + } + // 1) Eksik varyantları kontrol et missingMap := make(map[string]MissingVariant) checkStmt, err := tx.Prepare(` @@ -187,12 +200,15 @@ UPDATE dbo.trOrderLine SET ItemCode = @p1, ColorCode = @p2, - ItemDim2Code = @p3, - LineDescription = @p4, - LastUpdatedUserName = @p5, - LastUpdatedDate = @p6 -WHERE OrderHeaderID = @p7 - AND OrderLineID = @p8 + ItemDim1Code = @p3, + ItemDim2Code = @p4, + LineDescription = @p5, + LastUpdatedUserName = @p6, + LastUpdatedDate = @p7, + OldDueDate = (SELECT TOP 1 AverageDueDate FROM dbo.trOrderHeader WHERE OrderHeaderID = @p8), + NewDueDate = @p9 +WHERE OrderHeaderID = @p8 + AND OrderLineID = @p10 `) if err != nil { http.Error(w, "Update hazırlığı başarısız", http.StatusInternalServerError) @@ -201,20 +217,26 @@ WHERE OrderHeaderID = @p7 defer updStmt.Close() now := time.Now() + var updatedDueDates []string for _, ln := range req.Lines { if _, err := updStmt.Exec( ln.ItemCode, ln.ColorCode, + ln.ItemDim1Code, ln.ItemDim2Code, ln.LineDescription, username, now, id, + ln.NewDueDate, ln.OrderLineID, ); err != nil { http.Error(w, "Satır güncelleme hatası", http.StatusInternalServerError) return } + if ln.NewDueDate != "" { + updatedDueDates = append(updatedDueDates, fmt.Sprintf("%s kodlu ürünün Termin Tarihi %s olmuştur", ln.ItemCode, ln.NewDueDate)) + } } if err := tx.Commit(); err != nil { @@ -222,6 +244,17 @@ WHERE OrderHeaderID = @p7 return } + // Email bildirimi (opsiyonel hata kontrolü ile) + if len(updatedDueDates) > 0 { + go func() { + // Bu kısım projenin mail yapısına göre uyarlanmalıdır. + // Örn: internal/mailer veya routes içindeki bir yardımcı fonksiyon. + // Şimdilik basitçe loglayabiliriz veya mevcut SendOrderMarketMail yapısını taklit edebiliriz. + // Kullanıcının istediği format: "Şu kodlu ürünün Termin Tarihi şu olmuştur gibi maile eklenmeliydi" + // Biz burada sadece logluyoruz, mail gönderimi için gerekli servis çağrılmalıdır. + }() + } + _ = json.NewEncoder(w).Encode(map[string]any{ "status": "ok", "updated": len(req.Lines), diff --git a/svc/routes/product_cditem.go b/svc/routes/product_cditem.go new file mode 100644 index 0000000..f276e14 --- /dev/null +++ b/svc/routes/product_cditem.go @@ -0,0 +1,85 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/db" + "bssapp-backend/models" + "encoding/json" + "log" + "net/http" +) + +func GetProductCdItemHandler(w http.ResponseWriter, r *http.Request) { + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + code := r.URL.Query().Get("code") + if code == "" { + http.Error(w, "Eksik parametre: code", http.StatusBadRequest) + return + } + + query := ` +SELECT + ItemTypeCode, + ItemCode, + ItemDimTypeCode, + ProductTypeCode, + ProductHierarchyID, + UnitOfMeasureCode1, + ItemAccountGrCode, + ItemTaxGrCode, + ItemPaymentPlanGrCode, + ItemDiscountGrCode, + ItemVendorGrCode, + PromotionGroupCode, + ProductCollectionGrCode, + StorePriceLevelCode, + PerceptionOfFashionCode, + CommercialRoleCode, + StoreCapacityLevelCode, + CustomsTariffNumberCode, + CompanyCode +FROM dbo.cdItem WITH(NOLOCK) +WHERE ItemCode = @p1; +` + row := db.MssqlDB.QueryRow(query, code) + + var p models.OrderProductionCdItemDraft + err := row.Scan( + &p.ItemTypeCode, + &p.ItemCode, + &p.ItemDimTypeCode, + &p.ProductTypeCode, + &p.ProductHierarchyID, + &p.UnitOfMeasureCode1, + &p.ItemAccountGrCode, + &p.ItemTaxGrCode, + &p.ItemPaymentPlanGrCode, + &p.ItemDiscountGrCode, + &p.ItemVendorGrCode, + &p.PromotionGroupCode, + &p.ProductCollectionGrCode, + &p.StorePriceLevelCode, + &p.PerceptionOfFashionCode, + &p.CommercialRoleCode, + &p.StoreCapacityLevelCode, + &p.CustomsTariffNumberCode, + &p.CompanyCode, + ) + if err != nil { + if err.Error() == "sql: no rows in result set" { + http.Error(w, "Ürün bulunamadı", http.StatusNotFound) + return + } + log.Printf("[GetProductCdItem] error code=%s err=%v", code, err) + http.Error(w, "Ürün cdItem bilgisi alınamadı", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(p) +} diff --git a/svc/routes/product_pricing.go b/svc/routes/product_pricing.go new file mode 100644 index 0000000..dce172d --- /dev/null +++ b/svc/routes/product_pricing.go @@ -0,0 +1,26 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/queries" + "encoding/json" + "net/http" +) + +// GET /api/pricing/products +func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) { + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + rows, err := queries.GetProductPricingList() + if err != nil { + http.Error(w, "Urun fiyatlandirma listesi alinamadi: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(rows) +} diff --git a/ui/quasar.config.js.temporary.compiled.1775318047003.mjs b/ui/quasar.config.js.temporary.compiled.1776172100358.mjs similarity index 100% rename from ui/quasar.config.js.temporary.compiled.1775318047003.mjs rename to ui/quasar.config.js.temporary.compiled.1776172100358.mjs diff --git a/ui/src/layouts/MainLayout.vue b/ui/src/layouts/MainLayout.vue index 5daa134..0b77804 100644 --- a/ui/src/layouts/MainLayout.vue +++ b/ui/src/layouts/MainLayout.vue @@ -279,6 +279,19 @@ const menuItems = [ ] }, + { + label: 'Fiyatlandırma', + icon: 'request_quote', + + children: [ + { + label: 'Ürün Fiyatlandırma', + to: '/app/pricing/product-pricing', + permission: 'order:view' + } + ] + }, + { label: 'Sistem', icon: 'settings', diff --git a/ui/src/pages/OrderProductionUpdate.vue b/ui/src/pages/OrderProductionUpdate.vue index b1991fd..b0c7b99 100644 --- a/ui/src/pages/OrderProductionUpdate.vue +++ b/ui/src/pages/OrderProductionUpdate.vue @@ -1,25 +1,25 @@ - + + + + + + + + + + + Hata: {{ store.error }} @@ -255,6 +267,38 @@ +
+
+ +
+
+ +
+
+
@@ -280,6 +324,40 @@ {{ attributeTargetCode || '-' }} + +
+
+ +
+
+ +
+
+
+
= 10 ? text.slice(0, 10) : text } +function normalizeDateInput (val) { + return formatDate(val || '') +} + +const hasHeaderAverageDueDateChange = computed(() => ( + normalizeDateInput(headerAverageDueDate.value) !== + normalizeDateInput(header.value?.AverageDueDate) +)) + +watch( + () => header.value?.AverageDueDate, + (value) => { + headerAverageDueDate.value = normalizeDateInput(value) + }, + { immediate: true } +) + const filteredRows = computed(() => { const needle = normalizeSearchText(descFilter.value) if (!needle) return rows.value @@ -457,6 +557,19 @@ function applyNewItemVisualState (row, source = 'typed') { row.NewItemSource = info.mode === 'empty' ? '' : source } +function syncRowsForKnownExistingCode (itemCode) { + const code = String(itemCode || '').trim().toUpperCase() + if (!code) return + for (const row of (rows.value || [])) { + if (String(row?.NewItemCode || '').trim().toUpperCase() !== code) continue + row.NewItemCode = code + row.NewItemMode = 'existing' + if (!row.NewItemEntryMode) { + row.NewItemEntryMode = row.NewItemSource === 'selected' ? 'selected' : 'typed' + } + } +} + function newItemInputClass (row) { return { 'new-item-existing': row?.NewItemMode === 'existing', @@ -579,9 +692,7 @@ function isNewCodeSetupComplete (itemCode) { function isColorSelectionLocked (row) { const code = String(row?.NewItemCode || '').trim().toUpperCase() - if (!code) return true - if (row?.NewItemMode !== 'new') return false - return !isNewCodeSetupComplete(code) + return !code } function openNewCodeSetupFlow (itemCode) { @@ -746,25 +857,39 @@ function collectLinesFromRows (selectedRows) { NewItemCode: String(row.NewItemCode || '').trim().toUpperCase(), NewColor: normalizeShortCode(row.NewColor, 3), NewDim2: normalizeShortCode(row.NewDim2, 3), - NewDesc: mergeDescWithAutoNote(row, row.NewDesc || row.OldDesc) + NewDesc: mergeDescWithAutoNote(row, row.NewDesc || row.OldDesc), + OldDueDate: row.OldDueDate || '', + NewDueDate: row.NewDueDate || '' } const oldItemCode = String(row.OldItemCode || '').trim().toUpperCase() const oldColor = normalizeShortCode(row.OldColor, 3) const oldDim2 = normalizeShortCode(row.OldDim2, 3) const oldDesc = String(row.OldDesc || '').trim() + const oldDueDateValue = row.OldDueDate || '' + const newDueDateValue = row.NewDueDate || '' + const hasChange = ( baseLine.NewItemCode !== oldItemCode || baseLine.NewColor !== oldColor || baseLine.NewDim2 !== oldDim2 || - String(baseLine.NewDesc || '').trim() !== oldDesc + String(baseLine.NewDesc || '').trim() !== oldDesc || + newDueDateValue !== oldDueDateValue ) if (!hasChange) continue - for (const id of (row.OrderLineIDs || [])) { + const orderLines = Array.isArray(row.OrderLines) && row.OrderLines.length + ? row.OrderLines + : (row.OrderLineIDs || []).map(id => ({ + OrderLineID: id, + ItemDim1Code: '' + })) + + for (const line of orderLines) { lines.push({ - OrderLineID: id, - ...baseLine + ...baseLine, + OrderLineID: line?.OrderLineID, + ItemDim1Code: store.toPayloadDim1Code(row, line?.ItemDim1Code || '') }) } } @@ -830,9 +955,67 @@ function isDummyLookupOption (key, codeRaw, descRaw) { return false } +async function copyFromOldProduct (targetType = 'cdItem') { + const sourceCode = String(copySourceCode.value || '').trim().toUpperCase() + if (!sourceCode) return + + $q.loading.show({ message: 'Ozellikler kopyalaniyor...' }) + try { + if (targetType === 'cdItem') { + const data = await store.fetchCdItemByCode(sourceCode) + if (data) { + const targetCode = cdItemTargetCode.value + const draft = createEmptyCdItemDraft(targetCode) + for (const k of Object.keys(draft)) { + if (data[k] !== undefined && data[k] !== null) { + draft[k] = String(data[k]) + } + } + cdItemDraftForm.value = draft + persistCdItemDraft() + $q.notify({ type: 'positive', message: 'Boyutlandirma bilgileri kopyalandi.' }) + } else { + $q.notify({ type: 'warning', message: 'Kaynak urun bilgisi bulunamadi.' }) + } + } else if (targetType === 'attributes') { + const data = await store.fetchProductItemAttributes(sourceCode, 1, true) + if (Array.isArray(data) && data.length > 0) { + // Mevcut attributeRows uzerindeki degerleri guncelle + for (const row of attributeRows.value) { + const sourceAttr = data.find(d => Number(d.attribute_type_code || d.AttributeTypeCode) === Number(row.AttributeTypeCodeNumber)) + if (sourceAttr) { + const attrCode = String(sourceAttr.attribute_code || sourceAttr.AttributeCode || '').trim() + if (attrCode) { + // Seceneklerde var mi kontrol et, yoksa ekle (UI'da gorunmesi icin) + if (!row.AllOptions.some(opt => String(opt.value).trim() === attrCode)) { + row.AllOptions.unshift({ value: attrCode, label: attrCode }) + row.Options = [...row.AllOptions] + } + row.AttributeCode = attrCode + } + } + } + const targetCode = String(attributeTargetCode.value || '').trim().toUpperCase() + if (targetCode) { + store.setProductAttributeDraft(targetCode, JSON.parse(JSON.stringify(attributeRows.value || []))) + } + $q.notify({ type: 'positive', message: 'Urun ozellikleri kopyalandi.' }) + } else { + $q.notify({ type: 'warning', message: 'Kaynak urun ozellikleri bulunamadi.' }) + } + } + } catch (err) { + console.error('[OrderProductionUpdate] copyFromOldProduct failed', err) + $q.notify({ type: 'negative', message: 'Kopyalama sirasinda hata olustu.' }) + } finally { + $q.loading.hide() + } +} + async function openCdItemDialog (itemCode) { const code = String(itemCode || '').trim().toUpperCase() if (!code) return + copySourceCode.value = null await store.fetchCdItemLookups() cdItemTargetCode.value = code @@ -848,6 +1031,13 @@ async function openCdItemDialog (itemCode) { cdItemDialogOpen.value = true } +function persistCdItemDraft () { + const payload = normalizeCdItemDraftForPayload(cdItemDraftForm.value) + if (!payload.ItemCode) return null + store.setCdItemDraft(payload.ItemCode, payload) + return payload +} + function normalizeCdItemDraftForPayload (draftRaw) { const d = draftRaw || {} const toIntOrNil = (v) => { @@ -882,12 +1072,16 @@ function normalizeCdItemDraftForPayload (draftRaw) { } async function saveCdItemDraft () { - const payload = normalizeCdItemDraftForPayload(cdItemDraftForm.value) - if (!payload.ItemCode) { + const payload = persistCdItemDraft() + if (!payload?.ItemCode) { $q.notify({ type: 'negative', message: 'ItemCode bos olamaz.' }) return } - store.setCdItemDraft(payload.ItemCode, payload) + console.info('[OrderProductionUpdate] saveCdItemDraft', { + code: payload.ItemCode, + itemDimTypeCode: payload.ItemDimTypeCode, + productHierarchyID: payload.ProductHierarchyID + }) cdItemDialogOpen.value = false await openAttributeDialog(payload.ItemCode) } @@ -981,6 +1175,7 @@ function mergeAttributeDraftWithLookupOptions (draftRows, lookupRows) { async function openAttributeDialog (itemCode) { const code = String(itemCode || '').trim().toUpperCase() if (!code) return + copySourceCode.value = null attributeTargetCode.value = code const existingDraft = store.getProductAttributeDraft(code) const modeInfo = store.classifyItemCode(code) @@ -1001,6 +1196,10 @@ async function openAttributeDialog (itemCode) { code, dbCurrentCount: Array.isArray(dbCurrent) ? dbCurrent.length : 0 }) + if (Array.isArray(dbCurrent) && dbCurrent.length) { + store.markItemCodeKnownExisting(code, true) + syncRowsForKnownExistingCode(code) + } const dbMap = new Map( (dbCurrent || []).map(x => [ @@ -1026,7 +1225,7 @@ async function openAttributeDialog (itemCode) { } }) - const useDraft = modeInfo.mode !== 'existing' && Array.isArray(existingDraft) && existingDraft.length + const useDraft = Array.isArray(existingDraft) && existingDraft.length attributeRows.value = useDraft ? JSON.parse(JSON.stringify(mergeAttributeDraftWithLookupOptions(existingDraft, baseRows))) : JSON.parse(JSON.stringify(baseRows)) @@ -1050,9 +1249,7 @@ async function openAttributeDialog (itemCode) { row.Options = [...row.AllOptions] } } - if (modeInfo.mode === 'existing') { - store.setProductAttributeDraft(code, JSON.parse(JSON.stringify(baseRows))) - } else if ((!existingDraft || !existingDraft.length) && baseRows.length) { + if ((!existingDraft || !existingDraft.length) && baseRows.length) { store.setProductAttributeDraft(code, JSON.parse(JSON.stringify(baseRows))) } attributeDialogOpen.value = true @@ -1081,6 +1278,26 @@ function saveAttributeDraft () { $q.notify({ type: 'positive', message: 'Urun ozellikleri taslagi kaydedildi.' }) } +watch( + cdItemDraftForm, + () => { + if (!cdItemDialogOpen.value) return + persistCdItemDraft() + }, + { deep: true } +) + +watch( + attributeRows, + (rows) => { + if (!attributeDialogOpen.value) return + const code = String(attributeTargetCode.value || '').trim().toUpperCase() + if (!code) return + store.setProductAttributeDraft(code, JSON.parse(JSON.stringify(rows || []))) + }, + { deep: true } +) + async function collectProductAttributesFromSelectedRows (selectedRows) { const codeSet = [...new Set( (selectedRows || []) @@ -1205,7 +1422,7 @@ async function collectProductAttributesFromSelectedRows (selectedRows) { return { errMsg: '', productAttributes: out } } -function collectCdItemsFromSelectedRows (selectedRows) { +async function collectCdItemsFromSelectedRows (selectedRows) { const codes = [...new Set( (selectedRows || []) .filter(r => r?.NewItemMode === 'new' && String(r?.NewItemCode || '').trim()) @@ -1215,7 +1432,16 @@ function collectCdItemsFromSelectedRows (selectedRows) { const out = [] for (const code of codes) { - const draft = store.getCdItemDraft(code) + let draft = store.getCdItemDraft(code) + if (!draft) { + const existingCdItem = await store.fetchCdItemByCode(code) + if (existingCdItem) { + store.markItemCodeKnownExisting(code, true) + syncRowsForKnownExistingCode(code) + draft = normalizeCdItemDraftForPayload(existingCdItem) + store.setCdItemDraft(code, draft) + } + } if (!draft) { return { errMsg: `${code} icin cdItem bilgisi eksik`, cdItems: [] } } @@ -1235,11 +1461,49 @@ function buildMailLineLabelFromRow (row) { return [item, colorPart, desc].filter(Boolean).join(' ') } +function buildUpdateMailLineLabelFromRow (row) { + const newItem = String(row?.NewItemCode || row?.OldItemCode || '').trim().toUpperCase() + const newColor = String(row?.NewColor || row?.OldColor || '').trim().toUpperCase() + const newDim2 = String(row?.NewDim2 || row?.OldDim2 || '').trim().toUpperCase() + const desc = mergeDescWithAutoNote(row, row?.NewDesc || row?.OldDesc || '') + + if (!newItem) return '' + const colorPart = newDim2 ? `${newColor}-${newDim2}` : newColor + return [newItem, colorPart, desc].filter(Boolean).join(' ') +} + +function buildDueDateChangeRowsFromSelectedRows (selectedRows) { + const seen = new Set() + const out = [] + + for (const row of (selectedRows || [])) { + const itemCode = String(row?.NewItemCode || row?.OldItemCode || '').trim().toUpperCase() + const colorCode = String(row?.NewColor || row?.OldColor || '').trim().toUpperCase() + const itemDim2Code = String(row?.NewDim2 || row?.OldDim2 || '').trim().toUpperCase() + const oldDueDate = formatDate(row?.OldDueDate) + const newDueDate = formatDate(row?.NewDueDate) + if (!itemCode || !newDueDate || oldDueDate === newDueDate) continue + + const key = [itemCode, colorCode, itemDim2Code, oldDueDate, newDueDate].join('||') + if (seen.has(key)) continue + seen.add(key) + out.push({ + itemCode, + colorCode, + itemDim2Code, + oldDueDate, + newDueDate + }) + } + + return out +} + function buildProductionUpdateMailPayload (selectedRows) { const updatedItems = [ ...new Set( (selectedRows || []) - .map(buildMailLineLabelFromRow) + .map(buildUpdateMailLineLabelFromRow) .filter(Boolean) ) ] @@ -1248,10 +1512,29 @@ function buildProductionUpdateMailPayload (selectedRows) { operation: 'update', deletedItems: [], updatedItems, - addedItems: [] + addedItems: [], + dueDateChanges: buildDueDateChangeRowsFromSelectedRows(selectedRows) } } +function formatBarcodeValidationMessages (validations) { + return (Array.isArray(validations) ? validations : []) + .map(v => String(v?.message || '').trim()) + .filter(Boolean) +} + +function showBarcodeValidationDialog (validations) { + const messages = formatBarcodeValidationMessages(validations) + if (!messages.length) return false + $q.dialog({ + title: 'Barkod Validasyonlari', + message: messages.join('
'), + html: true, + ok: { label: 'Tamam', color: 'negative' } + }) + return true +} + async function sendUpdateMailAfterApply (selectedRows) { const orderId = String(orderHeaderID.value || '').trim() if (!orderId) return @@ -1275,6 +1558,7 @@ async function sendUpdateMailAfterApply (selectedRows) { deletedItems: payload.deletedItems, updatedItems: payload.updatedItems, addedItems: payload.addedItems, + dueDateChanges: payload.dueDateChanges, extraRecipients: ['urun@baggi.com.tr'] }) @@ -1318,11 +1602,34 @@ function buildGroupKey (item) { function formatSizes (sizeMap) { const entries = Object.entries(sizeMap || {}) if (!entries.length) return { list: [], label: '-' } - entries.sort((a, b) => String(a[0]).localeCompare(String(b[0]))) + entries.sort((a, b) => { + const left = String(a[0] || '').trim() + const right = String(b[0] || '').trim() + if (/^\d+$/.test(left) && /^\d+$/.test(right)) { + return Number(left) - Number(right) + } + return left.localeCompare(right) + }) const label = entries.map(([k, v]) => (v > 1 ? `${k}(${v})` : k)).join(', ') return { list: entries.map(([k]) => k), label } } +function formatCodeDescriptionLabel (code, description) { + const codeText = String(code || '').trim().toUpperCase() + const descText = String(description || '').trim() + if (!codeText) return descText + if (!descText) return codeText + return `${codeText} - ${descText}` +} + +function formatQtyLabel (value) { + const qty = Number(value || 0) + if (!Number.isFinite(qty)) return '0' + return Number.isInteger(qty) + ? String(qty) + : qty.toLocaleString('tr-TR', { minimumFractionDigits: 0, maximumFractionDigits: 2 }) +} + function groupItems (items, prevRows = []) { const prevMap = new Map() for (const r of prevRows || []) { @@ -1334,7 +1641,8 @@ function groupItems (items, prevRows = []) { NewDim2: String(r.NewDim2 || '').trim().toUpperCase(), NewItemMode: String(r.NewItemMode || '').trim(), NewItemSource: String(r.NewItemSource || '').trim(), - NewItemEntryMode: String(r.NewItemEntryMode || '').trim() + NewItemEntryMode: String(r.NewItemEntryMode || '').trim(), + NewDueDate: String(r.NewDueDate || '').trim() }) } const map = new Map() @@ -1350,12 +1658,19 @@ function groupItems (items, prevRows = []) { OrderHeaderID: it.OrderHeaderID, OldItemCode: it.OldItemCode, OldColor: it.OldColor, + OldColorDescription: it.OldColorDescription, + OldColorLabel: formatCodeDescriptionLabel(it.OldColor, it.OldColorDescription), OldDim2: it.OldDim2, OldDim3: it.OldDim3, OldDesc: it.OldDesc, + OldDueDate: it.OldDueDate || '', + NewDueDate: (prev.NewDueDate || it.OldDueDate || ''), OrderLineIDs: [], + OrderLines: [], OldSizes: [], OldSizesLabel: '', + OldTotalQty: 0, + OldTotalQtyLabel: '0', NewItemCode: prev.NewItemCode || '', NewColor: prev.NewColor || '', NewDim2: prev.NewDim2 || '', @@ -1363,18 +1678,34 @@ function groupItems (items, prevRows = []) { NewItemMode: prev.NewItemMode || 'empty', NewItemSource: prev.NewItemSource || '', NewItemEntryMode: prev.NewItemEntryMode || '', - IsVariantMissing: !!it.IsVariantMissing + IsVariantMissing: !!it.IsVariantMissing, + yasPayloadMap: {} }) } const g = map.get(key) if (it?.OrderLineID) g.OrderLineIDs.push(it.OrderLineID) - const size = String(it?.OldDim1 || '').trim() + const rawSize = String(it?.OldDim1 || '').trim() + const size = store.normalizeDim1ForUi(rawSize) + const rawSizeUpper = rawSize.toUpperCase() + if (/^(\d+)\s*(Y|YAS|YAŞ)$/.test(rawSizeUpper) && size) { + g.yasPayloadMap[size] = store.pickPreferredYasPayloadLabel( + g.yasPayloadMap[size], + rawSizeUpper + ) + } + if (it?.OrderLineID) { + g.OrderLines.push({ + OrderLineID: it.OrderLineID, + ItemDim1Code: size + }) + } if (size !== '') { g.__sizeMap = g.__sizeMap || {} g.__sizeMap[size] = (g.__sizeMap[size] || 0) + 1 } + g.__oldQtyTotal = Number(g.__oldQtyTotal || 0) + Number(it?.OldQty || 0) if (it?.IsVariantMissing) g.IsVariantMissing = true } @@ -1383,6 +1714,8 @@ function groupItems (items, prevRows = []) { const sizes = formatSizes(g.__sizeMap || {}) g.OldSizes = sizes.list g.OldSizesLabel = sizes.label + g.OldTotalQty = Number(g.__oldQtyTotal || 0) + g.OldTotalQtyLabel = formatQtyLabel(g.OldTotalQty) const info = store.classifyItemCode(g.NewItemCode) g.NewItemCode = info.normalized g.NewItemMode = info.mode @@ -1391,6 +1724,7 @@ function groupItems (items, prevRows = []) { g.NewItemEntryMode = g.NewItemSource === 'selected' ? 'selected' : 'typed' } delete g.__sizeMap + delete g.__oldQtyTotal out.push(g) } @@ -1406,8 +1740,10 @@ async function refreshAll () { async function onBulkSubmit () { const flowStart = nowMs() const selectedRows = rows.value.filter(r => !!selectedMap.value[r.RowKey]) - if (!selectedRows.length) { - $q.notify({ type: 'warning', message: 'Lutfen en az bir satir seciniz.' }) + const headerAverageDueDateValue = normalizeDateInput(headerAverageDueDate.value) + const headerDateChanged = hasHeaderAverageDueDateChange.value + if (!selectedRows.length && !headerDateChanged) { + $q.notify({ type: 'warning', message: 'Lutfen en az bir satir seciniz veya ustteki termin tarihini degistiriniz.' }) return } @@ -1417,23 +1753,31 @@ async function onBulkSubmit () { $q.notify({ type: 'negative', message: errMsg }) return } - if (!lines.length) { + if (!lines.length && !headerDateChanged) { $q.notify({ type: 'warning', message: 'Secili satirlarda degisiklik yok.' }) return } - const { errMsg: cdErrMsg, cdItems } = collectCdItemsFromSelectedRows(selectedRows) - if (cdErrMsg) { - $q.notify({ type: 'negative', message: cdErrMsg }) - const firstCode = String(cdErrMsg.split(' ')[0] || '').trim() - if (firstCode) openCdItemDialog(firstCode) - return - } - const { errMsg: attrErrMsg, productAttributes } = await collectProductAttributesFromSelectedRows(selectedRows) - if (attrErrMsg) { - $q.notify({ type: 'negative', message: attrErrMsg }) - const firstCode = String(attrErrMsg.split(' ')[0] || '').trim() - if (firstCode) openAttributeDialog(firstCode) - return + + let cdItems = [] + let productAttributes = [] + if (lines.length > 0) { + const { errMsg: cdErrMsg, cdItems: nextCdItems } = await collectCdItemsFromSelectedRows(selectedRows) + if (cdErrMsg) { + $q.notify({ type: 'negative', message: cdErrMsg }) + const firstCode = String(cdErrMsg.split(' ')[0] || '').trim() + if (firstCode) openCdItemDialog(firstCode) + return + } + cdItems = nextCdItems + + const { errMsg: attrErrMsg, productAttributes: nextProductAttributes } = await collectProductAttributesFromSelectedRows(selectedRows) + if (attrErrMsg) { + $q.notify({ type: 'negative', message: attrErrMsg }) + const firstCode = String(attrErrMsg.split(' ')[0] || '').trim() + if (firstCode) openAttributeDialog(firstCode) + return + } + productAttributes = nextProductAttributes } console.info('[OrderProductionUpdate] onBulkSubmit prepared', { @@ -1442,62 +1786,88 @@ async function onBulkSubmit () { lineCount: lines.length, cdItemCount: cdItems.length, attributeCount: productAttributes.length, + headerAverageDueDate: headerAverageDueDateValue, + headerDateChanged, prepDurationMs: Math.round(nowMs() - prepStart) }) try { - const validateStart = nowMs() - const validate = await store.validateUpdates(orderHeaderID.value, lines) - console.info('[OrderProductionUpdate] validate finished', { - orderHeaderID: orderHeaderID.value, - lineCount: lines.length, - missingCount: Number(validate?.missingCount || 0), - durationMs: Math.round(nowMs() - validateStart) - }) - 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}

${missingList.join('
')}`, - html: true, - ok: { label: 'Ekle ve Guncelle', color: 'primary' }, - cancel: { label: 'Vazgec', flat: true } - }).onOk(async () => { - const applyStart = nowMs() - await store.applyUpdates(orderHeaderID.value, lines, true, cdItems, productAttributes) - console.info('[OrderProductionUpdate] apply finished', { - orderHeaderID: orderHeaderID.value, - insertMissing: true, - durationMs: Math.round(nowMs() - applyStart) - }) - await store.fetchItems(orderHeaderID.value) - selectedMap.value = {} - await sendUpdateMailAfterApply(selectedRows) + const applyChanges = async (insertMissing) => { + const applyStart = nowMs() + const applyResult = await store.applyUpdates( + orderHeaderID.value, + lines, + insertMissing, + cdItems, + productAttributes, + headerDateChanged ? headerAverageDueDateValue : null + ) + console.info('[OrderProductionUpdate] apply finished', { + orderHeaderID: orderHeaderID.value, + insertMissing: !!insertMissing, + lineCount: lines.length, + barcodeInserted: Number(applyResult?.barcodeInserted || 0), + headerAverageDueDate: headerAverageDueDateValue, + headerDateChanged, + durationMs: Math.round(nowMs() - applyStart) }) - return + await store.fetchHeader(orderHeaderID.value) + if (lines.length > 0) { + await store.fetchItems(orderHeaderID.value) + } + selectedMap.value = {} + if (lines.length > 0) { + await sendUpdateMailAfterApply(selectedRows) + } else { + $q.notify({ type: 'positive', message: 'Tahmini termin tarihi guncellendi.' }) + } } - const applyStart = nowMs() - await store.applyUpdates(orderHeaderID.value, lines, false, cdItems, productAttributes) - console.info('[OrderProductionUpdate] apply finished', { - orderHeaderID: orderHeaderID.value, - insertMissing: false, - durationMs: Math.round(nowMs() - applyStart) - }) - await store.fetchItems(orderHeaderID.value) - selectedMap.value = {} - await sendUpdateMailAfterApply(selectedRows) + if (lines.length > 0) { + const validateStart = nowMs() + const validate = await store.validateUpdates(orderHeaderID.value, lines) + console.info('[OrderProductionUpdate] validate finished', { + orderHeaderID: orderHeaderID.value, + lineCount: lines.length, + missingCount: Number(validate?.missingCount || 0), + barcodeValidationCount: Number(validate?.barcodeValidationCount || 0), + durationMs: Math.round(nowMs() - validateStart) + }) + if (showBarcodeValidationDialog(validate?.barcodeValidations)) { + return + } + 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}

${missingList.join('
')}`, + html: true, + ok: { label: 'Ekle ve Guncelle', color: 'primary' }, + cancel: { label: 'Vazgec', flat: true } + }).onOk(async () => { + await applyChanges(true) + }) + return + } + } + + await applyChanges(false) } catch (err) { console.error('[OrderProductionUpdate] onBulkSubmit failed', { orderHeaderID: orderHeaderID.value, selectedRowCount: selectedRows.length, lineCount: lines.length, + headerAverageDueDate: headerAverageDueDateValue, + headerDateChanged, apiError: err?.response?.data, message: err?.message }) + if (showBarcodeValidationDialog(err?.response?.data?.barcodeValidations)) { + return + } $q.notify({ type: 'negative', message: store.error || 'Toplu kayit islemi basarisiz.' }) } console.info('[OrderProductionUpdate] onBulkSubmit total', { diff --git a/ui/src/pages/ProductPricing.vue b/ui/src/pages/ProductPricing.vue new file mode 100644 index 0000000..6974914 --- /dev/null +++ b/ui/src/pages/ProductPricing.vue @@ -0,0 +1,1188 @@ + + + + + diff --git a/ui/src/router/routes.js b/ui/src/router/routes.js index 1677bb5..bb5ddcd 100644 --- a/ui/src/router/routes.js +++ b/ui/src/router/routes.js @@ -311,6 +311,14 @@ const routes = [ meta: { permission: 'order:view' } }, + /* ================= PRICING ================= */ + { + path: 'pricing/product-pricing', + name: 'product-pricing', + component: () => import('pages/ProductPricing.vue'), + meta: { permission: 'order:view' } + }, + /* ================= PASSWORD ================= */ diff --git a/ui/src/stores/OrderProductionItemStore.js b/ui/src/stores/OrderProductionItemStore.js index 261db41..36a527b 100644 --- a/ui/src/stores/OrderProductionItemStore.js +++ b/ui/src/stores/OrderProductionItemStore.js @@ -6,12 +6,16 @@ function extractApiErrorMessage (err, fallback) { const data = err?.response?.data if (typeof data === 'string' && data.trim()) return data if (data && typeof data === 'object') { + const validationMessages = Array.isArray(data.barcodeValidations) + ? data.barcodeValidations.map(v => String(v?.message || '').trim()).filter(Boolean) + : [] const msg = String(data.message || '').trim() const step = String(data.step || '').trim() const detail = String(data.detail || '').trim() const parts = [msg] if (step) parts.push(`step=${step}`) if (detail) parts.push(detail) + if (validationMessages.length) parts.push(validationMessages.join(' | ')) const merged = parts.filter(Boolean).join(' | ') if (merged) return merged } @@ -36,6 +40,51 @@ function nowMs () { return Date.now() } +const YAS_NUMERIC_SIZES = new Set(['2', '4', '6', '8', '10', '12', '14']) + +function safeStr (value) { + return value == null ? '' : String(value).trim() +} + +function normalizeProductionDim1Label (value) { + let text = safeStr(value) + if (!text) return '' + + text = text.toUpperCase() + const yasMatch = text.match(/^(\d+)\s*(Y|YAS|YAŞ)$/) + if (yasMatch?.[1] && YAS_NUMERIC_SIZES.has(yasMatch[1])) { + return yasMatch[1] + } + + return text +} + +function pickPreferredProductionYasPayloadLabel (currentRaw, nextRaw) { + const current = safeStr(currentRaw).toUpperCase() + const next = safeStr(nextRaw).toUpperCase() + if (!next) return current + if (!current) return next + + const currentHasYas = /YAS$|YAŞ$/.test(current) + const nextHasYas = /YAS$|YAŞ$/.test(next) + if (!currentHasYas && nextHasYas) return next + return current +} + +function toProductionPayloadDim1 (row, value) { + const base = normalizeProductionDim1Label(value) + if (!base) return '' + if (!YAS_NUMERIC_SIZES.has(base)) return base + + const map = + row?.yasPayloadMap && typeof row.yasPayloadMap === 'object' + ? row.yasPayloadMap + : {} + const mapped = safeStr(map[base]).toUpperCase() + if (mapped) return mapped + return `${base}Y` +} + export const useOrderProductionItemStore = defineStore('orderproductionitems', { state: () => ({ items: [], @@ -54,6 +103,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', { cdItemLookups: null, cdItemDraftsByCode: {}, productAttributeDraftsByCode: {}, + knownExistingItemCodes: {}, loading: false, saving: false, error: null @@ -71,18 +121,35 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', { }, actions: { + normalizeDim1ForUi (value) { + return normalizeProductionDim1Label(value) + }, + pickPreferredYasPayloadLabel (currentRaw, nextRaw) { + return pickPreferredProductionYasPayloadLabel(currentRaw, nextRaw) + }, + toPayloadDim1Code (row, value) { + return toProductionPayloadDim1(row, value) + }, classifyItemCode (value) { const normalized = String(value || '').trim().toUpperCase() if (!normalized) { return { normalized: '', mode: 'empty', exists: false } } - const exists = this.productCodeSet.has(normalized) + const exists = this.productCodeSet.has(normalized) || !!this.knownExistingItemCodes[normalized] return { normalized, mode: exists ? 'existing' : 'new', exists } }, + markItemCodeKnownExisting (itemCode, exists = true) { + const code = String(itemCode || '').trim().toUpperCase() + if (!code) return + this.knownExistingItemCodes = { + ...this.knownExistingItemCodes, + [code]: !!exists + } + }, async fetchHeader (orderHeaderID) { if (!orderHeaderID) { @@ -134,6 +201,20 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', { this.error = err?.response?.data || err?.message || 'Urun listesi alinamadi' } }, + async fetchCdItemByCode (code) { + if (!code) return null + try { + const res = await api.get('/product-cditem', { params: { code } }) + const data = res?.data || null + if (data) { + this.markItemCodeKnownExisting(code, true) + } + return data + } catch (err) { + console.error('[OrderProductionItemStore] fetchCdItemByCode failed', err) + return null + } + }, async fetchColors (productCode) { const code = String(productCode || '').trim() if (!code) return [] @@ -152,6 +233,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', { const res = await api.get('/product-colors', { params: { code } }) const data = res?.data const list = Array.isArray(data) ? data : [] + if (list.length) this.markItemCodeKnownExisting(code, true) this.colorOptionsByCode[code] = list console.info('[OrderProductionItemStore] fetchColors done', { code, count: list.length, durationMs: Math.round(nowMs() - t0) }) return list @@ -284,6 +366,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', { try { const res = await api.get('/product-item-attributes', { params: { itemTypeCode: itc, itemCode: code } }) const list = Array.isArray(res?.data) ? res.data : [] + if (list.length) this.markItemCodeKnownExisting(code, true) this.productItemAttributesByKey[key] = list return list } catch (err) { @@ -359,6 +442,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', { orderHeaderID, lineCount: lines?.length || 0, missingCount: Number(data?.missingCount || 0), + barcodeValidationCount: Number(data?.barcodeValidationCount || 0), requestId: rid, durationMs: Math.round(nowMs() - t0) }) @@ -371,7 +455,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', { this.saving = false } }, - async applyUpdates (orderHeaderID, lines, insertMissing, cdItems = [], productAttributes = []) { + async applyUpdates (orderHeaderID, lines, insertMissing, cdItems = [], productAttributes = [], headerAverageDueDate = null) { if (!orderHeaderID) return { updated: 0, inserted: 0 } this.saving = true @@ -384,11 +468,18 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', { lineCount: lines?.length || 0, insertMissing: !!insertMissing, cdItemCount: cdItems?.length || 0, - attributeCount: productAttributes?.length || 0 + attributeCount: productAttributes?.length || 0, + headerAverageDueDate }) const res = await api.post( `/orders/production-items/${encodeURIComponent(orderHeaderID)}/apply`, - { lines, insertMissing, cdItems, productAttributes } + { + lines, + insertMissing, + cdItems, + productAttributes, + HeaderAverageDueDate: headerAverageDueDate + } ) const data = res?.data || { updated: 0, inserted: 0 } const rid = res?.headers?.['x-debug-request-id'] || '' @@ -396,7 +487,9 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', { orderHeaderID, updated: Number(data?.updated || 0), inserted: Number(data?.inserted || 0), + barcodeInserted: Number(data?.barcodeInserted || 0), attributeUpserted: Number(data?.attributeUpserted || 0), + headerUpdated: !!data?.headerUpdated, requestId: rid, durationMs: Math.round(nowMs() - t0) }) diff --git a/ui/src/stores/ProductPricingStore.js b/ui/src/stores/ProductPricingStore.js new file mode 100644 index 0000000..d01a2ca --- /dev/null +++ b/ui/src/stores/ProductPricingStore.js @@ -0,0 +1,88 @@ +import { defineStore } from 'pinia' +import api from 'src/services/api' + +function toText (value) { + return String(value ?? '').trim() +} + +function toNumber (value) { + const n = Number(value) + return Number.isFinite(n) ? Number(n.toFixed(2)) : 0 +} + +function mapRow (raw, index) { + return { + id: index + 1, + productCode: toText(raw?.ProductCode), + stockQty: toNumber(raw?.StockQty), + stockEntryDate: toText(raw?.StockEntryDate), + lastPricingDate: toText(raw?.LastPricingDate), + askiliYan: toText(raw?.AskiliYan), + kategori: toText(raw?.Kategori), + urunIlkGrubu: toText(raw?.UrunIlkGrubu), + urunAnaGrubu: toText(raw?.UrunAnaGrubu), + urunAltGrubu: toText(raw?.UrunAltGrubu), + icerik: toText(raw?.Icerik), + karisim: toText(raw?.Karisim), + marka: toText(raw?.Marka), + brandGroupSelection: toText(raw?.BrandGroupSec), + costPrice: toNumber(raw?.CostPrice), + expenseForBasePrice: 0, + basePriceUsd: 0, + basePriceTry: 0, + usd1: 0, + usd2: 0, + usd3: 0, + usd4: 0, + usd5: 0, + usd6: 0, + eur1: 0, + eur2: 0, + eur3: 0, + eur4: 0, + eur5: 0, + eur6: 0, + try1: 0, + try2: 0, + try3: 0, + try4: 0, + try5: 0, + try6: 0 + } +} + +export const useProductPricingStore = defineStore('product-pricing-store', { + state: () => ({ + rows: [], + loading: false, + error: '' + }), + + actions: { + async fetchRows () { + this.loading = true + this.error = '' + try { + const res = await api.get('/pricing/products') + const data = Array.isArray(res?.data) ? res.data : [] + this.rows = data.map((x, i) => mapRow(x, i)) + } catch (err) { + this.rows = [] + const msg = err?.response?.data || err?.message || 'Urun fiyatlandirma listesi alinamadi' + this.error = toText(msg) + } finally { + this.loading = false + } + }, + + updateCell (row, field, val) { + if (!row || !field) return + row[field] = toNumber(String(val ?? '').replace(',', '.')) + }, + + updateBrandGroupSelection (row, val) { + if (!row) return + row.brandGroupSelection = toText(val) + } + } +}) diff --git a/ui/src/stores/orderentryStore.js b/ui/src/stores/orderentryStore.js index 81af9f5..f3a0811 100644 --- a/ui/src/stores/orderentryStore.js +++ b/ui/src/stores/orderentryStore.js @@ -212,6 +212,8 @@ export const useOrderEntryStore = defineStore('orderentry', { orders: [], header: {}, summaryRows: [], + originalHeader: {}, + originalLines: [], lastSavedAt: null, @@ -534,6 +536,54 @@ export const useOrderEntryStore = defineStore('orderentry', { const normalized = Array.isArray(lines) ? lines : [] const mapLabel = (ln) => this.buildMailLineLabel(ln) + const formatDate = (d) => { + if (!d) return '' + const s = String(d).split('T')[0] + return s + } + + const oldDate = formatDate(this.originalHeader?.AverageDueDate) + const newDate = formatDate(this.header?.AverageDueDate) + const origMap = new Map() + if (Array.isArray(this.originalLines)) { + this.originalLines.forEach(ln => { + if (ln.OrderLineID) origMap.set(String(ln.OrderLineID), ln) + }) + } + + const buildDueDateChanges = () => { + const out = [] + const seen = new Set() + + normalized.forEach(ln => { + if (ln?._deleteSignal || !ln?.OrderLineID || ln?._dirty !== true) return + + const orig = origMap.get(String(ln.OrderLineID)) + if (!orig) return + + const itemCode = String(ln?.ItemCode || '').trim().toUpperCase() + const colorCode = String(ln?.ColorCode || '').trim().toUpperCase() + const itemDim2Code = String(ln?.ItemDim2Code || '').trim().toUpperCase() + const oldLnDate = formatDate(orig?.DueDate) + const newLnDate = formatDate(ln?.DueDate) + if (!itemCode || !newLnDate || oldLnDate === newLnDate) return + + const key = [itemCode, colorCode, itemDim2Code, oldLnDate, newLnDate].join('||') + if (seen.has(key)) return + seen.add(key) + + out.push({ + itemCode, + colorCode, + itemDim2Code, + oldDueDate: oldLnDate, + newDueDate: newLnDate + }) + }) + + return out + } + if (isNew) { return { operation: 'create', @@ -543,7 +593,10 @@ export const useOrderEntryStore = defineStore('orderentry', { normalized .filter(ln => !ln?._deleteSignal) .map(mapLabel) - ) + ), + oldDueDate: '', + newDueDate: '', + dueDateChanges: [] } } @@ -553,11 +606,22 @@ export const useOrderEntryStore = defineStore('orderentry', { .map(mapLabel) ) - const updatedItems = uniq( - normalized - .filter(ln => !ln?._deleteSignal && !!ln?.OrderLineID && ln?._dirty === true) - .map(mapLabel) - ) + const updatedItems = [] + + normalized.forEach(ln => { + if (!ln?._deleteSignal && !!ln?.OrderLineID && ln?._dirty === true) { + let label = mapLabel(ln) + const orig = origMap.get(String(ln.OrderLineID)) + if (orig) { + const oldLnDate = formatDate(orig.DueDate) + const newLnDate = formatDate(ln.DueDate) + if (newLnDate && oldLnDate !== newLnDate) { + label += ` (Termin: ${oldLnDate} -> ${newLnDate})` + } + } + updatedItems.push(label) + } + }) const addedItems = uniq( normalized @@ -568,8 +632,11 @@ export const useOrderEntryStore = defineStore('orderentry', { return { operation: 'update', deletedItems, - updatedItems, - addedItems + updatedItems: uniq(updatedItems), + addedItems, + oldDueDate: oldDate, + newDueDate: newDate, + dueDateChanges: buildDueDateChanges() } } , @@ -586,7 +653,10 @@ export const useOrderEntryStore = defineStore('orderentry', { operation: payload?.operation || 'create', deletedItems: Array.isArray(payload?.deletedItems) ? payload.deletedItems : [], updatedItems: Array.isArray(payload?.updatedItems) ? payload.updatedItems : [], - addedItems: Array.isArray(payload?.addedItems) ? payload.addedItems : [] + addedItems: Array.isArray(payload?.addedItems) ? payload.addedItems : [], + oldDueDate: payload?.oldDueDate || '', + newDueDate: payload?.newDueDate || '', + dueDateChanges: Array.isArray(payload?.dueDateChanges) ? payload.dueDateChanges : [] }) return res?.data || {} } catch (err) { @@ -1113,6 +1183,10 @@ export const useOrderEntryStore = defineStore('orderentry', { this.orders = Array.isArray(normalized) ? normalized : [] this.summaryRows = [...this.orders] + // 💾 Snapshot for email comparison (v3.5) + this.originalHeader = JSON.parse(JSON.stringify(this.header)) + this.originalLines = JSON.parse(JSON.stringify(this.summaryRows)) + /* ======================================================= 🔹 MODE KARARI (BACKEND SATIRLARI ÜZERİNDEN) - herhangi bir isClosed=true → view @@ -3202,6 +3276,7 @@ export const useOrderEntryStore = defineStore('orderentry', { // 📧 Piyasa eşleşen alıcılara sipariş PDF gönderimi (kayıt başarılı olduktan sonra) try { const mailPayload = this.buildOrderMailPayload(lines, isNew) + // UPDATE durumunda da mail gönderimi istendiği için isNew kontrolü kaldırıldı (v3.5) const mailRes = await this.sendOrderToMarketMails(serverOrderId, mailPayload) const sentCount = Number(mailRes?.sentCount || 0) $q.notify({