Compare commits
17 Commits
27a5f473f2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1054a15547 | ||
|
|
da9d7c2fd5 | ||
|
|
539ca4b587 | ||
|
|
10f90cbaf3 | ||
|
|
13c9df1e96 | ||
|
|
462fb4058d | ||
|
|
a2f70160bc | ||
|
|
3732004a29 | ||
|
|
c8c37b4e69 | ||
|
|
ad8d459491 | ||
|
|
9b57e0846e | ||
|
|
55e36366c3 | ||
|
|
7512e7fe7c | ||
|
|
81d1af61be | ||
|
|
b59889bbdb | ||
|
|
ef33a56a49 | ||
|
|
49863d7569 |
@@ -0,0 +1,21 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS mk_order_price_list_user_price_group (
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
price_group TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (user_id, price_group)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_order_price_list_user_price_group_user
|
||||||
|
ON mk_order_price_list_user_price_group (user_id);
|
||||||
|
|
||||||
|
ALTER TABLE mk_order_price_list_user_price_group
|
||||||
|
DROP CONSTRAINT IF EXISTS ck_order_price_list_user_price_group;
|
||||||
|
|
||||||
|
ALTER TABLE mk_order_price_list_user_price_group
|
||||||
|
ADD CONSTRAINT ck_order_price_list_user_price_group
|
||||||
|
CHECK (price_group IN (
|
||||||
|
'usd1','usd2','usd3','usd4','usd5','usd6',
|
||||||
|
'eur1','eur2','eur3','eur4','eur5','eur6',
|
||||||
|
'try1','try2','try3','try4','try5','try6'
|
||||||
|
));
|
||||||
55
svc/main.go
55
svc/main.go
@@ -360,6 +360,36 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"system", "update",
|
"system", "update",
|
||||||
wrapV3(routes.SavePricingFirstGroupMailMappingHandler(pgDB)),
|
wrapV3(routes.SavePricingFirstGroupMailMappingHandler(pgDB)),
|
||||||
)
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/system/order-price-list-mail-mappings/lookups", "GET",
|
||||||
|
"system", "update",
|
||||||
|
wrapV3(routes.GetOrderPriceListFirstGroupMailMappingLookupsHandler(pgDB)),
|
||||||
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/system/order-price-list-mail-mappings", "GET",
|
||||||
|
"system", "update",
|
||||||
|
wrapV3(routes.GetOrderPriceListFirstGroupMailMappingsHandler(pgDB)),
|
||||||
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/system/order-price-list-mail-mappings/{group}", "PUT",
|
||||||
|
"system", "update",
|
||||||
|
wrapV3(routes.SaveOrderPriceListFirstGroupMailMappingHandler(pgDB)),
|
||||||
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/system/order-price-list-user-price-groups/lookups", "GET",
|
||||||
|
"system", "update",
|
||||||
|
wrapV3(routes.GetOrderPriceListPriceGroupLookupsHandler(pgDB)),
|
||||||
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/system/order-price-list-user-price-groups", "GET",
|
||||||
|
"system", "update",
|
||||||
|
wrapV3(routes.GetOrderPriceListUserPriceGroupRowsHandler(pgDB)),
|
||||||
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/system/order-price-list-user-price-groups/{id}", "PUT",
|
||||||
|
"system", "update",
|
||||||
|
wrapV3(routes.SaveUserOrderPriceListPriceGroupsHandler(pgDB)),
|
||||||
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/language/translations", "GET",
|
"/api/language/translations", "GET",
|
||||||
"language", "update",
|
"language", "update",
|
||||||
@@ -424,6 +454,21 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"system", "update",
|
"system", "update",
|
||||||
wrapV3(routes.SaveUserPermissionsHandler(pgDB)),
|
wrapV3(routes.SaveUserPermissionsHandler(pgDB)),
|
||||||
)
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/users/{id}/order-price-list-price-groups", "GET",
|
||||||
|
"user", "update",
|
||||||
|
wrapV3(routes.GetUserOrderPriceListPriceGroupsHandler(pgDB)),
|
||||||
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/users/order-price-list-price-groups/lookups", "GET",
|
||||||
|
"user", "update",
|
||||||
|
wrapV3(routes.GetOrderPriceListPriceGroupLookupsHandler(pgDB)),
|
||||||
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/users/{id}/order-price-list-price-groups", "PUT",
|
||||||
|
"user", "update",
|
||||||
|
wrapV3(routes.SaveUserOrderPriceListPriceGroupsHandler(pgDB)),
|
||||||
|
)
|
||||||
|
|
||||||
// ✅ permissions/routes (system:view)
|
// ✅ permissions/routes (system:view)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
@@ -795,6 +840,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"order", "view",
|
"order", "view",
|
||||||
wrapV3(http.HandlerFunc(routes.GetProductPricingFilterOptionsHandler)),
|
wrapV3(http.HandlerFunc(routes.GetProductPricingFilterOptionsHandler)),
|
||||||
)
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/order/price-list/my-price-groups", "GET",
|
||||||
|
"order", "view",
|
||||||
|
wrapV3(routes.GetMyOrderPriceListPriceGroupsHandler(pgDB)),
|
||||||
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/order/price-list/campaigns", "GET",
|
"/api/order/price-list/campaigns", "GET",
|
||||||
"order", "view",
|
"order", "view",
|
||||||
@@ -815,6 +865,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"order", "view",
|
"order", "view",
|
||||||
wrapV3(http.HandlerFunc(routes.ExportProductPriceListPDFHandler(pgDB))),
|
wrapV3(http.HandlerFunc(routes.ExportProductPriceListPDFHandler(pgDB))),
|
||||||
)
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/order/price-list/export-notify", "POST",
|
||||||
|
"order", "view",
|
||||||
|
wrapV3(http.HandlerFunc(routes.NotifyOrderPriceListExportHandler(pgDB, ml))),
|
||||||
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/product-size-match/rules", "GET",
|
"/api/product-size-match/rules", "GET",
|
||||||
"order", "view",
|
"order", "view",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type UserDetail struct {
|
|||||||
Departments []DeptOption `json:"departments"`
|
Departments []DeptOption `json:"departments"`
|
||||||
Piyasalar []DeptOption `json:"piyasalar"`
|
Piyasalar []DeptOption `json:"piyasalar"`
|
||||||
NebimUsers []NebimOption `json:"nebim_users"`
|
NebimUsers []NebimOption `json:"nebim_users"`
|
||||||
|
OrderPriceListPriceGroups []string `json:"order_price_list_price_groups"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================================================
|
// ======================================================
|
||||||
@@ -39,6 +40,7 @@ type UserWrite struct {
|
|||||||
Departments []DeptOption `json:"departments"`
|
Departments []DeptOption `json:"departments"`
|
||||||
Piyasalar []DeptOption `json:"piyasalar"`
|
Piyasalar []DeptOption `json:"piyasalar"`
|
||||||
NebimUsers []NebimOption `json:"nebim_users"`
|
NebimUsers []NebimOption `json:"nebim_users"`
|
||||||
|
OrderPriceListPriceGroups []string `json:"order_price_list_price_groups"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================================================
|
// ======================================================
|
||||||
|
|||||||
@@ -45,3 +45,26 @@ const InsertPricingFirstGroupMailMapping = `
|
|||||||
INSERT INTO mk_pricing_first_group_mail (urun_ilk_grubu, mail_id)
|
INSERT INTO mk_pricing_first_group_mail (urun_ilk_grubu, mail_id)
|
||||||
VALUES ($1, $2)
|
VALUES ($1, $2)
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const GetOrderPriceListFirstGroupMailMappingRows = `
|
||||||
|
SELECT
|
||||||
|
f.urun_ilk_grubu,
|
||||||
|
m.id::text,
|
||||||
|
m.email,
|
||||||
|
COALESCE(NULLIF(m.display_name, ''), m.email) AS display_name
|
||||||
|
FROM mk_order_price_list_first_group_mail f
|
||||||
|
JOIN mk_mail m
|
||||||
|
ON m.id = f.mail_id
|
||||||
|
AND m.is_active = true
|
||||||
|
ORDER BY f.urun_ilk_grubu, m.email
|
||||||
|
`
|
||||||
|
|
||||||
|
const DeleteOrderPriceListFirstGroupMailsByGroup = `
|
||||||
|
DELETE FROM mk_order_price_list_first_group_mail
|
||||||
|
WHERE urun_ilk_grubu = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
const InsertOrderPriceListFirstGroupMailMapping = `
|
||||||
|
INSERT INTO mk_order_price_list_first_group_mail (urun_ilk_grubu, mail_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
`
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
package queries
|
package queries
|
||||||
|
|
||||||
// GetProductVariantDimsForPricing:
|
// GetProductVariantDimsForPricing:
|
||||||
// Pull variant dimension combos from Nebim stock tables (same source as product-stock-query UI).
|
// Pull variant dimension combos from Nebim's variant master table.
|
||||||
// We intentionally keep it small: only the keys we need to write dim-aware prices into PG sdprc.
|
// We intentionally keep it small: only the keys we need to write dim-aware prices into PG sdprc.
|
||||||
//
|
//
|
||||||
// Note: Column semantics depend on your Nebim setup. We treat ItemDim1Code/ItemDim3Code as the
|
// Note: Column semantics depend on your Nebim setup. Here ColorCode and ItemDim2Code are the
|
||||||
// primary variant dimensions used by the e-commerce sdprc dim filters.
|
// user-visible/key dimensions; PG stores them as mmitem_dim.val1 and mmitem_dim.val3.
|
||||||
|
// ItemDim1Code is kept only as size context for mmitem_dim seeding.
|
||||||
const GetProductVariantDimsForPricing = `
|
const GetProductVariantDimsForPricing = `
|
||||||
DECLARE @ProductCode NVARCHAR(50) = @p1;
|
DECLARE @ProductCode NVARCHAR(50) = @p1;
|
||||||
|
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
LTRIM(RTRIM(ISNULL(S.ColorCode,''))) AS ColorCode,
|
LTRIM(RTRIM(ISNULL(V.ColorCode,''))) AS ColorCode,
|
||||||
LTRIM(RTRIM(ISNULL(S.ItemDim1Code,''))) AS ItemDim1Code,
|
LTRIM(RTRIM(ISNULL(V.ItemDim1Code,''))) AS ItemDim1Code,
|
||||||
LTRIM(RTRIM(ISNULL(S.ItemDim3Code,''))) AS ItemDim3Code
|
LTRIM(RTRIM(ISNULL(V.ItemDim2Code,''))) AS ItemDim3Code
|
||||||
FROM trStock S WITH(NOLOCK)
|
FROM prItemVariant V WITH(NOLOCK)
|
||||||
WHERE S.ItemTypeCode = 1
|
WHERE V.ItemTypeCode = 1
|
||||||
AND S.ItemCode = @ProductCode
|
AND V.ItemCode = @ProductCode
|
||||||
AND LEN(S.ItemCode) = 13
|
AND LEN(V.ItemCode) = 13
|
||||||
AND LEN(@ProductCode) = 13
|
AND LEN(@ProductCode) = 13
|
||||||
ORDER BY
|
ORDER BY
|
||||||
LTRIM(RTRIM(ISNULL(S.ColorCode,''))),
|
LTRIM(RTRIM(ISNULL(V.ColorCode,''))),
|
||||||
LTRIM(RTRIM(ISNULL(S.ItemDim1Code,''))),
|
LTRIM(RTRIM(ISNULL(V.ItemDim1Code,''))),
|
||||||
LTRIM(RTRIM(ISNULL(S.ItemDim3Code,'')));
|
LTRIM(RTRIM(ISNULL(V.ItemDim2Code,'')));
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package queries
|
package queries
|
||||||
|
|
||||||
// GetWholesaleCampaignVariantStockByProducts:
|
// GetWholesaleCampaignVariantStockByProducts:
|
||||||
// Returns per-product variant keys (ColorCode/ItemDim1Code/ItemDim3Code) and available stock qty.
|
// Returns per-product variant keys (ColorCode/ItemDim1Code/ItemDim2Code-as-ItemDim3Code) and available stock qty.
|
||||||
// We aggregate across warehouses/stores; semantics align with product-stock-query's "Kullanilabilir_Envanter".
|
// We aggregate across warehouses/stores; semantics align with product-stock-query's "Kullanilabilir_Envanter".
|
||||||
|
// In this Nebim setup, the second user-visible/key variant maps to ItemDim2Code, while PG stores it in mmitem_dim.val3.
|
||||||
const GetWholesaleCampaignVariantStockByProducts = `
|
const GetWholesaleCampaignVariantStockByProducts = `
|
||||||
DECLARE @Codes NVARCHAR(MAX) = @p1;
|
DECLARE @Codes NVARCHAR(MAX) = @p1;
|
||||||
|
|
||||||
@@ -16,11 +17,50 @@ DECLARE @Codes NVARCHAR(MAX) = @p1;
|
|||||||
CROSS APPLY D.XmlData.nodes('/i') AS X(C)
|
CROSS APPLY D.XmlData.nodes('/i') AS X(C)
|
||||||
WHERE LTRIM(RTRIM(X.C.value('.', 'NVARCHAR(50)'))) <> ''
|
WHERE LTRIM(RTRIM(X.C.value('.', 'NVARCHAR(50)'))) <> ''
|
||||||
),
|
),
|
||||||
|
VARIANT_MASTER AS (
|
||||||
|
SELECT
|
||||||
|
V.ItemCode,
|
||||||
|
LTRIM(RTRIM(ISNULL(V.ColorCode,''))) AS ColorCode,
|
||||||
|
LTRIM(RTRIM(ISNULL(V.ItemDim2Code,''))) AS ItemDim3Code,
|
||||||
|
MAX(LTRIM(RTRIM(ISNULL(V.ItemDim1Code,'')))) AS ItemDim1Code
|
||||||
|
FROM prItemVariant V WITH(NOLOCK)
|
||||||
|
JOIN INP ON INP.ItemCode = V.ItemCode
|
||||||
|
WHERE V.ItemTypeCode = 1
|
||||||
|
AND LEN(V.ItemCode) = 13
|
||||||
|
GROUP BY
|
||||||
|
V.ItemCode, V.ColorCode, V.ItemDim2Code
|
||||||
|
),
|
||||||
|
VARIANT_STOCK AS (
|
||||||
|
SELECT
|
||||||
|
S.ItemCode,
|
||||||
|
LTRIM(RTRIM(ISNULL(S.ColorCode,''))) AS ColorCode,
|
||||||
|
LTRIM(RTRIM(ISNULL(S.ItemDim2Code,''))) AS ItemDim3Code,
|
||||||
|
MAX(LTRIM(RTRIM(ISNULL(S.ItemDim1Code,'')))) AS ItemDim1Code
|
||||||
|
FROM trStock S WITH(NOLOCK)
|
||||||
|
JOIN INP ON INP.ItemCode = S.ItemCode
|
||||||
|
WHERE S.ItemTypeCode = 1
|
||||||
|
AND LEN(S.ItemCode) = 13
|
||||||
|
GROUP BY
|
||||||
|
S.ItemCode, S.ColorCode, S.ItemDim2Code
|
||||||
|
),
|
||||||
|
VARIANT AS (
|
||||||
|
SELECT
|
||||||
|
X.ItemCode,
|
||||||
|
X.ColorCode,
|
||||||
|
X.ItemDim3Code,
|
||||||
|
MAX(X.ItemDim1Code) AS ItemDim1Code
|
||||||
|
FROM (
|
||||||
|
SELECT ItemCode, ColorCode, ItemDim3Code, ItemDim1Code FROM VARIANT_MASTER
|
||||||
|
UNION ALL
|
||||||
|
SELECT ItemCode, ColorCode, ItemDim3Code, ItemDim1Code FROM VARIANT_STOCK
|
||||||
|
) X
|
||||||
|
GROUP BY X.ItemCode, X.ColorCode, X.ItemDim3Code
|
||||||
|
),
|
||||||
STOCK AS (
|
STOCK AS (
|
||||||
SELECT
|
SELECT
|
||||||
S.ItemCode,
|
S.ItemCode,
|
||||||
LTRIM(RTRIM(ISNULL(S.ColorCode,''))) AS ColorCode,
|
LTRIM(RTRIM(ISNULL(S.ColorCode,''))) AS ColorCode,
|
||||||
LTRIM(RTRIM(ISNULL(S.ItemDim3Code,''))) AS ItemDim3Code,
|
LTRIM(RTRIM(ISNULL(S.ItemDim2Code,''))) AS ItemDim3Code,
|
||||||
MAX(LTRIM(RTRIM(ISNULL(S.ItemDim1Code,'')))) AS ItemDim1Code,
|
MAX(LTRIM(RTRIM(ISNULL(S.ItemDim1Code,'')))) AS ItemDim1Code,
|
||||||
SUM(S.In_Qty1 - S.Out_Qty1) AS InventoryQty1
|
SUM(S.In_Qty1 - S.Out_Qty1) AS InventoryQty1
|
||||||
FROM trStock S WITH(NOLOCK)
|
FROM trStock S WITH(NOLOCK)
|
||||||
@@ -28,13 +68,13 @@ STOCK AS (
|
|||||||
WHERE S.ItemTypeCode = 1
|
WHERE S.ItemTypeCode = 1
|
||||||
AND LEN(S.ItemCode) = 13
|
AND LEN(S.ItemCode) = 13
|
||||||
GROUP BY
|
GROUP BY
|
||||||
S.ItemCode, S.ColorCode, S.ItemDim3Code
|
S.ItemCode, S.ColorCode, S.ItemDim2Code
|
||||||
),
|
),
|
||||||
PICK AS (
|
PICK AS (
|
||||||
SELECT
|
SELECT
|
||||||
P.ItemCode,
|
P.ItemCode,
|
||||||
LTRIM(RTRIM(ISNULL(P.ColorCode,''))) AS ColorCode,
|
LTRIM(RTRIM(ISNULL(P.ColorCode,''))) AS ColorCode,
|
||||||
LTRIM(RTRIM(ISNULL(P.ItemDim3Code,''))) AS ItemDim3Code,
|
LTRIM(RTRIM(ISNULL(P.ItemDim2Code,''))) AS ItemDim3Code,
|
||||||
MAX(LTRIM(RTRIM(ISNULL(P.ItemDim1Code,'')))) AS ItemDim1Code,
|
MAX(LTRIM(RTRIM(ISNULL(P.ItemDim1Code,'')))) AS ItemDim1Code,
|
||||||
SUM(P.Qty1) AS PickingQty1
|
SUM(P.Qty1) AS PickingQty1
|
||||||
FROM PickingStates P
|
FROM PickingStates P
|
||||||
@@ -42,13 +82,13 @@ PICK AS (
|
|||||||
WHERE P.ItemTypeCode = 1
|
WHERE P.ItemTypeCode = 1
|
||||||
AND LEN(P.ItemCode) = 13
|
AND LEN(P.ItemCode) = 13
|
||||||
GROUP BY
|
GROUP BY
|
||||||
P.ItemCode, P.ColorCode, P.ItemDim3Code
|
P.ItemCode, P.ColorCode, P.ItemDim2Code
|
||||||
),
|
),
|
||||||
RESERVE AS (
|
RESERVE AS (
|
||||||
SELECT
|
SELECT
|
||||||
R.ItemCode,
|
R.ItemCode,
|
||||||
LTRIM(RTRIM(ISNULL(R.ColorCode,''))) AS ColorCode,
|
LTRIM(RTRIM(ISNULL(R.ColorCode,''))) AS ColorCode,
|
||||||
LTRIM(RTRIM(ISNULL(R.ItemDim3Code,''))) AS ItemDim3Code,
|
LTRIM(RTRIM(ISNULL(R.ItemDim2Code,''))) AS ItemDim3Code,
|
||||||
MAX(LTRIM(RTRIM(ISNULL(R.ItemDim1Code,'')))) AS ItemDim1Code,
|
MAX(LTRIM(RTRIM(ISNULL(R.ItemDim1Code,'')))) AS ItemDim1Code,
|
||||||
SUM(R.Qty1) AS ReserveQty1
|
SUM(R.Qty1) AS ReserveQty1
|
||||||
FROM ReserveStates R
|
FROM ReserveStates R
|
||||||
@@ -56,13 +96,13 @@ RESERVE AS (
|
|||||||
WHERE R.ItemTypeCode = 1
|
WHERE R.ItemTypeCode = 1
|
||||||
AND LEN(R.ItemCode) = 13
|
AND LEN(R.ItemCode) = 13
|
||||||
GROUP BY
|
GROUP BY
|
||||||
R.ItemCode, R.ColorCode, R.ItemDim3Code
|
R.ItemCode, R.ColorCode, R.ItemDim2Code
|
||||||
),
|
),
|
||||||
DISP AS (
|
DISP AS (
|
||||||
SELECT
|
SELECT
|
||||||
D.ItemCode,
|
D.ItemCode,
|
||||||
LTRIM(RTRIM(ISNULL(D.ColorCode,''))) AS ColorCode,
|
LTRIM(RTRIM(ISNULL(D.ColorCode,''))) AS ColorCode,
|
||||||
LTRIM(RTRIM(ISNULL(D.ItemDim3Code,''))) AS ItemDim3Code,
|
LTRIM(RTRIM(ISNULL(D.ItemDim2Code,''))) AS ItemDim3Code,
|
||||||
MAX(LTRIM(RTRIM(ISNULL(D.ItemDim1Code,'')))) AS ItemDim1Code,
|
MAX(LTRIM(RTRIM(ISNULL(D.ItemDim1Code,'')))) AS ItemDim1Code,
|
||||||
SUM(D.Qty1) AS DispOrderQty1
|
SUM(D.Qty1) AS DispOrderQty1
|
||||||
FROM DispOrderStates D
|
FROM DispOrderStates D
|
||||||
@@ -70,30 +110,28 @@ DISP AS (
|
|||||||
WHERE D.ItemTypeCode = 1
|
WHERE D.ItemTypeCode = 1
|
||||||
AND LEN(D.ItemCode) = 13
|
AND LEN(D.ItemCode) = 13
|
||||||
GROUP BY
|
GROUP BY
|
||||||
D.ItemCode, D.ColorCode, D.ItemDim3Code
|
D.ItemCode, D.ColorCode, D.ItemDim2Code
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
S.ItemCode AS ItemCode,
|
V.ItemCode AS ItemCode,
|
||||||
S.ColorCode AS ColorCode,
|
V.ColorCode AS ColorCode,
|
||||||
S.ItemDim1Code AS ItemDim1Code,
|
V.ItemDim1Code AS ItemDim1Code,
|
||||||
S.ItemDim3Code AS ItemDim3Code,
|
V.ItemDim3Code AS ItemDim3Code,
|
||||||
CAST(ROUND(
|
CAST(ROUND(
|
||||||
S.InventoryQty1
|
ISNULL(S.InventoryQty1,0)
|
||||||
- ISNULL(PK.PickingQty1,0)
|
- ISNULL(PK.PickingQty1,0)
|
||||||
- ISNULL(RS.ReserveQty1,0)
|
- ISNULL(RS.ReserveQty1,0)
|
||||||
- ISNULL(DP.DispOrderQty1,0),
|
- ISNULL(DP.DispOrderQty1,0),
|
||||||
2
|
2
|
||||||
) AS FLOAT) AS StockQty
|
) AS FLOAT) AS StockQty
|
||||||
FROM STOCK S
|
FROM VARIANT V
|
||||||
|
LEFT JOIN STOCK S
|
||||||
|
ON S.ItemCode=V.ItemCode AND S.ColorCode=V.ColorCode AND S.ItemDim3Code=V.ItemDim3Code
|
||||||
LEFT JOIN PICK PK
|
LEFT JOIN PICK PK
|
||||||
ON PK.ItemCode=S.ItemCode AND PK.ColorCode=S.ColorCode AND PK.ItemDim3Code=S.ItemDim3Code
|
ON PK.ItemCode=V.ItemCode AND PK.ColorCode=V.ColorCode AND PK.ItemDim3Code=V.ItemDim3Code
|
||||||
LEFT JOIN RESERVE RS
|
LEFT JOIN RESERVE RS
|
||||||
ON RS.ItemCode=S.ItemCode AND RS.ColorCode=S.ColorCode AND RS.ItemDim3Code=S.ItemDim3Code
|
ON RS.ItemCode=V.ItemCode AND RS.ColorCode=V.ColorCode AND RS.ItemDim3Code=V.ItemDim3Code
|
||||||
LEFT JOIN DISP DP
|
LEFT JOIN DISP DP
|
||||||
ON DP.ItemCode=S.ItemCode AND DP.ColorCode=S.ColorCode AND DP.ItemDim3Code=S.ItemDim3Code
|
ON DP.ItemCode=V.ItemCode AND DP.ColorCode=V.ColorCode AND DP.ItemDim3Code=V.ItemDim3Code
|
||||||
WHERE (S.InventoryQty1
|
ORDER BY V.ItemCode, V.ColorCode, V.ItemDim3Code;
|
||||||
- ISNULL(PK.PickingQty1,0)
|
|
||||||
- ISNULL(RS.ReserveQty1,0)
|
|
||||||
- ISNULL(DP.DispOrderQty1,0)) <> 0
|
|
||||||
ORDER BY S.ItemCode, S.ColorCode, S.ItemDim3Code;
|
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ CREATE TABLE IF NOT EXISTS mk_pricing_first_group_mail (
|
|||||||
)
|
)
|
||||||
`,
|
`,
|
||||||
`CREATE INDEX IF NOT EXISTS ix_pricing_first_group_mail_group ON mk_pricing_first_group_mail (urun_ilk_grubu)`,
|
`CREATE INDEX IF NOT EXISTS ix_pricing_first_group_mail_group ON mk_pricing_first_group_mail (urun_ilk_grubu)`,
|
||||||
|
`
|
||||||
|
CREATE TABLE IF NOT EXISTS mk_order_price_list_first_group_mail (
|
||||||
|
urun_ilk_grubu TEXT NOT NULL,
|
||||||
|
mail_id UUID NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (urun_ilk_grubu, mail_id)
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS ix_order_price_list_first_group_mail_group ON mk_order_price_list_first_group_mail (urun_ilk_grubu)`,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range stmts {
|
for _, s := range stmts {
|
||||||
@@ -68,7 +77,7 @@ func GetCostingFirstGroupMailMappingLookupsHandler(pg *sql.DB) http.HandlerFunc
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := ensureFirstGroupMailMappingTables(pg); err != nil {
|
if err := ensureFirstGroupMailMappingTables(pg); err != nil {
|
||||||
http.Error(w, "mapping table bootstrap error", http.StatusInternalServerError)
|
http.Error(w, "mapping table bootstrap error: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +139,11 @@ func GetPricingFirstGroupMailMappingLookupsHandler(pg *sql.DB) http.HandlerFunc
|
|||||||
return GetCostingFirstGroupMailMappingLookupsHandler(pg)
|
return GetCostingFirstGroupMailMappingLookupsHandler(pg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetOrderPriceListFirstGroupMailMappingLookupsHandler(pg *sql.DB) http.HandlerFunc {
|
||||||
|
// same lookups as costing/pricing
|
||||||
|
return GetCostingFirstGroupMailMappingLookupsHandler(pg)
|
||||||
|
}
|
||||||
|
|
||||||
func GetCostingFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
func GetCostingFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
@@ -140,7 +154,7 @@ func GetCostingFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := ensureFirstGroupMailMappingTables(pg); err != nil {
|
if err := ensureFirstGroupMailMappingTables(pg); err != nil {
|
||||||
http.Error(w, "mapping table bootstrap error", http.StatusInternalServerError)
|
http.Error(w, "mapping table bootstrap error: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +277,7 @@ func GetPricingFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := ensureFirstGroupMailMappingTables(pg); err != nil {
|
if err := ensureFirstGroupMailMappingTables(pg); err != nil {
|
||||||
http.Error(w, "mapping table bootstrap error", http.StatusInternalServerError)
|
http.Error(w, "mapping table bootstrap error: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,3 +506,195 @@ func SavePricingFirstGroupMailMappingHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetOrderPriceListFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
||||||
|
return getFirstGroupMailMappingsByQuery(pg, queries.GetOrderPriceListFirstGroupMailMappingRows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveOrderPriceListFirstGroupMailMappingHandler(pg *sql.DB) http.HandlerFunc {
|
||||||
|
return saveFirstGroupMailMappingByQueries(
|
||||||
|
pg,
|
||||||
|
queries.DeleteOrderPriceListFirstGroupMailsByGroup,
|
||||||
|
queries.InsertOrderPriceListFirstGroupMailMapping,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFirstGroupMailMappingsByQuery(pg *sql.DB, mappingQuery string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
mssql := db.GetDB()
|
||||||
|
if mssql == nil {
|
||||||
|
http.Error(w, "mssql connection not available", http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ensureFirstGroupMailMappingTables(pg); err != nil {
|
||||||
|
http.Error(w, "mapping table bootstrap error: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
traceID := utils.TraceIDFromRequest(r)
|
||||||
|
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
||||||
|
|
||||||
|
allCodes := make([]string, 0, 512)
|
||||||
|
titleByCode := make(map[string]string, 512)
|
||||||
|
fgRows, err := queries.ListProductFirstGroupCodeDescOptions(ctx, mssql, "", 5000)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "first group lookup error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer fgRows.Close()
|
||||||
|
for fgRows.Next() {
|
||||||
|
var code string
|
||||||
|
var title string
|
||||||
|
if err := fgRows.Scan(&code, &title); err != nil {
|
||||||
|
http.Error(w, "first group scan error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code = strings.TrimSpace(code)
|
||||||
|
title = strings.TrimSpace(title)
|
||||||
|
if code != "" {
|
||||||
|
allCodes = append(allCodes, code)
|
||||||
|
if _, ok := titleByCode[code]; !ok {
|
||||||
|
titleByCode[code] = title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := fgRows.Err(); err != nil {
|
||||||
|
http.Error(w, "first group rows error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
allCodes = normalizeIDList(allCodes)
|
||||||
|
|
||||||
|
rows, err := pg.Query(mappingQuery)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "mapping query error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
byGroup := map[string]*models.FirstGroupMailMappingRow{}
|
||||||
|
for _, code := range allCodes {
|
||||||
|
byGroup[code] = &models.FirstGroupMailMappingRow{
|
||||||
|
UrunIlkGrubu: code,
|
||||||
|
GroupCode: code,
|
||||||
|
GroupTitle: titleByCode[code],
|
||||||
|
MailIDs: make([]string, 0, 8),
|
||||||
|
Mails: make([]models.FirstGroupMailOption, 0, 8),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var group sql.NullString
|
||||||
|
var mailID sql.NullString
|
||||||
|
var email sql.NullString
|
||||||
|
var displayName sql.NullString
|
||||||
|
if err := rows.Scan(&group, &mailID, &email, &displayName); err != nil {
|
||||||
|
http.Error(w, "mapping scan error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := strings.TrimSpace(group.String)
|
||||||
|
if code == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
row, ok := byGroup[code]
|
||||||
|
if !ok {
|
||||||
|
row = &models.FirstGroupMailMappingRow{
|
||||||
|
UrunIlkGrubu: code,
|
||||||
|
GroupCode: code,
|
||||||
|
GroupTitle: titleByCode[code],
|
||||||
|
MailIDs: make([]string, 0, 8),
|
||||||
|
Mails: make([]models.FirstGroupMailOption, 0, 8),
|
||||||
|
}
|
||||||
|
byGroup[code] = row
|
||||||
|
allCodes = append(allCodes, code)
|
||||||
|
}
|
||||||
|
if mailID.Valid && strings.TrimSpace(mailID.String) != "" {
|
||||||
|
id := strings.TrimSpace(mailID.String)
|
||||||
|
row.MailIDs = append(row.MailIDs, id)
|
||||||
|
label := strings.TrimSpace(displayName.String)
|
||||||
|
if label == "" {
|
||||||
|
label = strings.TrimSpace(email.String)
|
||||||
|
}
|
||||||
|
row.Mails = append(row.Mails, models.FirstGroupMailOption{ID: id, Label: label})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
http.Error(w, "mapping rows error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allCodes = normalizeIDList(allCodes)
|
||||||
|
out := make([]models.FirstGroupMailMappingRow, 0, len(allCodes))
|
||||||
|
for _, code := range allCodes {
|
||||||
|
if r := byGroup[code]; r != nil {
|
||||||
|
r.MailIDs = normalizeIDList(r.MailIDs)
|
||||||
|
if strings.TrimSpace(r.GroupTitle) == "" {
|
||||||
|
r.GroupTitle = titleByCode[code]
|
||||||
|
}
|
||||||
|
out = append(out, *r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveFirstGroupMailMappingByQueries(pg *sql.DB, deleteQuery string, insertQuery string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
group := strings.TrimSpace(mux.Vars(r)["group"])
|
||||||
|
if group == "" {
|
||||||
|
http.Error(w, "invalid urun_ilk_grubu", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload FirstGroupMailSavePayload
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
|
http.Error(w, "invalid payload", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mailIDs := normalizeIDList(payload.MailIDs)
|
||||||
|
for _, mailID := range mailIDs {
|
||||||
|
var mailExists bool
|
||||||
|
if err := pg.QueryRow(queries.ExistsActiveMailByID, mailID).Scan(&mailExists); err != nil {
|
||||||
|
http.Error(w, "mail validate error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !mailExists {
|
||||||
|
http.Error(w, "mail not found: "+mailID, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := pg.Begin()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "transaction start error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
if _, err := tx.Exec(deleteQuery, group); err != nil {
|
||||||
|
http.Error(w, "mapping delete error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, mailID := range mailIDs {
|
||||||
|
if _, err := tx.Exec(insertQuery, group, mailID); err != nil {
|
||||||
|
http.Error(w, "mapping insert error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
http.Error(w, "transaction commit error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"urun_ilk_grubu": group,
|
||||||
|
"mail_ids": mailIDs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -555,6 +555,17 @@ func UserCreateRoute(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ensureOrderPriceListUserPriceGroupSchema(db); err != nil {
|
||||||
|
log.Printf("USER CREATE PRICE GROUP SCHEMA ERROR user_id=%d err=%v", newID, err)
|
||||||
|
http.Error(w, "Fiyat grubu tablosu hazirlanamadi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := saveOrderPriceListUserPriceGroupsTx(tx, newID, payload.OrderPriceListPriceGroups); err != nil {
|
||||||
|
log.Printf("USER CREATE PRICE GROUP SAVE ERROR user_id=%d err=%v", newID, err)
|
||||||
|
http.Error(w, "Fiyat gruplari eklenemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
if pe, ok := err.(*pq.Error); ok {
|
if pe, ok := err.(*pq.Error); ok {
|
||||||
log.Printf(
|
log.Printf(
|
||||||
|
|||||||
192
svc/routes/order_price_list_export_notify.go
Normal file
192
svc/routes/order_price_list_export_notify.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bssapp-backend/auth"
|
||||||
|
"bssapp-backend/internal/mailer"
|
||||||
|
|
||||||
|
"github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
type orderPriceListExportNotifyRequest struct {
|
||||||
|
Format string `json:"format"`
|
||||||
|
RowCount int `json:"row_count"`
|
||||||
|
PriceFields []string `json:"price_fields"`
|
||||||
|
ProductCodes []string `json:"product_codes"`
|
||||||
|
CampaignLabels []string `json:"campaign_labels"`
|
||||||
|
FirstGroups []string `json:"first_groups"`
|
||||||
|
UrunIlkGrubu string `json:"urun_ilk_grubu"`
|
||||||
|
UrunAnaGrubu string `json:"urun_ana_grubu"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotifyOrderPriceListExportHandler(pg *sql.DB, ml *mailer.GraphMailer) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
claims, ok := auth.GetClaimsFromContext(r.Context())
|
||||||
|
if !ok || claims == nil {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ml == nil {
|
||||||
|
http.Error(w, "mailer unavailable", http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ensureFirstGroupMailMappingTables(pg); err != nil {
|
||||||
|
http.Error(w, "mapping table bootstrap error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req orderPriceListExportNotifyRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid payload", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := normalizeExportNotifyList(req.FirstGroups, 100)
|
||||||
|
if len(groups) == 0 {
|
||||||
|
groups = normalizeExportNotifyList([]string{req.UrunIlkGrubu}, 100)
|
||||||
|
}
|
||||||
|
recipients, err := loadOrderPriceListRecipients(pg, groups)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "recipient lookup error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(recipients) == 0 {
|
||||||
|
log.Printf("[order-price-list-export-notify] no recipients groups=%v", groups)
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "sent": false, "reason": "no_recipients"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actor := strings.TrimSpace(claims.Username)
|
||||||
|
if actor == "" {
|
||||||
|
actor = fmt.Sprintf("user-%d", claims.ID)
|
||||||
|
}
|
||||||
|
format := strings.ToUpper(strings.TrimSpace(req.Format))
|
||||||
|
if format == "" {
|
||||||
|
format = "CIKTI"
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
htmlBody := buildOrderPriceListExportNotifyHTML(req, actor, format, groups, now)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := ml.Send(ctx, mailer.Message{
|
||||||
|
To: recipients,
|
||||||
|
Subject: fmt.Sprintf("Fiyat Listesi Ciktisi Alindi | %s | %s", actor, now.Format("02.01.2006 15:04")),
|
||||||
|
BodyHTML: htmlBody,
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("[order-price-list-export-notify] send failed user=%s format=%s err=%v", actor, format, err)
|
||||||
|
http.Error(w, "mail send error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "sent": true, "recipient_count": len(recipients)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadOrderPriceListRecipients(pg *sql.DB, groups []string) ([]string, error) {
|
||||||
|
groups = normalizeExportNotifyList(groups, 100)
|
||||||
|
if len(groups) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
rows, err := pg.Query(`
|
||||||
|
SELECT DISTINCT TRIM(m.email) AS email
|
||||||
|
FROM mk_order_price_list_first_group_mail f
|
||||||
|
JOIN mk_mail m
|
||||||
|
ON m.id = f.mail_id
|
||||||
|
WHERE m.is_active = true
|
||||||
|
AND COALESCE(TRIM(m.email), '') <> ''
|
||||||
|
AND UPPER(TRIM(f.urun_ilk_grubu)) = ANY($1)
|
||||||
|
ORDER BY email
|
||||||
|
`, pq.Array(upperExportNotifyList(groups)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
out := make([]string, 0, 16)
|
||||||
|
for rows.Next() {
|
||||||
|
var email string
|
||||||
|
if err := rows.Scan(&email); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
email = strings.TrimSpace(email)
|
||||||
|
if email != "" {
|
||||||
|
out = append(out, email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildOrderPriceListExportNotifyHTML(req orderPriceListExportNotifyRequest, actor string, format string, groups []string, now time.Time) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
<h3>Fiyat Listesi Ciktisi Alindi</h3>
|
||||||
|
<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;border:1px solid #ddd">
|
||||||
|
<tr><td><b>Islem Yapan</b></td><td>%s</td></tr>
|
||||||
|
<tr><td><b>Cikti Tipi</b></td><td>%s</td></tr>
|
||||||
|
<tr><td><b>Tarih</b></td><td>%s</td></tr>
|
||||||
|
<tr><td><b>Satir Sayisi</b></td><td>%d</td></tr>
|
||||||
|
<tr><td><b>Secili Fiyat Gruplari</b></td><td>%s</td></tr>
|
||||||
|
<tr><td><b>Urun Ilk Gruplari</b></td><td>%s</td></tr>
|
||||||
|
<tr><td><b>Urun Ana Grubu</b></td><td>%s</td></tr>
|
||||||
|
<tr><td><b>Urun Kodlari</b></td><td>%s</td></tr>
|
||||||
|
<tr><td><b>Kampanya Filtreleri</b></td><td>%s</td></tr>
|
||||||
|
</table>`,
|
||||||
|
html.EscapeString(actor),
|
||||||
|
html.EscapeString(format),
|
||||||
|
html.EscapeString(now.Format("02.01.2006 15:04")),
|
||||||
|
req.RowCount,
|
||||||
|
html.EscapeString(strings.Join(normalizeExportNotifyList(req.PriceFields, 24), ", ")),
|
||||||
|
html.EscapeString(strings.Join(groups, ", ")),
|
||||||
|
html.EscapeString(orderNotifyDash(req.UrunAnaGrubu)),
|
||||||
|
html.EscapeString(strings.Join(normalizeExportNotifyList(req.ProductCodes, 50), ", ")),
|
||||||
|
html.EscapeString(strings.Join(normalizeExportNotifyList(req.CampaignLabels, 20), ", ")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeExportNotifyList(in []string, limit int) []string {
|
||||||
|
out := make([]string, 0, len(in))
|
||||||
|
seen := make(map[string]struct{}, len(in))
|
||||||
|
for _, raw := range in {
|
||||||
|
v := strings.TrimSpace(raw)
|
||||||
|
if v == "" || v == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.ToUpper(v)
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
out = append(out, v)
|
||||||
|
if limit > 0 && len(out) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func upperExportNotifyList(in []string) []string {
|
||||||
|
out := make([]string, 0, len(in))
|
||||||
|
for _, v := range normalizeExportNotifyList(in, 0) {
|
||||||
|
out = append(out, strings.ToUpper(strings.TrimSpace(v)))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func orderNotifyDash(v string) string {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if v == "" {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
278
svc/routes/order_price_list_user_price_groups.go
Normal file
278
svc/routes/order_price_list_user_price_groups.go
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bssapp-backend/auth"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
type orderPriceListPriceGroupOption struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type orderPriceListUserPriceGroupRow struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
PriceGroups []string `json:"price_groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type orderPriceListUserPriceGroupPayload struct {
|
||||||
|
PriceGroups []string `json:"price_groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderPriceListAllPriceGroups = []orderPriceListPriceGroupOption{
|
||||||
|
{Value: "usd1", Label: "USD 1"},
|
||||||
|
{Value: "usd2", Label: "USD 2"},
|
||||||
|
{Value: "usd3", Label: "USD 3"},
|
||||||
|
{Value: "usd4", Label: "USD 4"},
|
||||||
|
{Value: "usd5", Label: "USD 5"},
|
||||||
|
{Value: "usd6", Label: "USD 6"},
|
||||||
|
{Value: "eur1", Label: "EUR 1"},
|
||||||
|
{Value: "eur2", Label: "EUR 2"},
|
||||||
|
{Value: "eur3", Label: "EUR 3"},
|
||||||
|
{Value: "eur4", Label: "EUR 4"},
|
||||||
|
{Value: "eur5", Label: "EUR 5"},
|
||||||
|
{Value: "eur6", Label: "EUR 6"},
|
||||||
|
{Value: "try1", Label: "TRY 1"},
|
||||||
|
{Value: "try2", Label: "TRY 2"},
|
||||||
|
{Value: "try3", Label: "TRY 3"},
|
||||||
|
{Value: "try4", Label: "TRY 4"},
|
||||||
|
{Value: "try5", Label: "TRY 5"},
|
||||||
|
{Value: "try6", Label: "TRY 6"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureOrderPriceListUserPriceGroupSchema(db *sql.DB) error {
|
||||||
|
stmts := []string{
|
||||||
|
`CREATE TABLE IF NOT EXISTS mk_order_price_list_user_price_group (
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
price_group TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (user_id, price_group)
|
||||||
|
)`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS ix_order_price_list_user_price_group_user ON mk_order_price_list_user_price_group (user_id)`,
|
||||||
|
`ALTER TABLE mk_order_price_list_user_price_group DROP CONSTRAINT IF EXISTS ck_order_price_list_user_price_group`,
|
||||||
|
`ALTER TABLE mk_order_price_list_user_price_group ADD CONSTRAINT ck_order_price_list_user_price_group CHECK (price_group IN ('usd1','usd2','usd3','usd4','usd5','usd6','eur1','eur2','eur3','eur4','eur5','eur6','try1','try2','try3','try4','try5','try6'))`,
|
||||||
|
}
|
||||||
|
for _, stmt := range stmts {
|
||||||
|
if _, err := db.Exec(stmt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeOrderPriceListPriceGroups(groups []string) []string {
|
||||||
|
allowed := map[string]bool{}
|
||||||
|
order := []string{}
|
||||||
|
for _, opt := range orderPriceListAllPriceGroups {
|
||||||
|
allowed[opt.Value] = true
|
||||||
|
order = append(order, opt.Value)
|
||||||
|
}
|
||||||
|
set := map[string]bool{}
|
||||||
|
for _, item := range groups {
|
||||||
|
v := strings.ToLower(strings.TrimSpace(item))
|
||||||
|
if allowed[v] {
|
||||||
|
set[v] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(set))
|
||||||
|
for _, v := range order {
|
||||||
|
if set[v] {
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadOrderPriceListUserPriceGroups(db *sql.DB, userID int64) ([]string, error) {
|
||||||
|
if err := ensureOrderPriceListUserPriceGroupSchema(db); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rows, err := db.Query(`
|
||||||
|
SELECT price_group
|
||||||
|
FROM mk_order_price_list_user_price_group
|
||||||
|
WHERE user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
CASE SUBSTRING(price_group, 1, 3)
|
||||||
|
WHEN 'usd' THEN 1
|
||||||
|
WHEN 'eur' THEN 2
|
||||||
|
WHEN 'try' THEN 3
|
||||||
|
ELSE 9
|
||||||
|
END,
|
||||||
|
CAST(SUBSTRING(price_group, 4) AS INT)
|
||||||
|
`, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []string
|
||||||
|
for rows.Next() {
|
||||||
|
var group string
|
||||||
|
if err := rows.Scan(&group); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, strings.ToLower(strings.TrimSpace(group)))
|
||||||
|
}
|
||||||
|
return normalizeOrderPriceListPriceGroups(out), rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveOrderPriceListUserPriceGroupsTx(tx *sql.Tx, userID int64, groups []string) error {
|
||||||
|
if _, err := tx.Exec(`DELETE FROM mk_order_price_list_user_price_group WHERE user_id = $1`, userID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, group := range normalizeOrderPriceListPriceGroups(groups) {
|
||||||
|
if _, err := tx.Exec(`
|
||||||
|
INSERT INTO mk_order_price_list_user_price_group (user_id, price_group, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $3)
|
||||||
|
ON CONFLICT (user_id, price_group)
|
||||||
|
DO UPDATE SET updated_at = EXCLUDED.updated_at
|
||||||
|
`, userID, group, time.Now()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOrderPriceListPriceGroupLookupsHandler(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
if err := ensureOrderPriceListUserPriceGroupSchema(db); err != nil {
|
||||||
|
log.Printf("[order-price-list-price-groups] schema error: %v", err)
|
||||||
|
http.Error(w, "price group schema error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"price_groups": orderPriceListAllPriceGroups})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMyOrderPriceListPriceGroupsHandler(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
claims, ok := auth.GetClaimsFromContext(r.Context())
|
||||||
|
if !ok || claims == nil {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
groups, err := loadOrderPriceListUserPriceGroups(db, int64(claims.ID))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[order-price-list-price-groups] my groups error user=%d err=%v", claims.ID, err)
|
||||||
|
http.Error(w, "price groups lookup error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"price_groups": groups,
|
||||||
|
"restricted": len(groups) > 0,
|
||||||
|
"all_groups": orderPriceListAllPriceGroups,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserOrderPriceListPriceGroupsHandler(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
http.Error(w, "invalid user id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
groups, err := loadOrderPriceListUserPriceGroups(db, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[order-price-list-price-groups] user groups error user=%d err=%v", id, err)
|
||||||
|
http.Error(w, "price groups lookup error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"price_groups": groups})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveUserOrderPriceListPriceGroupsHandler(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
http.Error(w, "invalid user id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var payload orderPriceListUserPriceGroupPayload
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
|
http.Error(w, "invalid payload", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ensureOrderPriceListUserPriceGroupSchema(db); err != nil {
|
||||||
|
http.Error(w, "price group schema error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "transaction error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
if err := saveOrderPriceListUserPriceGroupsTx(tx, id, payload.PriceGroups); err != nil {
|
||||||
|
log.Printf("[order-price-list-price-groups] save error user=%d err=%v", id, err)
|
||||||
|
http.Error(w, "price groups save error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
http.Error(w, "commit error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOrderPriceListUserPriceGroupRowsHandler(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
if err := ensureOrderPriceListUserPriceGroupSchema(db); err != nil {
|
||||||
|
http.Error(w, "price group schema error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rows, err := db.Query(`
|
||||||
|
SELECT u.id, u.username, COALESCE(u.full_name, ''), COALESCE(u.email, ''),
|
||||||
|
COALESCE(array_agg(m.price_group ORDER BY
|
||||||
|
CASE SUBSTRING(m.price_group, 1, 3)
|
||||||
|
WHEN 'usd' THEN 1
|
||||||
|
WHEN 'eur' THEN 2
|
||||||
|
WHEN 'try' THEN 3
|
||||||
|
ELSE 9
|
||||||
|
END,
|
||||||
|
CAST(SUBSTRING(m.price_group, 4) AS INT)
|
||||||
|
) FILTER (WHERE m.price_group IS NOT NULL), ARRAY[]::text[]) AS price_groups
|
||||||
|
FROM mk_dfusr u
|
||||||
|
LEFT JOIN mk_order_price_list_user_price_group m ON m.user_id = u.id
|
||||||
|
WHERE COALESCE(u.is_active, TRUE) = TRUE
|
||||||
|
GROUP BY u.id, u.username, u.full_name, u.email
|
||||||
|
ORDER BY u.username
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[order-price-list-price-groups] rows error: %v", err)
|
||||||
|
http.Error(w, "price groups rows error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
out := []orderPriceListUserPriceGroupRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var row orderPriceListUserPriceGroupRow
|
||||||
|
if err := rows.Scan(&row.UserID, &row.Username, &row.FullName, &row.Email, pq.Array(&row.PriceGroups)); err != nil {
|
||||||
|
http.Error(w, "price groups scan error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row.PriceGroups = normalizeOrderPriceListPriceGroups(row.PriceGroups)
|
||||||
|
out = append(out, row)
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -545,7 +545,7 @@ func csvEscape(value string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func csvFloat(value float64) string {
|
func csvFloat(value float64) string {
|
||||||
return fmt.Sprintf("%.2f", value)
|
return strings.ReplaceAll(fmt.Sprintf("%.2f", value), ".", ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
func exportPriceFieldTitle(field string) string {
|
func exportPriceFieldTitle(field string) string {
|
||||||
|
|||||||
@@ -384,6 +384,25 @@ WHERE is_active = TRUE
|
|||||||
Dim3 sql.NullInt64
|
Dim3 sql.NullInt64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasDim3Combo := func(dims []dimCombo) bool {
|
||||||
|
for _, d := range dims {
|
||||||
|
if d.Dim3.Valid && d.Dim3.Int64 > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
filterDim3Combos := func(dims []dimCombo) []dimCombo {
|
||||||
|
out := make([]dimCombo, 0, len(dims))
|
||||||
|
for _, d := range dims {
|
||||||
|
if d.Dim3.Valid && d.Dim3.Int64 > 0 {
|
||||||
|
out = append(out, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
type sdprcWriteRow struct {
|
type sdprcWriteRow struct {
|
||||||
Currency string `json:"currency"`
|
Currency string `json:"currency"`
|
||||||
SdprcGrpID int `json:"sdprcgrp_id"`
|
SdprcGrpID int `json:"sdprcgrp_id"`
|
||||||
@@ -562,28 +581,25 @@ DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
|
|||||||
}
|
}
|
||||||
// Resolve to PG dim ids. For this installation we align with mmitem_dim key:
|
// Resolve to PG dim ids. For this installation we align with mmitem_dim key:
|
||||||
// - dim1 = color
|
// - dim1 = color
|
||||||
// - dim3 = itemdim3 (optional)
|
// - dim3 = ItemDim2Code/yaka (optional)
|
||||||
// Size (ItemDim1Code) is not part of the key here.
|
// Size (ItemDim1Code) is not part of the key here.
|
||||||
d1 := int64(0)
|
d1 := int64(0)
|
||||||
if id, ok := resolveDimvalFromToken(pgTx, "dimval1", colorCode); ok {
|
if id, ok := resolveDimvalFromToken(pgTx, "dimval1", colorCode); ok {
|
||||||
d1 = id
|
d1 = id
|
||||||
resolvedDim1++
|
resolvedDim1++
|
||||||
} else if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim3Code); ok {
|
|
||||||
d1 = id
|
|
||||||
resolvedDim1++
|
|
||||||
} else if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim1Code); ok {
|
|
||||||
d1 = id
|
|
||||||
resolvedDim1++
|
|
||||||
}
|
}
|
||||||
if d1 <= 0 {
|
if d1 <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var d3 sql.NullInt64
|
var d3 sql.NullInt64
|
||||||
// dim3 corresponds to mmitem_dim.val3 (ItemDim3Code).
|
// dim3 corresponds to mmitem_dim.val3.
|
||||||
if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim3Code); ok {
|
if id, ok := resolveDimvalFromToken(pgTx, "dimval3", dim3Code); ok {
|
||||||
d3 = sql.NullInt64{Int64: id, Valid: true}
|
d3 = sql.NullInt64{Int64: id, Valid: true}
|
||||||
resolvedDim3++
|
resolvedDim3++
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(dim3Code) != "" && !d3.Valid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
key := fmt.Sprintf("%d|%d", d1, func() int64 {
|
key := fmt.Sprintf("%d|%d", d1, func() int64 {
|
||||||
if d3.Valid {
|
if d3.Valid {
|
||||||
return d3.Int64
|
return d3.Int64
|
||||||
@@ -619,16 +635,13 @@ DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
|
|||||||
if c.Dim1 <= 0 {
|
if c.Dim1 <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
v2 := int64(0)
|
|
||||||
var v2any any = nil
|
var v2any any = nil
|
||||||
if c.Dim3.Valid && c.Dim3.Int64 > 0 {
|
// Active key: val1=color, val3=ItemDim2Code/yaka. val2 is size and is not part of price/campaign key.
|
||||||
v2 = c.Dim3.Int64
|
|
||||||
v2any = v2
|
|
||||||
}
|
|
||||||
// If we managed to resolve an "ItemDim3Code" id too, store it in val3 and mark mmdim_id=3.
|
|
||||||
v3 := int64(0)
|
v3 := int64(0)
|
||||||
if extraVal3 != nil {
|
if c.Dim3.Valid && c.Dim3.Int64 > 0 {
|
||||||
if vv, ok := extraVal3[fmt.Sprintf("%d|%d", c.Dim1, v2)]; ok && vv > 0 {
|
v3 = c.Dim3.Int64
|
||||||
|
} else if extraVal3 != nil {
|
||||||
|
if vv, ok := extraVal3[fmt.Sprintf("%d|0", c.Dim1)]; ok && vv > 0 {
|
||||||
v3 = vv
|
v3 = vv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -687,13 +700,17 @@ WHERE mmitem_id = $1
|
|||||||
if !v1.Valid || v1.Int64 <= 0 {
|
if !v1.Valid || v1.Int64 <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Variant key in this installation: (val1=color, val3=itemdim3_if_any). Ignore val2 (size).
|
// Variant key in this installation: (val1=color, val3=ItemDim2Code/yaka_if_any). Ignore val2 (size).
|
||||||
d1 := v1.Int64
|
d1 := v1.Int64
|
||||||
_ = mmdimID
|
|
||||||
_ = v2
|
_ = v2
|
||||||
|
|
||||||
var d3 sql.NullInt64
|
var d3 sql.NullInt64
|
||||||
if v3.Valid && v3.Int64 > 0 {
|
if mmdimID.Valid && mmdimID.Int64 == 3 {
|
||||||
|
if !v3.Valid || v3.Int64 <= 1000 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v3.Valid && v3.Int64 > 1000 {
|
||||||
d3 = sql.NullInt64{Int64: v3.Int64, Valid: true}
|
d3 = sql.NullInt64{Int64: v3.Int64, Valid: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1071,18 +1088,41 @@ VALUES (
|
|||||||
_ = upsertDimCombosCache(code, dims) // best-effort cache fill
|
_ = upsertDimCombosCache(code, dims) // best-effort cache fill
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Last resort: MSSQL stock tokens, then seed mmitem_dim. Do not use
|
// 2) Merge MSSQL variant master combos. PG may be partially populated; missing
|
||||||
// mk_mmitem_dim_combo as a write source; stale cache rows can create wrong keys.
|
// colors/dim3 combos still need to be seeded before sdprc/zbggcampaign writes.
|
||||||
if len(dims) == 0 {
|
if d, err := loadDimsFromMssqlStock(code); err != nil {
|
||||||
d, err := loadDimsFromMssqlStock(code)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("save:pg:dims:mssql:error", "product_code", code, "err", err)
|
logger.Error("save:pg:dims:mssql:error", "product_code", code, "err", err)
|
||||||
} else {
|
} else if len(d) > 0 {
|
||||||
dims = d
|
if hasDim3Combo(d) {
|
||||||
_ = upsertDimCombosCache(code, dims)
|
dims = filterDim3Combos(dims)
|
||||||
// If PG doesn't have mmitem_dim rows for this product yet, try to seed them.
|
|
||||||
ensureMMItemDimRows(mmItemID, dims, nil)
|
|
||||||
}
|
}
|
||||||
|
seenDims := make(map[string]struct{}, len(dims)+len(d))
|
||||||
|
merged := make([]dimCombo, 0, len(dims)+len(d))
|
||||||
|
dim3Value := func(v sql.NullInt64) int64 {
|
||||||
|
if v.Valid {
|
||||||
|
return v.Int64
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for _, c := range dims {
|
||||||
|
k := fmt.Sprintf("%d|%d", c.Dim1, dim3Value(c.Dim3))
|
||||||
|
if _, ok := seenDims[k]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenDims[k] = struct{}{}
|
||||||
|
merged = append(merged, c)
|
||||||
|
}
|
||||||
|
for _, c := range d {
|
||||||
|
k := fmt.Sprintf("%d|%d", c.Dim1, dim3Value(c.Dim3))
|
||||||
|
if _, ok := seenDims[k]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenDims[k] = struct{}{}
|
||||||
|
merged = append(merged, c)
|
||||||
|
}
|
||||||
|
dims = merged
|
||||||
|
_ = upsertDimCombosCache(code, dims)
|
||||||
|
ensureMMItemDimRows(mmItemID, d, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -184,6 +184,12 @@ func handleUserGet(db *sql.DB, w http.ResponseWriter, userID int64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if groups, err := loadOrderPriceListUserPriceGroups(db, userID); err == nil {
|
||||||
|
u.OrderPriceListPriceGroups = groups
|
||||||
|
} else {
|
||||||
|
log.Printf("WARN [UserDetail] order price list price groups lookup failed user_id=%d err=%v", userID, err)
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// 🟢 RESPONSE
|
// 🟢 RESPONSE
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
@@ -326,6 +332,17 @@ func handleUserUpdate(db *sql.DB, w http.ResponseWriter, r *http.Request, userID
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ensureOrderPriceListUserPriceGroupSchema(db); err != nil {
|
||||||
|
log.Printf("ERROR [UserDetail] price group schema failed user_id=%d err=%v", userID, err)
|
||||||
|
http.Error(w, "Fiyat grubu tablosu hazirlanamadi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := saveOrderPriceListUserPriceGroupsTx(tx, userID, payload.OrderPriceListPriceGroups); err != nil {
|
||||||
|
log.Printf("ERROR [UserDetail] price groups save failed user_id=%d err=%v", userID, err)
|
||||||
|
http.Error(w, "Fiyat gruplari guncellenemedi", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
log.Printf("❌ [UserDetail] commit failed user_id=%d err=%v", userID, err)
|
log.Printf("❌ [UserDetail] commit failed user_id=%d err=%v", userID, err)
|
||||||
http.Error(w, "Commit başarısız", http.StatusInternalServerError)
|
http.Error(w, "Commit başarısız", http.StatusInternalServerError)
|
||||||
@@ -384,6 +401,7 @@ func handleUserDelete(db *sql.DB, w http.ResponseWriter, r *http.Request, userID
|
|||||||
`DELETE FROM dfusr_dprt WHERE dfusr_id = $1`,
|
`DELETE FROM dfusr_dprt WHERE dfusr_id = $1`,
|
||||||
`DELETE FROM dfusr_piyasa WHERE dfusr_id = $1`,
|
`DELETE FROM dfusr_piyasa WHERE dfusr_id = $1`,
|
||||||
`DELETE FROM dfusr_nebim_user WHERE dfusr_id = $1`,
|
`DELETE FROM dfusr_nebim_user WHERE dfusr_id = $1`,
|
||||||
|
`DELETE FROM mk_order_price_list_user_price_group WHERE user_id = $1`,
|
||||||
}
|
}
|
||||||
|
|
||||||
isUndefinedTable := func(err error) bool {
|
isUndefinedTable := func(err error) bool {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -420,6 +421,31 @@ type wholesaleVariantRow struct {
|
|||||||
CampaignLast string `json:"campaign_last_dttm"`
|
CampaignLast string `json:"campaign_last_dttm"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildNebimVariantDisplayCode(colorCode string, dim3Code string) string {
|
||||||
|
colorCode = strings.TrimSpace(colorCode)
|
||||||
|
dim3Code = strings.TrimSpace(dim3Code)
|
||||||
|
if colorCode != "" && dim3Code != "" {
|
||||||
|
return colorCode + "-" + dim3Code
|
||||||
|
}
|
||||||
|
if colorCode != "" {
|
||||||
|
return colorCode
|
||||||
|
}
|
||||||
|
return dim3Code
|
||||||
|
}
|
||||||
|
|
||||||
|
func chooseDisplayDimToken(raw string, resolvedID int64, reverse map[int64]string) string {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw != "" {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
if resolvedID > 0 {
|
||||||
|
if tok := strings.TrimSpace(reverse[resolvedID]); tok != "" {
|
||||||
|
return tok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
type wholesaleCampaignHistoryRow struct {
|
type wholesaleCampaignHistoryRow struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
CampaignID *int64 `json:"campaign_id"`
|
CampaignID *int64 `json:"campaign_id"`
|
||||||
@@ -736,6 +762,7 @@ DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
|
|||||||
ItemID int64
|
ItemID int64
|
||||||
Dim1 int64
|
Dim1 int64
|
||||||
Dim3Key int64
|
Dim3Key int64
|
||||||
|
HasMSSQL bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build base variant keys from PG's authoritative table (mmitem_dim).
|
// Build base variant keys from PG's authoritative table (mmitem_dim).
|
||||||
@@ -745,7 +772,21 @@ DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
|
|||||||
}
|
}
|
||||||
tmpMap := make(map[string]tmpRow, 4096)
|
tmpMap := make(map[string]tmpRow, 4096)
|
||||||
hasMMItemDim := make(map[int64]bool, len(itemIDs))
|
hasMMItemDim := make(map[int64]bool, len(itemIDs))
|
||||||
dimIDs := make([]int64, 0, 8192)
|
dim1IDs := make([]int64, 0, 8192)
|
||||||
|
dim3IDs := make([]int64, 0, 8192)
|
||||||
|
itemDim1Candidates := make(map[int64][]int64, len(itemIDs))
|
||||||
|
itemDim3Candidates := make(map[int64][]int64, len(itemIDs))
|
||||||
|
addCandidate := func(dst map[int64][]int64, itemID int64, id int64) {
|
||||||
|
if itemID <= 0 || id <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, existing := range dst[itemID] {
|
||||||
|
if existing == id {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dst[itemID] = append(dst[itemID], id)
|
||||||
|
}
|
||||||
if len(itemIDs) > 0 {
|
if len(itemIDs) > 0 {
|
||||||
rows, err := pg.QueryContext(ctx, `
|
rows, err := pg.QueryContext(ctx, `
|
||||||
SELECT mmitem_id, mmdim_id, val1, val2, val3
|
SELECT mmitem_id, mmdim_id, val1, val2, val3
|
||||||
@@ -772,13 +813,19 @@ WHERE mmitem_id = ANY($1::bigint[])
|
|||||||
if !v1.Valid || v1.Int64 <= 0 {
|
if !v1.Valid || v1.Int64 <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Variant key in this installation: (val1=color, val3=itemdim3_if_any). Ignore val2 (size).
|
// Variant key in this installation: (val1=color, val3=ItemDim2Code/yaka_if_any). Ignore val2 (size).
|
||||||
d1 := v1.Int64
|
d1 := v1.Int64
|
||||||
_ = mmdimID
|
|
||||||
_ = v2
|
_ = v2
|
||||||
|
addCandidate(itemDim1Candidates, itemID, d1)
|
||||||
d3k := int64(0)
|
d3k := int64(0)
|
||||||
if v3.Valid && v3.Int64 > 0 {
|
if mmdimID.Valid && mmdimID.Int64 == 3 {
|
||||||
|
if !v3.Valid || v3.Int64 <= 1000 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v3.Valid && v3.Int64 > 1000 {
|
||||||
d3k = v3.Int64
|
d3k = v3.Int64
|
||||||
|
addCandidate(itemDim3Candidates, itemID, d3k)
|
||||||
}
|
}
|
||||||
|
|
||||||
code := strings.TrimSpace(itemToCode[itemID])
|
code := strings.TrimSpace(itemToCode[itemID])
|
||||||
@@ -797,21 +844,26 @@ WHERE mmitem_id = ANY($1::bigint[])
|
|||||||
Dim1: d1,
|
Dim1: d1,
|
||||||
Dim3Key: d3k,
|
Dim3Key: d3k,
|
||||||
}
|
}
|
||||||
dimIDs = append(dimIDs, d1)
|
dim1IDs = append(dim1IDs, d1)
|
||||||
if d3k > 0 {
|
if d3k > 0 {
|
||||||
dimIDs = append(dimIDs, d3k)
|
dim3IDs = append(dim3IDs, d3k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rows.Close()
|
rows.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve dim ids -> tokens for a readable VariantCode.
|
// Resolve dim ids -> tokens for a fallback readable VariantCode.
|
||||||
idToToken := map[int64]string{}
|
// MSSQL/Nebim tokens override this below; PG ids are only storage keys.
|
||||||
if len(dimIDs) > 0 {
|
idToDim1Token := map[int64]string{}
|
||||||
|
idToDim3Token := map[int64]string{}
|
||||||
|
loadReverseTokens := func(column string, ids []int64, out map[int64]string) {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
// uniq
|
// uniq
|
||||||
uniq := make([]int64, 0, len(dimIDs))
|
uniq := make([]int64, 0, len(ids))
|
||||||
seen := make(map[int64]struct{}, len(dimIDs))
|
seen := make(map[int64]struct{}, len(ids))
|
||||||
for _, id := range dimIDs {
|
for _, id := range ids {
|
||||||
if id <= 0 {
|
if id <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -821,14 +873,16 @@ WHERE mmitem_id = ANY($1::bigint[])
|
|||||||
seen[id] = struct{}{}
|
seen[id] = struct{}{}
|
||||||
uniq = append(uniq, id)
|
uniq = append(uniq, id)
|
||||||
}
|
}
|
||||||
if len(uniq) > 0 {
|
if len(uniq) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
rows, err := pg.QueryContext(ctx, `
|
rows, err := pg.QueryContext(ctx, `
|
||||||
SELECT DISTINCT ON (dim_id) dim_id, token
|
SELECT DISTINCT ON (dim_id) dim_id, token
|
||||||
FROM mk_dim_token_map
|
FROM mk_dim_token_map
|
||||||
WHERE dim_column = 'dimval1'
|
WHERE dim_column = $1
|
||||||
AND dim_id = ANY($1::bigint[])
|
AND dim_id = ANY($2::bigint[])
|
||||||
ORDER BY dim_id, updated_at DESC;
|
ORDER BY dim_id, updated_at DESC;
|
||||||
`, pq.Array(uniq))
|
`, column, pq.Array(uniq))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var id int64
|
var id int64
|
||||||
@@ -836,20 +890,21 @@ ORDER BY dim_id, updated_at DESC;
|
|||||||
_ = rows.Scan(&id, &tok)
|
_ = rows.Scan(&id, &tok)
|
||||||
tok = strings.TrimSpace(tok)
|
tok = strings.TrimSpace(tok)
|
||||||
if tok != "" {
|
if tok != "" {
|
||||||
idToToken[id] = tok
|
out[id] = tok
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rows.Close()
|
rows.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
loadReverseTokens("dimval1", dim1IDs, idToDim1Token)
|
||||||
|
loadReverseTokens("dimval3", dim3IDs, idToDim3Token)
|
||||||
for k, v := range tmpMap {
|
for k, v := range tmpMap {
|
||||||
t1 := strings.TrimSpace(idToToken[v.Dim1])
|
t1 := strings.TrimSpace(idToDim1Token[v.Dim1])
|
||||||
if t1 == "" {
|
if t1 == "" {
|
||||||
t1 = fmt.Sprintf("%d", v.Dim1)
|
t1 = fmt.Sprintf("%d", v.Dim1)
|
||||||
}
|
}
|
||||||
if v.Dim3Key > 0 {
|
if v.Dim3Key > 0 {
|
||||||
t3 := strings.TrimSpace(idToToken[v.Dim3Key])
|
t3 := strings.TrimSpace(idToDim3Token[v.Dim3Key])
|
||||||
if t3 == "" {
|
if t3 == "" {
|
||||||
t3 = fmt.Sprintf("%d", v.Dim3Key)
|
t3 = fmt.Sprintf("%d", v.Dim3Key)
|
||||||
}
|
}
|
||||||
@@ -859,6 +914,120 @@ ORDER BY dim_id, updated_at DESC;
|
|||||||
}
|
}
|
||||||
tmpMap[k] = v
|
tmpMap[k] = v
|
||||||
}
|
}
|
||||||
|
canonicalToken := func(column string, id int64) string {
|
||||||
|
if id <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
target := idToDim1Token
|
||||||
|
if column == "dimval3" {
|
||||||
|
target = idToDim3Token
|
||||||
|
}
|
||||||
|
if tok := strings.TrimSpace(target[id]); tok != "" {
|
||||||
|
return tok
|
||||||
|
}
|
||||||
|
var tok string
|
||||||
|
if err := pg.QueryRowContext(ctx, `
|
||||||
|
SELECT token
|
||||||
|
FROM mk_dim_token_map
|
||||||
|
WHERE dim_column = $1
|
||||||
|
AND dim_id = $2
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN token ~ '^[0-9]{3}$' THEN 0 ELSE 1 END,
|
||||||
|
length(token),
|
||||||
|
updated_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
`, column, id).Scan(&tok); err == nil {
|
||||||
|
tok = strings.TrimSpace(tok)
|
||||||
|
if tok != "" {
|
||||||
|
target[id] = tok
|
||||||
|
return tok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
sortDimIDs := func(ids []int64) []int64 {
|
||||||
|
out := append([]int64(nil), ids...)
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
sortTokens := func(tokens []string) []string {
|
||||||
|
out := append([]string(nil), tokens...)
|
||||||
|
sort.Slice(out, func(i, j int) bool {
|
||||||
|
li := strings.TrimLeft(out[i], "0")
|
||||||
|
lj := strings.TrimLeft(out[j], "0")
|
||||||
|
if li == "" {
|
||||||
|
li = "0"
|
||||||
|
}
|
||||||
|
if lj == "" {
|
||||||
|
lj = "0"
|
||||||
|
}
|
||||||
|
ni, ei := strconv.ParseInt(li, 10, 64)
|
||||||
|
nj, ej := strconv.ParseInt(lj, 10, 64)
|
||||||
|
if ei == nil && ej == nil && ni != nj {
|
||||||
|
return ni < nj
|
||||||
|
}
|
||||||
|
return out[i] < out[j]
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
addToken := func(dst map[int64][]string, itemID int64, token string) {
|
||||||
|
token = strings.ToUpper(normalizeDimParam(token))
|
||||||
|
if itemID <= 0 || token == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, existing := range dst[itemID] {
|
||||||
|
if existing == token {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dst[itemID] = append(dst[itemID], token)
|
||||||
|
}
|
||||||
|
buildInferredMap := func(column string, tokenByItem map[int64][]string, idsByItem map[int64][]int64) map[string]int64 {
|
||||||
|
out := make(map[string]int64, 128)
|
||||||
|
for itemID, tokens := range tokenByItem {
|
||||||
|
sortedTokens := sortTokens(tokens)
|
||||||
|
sortedIDs := sortDimIDs(idsByItem[itemID])
|
||||||
|
if len(sortedTokens) == 0 || len(sortedIDs) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
limit := len(sortedTokens)
|
||||||
|
if len(sortedIDs) < limit {
|
||||||
|
limit = len(sortedIDs)
|
||||||
|
}
|
||||||
|
for i := 0; i < limit; i++ {
|
||||||
|
token := sortedTokens[i]
|
||||||
|
id := sortedIDs[i]
|
||||||
|
if token == "" || id <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := fmt.Sprintf("%s|%d|%s", column, itemID, token)
|
||||||
|
if _, exists := out[key]; !exists {
|
||||||
|
out[key] = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
persistDimToken := func(column string, token string, id int64) {
|
||||||
|
token = strings.ToUpper(normalizeDimParam(token))
|
||||||
|
if column == "" || token == "" || id <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = pg.ExecContext(ctx, `
|
||||||
|
INSERT INTO mk_dim_token_map (dim_column, token, dim_id, updated_at)
|
||||||
|
VALUES ($1,$2,$3,now())
|
||||||
|
ON CONFLICT (dim_column, token)
|
||||||
|
DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
|
||||||
|
`, column, token, id)
|
||||||
|
}
|
||||||
|
type msVariantRow struct {
|
||||||
|
ItemCode string
|
||||||
|
ColorCode string
|
||||||
|
Dim1Code string
|
||||||
|
Dim3Code string
|
||||||
|
Qty sql.NullFloat64
|
||||||
|
}
|
||||||
|
|
||||||
// MSSQL: stock list for selected products; map to (mmitem_id, dim1, dim3_key) via token->id mapping.
|
// MSSQL: stock list for selected products; map to (mmitem_id, dim1, dim3_key) via token->id mapping.
|
||||||
joined := strings.Join(codes, ",")
|
joined := strings.Join(codes, ",")
|
||||||
@@ -867,11 +1036,15 @@ ORDER BY dim_id, updated_at DESC;
|
|||||||
http.Error(w, "variant stock query error: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "variant stock query error: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer msRows.Close()
|
msVariants := make([]msVariantRow, 0, 1024)
|
||||||
|
colorTokensByItem := make(map[int64][]string, len(itemIDs))
|
||||||
|
dim3TokensByItem := make(map[int64][]string, len(itemIDs))
|
||||||
|
itemHasMssqlDim3 := make(map[int64]bool, len(itemIDs))
|
||||||
for msRows.Next() {
|
for msRows.Next() {
|
||||||
var itemCode, colorCode, dim1Code, dim3Code string
|
var itemCode, colorCode, dim1Code, dim3Code string
|
||||||
var qty sql.NullFloat64
|
var qty sql.NullFloat64
|
||||||
if err := msRows.Scan(&itemCode, &colorCode, &dim1Code, &dim3Code, &qty); err != nil {
|
if err := msRows.Scan(&itemCode, &colorCode, &dim1Code, &dim3Code, &qty); err != nil {
|
||||||
|
msRows.Close()
|
||||||
http.Error(w, "variant stock scan error", http.StatusInternalServerError)
|
http.Error(w, "variant stock scan error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -883,31 +1056,86 @@ ORDER BY dim_id, updated_at DESC;
|
|||||||
if itemID <= 0 {
|
if itemID <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
msVariants = append(msVariants, msVariantRow{
|
||||||
|
ItemCode: itemCode,
|
||||||
|
ColorCode: colorCode,
|
||||||
|
Dim1Code: dim1Code,
|
||||||
|
Dim3Code: dim3Code,
|
||||||
|
Qty: qty,
|
||||||
|
})
|
||||||
|
addToken(colorTokensByItem, itemID, colorCode)
|
||||||
|
addToken(dim3TokensByItem, itemID, dim3Code)
|
||||||
|
if strings.TrimSpace(dim3Code) != "" {
|
||||||
|
itemHasMssqlDim3[itemID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := msRows.Err(); err != nil {
|
||||||
|
msRows.Close()
|
||||||
|
http.Error(w, "variant stock rows error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msRows.Close()
|
||||||
|
|
||||||
// Map Nebim tokens to PG integer ids (dimval1 namespace).
|
inferredDim1 := buildInferredMap("dimval1", colorTokensByItem, itemDim1Candidates)
|
||||||
// This app uses key: dim1=<color>, dim3=<itemdim3> to match mmitem_dim (val1,val3).
|
inferredDim3 := buildInferredMap("dimval3", dim3TokensByItem, itemDim3Candidates)
|
||||||
|
for key, row := range tmpMap {
|
||||||
|
if itemHasMssqlDim3[row.ItemID] && row.Dim3Key == 0 {
|
||||||
|
delete(tmpMap, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolveProductDimID := func(itemID int64, column string, token string, inferred map[string]int64) (int64, bool) {
|
||||||
|
token = strings.ToUpper(normalizeDimParam(token))
|
||||||
|
if token == "" {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
if id := inferred[fmt.Sprintf("%s|%d|%s", column, itemID, token)]; id > 0 {
|
||||||
|
persistDimToken(column, token, id)
|
||||||
|
return id, true
|
||||||
|
}
|
||||||
|
if id, ok := resolveDimID(column, token); ok {
|
||||||
|
return id, true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ms := range msVariants {
|
||||||
|
itemCode := ms.ItemCode
|
||||||
|
colorCode := ms.ColorCode
|
||||||
|
dim1Code := ms.Dim1Code
|
||||||
|
dim3Code := ms.Dim3Code
|
||||||
|
qty := ms.Qty
|
||||||
|
itemID := codeToItem[itemCode]
|
||||||
|
if itemID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map Nebim tokens to PG integer ids. Color and yaka must use separate token namespaces,
|
||||||
|
// because the same visible token (for example "001") can exist in both dimensions.
|
||||||
d1 := int64(0)
|
d1 := int64(0)
|
||||||
if id, ok := resolveDimID("dimval1", colorCode); ok {
|
if id, ok := resolveProductDimID(itemID, "dimval1", colorCode, inferredDim1); ok {
|
||||||
d1 = id
|
d1 = id
|
||||||
}
|
}
|
||||||
if d1 <= 0 {
|
if d1 <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
d3k := int64(0)
|
d3k := int64(0)
|
||||||
if id, ok := resolveDimID("dimval1", dim3Code); ok {
|
if id, ok := resolveProductDimID(itemID, "dimval3", dim3Code, inferredDim3); ok {
|
||||||
d3k = id
|
d3k = id
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(dim3Code) != "" && d3k <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
key := fmt.Sprintf("%d|%d|%d", itemID, d1, d3k)
|
key := fmt.Sprintf("%d|%d|%d", itemID, d1, d3k)
|
||||||
prev, ok := tmpMap[key]
|
prev, ok := tmpMap[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
// If PG does not have mmitem_dim rows for this item yet, seed it from MSSQL and include it.
|
// Seed missing MSSQL combos even when the product already has some mmitem_dim rows.
|
||||||
if !hasMMItemDim[itemID] {
|
// PG remains the storage key source, but MSSQL may reveal new/missing color or dim3 combos.
|
||||||
var v2 any = nil
|
var v2 any = nil
|
||||||
if d3k > 0 {
|
if sizeID, ok := resolveDimID("dimval1", dim1Code); ok && sizeID > 0 {
|
||||||
v2 = d3k
|
v2 = sizeID
|
||||||
}
|
}
|
||||||
v3 := int64(0)
|
v3 := int64(0)
|
||||||
if id, ok := resolveDimID("dimval1", dim3Code); ok {
|
if id, ok := resolveProductDimID(itemID, "dimval3", dim3Code, inferredDim3); ok {
|
||||||
v3 = id
|
v3 = id
|
||||||
}
|
}
|
||||||
mmdimID := int64(2)
|
mmdimID := int64(2)
|
||||||
@@ -943,14 +1171,13 @@ WHERE NOT EXISTS (
|
|||||||
Dim3Key: d3k,
|
Dim3Key: d3k,
|
||||||
}
|
}
|
||||||
// Keep dim token cache for VariantCode formatting.
|
// Keep dim token cache for VariantCode formatting.
|
||||||
dimIDs = append(dimIDs, d1)
|
dim1IDs = append(dim1IDs, d1)
|
||||||
if d3k > 0 {
|
if d3k > 0 {
|
||||||
dimIDs = append(dimIDs, d3k)
|
dim3IDs = append(dim3IDs, d3k)
|
||||||
}
|
}
|
||||||
prev = tmpMap[key]
|
prev = tmpMap[key]
|
||||||
ok = true
|
ok = true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -960,6 +1187,10 @@ WHERE NOT EXISTS (
|
|||||||
q = qty.Float64
|
q = qty.Float64
|
||||||
}
|
}
|
||||||
prev.StockQty += q
|
prev.StockQty += q
|
||||||
|
displayColor := chooseDisplayDimToken(colorCode, d1, map[int64]string{d1: canonicalToken("dimval1", d1)})
|
||||||
|
displayDim3 := chooseDisplayDimToken(dim3Code, d3k, map[int64]string{d3k: canonicalToken("dimval3", d3k)})
|
||||||
|
prev.VariantCode = buildNebimVariantDisplayCode(displayColor, displayDim3)
|
||||||
|
prev.HasMSSQL = true
|
||||||
tmpMap[key] = prev
|
tmpMap[key] = prev
|
||||||
_ = colorCode // display-only
|
_ = colorCode // display-only
|
||||||
}
|
}
|
||||||
@@ -968,6 +1199,15 @@ WHERE NOT EXISTS (
|
|||||||
for _, v := range tmpMap {
|
for _, v := range tmpMap {
|
||||||
tmp = append(tmp, v)
|
tmp = append(tmp, v)
|
||||||
}
|
}
|
||||||
|
sort.SliceStable(tmp, func(i, j int) bool {
|
||||||
|
if tmp[i].ProductCode != tmp[j].ProductCode {
|
||||||
|
return tmp[i].ProductCode < tmp[j].ProductCode
|
||||||
|
}
|
||||||
|
if tmp[i].HasMSSQL != tmp[j].HasMSSQL {
|
||||||
|
return tmp[i].HasMSSQL
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(tmp[i].VariantCode) < strings.TrimSpace(tmp[j].VariantCode)
|
||||||
|
})
|
||||||
|
|
||||||
// Bulk load campaign assignment for each (mmitem_id, dim1, dim3_key)
|
// Bulk load campaign assignment for each (mmitem_id, dim1, dim3_key)
|
||||||
type keyRec struct {
|
type keyRec struct {
|
||||||
|
|||||||
@@ -413,6 +413,16 @@ const menuItems = [
|
|||||||
label: 'Fiyatlandırma Mail Eşleştirme',
|
label: 'Fiyatlandırma Mail Eşleştirme',
|
||||||
to: '/app/pricing-mail-mapping',
|
to: '/app/pricing-mail-mapping',
|
||||||
permission: 'system:update'
|
permission: 'system:update'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Fiyat Listesi Mail Eşleştirme',
|
||||||
|
to: '/app/order-price-list-mail-mapping',
|
||||||
|
permission: 'system:update'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Kullanıcı Fiyat Eşleştirme',
|
||||||
|
to: '/app/order-price-list-user-price-groups',
|
||||||
|
permission: 'system:update'
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row items-center q-gutter-xs top-actions-row top-actions-row--actions">
|
<div class="row items-center q-gutter-xs top-actions-row top-actions-row--actions">
|
||||||
|
<div class="toolbar-group">
|
||||||
<q-btn-dropdown dense color="secondary" outline icon="view_module" label="Fiyat Secimi" :auto-close="false" :disable="pageBusy">
|
<q-btn-dropdown dense color="secondary" outline icon="view_module" label="Fiyat Secimi" :auto-close="false" :disable="pageBusy">
|
||||||
<q-list dense class="currency-menu-list">
|
<q-list dense class="currency-menu-list">
|
||||||
<q-item clickable @click="selectAllPrices">
|
<q-item clickable @click="selectAllPrices">
|
||||||
@@ -79,7 +80,7 @@
|
|||||||
<q-item-section>Tumunu Temizle</q-item-section>
|
<q-item-section>Tumunu Temizle</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-separator />
|
<q-separator />
|
||||||
<q-item v-for="option in priceOptions" :key="option.value" clickable @click="togglePriceOption(option.value)">
|
<q-item v-for="option in allowedPriceOptions" :key="option.value" clickable @click="togglePriceOption(option.value)">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
dense
|
dense
|
||||||
@@ -103,7 +104,16 @@
|
|||||||
:disable="pageBusy"
|
:disable="pageBusy"
|
||||||
@click="leftDetailsExpanded = !leftDetailsExpanded"
|
@click="leftDetailsExpanded = !leftDetailsExpanded"
|
||||||
/>
|
/>
|
||||||
|
<q-toggle
|
||||||
|
v-model="showInStockOnly"
|
||||||
|
dense
|
||||||
|
color="primary"
|
||||||
|
label="Sadece stogu olanlar"
|
||||||
|
:disable="pageBusy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toolbar-group">
|
||||||
<q-btn-dropdown dense color="primary" outline icon="download" label="Cikti Al" :disable="pageBusy || filteredRows.length === 0">
|
<q-btn-dropdown dense color="primary" outline icon="download" label="Cikti Al" :disable="pageBusy || filteredRows.length === 0">
|
||||||
<q-list dense style="min-width: 220px;">
|
<q-list dense style="min-width: 220px;">
|
||||||
<q-item clickable @click="exportVisibleExcel">
|
<q-item clickable @click="exportVisibleExcel">
|
||||||
@@ -116,9 +126,11 @@
|
|||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-btn-dropdown>
|
</q-btn-dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
<q-space />
|
<q-space />
|
||||||
|
|
||||||
|
<div class="toolbar-group toolbar-group--paging">
|
||||||
<q-pagination
|
<q-pagination
|
||||||
v-model="currentPage"
|
v-model="currentPage"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -135,6 +147,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }">
|
<div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }">
|
||||||
<div v-if="showGuidanceOverlay" class="empty-overlay">
|
<div v-if="showGuidanceOverlay" class="empty-overlay">
|
||||||
@@ -152,7 +165,7 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="top-x-scroll-inner"
|
class="top-x-scroll-inner"
|
||||||
:style="{ width: `${tableMinWidth}px` }"
|
:style="{ width: `${tableScrollWidth}px` }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<q-table
|
<q-table
|
||||||
@@ -315,6 +328,137 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-menu>
|
</q-menu>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-else-if="col.name === 'variantCodes'"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
round
|
||||||
|
size="8px"
|
||||||
|
icon="filter_alt"
|
||||||
|
:color="selectedVariantCodes.length > 0 ? 'primary' : 'grey-7'"
|
||||||
|
:disable="pageBusy"
|
||||||
|
class="header-filter-btn"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<q-badge v-if="selectedVariantCodes.length > 0" color="primary" floating rounded>
|
||||||
|
{{ selectedVariantCodes.length }}
|
||||||
|
</q-badge>
|
||||||
|
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
|
||||||
|
<div class="excel-filter-menu">
|
||||||
|
<q-input
|
||||||
|
v-model="variantFilterSearch"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
clearable
|
||||||
|
class="excel-filter-select"
|
||||||
|
placeholder="Varyant ara"
|
||||||
|
/>
|
||||||
|
<div class="excel-filter-actions row items-center justify-between q-pt-xs">
|
||||||
|
<q-btn flat dense size="sm" label="Tumunu Sec" :disable="pageBusy || filteredVariantOptions.length === 0" @click="selectAllVariantOptions" />
|
||||||
|
<q-btn flat dense size="sm" label="Temizle" :disable="pageBusy || selectedVariantCodes.length === 0" @click="clearVariantOptions" />
|
||||||
|
</div>
|
||||||
|
<q-virtual-scroll
|
||||||
|
v-if="filteredVariantOptions.length > 0"
|
||||||
|
class="excel-filter-options"
|
||||||
|
:items="filteredVariantOptions"
|
||||||
|
:virtual-scroll-item-size="32"
|
||||||
|
separator
|
||||||
|
>
|
||||||
|
<template #default="{ item: option }">
|
||||||
|
<q-item
|
||||||
|
:key="`variant-${option.value}`"
|
||||||
|
dense
|
||||||
|
clickable
|
||||||
|
:disable="pageBusy"
|
||||||
|
class="excel-filter-option"
|
||||||
|
@click="toggleVariantValue(option.value)"
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-checkbox
|
||||||
|
dense
|
||||||
|
size="sm"
|
||||||
|
:model-value="selectedVariantCodeSet.has(option.value)"
|
||||||
|
:disable="pageBusy"
|
||||||
|
@update:model-value="() => toggleVariantValue(option.value)"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ option.label }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-virtual-scroll>
|
||||||
|
<div v-else class="excel-filter-empty">Sonuc yok</div>
|
||||||
|
</div>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-else-if="isLocalFilterableColumn(col.name)"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
round
|
||||||
|
size="8px"
|
||||||
|
icon="filter_alt"
|
||||||
|
:color="getColumnFilterValues(col.name).length > 0 ? 'primary' : 'grey-7'"
|
||||||
|
:disable="pageBusy"
|
||||||
|
class="header-filter-btn"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<q-badge v-if="getColumnFilterValues(col.name).length > 0" color="primary" floating rounded>
|
||||||
|
{{ getColumnFilterValues(col.name).length }}
|
||||||
|
</q-badge>
|
||||||
|
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
|
||||||
|
<div class="excel-filter-menu">
|
||||||
|
<q-input
|
||||||
|
:model-value="columnFilterSearch[col.name] || ''"
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
clearable
|
||||||
|
class="excel-filter-select"
|
||||||
|
placeholder="Filtre ara"
|
||||||
|
@update:model-value="(val) => setColumnFilterSearch(col.name, val)"
|
||||||
|
/>
|
||||||
|
<div class="excel-filter-actions row items-center justify-between q-pt-xs">
|
||||||
|
<q-btn flat dense size="sm" label="Tumunu Sec" :disable="pageBusy || getFilteredColumnOptions(col).length === 0" @click="selectAllColumnOptions(col)" />
|
||||||
|
<q-btn flat dense size="sm" label="Temizle" :disable="pageBusy || getColumnFilterValues(col.name).length === 0" @click="clearColumnOptions(col.name)" />
|
||||||
|
</div>
|
||||||
|
<q-virtual-scroll
|
||||||
|
v-if="getFilteredColumnOptions(col).length > 0"
|
||||||
|
class="excel-filter-options"
|
||||||
|
:items="getFilteredColumnOptions(col)"
|
||||||
|
:virtual-scroll-item-size="32"
|
||||||
|
separator
|
||||||
|
>
|
||||||
|
<template #default="{ item: option }">
|
||||||
|
<q-item
|
||||||
|
:key="`${col.name}-${option.value}`"
|
||||||
|
dense
|
||||||
|
clickable
|
||||||
|
:disable="pageBusy"
|
||||||
|
class="excel-filter-option"
|
||||||
|
@click="toggleColumnFilterValue(col.name, option.value)"
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-checkbox
|
||||||
|
dense
|
||||||
|
size="sm"
|
||||||
|
:model-value="getColumnFilterSet(col.name).has(option.value)"
|
||||||
|
:disable="pageBusy"
|
||||||
|
@update:model-value="() => toggleColumnFilterValue(col.name, option.value)"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ option.label || '-' }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-virtual-scroll>
|
||||||
|
<div v-else class="excel-filter-empty">Sonuc yok</div>
|
||||||
|
</div>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
<span v-else class="header-filter-ghost"></span>
|
<span v-else class="header-filter-ghost"></span>
|
||||||
</div>
|
</div>
|
||||||
</q-th>
|
</q-th>
|
||||||
@@ -340,10 +484,14 @@
|
|||||||
<q-img
|
<q-img
|
||||||
v-if="props.row.imageUrl"
|
v-if="props.row.imageUrl"
|
||||||
:src="props.row.imageUrl"
|
:src="props.row.imageUrl"
|
||||||
class="product-thumb"
|
class="product-thumb cursor-pointer"
|
||||||
fit="cover"
|
fit="cover"
|
||||||
no-spinner
|
no-spinner
|
||||||
|
@click.stop="openProductCard(props.row)"
|
||||||
/>
|
/>
|
||||||
|
<div v-else class="product-thumb-placeholder cursor-pointer" @click.stop="openProductCard(props.row)">
|
||||||
|
<q-icon name="image_not_supported" size="24px" color="grey-6" />
|
||||||
|
</div>
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -353,8 +501,11 @@
|
|||||||
:class="[props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
|
:class="[props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
|
||||||
:style="getBodyCellStyle(props.col)"
|
:style="getBodyCellStyle(props.col)"
|
||||||
>
|
>
|
||||||
<q-badge v-if="props.row.campaignLabel" color="primary" outline :label="props.row.campaignLabel" />
|
<div class="campaign-cell-content">
|
||||||
<span v-else class="text-grey-6">-</span>
|
<span v-if="props.row.campaignLabel" class="campaign-text" :title="props.row.campaignLabel">
|
||||||
|
{{ props.row.campaignLabel }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -364,7 +515,9 @@
|
|||||||
:class="[props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
|
:class="[props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
|
||||||
:style="getBodyCellStyle(props.col)"
|
:style="getBodyCellStyle(props.col)"
|
||||||
>
|
>
|
||||||
|
<span class="cell-text campaign-rate-text" :title="String(props.row.campaignRate ?? '')">
|
||||||
{{ props.row.campaignRate ? formatPrice(props.row.campaignRate) : '' }}
|
{{ props.row.campaignRate ? formatPrice(props.row.campaignRate) : '' }}
|
||||||
|
</span>
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -374,11 +527,139 @@
|
|||||||
:class="['text-right', props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
|
:class="['text-right', props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
|
||||||
:style="getBodyCellStyle(props.col)"
|
:style="getBodyCellStyle(props.col)"
|
||||||
>
|
>
|
||||||
|
<span :class="['cell-text', 'price-cell-text', { 'campaign-price-text': name.endsWith('Campaign') }]" :title="formatPrice(props.row[name])">
|
||||||
{{ formatPrice(props.row[name]) }}
|
{{ formatPrice(props.row[name]) }}
|
||||||
|
</span>
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
</q-table>
|
</q-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<q-dialog v-model="productCardDialog" maximized @hide="onProductCardDialogHide">
|
||||||
|
<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-content">
|
||||||
|
<div class="product-card-images">
|
||||||
|
<q-carousel
|
||||||
|
v-if="productCardImages.length"
|
||||||
|
v-model="productCardSlide"
|
||||||
|
animated
|
||||||
|
swipeable
|
||||||
|
navigation
|
||||||
|
arrows
|
||||||
|
height="100%"
|
||||||
|
class="product-card-carousel rounded-borders"
|
||||||
|
>
|
||||||
|
<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 field-row-head"><span class="k">Urun</span><span class="v">{{ productCardData.productCode || '-' }} / {{ productCardData.variantCodes || '-' }}</span></div>
|
||||||
|
<div class="field-row"><span class="k">Urun Kodu</span><span class="v">{{ productCardData.productCode || '-' }}</span></div>
|
||||||
|
<div class="field-row"><span class="k">Varyant</span><span class="v">{{ productCardData.variantCodes || '-' }}</span></div>
|
||||||
|
<div class="field-row"><span class="k">Marka</span><span class="v">{{ productCardData.marka || '-' }}</span></div>
|
||||||
|
<div class="field-row"><span class="k">Marka Grubu</span><span class="v">{{ productCardData.brandGroupSelection || '-' }}</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 Ilk Grubu</span><span class="v">{{ productCardData.urunIlkGrubu || '-' }}</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">Icerik</span><span class="v">{{ productCardData.icerik || '-' }}</span></div>
|
||||||
|
<div class="field-row"><span class="k">Karisim</span><span class="v">{{ productCardData.karisim || '-' }}</span></div>
|
||||||
|
<div class="field-row"><span class="k">Kampanya</span><span class="v">{{ productCardData.campaignLabel || '-' }}</span></div>
|
||||||
|
<div class="field-row"><span class="k">Stok</span><span class="v">{{ formatStock(productCardData.stockQty || 0) }}</span></div>
|
||||||
|
|
||||||
|
<div class="product-card-section">
|
||||||
|
<div class="product-card-section-title">Beden Stoklari</div>
|
||||||
|
<q-inner-loading :showing="productCardStockLoading">
|
||||||
|
<q-spinner size="24px" color="primary" />
|
||||||
|
</q-inner-loading>
|
||||||
|
<div v-if="productCardSizeRows.length" class="size-stock-grid">
|
||||||
|
<div v-for="item in productCardSizeRows" :key="item.size" class="size-stock-cell">
|
||||||
|
<span class="size-label">{{ item.size }}</span>
|
||||||
|
<span class="size-qty">{{ formatStock(item.qty) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!productCardStockLoading" class="product-card-empty-text">Beden stogu bulunamadi.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product-card-price-panel">
|
||||||
|
<div class="product-card-section product-card-price-section">
|
||||||
|
<div class="product-card-section-title">Fiyat Bilgileri</div>
|
||||||
|
<div class="price-info-header">
|
||||||
|
<span>Fiyat</span>
|
||||||
|
<span>Liste</span>
|
||||||
|
<span>Kampanyali</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="productCardPriceRows.length" class="price-info-grid">
|
||||||
|
<div v-for="item in productCardPriceRows" :key="item.key" :class="['price-info-row', `price-info-row-${item.currency}`, { 'has-campaign-price': item.hasCampaignPrice }]">
|
||||||
|
<span class="price-label">{{ item.label }}</span>
|
||||||
|
<span class="price-value">{{ item.price || '-' }}</span>
|
||||||
|
<span class="price-campaign">{{ item.campaignPrice || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="product-card-empty-text">Secili fiyat kolonu yok.</div>
|
||||||
|
</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">
|
||||||
|
<q-carousel
|
||||||
|
v-if="fullscreenImages.length"
|
||||||
|
v-model="productImageFullscreenSlide"
|
||||||
|
animated
|
||||||
|
swipeable
|
||||||
|
navigation
|
||||||
|
arrows
|
||||||
|
height="calc(100vh - 120px)"
|
||||||
|
class="image-fullscreen-carousel"
|
||||||
|
>
|
||||||
|
<q-carousel-slide
|
||||||
|
v-for="(img, idx) in fullscreenImages"
|
||||||
|
:key="'full-img-' + idx"
|
||||||
|
:name="idx"
|
||||||
|
class="column no-wrap flex-center"
|
||||||
|
>
|
||||||
|
<div class="image-fullscreen-stage">
|
||||||
|
<q-img :src="img" fit="contain" class="image-fullscreen-img" />
|
||||||
|
</div>
|
||||||
|
</q-carousel-slide>
|
||||||
|
</q-carousel>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -390,10 +671,18 @@ import api from 'src/services/api'
|
|||||||
const PAGE_LIMIT = 250
|
const PAGE_LIMIT = 250
|
||||||
const GUIDANCE_MSG = 'Liste icin filtre secin.'
|
const GUIDANCE_MSG = 'Liste icin filtre secin.'
|
||||||
|
|
||||||
const priceOptions = ['USD', 'EUR', 'TRY'].flatMap((cur) => [1, 2, 3, 4, 5, 6].map((lv) => ({
|
const allPriceOptions = ['USD', 'EUR', 'TRY'].flatMap((cur) => [1, 2, 3, 4, 5, 6].map((lv) => ({
|
||||||
label: `${cur} ${lv}`,
|
label: `${cur} ${lv}`,
|
||||||
value: `${cur.toLowerCase()}${lv}`
|
value: `${cur.toLowerCase()}${lv}`
|
||||||
})))
|
})))
|
||||||
|
const allowedPriceGroupValues = ref([])
|
||||||
|
const priceGroupRestricted = ref(false)
|
||||||
|
const allowedPriceOptions = computed(() => {
|
||||||
|
if (!priceGroupRestricted.value) return allPriceOptions
|
||||||
|
const allowed = new Set(allowedPriceGroupValues.value || [])
|
||||||
|
return allPriceOptions.filter((x) => allowed.has(x.value))
|
||||||
|
})
|
||||||
|
const priceOptions = allPriceOptions
|
||||||
const campaignPairs = priceOptions.map((x) => ({ base: x.value, derived: `${x.value}Campaign` }))
|
const campaignPairs = priceOptions.map((x) => ({ base: x.value, derived: `${x.value}Campaign` }))
|
||||||
const priceColumnNames = campaignPairs.flatMap((p) => [p.base, p.derived])
|
const priceColumnNames = campaignPairs.flatMap((p) => [p.base, p.derived])
|
||||||
|
|
||||||
@@ -401,9 +690,14 @@ const topUrunIlkGrubu = ref(null)
|
|||||||
const topUrunAnaGrubu = ref(null)
|
const topUrunAnaGrubu = ref(null)
|
||||||
const selectedProductCodes = ref([])
|
const selectedProductCodes = ref([])
|
||||||
const selectedCampaignLabels = ref([])
|
const selectedCampaignLabels = ref([])
|
||||||
|
const selectedVariantCodes = ref([])
|
||||||
const campaignFilterSearch = ref('')
|
const campaignFilterSearch = ref('')
|
||||||
const selectedPriceOptions = ref(priceOptions.map((x) => x.value))
|
const variantFilterSearch = ref('')
|
||||||
|
const columnFilters = ref({})
|
||||||
|
const columnFilterSearch = ref({})
|
||||||
|
const selectedPriceOptions = ref(['usd5', 'try5'])
|
||||||
const leftDetailsExpanded = ref(true)
|
const leftDetailsExpanded = ref(true)
|
||||||
|
const showInStockOnly = ref(false)
|
||||||
|
|
||||||
const rows = ref([])
|
const rows = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -418,13 +712,39 @@ const serverFilterLoading = ref({})
|
|||||||
const serverFilterLastQuery = ref({})
|
const serverFilterLastQuery = ref({})
|
||||||
const filterSearch = ref({ productCode: '', urunIlkGrubu: '', urunAnaGrubu: '' })
|
const filterSearch = ref({ productCode: '', urunIlkGrubu: '', urunAnaGrubu: '' })
|
||||||
const imageCache = new Map()
|
const imageCache = new Map()
|
||||||
|
const imageListCache = new Map()
|
||||||
|
const variantCodeCollator = new Intl.Collator('tr', { numeric: true, sensitivity: 'base' })
|
||||||
const mainTableRef = ref(null)
|
const mainTableRef = ref(null)
|
||||||
const topScrollRef = ref(null)
|
const topScrollRef = ref(null)
|
||||||
let syncingScroll = false
|
let syncingScroll = false
|
||||||
|
|
||||||
|
const productCardDialog = ref(false)
|
||||||
|
const productCardData = ref({})
|
||||||
|
const productCardImages = ref([])
|
||||||
|
const productCardSlide = ref(0)
|
||||||
|
const productCardStockLoading = ref(false)
|
||||||
|
const productCardSizeRows = ref([])
|
||||||
|
const productImageFullscreenDialog = ref(false)
|
||||||
|
const productImageFullscreenSlide = ref(0)
|
||||||
|
const fullscreenImages = computed(() => productCardImages.value || [])
|
||||||
|
|
||||||
const selectedPriceSet = computed(() => new Set(selectedPriceOptions.value || []))
|
const selectedPriceSet = computed(() => new Set(selectedPriceOptions.value || []))
|
||||||
|
const productCardPriceRows = computed(() => {
|
||||||
|
const row = productCardData.value || {}
|
||||||
|
return priceOptions
|
||||||
|
.filter((option) => selectedPriceSet.value.has(option.value))
|
||||||
|
.map((option) => ({
|
||||||
|
key: option.value,
|
||||||
|
label: option.label,
|
||||||
|
currency: String(option.value || '').slice(0, 3).toLowerCase(),
|
||||||
|
price: formatPrice(row?.[option.value]),
|
||||||
|
campaignPrice: formatPrice(row?.[`${option.value}Campaign`]),
|
||||||
|
hasCampaignPrice: Number(row?.[`${option.value}Campaign`] || 0) > 0
|
||||||
|
}))
|
||||||
|
})
|
||||||
const selectedProductCodeSet = computed(() => new Set(selectedProductCodes.value || []))
|
const selectedProductCodeSet = computed(() => new Set(selectedProductCodes.value || []))
|
||||||
const selectedCampaignLabelSet = computed(() => new Set(selectedCampaignLabels.value || []))
|
const selectedCampaignLabelSet = computed(() => new Set(selectedCampaignLabels.value || []))
|
||||||
|
const selectedVariantCodeSet = computed(() => new Set(selectedVariantCodes.value || []))
|
||||||
const pageBusy = computed(() => loading.value || renderPending.value)
|
const pageBusy = computed(() => loading.value || renderPending.value)
|
||||||
const canFetch = computed(() => Boolean(topUrunIlkGrubu.value || topUrunAnaGrubu.value || selectedProductCodes.value.length > 0))
|
const canFetch = computed(() => Boolean(topUrunIlkGrubu.value || topUrunAnaGrubu.value || selectedProductCodes.value.length > 0))
|
||||||
const showGuidanceOverlay = computed(() => !loading.value && rows.value.length === 0 && error.value === GUIDANCE_MSG)
|
const showGuidanceOverlay = computed(() => !loading.value && rows.value.length === 0 && error.value === GUIDANCE_MSG)
|
||||||
@@ -447,6 +767,21 @@ const filteredCampaignOptions = computed(() => {
|
|||||||
const list = campaignOptions.value
|
const list = campaignOptions.value
|
||||||
return q ? list.filter((x) => x.label.toLocaleLowerCase('tr').includes(q)) : list
|
return q ? list.filter((x) => x.label.toLocaleLowerCase('tr').includes(q)) : list
|
||||||
})
|
})
|
||||||
|
const variantOptions = computed(() => {
|
||||||
|
const uniq = new Set()
|
||||||
|
for (const row of rows.value || []) {
|
||||||
|
const val = toText(row?.variantCodes)
|
||||||
|
if (val) uniq.add(val)
|
||||||
|
}
|
||||||
|
return Array.from(uniq)
|
||||||
|
.sort((a, b) => variantCodeCollator.compare(a, b))
|
||||||
|
.map((value) => ({ label: value, value }))
|
||||||
|
})
|
||||||
|
const filteredVariantOptions = computed(() => {
|
||||||
|
const q = toText(variantFilterSearch.value).toLocaleLowerCase('tr')
|
||||||
|
const list = variantOptions.value
|
||||||
|
return q ? list.filter((x) => x.label.toLocaleLowerCase('tr').includes(q)) : list
|
||||||
|
})
|
||||||
|
|
||||||
function toText (value) {
|
function toText (value) {
|
||||||
return String(value ?? '').trim()
|
return String(value ?? '').trim()
|
||||||
@@ -474,6 +809,59 @@ function formatStock (value) {
|
|||||||
return n.toLocaleString('tr-TR', { maximumFractionDigits: 2 })
|
return n.toLocaleString('tr-TR', { maximumFractionDigits: 2 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseStockNumber (value) {
|
||||||
|
if (typeof value === 'number') return Number.isFinite(value) ? value : 0
|
||||||
|
const text = String(value ?? '').trim()
|
||||||
|
if (!text) return 0
|
||||||
|
const normalized = text.replace(/\./g, '').replace(',', '.')
|
||||||
|
const n = Number.parseFloat(normalized)
|
||||||
|
return Number.isFinite(n) ? n : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCardToken (value) {
|
||||||
|
return String(value ?? '').trim().toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVariantTokens (variantCode) {
|
||||||
|
const parts = String(variantCode || '').split('-').map((x) => normalizeCardToken(x)).filter(Boolean)
|
||||||
|
return {
|
||||||
|
color: parts[0] || '',
|
||||||
|
dim2: parts.length > 1 ? parts.slice(1).join('-') : ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stockRowText (row, ...keys) {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = String(row?.[key] ?? '').trim()
|
||||||
|
if (value) return value
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesProductCardVariant (stockRow, cardRow) {
|
||||||
|
const tokens = parseVariantTokens(cardRow?.variantCodes)
|
||||||
|
if (!tokens.color && !tokens.dim2) return true
|
||||||
|
const color = normalizeCardToken(stockRowText(stockRow, 'Renk_Kodu', 'ColorCode', 'colorCode'))
|
||||||
|
const dim2 = normalizeCardToken(stockRowText(stockRow, 'Yaka', 'ItemDim2Code', 'itemDim2Code', 'Renk2'))
|
||||||
|
if (tokens.color && color !== tokens.color) return false
|
||||||
|
if (tokens.dim2 && dim2 !== tokens.dim2) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSizeStockRows (stockRows, cardRow) {
|
||||||
|
const totals = new Map()
|
||||||
|
for (const item of stockRows || []) {
|
||||||
|
if (!matchesProductCardVariant(item, cardRow)) continue
|
||||||
|
const size = stockRowText(item, 'Beden', 'Size', 'ItemDim1Code', 'itemDim1Code')
|
||||||
|
if (!size) continue
|
||||||
|
const qty = parseStockNumber(item?.Kullanilabilir_Envanter ?? item?.StockQty ?? item?.qty)
|
||||||
|
totals.set(size, (totals.get(size) || 0) + qty)
|
||||||
|
}
|
||||||
|
return Array.from(totals.entries())
|
||||||
|
.map(([size, qty]) => ({ size, qty }))
|
||||||
|
.sort((a, b) => variantCodeCollator.compare(a.size, b.size))
|
||||||
|
}
|
||||||
|
|
||||||
function mapProductRow (raw, index) {
|
function mapProductRow (raw, index) {
|
||||||
const row = {
|
const row = {
|
||||||
id: index + 1,
|
id: index + 1,
|
||||||
@@ -537,7 +925,7 @@ function buildRows (products, variants) {
|
|||||||
out.push(row)
|
out.push(row)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
list.sort((a, b) => toText(a?.variant_code).localeCompare(toText(b?.variant_code), 'tr'))
|
list.sort((a, b) => variantCodeCollator.compare(toText(a?.variant_code), toText(b?.variant_code)))
|
||||||
for (const v of list) {
|
for (const v of list) {
|
||||||
const d1 = Number(v?.dim1 || 0)
|
const d1 = Number(v?.dim1 || 0)
|
||||||
const d3 = v?.dim3 == null ? 0 : Number(v?.dim3 || 0)
|
const d3 = v?.dim3 == null ? 0 : Number(v?.dim3 || 0)
|
||||||
@@ -585,6 +973,20 @@ async function fetchServerFilterOptions (field, q = '') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchMyPriceGroups () {
|
||||||
|
try {
|
||||||
|
const res = await api.get('/order/price-list/my-price-groups')
|
||||||
|
priceGroupRestricted.value = !!res?.data?.restricted
|
||||||
|
allowedPriceGroupValues.value = Array.isArray(res?.data?.price_groups) ? res.data.price_groups : []
|
||||||
|
normalizeSelectedPriceOptions()
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[order-price-list][ui] price-groups lookup failed', err?.response?.data || err?.message || err)
|
||||||
|
priceGroupRestricted.value = false
|
||||||
|
allowedPriceGroupValues.value = []
|
||||||
|
normalizeSelectedPriceOptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onTopFilterSearchUrunIlkGrubu (val, update) {
|
function onTopFilterSearchUrunIlkGrubu (val, update) {
|
||||||
update(() => {
|
update(() => {
|
||||||
filterSearch.value.urunIlkGrubu = toText(val)
|
filterSearch.value.urunIlkGrubu = toText(val)
|
||||||
@@ -651,6 +1053,79 @@ function clearCampaignOptions () {
|
|||||||
selectedCampaignLabels.value = []
|
selectedCampaignLabels.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleVariantValue (value) {
|
||||||
|
const v = toText(value)
|
||||||
|
if (!v) return
|
||||||
|
const set = new Set(selectedVariantCodes.value || [])
|
||||||
|
if (set.has(v)) set.delete(v)
|
||||||
|
else set.add(v)
|
||||||
|
selectedVariantCodes.value = Array.from(set).sort((a, b) => variantCodeCollator.compare(a, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllVariantOptions () {
|
||||||
|
const set = new Set(selectedVariantCodes.value || [])
|
||||||
|
for (const option of filteredVariantOptions.value) {
|
||||||
|
const v = toText(option.value)
|
||||||
|
if (v) set.add(v)
|
||||||
|
}
|
||||||
|
selectedVariantCodes.value = Array.from(set).sort((a, b) => variantCodeCollator.compare(a, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearVariantOptions () {
|
||||||
|
selectedVariantCodes.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLocalFilterableColumn (name) {
|
||||||
|
if (!name || ['image', 'productCode', 'variantCodes', 'campaignLabel'].includes(name)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumnFilterValues (name) {
|
||||||
|
const list = columnFilters.value?.[name]
|
||||||
|
return Array.isArray(list) ? list : []
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumnFilterSet (name) {
|
||||||
|
return new Set(getColumnFilterValues(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
function setColumnFilterSearch (name, value) {
|
||||||
|
columnFilterSearch.value = { ...columnFilterSearch.value, [name]: toText(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumnOptions (col) {
|
||||||
|
const uniq = new Set()
|
||||||
|
for (const row of rows.value || []) {
|
||||||
|
uniq.add(exportCell(row, col))
|
||||||
|
}
|
||||||
|
return Array.from(uniq)
|
||||||
|
.sort((a, b) => String(a).localeCompare(String(b), 'tr', { numeric: true, sensitivity: 'base' }))
|
||||||
|
.map((value) => ({ label: value || '-', value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilteredColumnOptions (col) {
|
||||||
|
const q = toText(columnFilterSearch.value?.[col.name]).toLocaleLowerCase('tr')
|
||||||
|
const list = getColumnOptions(col)
|
||||||
|
return q ? list.filter((x) => String(x.label || '').toLocaleLowerCase('tr').includes(q)) : list
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleColumnFilterValue (name, value) {
|
||||||
|
const set = new Set(getColumnFilterValues(name))
|
||||||
|
if (set.has(value)) set.delete(value)
|
||||||
|
else set.add(value)
|
||||||
|
columnFilters.value = { ...columnFilters.value, [name]: Array.from(set) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllColumnOptions (col) {
|
||||||
|
const set = new Set(getColumnFilterValues(col.name))
|
||||||
|
for (const option of getFilteredColumnOptions(col)) set.add(option.value)
|
||||||
|
columnFilters.value = { ...columnFilters.value, [col.name]: Array.from(set) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearColumnOptions (name) {
|
||||||
|
columnFilters.value = { ...columnFilters.value, [name]: [] }
|
||||||
|
}
|
||||||
|
|
||||||
function onTopUrunIlkGrubuChange () {
|
function onTopUrunIlkGrubuChange () {
|
||||||
topUrunAnaGrubu.value = null
|
topUrunAnaGrubu.value = null
|
||||||
void fetchServerFilterOptions('urunAnaGrubu', '')
|
void fetchServerFilterOptions('urunAnaGrubu', '')
|
||||||
@@ -703,7 +1178,7 @@ async function reloadData ({ page = 1 } = {}) {
|
|||||||
}
|
}
|
||||||
rows.value = buildRows(products, variants)
|
rows.value = buildRows(products, variants)
|
||||||
error.value = ''
|
error.value = ''
|
||||||
void loadImagesForRows(rows.value.slice(0, 120))
|
void loadImagesForRows(rows.value)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
rows.value = []
|
rows.value = []
|
||||||
@@ -724,10 +1199,18 @@ async function loadImagesForRows (list) {
|
|||||||
seen.add(key)
|
seen.add(key)
|
||||||
targets.push({ row, key })
|
targets.push({ row, key })
|
||||||
}
|
}
|
||||||
await Promise.all(targets.map(async ({ row, key }) => {
|
const concurrency = 12
|
||||||
|
let cursor = 0
|
||||||
|
let loaded = 0
|
||||||
|
const workers = Array.from({ length: Math.min(concurrency, targets.length) }, async () => {
|
||||||
|
for (;;) {
|
||||||
|
const target = targets[cursor]
|
||||||
|
cursor += 1
|
||||||
|
if (!target) return
|
||||||
|
const { row, key } = target
|
||||||
if (imageCache.has(key)) {
|
if (imageCache.has(key)) {
|
||||||
row.imageUrl = imageCache.get(key)
|
row.imageUrl = imageCache.get(key)
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await api.get('/product-images', {
|
const res = await api.get('/product-images', {
|
||||||
@@ -739,19 +1222,109 @@ async function loadImagesForRows (list) {
|
|||||||
timeout: 15000
|
timeout: 15000
|
||||||
})
|
})
|
||||||
const first = Array.isArray(res?.data) ? res.data[0] : null
|
const first = Array.isArray(res?.data) ? res.data[0] : null
|
||||||
const url = toText(first?.thumb_url || first?.content_url || first?.full_url)
|
const url = resolveProductImageUrl(first)
|
||||||
imageCache.set(key, url)
|
imageCache.set(key, url)
|
||||||
row.imageUrl = url
|
row.imageUrl = url
|
||||||
|
imageListCache.set(key, Array.isArray(res?.data) ? res.data : [])
|
||||||
} catch {
|
} catch {
|
||||||
imageCache.set(key, '')
|
imageCache.set(key, '')
|
||||||
}
|
}
|
||||||
}))
|
loaded += 1
|
||||||
|
if (loaded % 12 === 0) rows.value = [...rows.value]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await Promise.all(workers)
|
||||||
|
rows.value = [...rows.value]
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeUploadsPath (storagePath) {
|
||||||
|
const raw = toText(storagePath)
|
||||||
|
if (!raw) return ''
|
||||||
|
const normalized = raw.replace(/\\/g, '/')
|
||||||
|
const idx = normalized.toLowerCase().indexOf('/uploads/')
|
||||||
|
if (idx >= 0) return normalized.slice(idx)
|
||||||
|
if (normalized.toLowerCase().startsWith('uploads/')) return `/${normalized}`
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveProductImageUrl (item) {
|
||||||
|
if (!item || typeof item !== 'object') return ''
|
||||||
|
const imageId = Number(item.id || item.ID || 0)
|
||||||
|
if (Number.isFinite(imageId) && imageId > 0) return `/api/product-images/${imageId}/content`
|
||||||
|
const thumbUrl = toText(item.thumb_url || item.thumbUrl)
|
||||||
|
if (thumbUrl) return thumbUrl
|
||||||
|
const fullUrl = toText(item.full_url || item.fullUrl)
|
||||||
|
if (fullUrl) return fullUrl
|
||||||
|
const contentUrl = toText(item.content_url || item.ContentURL)
|
||||||
|
if (contentUrl) return contentUrl.startsWith('/api/') ? contentUrl : contentUrl
|
||||||
|
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage)
|
||||||
|
if (uploadsPath) return uploadsPath
|
||||||
|
const fileName = toText(item.file_name || item.FileName)
|
||||||
|
return fileName ? `/uploads/image/${fileName}` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchImageListForRow (row) {
|
||||||
|
const key = `${row.productCode}|${row.dim1 || 0}|${row.dim3 || 0}`
|
||||||
|
if (imageListCache.has(key)) return imageListCache.get(key) || []
|
||||||
|
const res = await api.get('/product-images', {
|
||||||
|
params: {
|
||||||
|
code: row.productCode,
|
||||||
|
dim1_id: row.dim1 || '',
|
||||||
|
dim3_id: row.dim3 || ''
|
||||||
|
},
|
||||||
|
timeout: 15000
|
||||||
|
})
|
||||||
|
const list = Array.isArray(res?.data) ? res.data : []
|
||||||
|
imageListCache.set(key, list)
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openProductCard (row) {
|
||||||
|
if (!row) return
|
||||||
|
productCardData.value = { ...row }
|
||||||
|
productCardSizeRows.value = []
|
||||||
|
productCardDialog.value = true
|
||||||
|
productCardSlide.value = 0
|
||||||
|
productCardStockLoading.value = true
|
||||||
|
try {
|
||||||
|
const [list, stockRes] = await Promise.all([
|
||||||
|
fetchImageListForRow(row),
|
||||||
|
api.get('/product-stock-query', { params: { code: row.productCode }, timeout: 30000 })
|
||||||
|
])
|
||||||
|
const images = list.map(resolveProductImageUrl).filter(Boolean)
|
||||||
|
if (row.imageUrl && !images.includes(row.imageUrl)) images.unshift(row.imageUrl)
|
||||||
|
productCardImages.value = Array.from(new Set(images))
|
||||||
|
productCardSizeRows.value = buildSizeStockRows(Array.isArray(stockRes?.data) ? stockRes.data : [], row)
|
||||||
|
} catch {
|
||||||
|
productCardImages.value = row.imageUrl ? [row.imageUrl] : []
|
||||||
|
productCardSizeRows.value = []
|
||||||
|
} finally {
|
||||||
|
productCardStockLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openProductImageFullscreen (src) {
|
||||||
|
const value = toText(src)
|
||||||
|
if (!value) return
|
||||||
|
const idx = Math.max(0, fullscreenImages.value.findIndex((x) => toText(x) === value))
|
||||||
|
productImageFullscreenSlide.value = idx
|
||||||
|
productImageFullscreenDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onProductCardDialogHide () {
|
||||||
|
productImageFullscreenDialog.value = false
|
||||||
|
productCardStockLoading.value = false
|
||||||
|
productCardSizeRows.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetSelections () {
|
function resetSelections () {
|
||||||
topUrunIlkGrubu.value = null
|
topUrunIlkGrubu.value = null
|
||||||
topUrunAnaGrubu.value = null
|
topUrunAnaGrubu.value = null
|
||||||
selectedProductCodes.value = []
|
selectedProductCodes.value = []
|
||||||
|
selectedCampaignLabels.value = []
|
||||||
|
selectedVariantCodes.value = []
|
||||||
|
columnFilters.value = {}
|
||||||
|
columnFilterSearch.value = {}
|
||||||
rows.value = []
|
rows.value = []
|
||||||
error.value = GUIDANCE_MSG
|
error.value = GUIDANCE_MSG
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
@@ -764,20 +1337,33 @@ function onPageChange (page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function togglePriceOption (value) {
|
function togglePriceOption (value) {
|
||||||
|
if (!allowedPriceOptions.value.some((x) => x.value === value)) return
|
||||||
const set = new Set(selectedPriceOptions.value || [])
|
const set = new Set(selectedPriceOptions.value || [])
|
||||||
if (set.has(value)) set.delete(value)
|
if (set.has(value)) set.delete(value)
|
||||||
else set.add(value)
|
else set.add(value)
|
||||||
selectedPriceOptions.value = priceOptions.map((x) => x.value).filter((x) => set.has(x))
|
selectedPriceOptions.value = allowedPriceOptions.value.map((x) => x.value).filter((x) => set.has(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectAllPrices () {
|
function selectAllPrices () {
|
||||||
selectedPriceOptions.value = priceOptions.map((x) => x.value)
|
selectedPriceOptions.value = allowedPriceOptions.value.map((x) => x.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAllPrices () {
|
function clearAllPrices () {
|
||||||
selectedPriceOptions.value = []
|
selectedPriceOptions.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSelectedPriceOptions () {
|
||||||
|
const allowedValues = allowedPriceOptions.value.map((x) => x.value)
|
||||||
|
const allowed = new Set(allowedValues)
|
||||||
|
const current = (selectedPriceOptions.value || []).filter((x) => allowed.has(x))
|
||||||
|
if (current.length > 0 || allowedValues.length === 0) {
|
||||||
|
selectedPriceOptions.value = current
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const preferred = ['usd5', 'try5'].filter((x) => allowed.has(x))
|
||||||
|
selectedPriceOptions.value = preferred.length ? preferred : allowedValues.slice(0, 2)
|
||||||
|
}
|
||||||
|
|
||||||
function col (name, label, field, width, extra = {}) {
|
function col (name, label, field, width, extra = {}) {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
@@ -793,34 +1379,31 @@ function col (name, label, field, width, extra = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allColumns = [
|
const allColumns = [
|
||||||
col('image', '', 'imageUrl', 58, { align: 'center', classes: 'image-col sticky-col' }),
|
col('image', '', 'imageUrl', 108, { align: 'center', classes: 'image-col sticky-col' }),
|
||||||
col('brandGroupSelection', 'MARKA GRUBU', 'brandGroupSelection', 86, { classes: 'ps-col sticky-col' }),
|
col('brandGroupSelection', 'MARKA GRUBU', 'brandGroupSelection', 86, { classes: 'ps-col sticky-col' }),
|
||||||
col('marka', 'MARKA', 'marka', 62, { sortable: true, classes: 'ps-col sticky-col' }),
|
col('marka', 'MARKA', 'marka', 72, { sortable: true, classes: 'ps-col sticky-col' }),
|
||||||
col('productCode', 'URUN KODU', 'productCode', 112, { sortable: true, classes: 'ps-col product-code-col sticky-col' }),
|
col('productCode', 'URUN KODU', 'productCode', 112, { sortable: true, classes: 'ps-col product-code-col sticky-col' }),
|
||||||
col('variantCodes', 'VARYANT', 'variantCodes', 112, { classes: 'ps-col variant-col sticky-col' }),
|
col('variantCodes', 'VARYANT', 'variantCodes', 82, { align: 'center', classes: 'ps-col variant-col sticky-col center-col' }),
|
||||||
col('variantStocks', 'STOK', 'stockQty', 62, { align: 'right', sortable: true, classes: 'ps-col variant-stock-col sticky-col' }),
|
col('variantStocks', 'STOK', 'stockQty', 64, { align: 'center', sortable: true, classes: 'ps-col variant-stock-col sticky-col center-col' }),
|
||||||
col('campaignLabel', 'KAMPANYA', 'campaignLabel', 150, { classes: 'ps-col campaign-col sticky-col' }),
|
col('campaignLabel', 'KAMPANYA', 'campaignLabel', 118, { align: 'center', classes: 'ps-col campaign-col sticky-col center-col' }),
|
||||||
col('campaignRate', 'IND %', 'campaignRate', 58, { align: 'right', classes: 'ps-col campaign-rate-col sticky-col' }),
|
col('campaignRate', 'IND %', 'campaignRate', 56, { align: 'center', classes: 'ps-col campaign-rate-col sticky-col center-col' }),
|
||||||
col('stockEntryDate', 'STOK GIRIS', 'stockEntryDate', 86, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
|
col('askiliYan', 'ASKILI YAN', 'askiliYan', 58, { sortable: true, classes: 'ps-col center-col' }),
|
||||||
col('lastPricingDate', 'SON FIYAT', 'lastPricingDate', 86, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
|
col('kategori', 'KATEGORI', 'kategori', 72, { sortable: true, classes: 'ps-col center-col' }),
|
||||||
col('lastCampaignDate', 'SON KAMPANYA', 'lastCampaignDate', 98, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
|
col('urunIlkGrubu', 'URUN ILK GRUBU', 'urunIlkGrubu', 72, { sortable: true, classes: 'ps-col center-col' }),
|
||||||
col('askiliYan', 'ASKILI YAN', 'askiliYan', 58, { sortable: true, classes: 'ps-col' }),
|
col('urunAnaGrubu', 'URUN ANA GRUBU', 'urunAnaGrubu', 84, { sortable: true, classes: 'ps-col center-col' }),
|
||||||
col('kategori', 'KATEGORI', 'kategori', 58, { sortable: true, classes: 'ps-col' }),
|
col('urunAltGrubu', 'URUN ALT GRUBU', 'urunAltGrubu', 84, { sortable: true, classes: 'ps-col center-col' }),
|
||||||
col('urunIlkGrubu', 'URUN ILK GRUBU', 'urunIlkGrubu', 70, { sortable: true, classes: 'ps-col' }),
|
col('icerik', 'ICERIK', 'icerik', 92, { sortable: true, classes: 'ps-col' }),
|
||||||
col('urunAnaGrubu', 'URUN ANA GRUBU', 'urunAnaGrubu', 74, { sortable: true, classes: 'ps-col' }),
|
col('karisim', 'KARISIM', 'karisim', 88, { sortable: true, classes: 'ps-col karisim-wrap-col' }),
|
||||||
col('urunAltGrubu', 'URUN ALT GRUBU', 'urunAltGrubu', 74, { sortable: true, classes: 'ps-col' }),
|
|
||||||
col('icerik', 'ICERIK', 'icerik', 66, { sortable: true, classes: 'ps-col' }),
|
|
||||||
col('karisim', 'KARISIM', 'karisim', 66, { sortable: true, classes: 'ps-col' }),
|
|
||||||
...campaignPairs.flatMap((p) => [
|
...campaignPairs.flatMap((p) => [
|
||||||
col(p.base, p.base.toUpperCase().replace(/([A-Z]+)(\d)/, '$1 $2'), p.base, 78, { align: 'right', classes: `${p.base.slice(0, 3)}-col` }),
|
col(p.base, p.base.toUpperCase().replace(/([A-Z]+)(\d)/, '$1 $2'), p.base, 78, { align: 'right', classes: `${p.base.slice(0, 3)}-col` }),
|
||||||
col(p.derived, `${p.base.toUpperCase().replace(/([A-Z]+)(\d)/, '$1 $2')} KMP`, p.derived, 88, { align: 'right', classes: `${p.base.slice(0, 3)}-col campaign-price-col` })
|
col(p.derived, `${p.base.toUpperCase().replace(/([A-Z]+)(\d)/, '$1 $2')} KMP`, p.derived, 88, { align: 'right', classes: `${p.base.slice(0, 3)}-col campaign-price-col` })
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
|
|
||||||
const hideableLeftDetailColumnNames = new Set([
|
const compactHiddenColumnNames = new Set([
|
||||||
'stockEntryDate',
|
'variantStocks',
|
||||||
'lastPricingDate',
|
'campaignLabel',
|
||||||
'lastCampaignDate',
|
'campaignRate',
|
||||||
'askiliYan',
|
'askiliYan',
|
||||||
'kategori',
|
'kategori',
|
||||||
'urunIlkGrubu',
|
'urunIlkGrubu',
|
||||||
@@ -833,16 +1416,37 @@ const hideableLeftDetailColumnNames = new Set([
|
|||||||
const visibleColumns = computed(() => allColumns.filter((c) => {
|
const visibleColumns = computed(() => allColumns.filter((c) => {
|
||||||
if (/^(usd|eur|try)[1-6]$/.test(c.name)) return selectedPriceSet.value.has(c.name)
|
if (/^(usd|eur|try)[1-6]$/.test(c.name)) return selectedPriceSet.value.has(c.name)
|
||||||
if (/^(usd|eur|try)[1-6]Campaign$/.test(c.name)) return selectedPriceSet.value.has(c.name.replace(/Campaign$/, ''))
|
if (/^(usd|eur|try)[1-6]Campaign$/.test(c.name)) return selectedPriceSet.value.has(c.name.replace(/Campaign$/, ''))
|
||||||
if (!leftDetailsExpanded.value && hideableLeftDetailColumnNames.has(c.name)) return false
|
if (!leftDetailsExpanded.value && compactHiddenColumnNames.has(c.name)) return false
|
||||||
return true
|
return true
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const filteredRows = computed(() => {
|
const filteredRows = computed(() => {
|
||||||
const campaignSet = selectedCampaignLabelSet.value
|
const campaignSet = selectedCampaignLabelSet.value
|
||||||
if (campaignSet.size === 0) return rows.value || []
|
const variantSet = selectedVariantCodeSet.value
|
||||||
return (rows.value || []).filter((row) => campaignSet.has(toText(row?.campaignLabel)))
|
const localFilters = columnFilters.value || {}
|
||||||
|
let list = rows.value || []
|
||||||
|
if (showInStockOnly.value) {
|
||||||
|
list = list.filter((row) => Number(row?.stockQty ?? 0) > 0)
|
||||||
|
}
|
||||||
|
if (campaignSet.size > 0) {
|
||||||
|
list = list.filter((row) => campaignSet.has(toText(row?.campaignLabel)))
|
||||||
|
}
|
||||||
|
if (variantSet.size > 0) {
|
||||||
|
list = list.filter((row) => variantSet.has(toText(row?.variantCodes)))
|
||||||
|
}
|
||||||
|
const active = Object.entries(localFilters).filter(([, values]) => Array.isArray(values) && values.length > 0)
|
||||||
|
if (active.length > 0) {
|
||||||
|
const byName = new Map(allColumns.map((c) => [c.name, c]))
|
||||||
|
list = list.filter((row) => active.every(([name, values]) => {
|
||||||
|
const col = byName.get(name)
|
||||||
|
if (!col) return true
|
||||||
|
return new Set(values).has(exportCell(row, col))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return list
|
||||||
})
|
})
|
||||||
const tableMinWidth = computed(() => visibleColumns.value.reduce((sum, c) => sum + extractWidth(c.style), 0))
|
const tableMinWidth = computed(() => visibleColumns.value.reduce((sum, c) => sum + extractWidth(c.style), 0))
|
||||||
|
const tableScrollWidth = computed(() => tableMinWidth.value + stickyScrollComp.value + 48)
|
||||||
const tableStyle = computed(() => ({
|
const tableStyle = computed(() => ({
|
||||||
width: `${tableMinWidth.value}px`,
|
width: `${tableMinWidth.value}px`,
|
||||||
minWidth: `${tableMinWidth.value}px`,
|
minWidth: `${tableMinWidth.value}px`,
|
||||||
@@ -850,9 +1454,30 @@ const tableStyle = computed(() => ({
|
|||||||
}))
|
}))
|
||||||
const stickyColumnNames = computed(() => {
|
const stickyColumnNames = computed(() => {
|
||||||
const visible = new Set(visibleColumns.value.map((x) => x.name))
|
const visible = new Set(visibleColumns.value.map((x) => x.name))
|
||||||
return ['image', 'brandGroupSelection', 'marka', 'productCode', 'variantCodes', 'variantStocks', 'campaignLabel', 'campaignRate'].filter((x) => visible.has(x))
|
const expanded = [
|
||||||
|
'image',
|
||||||
|
'brandGroupSelection',
|
||||||
|
'marka',
|
||||||
|
'productCode',
|
||||||
|
'variantCodes',
|
||||||
|
'variantStocks',
|
||||||
|
'campaignLabel',
|
||||||
|
'campaignRate',
|
||||||
|
'askiliYan',
|
||||||
|
'kategori',
|
||||||
|
'urunIlkGrubu',
|
||||||
|
'urunAnaGrubu',
|
||||||
|
'urunAltGrubu',
|
||||||
|
'icerik',
|
||||||
|
'karisim'
|
||||||
|
]
|
||||||
|
const compact = ['image', 'brandGroupSelection', 'marka', 'productCode', 'variantCodes']
|
||||||
|
return (leftDetailsExpanded.value ? expanded : compact).filter((x) => visible.has(x))
|
||||||
|
})
|
||||||
|
const stickyBoundaryColumnName = computed(() => {
|
||||||
|
const list = stickyColumnNames.value
|
||||||
|
return list.length ? list[list.length - 1] : ''
|
||||||
})
|
})
|
||||||
const stickyBoundaryColumnName = 'campaignRate'
|
|
||||||
const stickyColumnNameSet = computed(() => new Set(stickyColumnNames.value))
|
const stickyColumnNameSet = computed(() => new Set(stickyColumnNames.value))
|
||||||
const stickyLeftMap = computed(() => {
|
const stickyLeftMap = computed(() => {
|
||||||
const map = {}
|
const map = {}
|
||||||
@@ -866,8 +1491,9 @@ const stickyLeftMap = computed(() => {
|
|||||||
return map
|
return map
|
||||||
})
|
})
|
||||||
const stickyScrollComp = computed(() => {
|
const stickyScrollComp = computed(() => {
|
||||||
const boundaryCol = allColumns.find((x) => x.name === stickyBoundaryColumnName)
|
const boundaryName = stickyBoundaryColumnName.value
|
||||||
return ((stickyLeftMap.value[stickyBoundaryColumnName] || 0) + extractWidth(boundaryCol?.style)) * 1.2
|
const boundaryCol = allColumns.find((x) => x.name === boundaryName)
|
||||||
|
return ((stickyLeftMap.value[boundaryName] || 0) + extractWidth(boundaryCol?.style)) * 1.2
|
||||||
})
|
})
|
||||||
|
|
||||||
function isStickyCol (name) {
|
function isStickyCol (name) {
|
||||||
@@ -875,7 +1501,7 @@ function isStickyCol (name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isStickyBoundary (name) {
|
function isStickyBoundary (name) {
|
||||||
return name === stickyBoundaryColumnName
|
return name === stickyBoundaryColumnName.value
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHeaderCellStyle (col) {
|
function getHeaderCellStyle (col) {
|
||||||
@@ -903,13 +1529,30 @@ function exportCell (row, col) {
|
|||||||
return toText(row[col.field])
|
return toText(row[col.field])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isExcelNumericColumn (col) {
|
||||||
|
return priceColumnNames.includes(col.name) || col.name === 'campaignRate'
|
||||||
|
}
|
||||||
|
|
||||||
|
function excelNumericCell (value) {
|
||||||
|
const n = Number(value)
|
||||||
|
if (!Number.isFinite(n) || n === 0) return '<td></td>'
|
||||||
|
const display = n.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||||
|
const raw = n.toFixed(2)
|
||||||
|
return `<td style="mso-number-format:'0.00';text-align:right;" x:num="${raw}">${escapeHtml(display)}</td>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportExcelCellHtml (row, col) {
|
||||||
|
if (priceColumnNames.includes(col.name)) return excelNumericCell(row[col.field])
|
||||||
|
if (col.name === 'campaignRate') return excelNumericCell(row.campaignRate)
|
||||||
|
return `<td>${escapeHtml(exportCell(row, col))}</td>`
|
||||||
|
}
|
||||||
|
|
||||||
function exportVisibleExcel () {
|
function exportVisibleExcel () {
|
||||||
const cols = visibleColumns.value
|
const cols = visibleColumns.value.filter((c) => c.name !== 'image')
|
||||||
const body = filteredRows.value.map((row) => `<tr>${cols.map((c) => {
|
const body = filteredRows.value.map((row) => `<tr>${cols.map((c) => {
|
||||||
if (c.name === 'image' && row.imageUrl) return `<td><img src="${row.imageUrl}" width="54" height="54"></td>`
|
return exportExcelCellHtml(row, c)
|
||||||
return `<td>${escapeHtml(exportCell(row, c))}</td>`
|
|
||||||
}).join('')}</tr>`).join('')
|
}).join('')}</tr>`).join('')
|
||||||
const html = `<!doctype html><html><head><meta charset="utf-8"></head><body><table border="1"><thead><tr>${cols.map((c) => `<th>${escapeHtml(c.label || 'Gorsel')}</th>`).join('')}</tr></thead><tbody>${body}</tbody></table></body></html>`
|
const html = `<!doctype html><html xmlns:x="urn:schemas-microsoft-com:office:excel"><head><meta charset="utf-8"></head><body><table border="1"><thead><tr>${cols.map((c) => `<th>${escapeHtml(c.label)}</th>`).join('')}</tr></thead><tbody>${body}</tbody></table></body></html>`
|
||||||
const blob = new Blob([html], { type: 'application/vnd.ms-excel;charset=utf-8' })
|
const blob = new Blob([html], { type: 'application/vnd.ms-excel;charset=utf-8' })
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a')
|
||||||
@@ -919,13 +1562,14 @@ function exportVisibleExcel () {
|
|||||||
a.click()
|
a.click()
|
||||||
a.remove()
|
a.remove()
|
||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
|
void notifyExportTaken('excel')
|
||||||
}
|
}
|
||||||
|
|
||||||
function printVisibleRows () {
|
function printVisibleRows () {
|
||||||
const cols = visibleColumns.value
|
const cols = visibleColumns.value
|
||||||
const body = filteredRows.value.map((row) => `<tr>${cols.map((c) => {
|
const body = filteredRows.value.map((row) => `<tr>${cols.map((c) => {
|
||||||
if (c.name === 'image' && row.imageUrl) return `<td><img src="${row.imageUrl}" class="thumb"></td>`
|
if (c.name === 'image' && row.imageUrl) return `<td><img src="${row.imageUrl}" class="thumb"></td>`
|
||||||
return `<td class="${priceColumnNames.includes(c.name) ? 'num' : ''}">${escapeHtml(exportCell(row, c))}</td>`
|
return `<td class="${isExcelNumericColumn(c) ? 'num' : ''}">${escapeHtml(exportCell(row, c))}</td>`
|
||||||
}).join('')}</tr>`).join('')
|
}).join('')}</tr>`).join('')
|
||||||
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Fiyat Listesi</title><style>
|
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Fiyat Listesi</title><style>
|
||||||
@page { size: A3 landscape; margin: 8mm; }
|
@page { size: A3 landscape; margin: 8mm; }
|
||||||
@@ -935,13 +1579,31 @@ function printVisibleRows () {
|
|||||||
th { background: #957116; color: #fff; }
|
th { background: #957116; color: #fff; }
|
||||||
th, td { border: 1px solid #ccc; padding: 3px; vertical-align: middle; }
|
th, td { border: 1px solid #ccc; padding: 3px; vertical-align: middle; }
|
||||||
.num { text-align: right; }
|
.num { text-align: right; }
|
||||||
.thumb { width: 42px; height: 42px; object-fit: cover; }
|
.thumb { width: 100px; height: 100px; object-fit: cover; }
|
||||||
</style></head><body><h1>Fiyat Listesi</h1><table><thead><tr>${cols.map((c) => `<th>${escapeHtml(c.label || 'Gorsel')}</th>`).join('')}</tr></thead><tbody>${body}</tbody></table><script>window.onload=function(){window.print()}<\/script></body></html>`
|
</style></head><body><h1>Fiyat Listesi</h1><table><thead><tr>${cols.map((c) => `<th>${escapeHtml(c.label || 'Gorsel')}</th>`).join('')}</tr></thead><tbody>${body}</tbody></table><script>window.onload=function(){window.print()}<\/script></body></html>`
|
||||||
const win = window.open('', '_blank')
|
const win = window.open('', '_blank')
|
||||||
if (!win) return
|
if (!win) return
|
||||||
win.document.open()
|
win.document.open()
|
||||||
win.document.write(html)
|
win.document.write(html)
|
||||||
win.document.close()
|
win.document.close()
|
||||||
|
void notifyExportTaken('pdf')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function notifyExportTaken (format) {
|
||||||
|
try {
|
||||||
|
await api.post('/order/price-list/export-notify', {
|
||||||
|
format,
|
||||||
|
row_count: filteredRows.value.length,
|
||||||
|
price_fields: [...selectedPriceOptions.value],
|
||||||
|
product_codes: [...selectedProductCodes.value],
|
||||||
|
campaign_labels: [...selectedCampaignLabels.value],
|
||||||
|
first_groups: Array.from(new Set(filteredRows.value.map((row) => toText(row.urunIlkGrubu)).filter(Boolean))).sort((a, b) => a.localeCompare(b, 'tr')),
|
||||||
|
urun_ilk_grubu: topUrunIlkGrubu.value || '',
|
||||||
|
urun_ana_grubu: topUrunAnaGrubu.value || ''
|
||||||
|
}, { timeout: 30000 })
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[order-price-list][ui] export notify failed', err?.response?.data || err?.message || err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTableMiddleEl () {
|
function getTableMiddleEl () {
|
||||||
@@ -992,7 +1654,12 @@ watch([tableMinWidth, rows], async () => {
|
|||||||
bindTableScrollSync()
|
bindTableScrollSync()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(allowedPriceOptions, () => {
|
||||||
|
normalizeSelectedPriceOptions()
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
void fetchMyPriceGroups()
|
||||||
void fetchServerFilterOptions('urunIlkGrubu', '')
|
void fetchServerFilterOptions('urunIlkGrubu', '')
|
||||||
void fetchServerFilterOptions('urunAnaGrubu', '')
|
void fetchServerFilterOptions('urunAnaGrubu', '')
|
||||||
void fetchServerFilterOptions('productCode', '')
|
void fetchServerFilterOptions('productCode', '')
|
||||||
@@ -1006,8 +1673,8 @@ onMounted(() => {
|
|||||||
height: calc(100vh - 58px);
|
height: calc(100vh - 58px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
--pricing-row-height: 31px;
|
--pricing-row-height: 108px;
|
||||||
--pricing-header-height: 72px;
|
--pricing-header-height: 88px;
|
||||||
--pricing-table-height: calc(100vh - 156px);
|
--pricing-table-height: calc(100vh - 156px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1015,12 +1682,59 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
align-items: flex-end;
|
align-items: flex-start;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-actions-row {
|
.top-actions-row {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: flex-end;
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 60;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
background: #fff;
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-actions-row--filters,
|
||||||
|
.top-actions-row--actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-group {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-group--paging {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-group :deep(.q-btn) {
|
||||||
|
min-height: 32px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-group :deep(.q-btn__content) {
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-group :deep(.q-btn__wrapper) {
|
||||||
|
padding: 4px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-wrap {
|
.table-wrap {
|
||||||
@@ -1032,30 +1746,58 @@ onMounted(() => {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
--top-scroll-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-thumb {
|
.product-thumb {
|
||||||
width: 46px;
|
width: 100px;
|
||||||
height: 46px;
|
height: 100px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #f4f4f4;
|
background: #f4f4f4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-thumb-placeholder {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f4f4f4;
|
||||||
|
border: 1px dashed rgba(0, 0, 0, 0.16);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.image-cell {
|
.image-cell {
|
||||||
padding: 2px 4px !important;
|
padding: 4px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-x-scroll {
|
.top-x-scroll {
|
||||||
flex: 0 0 14px;
|
flex: 0 0 var(--top-scroll-height);
|
||||||
height: 14px;
|
height: var(--top-scroll-height);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
background: #fff;
|
background: #f8fafc;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.14);
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-x-scroll-inner {
|
.top-x-scroll-inner {
|
||||||
height: 1px;
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-x-scroll::-webkit-scrollbar {
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-x-scroll::-webkit-scrollbar-track {
|
||||||
|
background: #edf1f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-x-scroll::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(25, 118, 210, 0.42);
|
||||||
|
border: 3px solid #edf1f5;
|
||||||
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane-table {
|
.pane-table {
|
||||||
@@ -1064,9 +1806,9 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pricing-table :deep(.q-table__middle) {
|
.pricing-table :deep(.q-table__middle) {
|
||||||
height: calc(var(--pricing-table-height) - 14px);
|
height: calc(var(--pricing-table-height) - var(--top-scroll-height));
|
||||||
min-height: calc(var(--pricing-table-height) - 14px);
|
min-height: calc(var(--pricing-table-height) - var(--top-scroll-height));
|
||||||
max-height: calc(var(--pricing-table-height) - 14px);
|
max-height: calc(var(--pricing-table-height) - var(--top-scroll-height));
|
||||||
overflow: auto !important;
|
overflow: auto !important;
|
||||||
scrollbar-gutter: stable both-edges;
|
scrollbar-gutter: stable both-edges;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
@@ -1074,6 +1816,19 @@ onMounted(() => {
|
|||||||
|
|
||||||
.pricing-table :deep(.q-table) {
|
.pricing-table :deep(.q-table) {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
font-size: 11px;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
margin-right: var(--sticky-scroll-comp, 0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(.q-table__container) {
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
background: transparent !important;
|
||||||
|
height: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pricing-table :deep(td),
|
.pricing-table :deep(td),
|
||||||
@@ -1081,11 +1836,25 @@ onMounted(() => {
|
|||||||
height: var(--pricing-row-height) !important;
|
height: var(--pricing-row-height) !important;
|
||||||
min-height: var(--pricing-row-height) !important;
|
min-height: var(--pricing-row-height) !important;
|
||||||
max-height: var(--pricing-row-height) !important;
|
max-height: var(--pricing-row-height) !important;
|
||||||
line-height: var(--pricing-row-height);
|
line-height: 1.25;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(td > div),
|
||||||
|
.pricing-table :deep(td > .q-td) {
|
||||||
|
height: 100% !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(td) {
|
||||||
|
overflow: hidden !important;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle !important;
|
||||||
|
}
|
||||||
|
|
||||||
.pricing-table :deep(th),
|
.pricing-table :deep(th),
|
||||||
.pricing-table :deep(.q-table thead tr),
|
.pricing-table :deep(.q-table thead tr),
|
||||||
.pricing-table :deep(.q-table thead tr.header-row-fixed),
|
.pricing-table :deep(.q-table thead tr.header-row-fixed),
|
||||||
@@ -1099,9 +1868,10 @@ onMounted(() => {
|
|||||||
.pricing-table :deep(th) {
|
.pricing-table :deep(th) {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
white-space: nowrap;
|
white-space: normal;
|
||||||
word-break: normal;
|
word-break: break-word;
|
||||||
text-overflow: ellipsis;
|
overflow-wrap: anywhere;
|
||||||
|
overflow: hidden;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@@ -1156,8 +1926,90 @@ onMounted(() => {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-price-list-table :deep(.campaign-price-col) {
|
.pricing-table :deep(td.ps-col),
|
||||||
background: #f6fbf7;
|
.pricing-table :deep(td.usd-col),
|
||||||
|
.pricing-table :deep(td.eur-col),
|
||||||
|
.pricing-table :deep(td.try-col) {
|
||||||
|
vertical-align: middle !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th.usd-col),
|
||||||
|
.pricing-table :deep(td.usd-col) {
|
||||||
|
background: #fff;
|
||||||
|
color: #16803a;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th.eur-col),
|
||||||
|
.pricing-table :deep(td.eur-col) {
|
||||||
|
background: #fff;
|
||||||
|
color: #b91c1c;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th.try-col),
|
||||||
|
.pricing-table :deep(td.try-col) {
|
||||||
|
background: #fff;
|
||||||
|
color: #185abc;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th.usd-col),
|
||||||
|
.pricing-table :deep(th.eur-col),
|
||||||
|
.pricing-table :deep(th.try-col),
|
||||||
|
.pricing-table :deep(td.usd-col),
|
||||||
|
.pricing-table :deep(td.eur-col),
|
||||||
|
.pricing-table :deep(td.try-col) {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th.usd-col.campaign-price-col),
|
||||||
|
.pricing-table :deep(td.usd-col.campaign-price-col) {
|
||||||
|
background: #dff6e7;
|
||||||
|
color: #0f6b2f;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th.eur-col.campaign-price-col),
|
||||||
|
.pricing-table :deep(td.eur-col.campaign-price-col) {
|
||||||
|
background: #fde2e2;
|
||||||
|
color: #a61717;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(th.try-col.campaign-price-col),
|
||||||
|
.pricing-table :deep(td.try-col.campaign-price-col) {
|
||||||
|
background: #e2edff;
|
||||||
|
color: #174ea6;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(td.center-col) {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(td.karisim-wrap-col) {
|
||||||
|
white-space: normal !important;
|
||||||
|
text-overflow: clip !important;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
vertical-align: top !important;
|
||||||
|
text-align: left !important;
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 1.15;
|
||||||
|
padding: 6px 4px !important;
|
||||||
|
align-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-table :deep(td.karisim-wrap-col .q-td__content),
|
||||||
|
.pricing-table :deep(td.karisim-wrap-col > div) {
|
||||||
|
align-items: flex-start !important;
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
white-space: normal !important;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-with-filter {
|
.header-with-filter {
|
||||||
@@ -1175,15 +2027,66 @@ onMounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
line-height: 1.15;
|
line-height: 1.12;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 4;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cell-text {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 1.1;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-cell-text {
|
||||||
|
width: 100%;
|
||||||
|
text-align: right;
|
||||||
|
font-weight: 800;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.campaign-price-text {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.campaign-cell-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.campaign-text {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
color: #c62828;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.12;
|
||||||
|
text-align: center;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.campaign-rate-text {
|
||||||
|
width: 100%;
|
||||||
|
color: #c62828;
|
||||||
|
font-weight: 900;
|
||||||
|
text-align: center;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
.header-filter-btn {
|
.header-filter-btn {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
@@ -1259,4 +2162,325 @@ onMounted(() => {
|
|||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-card-dialog {
|
||||||
|
--pc-media-h: calc(100vh - 180px);
|
||||||
|
--pc-media-w: min(28vw, 440px);
|
||||||
|
background: #f9f8f5;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.product-card-dialog > .q-card__section:last-child) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(360px, 420px) minmax(360px, 440px) minmax(320px, 420px);
|
||||||
|
gap: 14px;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-images {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
height: var(--pc-media-h);
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-carousel {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
background: linear-gradient(180deg, #f6f1e6 0%, #efe6d3 100%);
|
||||||
|
border: 1px solid #e4dac7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-image-stage {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: linear-gradient(180deg, #f6f1e6 0%, #efe6d3 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-image-empty {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: var(--pc-media-h);
|
||||||
|
border: 1px dashed #c5b28d;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #faf6ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-fields {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
border: 1px solid #e4dac7;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #fdfaf4 100%);
|
||||||
|
padding: 12px;
|
||||||
|
height: var(--pc-media-h);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-price-panel {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: var(--pc-media-h);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 150px 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #efe5d5;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-row.field-row-head {
|
||||||
|
background: #f8f3e9;
|
||||||
|
border: 1px solid #e6dccb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-row .k {
|
||||||
|
color: #6b5a33;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-row .v {
|
||||||
|
color: #1f1f1f;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-section {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 12px;
|
||||||
|
border: 1px solid #e6dccb;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fffdf8;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-price-section {
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 0;
|
||||||
|
overflow: auto;
|
||||||
|
background: linear-gradient(180deg, #fffdf8 0%, #fff7ec 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-section-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #5e4a22;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-info-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 74px 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: #6b5a33;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-info-header span:first-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-info-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 74px 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid #f0e5d2;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-label {
|
||||||
|
font-weight: 800;
|
||||||
|
color: #6b5a33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value,
|
||||||
|
.price-campaign {
|
||||||
|
min-height: 26px;
|
||||||
|
padding: 5px 7px;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: right;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-weight: 800;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-campaign {
|
||||||
|
color: #8a8a8a;
|
||||||
|
background: #f4f4f4;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-info-row-usd .price-value {
|
||||||
|
color: #16803a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-info-row-eur .price-value {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-info-row-try .price-value {
|
||||||
|
color: #185abc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-info-row-usd.has-campaign-price .price-campaign {
|
||||||
|
background: #dff6e7;
|
||||||
|
color: #0f6b2f;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-info-row-eur.has-campaign-price .price-campaign {
|
||||||
|
background: #fde2e2;
|
||||||
|
color: #a61717;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-info-row-try.has-campaign-price .price-campaign {
|
||||||
|
background: #e2edff;
|
||||||
|
color: #174ea6;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-stock-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(62px, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-stock-cell {
|
||||||
|
min-height: 42px;
|
||||||
|
border: 1px solid #eadfca;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #6b5a33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-qty {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #1f1f1f;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-empty-text {
|
||||||
|
color: #7a6d55;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-fullscreen-dialog {
|
||||||
|
background: #f4f0e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-fullscreen-body {
|
||||||
|
height: calc(100vh - 72px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-fullscreen-carousel {
|
||||||
|
width: min(98vw, 1500px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-fullscreen-stage {
|
||||||
|
width: min(96vw, 1400px);
|
||||||
|
height: calc(100vh - 120px);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: linear-gradient(180deg, #f1e7d3 0%, #e9dcc4 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-fullscreen-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-btn,
|
||||||
|
.q-icon,
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.product-card-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-images,
|
||||||
|
.product-card-fields,
|
||||||
|
.product-card-price-panel {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: auto;
|
||||||
|
height: auto;
|
||||||
|
min-height: 320px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
172
ui/src/pages/OrderPriceListMailMapping.vue
Normal file
172
ui/src/pages/OrderPriceListMailMapping.vue
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
<template>
|
||||||
|
<q-page v-if="canUpdateSystem" class="q-pa-md">
|
||||||
|
<div class="row justify-end q-mb-md">
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
icon="save"
|
||||||
|
label="Degisiklikleri Kaydet"
|
||||||
|
:loading="store.saving"
|
||||||
|
:disable="!hasChanges"
|
||||||
|
@click="saveChanges"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-table
|
||||||
|
flat
|
||||||
|
bordered
|
||||||
|
dense
|
||||||
|
row-key="group_code"
|
||||||
|
:loading="store.loading"
|
||||||
|
:rows="store.rows"
|
||||||
|
:columns="columns"
|
||||||
|
:rows-per-page-options="[0]"
|
||||||
|
:pagination="{ rowsPerPage: 0 }"
|
||||||
|
>
|
||||||
|
<template #body-cell-mail_selector="props">
|
||||||
|
<q-td :props="props">
|
||||||
|
<q-select
|
||||||
|
:model-value="editableByGroup[props.row.group_code] || []"
|
||||||
|
:options="mailOptionsByGroup[props.row.group_code] || allMailOptions"
|
||||||
|
option-value="id"
|
||||||
|
option-label="label"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
multiple
|
||||||
|
use-chips
|
||||||
|
use-input
|
||||||
|
input-debounce="0"
|
||||||
|
clearable
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
label="Mail ara ve sec"
|
||||||
|
@filter="(val, update) => filterMailOptions(props.row.group_code, val, update)"
|
||||||
|
@update:model-value="(val) => updateRowSelection(props.row.group_code, val)"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</q-page>
|
||||||
|
|
||||||
|
<q-page v-else class="q-pa-md flex flex-center">
|
||||||
|
<div class="text-negative text-subtitle1">
|
||||||
|
Bu module erisim yetkiniz yok.
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useQuasar } from 'quasar'
|
||||||
|
import { usePermission } from 'src/composables/usePermission'
|
||||||
|
import { useOrderPriceListMailMappingStore } from 'src/stores/orderPriceListMailMappingStore'
|
||||||
|
|
||||||
|
const $q = useQuasar()
|
||||||
|
const store = useOrderPriceListMailMappingStore()
|
||||||
|
|
||||||
|
const { canUpdate } = usePermission()
|
||||||
|
const canUpdateSystem = canUpdate('system')
|
||||||
|
|
||||||
|
const editableByGroup = ref({})
|
||||||
|
const originalByGroup = ref({})
|
||||||
|
const mailOptionsByGroup = ref({})
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ name: 'group_code', label: 'Urun Ilk Grup Kodu', field: 'group_code', align: 'left' },
|
||||||
|
{ name: 'group_title', label: 'Urun Ilk Grup Aciklama', field: 'group_title', align: 'left' },
|
||||||
|
{ name: 'mail_selector', label: 'Fiyat Listesi Mail Eslestirme', field: 'mail_selector', align: 'left' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const allMailOptions = computed(() =>
|
||||||
|
(store.mails || []).map((m) => ({ id: m.id, label: m.display_name || m.email }))
|
||||||
|
)
|
||||||
|
|
||||||
|
const changedGroups = computed(() => {
|
||||||
|
return (store.rows || [])
|
||||||
|
.map((r) => String(r.group_code || r.urun_ilk_grubu || '').trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((g) => {
|
||||||
|
const current = normalizeList(editableByGroup.value[g] || [])
|
||||||
|
const original = normalizeList(originalByGroup.value[g] || [])
|
||||||
|
return !isEqualList(current, original)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasChanges = computed(() => changedGroups.value.length > 0)
|
||||||
|
|
||||||
|
function normalizeList (list) {
|
||||||
|
return Array.from(
|
||||||
|
new Set(
|
||||||
|
(Array.isArray(list) ? list : [])
|
||||||
|
.map((x) => String(x).trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
)
|
||||||
|
).sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEqualList (a, b) {
|
||||||
|
if (a.length !== b.length) return false
|
||||||
|
for (let i = 0; i < a.length; i += 1) {
|
||||||
|
if (a[i] !== b[i]) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function initEditableState () {
|
||||||
|
const editable = {}
|
||||||
|
const original = {}
|
||||||
|
|
||||||
|
;(store.rows || []).forEach((row) => {
|
||||||
|
const g = String(row.group_code || row.urun_ilk_grubu || '').trim()
|
||||||
|
const selected = normalizeList(row.mail_ids || [])
|
||||||
|
editable[g] = [...selected]
|
||||||
|
original[g] = [...selected]
|
||||||
|
})
|
||||||
|
|
||||||
|
editableByGroup.value = editable
|
||||||
|
originalByGroup.value = original
|
||||||
|
mailOptionsByGroup.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRowSelection (group, newValue) {
|
||||||
|
const g = String(group || '').trim()
|
||||||
|
editableByGroup.value = { ...editableByGroup.value, [g]: normalizeList(newValue) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterMailOptions (group, search, update) {
|
||||||
|
const g = String(group || '').trim()
|
||||||
|
update(() => {
|
||||||
|
const q = String(search || '').trim().toLowerCase()
|
||||||
|
const filtered = !q
|
||||||
|
? allMailOptions.value
|
||||||
|
: allMailOptions.value.filter((opt) => String(opt.label || '').toLowerCase().includes(q))
|
||||||
|
|
||||||
|
mailOptionsByGroup.value = { ...mailOptionsByGroup.value, [g]: filtered }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init () {
|
||||||
|
try {
|
||||||
|
await Promise.all([store.fetchLookups(), store.fetchRows()])
|
||||||
|
initEditableState()
|
||||||
|
} catch (err) {
|
||||||
|
$q.notify({ type: 'negative', message: err?.message || 'Fiyat listesi mail eslestirmeleri yuklenemedi' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveChanges () {
|
||||||
|
if (!hasChanges.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const g of changedGroups.value) {
|
||||||
|
await store.saveGroupMails(g, editableByGroup.value[g] || [])
|
||||||
|
}
|
||||||
|
await store.fetchRows()
|
||||||
|
initEditableState()
|
||||||
|
$q.notify({ type: 'positive', message: 'Degisiklikler kaydedildi' })
|
||||||
|
} catch (err) {
|
||||||
|
$q.notify({ type: 'negative', message: err?.message || 'Kayit hatasi' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => { init() })
|
||||||
|
</script>
|
||||||
163
ui/src/pages/OrderPriceListUserPriceGroupMapping.vue
Normal file
163
ui/src/pages/OrderPriceListUserPriceGroupMapping.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<q-page v-if="canUpdateSystem" class="q-pa-md">
|
||||||
|
<div class="row items-center justify-between q-mb-md">
|
||||||
|
<div>
|
||||||
|
<div class="text-h6">Kullanici Fiyat Grubu Eslestirme</div>
|
||||||
|
<div class="text-caption text-grey-7">Fiyat Listesi ekraninda kullanicinin gorebilecegi fiyat gruplarini belirler.</div>
|
||||||
|
</div>
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
icon="save"
|
||||||
|
label="Degisiklikleri Kaydet"
|
||||||
|
:loading="store.saving"
|
||||||
|
:disable="!hasChanges"
|
||||||
|
@click="saveChanges"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-table
|
||||||
|
flat
|
||||||
|
bordered
|
||||||
|
dense
|
||||||
|
row-key="user_id"
|
||||||
|
:loading="store.loading"
|
||||||
|
:rows="store.rows"
|
||||||
|
:columns="columns"
|
||||||
|
:rows-per-page-options="[0]"
|
||||||
|
:pagination="{ rowsPerPage: 0 }"
|
||||||
|
>
|
||||||
|
<template #body-cell-price_groups="props">
|
||||||
|
<q-td :props="props">
|
||||||
|
<q-select
|
||||||
|
:model-value="editableByUser[props.row.user_id] || []"
|
||||||
|
:options="store.priceGroupOptions"
|
||||||
|
option-value="value"
|
||||||
|
option-label="label"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
multiple
|
||||||
|
use-chips
|
||||||
|
clearable
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
label="Fiyat gruplari"
|
||||||
|
@update:model-value="(val) => updateRowSelection(props.row.user_id, val)"
|
||||||
|
>
|
||||||
|
<template #before-options>
|
||||||
|
<q-item clickable @click="selectAll(props.row.user_id)">
|
||||||
|
<q-item-section>Tumunu Sec</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable @click="clearAll(props.row.user_id)">
|
||||||
|
<q-item-section>Temizle</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-separator />
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</q-page>
|
||||||
|
|
||||||
|
<q-page v-else class="q-pa-md flex flex-center">
|
||||||
|
<div class="text-negative text-subtitle1">
|
||||||
|
Bu module erisim yetkiniz yok.
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useQuasar } from 'quasar'
|
||||||
|
import { usePermission } from 'src/composables/usePermission'
|
||||||
|
import { useOrderPriceListUserPriceGroupStore } from 'src/stores/orderPriceListUserPriceGroupStore'
|
||||||
|
|
||||||
|
const $q = useQuasar()
|
||||||
|
const store = useOrderPriceListUserPriceGroupStore()
|
||||||
|
const { canUpdate } = usePermission()
|
||||||
|
const canUpdateSystem = canUpdate('system')
|
||||||
|
|
||||||
|
const editableByUser = ref({})
|
||||||
|
const originalByUser = ref({})
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ name: 'username', label: 'Kullanici Kodu', field: 'username', align: 'left', sortable: true },
|
||||||
|
{ name: 'full_name', label: 'Ad Soyad', field: 'full_name', align: 'left', sortable: true },
|
||||||
|
{ name: 'email', label: 'E-Posta', field: 'email', align: 'left' },
|
||||||
|
{ name: 'price_groups', label: 'Gorebilecegi Fiyat Gruplari', field: 'price_groups', align: 'left' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const changedUsers = computed(() => {
|
||||||
|
return (store.rows || [])
|
||||||
|
.map((r) => Number(r.user_id || 0))
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((id) => !isEqualList(normalizeList(editableByUser.value[id] || []), normalizeList(originalByUser.value[id] || [])))
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasChanges = computed(() => changedUsers.value.length > 0)
|
||||||
|
|
||||||
|
function normalizeList (list) {
|
||||||
|
const allowed = new Set((store.priceGroupOptions || []).map((x) => x.value))
|
||||||
|
return Array.from(new Set((Array.isArray(list) ? list : []).map((x) => String(x).trim()).filter((x) => allowed.has(x)))).sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEqualList (a, b) {
|
||||||
|
if (a.length !== b.length) return false
|
||||||
|
for (let i = 0; i < a.length; i += 1) {
|
||||||
|
if (a[i] !== b[i]) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function initEditableState () {
|
||||||
|
const editable = {}
|
||||||
|
const original = {}
|
||||||
|
for (const row of store.rows || []) {
|
||||||
|
const id = Number(row.user_id || 0)
|
||||||
|
if (!id) continue
|
||||||
|
const selected = normalizeList(row.price_groups || [])
|
||||||
|
editable[id] = [...selected]
|
||||||
|
original[id] = [...selected]
|
||||||
|
}
|
||||||
|
editableByUser.value = editable
|
||||||
|
originalByUser.value = original
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRowSelection (userId, newValue) {
|
||||||
|
const id = Number(userId || 0)
|
||||||
|
if (!id) return
|
||||||
|
editableByUser.value = { ...editableByUser.value, [id]: normalizeList(newValue) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAll (userId) {
|
||||||
|
updateRowSelection(userId, store.priceGroupOptions.map((x) => x.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAll (userId) {
|
||||||
|
updateRowSelection(userId, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init () {
|
||||||
|
try {
|
||||||
|
await Promise.all([store.fetchLookups(), store.fetchRows()])
|
||||||
|
initEditableState()
|
||||||
|
} catch (err) {
|
||||||
|
$q.notify({ type: 'negative', message: err?.message || 'Kullanici fiyat grubu eslestirmeleri yuklenemedi' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveChanges () {
|
||||||
|
if (!hasChanges.value) return
|
||||||
|
try {
|
||||||
|
for (const id of changedUsers.value) {
|
||||||
|
await store.saveUserPriceGroups(id, editableByUser.value[id] || [])
|
||||||
|
}
|
||||||
|
await store.fetchRows()
|
||||||
|
initEditableState()
|
||||||
|
$q.notify({ type: 'positive', message: 'Degisiklikler kaydedildi' })
|
||||||
|
} catch (err) {
|
||||||
|
$q.notify({ type: 'negative', message: err?.message || 'Kayit hatasi' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => { init() })
|
||||||
|
</script>
|
||||||
@@ -106,6 +106,13 @@
|
|||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-btn-dropdown>
|
</q-btn-dropdown>
|
||||||
|
<q-toggle
|
||||||
|
v-model="showInStockOnly"
|
||||||
|
dense
|
||||||
|
color="primary"
|
||||||
|
label="Sadece stogu olanlar"
|
||||||
|
:disable="pageBusy"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
@@ -1245,6 +1252,7 @@ const selectedCurrencies = ref(['USD', 'EUR', 'TRY'])
|
|||||||
const exportAllLoading = ref(false)
|
const exportAllLoading = ref(false)
|
||||||
const showSelectedOnly = ref(false)
|
const showSelectedOnly = ref(false)
|
||||||
const leftDetailsExpanded = ref(true)
|
const leftDetailsExpanded = ref(true)
|
||||||
|
const showInStockOnly = ref(false)
|
||||||
const calcLoadingMap = ref({})
|
const calcLoadingMap = ref({})
|
||||||
const bulkCalcLoading = ref(false)
|
const bulkCalcLoading = ref(false)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
@@ -1528,6 +1536,7 @@ function rowSelectionKey (row) {
|
|||||||
const filteredRows = computed(() => {
|
const filteredRows = computed(() => {
|
||||||
return rows.value.filter((row) => {
|
return rows.value.filter((row) => {
|
||||||
if (showSelectedOnly.value && !selectedMap.value[rowSelectionKey(row)]) return false
|
if (showSelectedOnly.value && !selectedMap.value[rowSelectionKey(row)]) return false
|
||||||
|
if (showInStockOnly.value && Number(row?.stockQty ?? 0) <= 0) return false
|
||||||
for (const { field } of multiFilterColumns) {
|
for (const { field } of multiFilterColumns) {
|
||||||
// Server-backed filters already reload full dataset (all pages) from backend.
|
// Server-backed filters already reload full dataset (all pages) from backend.
|
||||||
// Keep only non-server multi filters (e.g. brandGroupSelection) as local page filter.
|
// Keep only non-server multi filters (e.g. brandGroupSelection) as local page filter.
|
||||||
@@ -1816,10 +1825,20 @@ function getOriginalCellValue (row, field) {
|
|||||||
return row?.[`__orig_${field}`] ?? row?.[field] ?? 0
|
return row?.[`__orig_${field}`] ?? row?.[field] ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function exportDecimalValue (value) {
|
||||||
|
const n = parseNumber(value)
|
||||||
|
if (!Number.isFinite(n)) return ''
|
||||||
|
return n.toFixed(2).replace('.', ',')
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExportDecimalField (field) {
|
||||||
|
return editableColumnSet.has(field) || field === 'costPrice' || field === 'basePriceUsd' || field === 'basePriceTry'
|
||||||
|
}
|
||||||
|
|
||||||
function exportCellValue (row, field) {
|
function exportCellValue (row, field) {
|
||||||
if (field === 'stockQty') return formatStock(row?.[field])
|
if (field === 'stockQty') return formatStock(row?.[field])
|
||||||
if (field === 'stockEntryDate' || field === 'lastCostingDate' || field === 'lastPricingDate') return formatDateDisplay(row?.[field])
|
if (field === 'stockEntryDate' || field === 'lastCostingDate' || field === 'lastPricingDate') return formatDateDisplay(row?.[field])
|
||||||
if (editableColumnSet.has(field)) return String(round2(row?.[field] || 0))
|
if (isExportDecimalField(field)) return exportDecimalValue(row?.[field])
|
||||||
return String(row?.[field] ?? '').trim()
|
return String(row?.[field] ?? '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -277,6 +277,34 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="text-caption text-grey-7 q-mb-xs">Fiyat Listesi Fiyat Gruplari</div>
|
||||||
|
<q-select
|
||||||
|
v-model="form.order_price_list_price_groups"
|
||||||
|
:options="orderPriceListPriceGroupOptions"
|
||||||
|
option-label="label"
|
||||||
|
option-value="value"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
multiple
|
||||||
|
use-chips
|
||||||
|
clearable
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
behavior="menu"
|
||||||
|
>
|
||||||
|
<template #before-options>
|
||||||
|
<q-item clickable @click="selectAllOrderPriceGroups">
|
||||||
|
<q-item-section>Tumunu Sec</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable @click="clearOrderPriceGroups">
|
||||||
|
<q-item-section>Temizle</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-separator />
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -320,6 +348,7 @@ const {
|
|||||||
departmentOptions,
|
departmentOptions,
|
||||||
piyasaOptions,
|
piyasaOptions,
|
||||||
nebimUserOptions,
|
nebimUserOptions,
|
||||||
|
orderPriceListPriceGroupOptions,
|
||||||
sendingPasswordMail,
|
sendingPasswordMail,
|
||||||
lastPasswordMailSentAt
|
lastPasswordMailSentAt
|
||||||
} = storeToRefs(store)
|
} = storeToRefs(store)
|
||||||
@@ -373,6 +402,16 @@ function clearPiyasalar () {
|
|||||||
form.value.piyasalar = []
|
form.value.piyasalar = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectAllOrderPriceGroups () {
|
||||||
|
form.value.order_price_list_price_groups = (orderPriceListPriceGroupOptions.value || [])
|
||||||
|
.map((o) => o.value)
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearOrderPriceGroups () {
|
||||||
|
form.value.order_price_list_price_groups = []
|
||||||
|
}
|
||||||
|
|
||||||
/* ================= LIFECYCLE ================= */
|
/* ================= LIFECYCLE ================= */
|
||||||
watch(
|
watch(
|
||||||
() => userId.value,
|
() => userId.value,
|
||||||
|
|||||||
@@ -106,6 +106,13 @@
|
|||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-btn-dropdown>
|
</q-btn-dropdown>
|
||||||
|
<q-toggle
|
||||||
|
v-model="showInStockOnly"
|
||||||
|
dense
|
||||||
|
color="primary"
|
||||||
|
label="Sadece stogu olanlar"
|
||||||
|
:disable="pageBusy"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="row items-center q-gutter-xs">
|
<div class="row items-center q-gutter-xs">
|
||||||
<q-chip
|
<q-chip
|
||||||
@@ -884,6 +891,7 @@ const currentPage = ref(1)
|
|||||||
let reloadTimer = null
|
let reloadTimer = null
|
||||||
const variantRows = ref([])
|
const variantRows = ref([])
|
||||||
const variantRowsCache = new Map()
|
const variantRowsCache = new Map()
|
||||||
|
const variantCodeCollator = new Intl.Collator('tr', { numeric: true, sensitivity: 'base' })
|
||||||
const VARIANT_ROWS_CACHE_LIMIT = 16
|
const VARIANT_ROWS_CACHE_LIMIT = 16
|
||||||
|
|
||||||
const GUIDANCE_MSG = "Calismak icin once Urun Ilk Grubu veya Urun Ana Grubu Secin ve GRUPLARI GETIR'e Basin."
|
const GUIDANCE_MSG = "Calismak icin once Urun Ilk Grubu veya Urun Ana Grubu Secin ve GRUPLARI GETIR'e Basin."
|
||||||
@@ -1301,6 +1309,7 @@ const selectedPriceOptions = ref(priceOptionOptions.map((x) => x.value))
|
|||||||
const exportAllLoading = ref(false)
|
const exportAllLoading = ref(false)
|
||||||
const showSelectedOnly = ref(false)
|
const showSelectedOnly = ref(false)
|
||||||
const leftDetailsExpanded = ref(true)
|
const leftDetailsExpanded = ref(true)
|
||||||
|
const showInStockOnly = ref(false)
|
||||||
// Keep the old calc state around (not used on this screen) to avoid touching shared helpers.
|
// Keep the old calc state around (not used on this screen) to avoid touching shared helpers.
|
||||||
const calcLoadingMap = ref({})
|
const calcLoadingMap = ref({})
|
||||||
|
|
||||||
@@ -1601,6 +1610,7 @@ function rowSelectionKey (row) {
|
|||||||
const filteredRows = computed(() => {
|
const filteredRows = computed(() => {
|
||||||
return rows.value.filter((row) => {
|
return rows.value.filter((row) => {
|
||||||
if (showSelectedOnly.value && !selectedMap.value[rowSelectionKey(row)]) return false
|
if (showSelectedOnly.value && !selectedMap.value[rowSelectionKey(row)]) return false
|
||||||
|
if (showInStockOnly.value && Number(row?.stockQty ?? 0) <= 0) return false
|
||||||
for (const { field } of multiFilterColumns) {
|
for (const { field } of multiFilterColumns) {
|
||||||
// Server-backed filters already reload full dataset (all pages) from backend.
|
// Server-backed filters already reload full dataset (all pages) from backend.
|
||||||
// Keep only non-server multi filters (e.g. brandGroupSelection) as local page filter.
|
// Keep only non-server multi filters (e.g. brandGroupSelection) as local page filter.
|
||||||
@@ -1916,10 +1926,27 @@ function getOriginalCellValue (row, field) {
|
|||||||
return row?.[`__orig_${field}`] ?? row?.[field] ?? 0
|
return row?.[`__orig_${field}`] ?? row?.[field] ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function exportDecimalValue (value) {
|
||||||
|
const n = parseNumber(value)
|
||||||
|
if (!Number.isFinite(n)) return ''
|
||||||
|
return n.toFixed(2).replace('.', ',')
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExportDecimalField (field) {
|
||||||
|
return field === 'campaignRate' ||
|
||||||
|
field === 'belowBaseDiff' ||
|
||||||
|
field === 'costPrice' ||
|
||||||
|
field === 'basePriceUsd' ||
|
||||||
|
field === 'basePriceTry' ||
|
||||||
|
/^usd\d$/i.test(field) ||
|
||||||
|
/^eur\d$/i.test(field) ||
|
||||||
|
/^try\d$/i.test(field)
|
||||||
|
}
|
||||||
|
|
||||||
function exportCellValue (row, field) {
|
function exportCellValue (row, field) {
|
||||||
if (field === 'stockQty') return formatStock(row?.[field])
|
if (field === 'stockQty') return formatStock(row?.[field])
|
||||||
if (field === 'stockEntryDate' || field === 'lastCampaignDate') return formatDateDisplay(row?.[field])
|
if (field === 'stockEntryDate' || field === 'lastCampaignDate') return formatDateDisplay(row?.[field])
|
||||||
if (editableColumnSet.has(field)) return String(round2(row?.[field] || 0))
|
if (isExportDecimalField(field)) return exportDecimalValue(row?.[field])
|
||||||
return String(row?.[field] ?? '').trim()
|
return String(row?.[field] ?? '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2678,7 +2705,7 @@ async function buildVariantRowsForProductPage (baseProductRows = []) {
|
|||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
vs.sort((a, b) => String(a?.variant_code || '').localeCompare(String(b?.variant_code || ''), 'tr'))
|
vs.sort((a, b) => variantCodeCollator.compare(String(a?.variant_code || ''), String(b?.variant_code || '')))
|
||||||
for (const v of vs) {
|
for (const v of vs) {
|
||||||
const d1 = Number(v?.dim1 || 0)
|
const d1 = Number(v?.dim1 || 0)
|
||||||
const d3 = v?.dim3 == null ? null : Number(v?.dim3 || 0)
|
const d3 = v?.dim3 == null ? null : Number(v?.dim3 || 0)
|
||||||
|
|||||||
@@ -251,6 +251,18 @@ const routes = [
|
|||||||
component: () => import('../pages/PricingMailMapping.vue'),
|
component: () => import('../pages/PricingMailMapping.vue'),
|
||||||
meta: { permission: 'system:update' }
|
meta: { permission: 'system:update' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'order-price-list-mail-mapping',
|
||||||
|
name: 'order-price-list-mail-mapping',
|
||||||
|
component: () => import('../pages/OrderPriceListMailMapping.vue'),
|
||||||
|
meta: { permission: 'system:update' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'order-price-list-user-price-groups',
|
||||||
|
name: 'order-price-list-user-price-groups',
|
||||||
|
component: () => import('../pages/OrderPriceListUserPriceGroupMapping.vue'),
|
||||||
|
meta: { permission: 'system:update' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'language/translations',
|
path: 'language/translations',
|
||||||
name: 'translation-table',
|
name: 'translation-table',
|
||||||
|
|||||||
@@ -26,14 +26,16 @@ export const useUserDetailStore = defineStore('userDetail', {
|
|||||||
roles: [],
|
roles: [],
|
||||||
departments: null,
|
departments: null,
|
||||||
piyasalar: [],
|
piyasalar: [],
|
||||||
nebim_users: null
|
nebim_users: null,
|
||||||
|
order_price_list_price_groups: []
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ================= LOOKUPS ================= */
|
/* ================= LOOKUPS ================= */
|
||||||
roleOptions: [],
|
roleOptions: [],
|
||||||
departmentOptions: [],
|
departmentOptions: [],
|
||||||
piyasaOptions: [],
|
piyasaOptions: [],
|
||||||
nebimUserOptions: []
|
nebimUserOptions: [],
|
||||||
|
orderPriceListPriceGroupOptions: []
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -52,7 +54,8 @@ export const useUserDetailStore = defineStore('userDetail', {
|
|||||||
roles: [],
|
roles: [],
|
||||||
departments: null,
|
departments: null,
|
||||||
piyasalar: [],
|
piyasalar: [],
|
||||||
nebim_users: null
|
nebim_users: null,
|
||||||
|
order_price_list_price_groups: []
|
||||||
}
|
}
|
||||||
this.error = null
|
this.error = null
|
||||||
this.hasPassword = false
|
this.hasPassword = false
|
||||||
@@ -113,6 +116,7 @@ export const useUserDetailStore = defineStore('userDetail', {
|
|||||||
departments: departmentCodes.map(code => ({ code })),
|
departments: departmentCodes.map(code => ({ code })),
|
||||||
|
|
||||||
piyasalar: (this.form.piyasalar || []).map(code => ({ code })),
|
piyasalar: (this.form.piyasalar || []).map(code => ({ code })),
|
||||||
|
order_price_list_price_groups: this.form.order_price_list_price_groups || [],
|
||||||
|
|
||||||
nebim_users: nebimUsernames.map(username => {
|
nebim_users: nebimUsernames.map(username => {
|
||||||
const opt = (this.nebimUserOptions || []).find(x => x.value === username)
|
const opt = (this.nebimUserOptions || []).find(x => x.value === username)
|
||||||
@@ -146,6 +150,7 @@ export const useUserDetailStore = defineStore('userDetail', {
|
|||||||
this.form.departments = (data.departments || []).map(x => x.code)[0] || null
|
this.form.departments = (data.departments || []).map(x => x.code)[0] || null
|
||||||
this.form.piyasalar = (data.piyasalar || []).map(x => x.code)
|
this.form.piyasalar = (data.piyasalar || []).map(x => x.code)
|
||||||
this.form.nebim_users = (data.nebim_users || []).map(x => x.username)[0] || null
|
this.form.nebim_users = (data.nebim_users || []).map(x => x.username)[0] || null
|
||||||
|
this.form.order_price_list_price_groups = data.order_price_list_price_groups || []
|
||||||
|
|
||||||
this.hasPassword = !!data.has_password
|
this.hasPassword = !!data.has_password
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -237,17 +242,22 @@ export const useUserDetailStore = defineStore('userDetail', {
|
|||||||
===================================================== */
|
===================================================== */
|
||||||
async fetchLookups () {
|
async fetchLookups () {
|
||||||
// token otomatik
|
// token otomatik
|
||||||
const [roles, depts, piyasalar, nebims] = await Promise.all([
|
const [roles, depts, piyasalar, nebims, priceGroups] = await Promise.all([
|
||||||
api.get('/lookups/roles'),
|
api.get('/lookups/roles'),
|
||||||
api.get('/lookups/departments'),
|
api.get('/lookups/departments'),
|
||||||
api.get('/lookups/piyasalar'),
|
api.get('/lookups/piyasalar'),
|
||||||
api.get('/lookups/nebim-users')
|
api.get('/lookups/nebim-users'),
|
||||||
|
api.get('/users/order-price-list-price-groups/lookups')
|
||||||
])
|
])
|
||||||
|
|
||||||
this.roleOptions = roles?.data || roles || []
|
this.roleOptions = roles?.data || roles || []
|
||||||
this.departmentOptions = depts?.data || depts || []
|
this.departmentOptions = depts?.data || depts || []
|
||||||
this.piyasaOptions = piyasalar?.data || piyasalar || []
|
this.piyasaOptions = piyasalar?.data || piyasalar || []
|
||||||
this.nebimUserOptions = nebims?.data || nebims || []
|
this.nebimUserOptions = nebims?.data || nebims || []
|
||||||
|
this.orderPriceListPriceGroupOptions = (priceGroups?.data?.price_groups || []).map(x => ({
|
||||||
|
label: x.label || x.value,
|
||||||
|
value: x.value
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
47
ui/src/stores/orderPriceListMailMappingStore.js
Normal file
47
ui/src/stores/orderPriceListMailMappingStore.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import api from 'src/services/api'
|
||||||
|
|
||||||
|
export const useOrderPriceListMailMappingStore = defineStore('orderPriceListMailMapping', {
|
||||||
|
state: () => ({
|
||||||
|
loading: false,
|
||||||
|
saving: false,
|
||||||
|
firstGroups: [],
|
||||||
|
mails: [],
|
||||||
|
rows: []
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchLookups () {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const res = await api.get('/system/order-price-list-mail-mappings/lookups')
|
||||||
|
const payload = res?.data || {}
|
||||||
|
this.firstGroups = Array.isArray(payload.first_groups) ? payload.first_groups : []
|
||||||
|
this.mails = Array.isArray(payload.mails) ? payload.mails : []
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchRows () {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const res = await api.get('/system/order-price-list-mail-mappings')
|
||||||
|
this.rows = Array.isArray(res?.data) ? res.data : []
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveGroupMails (urunIlkGrubu, mailIds) {
|
||||||
|
this.saving = true
|
||||||
|
try {
|
||||||
|
await api.put(`/system/order-price-list-mail-mappings/${encodeURIComponent(String(urunIlkGrubu || '').trim())}`, {
|
||||||
|
mail_ids: Array.isArray(mailIds) ? mailIds : []
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.saving = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
42
ui/src/stores/orderPriceListUserPriceGroupStore.js
Normal file
42
ui/src/stores/orderPriceListUserPriceGroupStore.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import api from 'src/services/api'
|
||||||
|
|
||||||
|
export const useOrderPriceListUserPriceGroupStore = defineStore('orderPriceListUserPriceGroup', {
|
||||||
|
state: () => ({
|
||||||
|
loading: false,
|
||||||
|
saving: false,
|
||||||
|
rows: [],
|
||||||
|
priceGroupOptions: []
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchLookups () {
|
||||||
|
const res = await api.get('/system/order-price-list-user-price-groups/lookups')
|
||||||
|
this.priceGroupOptions = (res?.data?.price_groups || []).map((x) => ({
|
||||||
|
label: x.label || x.value,
|
||||||
|
value: x.value
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchRows () {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const res = await api.get('/system/order-price-list-user-price-groups')
|
||||||
|
this.rows = Array.isArray(res?.data) ? res.data : []
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveUserPriceGroups (userId, priceGroups) {
|
||||||
|
this.saving = true
|
||||||
|
try {
|
||||||
|
await api.put(`/system/order-price-list-user-price-groups/${encodeURIComponent(String(userId))}`, {
|
||||||
|
price_groups: Array.isArray(priceGroups) ? priceGroups : []
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.saving = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user