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