From d7d871fb8a6ab1a85cc7fb6a9dadc0802f5c1a55 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Tue, 31 Mar 2026 12:45:22 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- svc/main.go | 6 + svc/models/orderproductionupdate.go | 74 ++- svc/models/productattributes.go | 9 + svc/models/productsecondcolor.go | 7 +- svc/queries/orderproduction_items.go | 279 ++++++++++- svc/queries/productattributes.go | 42 ++ svc/queries/productsecondcolor.go | 10 +- svc/routes/order_pdf.go | 31 +- svc/routes/orderproductionitems.go | 82 +++- svc/routes/productattributes.go | 54 +++ svc/routes/productsecondcolor.go | 2 +- ui/.quasar/prod-spa/app.js | 75 +++ ui/.quasar/prod-spa/client-entry.js | 154 ++++++ ui/.quasar/prod-spa/client-prefetch.js | 116 +++++ ui/.quasar/prod-spa/quasar-user-options.js | 23 + ui/src/pages/OrderEntry.vue | 79 ++- ui/src/pages/OrderProductionUpdate.vue | 536 +++++++++++++++++++-- ui/src/stores/OrderProductionItemStore.js | 122 +++-- ui/src/stores/orderentryStore.js | 65 ++- 19 files changed, 1608 insertions(+), 158 deletions(-) create mode 100644 svc/models/productattributes.go create mode 100644 svc/queries/productattributes.go create mode 100644 svc/routes/productattributes.go create mode 100644 ui/.quasar/prod-spa/app.js create mode 100644 ui/.quasar/prod-spa/client-entry.js create mode 100644 ui/.quasar/prod-spa/client-prefetch.js create mode 100644 ui/.quasar/prod-spa/quasar-user-options.js diff --git a/svc/main.go b/svc/main.go index c05a938..dbf23af 100644 --- a/svc/main.go +++ b/svc/main.go @@ -522,6 +522,7 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router {"/api/order/get/{id}", "GET", "view", routes.GetOrderByIDHandler(mssql)}, {"/api/orders/list", "GET", "view", routes.OrderListRoute(mssql)}, {"/api/orders/production-list", "GET", "update", routes.OrderProductionListRoute(mssql)}, + {"/api/orders/production-items/cditem-lookups", "GET", "view", routes.OrderProductionCdItemLookupsRoute(mssql)}, {"/api/orders/production-items/{id}", "GET", "view", routes.OrderProductionItemsRoute(mssql)}, {"/api/orders/production-items/{id}/insert-missing", "POST", "update", routes.OrderProductionInsertMissingRoute(mssql)}, {"/api/orders/production-items/{id}/validate", "POST", "update", routes.OrderProductionValidateRoute(mssql)}, @@ -587,6 +588,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "order", "view", wrapV3(http.HandlerFunc(routes.GetProductSecondColorsHandler)), ) + bindV3(r, pgDB, + "/api/product-attributes", "GET", + "order", "view", + wrapV3(http.HandlerFunc(routes.GetProductAttributesHandler)), + ) bindV3(r, pgDB, "/api/product-stock-query", "GET", "order", "view", diff --git a/svc/models/orderproductionupdate.go b/svc/models/orderproductionupdate.go index 74539bd..75ba91e 100644 --- a/svc/models/orderproductionupdate.go +++ b/svc/models/orderproductionupdate.go @@ -9,16 +9,72 @@ type OrderProductionUpdateLine struct { } type OrderProductionUpdatePayload struct { - Lines []OrderProductionUpdateLine `json:"lines"` - InsertMissing bool `json:"insertMissing"` + Lines []OrderProductionUpdateLine `json:"lines"` + InsertMissing bool `json:"insertMissing"` + CdItems []OrderProductionCdItemDraft `json:"cdItems"` + ProductAttributes []OrderProductionItemAttributeRow `json:"productAttributes"` } type OrderProductionMissingVariant struct { - OrderLineID string `json:"OrderLineID"` - ItemTypeCode int16 `json:"ItemTypeCode"` - ItemCode string `json:"ItemCode"` - ColorCode string `json:"ColorCode"` - ItemDim1Code string `json:"ItemDim1Code"` - ItemDim2Code string `json:"ItemDim2Code"` - ItemDim3Code string `json:"ItemDim3Code"` + 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"` +} + +type OrderProductionCdItemDraft struct { + ItemTypeCode int16 `json:"ItemTypeCode"` + ItemCode string `json:"ItemCode"` + ItemDimTypeCode *int16 `json:"ItemDimTypeCode"` + ProductTypeCode *int16 `json:"ProductTypeCode"` + ProductHierarchyID *int `json:"ProductHierarchyID"` + UnitOfMeasureCode1 *string `json:"UnitOfMeasureCode1"` + ItemAccountGrCode *string `json:"ItemAccountGrCode"` + ItemTaxGrCode *string `json:"ItemTaxGrCode"` + ItemPaymentPlanGrCode *string `json:"ItemPaymentPlanGrCode"` + ItemDiscountGrCode *string `json:"ItemDiscountGrCode"` + ItemVendorGrCode *string `json:"ItemVendorGrCode"` + PromotionGroupCode *string `json:"PromotionGroupCode"` + ProductCollectionGrCode *string `json:"ProductCollectionGrCode"` + StorePriceLevelCode *string `json:"StorePriceLevelCode"` + PerceptionOfFashionCode *string `json:"PerceptionOfFashionCode"` + CommercialRoleCode *string `json:"CommercialRoleCode"` + StoreCapacityLevelCode *string `json:"StoreCapacityLevelCode"` + CustomsTariffNumberCode *string `json:"CustomsTariffNumberCode"` + CompanyCode *string `json:"CompanyCode"` +} + +type OrderProductionLookupOption struct { + Code string `json:"code"` + Description string `json:"description"` +} + +type OrderProductionItemAttributeRow struct { + ItemTypeCode int16 `json:"ItemTypeCode"` + ItemCode string `json:"ItemCode"` + AttributeTypeCode int `json:"AttributeTypeCode"` + AttributeCode string `json:"AttributeCode"` +} + +type OrderProductionCdItemLookups struct { + ItemDimTypeCodes []OrderProductionLookupOption `json:"itemDimTypeCodes"` + ProductTypeCodes []OrderProductionLookupOption `json:"productTypeCodes"` + ProductHierarchyIDs []OrderProductionLookupOption `json:"productHierarchyIDs"` + UnitOfMeasureCode1List []OrderProductionLookupOption `json:"unitOfMeasureCode1List"` + ItemAccountGrCodes []OrderProductionLookupOption `json:"itemAccountGrCodes"` + ItemTaxGrCodes []OrderProductionLookupOption `json:"itemTaxGrCodes"` + ItemPaymentPlanGrCodes []OrderProductionLookupOption `json:"itemPaymentPlanGrCodes"` + ItemDiscountGrCodes []OrderProductionLookupOption `json:"itemDiscountGrCodes"` + ItemVendorGrCodes []OrderProductionLookupOption `json:"itemVendorGrCodes"` + PromotionGroupCodes []OrderProductionLookupOption `json:"promotionGroupCodes"` + ProductCollectionGrCodes []OrderProductionLookupOption `json:"productCollectionGrCodes"` + StorePriceLevelCodes []OrderProductionLookupOption `json:"storePriceLevelCodes"` + PerceptionOfFashionCodes []OrderProductionLookupOption `json:"perceptionOfFashionCodes"` + CommercialRoleCodes []OrderProductionLookupOption `json:"commercialRoleCodes"` + StoreCapacityLevelCodes []OrderProductionLookupOption `json:"storeCapacityLevelCodes"` + CustomsTariffNumbers []OrderProductionLookupOption `json:"customsTariffNumbers"` + CompanyCodes []OrderProductionLookupOption `json:"companyCodes"` } diff --git a/svc/models/productattributes.go b/svc/models/productattributes.go new file mode 100644 index 0000000..77b6c46 --- /dev/null +++ b/svc/models/productattributes.go @@ -0,0 +1,9 @@ +package models + +type ProductAttributeOption struct { + ItemTypeCode int16 `json:"item_type_code"` + AttributeTypeCode int `json:"attribute_type_code"` + AttributeTypeDescription string `json:"attribute_type_description"` + AttributeCode string `json:"attribute_code"` + AttributeDescription string `json:"attribute_description"` +} diff --git a/svc/models/productsecondcolor.go b/svc/models/productsecondcolor.go index 3977480..347907a 100644 --- a/svc/models/productsecondcolor.go +++ b/svc/models/productsecondcolor.go @@ -1,7 +1,8 @@ package models type ProductSecondColor struct { - ProductCode string `json:"product_code"` - ColorCode string `json:"color_code"` - ItemDim2Code string `json:"item_dim2_code"` + ProductCode string `json:"product_code"` + ColorCode string `json:"color_code"` + ItemDim2Code string `json:"item_dim2_code"` + ColorDescription string `json:"color_description"` } diff --git a/svc/queries/orderproduction_items.go b/svc/queries/orderproduction_items.go index 012f82c..656b23a 100644 --- a/svc/queries/orderproduction_items.go +++ b/svc/queries/orderproduction_items.go @@ -3,6 +3,7 @@ package queries import ( "database/sql" "strconv" + "strings" "bssapp-backend/models" ) @@ -74,10 +75,17 @@ INSERT INTO dbo.prItemVariant ( ItemDim2Code, ItemDim3Code, PLU, + IsSalesOrderClosed, + IsPurchaseOrderClosed, + IsLocked, + IsBlocked, CreatedUserName, CreatedDate, LastUpdatedUserName, - LastUpdatedDate + LastUpdatedDate, + RowGuid, + UseInternet, + IsStoreOrderClosed ) SELECT m.ItemTypeCode, @@ -87,10 +95,17 @@ SELECT m.ItemDim2Code, m.ItemDim3Code, mp.BasePlu + ROW_NUMBER() OVER (ORDER BY m.ItemCode, m.ColorCode, m.ItemDim1Code, m.ItemDim2Code, m.ItemDim3Code), + 0, + 0, + 0, + 0, @p2, GETDATE(), @p2, - GETDATE() + GETDATE(), + NEWID(), + 0, + 0 FROM Missing m CROSS JOIN MaxPlu mp; ` @@ -143,7 +158,12 @@ WHERE ItemTypeCode = @p1 return true, nil } -func InsertMissingVariantsTx(tx *sql.Tx, missing []models.OrderProductionMissingVariant, username string) (int64, error) { +func InsertMissingVariantsTx( + tx *sql.Tx, + missing []models.OrderProductionMissingVariant, + username string, + cdItemByCode map[string]models.OrderProductionCdItemDraft, +) (int64, error) { if len(missing) == 0 { return 0, nil } @@ -161,7 +181,16 @@ FROM dbo.prItemVariant WITH (UPDLOCK, HOLDLOCK) for i, v := range missing { itemKey := strconv.FormatInt(int64(v.ItemTypeCode), 10) + "|" + v.ItemCode if _, ok := ensuredItems[itemKey]; !ok { - if err := ensureCdItemTx(tx, v.ItemTypeCode, v.ItemCode, username); err != nil { + draft, hasDraft := cdItemByCode[itemKey] + if !hasDraft { + draft, hasDraft = cdItemByCode[NormalizeCdItemMapKey(v.ItemTypeCode, v.ItemCode)] + } + var draftPtr *models.OrderProductionCdItemDraft + if hasDraft { + tmp := draft + draftPtr = &tmp + } + if err := ensureCdItemTx(tx, v.ItemTypeCode, v.ItemCode, username, draftPtr); err != nil { return inserted, err } ensuredItems[itemKey] = struct{}{} @@ -187,14 +216,26 @@ INSERT INTO dbo.prItemVariant ( ItemDim2Code, ItemDim3Code, PLU, + IsSalesOrderClosed, + IsPurchaseOrderClosed, + IsLocked, + IsBlocked, CreatedUserName, CreatedDate, LastUpdatedUserName, - LastUpdatedDate + LastUpdatedDate, + RowGuid, + UseInternet, + IsStoreOrderClosed ) VALUES ( @p1, @p2, @p3, @p4, @p5, @p6, - @p7, @p8, GETDATE(), @p8, GETDATE() + @p7, + 0, 0, 0, 0, + @p8, GETDATE(), @p8, GETDATE(), + NEWID(), + 0, + 0 ); `, v.ItemTypeCode, v.ItemCode, v.ColorCode, v.ItemDim1Code, v.ItemDim2Code, v.ItemDim3Code, plu, username) if err != nil { @@ -207,7 +248,17 @@ VALUES ( return inserted, nil } -func ensureCdItemTx(tx *sql.Tx, itemTypeCode int16, itemCode string, username string) error { +func NormalizeCdItemMapKey(itemTypeCode int16, itemCode string) string { + return strconv.FormatInt(int64(itemTypeCode), 10) + "|" + strings.ToUpper(strings.TrimSpace(itemCode)) +} + +func ensureCdItemTx( + tx *sql.Tx, + itemTypeCode int16, + itemCode string, + username string, + draft *models.OrderProductionCdItemDraft, +) error { _, err := tx.Exec(` IF NOT EXISTS ( SELECT 1 @@ -269,28 +320,90 @@ BEGIN INSERT INTO dbo.cdItem ( ItemTypeCode, ItemCode, ItemDimTypeCode, ProductTypeCode, ProductHierarchyID, - UnitOfMeasureCode1, UnitConvertRate, UnitConvertRateNotFixed, + UnitOfMeasureCode1, UnitOfMeasureCode2, UnitConvertRate, UnitConvertRateNotFixed, UseInternet, UsePOS, UseStore, EnablePartnerCompanies, UseManufacturing, UseSerialNumber, GenerateOpticalDataMatrixCode, ByWeight, SupplyPeriod, GuaranteePeriod, ShelfLife, OrderLeadTime, - IsFixedExpense, IsBlocked, IsLocked, LockedDate, IsSalesOrderClosed, IsPurchaseOrderClosed, + ItemAccountGrCode, ItemTaxGrCode, ItemPaymentPlanGrCode, ItemDiscountGrCode, ItemVendorGrCode, + PromotionGroupCode, PromotionGroupCode2, ProductCollectionGrCode, StorePriceLevelCode, PerceptionOfFashionCode, + CommercialRoleCode, StoreCapacityLevelCode, CustomsTariffNumberCode, IsFixedExpense, BOMEntityCode, CompanyCode, + IsBlocked, IsLocked, LockedDate, IsSalesOrderClosed, IsPurchaseOrderClosed, CreatedUserName, CreatedDate, LastUpdatedUserName, LastUpdatedDate, RowGuid, UseRoll, UseBatch, MaxCreditCardInstallmentCount, GenerateSerialNumber, - IsSubsequentDeliveryForR, IsSubsequentDeliveryForRI, IsUTSDeclaratedItem, IsStoreOrderClosed + IsSubsequentDeliveryForR, IsSubsequentDeliveryForRI, + IGACommissionGroup, UniFreeCommissionGroup, CustomsProductGroupCode, IsUTSDeclaratedItem, IsStoreOrderClosed ) VALUES ( @p1, @p2, 2, 1, 2, - 'AD', 0, 0, - 0, 1, 1, 0, 1, 0, + 'AD', '', 0, 0, + 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, '1900-01-01', 0, 0, + '', '10%', '', '', '', + '', '', '0', '0', '0', + '0', '', '', 0, '', '1', + 0, 0, '1900-01-01', 0, 0, @p3, GETDATE(), @p3, GETDATE(), NEWID(), 0, 0, 12, 0, - 0, 0, 0, 0 + 0, 0, + '', '', '0', 0, 0 ); END END `, itemTypeCode, itemCode, username) + if err != nil { + return err + } + + if draft == nil { + return nil + } + + _, err = tx.Exec(` +UPDATE dbo.cdItem +SET + ItemDimTypeCode = COALESCE(@p3, ItemDimTypeCode), + ProductTypeCode = COALESCE(@p4, ProductTypeCode), + ProductHierarchyID = COALESCE(@p5, ProductHierarchyID), + UnitOfMeasureCode1 = COALESCE(NULLIF(@p6,''), UnitOfMeasureCode1), + ItemAccountGrCode = COALESCE(NULLIF(@p7,''), ItemAccountGrCode), + ItemTaxGrCode = COALESCE(NULLIF(@p8,''), ItemTaxGrCode), + ItemPaymentPlanGrCode = COALESCE(NULLIF(@p9,''), ItemPaymentPlanGrCode), + ItemDiscountGrCode = COALESCE(NULLIF(@p10,''), ItemDiscountGrCode), + ItemVendorGrCode = COALESCE(NULLIF(@p11,''), ItemVendorGrCode), + PromotionGroupCode = COALESCE(NULLIF(@p12,''), PromotionGroupCode), + ProductCollectionGrCode = COALESCE(NULLIF(@p13,''), ProductCollectionGrCode), + StorePriceLevelCode = COALESCE(NULLIF(@p14,''), StorePriceLevelCode), + PerceptionOfFashionCode = COALESCE(NULLIF(@p15,''), PerceptionOfFashionCode), + CommercialRoleCode = COALESCE(NULLIF(@p16,''), CommercialRoleCode), + StoreCapacityLevelCode = COALESCE(NULLIF(@p17,''), StoreCapacityLevelCode), + CustomsTariffNumberCode = COALESCE(NULLIF(@p18,''), CustomsTariffNumberCode), + CompanyCode = COALESCE(NULLIF(@p19,''), CompanyCode), + LastUpdatedUserName = @p20, + LastUpdatedDate = GETDATE() +WHERE ItemTypeCode = @p1 + AND ItemCode = @p2; +`, + itemTypeCode, + itemCode, + draft.ItemDimTypeCode, + draft.ProductTypeCode, + draft.ProductHierarchyID, + draft.UnitOfMeasureCode1, + draft.ItemAccountGrCode, + draft.ItemTaxGrCode, + draft.ItemPaymentPlanGrCode, + draft.ItemDiscountGrCode, + draft.ItemVendorGrCode, + draft.PromotionGroupCode, + draft.ProductCollectionGrCode, + draft.StorePriceLevelCode, + draft.PerceptionOfFashionCode, + draft.CommercialRoleCode, + draft.StoreCapacityLevelCode, + draft.CustomsTariffNumberCode, + draft.CompanyCode, + username, + ) return err } @@ -317,3 +430,141 @@ WHERE OrderHeaderID = @p6 AND OrderLineID = @p7 } return updated, nil } + +func UpsertItemAttributesTx(tx *sql.Tx, attrs []models.OrderProductionItemAttributeRow, username string) (int64, error) { + if len(attrs) == 0 { + return 0, nil + } + + var affected int64 + for _, a := range attrs { + res, err := tx.Exec(` +IF EXISTS ( + SELECT 1 + FROM dbo.prItemAttribute + WHERE ItemTypeCode = @p1 + AND ItemCode = @p2 + AND AttributeTypeCode = @p3 +) +BEGIN + UPDATE dbo.prItemAttribute + SET + AttributeCode = @p4, + LastUpdatedUserName = @p5, + LastUpdatedDate = GETDATE() + WHERE ItemTypeCode = @p1 + AND ItemCode = @p2 + AND AttributeTypeCode = @p3 +END +ELSE +BEGIN + INSERT INTO dbo.prItemAttribute ( + ItemTypeCode, + ItemCode, + AttributeTypeCode, + AttributeCode, + CreatedUserName, + CreatedDate, + LastUpdatedUserName, + LastUpdatedDate, + RowGuid + ) + VALUES ( + @p1, + @p2, + @p3, + @p4, + @p5, + GETDATE(), + @p5, + GETDATE(), + NEWID() + ) +END +`, a.ItemTypeCode, a.ItemCode, a.AttributeTypeCode, a.AttributeCode, username) + if err != nil { + return affected, err + } + if rows, err := res.RowsAffected(); err == nil { + affected += rows + } + } + return affected, nil +} + +func GetOrderProductionLookupOptions(mssql *sql.DB) (models.OrderProductionCdItemLookups, error) { + out := models.OrderProductionCdItemLookups{} + + queryPairs := []struct { + Query string + Target *[]models.OrderProductionLookupOption + }{ + {`SELECT + CAST(t.ItemDimTypeCode AS NVARCHAR(50)) AS Code, + ISNULL(d.ItemDimTypeDescription, CAST(t.ItemDimTypeCode AS NVARCHAR(50))) AS [Description] + FROM dbo.bsItemDimType t WITH(NOLOCK) + LEFT JOIN dbo.bsItemDimTypeDesc d WITH(NOLOCK) + ON d.ItemDimTypeCode = t.ItemDimTypeCode + AND d.LangCode = 'TR' + WHERE ISNULL(t.IsBlocked, 0) = 0 + ORDER BY t.ItemDimTypeCode`, &out.ItemDimTypeCodes}, + {`SELECT DISTINCT CAST(ProductTypeCode AS NVARCHAR(50)) AS Code, CAST(ProductTypeCode AS NVARCHAR(50)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ProductTypeCode IS NOT NULL ORDER BY Code`, &out.ProductTypeCodes}, + {`SELECT + CAST(h.ProductHierarchyID AS NVARCHAR(50)) AS Code, + LTRIM(RTRIM( + CONCAT( + CAST(ISNULL(h.ProductHierarchyLevelCode01, 0) AS NVARCHAR(50)), + CASE + WHEN ISNULL(d.ProductHierarchyLevelDescription, '') <> '' THEN CONCAT(' - ', d.ProductHierarchyLevelDescription) + ELSE '' + END + ) + )) AS [Description] + FROM dbo.dfProductHierarchy h WITH(NOLOCK) + LEFT JOIN dbo.cdProductHierarchyLevelDesc d WITH(NOLOCK) + ON d.ProductHierarchyLevelCode = h.ProductHierarchyLevelCode01 + AND d.LangCode = 'TR' + ORDER BY h.ProductHierarchyID`, &out.ProductHierarchyIDs}, + {`SELECT DISTINCT CAST(UnitOfMeasureCode1 AS NVARCHAR(50)) AS Code, CAST(UnitOfMeasureCode1 AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(UnitOfMeasureCode1,'') <> '' ORDER BY Code`, &out.UnitOfMeasureCode1List}, + {`SELECT DISTINCT CAST(ItemAccountGrCode AS NVARCHAR(50)) AS Code, CAST(ItemAccountGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(ItemAccountGrCode,'') <> '' ORDER BY Code`, &out.ItemAccountGrCodes}, + {`SELECT DISTINCT CAST(ItemTaxGrCode AS NVARCHAR(50)) AS Code, CAST(ItemTaxGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(ItemTaxGrCode,'') <> '' ORDER BY Code`, &out.ItemTaxGrCodes}, + {`SELECT DISTINCT CAST(ItemPaymentPlanGrCode AS NVARCHAR(50)) AS Code, CAST(ItemPaymentPlanGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(ItemPaymentPlanGrCode,'') <> '' ORDER BY Code`, &out.ItemPaymentPlanGrCodes}, + {`SELECT DISTINCT CAST(ItemDiscountGrCode AS NVARCHAR(50)) AS Code, CAST(ItemDiscountGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(ItemDiscountGrCode,'') <> '' ORDER BY Code`, &out.ItemDiscountGrCodes}, + {`SELECT DISTINCT CAST(ItemVendorGrCode AS NVARCHAR(50)) AS Code, CAST(ItemVendorGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(ItemVendorGrCode,'') <> '' ORDER BY Code`, &out.ItemVendorGrCodes}, + {`SELECT DISTINCT CAST(PromotionGroupCode AS NVARCHAR(50)) AS Code, CAST(PromotionGroupCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(PromotionGroupCode,'') <> '' ORDER BY Code`, &out.PromotionGroupCodes}, + {`SELECT DISTINCT CAST(ProductCollectionGrCode AS NVARCHAR(50)) AS Code, CAST(ProductCollectionGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(ProductCollectionGrCode,'') <> '' ORDER BY Code`, &out.ProductCollectionGrCodes}, + {`SELECT DISTINCT CAST(StorePriceLevelCode AS NVARCHAR(50)) AS Code, CAST(StorePriceLevelCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(StorePriceLevelCode,'') <> '' ORDER BY Code`, &out.StorePriceLevelCodes}, + {`SELECT DISTINCT CAST(PerceptionOfFashionCode AS NVARCHAR(50)) AS Code, CAST(PerceptionOfFashionCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(PerceptionOfFashionCode,'') <> '' ORDER BY Code`, &out.PerceptionOfFashionCodes}, + {`SELECT DISTINCT CAST(CommercialRoleCode AS NVARCHAR(50)) AS Code, CAST(CommercialRoleCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(CommercialRoleCode,'') <> '' ORDER BY Code`, &out.CommercialRoleCodes}, + {`SELECT DISTINCT CAST(StoreCapacityLevelCode AS NVARCHAR(50)) AS Code, CAST(StoreCapacityLevelCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(StoreCapacityLevelCode,'') <> '' ORDER BY Code`, &out.StoreCapacityLevelCodes}, + {`SELECT DISTINCT CAST(CustomsTariffNumberCode AS NVARCHAR(50)) AS Code, CAST(CustomsTariffNumberCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(CustomsTariffNumberCode,'') <> '' ORDER BY Code`, &out.CustomsTariffNumbers}, + {`SELECT DISTINCT CAST(CompanyCode AS NVARCHAR(50)) AS Code, CAST(CompanyCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ISNULL(CompanyCode,'') <> '' ORDER BY Code`, &out.CompanyCodes}, + } + + for _, pair := range queryPairs { + rows, err := mssql.Query(pair.Query) + if err != nil { + return out, err + } + + list := make([]models.OrderProductionLookupOption, 0, 64) + for rows.Next() { + var item models.OrderProductionLookupOption + if err := rows.Scan(&item.Code, &item.Description); err != nil { + rows.Close() + return out, err + } + item.Code = strings.TrimSpace(item.Code) + item.Description = strings.TrimSpace(item.Description) + list = append(list, item) + } + if err := rows.Err(); err != nil { + rows.Close() + return out, err + } + rows.Close() + *pair.Target = list + } + + return out, nil +} diff --git a/svc/queries/productattributes.go b/svc/queries/productattributes.go new file mode 100644 index 0000000..9938be2 --- /dev/null +++ b/svc/queries/productattributes.go @@ -0,0 +1,42 @@ +package queries + +const GetProductAttributes = ` +;WITH TypeDesc AS ( + SELECT + t.ItemTypeCode, + t.AttributeTypeCode, + ISNULL(t.AttributeTypeDescription, CAST(t.AttributeTypeCode AS NVARCHAR(30))) AS AttributeTypeDescription + FROM dbo.cdItemAttributeTypeDesc AS t WITH(NOLOCK) + WHERE t.ItemTypeCode = @p1 + AND t.LangCode = 'TR' +), +Attr AS ( + SELECT + a.ItemTypeCode, + a.AttributeTypeCode, + ISNULL(a.AttributeCode, '') AS AttributeCode, + ISNULL(d.AttributeDescription, ISNULL(a.AttributeCode, '')) AS AttributeDescription + FROM dbo.cdItemAttribute AS a WITH(NOLOCK) + LEFT JOIN dbo.cdItemAttributeDesc AS d WITH(NOLOCK) + ON d.ItemTypeCode = a.ItemTypeCode + AND d.AttributeTypeCode = a.AttributeTypeCode + AND d.AttributeCode = a.AttributeCode + AND d.LangCode = 'TR' + WHERE a.ItemTypeCode = @p1 + AND ISNULL(a.IsBlocked, 0) = 0 +), +SELECT + a.ItemTypeCode, + a.AttributeTypeCode, + ISNULL(NULLIF(td.AttributeTypeDescription, ''), CAST(a.AttributeTypeCode AS NVARCHAR(30))) AS AttributeTypeDescription, + a.AttributeCode, + a.AttributeDescription +FROM Attr a +LEFT JOIN TypeDesc td + ON td.ItemTypeCode = a.ItemTypeCode + AND td.AttributeTypeCode = a.AttributeTypeCode +ORDER BY + a.AttributeTypeCode, + CASE WHEN a.AttributeCode = '-' THEN 0 ELSE 1 END, + a.AttributeCode; +` diff --git a/svc/queries/productsecondcolor.go b/svc/queries/productsecondcolor.go index 8213c89..8be5a41 100644 --- a/svc/queries/productsecondcolor.go +++ b/svc/queries/productsecondcolor.go @@ -4,7 +4,8 @@ const GetProductSecondColors = ` SELECT DISTINCT Product.ProductCode, ISNULL(prItemVariant.ColorCode, '') AS ColorCode, - ISNULL(prItemVariant.ItemDim2Code, '') AS ItemDim2Code + ISNULL(prItemVariant.ItemDim2Code, '') AS ItemDim2Code, + ISNULL(ColorDesc.ColorDescription, '') AS ColorDescription FROM prItemVariant WITH(NOLOCK) INNER JOIN ProductFilterWithDescription('TR') AS Product ON prItemVariant.ItemCode = Product.ProductCode @@ -14,5 +15,10 @@ FROM prItemVariant WITH(NOLOCK) WHERE Product.ProductCode = @ProductCode AND prItemVariant.ColorCode = @ColorCode AND ISNULL(prItemVariant.ItemDim2Code, '') <> '' -GROUP BY Product.ProductCode, prItemVariant.ItemDim2Code, prItemVariant.ColorCode +GROUP BY + Product.ProductCode, + prItemVariant.ItemDim2Code, + prItemVariant.ColorCode, + ColorDesc.ColorDescription +ORDER BY prItemVariant.ItemDim2Code ` diff --git a/svc/routes/order_pdf.go b/svc/routes/order_pdf.go index cd19976..9166b61 100644 --- a/svc/routes/order_pdf.go +++ b/svc/routes/order_pdf.go @@ -1597,6 +1597,17 @@ func renderOrderGrid(pdf *gofpdf.Fpdf, header *OrderHeader, rows []PdfRow, hasVa layout := newPdfLayout(pdf) catSizes := buildCategorySizeMap(rows) + normalizeYetiskinGarsonTokenGo := func(v string) string { + s := strings.ToUpper(strings.TrimSpace(v)) + if strings.Contains(s, "GARSON") { + return "GARSON" + } + if strings.Contains(s, "YETISKIN") || strings.Contains(s, "YETİSKİN") { + return "YETISKIN" + } + return "GENEL" + } + // Grup: ÜRÜN ANA GRUBU type group struct { Name string @@ -1609,15 +1620,19 @@ func renderOrderGrid(pdf *gofpdf.Fpdf, header *OrderHeader, rows []PdfRow, hasVa var order []string for _, r := range rows { - name := strings.TrimSpace(r.GroupMain) - if name == "" { - name = "GENEL" + ana := strings.TrimSpace(r.GroupMain) + if ana == "" { + ana = "GENEL" } - g, ok := groups[name] + yg := normalizeYetiskinGarsonTokenGo(r.YetiskinGarson) + name := strings.TrimSpace(fmt.Sprintf("%s %s", yg, ana)) + groupKey := fmt.Sprintf("%s::%s", yg, ana) + + g, ok := groups[groupKey] if !ok { g = &group{Name: name} - groups[name] = g - order = append(order, name) + groups[groupKey] = g + order = append(order, groupKey) } g.Rows = append(g.Rows, r) g.Adet += r.TotalQty @@ -1673,8 +1688,8 @@ func renderOrderGrid(pdf *gofpdf.Fpdf, header *OrderHeader, rows []PdfRow, hasVa newPage(firstPage, true) firstPage = false - for _, name := range order { - g := groups[name] + for _, key := range order { + g := groups[key] for _, row := range g.Rows { rh := calcRowHeight(pdf, layout, row) diff --git a/svc/routes/orderproductionitems.go b/svc/routes/orderproductionitems.go index 3d4cc70..d2d8fe9 100644 --- a/svc/routes/orderproductionitems.go +++ b/svc/routes/orderproductionitems.go @@ -9,12 +9,15 @@ import ( "errors" "log" "net/http" + "regexp" "strings" "github.com/gorilla/mux" mssql "github.com/microsoft/go-mssqldb" ) +var baggiModelCodeRegex = regexp.MustCompile(`^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$`) + // ====================================================== // 📌 OrderProductionItemsRoute — U ürün satırları // ====================================================== @@ -73,6 +76,23 @@ func OrderProductionItemsRoute(mssql *sql.DB) http.Handler { }) } +func OrderProductionCdItemLookupsRoute(mssql *sql.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + lookups, err := queries.GetOrderProductionLookupOptions(mssql) + if err != nil { + log.Printf("[OrderProductionCdItemLookupsRoute] lookup error: %v", err) + http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) + return + } + + if err := json.NewEncoder(w).Encode(lookups); err != nil { + log.Printf("[OrderProductionCdItemLookupsRoute] encode error: %v", err) + } + }) +} + // ====================================================== // 📌 OrderProductionInsertMissingRoute — eksik varyantları ekler // ====================================================== @@ -208,13 +228,24 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler { var inserted int64 if payload.InsertMissing { - inserted, err = queries.InsertMissingVariantsTx(tx, missing, username) + cdItemByCode := buildCdItemDraftMap(payload.CdItems) + inserted, err = queries.InsertMissingVariantsTx(tx, missing, username, cdItemByCode) if err != nil { writeDBError(w, http.StatusInternalServerError, "insert_missing_variants", id, username, len(missing), err) return } } + if err := validateProductAttributes(payload.ProductAttributes); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + attributeAffected, err := queries.UpsertItemAttributesTx(tx, payload.ProductAttributes, username) + if err != nil { + writeDBError(w, http.StatusInternalServerError, "upsert_item_attributes", id, username, len(payload.ProductAttributes), err) + return + } + updated, err := queries.UpdateOrderLinesTx(tx, id, payload.Lines, username) if err != nil { writeDBError(w, http.StatusInternalServerError, "update_order_lines", id, username, len(payload.Lines), err) @@ -227,8 +258,9 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler { } resp := map[string]any{ - "updated": updated, - "inserted": inserted, + "updated": updated, + "inserted": inserted, + "attributeUpserted": attributeAffected, } if err := json.NewEncoder(w).Encode(resp); err != nil { log.Printf("❌ encode error: %v", err) @@ -236,6 +268,44 @@ func OrderProductionApplyRoute(mssql *sql.DB) http.Handler { }) } +func validateProductAttributes(attrs []models.OrderProductionItemAttributeRow) error { + for _, a := range attrs { + if strings.TrimSpace(a.ItemCode) == "" { + return errors.New("Urun ozellikleri icin ItemCode zorunlu") + } + if !baggiModelCodeRegex.MatchString(strings.ToUpper(strings.TrimSpace(a.ItemCode))) { + return errors.New("Girdiginiz kod BAGGI kod sistemine uyumlu degil. Format: X999-XXX99999") + } + if a.ItemTypeCode <= 0 { + return errors.New("Urun ozellikleri icin ItemTypeCode zorunlu") + } + if a.AttributeTypeCode <= 0 { + return errors.New("Urun ozellikleri icin AttributeTypeCode zorunlu") + } + if strings.TrimSpace(a.AttributeCode) == "" { + return errors.New("Urun ozellikleri icin AttributeCode zorunlu") + } + } + return nil +} + +func buildCdItemDraftMap(list []models.OrderProductionCdItemDraft) map[string]models.OrderProductionCdItemDraft { + out := make(map[string]models.OrderProductionCdItemDraft, len(list)) + for _, item := range list { + code := strings.ToUpper(strings.TrimSpace(item.ItemCode)) + if code == "" { + continue + } + item.ItemCode = code + if item.ItemTypeCode == 0 { + item.ItemTypeCode = 1 + } + key := queries.NormalizeCdItemMapKey(item.ItemTypeCode, item.ItemCode) + out[key] = item + } + return out +} + func buildMissingVariants(mssql *sql.DB, orderHeaderID string, lines []models.OrderProductionUpdateLine) ([]models.OrderProductionMissingVariant, error) { missing := make([]models.OrderProductionMissingVariant, 0) @@ -279,9 +349,13 @@ func validateUpdateLines(lines []models.OrderProductionUpdateLine) error { if strings.TrimSpace(line.OrderLineID) == "" { return errors.New("OrderLineID zorunlu") } - if strings.TrimSpace(line.NewItemCode) == "" { + code := strings.ToUpper(strings.TrimSpace(line.NewItemCode)) + if code == "" { return errors.New("Yeni urun kodu zorunlu") } + if !baggiModelCodeRegex.MatchString(code) { + return errors.New("Girdiginiz kod BAGGI kod sistemine uyumlu degil. Format: X999-XXX99999") + } } return nil } diff --git a/svc/routes/productattributes.go b/svc/routes/productattributes.go new file mode 100644 index 0000000..de3eeb8 --- /dev/null +++ b/svc/routes/productattributes.go @@ -0,0 +1,54 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/db" + "bssapp-backend/models" + "bssapp-backend/queries" + "encoding/json" + "net/http" + "strconv" +) + +func GetProductAttributesHandler(w http.ResponseWriter, r *http.Request) { + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + itemTypeCode := int16(1) + if raw := r.URL.Query().Get("itemTypeCode"); raw != "" { + v, err := strconv.Atoi(raw) + if err != nil || v <= 0 { + http.Error(w, "itemTypeCode gecersiz", http.StatusBadRequest) + return + } + itemTypeCode = int16(v) + } + + rows, err := db.MssqlDB.Query(queries.GetProductAttributes, itemTypeCode) + if err != nil { + http.Error(w, "Product attributes alinamadi: "+err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + + list := make([]models.ProductAttributeOption, 0, 256) + for rows.Next() { + var x models.ProductAttributeOption + if err := rows.Scan( + &x.ItemTypeCode, + &x.AttributeTypeCode, + &x.AttributeTypeDescription, + &x.AttributeCode, + &x.AttributeDescription, + ); err != nil { + continue + } + list = append(list, x) + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(list) +} diff --git a/svc/routes/productsecondcolor.go b/svc/routes/productsecondcolor.go index 500a1cd..c0c1c82 100644 --- a/svc/routes/productsecondcolor.go +++ b/svc/routes/productsecondcolor.go @@ -45,7 +45,7 @@ func GetProductSecondColorsHandler(w http.ResponseWriter, r *http.Request) { var list []models.ProductSecondColor for rows.Next() { var c models.ProductSecondColor - if err := rows.Scan(&c.ProductCode, &c.ColorCode, &c.ItemDim2Code); err != nil { + if err := rows.Scan(&c.ProductCode, &c.ColorCode, &c.ItemDim2Code, &c.ColorDescription); err != nil { log.Println("⚠️ Satır okunamadı:", err) continue } diff --git a/ui/.quasar/prod-spa/app.js b/ui/.quasar/prod-spa/app.js new file mode 100644 index 0000000..caeaac1 --- /dev/null +++ b/ui/.quasar/prod-spa/app.js @@ -0,0 +1,75 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + + + + + +import { Quasar } from 'quasar' +import { markRaw } from 'vue' +import RootComponent from 'app/src/App.vue' + +import createStore from 'app/src/stores/index' +import createRouter from 'app/src/router/index' + + + + + +export default async function (createAppFn, quasarUserOptions) { + + + // Create the app instance. + // Here we inject into it the Quasar UI, the router & possibly the store. + const app = createAppFn(RootComponent) + + + + app.use(Quasar, quasarUserOptions) + + + + + const store = typeof createStore === 'function' + ? await createStore({}) + : createStore + + + app.use(store) + + + + + + const router = markRaw( + typeof createRouter === 'function' + ? await createRouter({store}) + : createRouter + ) + + + // make router instance available in store + + store.use(({ store }) => { store.router = router }) + + + + // Expose the app, the router and the store. + // Note that we are not mounting the app here, since bootstrapping will be + // different depending on whether we are in a browser or on the server. + return { + app, + store, + router + } +} diff --git a/ui/.quasar/prod-spa/client-entry.js b/ui/.quasar/prod-spa/client-entry.js new file mode 100644 index 0000000..5de66d0 --- /dev/null +++ b/ui/.quasar/prod-spa/client-entry.js @@ -0,0 +1,154 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + + +import { createApp } from 'vue' + + + + + + + +import '@quasar/extras/roboto-font/roboto-font.css' + +import '@quasar/extras/material-icons/material-icons.css' + + + + +// We load Quasar stylesheet file +import 'quasar/dist/quasar.sass' + + + + +import 'src/css/app.css' + + +import createQuasarApp from './app.js' +import quasarUserOptions from './quasar-user-options.js' + + + + + + + + +const publicPath = `/` + + +async function start ({ + app, + router + , store +}, bootFiles) { + + let hasRedirected = false + const getRedirectUrl = url => { + try { return router.resolve(url).href } + catch (err) {} + + return Object(url) === url + ? null + : url + } + const redirect = url => { + hasRedirected = true + + if (typeof url === 'string' && /^https?:\/\//.test(url)) { + window.location.href = url + return + } + + const href = getRedirectUrl(url) + + // continue if we didn't fail to resolve the url + if (href !== null) { + window.location.href = href + window.location.reload() + } + } + + const urlPath = window.location.href.replace(window.location.origin, '') + + for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) { + try { + await bootFiles[i]({ + app, + router, + store, + ssrContext: null, + redirect, + urlPath, + publicPath + }) + } + catch (err) { + if (err && err.url) { + redirect(err.url) + return + } + + console.error('[Quasar] boot error:', err) + return + } + } + + if (hasRedirected === true) return + + + app.use(router) + + + + + + + app.mount('#q-app') + + + +} + +createQuasarApp(createApp, quasarUserOptions) + + .then(app => { + // eventually remove this when Cordova/Capacitor/Electron support becomes old + const [ method, mapFn ] = Promise.allSettled !== void 0 + ? [ + 'allSettled', + bootFiles => bootFiles.map(result => { + if (result.status === 'rejected') { + console.error('[Quasar] boot error:', result.reason) + return + } + return result.value.default + }) + ] + : [ + 'all', + bootFiles => bootFiles.map(entry => entry.default) + ] + + return Promise[ method ]([ + + import(/* webpackMode: "eager" */ 'boot/dayjs') + + ]).then(bootFiles => { + const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function') + start(app, boot) + }) + }) + diff --git a/ui/.quasar/prod-spa/client-prefetch.js b/ui/.quasar/prod-spa/client-prefetch.js new file mode 100644 index 0000000..9bbe3c5 --- /dev/null +++ b/ui/.quasar/prod-spa/client-prefetch.js @@ -0,0 +1,116 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + + + +import App from 'app/src/App.vue' +let appPrefetch = typeof App.preFetch === 'function' + ? App.preFetch + : ( + // Class components return the component options (and the preFetch hook) inside __c property + App.__c !== void 0 && typeof App.__c.preFetch === 'function' + ? App.__c.preFetch + : false + ) + + +function getMatchedComponents (to, router) { + const route = to + ? (to.matched ? to : router.resolve(to).route) + : router.currentRoute.value + + if (!route) { return [] } + + const matched = route.matched.filter(m => m.components !== void 0) + + if (matched.length === 0) { return [] } + + return Array.prototype.concat.apply([], matched.map(m => { + return Object.keys(m.components).map(key => { + const comp = m.components[key] + return { + path: m.path, + c: comp + } + }) + })) +} + +export function addPreFetchHooks ({ router, store, publicPath }) { + // Add router hook for handling preFetch. + // Doing it after initial route is resolved so that we don't double-fetch + // the data that we already have. Using router.beforeResolve() so that all + // async components are resolved. + router.beforeResolve((to, from, next) => { + const + urlPath = window.location.href.replace(window.location.origin, ''), + matched = getMatchedComponents(to, router), + prevMatched = getMatchedComponents(from, router) + + let diffed = false + const preFetchList = matched + .filter((m, i) => { + return diffed || (diffed = ( + !prevMatched[i] || + prevMatched[i].c !== m.c || + m.path.indexOf('/:') > -1 // does it has params? + )) + }) + .filter(m => m.c !== void 0 && ( + typeof m.c.preFetch === 'function' + // Class components return the component options (and the preFetch hook) inside __c property + || (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function') + )) + .map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch) + + + if (appPrefetch !== false) { + preFetchList.unshift(appPrefetch) + appPrefetch = false + } + + + if (preFetchList.length === 0) { + return next() + } + + let hasRedirected = false + const redirect = url => { + hasRedirected = true + next(url) + } + const proceed = () => { + + if (hasRedirected === false) { next() } + } + + + + preFetchList.reduce( + (promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({ + store, + currentRoute: to, + previousRoute: from, + redirect, + urlPath, + publicPath + })), + Promise.resolve() + ) + .then(proceed) + .catch(e => { + console.error(e) + proceed() + }) + }) +} diff --git a/ui/.quasar/prod-spa/quasar-user-options.js b/ui/.quasar/prod-spa/quasar-user-options.js new file mode 100644 index 0000000..ac1dae3 --- /dev/null +++ b/ui/.quasar/prod-spa/quasar-user-options.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + +import lang from 'quasar/lang/tr.js' + + + +import {Loading,Dialog,Notify} from 'quasar' + + + +export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} } + diff --git a/ui/src/pages/OrderEntry.vue b/ui/src/pages/OrderEntry.vue index a6cd714..fa3b68f 100644 --- a/ui/src/pages/OrderEntry.vue +++ b/ui/src/pages/OrderEntry.vue @@ -323,12 +323,12 @@ ======================================================== -->
- @@ -205,6 +226,127 @@ Hata: {{ store.error }} + + + + +
Yeni Kod cdItem Bilgileri
+ + + {{ cdItemTargetCode || '-' }} + +
+ + +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+ + + + + +
+
+ + + + +
Urun Ozellikleri (2. Pop-up)
+ + {{ attributeTargetCode || '-' }} +
+ + +
+ Ilk etap dummy: isBlocked=0 kabul edilmis satirlar gibi listelenir. +
+
+
+ +
+
+ +
+
+
+ + + + + +
+
@@ -219,6 +361,8 @@ import { normalizeSearchText } from 'src/utils/searchText' const route = useRoute() const $q = useQuasar() const store = useOrderProductionItemStore() +const BAGGI_CODE_PATTERN = /^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$/ +const BAGGI_CODE_ERROR = 'Girdiginiz kod BAGGI kod sistemine uyumlu degil. Format: X999-XXX99999' const orderHeaderID = computed(() => String(route.params.orderHeaderID || '').trim()) const header = computed(() => store.header || {}) @@ -235,6 +379,12 @@ const descFilter = ref('') const productOptions = ref([]) const productSearch = ref('') const selectedMap = ref({}) +const cdItemDialogOpen = ref(false) +const cdItemTargetCode = ref('') +const cdItemDraftForm = ref(createEmptyCdItemDraft('')) +const attributeDialogOpen = ref(false) +const attributeTargetCode = ref('') +const attributeRows = ref([]) const columns = [ { name: 'select', label: '', field: 'select', align: 'center', sortable: false, style: 'width:44px;', headerStyle: 'width:44px;' }, @@ -300,24 +450,70 @@ const selectedVisibleCount = computed(() => visibleRowKeys.value.filter(k => !!s const allSelectedVisible = computed(() => visibleRowKeys.value.length > 0 && selectedVisibleCount.value === visibleRowKeys.value.length) const someSelectedVisible = computed(() => selectedVisibleCount.value > 0) -function onSelectProduct (row, code) { - productSearch.value = '' - onNewItemChange(row, code) +function applyNewItemVisualState (row, source = 'typed') { + const info = store.classifyItemCode(row?.NewItemCode || '') + row.NewItemCode = info.normalized + row.NewItemMode = info.mode + row.NewItemSource = info.mode === 'empty' ? '' : source } -function onNewItemChange (row, val) { +function newItemInputClass (row) { + return { + 'new-item-existing': row?.NewItemMode === 'existing', + 'new-item-new': row?.NewItemMode === 'new' + } +} + +function newItemBadgeColor (row) { + return row?.NewItemMode === 'existing' ? 'positive' : 'warning' +} + +function newItemBadgeLabel (row) { + return row?.NewItemMode === 'existing' ? 'MEVCUT KOD' : 'YENI KOD' +} + +function newItemHintText (row) { + if (row?.NewItemMode === 'existing') { + return row?.NewItemSource === 'selected' + ? 'Urun listesinden secildi' + : 'Elle girildi (sistemde bulundu)' + } + if (row?.NewItemMode === 'new') { + return store.getCdItemDraft(row?.NewItemCode) ? 'Yeni kod: cdItem taslagi hazir' : 'Yeni kod: cdItem taslagi gerekli' + } + return '' +} + +function onSelectProduct (row, code) { + productSearch.value = '' + onNewItemChange(row, code, 'selected') +} + +function onNewItemChange (row, val, source = 'typed') { + const prevCode = String(row?.NewItemCode || '').trim().toUpperCase() const next = String(val || '').trim().toUpperCase() if (next.length > 13) { $q.notify({ type: 'negative', message: 'Model kodu en fazla 13 karakter olabilir.' }) row.NewItemCode = next.slice(0, 13) + applyNewItemVisualState(row, source) + return + } + if (next.length === 13 && !isValidBaggiModelCode(next)) { + $q.notify({ type: 'negative', message: BAGGI_CODE_ERROR }) + row.NewItemCode = prevCode + applyNewItemVisualState(row, source) return } row.NewItemCode = next ? next.toUpperCase() : '' + applyNewItemVisualState(row, source) row.NewColor = '' row.NewDim2 = '' if (row.NewItemCode) { store.fetchColors(row.NewItemCode) } + if (row.NewItemMode === 'new' && isValidBaggiModelCode(row.NewItemCode) && row.NewItemCode !== prevCode) { + openCdItemDialog(row.NewItemCode) + } } function onNewColorChange (row) { @@ -341,7 +537,11 @@ function getSecondColorOptions (row) { const code = row?.NewItemCode || '' const color = row?.NewColor || '' const key = `${code}::${color}` - return store.secondColorOptionsByKey[key] || [] + const list = store.secondColorOptionsByKey[key] || [] + return list.map(c => ({ + ...c, + item_dim2_label: `${c.item_dim2_code} - ${c.color_description || ''}`.trim() + })) } function toggleRowSelection (rowKey, checked) { @@ -360,33 +560,12 @@ function toggleSelectAllVisible (checked) { selectedMap.value = next } -function onCreateColorValue (row, val, done) { - const code = normalizeShortCode(val, 3) - if (!code) { - done(null) - return - } - row.NewColor = code - onNewColorChange(row) - done(code, 'add-unique') -} - -function onCreateSecondColorValue (row, val, done) { - const code = normalizeShortCode(val, 3) - if (!code) { - done(null) - return - } - row.NewDim2 = code - done(code, 'add-unique') -} - function normalizeShortCode (value, maxLen) { return String(value || '').trim().toUpperCase().slice(0, maxLen) } function isValidBaggiModelCode (code) { - return /^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$/.test(code) + return BAGGI_CODE_PATTERN.test(code) } function validateRowInput (row) { @@ -398,7 +577,7 @@ function validateRowInput (row) { if (!newItemCode) return 'Yeni model kodu zorunludur.' if (!isValidBaggiModelCode(newItemCode)) { - return 'Girdiginiz yapi BAGGI kod yapisina uygun degildir. Format: X999-XXX99999' + return BAGGI_CODE_ERROR } if (oldColor && !newColor) return 'Eski kayitta 1. renk oldugu icin yeni 1. renk zorunludur.' if (newColor && newColor.length !== 3) return 'Yeni 1. renk kodu 3 karakter olmalidir.' @@ -437,6 +616,238 @@ function collectLinesFromRows (selectedRows) { return { errMsg: '', lines } } +function createEmptyCdItemDraft (itemCode) { + return { + ItemTypeCode: '1', + ItemCode: String(itemCode || '').trim().toUpperCase(), + ItemDimTypeCode: '', + ProductTypeCode: '', + ProductHierarchyID: '', + UnitOfMeasureCode1: '', + ItemAccountGrCode: '', + ItemTaxGrCode: '', + ItemPaymentPlanGrCode: '', + ItemDiscountGrCode: '', + ItemVendorGrCode: '', + PromotionGroupCode: '', + ProductCollectionGrCode: '', + StorePriceLevelCode: '', + PerceptionOfFashionCode: '', + CommercialRoleCode: '', + StoreCapacityLevelCode: '', + CustomsTariffNumberCode: '', + CompanyCode: '' + } +} + +function lookupOptions (key) { + const list = store.cdItemLookups?.[key] || [] + return list.map(x => { + const code = String(x?.code || '').trim() + const desc = String(x?.description || '').trim() + return { + value: code, + label: desc ? `${code} - ${desc}` : code + } + }) +} + +async function openCdItemDialog (itemCode) { + const code = String(itemCode || '').trim().toUpperCase() + if (!code) return + await store.fetchCdItemLookups() + + cdItemTargetCode.value = code + const existing = store.getCdItemDraft(code) + const draft = createEmptyCdItemDraft(code) + if (existing) { + for (const [k, v] of Object.entries(existing)) { + if (v == null) continue + draft[k] = String(v) + } + } + cdItemDraftForm.value = draft + cdItemDialogOpen.value = true +} + +function normalizeCdItemDraftForPayload (draftRaw) { + const d = draftRaw || {} + const toIntOrNil = (v) => { + const n = Number(v) + return Number.isFinite(n) && n > 0 ? n : null + } + const toStrOrNil = (v) => { + const s = String(v || '').trim() + return s || null + } + return { + ItemTypeCode: toIntOrNil(d.ItemTypeCode) || 1, + ItemCode: String(d.ItemCode || '').trim().toUpperCase(), + ItemDimTypeCode: toIntOrNil(d.ItemDimTypeCode), + ProductTypeCode: toIntOrNil(d.ProductTypeCode), + ProductHierarchyID: toIntOrNil(d.ProductHierarchyID), + UnitOfMeasureCode1: toStrOrNil(d.UnitOfMeasureCode1), + ItemAccountGrCode: toStrOrNil(d.ItemAccountGrCode), + ItemTaxGrCode: toStrOrNil(d.ItemTaxGrCode), + ItemPaymentPlanGrCode: toStrOrNil(d.ItemPaymentPlanGrCode), + ItemDiscountGrCode: toStrOrNil(d.ItemDiscountGrCode), + ItemVendorGrCode: toStrOrNil(d.ItemVendorGrCode), + PromotionGroupCode: toStrOrNil(d.PromotionGroupCode), + ProductCollectionGrCode: toStrOrNil(d.ProductCollectionGrCode), + StorePriceLevelCode: toStrOrNil(d.StorePriceLevelCode), + PerceptionOfFashionCode: toStrOrNil(d.PerceptionOfFashionCode), + CommercialRoleCode: toStrOrNil(d.CommercialRoleCode), + StoreCapacityLevelCode: toStrOrNil(d.StoreCapacityLevelCode), + CustomsTariffNumberCode: toStrOrNil(d.CustomsTariffNumberCode), + CompanyCode: toStrOrNil(d.CompanyCode) + } +} + +function saveCdItemDraft () { + const payload = normalizeCdItemDraftForPayload(cdItemDraftForm.value) + if (!payload.ItemCode) { + $q.notify({ type: 'negative', message: 'ItemCode bos olamaz.' }) + return + } + store.setCdItemDraft(payload.ItemCode, payload) + cdItemDialogOpen.value = false +} + +function createDummyAttributeRows () { + const sharedOptions = [ + { value: 'DAMATLIK', label: 'DAMATLIK - DAMATLIK' }, + { value: 'TAKIM', label: 'TAKIM - TAKIM ELBISE' }, + { value: 'CEKET', label: 'CEKET - CEKET' }, + { value: 'PANTOLON', label: 'PANTOLON - PANTOLON' } + ] + const rows = [{ + AttributeTypeCodeNumber: 1, + TypeLabel: '1-001 Urun Ana Grubu', + AttributeCode: '', + Options: sharedOptions + }] + for (let i = 2; i <= 50; i++) { + const code = String(i).padStart(3, '0') + rows.push({ + AttributeTypeCodeNumber: i, + TypeLabel: `1-${code} Dummy Ozellik ${i}`, + AttributeCode: '', + Options: sharedOptions + }) + } + return rows +} + +function buildAttributeRowsFromLookup (list) { + const grouped = new Map() + for (const it of (list || [])) { + const typeCode = Number(it?.attribute_type_code || 0) + if (!typeCode) continue + if (!grouped.has(typeCode)) { + grouped.set(typeCode, { + typeCode, + typeDesc: String(it?.attribute_type_description || '').trim() || String(typeCode), + options: [] + }) + } + const g = grouped.get(typeCode) + const code = String(it?.attribute_code || '').trim() + const desc = String(it?.attribute_description || '').trim() + g.options.push({ + value: code, + label: `${code} - ${desc || code}` + }) + } + + const rows = [...grouped.values()] + .sort((a, b) => a.typeCode - b.typeCode) + .map(g => ({ + AttributeTypeCodeNumber: g.typeCode, + TypeLabel: `${g.typeCode}-${g.typeDesc}`, + AttributeCode: '', + Options: g.options + })) + return rows +} + +async function openAttributeDialog (itemCode) { + const code = String(itemCode || '').trim().toUpperCase() + if (!code) return + attributeTargetCode.value = code + const existing = store.getProductAttributeDraft(code) + const fetched = await store.fetchProductAttributes(1) + const fromLookup = buildAttributeRowsFromLookup(fetched) + const baseRows = fromLookup.length ? fromLookup : createDummyAttributeRows() + attributeRows.value = Array.isArray(existing) && existing.length + ? JSON.parse(JSON.stringify(existing)) + : baseRows + attributeDialogOpen.value = true +} + +function saveAttributeDraft () { + const code = String(attributeTargetCode.value || '').trim().toUpperCase() + if (!code) return + for (const row of (attributeRows.value || [])) { + const selected = String(row?.AttributeCode || '').trim() + if (!selected) { + $q.notify({ type: 'negative', message: `Urun ozelliklerinde secim zorunlu: ${row?.TypeLabel || ''}` }) + return + } + } + store.setProductAttributeDraft(code, JSON.parse(JSON.stringify(attributeRows.value || []))) + attributeDialogOpen.value = false + $q.notify({ type: 'positive', message: 'Urun ozellikleri taslagi kaydedildi.' }) +} + +function collectProductAttributesFromSelectedRows (selectedRows) { + const codeSet = [...new Set( + (selectedRows || []) + .map(r => String(r?.NewItemCode || '').trim().toUpperCase()) + .filter(Boolean) + )] + const out = [] + + for (const code of codeSet) { + const rows = store.getProductAttributeDraft(code) + if (!Array.isArray(rows) || !rows.length) { + return { errMsg: `${code} icin urun ozellikleri secilmedi`, productAttributes: [] } + } + for (const row of rows) { + const attributeTypeCode = Number(row?.AttributeTypeCodeNumber || 0) + const attributeCode = String(row?.AttributeCode || '').trim() + if (!attributeTypeCode || !attributeCode) { + return { errMsg: `${code} icin urun ozellikleri eksik`, productAttributes: [] } + } + out.push({ + ItemTypeCode: 1, + ItemCode: code, + AttributeTypeCode: attributeTypeCode, + AttributeCode: attributeCode + }) + } + } + return { errMsg: '', productAttributes: out } +} + +function collectCdItemsFromSelectedRows (selectedRows) { + const codes = [...new Set( + (selectedRows || []) + .filter(r => r?.NewItemMode === 'new' && String(r?.NewItemCode || '').trim()) + .map(r => String(r.NewItemCode).trim().toUpperCase()) + )] + if (!codes.length) return { errMsg: '', cdItems: [] } + + const out = [] + for (const code of codes) { + const draft = store.getCdItemDraft(code) + if (!draft) { + return { errMsg: `${code} icin cdItem bilgisi eksik`, cdItems: [] } + } + out.push(normalizeCdItemDraftForPayload(draft)) + } + return { errMsg: '', cdItems: out } +} + function buildMailLineLabelFromRow (row) { const item = String(row?.NewItemCode || row?.OldItemCode || '').trim().toUpperCase() const color1 = String(row?.NewColor || row?.OldColor || '').trim().toUpperCase() @@ -516,14 +927,23 @@ function formatSizes (sizeMap) { function groupItems (items, prevRows = []) { const prevMap = new Map() for (const r of prevRows || []) { - if (r?.RowKey) prevMap.set(r.RowKey, String(r.NewDesc || '').trim()) + if (!r?.RowKey) continue + prevMap.set(r.RowKey, { + NewDesc: String(r.NewDesc || '').trim(), + NewItemCode: String(r.NewItemCode || '').trim().toUpperCase(), + NewColor: String(r.NewColor || '').trim().toUpperCase(), + NewDim2: String(r.NewDim2 || '').trim().toUpperCase(), + NewItemMode: String(r.NewItemMode || '').trim(), + NewItemSource: String(r.NewItemSource || '').trim() + }) } const map = new Map() for (const it of items) { const key = buildGroupKey(it) if (!map.has(key)) { - const prevDesc = prevMap.get(key) || '' + const prev = prevMap.get(key) || {} + const prevDesc = prev.NewDesc || '' const fallbackDesc = String((it?.NewDesc || it?.OldDesc) || '').trim() map.set(key, { RowKey: key, @@ -536,10 +956,12 @@ function groupItems (items, prevRows = []) { OrderLineIDs: [], OldSizes: [], OldSizesLabel: '', - NewItemCode: '', - NewColor: '', - NewDim2: '', + NewItemCode: prev.NewItemCode || '', + NewColor: prev.NewColor || '', + NewDim2: prev.NewDim2 || '', NewDesc: prevDesc || fallbackDesc, + NewItemMode: prev.NewItemMode || 'empty', + NewItemSource: prev.NewItemSource || '', IsVariantMissing: !!it.IsVariantMissing }) } @@ -560,6 +982,10 @@ function groupItems (items, prevRows = []) { const sizes = formatSizes(g.__sizeMap || {}) g.OldSizes = sizes.list g.OldSizesLabel = sizes.label + const info = store.classifyItemCode(g.NewItemCode) + g.NewItemCode = info.normalized + g.NewItemMode = info.mode + if (info.mode === 'empty') g.NewItemSource = '' delete g.__sizeMap out.push(g) } @@ -589,6 +1015,20 @@ async function onBulkSubmit () { $q.notify({ type: 'negative', message: 'Secili satirlarda guncellenecek kayit bulunamadi.' }) 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 } = collectProductAttributesFromSelectedRows(selectedRows) + if (attrErrMsg) { + $q.notify({ type: 'negative', message: attrErrMsg }) + const firstCode = String(attrErrMsg.split(' ')[0] || '').trim() + if (firstCode) openAttributeDialog(firstCode) + return + } try { const validate = await store.validateUpdates(orderHeaderID.value, lines) @@ -604,7 +1044,7 @@ async function onBulkSubmit () { ok: { label: 'Ekle ve Guncelle', color: 'primary' }, cancel: { label: 'Vazgec', flat: true } }).onOk(async () => { - await store.applyUpdates(orderHeaderID.value, lines, true) + await store.applyUpdates(orderHeaderID.value, lines, true, cdItems, productAttributes) await store.fetchItems(orderHeaderID.value) selectedMap.value = {} await sendUpdateMailAfterApply(selectedRows) @@ -612,7 +1052,7 @@ async function onBulkSubmit () { return } - await store.applyUpdates(orderHeaderID.value, lines, false) + await store.applyUpdates(orderHeaderID.value, lines, false, cdItems, productAttributes) await store.fetchItems(orderHeaderID.value) selectedMap.value = {} await sendUpdateMailAfterApply(selectedRows) @@ -738,6 +1178,16 @@ async function onBulkSubmit () { background: #e3f3ff; } +.prod-table :deep(.new-item-existing .q-field__control) { + background: #eaf9ef !important; + border-left: 3px solid #21ba45; +} + +.prod-table :deep(.new-item-new .q-field__control) { + background: #fff5e9 !important; + border-left: 3px solid #f2a100; +} + .prod-table :deep(td.col-desc), .prod-table :deep(th.col-desc), .prod-table :deep(td.col-wrap), diff --git a/ui/src/stores/OrderProductionItemStore.js b/ui/src/stores/OrderProductionItemStore.js index 575736e..16b17ef 100644 --- a/ui/src/stores/OrderProductionItemStore.js +++ b/ui/src/stores/OrderProductionItemStore.js @@ -2,40 +2,6 @@ import { defineStore } from 'pinia' import api from 'src/services/api' -function normalizeTextForMatch (v) { - return String(v || '') - .trim() - .toUpperCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') -} - -// Production ekranlari icin beden grup tespiti helper'i. -// Ozel kural: -// YETISKIN/GARSON = GARSON ve URUN ANA GRUBU "GOMLEK ATA YAKA" veya "GOMLEK KLASIK" ise => yas -export function detectProductionBedenGroup (bedenList, urunAnaGrubu = '', urunKategori = '', yetiskinGarson = '') { - const list = Array.isArray(bedenList) ? bedenList : [] - const hasLetterSizes = list - .map(v => String(v || '').trim().toUpperCase()) - .some(v => ['XS', 'S', 'M', 'L', 'XL', '2XL', '3XL', '4XL', '5XL', '6XL', '7XL'].includes(v)) - - const ana = normalizeTextForMatch(urunAnaGrubu) - const kat = normalizeTextForMatch(urunKategori) - const yg = normalizeTextForMatch(yetiskinGarson) - - if ((kat.includes('GARSON') || yg.includes('GARSON')) && - (ana.includes('GOMLEK ATAYAKA') || ana.includes('GOMLEK ATA YAKA') || ana.includes('GOMLEK KLASIK'))) { - return 'yas' - } - - if (hasLetterSizes) return 'gom' - if ((ana.includes('AYAKKABI') || kat.includes('AYAKKABI')) && (kat.includes('GARSON') || yg.includes('GARSON'))) return 'ayk_garson' - if (kat.includes('GARSON') || yg.includes('GARSON') || ana.includes('GARSON')) return 'yas' - if (ana.includes('PANTOLON') && kat.includes('YETISKIN')) return 'pan' - if (ana.includes('AKSESUAR')) return 'aksbir' - return 'tak' -} - function extractApiErrorMessage (err, fallback) { const data = err?.response?.data if (typeof data === 'string' && data.trim()) return data @@ -70,12 +36,40 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', { products: [], colorOptionsByCode: {}, secondColorOptionsByKey: {}, + productAttributesByItemType: {}, + cdItemLookups: null, + cdItemDraftsByCode: {}, + productAttributeDraftsByCode: {}, loading: false, saving: false, error: null }), + getters: { + productCodeSet (state) { + const set = new Set() + for (const p of (state.products || [])) { + const code = String(p?.ProductCode || '').trim().toUpperCase() + if (code) set.add(code) + } + return set + } + }, + actions: { + classifyItemCode (value) { + const normalized = String(value || '').trim().toUpperCase() + if (!normalized) { + return { normalized: '', mode: 'empty', exists: false } + } + const exists = this.productCodeSet.has(normalized) + return { + normalized, + mode: exists ? 'existing' : 'new', + exists + } + }, + async fetchHeader (orderHeaderID) { if (!orderHeaderID) { this.header = null @@ -166,6 +160,62 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', { return [] } }, + async fetchProductAttributes (itemTypeCode = 1) { + const key = String(itemTypeCode || 1) + if (this.productAttributesByItemType[key]) { + return this.productAttributesByItemType[key] + } + try { + const res = await api.get('/product-attributes', { params: { itemTypeCode } }) + const list = Array.isArray(res?.data) ? res.data : [] + this.productAttributesByItemType[key] = list + return list + } catch (err) { + this.error = err?.response?.data || err?.message || 'Urun ozellikleri alinamadi' + return [] + } + }, + async fetchCdItemLookups (force = false) { + if (this.cdItemLookups && !force) return this.cdItemLookups + try { + const res = await api.get('/orders/production-items/cditem-lookups') + this.cdItemLookups = res?.data || null + return this.cdItemLookups + } catch (err) { + this.error = err?.response?.data || err?.message || 'cdItem lookup listesi alinamadi' + return null + } + }, + setCdItemDraft (itemCode, draft) { + const code = String(itemCode || '').trim().toUpperCase() + if (!code) return + this.cdItemDraftsByCode = { + ...this.cdItemDraftsByCode, + [code]: { + ...(draft || {}), + ItemCode: code, + ItemTypeCode: Number(draft?.ItemTypeCode || 1) + } + } + }, + getCdItemDraft (itemCode) { + const code = String(itemCode || '').trim().toUpperCase() + if (!code) return null + return this.cdItemDraftsByCode[code] || null + }, + setProductAttributeDraft (itemCode, rows) { + const code = String(itemCode || '').trim().toUpperCase() + if (!code) return + this.productAttributeDraftsByCode = { + ...this.productAttributeDraftsByCode, + [code]: Array.isArray(rows) ? rows : [] + } + }, + getProductAttributeDraft (itemCode) { + const code = String(itemCode || '').trim().toUpperCase() + if (!code) return [] + return this.productAttributeDraftsByCode[code] || [] + }, async validateUpdates (orderHeaderID, lines) { if (!orderHeaderID) return { missingCount: 0, missing: [] } @@ -186,7 +236,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', { this.saving = false } }, - async applyUpdates (orderHeaderID, lines, insertMissing) { + async applyUpdates (orderHeaderID, lines, insertMissing, cdItems = [], productAttributes = []) { if (!orderHeaderID) return { updated: 0, inserted: 0 } this.saving = true @@ -195,7 +245,7 @@ export const useOrderProductionItemStore = defineStore('orderproductionitems', { try { const res = await api.post( `/orders/production-items/${encodeURIComponent(orderHeaderID)}/apply`, - { lines, insertMissing } + { lines, insertMissing, cdItems, productAttributes } ) return res?.data || { updated: 0, inserted: 0 } } catch (err) { diff --git a/ui/src/stores/orderentryStore.js b/ui/src/stores/orderentryStore.js index c9e157e..999a380 100644 --- a/ui/src/stores/orderentryStore.js +++ b/ui/src/stores/orderentryStore.js @@ -42,20 +42,21 @@ export function buildComboKey(row, beden) { -export const BEDEN_SCHEMA = [ - { key: 'tak', title: 'TAKIM ELBISE', values: ['44','46','48','50','52','54','56','58','60','62','64','66','68','70','72','74'] }, - { key: 'ayk', title: 'AYAKKABI', values: ['39','40','41','42','43','44','45'] }, - { key: 'ayk_garson', title: 'AYAKKABI GARSON', values: ['22','23','24','25','26','27','28','29','30','31','32','33','34','35','STD'] }, - { key: 'yas', title: 'YAS', values: ['2','4','6','8','10','12','14'] }, - { key: 'pan', title: 'PANTOLON', values: ['38','40','42','44','46','48','50','52','54','56','58','60','62','64','66','68'] }, - { key: 'gom', title: 'GOMLEK', values: ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL'] }, - { key: 'aksbir', title: 'AKSESUAR', values: [' ', '44', 'STD', '110', '115', '120', '125', '130', '135'] } -] +const SIZE_GROUP_TITLES = { + tak: 'TAKIM ELBISE', + ayk: 'AYAKKABI', + ayk_garson: 'AYAKKABI GARSON', + yas: 'YAS', + pan: 'PANTOLON', + gom: 'GOMLEK', + aksbir: 'AKSESUAR' +} -export const schemaByKey = BEDEN_SCHEMA.reduce((m, g) => { - m[g.key] = g - return m -}, {}) +const FALLBACK_SCHEMA_MAP = { + tak: { key: 'tak', title: 'TAKIM ELBISE', values: ['44', '46', '48', '50', '52', '54', '56', '58', '60', '62', '64', '66', '68', '70', '72', '74'] } +} + +export const schemaByKey = { ...FALLBACK_SCHEMA_MAP } const productSizeMatchCache = { loaded: false, @@ -111,6 +112,23 @@ function setProductSizeMatchCache(payload) { productSizeMatchCache.schemas = normalizedSchemas } +function buildSchemaMapFromCacheSchemas() { + const out = {} + const src = productSizeMatchCache.schemas || {} + for (const [keyRaw, valuesRaw] of Object.entries(src)) { + const key = String(keyRaw || '').trim() + if (!key) continue + const values = Array.isArray(valuesRaw) ? valuesRaw : [] + out[key] = { + key, + title: SIZE_GROUP_TITLES[key] || key.toUpperCase(), + values: values.map(v => String(v == null ? '' : v)) + } + } + if (!out.tak) out.tak = { ...FALLBACK_SCHEMA_MAP.tak } + return out +} + export const stockMap = ref({}) export const bedenStock = ref([]) @@ -257,25 +275,17 @@ export const useOrderEntryStore = defineStore('orderentry', { , /* =========================================================== 🧩 initSchemaMap — BEDEN ŞEMA İNİT - - TEK SOURCE OF TRUTH: BEDEN_SCHEMA + - TEK SOURCE OF TRUTH: SQL mk_size_group (cache) =========================================================== */ initSchemaMap() { if (this.schemaMap && Object.keys(this.schemaMap).length > 0) { return } - - const map = {} - - for (const g of BEDEN_SCHEMA) { - map[g.key] = { - key: g.key, - title: g.title, - values: [...g.values] - } + this.schemaMap = buildSchemaMapFromCacheSchemas() + if (!Object.keys(this.schemaMap).length) { + this.schemaMap = { ...FALLBACK_SCHEMA_MAP } } - this.schemaMap = map - console.log( '🧩 schemaMap INIT edildi:', Object.keys(this.schemaMap) @@ -284,17 +294,20 @@ export const useOrderEntryStore = defineStore('orderentry', { async ensureProductSizeMatchRules($q = null, force = false) { if (!force && productSizeMatchCache.loaded && productSizeMatchCache.rules.length > 0) { + this.schemaMap = buildSchemaMapFromCacheSchemas() return true } try { const res = await api.get('/product-size-match/rules') setProductSizeMatchCache(res?.data || {}) + this.schemaMap = buildSchemaMapFromCacheSchemas() return true } catch (err) { if (force) { resetProductSizeMatchCache() } + this.schemaMap = { ...FALLBACK_SCHEMA_MAP } console.warn('⚠ product-size-match rules alınamadı:', err) $q?.notify?.({ type: 'warning', @@ -4044,6 +4057,8 @@ export function toSummaryRowFromForm(form) { urunAnaGrubu: form.urunAnaGrubu || '', urunAltGrubu: form.urunAltGrubu || '', + kategori: form.kategori || '', + yetiskinGarson: form.yetiskinGarson || form.askiliyan || '', aciklama: form.aciklama || '', fiyat: Number(form.fiyat || 0),