Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-03-11 17:53:30 +03:00
parent aba71341b9
commit 6ff8747411
8 changed files with 1373 additions and 129 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -132,6 +132,7 @@ SELECT
P.ProductAtt38Desc AS BIRINCI_PARCA_FIT, P.ProductAtt38Desc AS BIRINCI_PARCA_FIT,
P.ProductAtt39Desc AS IKINCI_PARCA_FIT, P.ProductAtt39Desc AS IKINCI_PARCA_FIT,
P.ProductAtt40Desc AS BOS2, P.ProductAtt40Desc AS BOS2,
P.ProductAtt41Desc AS URUN_ICERIGI,
P.ProductAtt41Desc AS KISA_KAR, P.ProductAtt41Desc AS KISA_KAR,
P.ProductAtt42Desc AS SERI_FASON, P.ProductAtt42Desc AS SERI_FASON,
P.ProductAtt43Desc AS STOK_GIRIS_YONTEMI, P.ProductAtt43Desc AS STOK_GIRIS_YONTEMI,

View File

@@ -13,7 +13,7 @@ DECLARE @Fit NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p7)), '');
DECLARE @Drop NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p8)), ''); DECLARE @Drop NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p8)), '');
DECLARE @Beden NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p9)), ''); DECLARE @Beden NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p9)), '');
DECLARE @AttrBase TABLE CREATE TABLE #AttrBase
( (
ProductCode NVARCHAR(50) NOT NULL, ProductCode NVARCHAR(50) NOT NULL,
Kategori NVARCHAR(100) NOT NULL, Kategori NVARCHAR(100) NOT NULL,
@@ -24,8 +24,10 @@ DECLARE @AttrBase TABLE
DropVal NVARCHAR(100) NOT NULL DropVal NVARCHAR(100) NOT NULL
); );
INSERT INTO @AttrBase (ProductCode, Kategori, UrunAnaGrubu, UrunAltGrubu, UrunIcerigi, Fit, DropVal) IF OBJECT_ID('dbo.ProductFilterTRCache','U') IS NOT NULL
SELECT BEGIN
INSERT INTO #AttrBase (ProductCode, Kategori, UrunAnaGrubu, UrunAltGrubu, UrunIcerigi, Fit, DropVal)
SELECT
ProductCode, ProductCode,
Kategori = LTRIM(RTRIM(ProductAtt44Desc)), Kategori = LTRIM(RTRIM(ProductAtt44Desc)),
UrunAnaGrubu = LTRIM(RTRIM(ProductAtt01Desc)), UrunAnaGrubu = LTRIM(RTRIM(ProductAtt01Desc)),
@@ -33,33 +35,58 @@ SELECT
UrunIcerigi = LTRIM(RTRIM(ProductAtt41Desc)), UrunIcerigi = LTRIM(RTRIM(ProductAtt41Desc)),
Fit = LTRIM(RTRIM(ProductAtt38Desc)), Fit = LTRIM(RTRIM(ProductAtt38Desc)),
DropVal = LTRIM(RTRIM(ProductAtt11Desc)) DropVal = LTRIM(RTRIM(ProductAtt11Desc))
FROM ProductFilterWithDescription('TR') FROM dbo.ProductFilterTRCache
WHERE LEN(ProductCode) = 13 WHERE LEN(ProductCode) = 13
AND (@Kategori IS NULL OR ProductAtt44Desc = @Kategori) AND (@Kategori IS NULL OR ProductAtt44Desc = @Kategori)
AND (@UrunAnaGrubu IS NULL OR ProductAtt01Desc = @UrunAnaGrubu) AND (@UrunAnaGrubu IS NULL OR ProductAtt01Desc = @UrunAnaGrubu)
AND (@UrunAltGrubu IS NULL OR ProductAtt02Desc = @UrunAltGrubu) AND (@UrunAltGrubu IS NULL OR ProductAtt02Desc = @UrunAltGrubu)
AND (@UrunIcerigi IS NULL OR ProductAtt41Desc = @UrunIcerigi) AND (@UrunIcerigi IS NULL OR ProductAtt41Desc = @UrunIcerigi)
AND (@Fit IS NULL OR ProductAtt38Desc = @Fit) AND (@Fit IS NULL OR ProductAtt38Desc = @Fit)
AND (@Drop IS NULL OR ProductAtt11Desc = @Drop); 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 IF @Kategori IS NULL OR @UrunAnaGrubu IS NULL
BEGIN BEGIN
CREATE CLUSTERED INDEX IX_AttrBase_ProductCode ON #AttrBase(ProductCode);
SELECT 'kategori' AS FieldName, X.FieldValue SELECT 'kategori' AS FieldName, X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = AB.Kategori SELECT DISTINCT FieldValue = AB.Kategori
FROM @AttrBase AB FROM #AttrBase AB
WHERE AB.Kategori <> '' WHERE AB.Kategori <> ''
) X ) X
UNION ALL UNION ALL
SELECT 'urun_ana_grubu', X.FieldValue SELECT 'urun_ana_grubu', X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = AB.UrunAnaGrubu SELECT DISTINCT FieldValue = AB.UrunAnaGrubu
FROM @AttrBase AB FROM #AttrBase AB
WHERE AB.UrunAnaGrubu <> '' WHERE AB.UrunAnaGrubu <> ''
) X; ) X;
RETURN; RETURN;
END; 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 ;WITH INV AS
( (
SELECT SELECT
@@ -77,7 +104,7 @@ END;
P.ItemCode, P.ColorCode, P.ItemDim1Code, P.ItemDim2Code, P.ItemCode, P.ColorCode, P.ItemDim1Code, P.ItemDim2Code,
P.Qty1 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1, 0 AS InventoryQty1 P.Qty1 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1, 0 AS InventoryQty1
FROM PickingStates P 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 WHERE P.ItemTypeCode = 1
AND LEN(P.ItemCode) = 13 AND LEN(P.ItemCode) = 13
@@ -86,7 +113,7 @@ END;
R.ItemCode, R.ColorCode, R.ItemDim1Code, R.ItemDim2Code, R.ItemCode, R.ColorCode, R.ItemDim1Code, R.ItemDim2Code,
0, R.Qty1, 0, 0 0, R.Qty1, 0, 0
FROM ReserveStates R 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 WHERE R.ItemTypeCode = 1
AND LEN(R.ItemCode) = 13 AND LEN(R.ItemCode) = 13
@@ -95,7 +122,7 @@ END;
D.ItemCode, D.ColorCode, D.ItemDim1Code, D.ItemDim2Code, D.ItemCode, D.ColorCode, D.ItemDim1Code, D.ItemDim2Code,
0, 0, D.Qty1, 0 0, 0, D.Qty1, 0
FROM DispOrderStates D 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 WHERE D.ItemTypeCode = 1
AND LEN(D.ItemCode) = 13 AND LEN(D.ItemCode) = 13
@@ -104,7 +131,7 @@ END;
T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code,
0, 0, 0, SUM(T.In_Qty1 - T.Out_Qty1) 0, 0, 0, SUM(T.In_Qty1 - T.Out_Qty1)
FROM trStock T WITH (NOLOCK) 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 WHERE T.ItemTypeCode = 1
AND LEN(T.ItemCode) = 13 AND LEN(T.ItemCode) = 13
GROUP BY T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code GROUP BY T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code
@@ -129,21 +156,21 @@ Avail AS
SELECT 'kategori' AS FieldName, X.FieldValue SELECT 'kategori' AS FieldName, X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = AB.Kategori SELECT DISTINCT FieldValue = AB.Kategori
FROM @AttrBase AB FROM #AttrBase AB
WHERE AB.Kategori <> '' WHERE AB.Kategori <> ''
) X ) X
UNION ALL UNION ALL
SELECT 'urun_ana_grubu', X.FieldValue SELECT 'urun_ana_grubu', X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = AB.UrunAnaGrubu SELECT DISTINCT FieldValue = AB.UrunAnaGrubu
FROM @AttrBase AB FROM #AttrBase AB
WHERE AB.UrunAnaGrubu <> '' WHERE AB.UrunAnaGrubu <> ''
) X ) X
UNION ALL UNION ALL
SELECT 'urun_alt_grubu', X.FieldValue SELECT 'urun_alt_grubu', X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = AB.UrunAltGrubu SELECT DISTINCT FieldValue = AB.UrunAltGrubu
FROM @AttrBase AB FROM #AttrBase AB
WHERE AB.UrunAltGrubu <> '' WHERE AB.UrunAltGrubu <> ''
AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@Kategori IS NULL OR AB.Kategori = @Kategori)
AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu)
@@ -153,7 +180,7 @@ SELECT 'renk', X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.Renk END SELECT DISTINCT FieldValue = CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.Renk END
FROM Avail A 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) <> '' WHERE (CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.Renk END) <> ''
AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@Kategori IS NULL OR AB.Kategori = @Kategori)
AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu)
@@ -169,7 +196,7 @@ SELECT 'renk2', X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = A.Renk2 SELECT DISTINCT FieldValue = A.Renk2
FROM Avail A FROM Avail A
INNER JOIN @AttrBase AB ON AB.ProductCode = A.ItemCode INNER JOIN #AttrBase AB ON AB.ProductCode = A.ItemCode
WHERE A.Renk2 <> '' WHERE A.Renk2 <> ''
AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@Kategori IS NULL OR AB.Kategori = @Kategori)
AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu)
@@ -184,7 +211,7 @@ UNION ALL
SELECT 'urun_icerigi', X.FieldValue SELECT 'urun_icerigi', X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = AB.UrunIcerigi SELECT DISTINCT FieldValue = AB.UrunIcerigi
FROM @AttrBase AB FROM #AttrBase AB
WHERE AB.UrunIcerigi <> '' WHERE AB.UrunIcerigi <> ''
AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@Kategori IS NULL OR AB.Kategori = @Kategori)
AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu)
@@ -193,7 +220,7 @@ UNION ALL
SELECT 'fit', X.FieldValue SELECT 'fit', X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = AB.Fit SELECT DISTINCT FieldValue = AB.Fit
FROM @AttrBase AB FROM #AttrBase AB
WHERE AB.Fit <> '' WHERE AB.Fit <> ''
AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@Kategori IS NULL OR AB.Kategori = @Kategori)
AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu)
@@ -202,7 +229,7 @@ UNION ALL
SELECT 'drop', X.FieldValue SELECT 'drop', X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = AB.DropVal SELECT DISTINCT FieldValue = AB.DropVal
FROM @AttrBase AB FROM #AttrBase AB
WHERE AB.DropVal <> '' WHERE AB.DropVal <> ''
AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@Kategori IS NULL OR AB.Kategori = @Kategori)
AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu)
@@ -212,7 +239,7 @@ SELECT 'beden', X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = A.Beden SELECT DISTINCT FieldValue = A.Beden
FROM Avail A FROM Avail A
INNER JOIN @AttrBase AB ON AB.ProductCode = A.ItemCode INNER JOIN #AttrBase AB ON AB.ProductCode = A.ItemCode
WHERE A.Beden <> '' WHERE A.Beden <> ''
AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@Kategori IS NULL OR AB.Kategori = @Kategori)
AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu)
@@ -222,7 +249,8 @@ FROM (
AND (@Drop IS NULL OR AB.DropVal = @Drop) 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 (@Renk IS NULL OR (CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.Renk END) = @Renk)
AND (@Renk2 IS NULL OR A.Renk2 = @Renk2) AND (@Renk2 IS NULL OR A.Renk2 = @Renk2)
) X; ) X
OPTION (RECOMPILE);
` `
// GetProductStockQueryByAttributes: // GetProductStockQueryByAttributes:
@@ -238,8 +266,158 @@ DECLARE @Fit NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p7)), '');
DECLARE @Drop NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p8)), ''); DECLARE @Drop NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p8)), '');
DECLARE @Beden NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p9)), ''); 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 SELECT
ProductCode, ProductCode,
ProductDescription, ProductDescription,
@@ -280,8 +458,12 @@ DECLARE @Beden NVARCHAR(100) = NULLIF(LTRIM(RTRIM(@p9)), '');
AND (@UrunAltGrubu IS NULL OR ProductAtt02Desc = @UrunAltGrubu) AND (@UrunAltGrubu IS NULL OR ProductAtt02Desc = @UrunAltGrubu)
AND (@UrunIcerigi IS NULL OR ProductAtt41Desc = @UrunIcerigi) AND (@UrunIcerigi IS NULL OR ProductAtt41Desc = @UrunIcerigi)
AND (@Fit IS NULL OR ProductAtt38Desc = @Fit) 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 INV AS
( (
SELECT SELECT
@@ -307,9 +489,15 @@ INV AS
P.ItemTypeCode, P.ItemCode, P.ColorCode, P.ItemDim1Code, P.ItemDim2Code, P.ItemDim3Code, 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 P.Qty1 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1, 0 AS InventoryQty1
FROM PickingStates P 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 WHERE P.ItemTypeCode = 1
AND LEN(P.ItemCode) = 13 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 UNION ALL
SELECT SELECT
@@ -317,9 +505,15 @@ INV AS
R.ItemTypeCode, R.ItemCode, R.ColorCode, R.ItemDim1Code, R.ItemDim2Code, R.ItemDim3Code, R.ItemTypeCode, R.ItemCode, R.ColorCode, R.ItemDim1Code, R.ItemDim2Code, R.ItemDim3Code,
0, R.Qty1, 0, 0 0, R.Qty1, 0, 0
FROM ReserveStates R 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 WHERE R.ItemTypeCode = 1
AND LEN(R.ItemCode) = 13 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 UNION ALL
SELECT SELECT
@@ -327,9 +521,15 @@ INV AS
D.ItemTypeCode, D.ItemCode, D.ColorCode, D.ItemDim1Code, D.ItemDim2Code, D.ItemDim3Code, D.ItemTypeCode, D.ItemCode, D.ColorCode, D.ItemDim1Code, D.ItemDim2Code, D.ItemDim3Code,
0, 0, D.Qty1, 0 0, 0, D.Qty1, 0
FROM DispOrderStates D 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 WHERE D.ItemTypeCode = 1
AND LEN(D.ItemCode) = 13 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 UNION ALL
SELECT SELECT
@@ -337,9 +537,15 @@ INV AS
T.ItemTypeCode, T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemDim3Code, T.ItemTypeCode, T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemDim3Code,
0, 0, 0, SUM(T.In_Qty1 - T.Out_Qty1) 0, 0, 0, SUM(T.In_Qty1 - T.Out_Qty1)
FROM trStock T WITH (NOLOCK) 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 WHERE T.ItemTypeCode = 1
AND LEN(T.ItemCode) = 13 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 GROUP BY
T.CompanyCode, T.OfficeCode, T.StoreTypeCode, T.StoreCode, T.WarehouseCode, T.CompanyCode, T.OfficeCode, T.StoreTypeCode, T.StoreCode, T.WarehouseCode,
T.ItemTypeCode, T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemDim3Code T.ItemTypeCode, T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemDim3Code
@@ -391,7 +597,7 @@ Grouped AS
A.ColorCode, A.ColorCode,
A.ItemDim2Code A.ItemDim2Code
FROM Avail A 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) 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 (@Renk2 IS NULL OR A.ItemDim2Code = @Renk2)
AND ( AND (
@@ -454,7 +660,7 @@ INNER JOIN Grouped G
ON G.ItemCode = A.ItemCode ON G.ItemCode = A.ItemCode
AND G.ColorCode = A.ColorCode AND G.ColorCode = A.ColorCode
AND ISNULL(G.ItemDim2Code, '') = ISNULL(A.ItemDim2Code, '') AND ISNULL(G.ItemDim2Code, '') = ISNULL(A.ItemDim2Code, '')
INNER JOIN AttrFiltered AF INNER JOIN #AttrFiltered AF
ON AF.ProductCode = A.ItemCode ON AF.ProductCode = A.ItemCode
LEFT JOIN cdWarehouseDesc W WITH (NOLOCK) LEFT JOIN cdWarehouseDesc W WITH (NOLOCK)
ON W.WarehouseCode = A.WarehouseCode ON W.WarehouseCode = A.WarehouseCode
@@ -469,5 +675,6 @@ OUTER APPLY (
AND PB.ItemCode = A.ItemCode AND PB.ItemCode = A.ItemCode
AND LEN(PB.ItemCode) = 13 AND LEN(PB.ItemCode) = 13
ORDER BY PB.PriceDate DESC ORDER BY PB.PriceDate DESC
) P; ) P
OPTION (RECOMPILE);
` `

View File

@@ -23,11 +23,53 @@ type ProductImageItem struct {
ContentURL string `json:"content_url"` 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 // LIST PRODUCT IMAGES
// //
// GET /api/product-images?code=...&color=... // GET /api/product-images?code=...&color=...&yaka=...
func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc { func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { 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")) code := strings.TrimSpace(r.URL.Query().Get("code"))
color := strings.TrimSpace(r.URL.Query().Get("color")) 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 == "" { if code == "" {
@@ -67,18 +113,13 @@ JOIN mmitem i
WHERE b.typ = 'img' WHERE b.typ = 'img'
AND b.src_table = 'mmitem' AND b.src_table = 'mmitem'
AND UPPER(i.code) = UPPER($1) AND UPPER(i.code) = UPPER($1)
AND (
$2 = ''
OR b.file_name ILIKE '%' || '-' || $2 || '-%'
OR b.file_name ILIKE '%' || '-' || $2 || '_%'
)
ORDER BY ORDER BY
COALESCE(b.sort_order,999999), COALESCE(b.sort_order,999999),
b.zlins_dttm DESC, b.zlins_dttm DESC,
b.id DESC b.id DESC
` `
rows, err := pg.Query(query, code, color) rows, err := pg.Query(query, code)
if err != nil { if err != nil {
@@ -86,6 +127,7 @@ ORDER BY
"req_id", reqID, "req_id", reqID,
"code", code, "code", code,
"color", color, "color", color,
"second_color", secondColor,
"err", err.Error(), "err", err.Error(),
) )
@@ -109,6 +151,9 @@ ORDER BY
); err != nil { ); err != nil {
continue continue
} }
if !imageFileMatches(it.FileName, color, secondColor) {
continue
}
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID) it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
@@ -119,6 +164,7 @@ ORDER BY
"req_id", reqID, "req_id", reqID,
"code", code, "code", code,
"color", color, "color", color,
"second_color", secondColor,
"count", len(items), "count", len(items),
) )

View File

@@ -181,21 +181,29 @@
</div> </div>
<div class="sub-image level2-image"> <div class="sub-image level2-image">
<q-card flat bordered class="product-image-card"> <q-card flat bordered class="product-image-card cursor-pointer" @click.stop="openProductCard(grp1, grp2)">
<q-card-section class="q-pa-xs product-image-wrap"> <q-card-section class="q-pa-xs product-image-wrap">
<q-img <q-img
v-if="getProductImageUrl(grp1.productCode, grp2.colorCode)" v-if="getProductImageUrl(grp1.productCode, grp2.colorCode, grp2.secondColor)"
:src="getProductImageUrl(grp1.productCode, grp2.colorCode)" :src="getProductImageUrl(grp1.productCode, grp2.colorCode, grp2.secondColor)"
fit="cover" fit="contain"
class="product-image" class="product-image"
loading="lazy" loading="lazy"
@error="onProductImageError(grp1.productCode, grp2.colorCode)" @error="onProductImageError(grp1.productCode, grp2.colorCode, grp2.secondColor)"
/> />
<div v-else class="product-image-placeholder"> <div v-else class="product-image-placeholder">
<q-icon name="image_not_supported" size="22px" color="grey-6" /> <q-icon name="image_not_supported" size="22px" color="grey-6" />
</div> </div>
</q-card-section> </q-card-section>
</q-card> </q-card>
<q-btn
dense
flat
color="primary"
label="Urun Detayi Gor"
class="detail-open-btn"
@click.stop="openProductCard(grp1, grp2)"
/>
</div> </div>
</div> </div>
@@ -231,6 +239,99 @@
</template> </template>
</div> </div>
</div> </div>
<q-dialog v-model="productCardDialog" maximized>
<q-card class="product-card-dialog">
<q-card-section class="row items-center q-pb-sm">
<div class="text-h6">Urun Karti</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-separator />
<q-card-section class="q-pt-md">
<div class="product-card-stock">
<div class="text-subtitle1 text-weight-bold">
{{ productCardData.productCode || '-' }} / {{ productCardData.colorCode || '-' }}{{ productCardData.secondColor ? '-' + productCardData.secondColor : '' }}
</div>
<div class="text-caption">Toplam Stok: {{ formatNumber(productCardData.totalQty || 0) }}</div>
<div class="stock-size-grid q-mt-sm">
<div v-for="sz in sizeLabels" :key="'dlg-sz-' + sz" class="stock-size-chip">
<span class="label">{{ sz }}</span>
<span class="value">{{ Number(productCardData.sizeTotals?.[sz] || 0) > 0 ? formatNumber(productCardData.sizeTotals[sz]) : '-' }}</span>
</div>
</div>
</div>
<q-separator class="q-my-md" />
<div class="product-card-content">
<div class="product-card-images">
<q-carousel
v-if="productCardImages.length"
v-model="productCardSlide"
animated
swipeable
navigation
arrows
height="560px"
class="product-card-carousel rounded-borders bg-grey-2"
>
<q-carousel-slide
v-for="(img, idx) in productCardImages"
:key="'img-' + idx"
:name="idx"
class="column no-wrap flex-center"
>
<div class="dialog-image-stage cursor-pointer" @click="openProductImageFullscreen(img)">
<q-img :src="img" fit="contain" class="dialog-image" />
</div>
</q-carousel-slide>
</q-carousel>
<div v-else class="dialog-image-empty">
<q-icon name="image_not_supported" size="36px" color="grey-6" />
</div>
</div>
<div class="product-card-fields">
<div class="field-row"><span class="k">Urun Kodu</span><span class="v">{{ productCardData.productCode || '-' }}</span></div>
<div class="field-row"><span class="k">Urun Renk</span><span class="v">{{ productCardData.colorCode || '-' }}</span></div>
<div class="field-row"><span class="k">Urun 2.Renk</span><span class="v">{{ productCardData.secondColor || '-' }}</span></div>
<div class="field-row"><span class="k">Kategori</span><span class="v">{{ productCardData.kategori || '-' }}</span></div>
<div class="field-row"><span class="k">Urun Ana Grubu</span><span class="v">{{ productCardData.urunAnaGrubu || '-' }}</span></div>
<div class="field-row"><span class="k">Urun Alt Grubu</span><span class="v">{{ productCardData.urunAltGrubu || '-' }}</span></div>
<div class="field-row"><span class="k">Urun Icerigi</span><span class="v">{{ productCardData.urunIcerigi || '-' }}</span></div>
<div class="field-row"><span class="k">Fit</span><span class="v">{{ productCardData.fit || '-' }}</span></div>
<div class="field-row"><span class="k">Drop</span><span class="v">{{ productCardData.drop || '-' }}</span></div>
<div class="field-row"><span class="k">Kumas</span><span class="v">{{ productCardData.kumas || '-' }}</span></div>
<div class="field-row"><span class="k">Karisim</span><span class="v">{{ productCardData.karisim || '-' }}</span></div>
</div>
</div>
</q-card-section>
</q-card>
</q-dialog>
<q-dialog v-model="productImageFullscreenDialog" maximized>
<q-card class="image-fullscreen-dialog">
<q-card-section class="row items-center q-pb-sm">
<div class="text-h6">Urun Fotografi</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-separator />
<q-card-section class="image-fullscreen-body">
<div class="image-fullscreen-stage cursor-pointer" @click="toggleFullscreenImageZoom">
<q-img
:src="productImageFullscreenSrc"
fit="contain"
class="image-fullscreen-img"
:style="fullscreenImageStyle"
/>
</div>
</q-card-section>
</q-card>
</q-dialog>
</q-page> </q-page>
<q-page v-else class="q-pa-md flex flex-center"> <q-page v-else class="q-pa-md flex flex-center">
@@ -284,6 +385,7 @@ const filters = ref({
}) })
const optionLists = ref({}) const optionLists = ref({})
const filteredOptionLists = ref({}) const filteredOptionLists = ref({})
const filterOptionsCache = ref({})
const rawRows = ref([]) const rawRows = ref([])
const productImageCache = ref({}) const productImageCache = ref({})
const productImageLoading = ref({}) const productImageLoading = ref({})
@@ -293,8 +395,19 @@ const productImageFallbackByKey = ref({})
const productImageContentLoading = ref({}) const productImageContentLoading = ref({})
const productImageBlobUrls = ref([]) const productImageBlobUrls = ref([])
const productImageListBlockedUntil = ref(0) 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 IMAGE_LIST_CONCURRENCY = 8
const FILTER_OPTIONS_CACHE_TTL_MS = 60 * 1000
const FILTER_OPTIONS_DEBOUNCE_MS = 250
let imageListActiveRequests = 0 let imageListActiveRequests = 0
let filterOptionsDebounceTimer = null
let filterOptionsRequestSeq = 0
const imageListWaitQueue = [] const imageListWaitQueue = []
const activeSchema = ref(storeSchemaByKey.tak) const activeSchema = ref(storeSchemaByKey.tak)
const activeGrpKey = ref('tak') const activeGrpKey = ref('tak')
@@ -324,6 +437,11 @@ const allDetailsExpanded = computed(() => {
const gridHeaderHeight = computed(() => const gridHeaderHeight = computed(() =>
showGridHeader.value ? '56px' : '0px' showGridHeader.value ? '56px' : '0px'
) )
const fullscreenImageStyle = computed(() => ({
transform: `scale(${productImageFullscreenZoom.value})`,
transformOrigin: 'center center',
transition: 'transform 0.15s ease-out'
}))
function emptySizeTotals() { function emptySizeTotals() {
const map = {} const map = {}
@@ -347,8 +465,20 @@ function sortByTotalQtyDesc(a, b) {
return String(a?.key || '').localeCompare(String(b?.key || ''), 'tr', { sensitivity: 'base' }) return String(a?.key || '').localeCompare(String(b?.key || ''), 'tr', { sensitivity: 'base' })
} }
function buildImageKey(code, color) { function buildImageKey(code, color, secondColor = '') {
return `${String(code || '').trim().toUpperCase()}::${String(color || '').trim().toUpperCase()}` 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) { function normalizeUploadsPath(storagePath) {
@@ -403,17 +533,17 @@ function resolveProductImageUrl(item) {
return { contentUrl, publicUrl } return { contentUrl, publicUrl }
} }
function getProductImageUrl(code, color) { function getProductImageUrl(code, color, secondColor = '') {
const key = buildImageKey(code, color) const key = buildImageKey(code, color, secondColor)
const existing = productImageCache.value[key] const existing = productImageCache.value[key]
if (existing !== undefined) return existing || '' if (existing !== undefined) return existing || ''
void ensureProductImage(code, color) void ensureProductImage(code, color, secondColor)
return '' return ''
} }
async function onProductImageError(code, color) { async function onProductImageError(code, color, secondColor = '') {
const key = buildImageKey(code, color) const key = buildImageKey(code, color, secondColor)
const fallback = String(productImageFallbackByKey.value[key] || '') const fallback = String(productImageFallbackByKey.value[key] || '')
if (fallback && !productImageContentLoading.value[key]) { if (fallback && !productImageContentLoading.value[key]) {
productImageContentLoading.value[key] = true productImageContentLoading.value[key] = true
@@ -438,10 +568,12 @@ async function onProductImageError(code, color) {
productImageCache.value[key] = '' productImageCache.value[key] = ''
} }
async function ensureProductImage(code, color) { async function ensureProductImage(code, color, secondColor = '') {
const key = buildImageKey(code, color) const key = buildImageKey(code, color, secondColor)
const codeTrim = String(code || '').trim().toUpperCase() const codeTrim = String(code || '').trim().toUpperCase()
const colorTrim = String(color || '').trim().toUpperCase() const colorTrim = String(color || '').trim().toUpperCase()
const secondTrim = String(secondColor || '').trim().toUpperCase()
const listKey = buildImageKey(codeTrim, colorTrim, secondTrim)
if (!codeTrim) { if (!codeTrim) {
productImageCache.value[key] = '' productImageCache.value[key] = ''
return '' return ''
@@ -455,44 +587,44 @@ async function ensureProductImage(code, color) {
productImageLoading.value[key] = true productImageLoading.value[key] = true
try { try {
if (!productImageListByCode.value[codeTrim]) { if (!productImageListByCode.value[listKey]) {
if (!productImageListLoading.value[codeTrim]) { if (!productImageListLoading.value[listKey]) {
productImageListLoading.value[codeTrim] = true productImageListLoading.value[listKey] = true
try { try {
if (imageListActiveRequests >= IMAGE_LIST_CONCURRENCY) { if (imageListActiveRequests >= IMAGE_LIST_CONCURRENCY) {
await new Promise((resolve) => imageListWaitQueue.push(resolve)) await new Promise((resolve) => imageListWaitQueue.push(resolve))
} }
imageListActiveRequests++ imageListActiveRequests++
const res = await api.get('/product-images', { params: { code: codeTrim } }) const params = { code: codeTrim, dim1: colorTrim, dim3: secondTrim }
productImageListByCode.value[codeTrim] = Array.isArray(res?.data) ? res.data : [] const res = await api.get('/product-images', { params })
productImageListByCode.value[listKey] = Array.isArray(res?.data) ? res.data : []
} catch (err) { } catch (err) {
productImageListByCode.value[codeTrim] = [] productImageListByCode.value[listKey] = []
const status = Number(err?.response?.status || 0) const status = Number(err?.response?.status || 0)
if (status >= 500 || status === 403 || status === 0) { if (status >= 500 || status === 403 || status === 0) {
// Backend dengesizken istek firtinasini kisaca kes. // Backend dengesizken istek firtinasini kisaca kes.
productImageListBlockedUntil.value = Date.now() + 30 * 1000 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 { } finally {
imageListActiveRequests = Math.max(0, imageListActiveRequests - 1) imageListActiveRequests = Math.max(0, imageListActiveRequests - 1)
const nextInQueue = imageListWaitQueue.shift() const nextInQueue = imageListWaitQueue.shift()
if (nextInQueue) nextInQueue() if (nextInQueue) nextInQueue()
delete productImageListLoading.value[codeTrim] delete productImageListLoading.value[listKey]
} }
} else { } else {
// Ayni code icin baska bir istek zaten calisiyorsa tamamlanmasini bekle. // 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)) await new Promise((resolve) => setTimeout(resolve, 25))
} }
} }
} }
const list = productImageListByCode.value[codeTrim] || [] const list = productImageListByCode.value[listKey] || []
let first = null let first = null
if (colorTrim) { if (colorTrim || secondTrim) {
const needle = `-${colorTrim.toLowerCase()}-`
first = list.find((item) => first = list.find((item) =>
String(item?.file_name || item?.FileName || '').toLowerCase().includes(needle) imageNameMatches(String(item?.file_name || item?.FileName || ''), colorTrim, secondTrim)
) || null ) || null
} }
if (!first) first = list[0] || null if (!first) first = list[0] || null
@@ -625,6 +757,11 @@ const level1Groups = computed(() => {
const depoAdi = String(item.Depo_Adi || '').trim() const depoAdi = String(item.Depo_Adi || '').trim()
const urunAnaGrubu = String(item.URUN_ANA_GRUBU || '').trim() const urunAnaGrubu = String(item.URUN_ANA_GRUBU || '').trim()
const urunAltGrubu = String(item.URUN_ALT_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 aciklama = String(item.Madde_Aciklamasi || '').trim()
const beden = normalizeSize(item.Beden || '') const beden = normalizeSize(item.Beden || '')
const qty = parseNumber(item.Kullanilabilir_Envanter) const qty = parseNumber(item.Kullanilabilir_Envanter)
@@ -654,6 +791,11 @@ const level1Groups = computed(() => {
secondColor, secondColor,
urunAnaGrubu, urunAnaGrubu,
urunAltGrubu, urunAltGrubu,
urunIcerigi,
fit,
drop,
kumas,
karisim,
aciklama, aciklama,
sizeTotals: emptySizeTotals(), sizeTotals: emptySizeTotals(),
totalQty: 0, totalQty: 0,
@@ -714,6 +856,11 @@ function buildFilterParams() {
return out return out
} }
function buildFilterCacheKey(params) {
const keys = Object.keys(params || {}).sort()
return keys.map((k) => `${k}=${String(params[k] || '').trim()}`).join('&')
}
function isFilterDisabled(key) { function isFilterDisabled(key) {
if (key === 'kategori') return false if (key === 'kategori') return false
if (key === 'urun_ana_grubu') { if (key === 'urun_ana_grubu') {
@@ -747,7 +894,12 @@ function onFilterValueChange(changedKey) {
filters.value.beden = '' filters.value.beden = ''
} }
if (filterOptionsDebounceTimer) {
clearTimeout(filterOptionsDebounceTimer)
}
filterOptionsDebounceTimer = setTimeout(() => {
void loadFilterOptions() void loadFilterOptions()
}, FILTER_OPTIONS_DEBOUNCE_MS)
} }
function filterOptions(field, val, update) { 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 loadingFilterOptions.value = true
try { try {
const res = await api.get('/product-stock-attribute-options', { 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 payload = res?.data && typeof res.data === 'object' ? res.data : {}
const next = {} const next = {}
const nextFiltered = {} const nextFiltered = {}
@@ -795,12 +961,22 @@ async function loadFilterOptions() {
optionLists.value = next optionLists.value = next
filteredOptionLists.value = nextFiltered filteredOptionLists.value = nextFiltered
filterOptionsCache.value[cacheKey] = {
expiresAt: now + FILTER_OPTIONS_CACHE_TTL_MS,
payload: {
optionLists: next,
filteredOptionLists: nextFiltered
}
}
} catch (err) { } catch (err) {
if (reqSeq !== filterOptionsRequestSeq) return
errorMessage.value = 'Urun ozellik secenekleri alinamadi.' errorMessage.value = 'Urun ozellik secenekleri alinamadi.'
console.error('loadFilterOptions error:', err) console.error('loadFilterOptions error:', err)
} finally { } finally {
if (reqSeq === filterOptionsRequestSeq) {
loadingFilterOptions.value = false loadingFilterOptions.value = false
} }
}
} }
async function fetchStockByAttributes() { async function fetchStockByAttributes() {
@@ -871,10 +1047,65 @@ async function fetchStockByAttributes() {
function onLevel2Click(productCode, grp2) { function onLevel2Click(productCode, grp2) {
toggleOpen(grp2.key) toggleOpen(grp2.key)
if (isOpen(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() { function resetForm() {
filters.value = { filters.value = {
kategori: '', kategori: '',
@@ -898,14 +1129,26 @@ function resetForm() {
productImageFallbackByKey.value = {} productImageFallbackByKey.value = {}
productImageContentLoading.value = {} productImageContentLoading.value = {}
productImageListBlockedUntil.value = 0 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(() => { onMounted(() => {
loadFilterOptions() void loadFilterOptions(true)
}) })
onUnmounted(() => { onUnmounted(() => {
if (filterOptionsDebounceTimer) {
clearTimeout(filterOptionsDebounceTimer)
filterOptionsDebounceTimer = null
}
for (const url of productImageBlobUrls.value) { for (const url of productImageBlobUrls.value) {
try { URL.revokeObjectURL(url) } catch {} try { URL.revokeObjectURL(url) } catch {}
} }
@@ -920,7 +1163,7 @@ onUnmounted(() => {
--grp-title-w: 44px; --grp-title-w: 44px;
--psq-header-h: 56px; --psq-header-h: 56px;
--psq-col-adet: calc(var(--col-adet) + var(--beden-w)); --psq-col-adet: calc(var(--col-adet) + var(--beden-w));
--psq-col-img: 126px; --psq-col-img: 190px;
--psq-l1-lift: 42px; --psq-l1-lift: 42px;
} }
@@ -1003,8 +1246,8 @@ onUnmounted(() => {
} }
.order-sub-header.level-2 { .order-sub-header.level-2 {
min-height: 82px !important; min-height: 252px !important;
height: 82px !important; height: 252px !important;
background: #fff9c4 !important; background: #fff9c4 !important;
border-top: 1px solid #d4c79f !important; border-top: 1px solid #d4c79f !important;
border-bottom: 1px solid #d4c79f !important; border-bottom: 1px solid #d4c79f !important;
@@ -1250,16 +1493,18 @@ onUnmounted(() => {
.order-sub-header.level-2 .sub-image.level2-image { .order-sub-header.level-2 .sub-image.level2-image {
grid-column: 8 / 9; grid-column: 8 / 9;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-left: 1px solid #d4c79f; border-left: 1px solid #d4c79f;
padding: 0 6px; gap: 8px;
padding: 8px 10px;
background: #fffef7; background: #fffef7;
} }
.product-image-card { .product-image-card {
width: 108px; width: 162px;
height: 66px; height: 216px;
border-radius: 8px; border-radius: 8px;
} }
@@ -1272,6 +1517,7 @@ onUnmounted(() => {
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 6px; border-radius: 6px;
background: #fff;
} }
.product-image-placeholder { .product-image-placeholder {
@@ -1284,6 +1530,178 @@ onUnmounted(() => {
border-radius: 6px; 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 { .order-sub-header.level-2 .sub-right .top-total {
grid-column: 1 / 2; grid-column: 1 / 2;
grid-row: 1; grid-row: 1;

View File

@@ -179,21 +179,29 @@
</div> </div>
<div class="sub-image level2-image"> <div class="sub-image level2-image">
<q-card flat bordered class="product-image-card"> <q-card flat bordered class="product-image-card cursor-pointer" @click.stop="openProductCard(grp1, grp2)">
<q-card-section class="q-pa-xs product-image-wrap"> <q-card-section class="q-pa-xs product-image-wrap">
<q-img <q-img
v-if="getProductImageUrl(grp1.productCode, grp2.colorCode)" v-if="getProductImageUrl(grp1.productCode, grp2.colorCode, grp2.secondColor)"
:src="getProductImageUrl(grp1.productCode, grp2.colorCode)" :src="getProductImageUrl(grp1.productCode, grp2.colorCode, grp2.secondColor)"
fit="cover" fit="contain"
class="product-image" class="product-image"
loading="lazy" loading="lazy"
@error="onProductImageError(grp1.productCode, grp2.colorCode)" @error="onProductImageError(grp1.productCode, grp2.colorCode, grp2.secondColor)"
/> />
<div v-else class="product-image-placeholder"> <div v-else class="product-image-placeholder">
<q-icon name="image_not_supported" size="22px" color="grey-6" /> <q-icon name="image_not_supported" size="22px" color="grey-6" />
</div> </div>
</q-card-section> </q-card-section>
</q-card> </q-card>
<q-btn
dense
flat
color="primary"
label="Urun Detayi Gor"
class="detail-open-btn"
@click.stop="openProductCard(grp1, grp2)"
/>
</div> </div>
</div> </div>
@@ -229,6 +237,99 @@
</template> </template>
</div> </div>
</div> </div>
<q-dialog v-model="productCardDialog" maximized>
<q-card class="product-card-dialog">
<q-card-section class="row items-center q-pb-sm">
<div class="text-h6">Urun Karti</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-separator />
<q-card-section class="q-pt-md">
<div class="product-card-stock">
<div class="text-subtitle1 text-weight-bold">
{{ productCardData.productCode || '-' }} / {{ productCardData.colorCode || '-' }}{{ productCardData.secondColor ? '-' + productCardData.secondColor : '' }}
</div>
<div class="text-caption">Toplam Stok: {{ formatNumber(productCardData.totalQty || 0) }}</div>
<div class="stock-size-grid q-mt-sm">
<div v-for="sz in sizeLabels" :key="'dlg-sz-' + sz" class="stock-size-chip">
<span class="label">{{ sz }}</span>
<span class="value">{{ Number(productCardData.sizeTotals?.[sz] || 0) > 0 ? formatNumber(productCardData.sizeTotals[sz]) : '-' }}</span>
</div>
</div>
</div>
<q-separator class="q-my-md" />
<div class="product-card-content">
<div class="product-card-images">
<q-carousel
v-if="productCardImages.length"
v-model="productCardSlide"
animated
swipeable
navigation
arrows
height="560px"
class="product-card-carousel rounded-borders bg-grey-2"
>
<q-carousel-slide
v-for="(img, idx) in productCardImages"
:key="'img-' + idx"
:name="idx"
class="column no-wrap flex-center"
>
<div class="dialog-image-stage cursor-pointer" @click="openProductImageFullscreen(img)">
<q-img :src="img" fit="contain" class="dialog-image" />
</div>
</q-carousel-slide>
</q-carousel>
<div v-else class="dialog-image-empty">
<q-icon name="image_not_supported" size="36px" color="grey-6" />
</div>
</div>
<div class="product-card-fields">
<div class="field-row"><span class="k">Urun Kodu</span><span class="v">{{ productCardData.productCode || '-' }}</span></div>
<div class="field-row"><span class="k">Urun Renk</span><span class="v">{{ productCardData.colorCode || '-' }}</span></div>
<div class="field-row"><span class="k">Urun 2.Renk</span><span class="v">{{ productCardData.secondColor || '-' }}</span></div>
<div class="field-row"><span class="k">Kategori</span><span class="v">{{ productCardData.kategori || '-' }}</span></div>
<div class="field-row"><span class="k">Urun Ana Grubu</span><span class="v">{{ productCardData.urunAnaGrubu || '-' }}</span></div>
<div class="field-row"><span class="k">Urun Alt Grubu</span><span class="v">{{ productCardData.urunAltGrubu || '-' }}</span></div>
<div class="field-row"><span class="k">Urun Icerigi</span><span class="v">{{ productCardData.urunIcerigi || '-' }}</span></div>
<div class="field-row"><span class="k">Fit</span><span class="v">{{ productCardData.fit || '-' }}</span></div>
<div class="field-row"><span class="k">Drop</span><span class="v">{{ productCardData.drop || '-' }}</span></div>
<div class="field-row"><span class="k">Kumas</span><span class="v">{{ productCardData.kumas || '-' }}</span></div>
<div class="field-row"><span class="k">Karisim</span><span class="v">{{ productCardData.karisim || '-' }}</span></div>
</div>
</div>
</q-card-section>
</q-card>
</q-dialog>
<q-dialog v-model="productImageFullscreenDialog" maximized>
<q-card class="image-fullscreen-dialog">
<q-card-section class="row items-center q-pb-sm">
<div class="text-h6">Urun Fotografi</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-separator />
<q-card-section class="image-fullscreen-body">
<div class="image-fullscreen-stage cursor-pointer" @click="toggleFullscreenImageZoom">
<q-img
:src="productImageFullscreenSrc"
fit="contain"
class="image-fullscreen-img"
:style="fullscreenImageStyle"
/>
</div>
</q-card-section>
</q-card>
</q-dialog>
</q-page> </q-page>
<q-page v-else class="q-pa-md flex flex-center"> <q-page v-else class="q-pa-md flex flex-center">
@@ -269,6 +370,13 @@ const productImageFallbackByKey = ref({})
const productImageContentLoading = ref({}) const productImageContentLoading = ref({})
const productImageBlobUrls = ref([]) const productImageBlobUrls = ref([])
const productImageListBlockedUntil = ref(0) 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 IMAGE_LIST_CONCURRENCY = 8
let imageListActiveRequests = 0 let imageListActiveRequests = 0
const imageListWaitQueue = [] const imageListWaitQueue = []
@@ -296,6 +404,11 @@ const allDetailsExpanded = computed(() => {
const gridHeaderHeight = computed(() => const gridHeaderHeight = computed(() =>
showGridHeader.value ? '56px' : '0px' showGridHeader.value ? '56px' : '0px'
) )
const fullscreenImageStyle = computed(() => ({
transform: `scale(${productImageFullscreenZoom.value})`,
transformOrigin: 'center center',
transition: 'transform 0.15s ease-out'
}))
function emptySizeTotals() { function emptySizeTotals() {
const map = {} const map = {}
@@ -312,8 +425,53 @@ function parseNumber(value) {
return Number.isFinite(n) ? n : 0 return Number.isFinite(n) ? n : 0
} }
function buildImageKey(code, color) { function sortByColorCodeAsc(a, b) {
return `${String(code || '').trim().toUpperCase()}::${String(color || '').trim().toUpperCase()}` 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) { function normalizeUploadsPath(storagePath) {
@@ -358,16 +516,16 @@ function resolveProductImageUrl(item) {
return { contentUrl, publicUrl } return { contentUrl, publicUrl }
} }
function getProductImageUrl(code, color) { function getProductImageUrl(code, color, secondColor = '') {
const key = buildImageKey(code, color) const key = buildImageKey(code, color, secondColor)
const existing = productImageCache.value[key] const existing = productImageCache.value[key]
if (existing !== undefined) return existing || '' if (existing !== undefined) return existing || ''
void ensureProductImage(code, color) void ensureProductImage(code, color, secondColor)
return '' return ''
} }
async function onProductImageError(code, color) { async function onProductImageError(code, color, secondColor = '') {
const key = buildImageKey(code, color) const key = buildImageKey(code, color, secondColor)
const fallback = String(productImageFallbackByKey.value[key] || '') const fallback = String(productImageFallbackByKey.value[key] || '')
if (fallback && !productImageContentLoading.value[key]) { if (fallback && !productImageContentLoading.value[key]) {
productImageContentLoading.value[key] = true productImageContentLoading.value[key] = true
@@ -389,10 +547,12 @@ async function onProductImageError(code, color) {
productImageCache.value[key] = '' productImageCache.value[key] = ''
} }
async function ensureProductImage(code, color) { async function ensureProductImage(code, color, secondColor = '') {
const key = buildImageKey(code, color) const key = buildImageKey(code, color, secondColor)
const codeTrim = String(code || '').trim().toUpperCase() const codeTrim = String(code || '').trim().toUpperCase()
const colorTrim = String(color || '').trim().toUpperCase() const colorTrim = String(color || '').trim().toUpperCase()
const secondTrim = String(secondColor || '').trim().toUpperCase()
const listKey = buildImageKey(codeTrim, colorTrim, secondTrim)
if (!codeTrim) { if (!codeTrim) {
productImageCache.value[key] = '' productImageCache.value[key] = ''
return '' return ''
@@ -406,42 +566,42 @@ async function ensureProductImage(code, color) {
productImageLoading.value[key] = true productImageLoading.value[key] = true
try { try {
if (!productImageListByCode.value[codeTrim]) { if (!productImageListByCode.value[listKey]) {
if (!productImageListLoading.value[codeTrim]) { if (!productImageListLoading.value[listKey]) {
productImageListLoading.value[codeTrim] = true productImageListLoading.value[listKey] = true
try { try {
if (imageListActiveRequests >= IMAGE_LIST_CONCURRENCY) { if (imageListActiveRequests >= IMAGE_LIST_CONCURRENCY) {
await new Promise((resolve) => imageListWaitQueue.push(resolve)) await new Promise((resolve) => imageListWaitQueue.push(resolve))
} }
imageListActiveRequests++ imageListActiveRequests++
const res = await api.get('/product-images', { params: { code: codeTrim } }) const params = { code: codeTrim, dim1: colorTrim, dim3: secondTrim }
productImageListByCode.value[codeTrim] = Array.isArray(res?.data) ? res.data : [] const res = await api.get('/product-images', { params })
productImageListByCode.value[listKey] = Array.isArray(res?.data) ? res.data : []
} catch (err) { } catch (err) {
productImageListByCode.value[codeTrim] = [] productImageListByCode.value[listKey] = []
const status = Number(err?.response?.status || 0) const status = Number(err?.response?.status || 0)
if (status >= 500 || status === 403 || status === 0) { if (status >= 500 || status === 403 || status === 0) {
productImageListBlockedUntil.value = Date.now() + 30 * 1000 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 { } finally {
imageListActiveRequests = Math.max(0, imageListActiveRequests - 1) imageListActiveRequests = Math.max(0, imageListActiveRequests - 1)
const nextInQueue = imageListWaitQueue.shift() const nextInQueue = imageListWaitQueue.shift()
if (nextInQueue) nextInQueue() if (nextInQueue) nextInQueue()
delete productImageListLoading.value[codeTrim] delete productImageListLoading.value[listKey]
} }
} else { } else {
while (productImageListLoading.value[codeTrim]) { while (productImageListLoading.value[listKey]) {
await new Promise((resolve) => setTimeout(resolve, 25)) await new Promise((resolve) => setTimeout(resolve, 25))
} }
} }
} }
const list = productImageListByCode.value[codeTrim] || [] const list = productImageListByCode.value[listKey] || []
let first = null let first = null
if (colorTrim) { if (colorTrim || secondTrim) {
const needle = `-${colorTrim.toLowerCase()}-`
first = list.find((item) => first = list.find((item) =>
String(item?.file_name || item?.FileName || '').toLowerCase().includes(needle) imageNameMatches(String(item?.file_name || item?.FileName || ''), colorTrim, secondTrim)
) || null ) || null
} }
if (!first) first = list[0] || null if (!first) first = list[0] || null
@@ -571,8 +731,14 @@ const level1Groups = computed(() => {
const secondColor = String(item.Yaka || '').trim() const secondColor = String(item.Yaka || '').trim()
const depoKodu = String(item.Depo_Kodu || '').trim() const depoKodu = String(item.Depo_Kodu || '').trim()
const depoAdi = String(item.Depo_Adi || '').trim() const depoAdi = String(item.Depo_Adi || '').trim()
const kategori = String(item.YETISKIN_GARSON || '').trim()
const urunAnaGrubu = String(item.URUN_ANA_GRUBU || '').trim() const urunAnaGrubu = String(item.URUN_ANA_GRUBU || '').trim()
const urunAltGrubu = String(item.URUN_ALT_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 aciklama = String(item.Madde_Aciklamasi || '').trim()
const beden = normalizeSize(item.Beden || '') const beden = normalizeSize(item.Beden || '')
const qty = parseNumber(item.Kullanilabilir_Envanter) const qty = parseNumber(item.Kullanilabilir_Envanter)
@@ -600,8 +766,14 @@ const level1Groups = computed(() => {
colorCode, colorCode,
colorDesc, colorDesc,
secondColor, secondColor,
kategori,
urunAnaGrubu, urunAnaGrubu,
urunAltGrubu, urunAltGrubu,
urunIcerigi,
fit,
drop,
kumas,
karisim,
aciklama, aciklama,
sizeTotals: emptySizeTotals(), sizeTotals: emptySizeTotals(),
totalQty: 0, totalQty: 0,
@@ -638,10 +810,12 @@ const level1Groups = computed(() => {
return Array.from(l1Map.values()).map((l1) => ({ return Array.from(l1Map.values()).map((l1) => ({
...l1, ...l1,
children: Array.from(l1.childrenMap.values()).map((l2) => ({ children: Array.from(l1.childrenMap.values())
.map((l2) => ({
...l2, ...l2,
children: Array.from(l2.childrenMap.values()) children: Array.from(l2.childrenMap.values())
})) }))
.sort(sortByColorCodeAsc)
})) }))
}) })
@@ -740,10 +914,65 @@ async function fetchStockByCode() {
function onLevel2Click(productCode, grp2) { function onLevel2Click(productCode, grp2) {
toggleOpen(grp2.key) toggleOpen(grp2.key)
if (isOpen(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() { function resetForm() {
selectedProductCode.value = '' selectedProductCode.value = ''
rawRows.value = [] rawRows.value = []
@@ -757,6 +986,13 @@ function resetForm() {
productImageFallbackByKey.value = {} productImageFallbackByKey.value = {}
productImageContentLoading.value = {} productImageContentLoading.value = {}
productImageListBlockedUntil.value = 0 productImageListBlockedUntil.value = 0
productCardDialog.value = false
productCardData.value = {}
productCardImages.value = []
productCardSlide.value = 0
productImageFullscreenDialog.value = false
productImageFullscreenSrc.value = ''
productImageFullscreenZoom.value = 1
} }
onMounted(() => { onMounted(() => {
@@ -777,7 +1013,7 @@ onUnmounted(() => {
--grp-title-w: 44px; --grp-title-w: 44px;
--psq-header-h: 56px; --psq-header-h: 56px;
--psq-col-adet: calc(var(--col-adet) + var(--beden-w)); --psq-col-adet: calc(var(--col-adet) + var(--beden-w));
--psq-col-img: 126px; --psq-col-img: 190px;
--psq-l1-lift: 42px; --psq-l1-lift: 42px;
} }
@@ -860,8 +1096,8 @@ onUnmounted(() => {
} }
.order-sub-header.level-2 { .order-sub-header.level-2 {
min-height: 82px !important; min-height: 252px !important;
height: 82px !important; height: 252px !important;
background: #fff9c4 !important; background: #fff9c4 !important;
border-top: 1px solid #d4c79f !important; border-top: 1px solid #d4c79f !important;
border-bottom: 1px solid #d4c79f !important; border-bottom: 1px solid #d4c79f !important;
@@ -1107,15 +1343,17 @@ onUnmounted(() => {
.order-sub-header.level-2 .sub-image.level2-image { .order-sub-header.level-2 .sub-image.level2-image {
grid-column: 8 / 9; grid-column: 8 / 9;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 6px; gap: 8px;
padding: 8px 10px;
border-left: 1px solid #d4c79f; border-left: 1px solid #d4c79f;
} }
.product-image-card { .product-image-card {
width: 110px; width: 162px;
height: 68px; height: 216px;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
} }
@@ -1128,6 +1366,7 @@ onUnmounted(() => {
.product-image { .product-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
background: #fff;
} }
.product-image-placeholder { .product-image-placeholder {
@@ -1139,6 +1378,178 @@ onUnmounted(() => {
background: #f5f6f7; 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 { .order-sub-header.level-2 .sub-right .top-total {
grid-column: 1 / 2; grid-column: 1 / 2;
grid-row: 1; grid-row: 1;