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.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,

View File

@@ -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);
`

View File

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

View File

@@ -181,21 +181,29 @@
</div>
<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-img
v-if="getProductImageUrl(grp1.productCode, grp2.colorCode)"
:src="getProductImageUrl(grp1.productCode, grp2.colorCode)"
fit="cover"
v-if="getProductImageUrl(grp1.productCode, grp2.colorCode, grp2.secondColor)"
:src="getProductImageUrl(grp1.productCode, grp2.colorCode, grp2.secondColor)"
fit="contain"
class="product-image"
loading="lazy"
@error="onProductImageError(grp1.productCode, grp2.colorCode)"
@error="onProductImageError(grp1.productCode, grp2.colorCode, grp2.secondColor)"
/>
<div v-else class="product-image-placeholder">
<q-icon name="image_not_supported" size="22px" color="grey-6" />
</div>
</q-card-section>
</q-card>
<q-btn
dense
flat
color="primary"
label="Urun Detayi Gor"
class="detail-open-btn"
@click.stop="openProductCard(grp1, grp2)"
/>
</div>
</div>
@@ -231,6 +239,99 @@
</template>
</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 v-else class="q-pa-md flex flex-center">
@@ -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;

View File

@@ -179,21 +179,29 @@
</div>
<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-img
v-if="getProductImageUrl(grp1.productCode, grp2.colorCode)"
:src="getProductImageUrl(grp1.productCode, grp2.colorCode)"
fit="cover"
v-if="getProductImageUrl(grp1.productCode, grp2.colorCode, grp2.secondColor)"
:src="getProductImageUrl(grp1.productCode, grp2.colorCode, grp2.secondColor)"
fit="contain"
class="product-image"
loading="lazy"
@error="onProductImageError(grp1.productCode, grp2.colorCode)"
@error="onProductImageError(grp1.productCode, grp2.colorCode, grp2.secondColor)"
/>
<div v-else class="product-image-placeholder">
<q-icon name="image_not_supported" size="22px" color="grey-6" />
</div>
</q-card-section>
</q-card>
<q-btn
dense
flat
color="primary"
label="Urun Detayi Gor"
class="detail-open-btn"
@click.stop="openProductCard(grp1, grp2)"
/>
</div>
</div>
@@ -229,6 +237,99 @@
</template>
</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 v-else class="q-pa-md flex flex-center">
@@ -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;