diff --git a/scripts/sql/product_filter_tr_cache_refresh.sql b/scripts/sql/product_filter_tr_cache_refresh.sql new file mode 100644 index 0000000..dda25aa --- /dev/null +++ b/scripts/sql/product_filter_tr_cache_refresh.sql @@ -0,0 +1,87 @@ +/* +Product filter cache refresh for Product Stock By Attributes endpoints. +This cache is used by backend queries when dbo.ProductFilterTRCache exists. +*/ + +USE BAGGI_V3; +GO + +IF OBJECT_ID('dbo.ProductFilterTRCache','U') IS NULL +BEGIN + CREATE TABLE dbo.ProductFilterTRCache + ( + ProductCode NVARCHAR(50) NOT NULL PRIMARY KEY, + ProductDescription NVARCHAR(255) NULL, + ProductAtt01Desc NVARCHAR(255) NULL, + ProductAtt02Desc NVARCHAR(255) NULL, + ProductAtt11Desc NVARCHAR(255) NULL, + ProductAtt38Desc NVARCHAR(255) NULL, + ProductAtt41Desc NVARCHAR(255) NULL, + ProductAtt44Desc NVARCHAR(255) NULL + ); +END +GO + +TRUNCATE TABLE dbo.ProductFilterTRCache; +GO + +INSERT INTO dbo.ProductFilterTRCache +( + ProductCode, + ProductDescription, + ProductAtt01Desc, + ProductAtt02Desc, + ProductAtt11Desc, + ProductAtt38Desc, + ProductAtt41Desc, + ProductAtt44Desc +) +SELECT + ProductCode, + ProductDescription, + ProductAtt01Desc, + ProductAtt02Desc, + ProductAtt11Desc, + ProductAtt38Desc, + ProductAtt41Desc, + ProductAtt44Desc +FROM ProductFilterWithDescription('TR') +WHERE LEN(ProductCode) = 13; +GO + +IF EXISTS ( + SELECT 1 FROM sys.indexes + WHERE name = 'IX_ProductFilterTRCache_Filter' + AND object_id = OBJECT_ID('dbo.ProductFilterTRCache') +) +BEGIN + DROP INDEX IX_ProductFilterTRCache_Filter ON dbo.ProductFilterTRCache; +END +GO + +IF NOT EXISTS ( + SELECT 1 FROM sys.indexes + WHERE name = 'IX_ProductFilterTRCache_KatAna' + AND object_id = OBJECT_ID('dbo.ProductFilterTRCache') +) +BEGIN + CREATE NONCLUSTERED INDEX IX_ProductFilterTRCache_KatAna + ON dbo.ProductFilterTRCache (ProductAtt44Desc, ProductAtt01Desc, ProductCode) + INCLUDE (ProductDescription, ProductAtt02Desc, ProductAtt41Desc, ProductAtt38Desc, ProductAtt11Desc); +END +GO + +IF NOT EXISTS ( + SELECT 1 FROM sys.indexes + WHERE name = 'IX_ProductFilterTRCache_KatAnaAlt' + AND object_id = OBJECT_ID('dbo.ProductFilterTRCache') +) +BEGIN + CREATE NONCLUSTERED INDEX IX_ProductFilterTRCache_KatAnaAlt + ON dbo.ProductFilterTRCache (ProductAtt44Desc, ProductAtt01Desc, ProductAtt02Desc, ProductCode) + INCLUDE (ProductDescription, ProductAtt41Desc, ProductAtt38Desc, ProductAtt11Desc); +END +GO + +UPDATE STATISTICS dbo.ProductFilterTRCache WITH FULLSCAN; +GO diff --git a/scripts/sql/product_stock_by_attributes_indexes.sql b/scripts/sql/product_stock_by_attributes_indexes.sql new file mode 100644 index 0000000..15f8589 --- /dev/null +++ b/scripts/sql/product_stock_by_attributes_indexes.sql @@ -0,0 +1,74 @@ +/* +Performance indexes for Product Stock By Attributes queries. +Target: SQL Server +*/ + +/* trStock (inventory aggregation) */ +IF NOT EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE name = 'IX_trStock_Item_Warehouse_Dims' + AND object_id = OBJECT_ID('dbo.trStock') +) +BEGIN + CREATE NONCLUSTERED INDEX IX_trStock_Item_Warehouse_Dims + ON dbo.trStock (ItemTypeCode, ItemCode, WarehouseCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code) + INCLUDE (In_Qty1, Out_Qty1, CompanyCode, OfficeCode, StoreTypeCode, StoreCode); +END; +GO + +/* PickingStates */ +IF NOT EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE name = 'IX_PickingStates_Item_Warehouse_Dims' + AND object_id = OBJECT_ID('dbo.PickingStates') +) +BEGIN + CREATE NONCLUSTERED INDEX IX_PickingStates_Item_Warehouse_Dims + ON dbo.PickingStates (ItemTypeCode, ItemCode, WarehouseCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code) + INCLUDE (Qty1, CompanyCode, OfficeCode, StoreTypeCode, StoreCode); +END; +GO + +/* ReserveStates */ +IF NOT EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE name = 'IX_ReserveStates_Item_Warehouse_Dims' + AND object_id = OBJECT_ID('dbo.ReserveStates') +) +BEGIN + CREATE NONCLUSTERED INDEX IX_ReserveStates_Item_Warehouse_Dims + ON dbo.ReserveStates (ItemTypeCode, ItemCode, WarehouseCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code) + INCLUDE (Qty1, CompanyCode, OfficeCode, StoreTypeCode, StoreCode); +END; +GO + +/* DispOrderStates */ +IF NOT EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE name = 'IX_DispOrderStates_Item_Warehouse_Dims' + AND object_id = OBJECT_ID('dbo.DispOrderStates') +) +BEGIN + CREATE NONCLUSTERED INDEX IX_DispOrderStates_Item_Warehouse_Dims + ON dbo.DispOrderStates (ItemTypeCode, ItemCode, WarehouseCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code) + INCLUDE (Qty1, CompanyCode, OfficeCode, StoreTypeCode, StoreCode); +END; +GO + +/* Latest price lookup */ +IF NOT EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE name = 'IX_prItemBasePrice_ItemType_ItemCode_PriceDate' + AND object_id = OBJECT_ID('dbo.prItemBasePrice') +) +BEGIN + CREATE NONCLUSTERED INDEX IX_prItemBasePrice_ItemType_ItemCode_PriceDate + ON dbo.prItemBasePrice (ItemTypeCode, ItemCode, PriceDate DESC) + INCLUDE (Price); +END; +GO diff --git a/svc/queries/productstockquery.go b/svc/queries/productstockquery.go index 8504bab..282f873 100644 --- a/svc/queries/productstockquery.go +++ b/svc/queries/productstockquery.go @@ -132,6 +132,7 @@ SELECT P.ProductAtt38Desc AS BIRINCI_PARCA_FIT, P.ProductAtt39Desc AS IKINCI_PARCA_FIT, P.ProductAtt40Desc AS BOS2, + P.ProductAtt41Desc AS URUN_ICERIGI, P.ProductAtt41Desc AS KISA_KAR, P.ProductAtt42Desc AS SERI_FASON, P.ProductAtt43Desc AS STOK_GIRIS_YONTEMI, diff --git a/svc/queries/productstockquery_by_attributes.go b/svc/queries/productstockquery_by_attributes.go index a8d072f..ce304f3 100644 --- a/svc/queries/productstockquery_by_attributes.go +++ b/svc/queries/productstockquery_by_attributes.go @@ -13,7 +13,7 @@ DECLARE @Fit NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p7)), ''); DECLARE @Drop NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p8)), ''); DECLARE @Beden NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p9)), ''); -DECLARE @AttrBase TABLE +CREATE TABLE #AttrBase ( ProductCode NVARCHAR(50) NOT NULL, Kategori NVARCHAR(100) NOT NULL, @@ -24,42 +24,69 @@ DECLARE @AttrBase TABLE DropVal NVARCHAR(100) NOT NULL ); -INSERT INTO @AttrBase (ProductCode, Kategori, UrunAnaGrubu, UrunAltGrubu, UrunIcerigi, Fit, DropVal) -SELECT - ProductCode, - Kategori = LTRIM(RTRIM(ProductAtt44Desc)), - UrunAnaGrubu = LTRIM(RTRIM(ProductAtt01Desc)), - UrunAltGrubu = LTRIM(RTRIM(ProductAtt02Desc)), - UrunIcerigi = LTRIM(RTRIM(ProductAtt41Desc)), - Fit = LTRIM(RTRIM(ProductAtt38Desc)), - DropVal = LTRIM(RTRIM(ProductAtt11Desc)) -FROM ProductFilterWithDescription('TR') -WHERE LEN(ProductCode) = 13 - AND (@Kategori IS NULL OR ProductAtt44Desc = @Kategori) - AND (@UrunAnaGrubu IS NULL OR ProductAtt01Desc = @UrunAnaGrubu) - AND (@UrunAltGrubu IS NULL OR ProductAtt02Desc = @UrunAltGrubu) - AND (@UrunIcerigi IS NULL OR ProductAtt41Desc = @UrunIcerigi) - AND (@Fit IS NULL OR ProductAtt38Desc = @Fit) - AND (@Drop IS NULL OR ProductAtt11Desc = @Drop); +IF OBJECT_ID('dbo.ProductFilterTRCache','U') IS NOT NULL +BEGIN + INSERT INTO #AttrBase (ProductCode, Kategori, UrunAnaGrubu, UrunAltGrubu, UrunIcerigi, Fit, DropVal) + SELECT + ProductCode, + Kategori = LTRIM(RTRIM(ProductAtt44Desc)), + UrunAnaGrubu = LTRIM(RTRIM(ProductAtt01Desc)), + UrunAltGrubu = LTRIM(RTRIM(ProductAtt02Desc)), + UrunIcerigi = LTRIM(RTRIM(ProductAtt41Desc)), + Fit = LTRIM(RTRIM(ProductAtt38Desc)), + DropVal = LTRIM(RTRIM(ProductAtt11Desc)) + FROM dbo.ProductFilterTRCache + WHERE LEN(ProductCode) = 13 + AND (@Kategori IS NULL OR ProductAtt44Desc = @Kategori) + AND (@UrunAnaGrubu IS NULL OR ProductAtt01Desc = @UrunAnaGrubu) + AND (@UrunAltGrubu IS NULL OR ProductAtt02Desc = @UrunAltGrubu) + AND (@UrunIcerigi IS NULL OR ProductAtt41Desc = @UrunIcerigi) + AND (@Fit IS NULL OR ProductAtt38Desc = @Fit) + AND (@Drop IS NULL OR ProductAtt11Desc = @Drop); +END +ELSE +BEGIN + INSERT INTO #AttrBase (ProductCode, Kategori, UrunAnaGrubu, UrunAltGrubu, UrunIcerigi, Fit, DropVal) + SELECT + ProductCode, + Kategori = LTRIM(RTRIM(ProductAtt44Desc)), + UrunAnaGrubu = LTRIM(RTRIM(ProductAtt01Desc)), + UrunAltGrubu = LTRIM(RTRIM(ProductAtt02Desc)), + UrunIcerigi = LTRIM(RTRIM(ProductAtt41Desc)), + Fit = LTRIM(RTRIM(ProductAtt38Desc)), + DropVal = LTRIM(RTRIM(ProductAtt11Desc)) + FROM ProductFilterWithDescription('TR') + WHERE LEN(ProductCode) = 13 + AND (@Kategori IS NULL OR ProductAtt44Desc = @Kategori) + AND (@UrunAnaGrubu IS NULL OR ProductAtt01Desc = @UrunAnaGrubu) + AND (@UrunAltGrubu IS NULL OR ProductAtt02Desc = @UrunAltGrubu) + AND (@UrunIcerigi IS NULL OR ProductAtt41Desc = @UrunIcerigi) + AND (@Fit IS NULL OR ProductAtt38Desc = @Fit) + AND (@Drop IS NULL OR ProductAtt11Desc = @Drop); +END; IF @Kategori IS NULL OR @UrunAnaGrubu IS NULL BEGIN + CREATE CLUSTERED INDEX IX_AttrBase_ProductCode ON #AttrBase(ProductCode); SELECT 'kategori' AS FieldName, X.FieldValue FROM ( SELECT DISTINCT FieldValue = AB.Kategori - FROM @AttrBase AB + FROM #AttrBase AB WHERE AB.Kategori <> '' ) X UNION ALL SELECT 'urun_ana_grubu', X.FieldValue FROM ( SELECT DISTINCT FieldValue = AB.UrunAnaGrubu - FROM @AttrBase AB + FROM #AttrBase AB WHERE AB.UrunAnaGrubu <> '' ) X; RETURN; END; +CREATE CLUSTERED INDEX IX_AttrBase_ProductCode ON #AttrBase(ProductCode); +CREATE NONCLUSTERED INDEX IX_AttrBase_Filter ON #AttrBase(Kategori, UrunAnaGrubu, UrunAltGrubu, UrunIcerigi, Fit, DropVal); + ;WITH INV AS ( SELECT @@ -77,7 +104,7 @@ END; P.ItemCode, P.ColorCode, P.ItemDim1Code, P.ItemDim2Code, P.Qty1 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1, 0 AS InventoryQty1 FROM PickingStates P - INNER JOIN @AttrBase AB ON AB.ProductCode = P.ItemCode + INNER JOIN #AttrBase AB ON AB.ProductCode = P.ItemCode WHERE P.ItemTypeCode = 1 AND LEN(P.ItemCode) = 13 @@ -86,7 +113,7 @@ END; R.ItemCode, R.ColorCode, R.ItemDim1Code, R.ItemDim2Code, 0, R.Qty1, 0, 0 FROM ReserveStates R - INNER JOIN @AttrBase AB ON AB.ProductCode = R.ItemCode + INNER JOIN #AttrBase AB ON AB.ProductCode = R.ItemCode WHERE R.ItemTypeCode = 1 AND LEN(R.ItemCode) = 13 @@ -95,7 +122,7 @@ END; D.ItemCode, D.ColorCode, D.ItemDim1Code, D.ItemDim2Code, 0, 0, D.Qty1, 0 FROM DispOrderStates D - INNER JOIN @AttrBase AB ON AB.ProductCode = D.ItemCode + INNER JOIN #AttrBase AB ON AB.ProductCode = D.ItemCode WHERE D.ItemTypeCode = 1 AND LEN(D.ItemCode) = 13 @@ -104,7 +131,7 @@ END; T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, 0, 0, 0, SUM(T.In_Qty1 - T.Out_Qty1) FROM trStock T WITH (NOLOCK) - INNER JOIN @AttrBase AB ON AB.ProductCode = T.ItemCode + INNER JOIN #AttrBase AB ON AB.ProductCode = T.ItemCode WHERE T.ItemTypeCode = 1 AND LEN(T.ItemCode) = 13 GROUP BY T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code @@ -129,21 +156,21 @@ Avail AS SELECT 'kategori' AS FieldName, X.FieldValue FROM ( SELECT DISTINCT FieldValue = AB.Kategori - FROM @AttrBase AB + FROM #AttrBase AB WHERE AB.Kategori <> '' ) X UNION ALL SELECT 'urun_ana_grubu', X.FieldValue FROM ( SELECT DISTINCT FieldValue = AB.UrunAnaGrubu - FROM @AttrBase AB + FROM #AttrBase AB WHERE AB.UrunAnaGrubu <> '' ) X UNION ALL SELECT 'urun_alt_grubu', X.FieldValue FROM ( SELECT DISTINCT FieldValue = AB.UrunAltGrubu - FROM @AttrBase AB + FROM #AttrBase AB WHERE AB.UrunAltGrubu <> '' AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) @@ -153,7 +180,7 @@ SELECT 'renk', X.FieldValue FROM ( SELECT DISTINCT FieldValue = CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.Renk END FROM Avail A - INNER JOIN @AttrBase AB ON AB.ProductCode = A.ItemCode + INNER JOIN #AttrBase AB ON AB.ProductCode = A.ItemCode WHERE (CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.Renk END) <> '' AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) @@ -169,7 +196,7 @@ SELECT 'renk2', X.FieldValue FROM ( SELECT DISTINCT FieldValue = A.Renk2 FROM Avail A - INNER JOIN @AttrBase AB ON AB.ProductCode = A.ItemCode + INNER JOIN #AttrBase AB ON AB.ProductCode = A.ItemCode WHERE A.Renk2 <> '' AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) @@ -184,7 +211,7 @@ UNION ALL SELECT 'urun_icerigi', X.FieldValue FROM ( SELECT DISTINCT FieldValue = AB.UrunIcerigi - FROM @AttrBase AB + FROM #AttrBase AB WHERE AB.UrunIcerigi <> '' AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) @@ -193,7 +220,7 @@ UNION ALL SELECT 'fit', X.FieldValue FROM ( SELECT DISTINCT FieldValue = AB.Fit - FROM @AttrBase AB + FROM #AttrBase AB WHERE AB.Fit <> '' AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) @@ -202,7 +229,7 @@ UNION ALL SELECT 'drop', X.FieldValue FROM ( SELECT DISTINCT FieldValue = AB.DropVal - FROM @AttrBase AB + FROM #AttrBase AB WHERE AB.DropVal <> '' AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) @@ -212,7 +239,7 @@ SELECT 'beden', X.FieldValue FROM ( SELECT DISTINCT FieldValue = A.Beden FROM Avail A - INNER JOIN @AttrBase AB ON AB.ProductCode = A.ItemCode + INNER JOIN #AttrBase AB ON AB.ProductCode = A.ItemCode WHERE A.Beden <> '' AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) @@ -222,7 +249,8 @@ FROM ( AND (@Drop IS NULL OR AB.DropVal = @Drop) AND (@Renk IS NULL OR (CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.Renk END) = @Renk) AND (@Renk2 IS NULL OR A.Renk2 = @Renk2) -) X; +) X +OPTION (RECOMPILE); ` // GetProductStockQueryByAttributes: @@ -238,8 +266,158 @@ DECLARE @Fit NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p7)), ''); DECLARE @Drop NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p8)), ''); DECLARE @Beden NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p9)), ''); -;WITH AttrFiltered AS +CREATE TABLE #AttrFiltered ( + ProductCode NVARCHAR(50) NOT NULL, + ProductDescription NVARCHAR(255) NULL, + ProductAtt01Desc NVARCHAR(255) NULL, + ProductAtt02Desc NVARCHAR(255) NULL, + ProductAtt10Desc NVARCHAR(255) NULL, + ProductAtt11Desc NVARCHAR(255) NULL, + ProductAtt21Desc NVARCHAR(255) NULL, + ProductAtt22Desc NVARCHAR(255) NULL, + ProductAtt23Desc NVARCHAR(255) NULL, + ProductAtt24Desc NVARCHAR(255) NULL, + ProductAtt25Desc NVARCHAR(255) NULL, + ProductAtt26Desc NVARCHAR(255) NULL, + ProductAtt27Desc NVARCHAR(255) NULL, + ProductAtt28Desc NVARCHAR(255) NULL, + ProductAtt29Desc NVARCHAR(255) NULL, + ProductAtt30Desc NVARCHAR(255) NULL, + ProductAtt31Desc NVARCHAR(255) NULL, + ProductAtt32Desc NVARCHAR(255) NULL, + ProductAtt33Desc NVARCHAR(255) NULL, + ProductAtt34Desc NVARCHAR(255) NULL, + ProductAtt35Desc NVARCHAR(255) NULL, + ProductAtt36Desc NVARCHAR(255) NULL, + ProductAtt37Desc NVARCHAR(255) NULL, + ProductAtt38Desc NVARCHAR(255) NULL, + ProductAtt39Desc NVARCHAR(255) NULL, + ProductAtt40Desc NVARCHAR(255) NULL, + ProductAtt41Desc NVARCHAR(255) NULL, + ProductAtt42Desc NVARCHAR(255) NULL, + ProductAtt43Desc NVARCHAR(255) NULL, + ProductAtt44Desc NVARCHAR(255) NULL, + ProductAtt45Desc NVARCHAR(255) NULL, + ProductAtt46Desc NVARCHAR(255) NULL +); + +IF OBJECT_ID('dbo.ProductFilterTRCache','U') IS NOT NULL +BEGIN + INSERT INTO #AttrFiltered + ( + ProductCode, + ProductDescription, + ProductAtt01Desc, + ProductAtt02Desc, + ProductAtt10Desc, + ProductAtt11Desc, + ProductAtt21Desc, + ProductAtt22Desc, + ProductAtt23Desc, + ProductAtt24Desc, + ProductAtt25Desc, + ProductAtt26Desc, + ProductAtt27Desc, + ProductAtt28Desc, + ProductAtt29Desc, + ProductAtt30Desc, + ProductAtt31Desc, + ProductAtt32Desc, + ProductAtt33Desc, + ProductAtt34Desc, + ProductAtt35Desc, + ProductAtt36Desc, + ProductAtt37Desc, + ProductAtt38Desc, + ProductAtt39Desc, + ProductAtt40Desc, + ProductAtt41Desc, + ProductAtt42Desc, + ProductAtt43Desc, + ProductAtt44Desc, + ProductAtt45Desc, + ProductAtt46Desc + ) + SELECT + C.ProductCode, + C.ProductDescription, + C.ProductAtt01Desc, + C.ProductAtt02Desc, + '' AS ProductAtt10Desc, + C.ProductAtt11Desc, + '' AS ProductAtt21Desc, + '' AS ProductAtt22Desc, + '' AS ProductAtt23Desc, + '' AS ProductAtt24Desc, + '' AS ProductAtt25Desc, + '' AS ProductAtt26Desc, + '' AS ProductAtt27Desc, + '' AS ProductAtt28Desc, + '' AS ProductAtt29Desc, + '' AS ProductAtt30Desc, + '' AS ProductAtt31Desc, + '' AS ProductAtt32Desc, + '' AS ProductAtt33Desc, + '' AS ProductAtt34Desc, + '' AS ProductAtt35Desc, + '' AS ProductAtt36Desc, + '' AS ProductAtt37Desc, + C.ProductAtt38Desc, + '' AS ProductAtt39Desc, + '' AS ProductAtt40Desc, + C.ProductAtt41Desc, + '' AS ProductAtt42Desc, + '' AS ProductAtt43Desc, + C.ProductAtt44Desc, + '' AS ProductAtt45Desc, + '' AS ProductAtt46Desc + FROM dbo.ProductFilterTRCache C + WHERE LEN(C.ProductCode) = 13 + AND (@Kategori IS NULL OR C.ProductAtt44Desc = @Kategori) + AND (@UrunAnaGrubu IS NULL OR C.ProductAtt01Desc = @UrunAnaGrubu) + AND (@UrunAltGrubu IS NULL OR C.ProductAtt02Desc = @UrunAltGrubu) + AND (@UrunIcerigi IS NULL OR C.ProductAtt41Desc = @UrunIcerigi) + AND (@Fit IS NULL OR C.ProductAtt38Desc = @Fit) + AND (@Drop IS NULL OR C.ProductAtt11Desc = @Drop); +END +ELSE +BEGIN + INSERT INTO #AttrFiltered + ( + ProductCode, + ProductDescription, + ProductAtt01Desc, + ProductAtt02Desc, + ProductAtt10Desc, + ProductAtt11Desc, + ProductAtt21Desc, + ProductAtt22Desc, + ProductAtt23Desc, + ProductAtt24Desc, + ProductAtt25Desc, + ProductAtt26Desc, + ProductAtt27Desc, + ProductAtt28Desc, + ProductAtt29Desc, + ProductAtt30Desc, + ProductAtt31Desc, + ProductAtt32Desc, + ProductAtt33Desc, + ProductAtt34Desc, + ProductAtt35Desc, + ProductAtt36Desc, + ProductAtt37Desc, + ProductAtt38Desc, + ProductAtt39Desc, + ProductAtt40Desc, + ProductAtt41Desc, + ProductAtt42Desc, + ProductAtt43Desc, + ProductAtt44Desc, + ProductAtt45Desc, + ProductAtt46Desc + ) SELECT ProductCode, ProductDescription, @@ -280,8 +458,12 @@ DECLARE @Beden NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p9)), ''); AND (@UrunAltGrubu IS NULL OR ProductAtt02Desc = @UrunAltGrubu) AND (@UrunIcerigi IS NULL OR ProductAtt41Desc = @UrunIcerigi) AND (@Fit IS NULL OR ProductAtt38Desc = @Fit) - AND (@Drop IS NULL OR ProductAtt11Desc = @Drop) -), + AND (@Drop IS NULL OR ProductAtt11Desc = @Drop); +END; + +CREATE CLUSTERED INDEX IX_AttrFiltered_ProductCode ON #AttrFiltered(ProductCode); + +;WITH INV AS ( SELECT @@ -307,9 +489,15 @@ INV AS P.ItemTypeCode, P.ItemCode, P.ColorCode, P.ItemDim1Code, P.ItemDim2Code, P.ItemDim3Code, P.Qty1 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1, 0 AS InventoryQty1 FROM PickingStates P - INNER JOIN AttrFiltered AF ON AF.ProductCode = P.ItemCode + INNER JOIN #AttrFiltered AF ON AF.ProductCode = P.ItemCode WHERE P.ItemTypeCode = 1 AND LEN(P.ItemCode) = 13 + AND P.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' + ) UNION ALL SELECT @@ -317,9 +505,15 @@ INV AS R.ItemTypeCode, R.ItemCode, R.ColorCode, R.ItemDim1Code, R.ItemDim2Code, R.ItemDim3Code, 0, R.Qty1, 0, 0 FROM ReserveStates R - INNER JOIN AttrFiltered AF ON AF.ProductCode = R.ItemCode + INNER JOIN #AttrFiltered AF ON AF.ProductCode = R.ItemCode WHERE R.ItemTypeCode = 1 AND LEN(R.ItemCode) = 13 + AND R.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' + ) UNION ALL SELECT @@ -327,9 +521,15 @@ INV AS D.ItemTypeCode, D.ItemCode, D.ColorCode, D.ItemDim1Code, D.ItemDim2Code, D.ItemDim3Code, 0, 0, D.Qty1, 0 FROM DispOrderStates D - INNER JOIN AttrFiltered AF ON AF.ProductCode = D.ItemCode + INNER JOIN #AttrFiltered AF ON AF.ProductCode = D.ItemCode WHERE D.ItemTypeCode = 1 AND LEN(D.ItemCode) = 13 + AND D.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' + ) UNION ALL SELECT @@ -337,9 +537,15 @@ INV AS T.ItemTypeCode, T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemDim3Code, 0, 0, 0, SUM(T.In_Qty1 - T.Out_Qty1) FROM trStock T WITH (NOLOCK) - INNER JOIN AttrFiltered AF ON AF.ProductCode = T.ItemCode + INNER JOIN #AttrFiltered AF ON AF.ProductCode = T.ItemCode WHERE T.ItemTypeCode = 1 AND LEN(T.ItemCode) = 13 + AND T.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' + ) GROUP BY T.CompanyCode, T.OfficeCode, T.StoreTypeCode, T.StoreCode, T.WarehouseCode, T.ItemTypeCode, T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemDim3Code @@ -391,7 +597,7 @@ Grouped AS A.ColorCode, A.ItemDim2Code FROM Avail A - INNER JOIN AttrFiltered AF ON AF.ProductCode = A.ItemCode + INNER JOIN #AttrFiltered AF ON AF.ProductCode = A.ItemCode WHERE (@Renk IS NULL OR (CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.ColorCode END) = @Renk) AND (@Renk2 IS NULL OR A.ItemDim2Code = @Renk2) AND ( @@ -454,7 +660,7 @@ INNER JOIN Grouped G ON G.ItemCode = A.ItemCode AND G.ColorCode = A.ColorCode AND ISNULL(G.ItemDim2Code, '') = ISNULL(A.ItemDim2Code, '') -INNER JOIN AttrFiltered AF +INNER JOIN #AttrFiltered AF ON AF.ProductCode = A.ItemCode LEFT JOIN cdWarehouseDesc W WITH (NOLOCK) ON W.WarehouseCode = A.WarehouseCode @@ -469,5 +675,6 @@ OUTER APPLY ( AND PB.ItemCode = A.ItemCode AND LEN(PB.ItemCode) = 13 ORDER BY PB.PriceDate DESC -) P; +) P +OPTION (RECOMPILE); ` diff --git a/svc/routes/product_images.go b/svc/routes/product_images.go index 8921342..4e2dcad 100644 --- a/svc/routes/product_images.go +++ b/svc/routes/product_images.go @@ -23,11 +23,53 @@ type ProductImageItem struct { ContentURL string `json:"content_url"` } +func tokenizeImageFileName(fileName string) []string { + up := strings.ToUpper(strings.TrimSpace(fileName)) + if up == "" { + return nil + } + return strings.FieldsFunc(up, func(r rune) bool { + isUpper := r >= 'A' && r <= 'Z' + isDigit := r >= '0' && r <= '9' + if isUpper || isDigit || r == '_' { + return false + } + return true + }) +} + +func imageFileMatches(fileName, color, secondColor string) bool { + color = strings.ToUpper(strings.TrimSpace(color)) + secondColor = strings.ToUpper(strings.TrimSpace(secondColor)) + if color == "" && secondColor == "" { + return true + } + + tokens := tokenizeImageFileName(fileName) + if len(tokens) == 0 { + return false + } + + hasToken := func(target string) bool { + if target == "" { + return true + } + for _, t := range tokens { + if t == target { + return true + } + } + return false + } + + return hasToken(color) && hasToken(secondColor) +} + // // LIST PRODUCT IMAGES // -// GET /api/product-images?code=...&color=... +// GET /api/product-images?code=...&color=...&yaka=... func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -41,6 +83,10 @@ func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc { code := strings.TrimSpace(r.URL.Query().Get("code")) color := strings.TrimSpace(r.URL.Query().Get("color")) + secondColor := strings.TrimSpace(r.URL.Query().Get("yaka")) + if secondColor == "" { + secondColor = strings.TrimSpace(r.URL.Query().Get("renk2")) + } if code == "" { @@ -67,18 +113,13 @@ JOIN mmitem i WHERE b.typ = 'img' AND b.src_table = 'mmitem' AND UPPER(i.code) = UPPER($1) - AND ( - $2 = '' - OR b.file_name ILIKE '%' || '-' || $2 || '-%' - OR b.file_name ILIKE '%' || '-' || $2 || '_%' - ) ORDER BY COALESCE(b.sort_order,999999), b.zlins_dttm DESC, b.id DESC ` - rows, err := pg.Query(query, code, color) + rows, err := pg.Query(query, code) if err != nil { @@ -86,6 +127,7 @@ ORDER BY "req_id", reqID, "code", code, "color", color, + "second_color", secondColor, "err", err.Error(), ) @@ -109,6 +151,9 @@ ORDER BY ); err != nil { continue } + if !imageFileMatches(it.FileName, color, secondColor) { + continue + } it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID) @@ -119,6 +164,7 @@ ORDER BY "req_id", reqID, "code", code, "color", color, + "second_color", secondColor, "count", len(items), ) diff --git a/ui/quasar.config.js.temporary.compiled.1773067361946.mjs b/ui/quasar.config.js.temporary.compiled.1773240229507.mjs similarity index 100% rename from ui/quasar.config.js.temporary.compiled.1773067361946.mjs rename to ui/quasar.config.js.temporary.compiled.1773240229507.mjs diff --git a/ui/src/pages/ProductStockByAttributes.vue b/ui/src/pages/ProductStockByAttributes.vue index 850590c..4582875 100644 --- a/ui/src/pages/ProductStockByAttributes.vue +++ b/ui/src/pages/ProductStockByAttributes.vue @@ -181,21 +181,29 @@
- +
+
@@ -231,6 +239,99 @@ + + + + +
Urun Karti
+ + +
+ + + + +
+
+ {{ productCardData.productCode || '-' }} / {{ productCardData.colorCode || '-' }}{{ productCardData.secondColor ? '-' + productCardData.secondColor : '' }} +
+
Toplam Stok: {{ formatNumber(productCardData.totalQty || 0) }}
+
+
+ {{ sz }} + {{ Number(productCardData.sizeTotals?.[sz] || 0) > 0 ? formatNumber(productCardData.sizeTotals[sz]) : '-' }} +
+
+
+ + + +
+
+ + +
+ +
+
+
+
+ +
+
+ +
+
Urun Kodu{{ productCardData.productCode || '-' }}
+
Urun Renk{{ productCardData.colorCode || '-' }}
+
Urun 2.Renk{{ productCardData.secondColor || '-' }}
+
Kategori{{ productCardData.kategori || '-' }}
+
Urun Ana Grubu{{ productCardData.urunAnaGrubu || '-' }}
+
Urun Alt Grubu{{ productCardData.urunAltGrubu || '-' }}
+
Urun Icerigi{{ productCardData.urunIcerigi || '-' }}
+
Fit{{ productCardData.fit || '-' }}
+
Drop{{ productCardData.drop || '-' }}
+
Kumas{{ productCardData.kumas || '-' }}
+
Karisim{{ productCardData.karisim || '-' }}
+
+
+
+
+
+ + + + +
Urun Fotografi
+ + +
+ + +
+ +
+
+
+
@@ -284,6 +385,7 @@ const filters = ref({ }) const optionLists = ref({}) const filteredOptionLists = ref({}) +const filterOptionsCache = ref({}) const rawRows = ref([]) const productImageCache = ref({}) const productImageLoading = ref({}) @@ -293,8 +395,19 @@ const productImageFallbackByKey = ref({}) const productImageContentLoading = ref({}) const productImageBlobUrls = ref([]) const productImageListBlockedUntil = ref(0) +const productCardDialog = ref(false) +const productCardData = ref({}) +const productCardImages = ref([]) +const productCardSlide = ref(0) +const productImageFullscreenDialog = ref(false) +const productImageFullscreenSrc = ref('') +const productImageFullscreenZoom = ref(1) const IMAGE_LIST_CONCURRENCY = 8 +const FILTER_OPTIONS_CACHE_TTL_MS = 60 * 1000 +const FILTER_OPTIONS_DEBOUNCE_MS = 250 let imageListActiveRequests = 0 +let filterOptionsDebounceTimer = null +let filterOptionsRequestSeq = 0 const imageListWaitQueue = [] const activeSchema = ref(storeSchemaByKey.tak) const activeGrpKey = ref('tak') @@ -324,6 +437,11 @@ const allDetailsExpanded = computed(() => { const gridHeaderHeight = computed(() => showGridHeader.value ? '56px' : '0px' ) +const fullscreenImageStyle = computed(() => ({ + transform: `scale(${productImageFullscreenZoom.value})`, + transformOrigin: 'center center', + transition: 'transform 0.15s ease-out' +})) function emptySizeTotals() { const map = {} @@ -347,8 +465,20 @@ function sortByTotalQtyDesc(a, b) { return String(a?.key || '').localeCompare(String(b?.key || ''), 'tr', { sensitivity: 'base' }) } -function buildImageKey(code, color) { - return `${String(code || '').trim().toUpperCase()}::${String(color || '').trim().toUpperCase()}` +function buildImageKey(code, color, secondColor = '') { + return `${String(code || '').trim().toUpperCase()}::${String(color || '').trim().toUpperCase()}::${String(secondColor || '').trim().toUpperCase()}` +} + +function imageNameMatches(fileName, color, secondColor) { + const text = String(fileName || '').toUpperCase() + if (!text) return false + const tokens = text.replace(/[^A-Z0-9_]+/g, ' ').trim().split(/\s+/).filter(Boolean) + if (!tokens.length) return false + const colorTrim = String(color || '').trim().toUpperCase() + const secondTrim = String(secondColor || '').trim().toUpperCase() + if (colorTrim && !tokens.includes(colorTrim)) return false + if (secondTrim && !tokens.includes(secondTrim)) return false + return true } function normalizeUploadsPath(storagePath) { @@ -403,17 +533,17 @@ function resolveProductImageUrl(item) { return { contentUrl, publicUrl } } -function getProductImageUrl(code, color) { - const key = buildImageKey(code, color) +function getProductImageUrl(code, color, secondColor = '') { + const key = buildImageKey(code, color, secondColor) const existing = productImageCache.value[key] if (existing !== undefined) return existing || '' - void ensureProductImage(code, color) + void ensureProductImage(code, color, secondColor) return '' } -async function onProductImageError(code, color) { - const key = buildImageKey(code, color) +async function onProductImageError(code, color, secondColor = '') { + const key = buildImageKey(code, color, secondColor) const fallback = String(productImageFallbackByKey.value[key] || '') if (fallback && !productImageContentLoading.value[key]) { productImageContentLoading.value[key] = true @@ -438,10 +568,12 @@ async function onProductImageError(code, color) { productImageCache.value[key] = '' } -async function ensureProductImage(code, color) { - const key = buildImageKey(code, color) +async function ensureProductImage(code, color, secondColor = '') { + const key = buildImageKey(code, color, secondColor) const codeTrim = String(code || '').trim().toUpperCase() const colorTrim = String(color || '').trim().toUpperCase() + const secondTrim = String(secondColor || '').trim().toUpperCase() + const listKey = buildImageKey(codeTrim, colorTrim, secondTrim) if (!codeTrim) { productImageCache.value[key] = '' return '' @@ -455,44 +587,44 @@ async function ensureProductImage(code, color) { productImageLoading.value[key] = true try { - if (!productImageListByCode.value[codeTrim]) { - if (!productImageListLoading.value[codeTrim]) { - productImageListLoading.value[codeTrim] = true + if (!productImageListByCode.value[listKey]) { + if (!productImageListLoading.value[listKey]) { + productImageListLoading.value[listKey] = true try { if (imageListActiveRequests >= IMAGE_LIST_CONCURRENCY) { await new Promise((resolve) => imageListWaitQueue.push(resolve)) } imageListActiveRequests++ - const res = await api.get('/product-images', { params: { code: codeTrim } }) - productImageListByCode.value[codeTrim] = Array.isArray(res?.data) ? res.data : [] + const params = { code: codeTrim, dim1: colorTrim, dim3: secondTrim } + const res = await api.get('/product-images', { params }) + productImageListByCode.value[listKey] = Array.isArray(res?.data) ? res.data : [] } catch (err) { - productImageListByCode.value[codeTrim] = [] + productImageListByCode.value[listKey] = [] const status = Number(err?.response?.status || 0) if (status >= 500 || status === 403 || status === 0) { // Backend dengesizken istek firtinasini kisaca kes. productImageListBlockedUntil.value = Date.now() + 30 * 1000 } - console.warn('[ProductStockByAttributes] product image list fetch failed', { code: codeTrim, err }) + console.warn('[ProductStockByAttributes] product image list fetch failed', { code: codeTrim, color: colorTrim, secondColor: secondTrim, err }) } finally { imageListActiveRequests = Math.max(0, imageListActiveRequests - 1) const nextInQueue = imageListWaitQueue.shift() if (nextInQueue) nextInQueue() - delete productImageListLoading.value[codeTrim] + delete productImageListLoading.value[listKey] } } else { // Ayni code icin baska bir istek zaten calisiyorsa tamamlanmasini bekle. - while (productImageListLoading.value[codeTrim]) { + while (productImageListLoading.value[listKey]) { await new Promise((resolve) => setTimeout(resolve, 25)) } } } - const list = productImageListByCode.value[codeTrim] || [] + const list = productImageListByCode.value[listKey] || [] let first = null - if (colorTrim) { - const needle = `-${colorTrim.toLowerCase()}-` + if (colorTrim || secondTrim) { first = list.find((item) => - String(item?.file_name || item?.FileName || '').toLowerCase().includes(needle) + imageNameMatches(String(item?.file_name || item?.FileName || ''), colorTrim, secondTrim) ) || null } if (!first) first = list[0] || null @@ -625,6 +757,11 @@ const level1Groups = computed(() => { const depoAdi = String(item.Depo_Adi || '').trim() const urunAnaGrubu = String(item.URUN_ANA_GRUBU || '').trim() const urunAltGrubu = String(item.URUN_ALT_GRUBU || '').trim() + const urunIcerigi = String(item.URUN_ICERIGI || item.KISA_KAR || '').trim() + const fit = String(item.BIRINCI_PARCA_FIT || '').trim() + const drop = String(item.DR || '').trim() + const kumas = String(item.BIRINCI_PARCA_KUMAS || '').trim() + const karisim = String(item.BIRINCI_PARCA_KARISIM || '').trim() const aciklama = String(item.Madde_Aciklamasi || '').trim() const beden = normalizeSize(item.Beden || '') const qty = parseNumber(item.Kullanilabilir_Envanter) @@ -654,6 +791,11 @@ const level1Groups = computed(() => { secondColor, urunAnaGrubu, urunAltGrubu, + urunIcerigi, + fit, + drop, + kumas, + karisim, aciklama, sizeTotals: emptySizeTotals(), totalQty: 0, @@ -714,6 +856,11 @@ function buildFilterParams() { return out } +function buildFilterCacheKey(params) { + const keys = Object.keys(params || {}).sort() + return keys.map((k) => `${k}=${String(params[k] || '').trim()}`).join('&') +} + function isFilterDisabled(key) { if (key === 'kategori') return false if (key === 'urun_ana_grubu') { @@ -747,7 +894,12 @@ function onFilterValueChange(changedKey) { filters.value.beden = '' } - void loadFilterOptions() + if (filterOptionsDebounceTimer) { + clearTimeout(filterOptionsDebounceTimer) + } + filterOptionsDebounceTimer = setTimeout(() => { + void loadFilterOptions() + }, FILTER_OPTIONS_DEBOUNCE_MS) } function filterOptions(field, val, update) { @@ -768,12 +920,26 @@ function filterOptions(field, val, update) { }) } -async function loadFilterOptions() { +async function loadFilterOptions(force = false) { + const params = buildFilterParams() + const cacheKey = buildFilterCacheKey(params) + const now = Date.now() + if (!force) { + const cached = filterOptionsCache.value[cacheKey] + if (cached && Number(cached.expiresAt || 0) > now && cached.payload) { + optionLists.value = cached.payload.optionLists + filteredOptionLists.value = cached.payload.filteredOptionLists + return + } + } + + const reqSeq = ++filterOptionsRequestSeq loadingFilterOptions.value = true try { const res = await api.get('/product-stock-attribute-options', { - params: buildFilterParams() + params }) + if (reqSeq !== filterOptionsRequestSeq) return const payload = res?.data && typeof res.data === 'object' ? res.data : {} const next = {} const nextFiltered = {} @@ -795,11 +961,21 @@ async function loadFilterOptions() { optionLists.value = next filteredOptionLists.value = nextFiltered + filterOptionsCache.value[cacheKey] = { + expiresAt: now + FILTER_OPTIONS_CACHE_TTL_MS, + payload: { + optionLists: next, + filteredOptionLists: nextFiltered + } + } } catch (err) { + if (reqSeq !== filterOptionsRequestSeq) return errorMessage.value = 'Urun ozellik secenekleri alinamadi.' console.error('loadFilterOptions error:', err) } finally { - loadingFilterOptions.value = false + if (reqSeq === filterOptionsRequestSeq) { + loadingFilterOptions.value = false + } } } @@ -871,10 +1047,65 @@ async function fetchStockByAttributes() { function onLevel2Click(productCode, grp2) { toggleOpen(grp2.key) if (isOpen(grp2.key)) { - void ensureProductImage(productCode, grp2.colorCode) + void ensureProductImage(productCode, grp2.colorCode, grp2.secondColor) } } +async function openProductCard(grp1, grp2) { + const productCode = String(grp1?.productCode || '').trim() + const colorCode = String(grp2?.colorCode || '').trim() + const secondColor = String(grp2?.secondColor || '').trim() + const listKey = buildImageKey(productCode, colorCode, secondColor) + + await ensureProductImage(productCode, colorCode, secondColor) + const list = Array.isArray(productImageListByCode.value[listKey]) ? productImageListByCode.value[listKey] : [] + const images = list + .map((item) => { + const resolved = resolveProductImageUrl(item) + return resolved.contentUrl || resolved.publicUrl || '' + }) + .filter((x) => String(x || '').trim() !== '') + + if (!images.length) { + const single = getProductImageUrl(productCode, colorCode, secondColor) + if (single) images.push(single) + } + + productCardImages.value = images + productCardSlide.value = 0 + productCardData.value = { + productCode, + colorCode, + secondColor, + kategori: String(filters.value?.kategori || '').trim(), + urunAnaGrubu: String(grp2?.urunAnaGrubu || '').trim(), + urunAltGrubu: String(grp2?.urunAltGrubu || '').trim(), + urunIcerigi: String(grp2?.urunIcerigi || '').trim(), + fit: String(grp2?.fit || '').trim(), + drop: String(grp2?.drop || '').trim(), + kumas: String(grp2?.kumas || '').trim(), + karisim: String(grp2?.karisim || '').trim(), + sizeTotals: grp2?.sizeTotals || {}, + totalQty: Number(grp2?.totalQty || 0) + } + productCardDialog.value = true +} + +function openProductImageFullscreen(src) { + const value = String(src || '').trim() + if (!value) return + productImageFullscreenSrc.value = value + productImageFullscreenZoom.value = 1 + productImageFullscreenDialog.value = true +} + +function toggleFullscreenImageZoom() { + const current = Number(productImageFullscreenZoom.value || 1) + if (current < 1.5) productImageFullscreenZoom.value = 1.8 + else if (current < 2.3) productImageFullscreenZoom.value = 2.6 + else productImageFullscreenZoom.value = 1 +} + function resetForm() { filters.value = { kategori: '', @@ -898,14 +1129,26 @@ function resetForm() { productImageFallbackByKey.value = {} productImageContentLoading.value = {} productImageListBlockedUntil.value = 0 - void loadFilterOptions() + productCardDialog.value = false + productCardData.value = {} + productCardImages.value = [] + productCardSlide.value = 0 + productImageFullscreenDialog.value = false + productImageFullscreenSrc.value = '' + productImageFullscreenZoom.value = 1 + filterOptionsCache.value = {} + void loadFilterOptions(true) } onMounted(() => { - loadFilterOptions() + void loadFilterOptions(true) }) onUnmounted(() => { + if (filterOptionsDebounceTimer) { + clearTimeout(filterOptionsDebounceTimer) + filterOptionsDebounceTimer = null + } for (const url of productImageBlobUrls.value) { try { URL.revokeObjectURL(url) } catch {} } @@ -920,7 +1163,7 @@ onUnmounted(() => { --grp-title-w: 44px; --psq-header-h: 56px; --psq-col-adet: calc(var(--col-adet) + var(--beden-w)); - --psq-col-img: 126px; + --psq-col-img: 190px; --psq-l1-lift: 42px; } @@ -1003,8 +1246,8 @@ onUnmounted(() => { } .order-sub-header.level-2 { - min-height: 82px !important; - height: 82px !important; + min-height: 252px !important; + height: 252px !important; background: #fff9c4 !important; border-top: 1px solid #d4c79f !important; border-bottom: 1px solid #d4c79f !important; @@ -1250,16 +1493,18 @@ onUnmounted(() => { .order-sub-header.level-2 .sub-image.level2-image { grid-column: 8 / 9; display: flex; + flex-direction: column; align-items: center; justify-content: center; border-left: 1px solid #d4c79f; - padding: 0 6px; + gap: 8px; + padding: 8px 10px; background: #fffef7; } .product-image-card { - width: 108px; - height: 66px; + width: 162px; + height: 216px; border-radius: 8px; } @@ -1272,6 +1517,7 @@ onUnmounted(() => { width: 100%; height: 100%; border-radius: 6px; + background: #fff; } .product-image-placeholder { @@ -1284,6 +1530,178 @@ onUnmounted(() => { border-radius: 6px; } +.detail-open-btn { + margin-top: 4px; + font-size: 11px; +} + +.product-card-dialog { + background: #fffef9; +} + +.product-card-stock { + background: #f8f5e7; + border: 1px solid #e2d9b6; + border-radius: 10px; + padding: 12px; +} + +.stock-size-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); + gap: 8px; +} + +.stock-size-chip { + border: 1px solid #d8cca6; + border-radius: 8px; + background: #fff; + padding: 6px 8px; + display: flex; + justify-content: space-between; + font-size: 12px; +} + +.stock-size-chip .label { + font-weight: 700; +} + +.product-card-content { + display: grid; + grid-template-columns: minmax(360px, 1fr) 420px; + gap: 12px; + align-items: stretch; + justify-content: start; +} + +.product-card-images { + grid-column: 2; + grid-row: 1; + min-height: 560px; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; +} + +.product-card-carousel { + width: 420px; + max-width: 100%; +} + +.dialog-image { + width: 100%; + height: 100%; +} + +.dialog-image-stage { + width: 420px; + max-width: 100%; + height: 560px; + overflow: hidden; + border-radius: 8px; + background: #f7f4e9; + display: flex; + align-items: center; + justify-content: center; +} + +.dialog-image-empty { + width: 420px; + max-width: 100%; + height: 560px; + border: 1px dashed #cabf9a; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + background: #faf7ee; +} + +.image-fullscreen-dialog { + background: #f4f0e2; +} + +.image-fullscreen-body { + height: calc(100vh - 72px); + display: flex; + align-items: center; + justify-content: center; +} + +.image-fullscreen-stage { + width: min(96vw, 1400px); + height: calc(100vh - 120px); + border-radius: 10px; + background: #efe7cc; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +} + +.image-fullscreen-img { + width: 100%; + height: 100%; +} + +.product-card-fields { + grid-column: 1; + grid-row: 1; + border: 1px solid #e2d9b6; + border-radius: 10px; + background: #fff; + padding: 10px; + height: 560px; + overflow: auto; +} + +.field-row { + display: grid; + grid-template-columns: 150px 1fr; + gap: 8px; + padding: 7px 0; + border-bottom: 1px solid #f0ead7; + font-size: 13px; +} + +.field-row:last-child { + border-bottom: none; +} + +.field-row .k { + color: #5a4f2c; + font-weight: 700; +} + +.field-row .v { + color: #1f1f1f; + word-break: break-word; +} + +.q-btn, +.q-icon, +.product-image-card, +.cursor-pointer { + cursor: pointer !important; +} + +@media (max-width: 1024px) { + .product-card-content { + grid-template-columns: 1fr; + } + + .product-card-images, + .product-card-fields { + grid-column: auto; + grid-row: auto; + } + + .product-card-fields { + height: auto; + } +} + .order-sub-header.level-2 .sub-right .top-total { grid-column: 1 / 2; grid-row: 1; diff --git a/ui/src/pages/ProductStockQuery.vue b/ui/src/pages/ProductStockQuery.vue index 67ec3da..e53f94d 100644 --- a/ui/src/pages/ProductStockQuery.vue +++ b/ui/src/pages/ProductStockQuery.vue @@ -179,21 +179,29 @@
- +
+
@@ -229,6 +237,99 @@ + + + + +
Urun Karti
+ + +
+ + + + +
+
+ {{ productCardData.productCode || '-' }} / {{ productCardData.colorCode || '-' }}{{ productCardData.secondColor ? '-' + productCardData.secondColor : '' }} +
+
Toplam Stok: {{ formatNumber(productCardData.totalQty || 0) }}
+
+
+ {{ sz }} + {{ Number(productCardData.sizeTotals?.[sz] || 0) > 0 ? formatNumber(productCardData.sizeTotals[sz]) : '-' }} +
+
+
+ + + +
+
+ + +
+ +
+
+
+
+ +
+
+ +
+
Urun Kodu{{ productCardData.productCode || '-' }}
+
Urun Renk{{ productCardData.colorCode || '-' }}
+
Urun 2.Renk{{ productCardData.secondColor || '-' }}
+
Kategori{{ productCardData.kategori || '-' }}
+
Urun Ana Grubu{{ productCardData.urunAnaGrubu || '-' }}
+
Urun Alt Grubu{{ productCardData.urunAltGrubu || '-' }}
+
Urun Icerigi{{ productCardData.urunIcerigi || '-' }}
+
Fit{{ productCardData.fit || '-' }}
+
Drop{{ productCardData.drop || '-' }}
+
Kumas{{ productCardData.kumas || '-' }}
+
Karisim{{ productCardData.karisim || '-' }}
+
+
+
+
+
+ + + + +
Urun Fotografi
+ + +
+ + +
+ +
+
+
+
@@ -269,6 +370,13 @@ const productImageFallbackByKey = ref({}) const productImageContentLoading = ref({}) const productImageBlobUrls = ref([]) const productImageListBlockedUntil = ref(0) +const productCardDialog = ref(false) +const productCardData = ref({}) +const productCardImages = ref([]) +const productCardSlide = ref(0) +const productImageFullscreenDialog = ref(false) +const productImageFullscreenSrc = ref('') +const productImageFullscreenZoom = ref(1) const IMAGE_LIST_CONCURRENCY = 8 let imageListActiveRequests = 0 const imageListWaitQueue = [] @@ -296,6 +404,11 @@ const allDetailsExpanded = computed(() => { const gridHeaderHeight = computed(() => showGridHeader.value ? '56px' : '0px' ) +const fullscreenImageStyle = computed(() => ({ + transform: `scale(${productImageFullscreenZoom.value})`, + transformOrigin: 'center center', + transition: 'transform 0.15s ease-out' +})) function emptySizeTotals() { const map = {} @@ -312,8 +425,53 @@ function parseNumber(value) { return Number.isFinite(n) ? n : 0 } -function buildImageKey(code, color) { - return `${String(code || '').trim().toUpperCase()}::${String(color || '').trim().toUpperCase()}` +function sortByColorCodeAsc(a, b) { + const compareCodeLike = (va, vb) => { + const sa = String(va || '').trim() + const sb = String(vb || '').trim() + const pa = sa.match(/^(\d+)(?:_(\d+))?$/) + const pb = sb.match(/^(\d+)(?:_(\d+))?$/) + if (pa && pb) { + const a1 = Number.parseInt(pa[1], 10) + const b1 = Number.parseInt(pb[1], 10) + if (a1 !== b1) return a1 - b1 + const a2 = Number.parseInt(pa[2] || '0', 10) + const b2 = Number.parseInt(pb[2] || '0', 10) + if (a2 !== b2) return a2 - b2 + } + return sa.localeCompare(sb, 'tr', { sensitivity: 'base' }) + } + + const ca = String(a?.colorCode || '').trim() + const cb = String(b?.colorCode || '').trim() + const na = Number.parseInt(ca, 10) + const nb = Number.parseInt(cb, 10) + const aNum = Number.isFinite(na) + const bNum = Number.isFinite(nb) + + if (aNum && bNum && na !== nb) return na - nb + if (aNum !== bNum) return aNum ? -1 : 1 + + const cmp = compareCodeLike(ca, cb) + if (cmp !== 0) return cmp + + return compareCodeLike(a?.secondColor, b?.secondColor) +} + +function buildImageKey(code, color, secondColor = '') { + return `${String(code || '').trim().toUpperCase()}::${String(color || '').trim().toUpperCase()}::${String(secondColor || '').trim().toUpperCase()}` +} + +function imageNameMatches(fileName, color, secondColor) { + const text = String(fileName || '').toUpperCase() + if (!text) return false + const tokens = text.replace(/[^A-Z0-9_]+/g, ' ').trim().split(/\s+/).filter(Boolean) + if (!tokens.length) return false + const colorTrim = String(color || '').trim().toUpperCase() + const secondTrim = String(secondColor || '').trim().toUpperCase() + if (colorTrim && !tokens.includes(colorTrim)) return false + if (secondTrim && !tokens.includes(secondTrim)) return false + return true } function normalizeUploadsPath(storagePath) { @@ -358,16 +516,16 @@ function resolveProductImageUrl(item) { return { contentUrl, publicUrl } } -function getProductImageUrl(code, color) { - const key = buildImageKey(code, color) +function getProductImageUrl(code, color, secondColor = '') { + const key = buildImageKey(code, color, secondColor) const existing = productImageCache.value[key] if (existing !== undefined) return existing || '' - void ensureProductImage(code, color) + void ensureProductImage(code, color, secondColor) return '' } -async function onProductImageError(code, color) { - const key = buildImageKey(code, color) +async function onProductImageError(code, color, secondColor = '') { + const key = buildImageKey(code, color, secondColor) const fallback = String(productImageFallbackByKey.value[key] || '') if (fallback && !productImageContentLoading.value[key]) { productImageContentLoading.value[key] = true @@ -389,10 +547,12 @@ async function onProductImageError(code, color) { productImageCache.value[key] = '' } -async function ensureProductImage(code, color) { - const key = buildImageKey(code, color) +async function ensureProductImage(code, color, secondColor = '') { + const key = buildImageKey(code, color, secondColor) const codeTrim = String(code || '').trim().toUpperCase() const colorTrim = String(color || '').trim().toUpperCase() + const secondTrim = String(secondColor || '').trim().toUpperCase() + const listKey = buildImageKey(codeTrim, colorTrim, secondTrim) if (!codeTrim) { productImageCache.value[key] = '' return '' @@ -406,42 +566,42 @@ async function ensureProductImage(code, color) { productImageLoading.value[key] = true try { - if (!productImageListByCode.value[codeTrim]) { - if (!productImageListLoading.value[codeTrim]) { - productImageListLoading.value[codeTrim] = true + if (!productImageListByCode.value[listKey]) { + if (!productImageListLoading.value[listKey]) { + productImageListLoading.value[listKey] = true try { if (imageListActiveRequests >= IMAGE_LIST_CONCURRENCY) { await new Promise((resolve) => imageListWaitQueue.push(resolve)) } imageListActiveRequests++ - const res = await api.get('/product-images', { params: { code: codeTrim } }) - productImageListByCode.value[codeTrim] = Array.isArray(res?.data) ? res.data : [] + const params = { code: codeTrim, dim1: colorTrim, dim3: secondTrim } + const res = await api.get('/product-images', { params }) + productImageListByCode.value[listKey] = Array.isArray(res?.data) ? res.data : [] } catch (err) { - productImageListByCode.value[codeTrim] = [] + productImageListByCode.value[listKey] = [] const status = Number(err?.response?.status || 0) if (status >= 500 || status === 403 || status === 0) { productImageListBlockedUntil.value = Date.now() + 30 * 1000 } - console.warn('[ProductStockQuery] product image list fetch failed', { code: codeTrim, err }) + console.warn('[ProductStockQuery] product image list fetch failed', { code: codeTrim, color: colorTrim, secondColor: secondTrim, err }) } finally { imageListActiveRequests = Math.max(0, imageListActiveRequests - 1) const nextInQueue = imageListWaitQueue.shift() if (nextInQueue) nextInQueue() - delete productImageListLoading.value[codeTrim] + delete productImageListLoading.value[listKey] } } else { - while (productImageListLoading.value[codeTrim]) { + while (productImageListLoading.value[listKey]) { await new Promise((resolve) => setTimeout(resolve, 25)) } } } - const list = productImageListByCode.value[codeTrim] || [] + const list = productImageListByCode.value[listKey] || [] let first = null - if (colorTrim) { - const needle = `-${colorTrim.toLowerCase()}-` + if (colorTrim || secondTrim) { first = list.find((item) => - String(item?.file_name || item?.FileName || '').toLowerCase().includes(needle) + imageNameMatches(String(item?.file_name || item?.FileName || ''), colorTrim, secondTrim) ) || null } if (!first) first = list[0] || null @@ -571,8 +731,14 @@ const level1Groups = computed(() => { const secondColor = String(item.Yaka || '').trim() const depoKodu = String(item.Depo_Kodu || '').trim() const depoAdi = String(item.Depo_Adi || '').trim() + const kategori = String(item.YETISKIN_GARSON || '').trim() const urunAnaGrubu = String(item.URUN_ANA_GRUBU || '').trim() const urunAltGrubu = String(item.URUN_ALT_GRUBU || '').trim() + const urunIcerigi = String(item.URUN_ICERIGI || item.KISA_KAR || '').trim() + const fit = String(item.BIRINCI_PARCA_FIT || '').trim() + const drop = String(item.DR || '').trim() + const kumas = String(item.BIRINCI_PARCA_KUMAS || '').trim() + const karisim = String(item.BIRINCI_PARCA_KARISIM || '').trim() const aciklama = String(item.Madde_Aciklamasi || '').trim() const beden = normalizeSize(item.Beden || '') const qty = parseNumber(item.Kullanilabilir_Envanter) @@ -600,8 +766,14 @@ const level1Groups = computed(() => { colorCode, colorDesc, secondColor, + kategori, urunAnaGrubu, urunAltGrubu, + urunIcerigi, + fit, + drop, + kumas, + karisim, aciklama, sizeTotals: emptySizeTotals(), totalQty: 0, @@ -638,10 +810,12 @@ const level1Groups = computed(() => { return Array.from(l1Map.values()).map((l1) => ({ ...l1, - children: Array.from(l1.childrenMap.values()).map((l2) => ({ - ...l2, - children: Array.from(l2.childrenMap.values()) - })) + children: Array.from(l1.childrenMap.values()) + .map((l2) => ({ + ...l2, + children: Array.from(l2.childrenMap.values()) + })) + .sort(sortByColorCodeAsc) })) }) @@ -740,10 +914,65 @@ async function fetchStockByCode() { function onLevel2Click(productCode, grp2) { toggleOpen(grp2.key) if (isOpen(grp2.key)) { - void ensureProductImage(productCode, grp2.colorCode) + void ensureProductImage(productCode, grp2.colorCode, grp2.secondColor) } } +async function openProductCard(grp1, grp2) { + const productCode = String(grp1?.productCode || '').trim() + const colorCode = String(grp2?.colorCode || '').trim() + const secondColor = String(grp2?.secondColor || '').trim() + const listKey = buildImageKey(productCode, colorCode, secondColor) + + await ensureProductImage(productCode, colorCode, secondColor) + const list = Array.isArray(productImageListByCode.value[listKey]) ? productImageListByCode.value[listKey] : [] + const images = list + .map((item) => { + const resolved = resolveProductImageUrl(item) + return resolved.contentUrl || resolved.publicUrl || '' + }) + .filter((x) => String(x || '').trim() !== '') + + if (!images.length) { + const single = getProductImageUrl(productCode, colorCode, secondColor) + if (single) images.push(single) + } + + productCardImages.value = images + productCardSlide.value = 0 + productCardData.value = { + productCode, + colorCode, + secondColor, + kategori: String(grp2?.kategori || '').trim(), + urunAnaGrubu: String(grp2?.urunAnaGrubu || '').trim(), + urunAltGrubu: String(grp2?.urunAltGrubu || '').trim(), + urunIcerigi: String(grp2?.urunIcerigi || '').trim(), + fit: String(grp2?.fit || '').trim(), + drop: String(grp2?.drop || '').trim(), + kumas: String(grp2?.kumas || '').trim(), + karisim: String(grp2?.karisim || '').trim(), + sizeTotals: grp2?.sizeTotals || {}, + totalQty: Number(grp2?.totalQty || 0) + } + productCardDialog.value = true +} + +function openProductImageFullscreen(src) { + const value = String(src || '').trim() + if (!value) return + productImageFullscreenSrc.value = value + productImageFullscreenZoom.value = 1 + productImageFullscreenDialog.value = true +} + +function toggleFullscreenImageZoom() { + const current = Number(productImageFullscreenZoom.value || 1) + if (current < 1.5) productImageFullscreenZoom.value = 1.8 + else if (current < 2.3) productImageFullscreenZoom.value = 2.6 + else productImageFullscreenZoom.value = 1 +} + function resetForm() { selectedProductCode.value = '' rawRows.value = [] @@ -757,6 +986,13 @@ function resetForm() { productImageFallbackByKey.value = {} productImageContentLoading.value = {} productImageListBlockedUntil.value = 0 + productCardDialog.value = false + productCardData.value = {} + productCardImages.value = [] + productCardSlide.value = 0 + productImageFullscreenDialog.value = false + productImageFullscreenSrc.value = '' + productImageFullscreenZoom.value = 1 } onMounted(() => { @@ -777,7 +1013,7 @@ onUnmounted(() => { --grp-title-w: 44px; --psq-header-h: 56px; --psq-col-adet: calc(var(--col-adet) + var(--beden-w)); - --psq-col-img: 126px; + --psq-col-img: 190px; --psq-l1-lift: 42px; } @@ -860,8 +1096,8 @@ onUnmounted(() => { } .order-sub-header.level-2 { - min-height: 82px !important; - height: 82px !important; + min-height: 252px !important; + height: 252px !important; background: #fff9c4 !important; border-top: 1px solid #d4c79f !important; border-bottom: 1px solid #d4c79f !important; @@ -1107,15 +1343,17 @@ onUnmounted(() => { .order-sub-header.level-2 .sub-image.level2-image { grid-column: 8 / 9; display: flex; + flex-direction: column; align-items: center; justify-content: center; - padding: 6px; + gap: 8px; + padding: 8px 10px; border-left: 1px solid #d4c79f; } .product-image-card { - width: 110px; - height: 68px; + width: 162px; + height: 216px; border-radius: 8px; overflow: hidden; } @@ -1128,6 +1366,7 @@ onUnmounted(() => { .product-image { width: 100%; height: 100%; + background: #fff; } .product-image-placeholder { @@ -1139,6 +1378,178 @@ onUnmounted(() => { background: #f5f6f7; } +.detail-open-btn { + margin-top: 4px; + font-size: 11px; +} + +.product-card-dialog { + background: #fffef9; +} + +.product-card-stock { + background: #f8f5e7; + border: 1px solid #e2d9b6; + border-radius: 10px; + padding: 12px; +} + +.stock-size-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); + gap: 8px; +} + +.stock-size-chip { + border: 1px solid #d8cca6; + border-radius: 8px; + background: #fff; + padding: 6px 8px; + display: flex; + justify-content: space-between; + font-size: 12px; +} + +.stock-size-chip .label { + font-weight: 700; +} + +.product-card-content { + display: grid; + grid-template-columns: minmax(360px, 1fr) 420px; + gap: 12px; + align-items: stretch; + justify-content: start; +} + +.product-card-images { + grid-column: 2; + grid-row: 1; + min-height: 560px; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; +} + +.product-card-carousel { + width: 420px; + max-width: 100%; +} + +.dialog-image { + width: 100%; + height: 100%; +} + +.dialog-image-stage { + width: 420px; + max-width: 100%; + height: 560px; + overflow: hidden; + border-radius: 8px; + background: #f7f4e9; + display: flex; + align-items: center; + justify-content: center; +} + +.dialog-image-empty { + width: 420px; + max-width: 100%; + height: 560px; + border: 1px dashed #cabf9a; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + background: #faf7ee; +} + +.image-fullscreen-dialog { + background: #f4f0e2; +} + +.image-fullscreen-body { + height: calc(100vh - 72px); + display: flex; + align-items: center; + justify-content: center; +} + +.image-fullscreen-stage { + width: min(96vw, 1400px); + height: calc(100vh - 120px); + border-radius: 10px; + background: #efe7cc; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +} + +.image-fullscreen-img { + width: 100%; + height: 100%; +} + +.product-card-fields { + grid-column: 1; + grid-row: 1; + border: 1px solid #e2d9b6; + border-radius: 10px; + background: #fff; + padding: 10px; + height: 560px; + overflow: auto; +} + +.field-row { + display: grid; + grid-template-columns: 150px 1fr; + gap: 8px; + padding: 7px 0; + border-bottom: 1px solid #f0ead7; + font-size: 13px; +} + +.field-row:last-child { + border-bottom: none; +} + +.field-row .k { + color: #5a4f2c; + font-weight: 700; +} + +.field-row .v { + color: #1f1f1f; + word-break: break-word; +} + +.q-btn, +.q-icon, +.product-image-card, +.cursor-pointer { + cursor: pointer !important; +} + +@media (max-width: 1024px) { + .product-card-content { + grid-template-columns: 1fr; + } + + .product-card-images, + .product-card-fields { + grid-column: auto; + grid-row: auto; + } + + .product-card-fields { + height: auto; + } +} + .order-sub-header.level-2 .sub-right .top-total { grid-column: 1 / 2; grid-row: 1;