Compare commits
15 Commits
ef33a56a49
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1054a15547 | ||
|
|
da9d7c2fd5 | ||
|
|
539ca4b587 | ||
|
|
10f90cbaf3 | ||
|
|
13c9df1e96 | ||
|
|
462fb4058d | ||
|
|
a2f70160bc | ||
|
|
3732004a29 | ||
|
|
c8c37b4e69 | ||
|
|
ad8d459491 | ||
|
|
9b57e0846e | ||
|
|
55e36366c3 | ||
|
|
7512e7fe7c | ||
|
|
81d1af61be | ||
|
|
b59889bbdb |
@@ -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",
|
||||
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,
|
||||
"/api/language/translations", "GET",
|
||||
"language", "update",
|
||||
@@ -424,6 +454,21 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
||||
"system", "update",
|
||||
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)
|
||||
bindV3(r, pgDB,
|
||||
@@ -795,6 +840,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
||||
"order", "view",
|
||||
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,
|
||||
"/api/order/price-list/campaigns", "GET",
|
||||
"order", "view",
|
||||
@@ -815,6 +865,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
||||
"order", "view",
|
||||
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,
|
||||
"/api/product-size-match/rules", "GET",
|
||||
"order", "view",
|
||||
|
||||
@@ -18,10 +18,11 @@ type UserDetail struct {
|
||||
HasPassword bool `json:"has_password"` // 🔐 SADECE DURUM
|
||||
|
||||
// ===== İLİŞKİLER =====
|
||||
Roles []string `json:"roles"`
|
||||
Departments []DeptOption `json:"departments"`
|
||||
Piyasalar []DeptOption `json:"piyasalar"`
|
||||
NebimUsers []NebimOption `json:"nebim_users"`
|
||||
Roles []string `json:"roles"`
|
||||
Departments []DeptOption `json:"departments"`
|
||||
Piyasalar []DeptOption `json:"piyasalar"`
|
||||
NebimUsers []NebimOption `json:"nebim_users"`
|
||||
OrderPriceListPriceGroups []string `json:"order_price_list_price_groups"`
|
||||
}
|
||||
|
||||
// ======================================================
|
||||
@@ -35,10 +36,11 @@ type UserWrite struct {
|
||||
Mobile string `json:"mobile"`
|
||||
Address string `json:"address"`
|
||||
|
||||
Roles []string `json:"roles"`
|
||||
Departments []DeptOption `json:"departments"`
|
||||
Piyasalar []DeptOption `json:"piyasalar"`
|
||||
NebimUsers []NebimOption `json:"nebim_users"`
|
||||
Roles []string `json:"roles"`
|
||||
Departments []DeptOption `json:"departments"`
|
||||
Piyasalar []DeptOption `json:"piyasalar"`
|
||||
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)
|
||||
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
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Note: Column semantics depend on your Nebim setup. We treat ItemDim1Code/ItemDim3Code as the
|
||||
// primary variant dimensions used by the e-commerce sdprc dim filters.
|
||||
// Note: Column semantics depend on your Nebim setup. Here ColorCode and ItemDim2Code are the
|
||||
// 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 = `
|
||||
DECLARE @ProductCode NVARCHAR(50) = @p1;
|
||||
|
||||
SELECT DISTINCT
|
||||
LTRIM(RTRIM(ISNULL(S.ColorCode,''))) AS ColorCode,
|
||||
LTRIM(RTRIM(ISNULL(S.ItemDim1Code,''))) AS ItemDim1Code,
|
||||
LTRIM(RTRIM(ISNULL(S.ItemDim3Code,''))) AS ItemDim3Code
|
||||
FROM trStock S WITH(NOLOCK)
|
||||
WHERE S.ItemTypeCode = 1
|
||||
AND S.ItemCode = @ProductCode
|
||||
AND LEN(S.ItemCode) = 13
|
||||
LTRIM(RTRIM(ISNULL(V.ColorCode,''))) AS ColorCode,
|
||||
LTRIM(RTRIM(ISNULL(V.ItemDim1Code,''))) AS ItemDim1Code,
|
||||
LTRIM(RTRIM(ISNULL(V.ItemDim2Code,''))) AS ItemDim3Code
|
||||
FROM prItemVariant V WITH(NOLOCK)
|
||||
WHERE V.ItemTypeCode = 1
|
||||
AND V.ItemCode = @ProductCode
|
||||
AND LEN(V.ItemCode) = 13
|
||||
AND LEN(@ProductCode) = 13
|
||||
ORDER BY
|
||||
LTRIM(RTRIM(ISNULL(S.ColorCode,''))),
|
||||
LTRIM(RTRIM(ISNULL(S.ItemDim1Code,''))),
|
||||
LTRIM(RTRIM(ISNULL(S.ItemDim3Code,'')));
|
||||
LTRIM(RTRIM(ISNULL(V.ColorCode,''))),
|
||||
LTRIM(RTRIM(ISNULL(V.ItemDim1Code,''))),
|
||||
LTRIM(RTRIM(ISNULL(V.ItemDim2Code,'')));
|
||||
`
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package queries
|
||||
|
||||
// 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".
|
||||
// In this Nebim setup, the second user-visible/key variant maps to ItemDim2Code, while PG stores it in mmitem_dim.val3.
|
||||
const GetWholesaleCampaignVariantStockByProducts = `
|
||||
DECLARE @Codes NVARCHAR(MAX) = @p1;
|
||||
|
||||
@@ -16,11 +17,50 @@ DECLARE @Codes NVARCHAR(MAX) = @p1;
|
||||
CROSS APPLY D.XmlData.nodes('/i') AS X(C)
|
||||
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 (
|
||||
SELECT
|
||||
S.ItemCode,
|
||||
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,
|
||||
SUM(S.In_Qty1 - S.Out_Qty1) AS InventoryQty1
|
||||
FROM trStock S WITH(NOLOCK)
|
||||
@@ -28,13 +68,13 @@ STOCK AS (
|
||||
WHERE S.ItemTypeCode = 1
|
||||
AND LEN(S.ItemCode) = 13
|
||||
GROUP BY
|
||||
S.ItemCode, S.ColorCode, S.ItemDim3Code
|
||||
S.ItemCode, S.ColorCode, S.ItemDim2Code
|
||||
),
|
||||
PICK AS (
|
||||
SELECT
|
||||
P.ItemCode,
|
||||
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,
|
||||
SUM(P.Qty1) AS PickingQty1
|
||||
FROM PickingStates P
|
||||
@@ -42,13 +82,13 @@ PICK AS (
|
||||
WHERE P.ItemTypeCode = 1
|
||||
AND LEN(P.ItemCode) = 13
|
||||
GROUP BY
|
||||
P.ItemCode, P.ColorCode, P.ItemDim3Code
|
||||
P.ItemCode, P.ColorCode, P.ItemDim2Code
|
||||
),
|
||||
RESERVE AS (
|
||||
SELECT
|
||||
R.ItemCode,
|
||||
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,
|
||||
SUM(R.Qty1) AS ReserveQty1
|
||||
FROM ReserveStates R
|
||||
@@ -56,13 +96,13 @@ RESERVE AS (
|
||||
WHERE R.ItemTypeCode = 1
|
||||
AND LEN(R.ItemCode) = 13
|
||||
GROUP BY
|
||||
R.ItemCode, R.ColorCode, R.ItemDim3Code
|
||||
R.ItemCode, R.ColorCode, R.ItemDim2Code
|
||||
),
|
||||
DISP AS (
|
||||
SELECT
|
||||
D.ItemCode,
|
||||
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,
|
||||
SUM(D.Qty1) AS DispOrderQty1
|
||||
FROM DispOrderStates D
|
||||
@@ -70,30 +110,28 @@ DISP AS (
|
||||
WHERE D.ItemTypeCode = 1
|
||||
AND LEN(D.ItemCode) = 13
|
||||
GROUP BY
|
||||
D.ItemCode, D.ColorCode, D.ItemDim3Code
|
||||
D.ItemCode, D.ColorCode, D.ItemDim2Code
|
||||
)
|
||||
SELECT
|
||||
S.ItemCode AS ItemCode,
|
||||
S.ColorCode AS ColorCode,
|
||||
S.ItemDim1Code AS ItemDim1Code,
|
||||
S.ItemDim3Code AS ItemDim3Code,
|
||||
V.ItemCode AS ItemCode,
|
||||
V.ColorCode AS ColorCode,
|
||||
V.ItemDim1Code AS ItemDim1Code,
|
||||
V.ItemDim3Code AS ItemDim3Code,
|
||||
CAST(ROUND(
|
||||
S.InventoryQty1
|
||||
ISNULL(S.InventoryQty1,0)
|
||||
- ISNULL(PK.PickingQty1,0)
|
||||
- ISNULL(RS.ReserveQty1,0)
|
||||
- ISNULL(DP.DispOrderQty1,0),
|
||||
2
|
||||
) 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
|
||||
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
|
||||
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
|
||||
ON DP.ItemCode=S.ItemCode AND DP.ColorCode=S.ColorCode AND DP.ItemDim3Code=S.ItemDim3Code
|
||||
WHERE (S.InventoryQty1
|
||||
- ISNULL(PK.PickingQty1,0)
|
||||
- ISNULL(RS.ReserveQty1,0)
|
||||
- ISNULL(DP.DispOrderQty1,0)) <> 0
|
||||
ORDER BY S.ItemCode, S.ColorCode, S.ItemDim3Code;
|
||||
ON DP.ItemCode=V.ItemCode AND DP.ColorCode=V.ColorCode AND DP.ItemDim3Code=V.ItemDim3Code
|
||||
ORDER BY V.ItemCode, V.ColorCode, V.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 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 {
|
||||
@@ -68,7 +77,7 @@ func GetCostingFirstGroupMailMappingLookupsHandler(pg *sql.DB) http.HandlerFunc
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -130,6 +139,11 @@ func GetPricingFirstGroupMailMappingLookupsHandler(pg *sql.DB) http.HandlerFunc
|
||||
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 {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
@@ -140,7 +154,7 @@ func GetCostingFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -263,7 +277,7 @@ func GetPricingFirstGroupMailMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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 pe, ok := err.(*pq.Error); ok {
|
||||
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 {
|
||||
return fmt.Sprintf("%.2f", value)
|
||||
return strings.ReplaceAll(fmt.Sprintf("%.2f", value), ".", ",")
|
||||
}
|
||||
|
||||
func exportPriceFieldTitle(field string) string {
|
||||
|
||||
@@ -384,6 +384,25 @@ WHERE is_active = TRUE
|
||||
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 {
|
||||
Currency string `json:"currency"`
|
||||
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:
|
||||
// - dim1 = color
|
||||
// - dim3 = itemdim3 (optional)
|
||||
// - dim3 = ItemDim2Code/yaka (optional)
|
||||
// Size (ItemDim1Code) is not part of the key here.
|
||||
d1 := int64(0)
|
||||
if id, ok := resolveDimvalFromToken(pgTx, "dimval1", colorCode); ok {
|
||||
d1 = id
|
||||
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 {
|
||||
continue
|
||||
}
|
||||
var d3 sql.NullInt64
|
||||
// dim3 corresponds to mmitem_dim.val3 (ItemDim3Code).
|
||||
if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim3Code); ok {
|
||||
// dim3 corresponds to mmitem_dim.val3.
|
||||
if id, ok := resolveDimvalFromToken(pgTx, "dimval3", dim3Code); ok {
|
||||
d3 = sql.NullInt64{Int64: id, Valid: true}
|
||||
resolvedDim3++
|
||||
}
|
||||
if strings.TrimSpace(dim3Code) != "" && !d3.Valid {
|
||||
continue
|
||||
}
|
||||
key := fmt.Sprintf("%d|%d", d1, func() int64 {
|
||||
if d3.Valid {
|
||||
return d3.Int64
|
||||
@@ -619,16 +635,13 @@ DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
|
||||
if c.Dim1 <= 0 {
|
||||
continue
|
||||
}
|
||||
v2 := int64(0)
|
||||
var v2any any = nil
|
||||
if c.Dim3.Valid && c.Dim3.Int64 > 0 {
|
||||
v2 = c.Dim3.Int64
|
||||
v2any = v2
|
||||
}
|
||||
// If we managed to resolve an "ItemDim3Code" id too, store it in val3 and mark mmdim_id=3.
|
||||
// Active key: val1=color, val3=ItemDim2Code/yaka. val2 is size and is not part of price/campaign key.
|
||||
v3 := int64(0)
|
||||
if extraVal3 != nil {
|
||||
if vv, ok := extraVal3[fmt.Sprintf("%d|%d", c.Dim1, v2)]; ok && vv > 0 {
|
||||
if c.Dim3.Valid && c.Dim3.Int64 > 0 {
|
||||
v3 = c.Dim3.Int64
|
||||
} else if extraVal3 != nil {
|
||||
if vv, ok := extraVal3[fmt.Sprintf("%d|0", c.Dim1)]; ok && vv > 0 {
|
||||
v3 = vv
|
||||
}
|
||||
}
|
||||
@@ -687,13 +700,17 @@ WHERE mmitem_id = $1
|
||||
if !v1.Valid || v1.Int64 <= 0 {
|
||||
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
|
||||
_ = mmdimID
|
||||
_ = v2
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
@@ -1071,18 +1088,41 @@ VALUES (
|
||||
_ = upsertDimCombosCache(code, dims) // best-effort cache fill
|
||||
}
|
||||
|
||||
// 2) Last resort: MSSQL stock tokens, then seed mmitem_dim. Do not use
|
||||
// mk_mmitem_dim_combo as a write source; stale cache rows can create wrong keys.
|
||||
if len(dims) == 0 {
|
||||
d, err := loadDimsFromMssqlStock(code)
|
||||
if err != nil {
|
||||
logger.Error("save:pg:dims:mssql:error", "product_code", code, "err", err)
|
||||
} else {
|
||||
dims = d
|
||||
_ = upsertDimCombosCache(code, dims)
|
||||
// If PG doesn't have mmitem_dim rows for this product yet, try to seed them.
|
||||
ensureMMItemDimRows(mmItemID, dims, nil)
|
||||
// 2) Merge MSSQL variant master combos. PG may be partially populated; missing
|
||||
// colors/dim3 combos still need to be seeded before sdprc/zbggcampaign writes.
|
||||
if d, err := loadDimsFromMssqlStock(code); err != nil {
|
||||
logger.Error("save:pg:dims:mssql:error", "product_code", code, "err", err)
|
||||
} else if len(d) > 0 {
|
||||
if hasDim3Combo(d) {
|
||||
dims = filterDim3Combos(dims)
|
||||
}
|
||||
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
|
||||
// --------------------------------------------------
|
||||
@@ -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 {
|
||||
log.Printf("❌ [UserDetail] commit failed user_id=%d err=%v", userID, err)
|
||||
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_piyasa 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 {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -420,6 +421,31 @@ type wholesaleVariantRow struct {
|
||||
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 {
|
||||
ID int64 `json:"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
|
||||
Dim1 int64
|
||||
Dim3Key int64
|
||||
HasMSSQL bool
|
||||
}
|
||||
|
||||
// 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)
|
||||
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 {
|
||||
rows, err := pg.QueryContext(ctx, `
|
||||
SELECT mmitem_id, mmdim_id, val1, val2, val3
|
||||
@@ -772,13 +813,19 @@ WHERE mmitem_id = ANY($1::bigint[])
|
||||
if !v1.Valid || v1.Int64 <= 0 {
|
||||
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
|
||||
_ = mmdimID
|
||||
_ = v2
|
||||
addCandidate(itemDim1Candidates, itemID, d1)
|
||||
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
|
||||
addCandidate(itemDim3Candidates, itemID, d3k)
|
||||
}
|
||||
|
||||
code := strings.TrimSpace(itemToCode[itemID])
|
||||
@@ -797,21 +844,26 @@ WHERE mmitem_id = ANY($1::bigint[])
|
||||
Dim1: d1,
|
||||
Dim3Key: d3k,
|
||||
}
|
||||
dimIDs = append(dimIDs, d1)
|
||||
dim1IDs = append(dim1IDs, d1)
|
||||
if d3k > 0 {
|
||||
dimIDs = append(dimIDs, d3k)
|
||||
dim3IDs = append(dim3IDs, d3k)
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
|
||||
// Resolve dim ids -> tokens for a readable VariantCode.
|
||||
idToToken := map[int64]string{}
|
||||
if len(dimIDs) > 0 {
|
||||
// Resolve dim ids -> tokens for a fallback readable VariantCode.
|
||||
// MSSQL/Nebim tokens override this below; PG ids are only storage keys.
|
||||
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 := make([]int64, 0, len(dimIDs))
|
||||
seen := make(map[int64]struct{}, len(dimIDs))
|
||||
for _, id := range dimIDs {
|
||||
uniq := make([]int64, 0, len(ids))
|
||||
seen := make(map[int64]struct{}, len(ids))
|
||||
for _, id := range ids {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
@@ -821,35 +873,38 @@ WHERE mmitem_id = ANY($1::bigint[])
|
||||
seen[id] = struct{}{}
|
||||
uniq = append(uniq, id)
|
||||
}
|
||||
if len(uniq) > 0 {
|
||||
rows, err := pg.QueryContext(ctx, `
|
||||
if len(uniq) == 0 {
|
||||
return
|
||||
}
|
||||
rows, err := pg.QueryContext(ctx, `
|
||||
SELECT DISTINCT ON (dim_id) dim_id, token
|
||||
FROM mk_dim_token_map
|
||||
WHERE dim_column = 'dimval1'
|
||||
AND dim_id = ANY($1::bigint[])
|
||||
WHERE dim_column = $1
|
||||
AND dim_id = ANY($2::bigint[])
|
||||
ORDER BY dim_id, updated_at DESC;
|
||||
`, pq.Array(uniq))
|
||||
if err == nil {
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var tok string
|
||||
_ = rows.Scan(&id, &tok)
|
||||
tok = strings.TrimSpace(tok)
|
||||
if tok != "" {
|
||||
idToToken[id] = tok
|
||||
}
|
||||
`, column, pq.Array(uniq))
|
||||
if err == nil {
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var tok string
|
||||
_ = rows.Scan(&id, &tok)
|
||||
tok = strings.TrimSpace(tok)
|
||||
if tok != "" {
|
||||
out[id] = tok
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
}
|
||||
loadReverseTokens("dimval1", dim1IDs, idToDim1Token)
|
||||
loadReverseTokens("dimval3", dim3IDs, idToDim3Token)
|
||||
for k, v := range tmpMap {
|
||||
t1 := strings.TrimSpace(idToToken[v.Dim1])
|
||||
t1 := strings.TrimSpace(idToDim1Token[v.Dim1])
|
||||
if t1 == "" {
|
||||
t1 = fmt.Sprintf("%d", v.Dim1)
|
||||
}
|
||||
if v.Dim3Key > 0 {
|
||||
t3 := strings.TrimSpace(idToToken[v.Dim3Key])
|
||||
t3 := strings.TrimSpace(idToDim3Token[v.Dim3Key])
|
||||
if t3 == "" {
|
||||
t3 = fmt.Sprintf("%d", v.Dim3Key)
|
||||
}
|
||||
@@ -859,6 +914,120 @@ ORDER BY dim_id, updated_at DESC;
|
||||
}
|
||||
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.
|
||||
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)
|
||||
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() {
|
||||
var itemCode, colorCode, dim1Code, dim3Code string
|
||||
var qty sql.NullFloat64
|
||||
if err := msRows.Scan(&itemCode, &colorCode, &dim1Code, &dim3Code, &qty); err != nil {
|
||||
msRows.Close()
|
||||
http.Error(w, "variant stock scan error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -883,40 +1056,95 @@ ORDER BY dim_id, updated_at DESC;
|
||||
if itemID <= 0 {
|
||||
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).
|
||||
// This app uses key: dim1=<color>, dim3=<itemdim3> to match mmitem_dim (val1,val3).
|
||||
inferredDim1 := buildInferredMap("dimval1", colorTokensByItem, itemDim1Candidates)
|
||||
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)
|
||||
if id, ok := resolveDimID("dimval1", colorCode); ok {
|
||||
if id, ok := resolveProductDimID(itemID, "dimval1", colorCode, inferredDim1); ok {
|
||||
d1 = id
|
||||
}
|
||||
if d1 <= 0 {
|
||||
continue
|
||||
}
|
||||
d3k := int64(0)
|
||||
if id, ok := resolveDimID("dimval1", dim3Code); ok {
|
||||
if id, ok := resolveProductDimID(itemID, "dimval3", dim3Code, inferredDim3); ok {
|
||||
d3k = id
|
||||
}
|
||||
if strings.TrimSpace(dim3Code) != "" && d3k <= 0 {
|
||||
continue
|
||||
}
|
||||
key := fmt.Sprintf("%d|%d|%d", itemID, d1, d3k)
|
||||
prev, ok := tmpMap[key]
|
||||
if !ok {
|
||||
// If PG does not have mmitem_dim rows for this item yet, seed it from MSSQL and include it.
|
||||
if !hasMMItemDim[itemID] {
|
||||
var v2 any = nil
|
||||
if d3k > 0 {
|
||||
v2 = d3k
|
||||
}
|
||||
v3 := int64(0)
|
||||
if id, ok := resolveDimID("dimval1", dim3Code); ok {
|
||||
v3 = id
|
||||
}
|
||||
mmdimID := int64(2)
|
||||
var v3any any = nil
|
||||
if v3 > 0 {
|
||||
mmdimID = 3
|
||||
v3any = v3
|
||||
}
|
||||
_, _ = pg.ExecContext(ctx, `
|
||||
// Seed missing MSSQL combos even when the product already has some mmitem_dim rows.
|
||||
// PG remains the storage key source, but MSSQL may reveal new/missing color or dim3 combos.
|
||||
var v2 any = nil
|
||||
if sizeID, ok := resolveDimID("dimval1", dim1Code); ok && sizeID > 0 {
|
||||
v2 = sizeID
|
||||
}
|
||||
v3 := int64(0)
|
||||
if id, ok := resolveProductDimID(itemID, "dimval3", dim3Code, inferredDim3); ok {
|
||||
v3 = id
|
||||
}
|
||||
mmdimID := int64(2)
|
||||
var v3any any = nil
|
||||
if v3 > 0 {
|
||||
mmdimID = 3
|
||||
v3any = v3
|
||||
}
|
||||
_, _ = pg.ExecContext(ctx, `
|
||||
INSERT INTO mmitem_dim (mmitem_id, mmdim_id, val1, val2, val3, is_active, qty)
|
||||
SELECT $1, $2, $3, $4, $5, TRUE, 0
|
||||
WHERE NOT EXISTS (
|
||||
@@ -930,26 +1158,25 @@ WHERE NOT EXISTS (
|
||||
LIMIT 1
|
||||
);
|
||||
`, itemID, mmdimID, d1, v2, v3any)
|
||||
hasMMItemDim[itemID] = true
|
||||
hasMMItemDim[itemID] = true
|
||||
|
||||
code := strings.TrimSpace(itemToCode[itemID])
|
||||
if code != "" {
|
||||
tmpMap[key] = tmpRow{
|
||||
ProductCode: code,
|
||||
VariantCode: "",
|
||||
StockQty: 0,
|
||||
ItemID: itemID,
|
||||
Dim1: d1,
|
||||
Dim3Key: d3k,
|
||||
}
|
||||
// Keep dim token cache for VariantCode formatting.
|
||||
dimIDs = append(dimIDs, d1)
|
||||
if d3k > 0 {
|
||||
dimIDs = append(dimIDs, d3k)
|
||||
}
|
||||
prev = tmpMap[key]
|
||||
ok = true
|
||||
code := strings.TrimSpace(itemToCode[itemID])
|
||||
if code != "" {
|
||||
tmpMap[key] = tmpRow{
|
||||
ProductCode: code,
|
||||
VariantCode: "",
|
||||
StockQty: 0,
|
||||
ItemID: itemID,
|
||||
Dim1: d1,
|
||||
Dim3Key: d3k,
|
||||
}
|
||||
// Keep dim token cache for VariantCode formatting.
|
||||
dim1IDs = append(dim1IDs, d1)
|
||||
if d3k > 0 {
|
||||
dim3IDs = append(dim3IDs, d3k)
|
||||
}
|
||||
prev = tmpMap[key]
|
||||
ok = true
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
@@ -960,6 +1187,10 @@ WHERE NOT EXISTS (
|
||||
q = qty.Float64
|
||||
}
|
||||
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
|
||||
_ = colorCode // display-only
|
||||
}
|
||||
@@ -968,6 +1199,15 @@ WHERE NOT EXISTS (
|
||||
for _, v := range tmpMap {
|
||||
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)
|
||||
type keyRec struct {
|
||||
|
||||
@@ -413,6 +413,16 @@ const menuItems = [
|
||||
label: 'Fiyatlandırma Mail Eşleştirme',
|
||||
to: '/app/pricing-mail-mapping',
|
||||
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,67 +70,80 @@
|
||||
</div>
|
||||
|
||||
<div class="row items-center q-gutter-xs top-actions-row top-actions-row--actions">
|
||||
<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-item clickable @click="selectAllPrices">
|
||||
<q-item-section>Tumunu Sec</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable @click="clearAllPrices">
|
||||
<q-item-section>Tumunu Temizle</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item v-for="option in priceOptions" :key="option.value" clickable @click="togglePriceOption(option.value)">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
dense
|
||||
:model-value="selectedPriceSet.has(option.value)"
|
||||
:disable="pageBusy"
|
||||
@click.stop
|
||||
@update:model-value="() => togglePriceOption(option.value)"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>{{ option.label }}</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
<div class="toolbar-group">
|
||||
<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-item clickable @click="selectAllPrices">
|
||||
<q-item-section>Tumunu Sec</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable @click="clearAllPrices">
|
||||
<q-item-section>Tumunu Temizle</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item v-for="option in allowedPriceOptions" :key="option.value" clickable @click="togglePriceOption(option.value)">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
dense
|
||||
:model-value="selectedPriceSet.has(option.value)"
|
||||
:disable="pageBusy"
|
||||
@click.stop
|
||||
@update:model-value="() => togglePriceOption(option.value)"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>{{ option.label }}</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
color="grey-8"
|
||||
icon="view_sidebar"
|
||||
:label="leftDetailsExpanded ? 'Detaylari Gizle' : 'Detaylari Goster'"
|
||||
:disable="pageBusy"
|
||||
@click="leftDetailsExpanded = !leftDetailsExpanded"
|
||||
/>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
color="grey-8"
|
||||
icon="view_sidebar"
|
||||
:label="leftDetailsExpanded ? 'Detaylari Gizle' : 'Detaylari Goster'"
|
||||
:disable="pageBusy"
|
||||
@click="leftDetailsExpanded = !leftDetailsExpanded"
|
||||
/>
|
||||
<q-toggle
|
||||
v-model="showInStockOnly"
|
||||
dense
|
||||
color="primary"
|
||||
label="Sadece stogu olanlar"
|
||||
:disable="pageBusy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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-item clickable @click="exportVisibleExcel">
|
||||
<q-item-section avatar><q-icon name="grid_on" /></q-item-section>
|
||||
<q-item-section>Excel'e Aktar</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable @click="printVisibleRows">
|
||||
<q-item-section avatar><q-icon name="picture_as_pdf" /></q-item-section>
|
||||
<q-item-section>PDF / Yazdir</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
<div class="toolbar-group">
|
||||
<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-item clickable @click="exportVisibleExcel">
|
||||
<q-item-section avatar><q-icon name="grid_on" /></q-item-section>
|
||||
<q-item-section>Excel'e Aktar</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable @click="printVisibleRows">
|
||||
<q-item-section avatar><q-icon name="picture_as_pdf" /></q-item-section>
|
||||
<q-item-section>PDF / Yazdir</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</div>
|
||||
|
||||
<q-space />
|
||||
|
||||
<q-pagination
|
||||
v-model="currentPage"
|
||||
color="primary"
|
||||
:max="Math.max(1, totalPages || 1)"
|
||||
:max-pages="8"
|
||||
boundary-links
|
||||
direction-links
|
||||
:disable="pageBusy"
|
||||
@update:model-value="onPageChange"
|
||||
/>
|
||||
<div class="text-caption text-grey-8">
|
||||
Sayfa {{ currentPage }} / {{ Math.max(1, totalPages || 1) }} - {{ filteredRows.length }} satir
|
||||
<div class="toolbar-group toolbar-group--paging">
|
||||
<q-pagination
|
||||
v-model="currentPage"
|
||||
color="primary"
|
||||
:max="Math.max(1, totalPages || 1)"
|
||||
:max-pages="8"
|
||||
boundary-links
|
||||
direction-links
|
||||
:disable="pageBusy"
|
||||
@update:model-value="onPageChange"
|
||||
/>
|
||||
<div class="text-caption text-grey-8">
|
||||
Sayfa {{ currentPage }} / {{ Math.max(1, totalPages || 1) }} - {{ filteredRows.length }} satir
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,7 +165,7 @@
|
||||
>
|
||||
<div
|
||||
class="top-x-scroll-inner"
|
||||
:style="{ width: `${tableMinWidth}px` }"
|
||||
:style="{ width: `${tableScrollWidth}px` }"
|
||||
/>
|
||||
</div>
|
||||
<q-table
|
||||
@@ -315,6 +328,137 @@
|
||||
</div>
|
||||
</q-menu>
|
||||
</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>
|
||||
</div>
|
||||
</q-th>
|
||||
@@ -340,10 +484,14 @@
|
||||
<q-img
|
||||
v-if="props.row.imageUrl"
|
||||
:src="props.row.imageUrl"
|
||||
class="product-thumb"
|
||||
class="product-thumb cursor-pointer"
|
||||
fit="cover"
|
||||
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>
|
||||
</template>
|
||||
|
||||
@@ -353,8 +501,11 @@
|
||||
:class="[props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<q-badge v-if="props.row.campaignLabel" color="primary" outline :label="props.row.campaignLabel" />
|
||||
<span v-else class="text-grey-6">-</span>
|
||||
<div class="campaign-cell-content">
|
||||
<span v-if="props.row.campaignLabel" class="campaign-text" :title="props.row.campaignLabel">
|
||||
{{ props.row.campaignLabel }}
|
||||
</span>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
@@ -364,7 +515,9 @@
|
||||
:class="[props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
{{ props.row.campaignRate ? formatPrice(props.row.campaignRate) : '' }}
|
||||
<span class="cell-text campaign-rate-text" :title="String(props.row.campaignRate ?? '')">
|
||||
{{ props.row.campaignRate ? formatPrice(props.row.campaignRate) : '' }}
|
||||
</span>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
@@ -374,11 +527,139 @@
|
||||
:class="['text-right', props.col.classes, { 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }]"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
{{ formatPrice(props.row[name]) }}
|
||||
<span :class="['cell-text', 'price-cell-text', { 'campaign-price-text': name.endsWith('Campaign') }]" :title="formatPrice(props.row[name])">
|
||||
{{ formatPrice(props.row[name]) }}
|
||||
</span>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -390,10 +671,18 @@ import api from 'src/services/api'
|
||||
const PAGE_LIMIT = 250
|
||||
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}`,
|
||||
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 priceColumnNames = campaignPairs.flatMap((p) => [p.base, p.derived])
|
||||
|
||||
@@ -401,9 +690,14 @@ const topUrunIlkGrubu = ref(null)
|
||||
const topUrunAnaGrubu = ref(null)
|
||||
const selectedProductCodes = ref([])
|
||||
const selectedCampaignLabels = ref([])
|
||||
const selectedVariantCodes = 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 showInStockOnly = ref(false)
|
||||
|
||||
const rows = ref([])
|
||||
const loading = ref(false)
|
||||
@@ -418,13 +712,39 @@ const serverFilterLoading = ref({})
|
||||
const serverFilterLastQuery = ref({})
|
||||
const filterSearch = ref({ productCode: '', urunIlkGrubu: '', urunAnaGrubu: '' })
|
||||
const imageCache = new Map()
|
||||
const imageListCache = new Map()
|
||||
const variantCodeCollator = new Intl.Collator('tr', { numeric: true, sensitivity: 'base' })
|
||||
const mainTableRef = ref(null)
|
||||
const topScrollRef = ref(null)
|
||||
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 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 selectedCampaignLabelSet = computed(() => new Set(selectedCampaignLabels.value || []))
|
||||
const selectedVariantCodeSet = computed(() => new Set(selectedVariantCodes.value || []))
|
||||
const pageBusy = computed(() => loading.value || renderPending.value)
|
||||
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)
|
||||
@@ -447,6 +767,21 @@ const filteredCampaignOptions = computed(() => {
|
||||
const list = campaignOptions.value
|
||||
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) {
|
||||
return String(value ?? '').trim()
|
||||
@@ -474,6 +809,59 @@ function formatStock (value) {
|
||||
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) {
|
||||
const row = {
|
||||
id: index + 1,
|
||||
@@ -537,7 +925,7 @@ function buildRows (products, variants) {
|
||||
out.push(row)
|
||||
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) {
|
||||
const d1 = Number(v?.dim1 || 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) {
|
||||
update(() => {
|
||||
filterSearch.value.urunIlkGrubu = toText(val)
|
||||
@@ -651,6 +1053,79 @@ function clearCampaignOptions () {
|
||||
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 () {
|
||||
topUrunAnaGrubu.value = null
|
||||
void fetchServerFilterOptions('urunAnaGrubu', '')
|
||||
@@ -703,7 +1178,7 @@ async function reloadData ({ page = 1 } = {}) {
|
||||
}
|
||||
rows.value = buildRows(products, variants)
|
||||
error.value = ''
|
||||
void loadImagesForRows(rows.value.slice(0, 120))
|
||||
void loadImagesForRows(rows.value)
|
||||
await nextTick()
|
||||
} catch (err) {
|
||||
rows.value = []
|
||||
@@ -724,10 +1199,18 @@ async function loadImagesForRows (list) {
|
||||
seen.add(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)) {
|
||||
row.imageUrl = imageCache.get(key)
|
||||
return
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const res = await api.get('/product-images', {
|
||||
@@ -739,19 +1222,109 @@ async function loadImagesForRows (list) {
|
||||
timeout: 15000
|
||||
})
|
||||
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)
|
||||
row.imageUrl = url
|
||||
imageListCache.set(key, Array.isArray(res?.data) ? res.data : [])
|
||||
} catch {
|
||||
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 () {
|
||||
topUrunIlkGrubu.value = null
|
||||
topUrunAnaGrubu.value = null
|
||||
selectedProductCodes.value = []
|
||||
selectedCampaignLabels.value = []
|
||||
selectedVariantCodes.value = []
|
||||
columnFilters.value = {}
|
||||
columnFilterSearch.value = {}
|
||||
rows.value = []
|
||||
error.value = GUIDANCE_MSG
|
||||
currentPage.value = 1
|
||||
@@ -764,20 +1337,33 @@ function onPageChange (page) {
|
||||
}
|
||||
|
||||
function togglePriceOption (value) {
|
||||
if (!allowedPriceOptions.value.some((x) => x.value === value)) return
|
||||
const set = new Set(selectedPriceOptions.value || [])
|
||||
if (set.has(value)) set.delete(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 () {
|
||||
selectedPriceOptions.value = priceOptions.map((x) => x.value)
|
||||
selectedPriceOptions.value = allowedPriceOptions.value.map((x) => x.value)
|
||||
}
|
||||
|
||||
function clearAllPrices () {
|
||||
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 = {}) {
|
||||
return {
|
||||
name,
|
||||
@@ -793,34 +1379,31 @@ function col (name, label, field, width, extra = {}) {
|
||||
}
|
||||
|
||||
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('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('variantCodes', 'VARYANT', 'variantCodes', 112, { classes: 'ps-col variant-col sticky-col' }),
|
||||
col('variantStocks', 'STOK', 'stockQty', 62, { align: 'right', sortable: true, classes: 'ps-col variant-stock-col sticky-col' }),
|
||||
col('campaignLabel', 'KAMPANYA', 'campaignLabel', 150, { classes: 'ps-col campaign-col sticky-col' }),
|
||||
col('campaignRate', 'IND %', 'campaignRate', 58, { align: 'right', classes: 'ps-col campaign-rate-col sticky-col' }),
|
||||
col('stockEntryDate', 'STOK GIRIS', 'stockEntryDate', 86, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
|
||||
col('lastPricingDate', 'SON FIYAT', 'lastPricingDate', 86, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
|
||||
col('lastCampaignDate', 'SON KAMPANYA', 'lastCampaignDate', 98, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
|
||||
col('askiliYan', 'ASKILI YAN', 'askiliYan', 58, { sortable: true, classes: 'ps-col' }),
|
||||
col('kategori', 'KATEGORI', 'kategori', 58, { sortable: true, classes: 'ps-col' }),
|
||||
col('urunIlkGrubu', 'URUN ILK GRUBU', 'urunIlkGrubu', 70, { sortable: true, classes: 'ps-col' }),
|
||||
col('urunAnaGrubu', 'URUN ANA GRUBU', 'urunAnaGrubu', 74, { sortable: true, classes: 'ps-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' }),
|
||||
col('variantCodes', 'VARYANT', 'variantCodes', 82, { align: 'center', classes: 'ps-col variant-col sticky-col center-col' }),
|
||||
col('variantStocks', 'STOK', 'stockQty', 64, { align: 'center', sortable: true, classes: 'ps-col variant-stock-col sticky-col center-col' }),
|
||||
col('campaignLabel', 'KAMPANYA', 'campaignLabel', 118, { align: 'center', classes: 'ps-col campaign-col sticky-col center-col' }),
|
||||
col('campaignRate', 'IND %', 'campaignRate', 56, { align: 'center', classes: 'ps-col campaign-rate-col sticky-col center-col' }),
|
||||
col('askiliYan', 'ASKILI YAN', 'askiliYan', 58, { sortable: true, classes: 'ps-col center-col' }),
|
||||
col('kategori', 'KATEGORI', 'kategori', 72, { sortable: true, classes: 'ps-col center-col' }),
|
||||
col('urunIlkGrubu', 'URUN ILK GRUBU', 'urunIlkGrubu', 72, { sortable: true, classes: 'ps-col center-col' }),
|
||||
col('urunAnaGrubu', 'URUN ANA GRUBU', 'urunAnaGrubu', 84, { sortable: true, classes: 'ps-col center-col' }),
|
||||
col('urunAltGrubu', 'URUN ALT GRUBU', 'urunAltGrubu', 84, { sortable: true, classes: 'ps-col center-col' }),
|
||||
col('icerik', 'ICERIK', 'icerik', 92, { sortable: true, classes: 'ps-col' }),
|
||||
col('karisim', 'KARISIM', 'karisim', 88, { sortable: true, classes: 'ps-col karisim-wrap-col' }),
|
||||
...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.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([
|
||||
'stockEntryDate',
|
||||
'lastPricingDate',
|
||||
'lastCampaignDate',
|
||||
const compactHiddenColumnNames = new Set([
|
||||
'variantStocks',
|
||||
'campaignLabel',
|
||||
'campaignRate',
|
||||
'askiliYan',
|
||||
'kategori',
|
||||
'urunIlkGrubu',
|
||||
@@ -833,16 +1416,37 @@ const hideableLeftDetailColumnNames = new Set([
|
||||
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]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
|
||||
}))
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const campaignSet = selectedCampaignLabelSet.value
|
||||
if (campaignSet.size === 0) return rows.value || []
|
||||
return (rows.value || []).filter((row) => campaignSet.has(toText(row?.campaignLabel)))
|
||||
const variantSet = selectedVariantCodeSet.value
|
||||
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 tableScrollWidth = computed(() => tableMinWidth.value + stickyScrollComp.value + 48)
|
||||
const tableStyle = computed(() => ({
|
||||
width: `${tableMinWidth.value}px`,
|
||||
minWidth: `${tableMinWidth.value}px`,
|
||||
@@ -850,9 +1454,30 @@ const tableStyle = computed(() => ({
|
||||
}))
|
||||
const stickyColumnNames = computed(() => {
|
||||
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 stickyLeftMap = computed(() => {
|
||||
const map = {}
|
||||
@@ -866,8 +1491,9 @@ const stickyLeftMap = computed(() => {
|
||||
return map
|
||||
})
|
||||
const stickyScrollComp = computed(() => {
|
||||
const boundaryCol = allColumns.find((x) => x.name === stickyBoundaryColumnName)
|
||||
return ((stickyLeftMap.value[stickyBoundaryColumnName] || 0) + extractWidth(boundaryCol?.style)) * 1.2
|
||||
const boundaryName = stickyBoundaryColumnName.value
|
||||
const boundaryCol = allColumns.find((x) => x.name === boundaryName)
|
||||
return ((stickyLeftMap.value[boundaryName] || 0) + extractWidth(boundaryCol?.style)) * 1.2
|
||||
})
|
||||
|
||||
function isStickyCol (name) {
|
||||
@@ -875,7 +1501,7 @@ function isStickyCol (name) {
|
||||
}
|
||||
|
||||
function isStickyBoundary (name) {
|
||||
return name === stickyBoundaryColumnName
|
||||
return name === stickyBoundaryColumnName.value
|
||||
}
|
||||
|
||||
function getHeaderCellStyle (col) {
|
||||
@@ -903,13 +1529,30 @@ function exportCell (row, col) {
|
||||
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 () {
|
||||
const cols = visibleColumns.value
|
||||
const cols = visibleColumns.value.filter((c) => c.name !== 'image')
|
||||
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 `<td>${escapeHtml(exportCell(row, c))}</td>`
|
||||
return exportExcelCellHtml(row, c)
|
||||
}).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 url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
@@ -919,13 +1562,14 @@ function exportVisibleExcel () {
|
||||
a.click()
|
||||
a.remove()
|
||||
URL.revokeObjectURL(url)
|
||||
void notifyExportTaken('excel')
|
||||
}
|
||||
|
||||
function printVisibleRows () {
|
||||
const cols = visibleColumns.value
|
||||
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>`
|
||||
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('')
|
||||
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Fiyat Listesi</title><style>
|
||||
@page { size: A3 landscape; margin: 8mm; }
|
||||
@@ -935,13 +1579,31 @@ function printVisibleRows () {
|
||||
th { background: #957116; color: #fff; }
|
||||
th, td { border: 1px solid #ccc; padding: 3px; vertical-align: middle; }
|
||||
.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>`
|
||||
const win = window.open('', '_blank')
|
||||
if (!win) return
|
||||
win.document.open()
|
||||
win.document.write(html)
|
||||
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 () {
|
||||
@@ -992,7 +1654,12 @@ watch([tableMinWidth, rows], async () => {
|
||||
bindTableScrollSync()
|
||||
})
|
||||
|
||||
watch(allowedPriceOptions, () => {
|
||||
normalizeSelectedPriceOptions()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void fetchMyPriceGroups()
|
||||
void fetchServerFilterOptions('urunIlkGrubu', '')
|
||||
void fetchServerFilterOptions('urunAnaGrubu', '')
|
||||
void fetchServerFilterOptions('productCode', '')
|
||||
@@ -1006,8 +1673,8 @@ onMounted(() => {
|
||||
height: calc(100vh - 58px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--pricing-row-height: 31px;
|
||||
--pricing-header-height: 72px;
|
||||
--pricing-row-height: 108px;
|
||||
--pricing-header-height: 88px;
|
||||
--pricing-table-height: calc(100vh - 156px);
|
||||
}
|
||||
|
||||
@@ -1015,12 +1682,59 @@ onMounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
align-items: flex-end;
|
||||
align-items: flex-start;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.top-actions-row {
|
||||
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 {
|
||||
@@ -1032,30 +1746,58 @@ onMounted(() => {
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--top-scroll-height: 22px;
|
||||
}
|
||||
|
||||
.product-thumb {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 4px;
|
||||
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 {
|
||||
padding: 2px 4px !important;
|
||||
padding: 4px !important;
|
||||
}
|
||||
|
||||
.top-x-scroll {
|
||||
flex: 0 0 14px;
|
||||
height: 14px;
|
||||
flex: 0 0 var(--top-scroll-height);
|
||||
height: var(--top-scroll-height);
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.14);
|
||||
scrollbar-gutter: stable;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -1064,9 +1806,9 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.pricing-table :deep(.q-table__middle) {
|
||||
height: calc(var(--pricing-table-height) - 14px);
|
||||
min-height: calc(var(--pricing-table-height) - 14px);
|
||||
max-height: calc(var(--pricing-table-height) - 14px);
|
||||
height: calc(var(--pricing-table-height) - var(--top-scroll-height));
|
||||
min-height: calc(var(--pricing-table-height) - var(--top-scroll-height));
|
||||
max-height: calc(var(--pricing-table-height) - var(--top-scroll-height));
|
||||
overflow: auto !important;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
overscroll-behavior: contain;
|
||||
@@ -1074,6 +1816,19 @@ onMounted(() => {
|
||||
|
||||
.pricing-table :deep(.q-table) {
|
||||
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),
|
||||
@@ -1081,11 +1836,25 @@ onMounted(() => {
|
||||
height: var(--pricing-row-height) !important;
|
||||
min-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;
|
||||
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(.q-table thead tr),
|
||||
.pricing-table :deep(.q-table thead tr.header-row-fixed),
|
||||
@@ -1099,9 +1868,10 @@ onMounted(() => {
|
||||
.pricing-table :deep(th) {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
white-space: nowrap;
|
||||
word-break: normal;
|
||||
text-overflow: ellipsis;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
@@ -1156,8 +1926,90 @@ onMounted(() => {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.order-price-list-table :deep(.campaign-price-col) {
|
||||
background: #f6fbf7;
|
||||
.pricing-table :deep(td.ps-col),
|
||||
.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 {
|
||||
@@ -1175,15 +2027,66 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
font-weight: 800;
|
||||
line-height: 1.15;
|
||||
line-height: 1.12;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-line-clamp: 4;
|
||||
-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 {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@@ -1259,4 +2162,325 @@ onMounted(() => {
|
||||
padding: 16px 20px;
|
||||
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>
|
||||
|
||||
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-list>
|
||||
</q-btn-dropdown>
|
||||
<q-toggle
|
||||
v-model="showInStockOnly"
|
||||
dense
|
||||
color="primary"
|
||||
label="Sadece stogu olanlar"
|
||||
:disable="pageBusy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
@@ -1245,6 +1252,7 @@ const selectedCurrencies = ref(['USD', 'EUR', 'TRY'])
|
||||
const exportAllLoading = ref(false)
|
||||
const showSelectedOnly = ref(false)
|
||||
const leftDetailsExpanded = ref(true)
|
||||
const showInStockOnly = ref(false)
|
||||
const calcLoadingMap = ref({})
|
||||
const bulkCalcLoading = ref(false)
|
||||
const saving = ref(false)
|
||||
@@ -1528,6 +1536,7 @@ function rowSelectionKey (row) {
|
||||
const filteredRows = computed(() => {
|
||||
return rows.value.filter((row) => {
|
||||
if (showSelectedOnly.value && !selectedMap.value[rowSelectionKey(row)]) return false
|
||||
if (showInStockOnly.value && Number(row?.stockQty ?? 0) <= 0) return false
|
||||
for (const { field } of multiFilterColumns) {
|
||||
// Server-backed filters already reload full dataset (all pages) from backend.
|
||||
// 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
|
||||
}
|
||||
|
||||
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) {
|
||||
if (field === 'stockQty') return formatStock(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()
|
||||
}
|
||||
|
||||
|
||||
@@ -277,6 +277,34 @@
|
||||
/>
|
||||
</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>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -320,6 +348,7 @@ const {
|
||||
departmentOptions,
|
||||
piyasaOptions,
|
||||
nebimUserOptions,
|
||||
orderPriceListPriceGroupOptions,
|
||||
sendingPasswordMail,
|
||||
lastPasswordMailSentAt
|
||||
} = storeToRefs(store)
|
||||
@@ -373,6 +402,16 @@ function clearPiyasalar () {
|
||||
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 ================= */
|
||||
watch(
|
||||
() => userId.value,
|
||||
|
||||
@@ -106,6 +106,13 @@
|
||||
</q-item>
|
||||
</q-list>
|
||||
</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">
|
||||
<q-chip
|
||||
@@ -884,6 +891,7 @@ const currentPage = ref(1)
|
||||
let reloadTimer = null
|
||||
const variantRows = ref([])
|
||||
const variantRowsCache = new Map()
|
||||
const variantCodeCollator = new Intl.Collator('tr', { numeric: true, sensitivity: 'base' })
|
||||
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."
|
||||
@@ -1301,6 +1309,7 @@ const selectedPriceOptions = ref(priceOptionOptions.map((x) => x.value))
|
||||
const exportAllLoading = ref(false)
|
||||
const showSelectedOnly = ref(false)
|
||||
const leftDetailsExpanded = ref(true)
|
||||
const showInStockOnly = ref(false)
|
||||
// Keep the old calc state around (not used on this screen) to avoid touching shared helpers.
|
||||
const calcLoadingMap = ref({})
|
||||
|
||||
@@ -1601,6 +1610,7 @@ function rowSelectionKey (row) {
|
||||
const filteredRows = computed(() => {
|
||||
return rows.value.filter((row) => {
|
||||
if (showSelectedOnly.value && !selectedMap.value[rowSelectionKey(row)]) return false
|
||||
if (showInStockOnly.value && Number(row?.stockQty ?? 0) <= 0) return false
|
||||
for (const { field } of multiFilterColumns) {
|
||||
// Server-backed filters already reload full dataset (all pages) from backend.
|
||||
// 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
|
||||
}
|
||||
|
||||
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) {
|
||||
if (field === 'stockQty') return formatStock(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()
|
||||
}
|
||||
|
||||
@@ -2678,7 +2705,7 @@ async function buildVariantRowsForProductPage (baseProductRows = []) {
|
||||
})
|
||||
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) {
|
||||
const d1 = Number(v?.dim1 || 0)
|
||||
const d3 = v?.dim3 == null ? null : Number(v?.dim3 || 0)
|
||||
|
||||
@@ -251,6 +251,18 @@ const routes = [
|
||||
component: () => import('../pages/PricingMailMapping.vue'),
|
||||
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',
|
||||
name: 'translation-table',
|
||||
|
||||
@@ -26,14 +26,16 @@ export const useUserDetailStore = defineStore('userDetail', {
|
||||
roles: [],
|
||||
departments: null,
|
||||
piyasalar: [],
|
||||
nebim_users: null
|
||||
nebim_users: null,
|
||||
order_price_list_price_groups: []
|
||||
},
|
||||
|
||||
/* ================= LOOKUPS ================= */
|
||||
roleOptions: [],
|
||||
departmentOptions: [],
|
||||
piyasaOptions: [],
|
||||
nebimUserOptions: []
|
||||
nebimUserOptions: [],
|
||||
orderPriceListPriceGroupOptions: []
|
||||
}),
|
||||
|
||||
actions: {
|
||||
@@ -52,7 +54,8 @@ export const useUserDetailStore = defineStore('userDetail', {
|
||||
roles: [],
|
||||
departments: null,
|
||||
piyasalar: [],
|
||||
nebim_users: null
|
||||
nebim_users: null,
|
||||
order_price_list_price_groups: []
|
||||
}
|
||||
this.error = null
|
||||
this.hasPassword = false
|
||||
@@ -113,6 +116,7 @@ export const useUserDetailStore = defineStore('userDetail', {
|
||||
departments: departmentCodes.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 => {
|
||||
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.piyasalar = (data.piyasalar || []).map(x => x.code)
|
||||
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
|
||||
} catch (e) {
|
||||
@@ -237,17 +242,22 @@ export const useUserDetailStore = defineStore('userDetail', {
|
||||
===================================================== */
|
||||
async fetchLookups () {
|
||||
// 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/departments'),
|
||||
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.departmentOptions = depts?.data || depts || []
|
||||
this.piyasaOptions = piyasalar?.data || piyasalar || []
|
||||
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