Merge remote-tracking branch 'origin/master'
This commit is contained in:
15
svc/main.go
15
svc/main.go
@@ -579,6 +579,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"finance", "export",
|
"finance", "export",
|
||||||
wrapV3(routes.ExportPDFHandler(mssql)),
|
wrapV3(routes.ExportPDFHandler(mssql)),
|
||||||
)
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/export-pdf", "POST",
|
||||||
|
"finance", "export",
|
||||||
|
wrapV3(routes.ExportPDFHandler(mssql)),
|
||||||
|
)
|
||||||
|
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/exportstamentheaderreport-pdf", "GET",
|
"/api/exportstamentheaderreport-pdf", "GET",
|
||||||
@@ -795,6 +800,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"pricing", "view",
|
"pricing", "view",
|
||||||
wrapV3(http.HandlerFunc(routes.GetProductPricingFilterOptionsHandler)),
|
wrapV3(http.HandlerFunc(routes.GetProductPricingFilterOptionsHandler)),
|
||||||
)
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/pricing/products/export-all", "GET",
|
||||||
|
"pricing", "view",
|
||||||
|
wrapV3(http.HandlerFunc(routes.ExportAllProductPricingHandler)),
|
||||||
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/pricing/brand-classification/lookups", "GET",
|
"/api/pricing/brand-classification/lookups", "GET",
|
||||||
"pricing", "view",
|
"pricing", "view",
|
||||||
@@ -840,6 +850,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"pricing", "view",
|
"pricing", "view",
|
||||||
wrapV3(routes.GetPricingParameterRulesHandler(pgDB)),
|
wrapV3(routes.GetPricingParameterRulesHandler(pgDB)),
|
||||||
)
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/pricing/pricing-rules/export-all", "GET",
|
||||||
|
"pricing", "view",
|
||||||
|
wrapV3(routes.ExportPricingRulesHandler(pgDB)),
|
||||||
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/pricing/production-product-costing/no-cost-products", "GET",
|
"/api/pricing/production-product-costing/no-cost-products", "GET",
|
||||||
"costing", "view",
|
"costing", "view",
|
||||||
|
|||||||
@@ -36,6 +36,583 @@ type ProductPricingPage struct {
|
|||||||
Limit int
|
Limit int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetAllProductPricingRows(ctx context.Context, chunkSize int, filters ProductPricingFilters, sortBy string, descending bool) ([]models.ProductPricing, error) {
|
||||||
|
if chunkSize <= 0 || chunkSize > 1000 {
|
||||||
|
chunkSize = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
paramIndex := 1
|
||||||
|
args := make([]any, 0, 64)
|
||||||
|
nextParam := func() string {
|
||||||
|
name := "@p" + strconv.Itoa(paramIndex)
|
||||||
|
paramIndex++
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
whereParts := []string{
|
||||||
|
"ProductAtt42 IN ('SERI', 'AKSESUAR')",
|
||||||
|
"IsBlocked = 0",
|
||||||
|
"LEN(LTRIM(RTRIM(ProductCode))) = 13",
|
||||||
|
}
|
||||||
|
addInFilter := func(expr string, values []string) {
|
||||||
|
clean := make([]string, 0, len(values))
|
||||||
|
for _, v := range values {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clean = append(clean, v)
|
||||||
|
}
|
||||||
|
if len(clean) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ors := make([]string, 0, len(clean))
|
||||||
|
for _, v := range clean {
|
||||||
|
p := nextParam()
|
||||||
|
ors = append(ors, expr+" = "+p)
|
||||||
|
args = append(args, v)
|
||||||
|
}
|
||||||
|
whereParts = append(whereParts, "("+strings.Join(ors, " OR ")+")")
|
||||||
|
}
|
||||||
|
brandGroupExpr := `CASE ABS(CHECKSUM(LTRIM(RTRIM(ProductCode)))) % 3
|
||||||
|
WHEN 0 THEN 'MARKA GRUBU A'
|
||||||
|
WHEN 1 THEN 'MARKA GRUBU B'
|
||||||
|
ELSE 'MARKA GRUBU C'
|
||||||
|
END`
|
||||||
|
addInFilter("LTRIM(RTRIM(ProductCode))", filters.ProductCode)
|
||||||
|
addInFilter(brandGroupExpr, filters.BrandGroup)
|
||||||
|
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '')", filters.AskiliYan)
|
||||||
|
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '')", filters.Kategori)
|
||||||
|
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '')", filters.UrunIlkGrubu)
|
||||||
|
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '')", filters.UrunAnaGrubu)
|
||||||
|
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '')", filters.UrunAltGrubu)
|
||||||
|
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '')", filters.Icerik)
|
||||||
|
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '')", filters.Karisim)
|
||||||
|
addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '')", filters.Marka)
|
||||||
|
if q := strings.TrimSpace(filters.Search); q != "" {
|
||||||
|
p := nextParam()
|
||||||
|
args = append(args, "%"+q+"%")
|
||||||
|
whereParts = append(whereParts, "("+strings.Join([]string{
|
||||||
|
"LTRIM(RTRIM(ProductCode)) LIKE " + p,
|
||||||
|
"COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') LIKE " + p,
|
||||||
|
"COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') LIKE " + p,
|
||||||
|
"COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') LIKE " + p,
|
||||||
|
"COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '') LIKE " + p,
|
||||||
|
"COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') LIKE " + p,
|
||||||
|
"COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') LIKE " + p,
|
||||||
|
"COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') LIKE " + p,
|
||||||
|
"COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') LIKE " + p,
|
||||||
|
}, " OR ")+")")
|
||||||
|
}
|
||||||
|
whereSQL := strings.Join(whereParts, " AND ")
|
||||||
|
|
||||||
|
sortBy = strings.TrimSpace(sortBy)
|
||||||
|
orderDir := "DESC"
|
||||||
|
if !descending {
|
||||||
|
orderDir = "ASC"
|
||||||
|
}
|
||||||
|
orderExpr := "CAST(ROUND(ISNULL(sb.InventoryQty1, 0) - ISNULL(pb.PickingQty1, 0) - ISNULL(rb.ReserveQty1, 0) - ISNULL(db.DispOrderQty1, 0), 2) AS DECIMAL(18, 2))"
|
||||||
|
if sortBy == "productCode" {
|
||||||
|
orderExpr = "rc.ProductCode"
|
||||||
|
orderDir = "ASC"
|
||||||
|
}
|
||||||
|
|
||||||
|
baseQuery := `
|
||||||
|
IF OBJECT_ID('tempdb..#req_codes') IS NOT NULL DROP TABLE #req_codes;
|
||||||
|
IF OBJECT_ID('tempdb..#stock_base') IS NOT NULL DROP TABLE #stock_base;
|
||||||
|
IF OBJECT_ID('tempdb..#pick_base') IS NOT NULL DROP TABLE #pick_base;
|
||||||
|
IF OBJECT_ID('tempdb..#reserve_base') IS NOT NULL DROP TABLE #reserve_base;
|
||||||
|
IF OBJECT_ID('tempdb..#disp_base') IS NOT NULL DROP TABLE #disp_base;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
f.ProductCode,
|
||||||
|
MAX(f.BrandGroupSec) AS BrandGroupSec,
|
||||||
|
MAX(f.AskiliYan) AS AskiliYan,
|
||||||
|
MAX(f.Kategori) AS Kategori,
|
||||||
|
MAX(f.UrunIlkGrubu) AS UrunIlkGrubu,
|
||||||
|
MAX(f.UrunAnaGrubu) AS UrunAnaGrubu,
|
||||||
|
MAX(f.UrunAltGrubu) AS UrunAltGrubu,
|
||||||
|
MAX(f.Icerik) AS Icerik,
|
||||||
|
MAX(f.Karisim) AS Karisim,
|
||||||
|
MAX(f.Marka) AS Marka,
|
||||||
|
MAX(f.BrandCode) AS BrandCode
|
||||||
|
INTO #req_codes
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(ProductCode)) AS ProductCode,
|
||||||
|
` + brandGroupExpr + ` AS BrandGroupSec,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') AS AskiliYan,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') AS Kategori,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') AS UrunIlkGrubu,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '') AS UrunAnaGrubu,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') AS UrunAltGrubu,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') AS Icerik,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') AS Karisim,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') AS Marka,
|
||||||
|
COALESCE(LTRIM(RTRIM(ProductAtt10)), '') AS BrandCode
|
||||||
|
FROM ProductFilterWithDescription('TR')
|
||||||
|
WHERE ` + whereSQL + `
|
||||||
|
) f
|
||||||
|
GROUP BY f.ProductCode;
|
||||||
|
|
||||||
|
CREATE CLUSTERED INDEX IX_req_codes_ProductCode ON #req_codes(ProductCode);
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(s.ItemCode)) AS ItemCode,
|
||||||
|
SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1
|
||||||
|
INTO #stock_base
|
||||||
|
FROM trStock s WITH(NOLOCK)
|
||||||
|
INNER JOIN #req_codes rc
|
||||||
|
ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode))
|
||||||
|
WHERE s.ItemTypeCode = 1
|
||||||
|
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
|
||||||
|
GROUP BY LTRIM(RTRIM(s.ItemCode));
|
||||||
|
|
||||||
|
CREATE CLUSTERED INDEX IX_stock_base_ItemCode ON #stock_base(ItemCode);
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(p.ItemCode)) AS ItemCode,
|
||||||
|
SUM(p.Qty1) AS PickingQty1
|
||||||
|
INTO #pick_base
|
||||||
|
FROM PickingStates p
|
||||||
|
INNER JOIN #req_codes rc
|
||||||
|
ON rc.ProductCode = LTRIM(RTRIM(p.ItemCode))
|
||||||
|
WHERE p.ItemTypeCode = 1
|
||||||
|
AND LEN(LTRIM(RTRIM(p.ItemCode))) = 13
|
||||||
|
GROUP BY LTRIM(RTRIM(p.ItemCode));
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(r.ItemCode)) AS ItemCode,
|
||||||
|
SUM(r.Qty1) AS ReserveQty1
|
||||||
|
INTO #reserve_base
|
||||||
|
FROM ReserveStates r
|
||||||
|
INNER JOIN #req_codes rc
|
||||||
|
ON rc.ProductCode = LTRIM(RTRIM(r.ItemCode))
|
||||||
|
WHERE r.ItemTypeCode = 1
|
||||||
|
AND LEN(LTRIM(RTRIM(r.ItemCode))) = 13
|
||||||
|
GROUP BY LTRIM(RTRIM(r.ItemCode));
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(d.ItemCode)) AS ItemCode,
|
||||||
|
SUM(d.Qty1) AS DispOrderQty1
|
||||||
|
INTO #disp_base
|
||||||
|
FROM DispOrderStates d
|
||||||
|
INNER JOIN #req_codes rc
|
||||||
|
ON rc.ProductCode = LTRIM(RTRIM(d.ItemCode))
|
||||||
|
WHERE d.ItemTypeCode = 1
|
||||||
|
AND LEN(LTRIM(RTRIM(d.ItemCode))) = 13
|
||||||
|
GROUP BY LTRIM(RTRIM(d.ItemCode));
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
rc.ProductCode,
|
||||||
|
rc.BrandGroupSec,
|
||||||
|
rc.AskiliYan,
|
||||||
|
rc.Kategori,
|
||||||
|
rc.UrunIlkGrubu,
|
||||||
|
rc.UrunAnaGrubu,
|
||||||
|
rc.UrunAltGrubu,
|
||||||
|
rc.Icerik,
|
||||||
|
rc.Karisim,
|
||||||
|
rc.Marka,
|
||||||
|
rc.BrandCode,
|
||||||
|
CAST(ROUND(
|
||||||
|
ISNULL(sb.InventoryQty1, 0)
|
||||||
|
- ISNULL(pb.PickingQty1, 0)
|
||||||
|
- ISNULL(rb.ReserveQty1, 0)
|
||||||
|
- ISNULL(db.DispOrderQty1, 0)
|
||||||
|
, 2) AS DECIMAL(18, 2)) AS StockQty
|
||||||
|
FROM #req_codes rc
|
||||||
|
LEFT JOIN #stock_base sb
|
||||||
|
ON sb.ItemCode = rc.ProductCode
|
||||||
|
LEFT JOIN #pick_base pb
|
||||||
|
ON pb.ItemCode = rc.ProductCode
|
||||||
|
LEFT JOIN #reserve_base rb
|
||||||
|
ON rb.ItemCode = rc.ProductCode
|
||||||
|
LEFT JOIN #disp_base db
|
||||||
|
ON db.ItemCode = rc.ProductCode
|
||||||
|
ORDER BY
|
||||||
|
` + orderExpr + ` ` + orderDir + `,
|
||||||
|
rc.ProductCode ASC;
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := db.MssqlDB.QueryContext(ctx, baseQuery, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
out := make([]models.ProductPricing, 0, 2048)
|
||||||
|
for rows.Next() {
|
||||||
|
var item models.ProductPricing
|
||||||
|
if err := rows.Scan(
|
||||||
|
&item.ProductCode,
|
||||||
|
&item.BrandGroupSec,
|
||||||
|
&item.AskiliYan,
|
||||||
|
&item.Kategori,
|
||||||
|
&item.UrunIlkGrubu,
|
||||||
|
&item.UrunAnaGrubu,
|
||||||
|
&item.UrunAltGrubu,
|
||||||
|
&item.Icerik,
|
||||||
|
&item.Karisim,
|
||||||
|
&item.Marka,
|
||||||
|
&item.BrandCode,
|
||||||
|
&item.StockQty,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, item)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := enrichAllProductPricingRows(ctx, out, chunkSize); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func enrichAllProductPricingRows(ctx context.Context, out []models.ProductPricing, chunkSize int) error {
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if chunkSize <= 0 || chunkSize > 1000 {
|
||||||
|
chunkSize = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
indexByCode := make(map[string]int, len(out))
|
||||||
|
codes := make([]string, 0, len(out))
|
||||||
|
for i := range out {
|
||||||
|
code := strings.TrimSpace(out[i].ProductCode)
|
||||||
|
if code == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
indexByCode[code] = i
|
||||||
|
codes = append(codes, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, chunk := range chunkStringSlice(codes, chunkSize) {
|
||||||
|
valueRows := make([]string, 0, len(chunk))
|
||||||
|
metricArgs := make([]any, 0, len(chunk))
|
||||||
|
for i, code := range chunk {
|
||||||
|
paramName := "@p" + strconv.Itoa(i+1)
|
||||||
|
valueRows = append(valueRows, "("+paramName+")")
|
||||||
|
metricArgs = append(metricArgs, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
metricsQuery := `
|
||||||
|
WITH req_codes AS (
|
||||||
|
SELECT DISTINCT LTRIM(RTRIM(v.ProductCode)) AS ProductCode
|
||||||
|
FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(ProductCode)
|
||||||
|
WHERE LEN(LTRIM(RTRIM(v.ProductCode))) > 0
|
||||||
|
),
|
||||||
|
latest_pricelist_line AS (
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(p.ItemCode)) AS ItemCode,
|
||||||
|
LTRIM(RTRIM(p.DocCurrencyCode)) AS DocCurrencyCode,
|
||||||
|
CAST(p.Price AS DECIMAL(18, 2)) AS Price,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY LTRIM(RTRIM(p.ItemCode)), LTRIM(RTRIM(p.DocCurrencyCode))
|
||||||
|
ORDER BY p.ValidDate DESC, p.ValidTime DESC, p.LastUpdatedDate DESC
|
||||||
|
) AS rn
|
||||||
|
FROM dbo.trPriceListLine p WITH(NOLOCK)
|
||||||
|
INNER JOIN req_codes rc
|
||||||
|
ON rc.ProductCode = LTRIM(RTRIM(p.ItemCode))
|
||||||
|
WHERE p.ItemTypeCode = 1
|
||||||
|
AND ISNULL(p.IsDisabled, 0) = 0
|
||||||
|
AND LTRIM(RTRIM(p.DocCurrencyCode)) IN ('USD', 'TRY')
|
||||||
|
AND (
|
||||||
|
(LTRIM(RTRIM(p.DocCurrencyCode)) = 'USD' AND LTRIM(RTRIM(p.PriceGroupCode)) = 'TM-USD')
|
||||||
|
OR (LTRIM(RTRIM(p.DocCurrencyCode)) = 'TRY' AND LTRIM(RTRIM(p.PriceGroupCode)) = 'TM-TRY')
|
||||||
|
)
|
||||||
|
AND p.Price IS NOT NULL
|
||||||
|
AND p.Price > 0
|
||||||
|
),
|
||||||
|
base_prices AS (
|
||||||
|
SELECT
|
||||||
|
ItemCode,
|
||||||
|
MAX(CASE WHEN DocCurrencyCode = 'USD' THEN Price END) AS BasePriceUsd,
|
||||||
|
MAX(CASE WHEN DocCurrencyCode = 'TRY' THEN Price END) AS BasePriceTry
|
||||||
|
FROM latest_pricelist_line
|
||||||
|
WHERE rn = 1
|
||||||
|
GROUP BY ItemCode
|
||||||
|
),
|
||||||
|
latest_base_price AS (
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(b.ItemCode)) AS ItemCode,
|
||||||
|
CAST(b.Price AS DECIMAL(18, 2)) AS CostPrice,
|
||||||
|
CONVERT(VARCHAR(10), b.PriceDate, 23) AS LastPricingDate,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY LTRIM(RTRIM(b.ItemCode))
|
||||||
|
ORDER BY b.PriceDate DESC, b.LastUpdatedDate DESC
|
||||||
|
) AS rn
|
||||||
|
FROM prItemBasePrice b
|
||||||
|
INNER JOIN req_codes rc
|
||||||
|
ON rc.ProductCode = LTRIM(RTRIM(b.ItemCode))
|
||||||
|
WHERE b.ItemTypeCode = 1
|
||||||
|
AND b.BasePriceCode = 1
|
||||||
|
AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD'
|
||||||
|
),
|
||||||
|
stock_entry_dates AS (
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(s.ItemCode)) AS ItemCode,
|
||||||
|
CONVERT(VARCHAR(10), MAX(s.OperationDate), 23) AS StockEntryDate
|
||||||
|
FROM trStock s WITH(NOLOCK)
|
||||||
|
INNER JOIN req_codes rc
|
||||||
|
ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode))
|
||||||
|
WHERE s.ItemTypeCode = 1
|
||||||
|
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
|
||||||
|
AND s.In_Qty1 > 0
|
||||||
|
AND LTRIM(RTRIM(s.InnerProcessCode)) = 'OP'
|
||||||
|
AND LTRIM(RTRIM(s.WarehouseCode)) IN (
|
||||||
|
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
|
||||||
|
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
|
||||||
|
'1-0-33','101','1-014','1-0-49','1-0-36'
|
||||||
|
)
|
||||||
|
GROUP BY LTRIM(RTRIM(s.ItemCode))
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
rc.ProductCode,
|
||||||
|
COALESCE(lp.CostPrice, 0) AS CostPrice,
|
||||||
|
COALESCE(bp.BasePriceUsd, 0) AS BasePriceUsd,
|
||||||
|
COALESCE(bp.BasePriceTry, 0) AS BasePriceTry,
|
||||||
|
COALESCE(se.StockEntryDate, '') AS StockEntryDate,
|
||||||
|
COALESCE(lp.LastPricingDate, '') AS LastPricingDate
|
||||||
|
FROM req_codes rc
|
||||||
|
LEFT JOIN latest_base_price lp
|
||||||
|
ON lp.ItemCode = rc.ProductCode
|
||||||
|
AND lp.rn = 1
|
||||||
|
LEFT JOIN base_prices bp
|
||||||
|
ON bp.ItemCode = rc.ProductCode
|
||||||
|
LEFT JOIN stock_entry_dates se
|
||||||
|
ON se.ItemCode = rc.ProductCode;
|
||||||
|
`
|
||||||
|
|
||||||
|
metricRows, err := db.MssqlDB.QueryContext(ctx, metricsQuery, metricArgs...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("metrics query failed: %w", err)
|
||||||
|
}
|
||||||
|
for metricRows.Next() {
|
||||||
|
var code string
|
||||||
|
var costPrice, basePriceUsd, basePriceTry float64
|
||||||
|
var stockEntryDate, lastPricingDate string
|
||||||
|
if err := metricRows.Scan(&code, &costPrice, &basePriceUsd, &basePriceTry, &stockEntryDate, &lastPricingDate); err != nil {
|
||||||
|
_ = metricRows.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if idx, ok := indexByCode[strings.TrimSpace(code)]; ok {
|
||||||
|
out[idx].CostPrice = costPrice
|
||||||
|
out[idx].BasePriceUsd = basePriceUsd
|
||||||
|
out[idx].BasePriceTry = basePriceTry
|
||||||
|
out[idx].StockEntryDate = stockEntryDate
|
||||||
|
out[idx].LastPricingDate = lastPricingDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := metricRows.Err(); err != nil {
|
||||||
|
_ = metricRows.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = metricRows.Close()
|
||||||
|
|
||||||
|
if uretimDB := db.GetUretimDB(); uretimDB != nil {
|
||||||
|
costingQuery := `
|
||||||
|
WITH req_codes AS (
|
||||||
|
SELECT DISTINCT LTRIM(RTRIM(v.ProductCode)) AS ProductCode
|
||||||
|
FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(ProductCode)
|
||||||
|
WHERE LEN(LTRIM(RTRIM(v.ProductCode))) > 0
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
LTRIM(RTRIM(m.UrunKodu)) AS UrunKodu,
|
||||||
|
CONVERT(VARCHAR(10), MAX(m.Tarihi), 23) AS LastCostingDate
|
||||||
|
FROM dbo.spUrtOnMLMas m WITH(NOLOCK)
|
||||||
|
INNER JOIN req_codes rc
|
||||||
|
ON rc.ProductCode = LTRIM(RTRIM(m.UrunKodu))
|
||||||
|
GROUP BY LTRIM(RTRIM(m.UrunKodu));
|
||||||
|
`
|
||||||
|
costRows, err := uretimDB.QueryContext(ctx, costingQuery, metricArgs...)
|
||||||
|
if err == nil {
|
||||||
|
for costRows.Next() {
|
||||||
|
var code, d string
|
||||||
|
if err := costRows.Scan(&code, &d); err != nil {
|
||||||
|
_ = costRows.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if idx, ok := indexByCode[strings.TrimSpace(code)]; ok && strings.TrimSpace(d) != "" {
|
||||||
|
out[idx].LastCostingDate = strings.TrimSpace(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := costRows.Err(); err != nil {
|
||||||
|
_ = costRows.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = costRows.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pg := db.PgDB; pg != nil {
|
||||||
|
tierSQL := `
|
||||||
|
WITH ranked AS (
|
||||||
|
SELECT
|
||||||
|
mmitem.code AS code,
|
||||||
|
sdprc.sdprcgrp_id AS grp,
|
||||||
|
sdprc.crn AS crn,
|
||||||
|
COALESCE(sdprc.prc, 0) AS prc,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY mmitem.code, sdprc.crn, sdprc.sdprcgrp_id
|
||||||
|
ORDER BY sdprc.zlins_dttm DESC
|
||||||
|
) AS rn
|
||||||
|
FROM sdprc
|
||||||
|
JOIN mmitem ON mmitem.id = sdprc.mmitem_id
|
||||||
|
WHERE mmitem.code = ANY($1)
|
||||||
|
AND sdprc.sdprcgrp_id BETWEEN 1 AND 6
|
||||||
|
AND sdprc.crn IN ('USD', 'EUR', 'TRY')
|
||||||
|
AND sdprc.prc IS NOT NULL
|
||||||
|
AND sdprc.prc > 0
|
||||||
|
)
|
||||||
|
SELECT code, grp, crn, prc
|
||||||
|
FROM ranked
|
||||||
|
WHERE rn = 1;
|
||||||
|
`
|
||||||
|
pgRows, err := pg.QueryContext(ctx, tierSQL, pq.Array(chunk))
|
||||||
|
if err == nil {
|
||||||
|
for pgRows.Next() {
|
||||||
|
var code, crn string
|
||||||
|
var grp int
|
||||||
|
var prc float64
|
||||||
|
if err := pgRows.Scan(&code, &grp, &crn, &prc); err != nil {
|
||||||
|
_ = pgRows.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
idx, ok := indexByCode[strings.TrimSpace(code)]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(crn)) {
|
||||||
|
case "USD":
|
||||||
|
switch grp {
|
||||||
|
case 1:
|
||||||
|
out[idx].USD1 = prc
|
||||||
|
case 2:
|
||||||
|
out[idx].USD2 = prc
|
||||||
|
case 3:
|
||||||
|
out[idx].USD3 = prc
|
||||||
|
case 4:
|
||||||
|
out[idx].USD4 = prc
|
||||||
|
case 5:
|
||||||
|
out[idx].USD5 = prc
|
||||||
|
case 6:
|
||||||
|
out[idx].USD6 = prc
|
||||||
|
}
|
||||||
|
case "EUR":
|
||||||
|
switch grp {
|
||||||
|
case 1:
|
||||||
|
out[idx].EUR1 = prc
|
||||||
|
case 2:
|
||||||
|
out[idx].EUR2 = prc
|
||||||
|
case 3:
|
||||||
|
out[idx].EUR3 = prc
|
||||||
|
case 4:
|
||||||
|
out[idx].EUR4 = prc
|
||||||
|
case 5:
|
||||||
|
out[idx].EUR5 = prc
|
||||||
|
case 6:
|
||||||
|
out[idx].EUR6 = prc
|
||||||
|
}
|
||||||
|
case "TRY":
|
||||||
|
switch grp {
|
||||||
|
case 1:
|
||||||
|
out[idx].TRY1 = prc
|
||||||
|
case 2:
|
||||||
|
out[idx].TRY2 = prc
|
||||||
|
case 3:
|
||||||
|
out[idx].TRY3 = prc
|
||||||
|
case 4:
|
||||||
|
out[idx].TRY4 = prc
|
||||||
|
case 5:
|
||||||
|
out[idx].TRY5 = prc
|
||||||
|
case 6:
|
||||||
|
out[idx].TRY6 = prc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := pgRows.Err(); err != nil {
|
||||||
|
_ = pgRows.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = pgRows.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pg := db.PgDB; pg != nil {
|
||||||
|
brandCodes := make([]string, 0, len(out))
|
||||||
|
seen := make(map[string]struct{}, len(out))
|
||||||
|
for _, it := range out {
|
||||||
|
code := strings.TrimSpace(it.BrandCode)
|
||||||
|
if code == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[code]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[code] = struct{}{}
|
||||||
|
brandCodes = append(brandCodes, code)
|
||||||
|
}
|
||||||
|
for _, chunk := range chunkStringSlice(brandCodes, chunkSize) {
|
||||||
|
rows, err := pg.QueryContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
m.brand_code,
|
||||||
|
COALESCE(g.title, '') AS grp_title
|
||||||
|
FROM mk_brandgrpmatch m
|
||||||
|
JOIN mk_brandgrp g ON g.id = m.grp_id
|
||||||
|
WHERE m.brand_code = ANY($1)
|
||||||
|
`, pq.Array(chunk))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
grpByBrand := make(map[string]string, len(chunk))
|
||||||
|
for rows.Next() {
|
||||||
|
var code, title string
|
||||||
|
if err := rows.Scan(&code, &title); err != nil {
|
||||||
|
_ = rows.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
grpByBrand[strings.TrimSpace(code)] = strings.TrimSpace(title)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
_ = rows.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = rows.Close()
|
||||||
|
for i := range out {
|
||||||
|
if title, ok := grpByBrand[strings.TrimSpace(out[i].BrandCode)]; ok {
|
||||||
|
out[i].BrandGroupSec = title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func chunkStringSlice(values []string, size int) [][]string {
|
||||||
|
if size <= 0 {
|
||||||
|
size = 1000
|
||||||
|
}
|
||||||
|
out := make([][]string, 0, (len(values)+size-1)/size)
|
||||||
|
for start := 0; start < len(values); start += size {
|
||||||
|
end := start + size
|
||||||
|
if end > len(values) {
|
||||||
|
end = len(values)
|
||||||
|
}
|
||||||
|
out = append(out, values[start:end])
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func GetProductPricingPage(ctx context.Context, page int, limit int, filters ProductPricingFilters, includeTotal bool, sortBy string, descending bool) (ProductPricingPage, error) {
|
func GetProductPricingPage(ctx context.Context, page int, limit int, filters ProductPricingFilters, includeTotal bool, sortBy string, descending bool) (ProductPricingPage, error) {
|
||||||
result := ProductPricingPage{
|
result := ProductPricingPage{
|
||||||
Rows: []models.ProductPricing{},
|
Rows: []models.ProductPricing{},
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import (
|
|||||||
"bssapp-backend/utils"
|
"bssapp-backend/utils"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Step-1/2 scope (distinct+cascade) comes from the PostgreSQL parameter cache.
|
// Step-1/2 scope (distinct+cascade) comes from the PostgreSQL parameter cache.
|
||||||
@@ -132,6 +135,28 @@ func GetPricingParameterRulesHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExportPricingRulesHandler(pg *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
traceID := utils.TraceIDFromRequest(r)
|
||||||
|
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
||||||
|
|
||||||
|
rows, err := queries.ListPricingParameterRules(ctx, pg, pricingRuleFiltersFromRequest(r))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "pricing parameter rules export error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = filterPricingRuleExportRows(rows, r)
|
||||||
|
sortPricingRuleExportRows(rows, strings.TrimSpace(r.URL.Query().Get("sort_by")), strings.TrimSpace(r.URL.Query().Get("desc")) != "0")
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("pricing_rules_all_%s.csv", time.Now().Format("2006-01-02"))
|
||||||
|
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
|
||||||
|
_, _ = w.Write([]byte("\uFEFF"))
|
||||||
|
_, _ = w.Write([]byte(buildPricingRuleCSV(rows)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func pricingRuleFiltersFromRequest(r *http.Request) queries.PricingRuleOptionFilters {
|
func pricingRuleFiltersFromRequest(r *http.Request) queries.PricingRuleOptionFilters {
|
||||||
return queries.PricingRuleOptionFilters{
|
return queries.PricingRuleOptionFilters{
|
||||||
AskiliYan: splitCSV(r.URL.Query().Get("askili_yan")),
|
AskiliYan: splitCSV(r.URL.Query().Get("askili_yan")),
|
||||||
@@ -146,6 +171,266 @@ func pricingRuleFiltersFromRequest(r *http.Request) queries.PricingRuleOptionFil
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func filterPricingRuleExportRows(rows []queries.PricingParameterRuleRow, r *http.Request) []queries.PricingParameterRuleRow {
|
||||||
|
rangeFilter := func(prefix string) (*float64, *float64) {
|
||||||
|
parse := func(raw string) *float64 {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseFloat(strings.ReplaceAll(raw, ",", "."), 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
return parse(r.URL.Query().Get(prefix + "_min")), parse(r.URL.Query().Get(prefix + "_max"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := []string{
|
||||||
|
"try_base", "try1", "try2", "try3", "try4", "try5", "try6", "try_step",
|
||||||
|
"usd_base", "usd1", "usd2", "usd3", "usd4", "usd5", "usd6", "usd_step",
|
||||||
|
"eur_base", "eur1", "eur2", "eur3", "eur4", "eur5", "eur6", "eur_step",
|
||||||
|
}
|
||||||
|
minMap := map[string]*float64{}
|
||||||
|
maxMap := map[string]*float64{}
|
||||||
|
for _, field := range fields {
|
||||||
|
minMap[field], maxMap[field] = rangeFilter(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]queries.PricingParameterRuleRow, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
ok := true
|
||||||
|
for _, field := range fields {
|
||||||
|
value := pricingRuleNumericValue(row, field)
|
||||||
|
if minMap[field] != nil && value < *minMap[field] {
|
||||||
|
ok = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if maxMap[field] != nil && value > *maxMap[field] {
|
||||||
|
ok = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
out = append(out, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func pricingRuleNumericValue(row queries.PricingParameterRuleRow, field string) float64 {
|
||||||
|
if row.Rule == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
switch field {
|
||||||
|
case "try_base":
|
||||||
|
return row.Rule.TryBase
|
||||||
|
case "try1":
|
||||||
|
return row.Rule.Try1
|
||||||
|
case "try2":
|
||||||
|
return row.Rule.Try2
|
||||||
|
case "try3":
|
||||||
|
return row.Rule.Try3
|
||||||
|
case "try4":
|
||||||
|
return row.Rule.Try4
|
||||||
|
case "try5":
|
||||||
|
return row.Rule.Try5
|
||||||
|
case "try6":
|
||||||
|
return row.Rule.Try6
|
||||||
|
case "try_step":
|
||||||
|
return row.Rule.TryStep
|
||||||
|
case "usd_base":
|
||||||
|
return row.Rule.UsdBase
|
||||||
|
case "usd1":
|
||||||
|
return row.Rule.Usd1
|
||||||
|
case "usd2":
|
||||||
|
return row.Rule.Usd2
|
||||||
|
case "usd3":
|
||||||
|
return row.Rule.Usd3
|
||||||
|
case "usd4":
|
||||||
|
return row.Rule.Usd4
|
||||||
|
case "usd5":
|
||||||
|
return row.Rule.Usd5
|
||||||
|
case "usd6":
|
||||||
|
return row.Rule.Usd6
|
||||||
|
case "usd_step":
|
||||||
|
return row.Rule.UsdStep
|
||||||
|
case "eur_base":
|
||||||
|
return row.Rule.EurBase
|
||||||
|
case "eur1":
|
||||||
|
return row.Rule.Eur1
|
||||||
|
case "eur2":
|
||||||
|
return row.Rule.Eur2
|
||||||
|
case "eur3":
|
||||||
|
return row.Rule.Eur3
|
||||||
|
case "eur4":
|
||||||
|
return row.Rule.Eur4
|
||||||
|
case "eur5":
|
||||||
|
return row.Rule.Eur5
|
||||||
|
case "eur6":
|
||||||
|
return row.Rule.Eur6
|
||||||
|
case "eur_step":
|
||||||
|
return row.Rule.EurStep
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortPricingRuleExportRows(rows []queries.PricingParameterRuleRow, sortBy string, desc bool) {
|
||||||
|
sortBy = strings.TrimSpace(sortBy)
|
||||||
|
if sortBy == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort.SliceStable(rows, func(i, j int) bool {
|
||||||
|
li, lj := rows[i], rows[j]
|
||||||
|
switch sortBy {
|
||||||
|
case "has_rule":
|
||||||
|
if desc {
|
||||||
|
return boolRank(li.HasRule) > boolRank(lj.HasRule)
|
||||||
|
}
|
||||||
|
return boolRank(li.HasRule) < boolRank(lj.HasRule)
|
||||||
|
case "is_active":
|
||||||
|
liActive, ljActive := false, false
|
||||||
|
if li.Rule != nil {
|
||||||
|
liActive = li.Rule.IsActive
|
||||||
|
}
|
||||||
|
if lj.Rule != nil {
|
||||||
|
ljActive = lj.Rule.IsActive
|
||||||
|
}
|
||||||
|
if desc {
|
||||||
|
return boolRank(liActive) > boolRank(ljActive)
|
||||||
|
}
|
||||||
|
return boolRank(liActive) < boolRank(ljActive)
|
||||||
|
case "askili_yan", "kategori", "urun_ilk_grubu", "urun_ana_grubu", "urun_alt_grubu", "icerik", "marka", "brand_code", "brand_group":
|
||||||
|
vi := pricingRuleStringValue(li, sortBy)
|
||||||
|
vj := pricingRuleStringValue(lj, sortBy)
|
||||||
|
if desc {
|
||||||
|
return strings.Compare(vi, vj) > 0
|
||||||
|
}
|
||||||
|
return strings.Compare(vi, vj) < 0
|
||||||
|
default:
|
||||||
|
vi := pricingRuleNumericValue(li, sortBy)
|
||||||
|
vj := pricingRuleNumericValue(lj, sortBy)
|
||||||
|
if desc {
|
||||||
|
return vi > vj
|
||||||
|
}
|
||||||
|
return vi < vj
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolRank(v bool) int {
|
||||||
|
if v {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func pricingRuleStringValue(row queries.PricingParameterRuleRow, field string) string {
|
||||||
|
switch field {
|
||||||
|
case "askili_yan":
|
||||||
|
return row.AskiliYan
|
||||||
|
case "kategori":
|
||||||
|
return row.Kategori
|
||||||
|
case "urun_ilk_grubu":
|
||||||
|
return row.UrunIlkGrubu
|
||||||
|
case "urun_ana_grubu":
|
||||||
|
return row.UrunAnaGrubu
|
||||||
|
case "urun_alt_grubu":
|
||||||
|
return row.UrunAltGrubu
|
||||||
|
case "icerik":
|
||||||
|
return row.Icerik
|
||||||
|
case "marka":
|
||||||
|
return row.Marka
|
||||||
|
case "brand_code":
|
||||||
|
return row.BrandCode
|
||||||
|
case "brand_group":
|
||||||
|
return row.BrandGroupSec
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPricingRuleCSV(rows []queries.PricingParameterRuleRow) string {
|
||||||
|
headers := []string{
|
||||||
|
"DURUM", "AKTIF", "ASKILI YAN", "KATEGORI", "URUN ILK GRUBU", "URUN ANA GRUBU", "URUN ALT GRUBU",
|
||||||
|
"ICERIK", "MARKA", "BRAND CODE", "MARKA GRUBU",
|
||||||
|
"TRY YUVARLAMA", "TRY TABAN", "TRY 1", "TRY 2", "TRY 3", "TRY 4", "TRY 5", "TRY 6",
|
||||||
|
"USD YUVARLAMA", "USD TABAN", "USD 1", "USD 2", "USD 3", "USD 4", "USD 5", "USD 6",
|
||||||
|
"EUR YUVARLAMA", "EUR TABAN", "EUR 1", "EUR 2", "EUR 3", "EUR 4", "EUR 5", "EUR 6",
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
for i, h := range headers {
|
||||||
|
b.WriteString(csvEscapeValue(h))
|
||||||
|
if i == len(headers)-1 {
|
||||||
|
b.WriteString("\n")
|
||||||
|
} else {
|
||||||
|
b.WriteString(";")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
active := "Pasif"
|
||||||
|
if row.Rule == nil || row.Rule.IsActive {
|
||||||
|
active = "Aktif"
|
||||||
|
}
|
||||||
|
values := []string{
|
||||||
|
map[bool]string{true: "Tanimli", false: "Yeni"}[row.HasRule],
|
||||||
|
active,
|
||||||
|
row.AskiliYan,
|
||||||
|
row.Kategori,
|
||||||
|
row.UrunIlkGrubu,
|
||||||
|
row.UrunAnaGrubu,
|
||||||
|
row.UrunAltGrubu,
|
||||||
|
row.Icerik,
|
||||||
|
row.Marka,
|
||||||
|
row.BrandCode,
|
||||||
|
row.BrandGroupSec,
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try_step")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try_base")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try1")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try2")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try3")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try4")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try5")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try6")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd_step")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd_base")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd1")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd2")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd3")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd4")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd5")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd6")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur_step")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur_base")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur1")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur2")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur3")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur4")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur5")),
|
||||||
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur6")),
|
||||||
|
}
|
||||||
|
for i, value := range values {
|
||||||
|
b.WriteString(csvEscapeValue(value))
|
||||||
|
if i == len(values)-1 {
|
||||||
|
b.WriteString("\n")
|
||||||
|
} else {
|
||||||
|
b.WriteString(";")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func csvEscapeValue(value string) string {
|
||||||
|
text := strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(value), "\r", " "), "\n", " ")
|
||||||
|
if strings.Contains(text, ";") || strings.Contains(text, "\"") {
|
||||||
|
text = `"` + strings.ReplaceAll(text, `"`, `""`) + `"`
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
func splitCSV(raw string) []string {
|
func splitCSV(raw string) []string {
|
||||||
raw = strings.TrimSpace(raw)
|
raw = strings.TrimSpace(raw)
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package routes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bssapp-backend/auth"
|
"bssapp-backend/auth"
|
||||||
|
"bssapp-backend/models"
|
||||||
"bssapp-backend/queries"
|
"bssapp-backend/queries"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -211,6 +213,392 @@ func GetProductPricingFilterOptionsHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
_ = json.NewEncoder(w).Encode(resp)
|
_ = json.NewEncoder(w).Encode(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET /api/pricing/products/export-all
|
||||||
|
func ExportAllProductPricingHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
started := time.Now()
|
||||||
|
traceID := buildPricingTraceID(r)
|
||||||
|
w.Header().Set("X-Trace-ID", traceID)
|
||||||
|
|
||||||
|
claims, ok := auth.GetClaimsFromContext(r.Context())
|
||||||
|
if !ok || claims == nil {
|
||||||
|
log.Printf("[ProductPricingExport] trace=%s unauthorized method=%s path=%s", traceID, r.Method, r.URL.Path)
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 170*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
filters := parseProductPricingFilters(r)
|
||||||
|
if len(filters.UrunAnaGrubu) > 3 {
|
||||||
|
http.Error(w, "Urun Ana Grubu en fazla 3 secilebilir", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sortBy := strings.TrimSpace(r.URL.Query().Get("sort_by"))
|
||||||
|
desc := true
|
||||||
|
if raw := strings.TrimSpace(r.URL.Query().Get("desc")); raw != "" {
|
||||||
|
if raw == "0" || strings.EqualFold(raw, "false") {
|
||||||
|
desc = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := queries.GetAllProductPricingRows(ctx, 1000, filters, sortBy, desc)
|
||||||
|
if err != nil {
|
||||||
|
if isPricingTimeoutLike(err, ctx.Err()) {
|
||||||
|
http.Error(w, "Urun fiyatlandirma export zaman asimina ugradi", http.StatusGatewayTimeout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "Urun fiyatlandirma export alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = filterProductPricingExportRows(rows, parseProductPricingExportFilters(r))
|
||||||
|
currencies := parseExportCurrencies(r.URL.Query().Get("currencies"))
|
||||||
|
content := buildProductPricingExportCSV(rows, currencies)
|
||||||
|
filename := fmt.Sprintf("product_pricing_all_%s.csv", time.Now().Format("2006-01-02"))
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
|
||||||
|
_, _ = w.Write([]byte("\uFEFF"))
|
||||||
|
_, _ = w.Write([]byte(content))
|
||||||
|
|
||||||
|
log.Printf(
|
||||||
|
"[ProductPricingExport] trace=%s success user=%s id=%d rows=%d duration_ms=%d",
|
||||||
|
traceID,
|
||||||
|
claims.Username,
|
||||||
|
claims.ID,
|
||||||
|
len(rows),
|
||||||
|
time.Since(started).Milliseconds(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type productPricingExportFilters struct {
|
||||||
|
BrandGroupSelection []string
|
||||||
|
ValueFilters map[string][]string
|
||||||
|
StockQtyMin *float64
|
||||||
|
StockQtyMax *float64
|
||||||
|
StockEntryFrom string
|
||||||
|
StockEntryTo string
|
||||||
|
LastPricingFrom string
|
||||||
|
LastPricingTo string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseProductPricingFilters(r *http.Request) queries.ProductPricingFilters {
|
||||||
|
return queries.ProductPricingFilters{
|
||||||
|
Search: strings.TrimSpace(r.URL.Query().Get("q")),
|
||||||
|
ProductCode: splitCSVParam(r.URL.Query().Get("product_code")),
|
||||||
|
BrandGroup: splitCSVParam(r.URL.Query().Get("brand_group_selection")),
|
||||||
|
AskiliYan: splitCSVParam(r.URL.Query().Get("askili_yan")),
|
||||||
|
Kategori: splitCSVParam(r.URL.Query().Get("kategori")),
|
||||||
|
UrunIlkGrubu: splitCSVParam(r.URL.Query().Get("urun_ilk_grubu")),
|
||||||
|
UrunAnaGrubu: splitCSVParam(r.URL.Query().Get("urun_ana_grubu")),
|
||||||
|
UrunAltGrubu: splitCSVParam(r.URL.Query().Get("urun_alt_grubu")),
|
||||||
|
Icerik: splitCSVParam(r.URL.Query().Get("icerik")),
|
||||||
|
Karisim: splitCSVParam(r.URL.Query().Get("karisim")),
|
||||||
|
Marka: splitCSVParam(r.URL.Query().Get("marka")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseProductPricingExportFilters(r *http.Request) productPricingExportFilters {
|
||||||
|
parseFloatPtr := func(raw string) *float64 {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseFloat(strings.ReplaceAll(raw, ",", "."), 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
return productPricingExportFilters{
|
||||||
|
BrandGroupSelection: splitCSVParam(r.URL.Query().Get("brand_group_selection_local")),
|
||||||
|
ValueFilters: map[string][]string{
|
||||||
|
"costPrice": splitCSVParam(r.URL.Query().Get("vf_costPrice")),
|
||||||
|
"expenseForBasePrice": splitCSVParam(r.URL.Query().Get("vf_expenseForBasePrice")),
|
||||||
|
"basePriceUsd": splitCSVParam(r.URL.Query().Get("vf_basePriceUsd")),
|
||||||
|
"basePriceTry": splitCSVParam(r.URL.Query().Get("vf_basePriceTry")),
|
||||||
|
"usd1": splitCSVParam(r.URL.Query().Get("vf_usd1")),
|
||||||
|
"usd2": splitCSVParam(r.URL.Query().Get("vf_usd2")),
|
||||||
|
"usd3": splitCSVParam(r.URL.Query().Get("vf_usd3")),
|
||||||
|
"usd4": splitCSVParam(r.URL.Query().Get("vf_usd4")),
|
||||||
|
"usd5": splitCSVParam(r.URL.Query().Get("vf_usd5")),
|
||||||
|
"usd6": splitCSVParam(r.URL.Query().Get("vf_usd6")),
|
||||||
|
"eur1": splitCSVParam(r.URL.Query().Get("vf_eur1")),
|
||||||
|
"eur2": splitCSVParam(r.URL.Query().Get("vf_eur2")),
|
||||||
|
"eur3": splitCSVParam(r.URL.Query().Get("vf_eur3")),
|
||||||
|
"eur4": splitCSVParam(r.URL.Query().Get("vf_eur4")),
|
||||||
|
"eur5": splitCSVParam(r.URL.Query().Get("vf_eur5")),
|
||||||
|
"eur6": splitCSVParam(r.URL.Query().Get("vf_eur6")),
|
||||||
|
"try1": splitCSVParam(r.URL.Query().Get("vf_try1")),
|
||||||
|
"try2": splitCSVParam(r.URL.Query().Get("vf_try2")),
|
||||||
|
"try3": splitCSVParam(r.URL.Query().Get("vf_try3")),
|
||||||
|
"try4": splitCSVParam(r.URL.Query().Get("vf_try4")),
|
||||||
|
"try5": splitCSVParam(r.URL.Query().Get("vf_try5")),
|
||||||
|
"try6": splitCSVParam(r.URL.Query().Get("vf_try6")),
|
||||||
|
},
|
||||||
|
StockQtyMin: parseFloatPtr(r.URL.Query().Get("stock_qty_min")),
|
||||||
|
StockQtyMax: parseFloatPtr(r.URL.Query().Get("stock_qty_max")),
|
||||||
|
StockEntryFrom: strings.TrimSpace(r.URL.Query().Get("stock_entry_from")),
|
||||||
|
StockEntryTo: strings.TrimSpace(r.URL.Query().Get("stock_entry_to")),
|
||||||
|
LastPricingFrom: strings.TrimSpace(r.URL.Query().Get("last_pricing_from")),
|
||||||
|
LastPricingTo: strings.TrimSpace(r.URL.Query().Get("last_pricing_to")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterProductPricingExportRows(rows []models.ProductPricing, f productPricingExportFilters) []models.ProductPricing {
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]models.ProductPricing, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
if len(f.BrandGroupSelection) > 0 && !containsText(f.BrandGroupSelection, row.BrandGroupSec) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !matchesProductPricingValueFilters(row, f.ValueFilters) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if f.StockQtyMin != nil && row.StockQty < *f.StockQtyMin {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if f.StockQtyMax != nil && row.StockQty > *f.StockQtyMax {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !matchesDateRangeString(row.StockEntryDate, f.StockEntryFrom, f.StockEntryTo) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !matchesDateRangeString(row.LastPricingDate, f.LastPricingFrom, f.LastPricingTo) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, row)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseExportCurrencies(raw string) []string {
|
||||||
|
requested := splitCSVParam(raw)
|
||||||
|
if len(requested) == 0 {
|
||||||
|
return []string{"USD", "EUR", "TRY"}
|
||||||
|
}
|
||||||
|
out := make([]string, 0, 3)
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, item := range requested {
|
||||||
|
cur := strings.ToUpper(strings.TrimSpace(item))
|
||||||
|
if cur != "USD" && cur != "EUR" && cur != "TRY" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if seen[cur] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[cur] = true
|
||||||
|
out = append(out, cur)
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return []string{"USD", "EUR", "TRY"}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesProductPricingValueFilters(row models.ProductPricing, filters map[string][]string) bool {
|
||||||
|
if len(filters) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for field, selected := range filters {
|
||||||
|
if len(selected) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !containsText(selected, productPricingValueKey(row, field)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPricingValueKey(row models.ProductPricing, field string) string {
|
||||||
|
value := 0.0
|
||||||
|
switch field {
|
||||||
|
case "costPrice":
|
||||||
|
value = row.CostPrice
|
||||||
|
case "expenseForBasePrice":
|
||||||
|
value = 0
|
||||||
|
case "basePriceUsd":
|
||||||
|
value = row.BasePriceUsd
|
||||||
|
case "basePriceTry":
|
||||||
|
value = row.BasePriceTry
|
||||||
|
case "usd1":
|
||||||
|
value = row.USD1
|
||||||
|
case "usd2":
|
||||||
|
value = row.USD2
|
||||||
|
case "usd3":
|
||||||
|
value = row.USD3
|
||||||
|
case "usd4":
|
||||||
|
value = row.USD4
|
||||||
|
case "usd5":
|
||||||
|
value = row.USD5
|
||||||
|
case "usd6":
|
||||||
|
value = row.USD6
|
||||||
|
case "eur1":
|
||||||
|
value = row.EUR1
|
||||||
|
case "eur2":
|
||||||
|
value = row.EUR2
|
||||||
|
case "eur3":
|
||||||
|
value = row.EUR3
|
||||||
|
case "eur4":
|
||||||
|
value = row.EUR4
|
||||||
|
case "eur5":
|
||||||
|
value = row.EUR5
|
||||||
|
case "eur6":
|
||||||
|
value = row.EUR6
|
||||||
|
case "try1":
|
||||||
|
value = row.TRY1
|
||||||
|
case "try2":
|
||||||
|
value = row.TRY2
|
||||||
|
case "try3":
|
||||||
|
value = row.TRY3
|
||||||
|
case "try4":
|
||||||
|
value = row.TRY4
|
||||||
|
case "try5":
|
||||||
|
value = row.TRY5
|
||||||
|
case "try6":
|
||||||
|
value = row.TRY6
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.2f", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesDateRangeString(value string, from string, to string) bool {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
from = strings.TrimSpace(from)
|
||||||
|
to = strings.TrimSpace(to)
|
||||||
|
if from == "" && to == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if value == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if from != "" && value < from {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if to != "" && value > to {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsText(list []string, value string) bool {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
for _, item := range list {
|
||||||
|
if strings.TrimSpace(item) == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func csvEscape(value string) string {
|
||||||
|
text := strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(value), "\r", " "), "\n", " ")
|
||||||
|
if strings.Contains(text, ";") || strings.Contains(text, "\"") {
|
||||||
|
text = `"` + strings.ReplaceAll(text, `"`, `""`) + `"`
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
func csvFloat(value float64) string {
|
||||||
|
return fmt.Sprintf("%.2f", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildProductPricingExportCSV(rows []models.ProductPricing, currencies []string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
headers := []string{
|
||||||
|
"MARKA GRUBU SECIMI",
|
||||||
|
"MARKA",
|
||||||
|
"URUN KODU",
|
||||||
|
"STOK ADET",
|
||||||
|
"STOK GIRIS TARIHI",
|
||||||
|
"SON MALIYETLENDIRME",
|
||||||
|
"SON FIYATLANDIRMA TARIHI",
|
||||||
|
"ASKILI YAN",
|
||||||
|
"KATEGORI",
|
||||||
|
"URUN ILK GRUBU",
|
||||||
|
"URUN ANA GRUBU",
|
||||||
|
"URUN ALT GRUBU",
|
||||||
|
"ICERIK",
|
||||||
|
"KARISIM",
|
||||||
|
"MALIYET FIYATI",
|
||||||
|
"TABAN FIYAT MASRAF",
|
||||||
|
"TABAN USD",
|
||||||
|
"TABAN TRY",
|
||||||
|
}
|
||||||
|
for _, h := range headers {
|
||||||
|
b.WriteString(csvEscape(h))
|
||||||
|
b.WriteString(";")
|
||||||
|
}
|
||||||
|
for idx, cur := range currencies {
|
||||||
|
for tier := 1; tier <= 6; tier++ {
|
||||||
|
b.WriteString(csvEscape(fmt.Sprintf("%s %d", cur, tier)))
|
||||||
|
if idx == len(currencies)-1 && tier == 6 {
|
||||||
|
b.WriteString("\n")
|
||||||
|
} else {
|
||||||
|
b.WriteString(";")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
base := []string{
|
||||||
|
row.BrandGroupSec,
|
||||||
|
row.Marka,
|
||||||
|
row.ProductCode,
|
||||||
|
csvFloat(row.StockQty),
|
||||||
|
row.StockEntryDate,
|
||||||
|
row.LastCostingDate,
|
||||||
|
row.LastPricingDate,
|
||||||
|
row.AskiliYan,
|
||||||
|
row.Kategori,
|
||||||
|
row.UrunIlkGrubu,
|
||||||
|
row.UrunAnaGrubu,
|
||||||
|
row.UrunAltGrubu,
|
||||||
|
row.Icerik,
|
||||||
|
row.Karisim,
|
||||||
|
csvFloat(row.CostPrice),
|
||||||
|
csvFloat(0),
|
||||||
|
csvFloat(row.BasePriceUsd),
|
||||||
|
csvFloat(row.BasePriceTry),
|
||||||
|
}
|
||||||
|
for _, item := range base {
|
||||||
|
b.WriteString(csvEscape(item))
|
||||||
|
b.WriteString(";")
|
||||||
|
}
|
||||||
|
for idx, cur := range currencies {
|
||||||
|
values := productPricingCurrencyValues(row, cur)
|
||||||
|
for tierIdx, value := range values {
|
||||||
|
b.WriteString(csvEscape(csvFloat(value)))
|
||||||
|
if idx == len(currencies)-1 && tierIdx == len(values)-1 {
|
||||||
|
b.WriteString("\n")
|
||||||
|
} else {
|
||||||
|
b.WriteString(";")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func productPricingCurrencyValues(row models.ProductPricing, currency string) []float64 {
|
||||||
|
switch currency {
|
||||||
|
case "USD":
|
||||||
|
return []float64{row.USD1, row.USD2, row.USD3, row.USD4, row.USD5, row.USD6}
|
||||||
|
case "EUR":
|
||||||
|
return []float64{row.EUR1, row.EUR2, row.EUR3, row.EUR4, row.EUR5, row.EUR6}
|
||||||
|
default:
|
||||||
|
return []float64{row.TRY1, row.TRY2, row.TRY3, row.TRY4, row.TRY5, row.TRY6}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func buildPricingTraceID(r *http.Request) string {
|
func buildPricingTraceID(r *http.Request) string {
|
||||||
if r != nil {
|
if r != nil {
|
||||||
if id := strings.TrimSpace(r.Header.Get("X-Request-ID")); id != "" {
|
if id := strings.TrimSpace(r.Header.Get("X-Request-ID")); id != "" {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"bssapp-backend/queries"
|
"bssapp-backend/queries"
|
||||||
"bytes"
|
"bytes"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -19,6 +20,28 @@ import (
|
|||||||
"github.com/jung-kurt/gofpdf"
|
"github.com/jung-kurt/gofpdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type statementPDFHeaderRow struct {
|
||||||
|
CariKod string `json:"cari_kod"`
|
||||||
|
CariIsim string `json:"cari_isim"`
|
||||||
|
BelgeTarihi string `json:"belge_tarihi"`
|
||||||
|
VadeTarihi string `json:"vade_tarihi"`
|
||||||
|
BelgeNo string `json:"belge_no"`
|
||||||
|
IslemTipi string `json:"islem_tipi"`
|
||||||
|
Aciklama string `json:"aciklama"`
|
||||||
|
ParaBirimi string `json:"para_birimi"`
|
||||||
|
Borc float64 `json:"borc"`
|
||||||
|
Alacak float64 `json:"alacak"`
|
||||||
|
Bakiye float64 `json:"bakiye"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type statementPDFPayload struct {
|
||||||
|
AccountCode string `json:"account_code"`
|
||||||
|
StartDate string `json:"start_date"`
|
||||||
|
EndDate string `json:"end_date"`
|
||||||
|
LangCode string `json:"lang_code"`
|
||||||
|
Rows []statementPDFHeaderRow `json:"rows"`
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================ SABİTLER ============================ */
|
/* ============================ SABİTLER ============================ */
|
||||||
|
|
||||||
// A4 Landscape (mm)
|
// A4 Landscape (mm)
|
||||||
@@ -468,12 +491,26 @@ func ExportPDFHandler(mssql *sql.DB) http.HandlerFunc {
|
|||||||
log.Printf("▶️ ExportPDFHandler: account=%s start=%s end=%s parislemler=%v",
|
log.Printf("▶️ ExportPDFHandler: account=%s start=%s end=%s parislemler=%v",
|
||||||
accountCode, startDate, endDate, parislemler)
|
accountCode, startDate, endDate, parislemler)
|
||||||
|
|
||||||
// 1) Header verileri
|
var (
|
||||||
headers, belgeNos, err := queries.GetStatementsPDF(r.Context(), accountCode, startDate, endDate, langCode, parislemler)
|
headers []models.StatementHeader
|
||||||
|
belgeNos []string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if strings.EqualFold(r.Method, http.MethodPost) {
|
||||||
|
accountCode, startDate, endDate, langCode, headers, err = parseStatementPDFPayload(r, langCode)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
belgeNos = queriesCollectBelgeNos(headers)
|
||||||
|
} else {
|
||||||
|
headers, belgeNos, err = queries.GetStatementsPDF(r.Context(), accountCode, startDate, endDate, langCode, parislemler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
log.Printf("✅ Header verileri alındı: %d kayıt, %d belge no", len(headers), len(belgeNos))
|
log.Printf("✅ Header verileri alındı: %d kayıt, %d belge no", len(headers), len(belgeNos))
|
||||||
|
|
||||||
// 2) Detay verileri
|
// 2) Detay verileri
|
||||||
@@ -506,10 +543,7 @@ func ExportPDFHandler(mssql *sql.DB) http.HandlerFunc {
|
|||||||
for _, k := range order {
|
for _, k := range order {
|
||||||
sort.SliceStable(groups[k].rows, func(i, j int) bool {
|
sort.SliceStable(groups[k].rows, func(i, j int) bool {
|
||||||
ri, rj := groups[k].rows[i], groups[k].rows[j]
|
ri, rj := groups[k].rows[i], groups[k].rows[j]
|
||||||
if ri.BelgeTarihi == rj.BelgeTarihi {
|
return parseStatementHeaderDate(ri.BelgeTarihi).Before(parseStatementHeaderDate(rj.BelgeTarihi))
|
||||||
return ri.BelgeNo < rj.BelgeNo
|
|
||||||
}
|
|
||||||
return ri.BelgeTarihi < rj.BelgeTarihi
|
|
||||||
})
|
})
|
||||||
if n := len(groups[k].rows); n > 0 {
|
if n := len(groups[k].rows); n > 0 {
|
||||||
groups[k].sonBakiye = groups[k].rows[n-1].Bakiye
|
groups[k].sonBakiye = groups[k].rows[n-1].Bakiye
|
||||||
@@ -648,6 +682,64 @@ func ExportPDFHandler(mssql *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseStatementPDFPayload(r *http.Request, fallbackLang string) (string, string, string, string, []models.StatementHeader, error) {
|
||||||
|
var payload statementPDFPayload
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
|
return "", "", "", "", nil, fmt.Errorf("invalid statement pdf payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
langCode := i18n.ResolveLangCode(payload.LangCode, fallbackLang)
|
||||||
|
headers := make([]models.StatementHeader, 0, len(payload.Rows))
|
||||||
|
for _, row := range payload.Rows {
|
||||||
|
headers = append(headers, models.StatementHeader{
|
||||||
|
CariKod: row.CariKod,
|
||||||
|
CariIsim: row.CariIsim,
|
||||||
|
BelgeTarihi: row.BelgeTarihi,
|
||||||
|
VadeTarihi: row.VadeTarihi,
|
||||||
|
BelgeNo: row.BelgeNo,
|
||||||
|
IslemTipi: row.IslemTipi,
|
||||||
|
Aciklama: row.Aciklama,
|
||||||
|
ParaBirimi: row.ParaBirimi,
|
||||||
|
Borc: row.Borc,
|
||||||
|
Alacak: row.Alacak,
|
||||||
|
Bakiye: row.Bakiye,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload.AccountCode, payload.StartDate, payload.EndDate, langCode, headers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseStatementHeaderDate(value string) time.Time {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
if t, err := time.Parse("2006-01-02", value); err == nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
if t, err := time.Parse(time.RFC3339, value); err == nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func queriesCollectBelgeNos(headers []models.StatementHeader) []string {
|
||||||
|
seen := make(map[string]struct{}, len(headers))
|
||||||
|
out := make([]string, 0, len(headers))
|
||||||
|
for _, h := range headers {
|
||||||
|
no := strings.TrimSpace(h.BelgeNo)
|
||||||
|
if no == "" || no == "Baslangic_devir" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[no]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[no] = struct{}{}
|
||||||
|
out = append(out, no)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
NOTLAR:
|
NOTLAR:
|
||||||
- Header artık dinamik yüksekliğe sahip (drawPageHeader -> contentTopY döner).
|
- Header artık dinamik yüksekliğe sahip (drawPageHeader -> contentTopY döner).
|
||||||
|
|||||||
@@ -395,20 +395,6 @@ function toggleFiltersCollapsed() {
|
|||||||
filtersCollapsed.value = !filtersCollapsed.value
|
filtersCollapsed.value = !filtersCollapsed.value
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildExportParams() {
|
|
||||||
return {
|
|
||||||
accountcode: String(selectedCari.value || '').trim(),
|
|
||||||
cari_search: String(selectedCari.value || '').trim(),
|
|
||||||
enddate: dateTo.value,
|
|
||||||
selected_date: dateTo.value,
|
|
||||||
parislemler: selectedMonType.value,
|
|
||||||
exclude_zero_12: '0',
|
|
||||||
exclude_zero_13: '0',
|
|
||||||
sort_by: String(masterPagination.value?.sortBy || ''),
|
|
||||||
sort_desc: masterPagination.value?.descending ? '1' : '0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadAgingScreenPDF() {
|
async function downloadAgingScreenPDF() {
|
||||||
if (!canExportFinance.value) {
|
if (!canExportFinance.value) {
|
||||||
$q.notify({ type: 'negative', message: 'PDF export yetkiniz yok', position: 'top-right' })
|
$q.notify({ type: 'negative', message: 'PDF export yetkiniz yok', position: 'top-right' })
|
||||||
@@ -420,7 +406,17 @@ async function downloadAgingScreenPDF() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await download('/finance/account-aging-statement/export-screen-pdf', buildExportParams())
|
const blob = await download('/finance/account-aging-statement/export-screen-pdf', {
|
||||||
|
accountcode: String(selectedCari.value || '').trim(),
|
||||||
|
cari_search: String(selectedCari.value || '').trim(),
|
||||||
|
enddate: dateTo.value,
|
||||||
|
selected_date: dateTo.value,
|
||||||
|
parislemler: selectedMonType.value,
|
||||||
|
exclude_zero_12: '0',
|
||||||
|
exclude_zero_13: '0',
|
||||||
|
sort_by: String(masterPagination.value?.sortBy || ''),
|
||||||
|
sort_desc: masterPagination.value?.descending ? '1' : '0'
|
||||||
|
})
|
||||||
const pdfUrl = window.URL.createObjectURL(new Blob([blob], { type: 'application/pdf' }))
|
const pdfUrl = window.URL.createObjectURL(new Blob([blob], { type: 'application/pdf' }))
|
||||||
window.open(pdfUrl, '_blank')
|
window.open(pdfUrl, '_blank')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -3,9 +3,6 @@
|
|||||||
<div class="top-bar row items-center justify-between q-mb-xs">
|
<div class="top-bar row items-center justify-between q-mb-xs">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-subtitle1 text-weight-bold">Fiyat Carpani Kurallari</div>
|
<div class="text-subtitle1 text-weight-bold">Fiyat Carpani Kurallari</div>
|
||||||
<div class="text-caption text-grey-7">
|
|
||||||
MSSQL urun kombinasyonlari ve bu kombinasyonlara bagli para birimi bazli fiyat kurallari.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
@@ -19,9 +16,17 @@
|
|||||||
|
|
||||||
<div class="action-bar row items-center justify-between q-mb-xs">
|
<div class="action-bar row items-center justify-between q-mb-xs">
|
||||||
<div class="text-caption text-grey-8">
|
<div class="text-caption text-grey-8">
|
||||||
{{ filteredRows.length }} / {{ rows.length }} kombinasyon gosteriliyor. Degistirilen satirlar otomatik secilir.
|
Satir: {{ filteredRows.length }} / {{ rows.length }} | Degisen: {{ selectedDirtyCount }} | Kopya: {{ copySelectedCount }} | Secili: {{ selectedCount }}
|
||||||
|
</div>
|
||||||
|
<div class="row items-center q-gutter-sm">
|
||||||
|
<div class="row items-center q-gutter-xs action-legend">
|
||||||
|
<q-chip dense square color="orange-1" text-color="deep-orange-8" icon="content_copy">
|
||||||
|
Kopya secimi
|
||||||
|
</q-chip>
|
||||||
|
<q-chip dense square color="light-green-1" text-color="green-9" icon="task_alt">
|
||||||
|
Kaydetme secimi
|
||||||
|
</q-chip>
|
||||||
</div>
|
</div>
|
||||||
<div class="row items-center q-gutter-xs">
|
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -30,6 +35,38 @@
|
|||||||
:disable="!hasAnyFilter"
|
:disable="!hasAnyFilter"
|
||||||
@click="clearAllFilters"
|
@click="clearAllFilters"
|
||||||
/>
|
/>
|
||||||
|
<q-btn
|
||||||
|
color="secondary"
|
||||||
|
unelevated
|
||||||
|
icon="content_copy"
|
||||||
|
label="Kopyala"
|
||||||
|
:disable="!canCopySelected"
|
||||||
|
@click="copySelectedToSelected"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
flat
|
||||||
|
icon="download"
|
||||||
|
label="Sayfayi Excel'e Aktar"
|
||||||
|
:disable="filteredRows.length === 0"
|
||||||
|
@click="exportCurrentView"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
outline
|
||||||
|
icon="download_for_offline"
|
||||||
|
label="Tum Filtreyi Excel'e Aktar"
|
||||||
|
:disable="rows.length === 0"
|
||||||
|
@click="exportAllFiltered"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
outline
|
||||||
|
icon="upload_file"
|
||||||
|
label="Verileri CSV'den Yukle"
|
||||||
|
:disable="loading || rows.length === 0"
|
||||||
|
@click="openImportDialog"
|
||||||
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
unelevated
|
unelevated
|
||||||
@@ -39,6 +76,13 @@
|
|||||||
:label="`Kaydet (${selectedDirtyCount})`"
|
:label="`Kaydet (${selectedDirtyCount})`"
|
||||||
@click="saveSelected"
|
@click="saveSelected"
|
||||||
/>
|
/>
|
||||||
|
<input
|
||||||
|
ref="fileInputRef"
|
||||||
|
type="file"
|
||||||
|
accept=".csv,text/csv"
|
||||||
|
class="hidden-file-input"
|
||||||
|
@change="onImportFileChange"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -75,10 +119,17 @@
|
|||||||
<q-checkbox
|
<q-checkbox
|
||||||
v-if="col.name === 'select'"
|
v-if="col.name === 'select'"
|
||||||
dense
|
dense
|
||||||
|
size="24px"
|
||||||
|
class="rule-select-checkbox"
|
||||||
|
color="positive"
|
||||||
:model-value="allSelectedVisible"
|
:model-value="allSelectedVisible"
|
||||||
:indeterminate="someSelectedVisible && !allSelectedVisible"
|
:indeterminate="someSelectedVisible && !allSelectedVisible"
|
||||||
@update:model-value="toggleSelectAllVisible"
|
@update:model-value="toggleSelectAllVisible"
|
||||||
/>
|
/>
|
||||||
|
<div v-else-if="col.name === 'copy_select'" class="selection-header-copy">
|
||||||
|
<q-icon name="content_copy" size="16px" />
|
||||||
|
<span>Kopya</span>
|
||||||
|
</div>
|
||||||
<div v-else class="header-with-filter">
|
<div v-else class="header-with-filter">
|
||||||
<span>{{ col.label }}</span>
|
<span>{{ col.label }}</span>
|
||||||
<q-btn
|
<q-btn
|
||||||
@@ -182,10 +233,33 @@
|
|||||||
:props="props"
|
:props="props"
|
||||||
:class="[col.classes, { 'sticky-col': isStickyCol(col.name), 'sticky-boundary': isStickyBoundary(col.name) }]"
|
:class="[col.classes, { 'sticky-col': isStickyCol(col.name), 'sticky-boundary': isStickyBoundary(col.name) }]"
|
||||||
:style="getBodyCellStyle(col)"
|
:style="getBodyCellStyle(col)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="col.name === 'copy_select'"
|
||||||
|
class="row items-center no-wrap justify-center copy-cell-wrap"
|
||||||
>
|
>
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
v-if="col.name === 'select'"
|
|
||||||
dense
|
dense
|
||||||
|
size="24px"
|
||||||
|
class="rule-select-checkbox"
|
||||||
|
color="secondary"
|
||||||
|
:model-value="isCopySelected(props.row)"
|
||||||
|
@update:model-value="(value) => toggleCopySelected(props.row, value)"
|
||||||
|
/>
|
||||||
|
<q-badge
|
||||||
|
v-if="copyRoleLabel(props.row)"
|
||||||
|
color="deep-orange"
|
||||||
|
class="q-ml-xs"
|
||||||
|
>
|
||||||
|
{{ copyRoleLabel(props.row) }}
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
<q-checkbox
|
||||||
|
v-else-if="col.name === 'select'"
|
||||||
|
dense
|
||||||
|
size="24px"
|
||||||
|
class="rule-select-checkbox"
|
||||||
|
color="positive"
|
||||||
:model-value="isRowSelected(props.row)"
|
:model-value="isRowSelected(props.row)"
|
||||||
@update:model-value="(value) => setRowSelected(props.row, value)"
|
@update:model-value="(value) => setRowSelected(props.row, value)"
|
||||||
/>
|
/>
|
||||||
@@ -219,7 +293,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import { Notify } from 'quasar'
|
import { Notify } from 'quasar'
|
||||||
import api from 'src/services/api'
|
import api, { download } from 'src/services/api'
|
||||||
import { usePermissionStore } from 'stores/permissionStore'
|
import { usePermissionStore } from 'stores/permissionStore'
|
||||||
|
|
||||||
const perm = usePermissionStore()
|
const perm = usePermissionStore()
|
||||||
@@ -228,7 +302,9 @@ const canUpdate = computed(() => perm.hasApiPermission('pricing:update'))
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const rows = ref([])
|
const rows = ref([])
|
||||||
const selected = ref([])
|
const fileInputRef = ref(null)
|
||||||
|
const selectedKeyMap = ref({})
|
||||||
|
const copySelectedKeys = ref([])
|
||||||
const tablePagination = ref({ rowsPerPage: 0, sortBy: 'urun_ilk_grubu', descending: false })
|
const tablePagination = ref({ rowsPerPage: 0, sortBy: 'urun_ilk_grubu', descending: false })
|
||||||
let emptyRetryTimer = null
|
let emptyRetryTimer = null
|
||||||
|
|
||||||
@@ -238,6 +314,46 @@ const numericFields = new Set([
|
|||||||
'eur_base', 'eur1', 'eur2', 'eur3', 'eur4', 'eur5', 'eur6', 'eur_step'
|
'eur_base', 'eur1', 'eur2', 'eur3', 'eur4', 'eur5', 'eur6', 'eur_step'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const importKeyFieldLabels = [
|
||||||
|
['askili_yan', 'ASKILI YAN'],
|
||||||
|
['kategori', 'KATEGORI'],
|
||||||
|
['urun_ilk_grubu', 'URUN ILK GRUBU'],
|
||||||
|
['urun_ana_grubu', 'URUN ANA GRUBU'],
|
||||||
|
['urun_alt_grubu', 'URUN ALT GRUBU'],
|
||||||
|
['icerik', 'ICERIK'],
|
||||||
|
['marka', 'MARKA'],
|
||||||
|
['brand_code', 'BRAND CODE'],
|
||||||
|
['brand_group', 'MARKA GRUBU']
|
||||||
|
]
|
||||||
|
|
||||||
|
const importFieldMap = {
|
||||||
|
AKTIF: 'is_active',
|
||||||
|
'TRY YUVARLAMA': 'try_step',
|
||||||
|
'TRY TABAN': 'try_base',
|
||||||
|
'TRY 1': 'try1',
|
||||||
|
'TRY 2': 'try2',
|
||||||
|
'TRY 3': 'try3',
|
||||||
|
'TRY 4': 'try4',
|
||||||
|
'TRY 5': 'try5',
|
||||||
|
'TRY 6': 'try6',
|
||||||
|
'USD YUVARLAMA': 'usd_step',
|
||||||
|
'USD TABAN': 'usd_base',
|
||||||
|
'USD 1': 'usd1',
|
||||||
|
'USD 2': 'usd2',
|
||||||
|
'USD 3': 'usd3',
|
||||||
|
'USD 4': 'usd4',
|
||||||
|
'USD 5': 'usd5',
|
||||||
|
'USD 6': 'usd6',
|
||||||
|
'EUR YUVARLAMA': 'eur_step',
|
||||||
|
'EUR TABAN': 'eur_base',
|
||||||
|
'EUR 1': 'eur1',
|
||||||
|
'EUR 2': 'eur2',
|
||||||
|
'EUR 3': 'eur3',
|
||||||
|
'EUR 4': 'eur4',
|
||||||
|
'EUR 5': 'eur5',
|
||||||
|
'EUR 6': 'eur6'
|
||||||
|
}
|
||||||
|
|
||||||
const multiFilterFields = [
|
const multiFilterFields = [
|
||||||
'has_rule', 'is_active', 'askili_yan', 'kategori', 'urun_ilk_grubu', 'urun_ana_grubu',
|
'has_rule', 'is_active', 'askili_yan', 'kategori', 'urun_ilk_grubu', 'urun_ana_grubu',
|
||||||
'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group'
|
'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group'
|
||||||
@@ -265,7 +381,8 @@ function col (name, label, field, width, extra = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
col('select', '', 'select', 34, { sortable: false, classes: 'selection-col', headerClasses: 'selection-col' }),
|
col('copy_select', 'KOPYA', 'copy_select', 86, { sortable: false, classes: 'copy-selection-col', headerClasses: 'copy-selection-col' }),
|
||||||
|
col('select', 'KAYDET', 'select', 72, { sortable: false, classes: 'save-selection-col', headerClasses: 'save-selection-col' }),
|
||||||
col('has_rule', 'DURUM', 'has_rule', 62, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
col('has_rule', 'DURUM', 'has_rule', 62, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||||
col('is_active', 'AKTIF', 'is_active', 48, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
col('is_active', 'AKTIF', 'is_active', 48, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||||
col('askili_yan', 'ASKILI YAN', 'askili_yan', 86, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
col('askili_yan', 'ASKILI YAN', 'askili_yan', 86, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||||
@@ -307,7 +424,7 @@ const columns = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const stickyColumnNames = [
|
const stickyColumnNames = [
|
||||||
'select', 'has_rule', 'is_active', 'askili_yan', 'kategori', 'urun_ilk_grubu',
|
'copy_select', 'select', 'has_rule', 'is_active', 'askili_yan', 'kategori', 'urun_ilk_grubu',
|
||||||
'urun_ana_grubu', 'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group'
|
'urun_ana_grubu', 'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group'
|
||||||
]
|
]
|
||||||
const stickyBoundaryColumnName = 'brand_group'
|
const stickyBoundaryColumnName = 'brand_group'
|
||||||
@@ -387,11 +504,32 @@ const filteredRows = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const exportableColumns = computed(() => columns.filter(col => col.name !== 'copy_select' && col.name !== 'select'))
|
||||||
|
const exportedRows = computed(() => {
|
||||||
|
const list = [...filteredRows.value]
|
||||||
|
const sortBy = String(tablePagination.value?.sortBy || '').trim()
|
||||||
|
const descending = Boolean(tablePagination.value?.descending)
|
||||||
|
if (!sortBy) return list
|
||||||
|
|
||||||
|
list.sort((a, b) => {
|
||||||
|
const av = exportSortValue(a, sortBy)
|
||||||
|
const bv = exportSortValue(b, sortBy)
|
||||||
|
if (typeof av === 'number' && typeof bv === 'number') return av - bv
|
||||||
|
return String(av ?? '').localeCompare(String(bv ?? ''), 'tr', { numeric: true, sensitivity: 'base' })
|
||||||
|
})
|
||||||
|
|
||||||
|
return descending ? list.reverse() : list
|
||||||
|
})
|
||||||
|
|
||||||
|
const copySelectedKeySet = computed(() => new Set(copySelectedKeys.value))
|
||||||
const visibleRowKeys = computed(() => filteredRows.value.map(row => row._row_key))
|
const visibleRowKeys = computed(() => filteredRows.value.map(row => row._row_key))
|
||||||
const selectedVisibleCount = computed(() => visibleRowKeys.value.filter(key => selected.value.some(row => row._row_key === key)).length)
|
const selectedVisibleCount = computed(() => visibleRowKeys.value.reduce((count, key) => count + (selectedKeyMap.value?.[key] ? 1 : 0), 0))
|
||||||
const allSelectedVisible = computed(() => visibleRowKeys.value.length > 0 && selectedVisibleCount.value === visibleRowKeys.value.length)
|
const allSelectedVisible = computed(() => visibleRowKeys.value.length > 0 && selectedVisibleCount.value === visibleRowKeys.value.length)
|
||||||
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
|
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
|
||||||
const selectedDirtyCount = computed(() => selected.value.filter(row => row?._dirty).length)
|
const selectedDirtyCount = computed(() => rows.value.reduce((count, row) => count + (selectedKeyMap.value?.[row._row_key] && row?._dirty ? 1 : 0), 0))
|
||||||
|
const selectedCount = computed(() => Object.keys(selectedKeyMap.value || {}).length)
|
||||||
|
const copySelectedCount = computed(() => copySelectedKeys.value.length)
|
||||||
|
const canCopySelected = computed(() => copySelectedCount.value >= 2)
|
||||||
const hasAnyFilter = computed(() => {
|
const hasAnyFilter = computed(() => {
|
||||||
return [...headerFilterFieldSet].some(field => hasFilter(field))
|
return [...headerFilterFieldSet].some(field => hasFilter(field))
|
||||||
})
|
})
|
||||||
@@ -456,21 +594,54 @@ function getBodyCellStyle (column) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isRowSelected (row) {
|
function isRowSelected (row) {
|
||||||
return selected.value.some(item => item._row_key === row._row_key)
|
return !!selectedKeyMap.value?.[row._row_key]
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCopySelected (row) {
|
||||||
|
return copySelectedKeySet.value.has(row._row_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRowSelected (row, value) {
|
function setRowSelected (row, value) {
|
||||||
|
const key = row?._row_key
|
||||||
|
if (!key) return
|
||||||
|
const next = { ...(selectedKeyMap.value || {}) }
|
||||||
if (value) {
|
if (value) {
|
||||||
if (!isRowSelected(row)) selected.value = [...selected.value, row]
|
next[key] = true
|
||||||
return
|
} else {
|
||||||
|
delete next[key]
|
||||||
}
|
}
|
||||||
selected.value = selected.value.filter(item => item._row_key !== row._row_key)
|
selectedKeyMap.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCopySelected (row, value) {
|
||||||
|
const key = row?._row_key
|
||||||
|
if (!key) return
|
||||||
|
const next = [...copySelectedKeys.value]
|
||||||
|
const idx = next.indexOf(key)
|
||||||
|
if (value) {
|
||||||
|
if (idx === -1) next.push(key)
|
||||||
|
} else if (idx >= 0) {
|
||||||
|
next.splice(idx, 1)
|
||||||
|
}
|
||||||
|
copySelectedKeys.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyRoleLabel (row) {
|
||||||
|
const key = row?._row_key
|
||||||
|
if (!key) return ''
|
||||||
|
const idx = copySelectedKeys.value.indexOf(key)
|
||||||
|
if (idx === 0) return 'Kaynak'
|
||||||
|
if (idx > 0) return 'Hedef'
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelectAllVisible (value) {
|
function toggleSelectAllVisible (value) {
|
||||||
const keys = new Set(visibleRowKeys.value)
|
const next = { ...(selectedKeyMap.value || {}) }
|
||||||
const remaining = selected.value.filter(row => !keys.has(row._row_key))
|
for (const key of visibleRowKeys.value) {
|
||||||
selected.value = value ? [...remaining, ...filteredRows.value] : remaining
|
if (value) next[key] = true
|
||||||
|
else delete next[key]
|
||||||
|
}
|
||||||
|
selectedKeyMap.value = next
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectDirtyRow (row) {
|
function selectDirtyRow (row) {
|
||||||
@@ -487,6 +658,294 @@ function updateNumber (row, field, value) {
|
|||||||
markDirty(row)
|
markDirty(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function exportSortValue (row, field) {
|
||||||
|
if (field === 'has_rule') return row?.has_rule ? 1 : 0
|
||||||
|
if (field === 'is_active') return row?.is_active ? 1 : 0
|
||||||
|
if (numericFields.has(field)) return finiteNumber(row?.[field], 0)
|
||||||
|
return String(row?.[field] ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportCellValue (row, field) {
|
||||||
|
if (field === 'has_rule') return row?.has_rule ? 'Tanimli' : 'Yeni'
|
||||||
|
if (field === 'is_active') return row?.is_active ? 'Aktif' : 'Pasif'
|
||||||
|
if (numericFields.has(field)) {
|
||||||
|
const value = row?.[field]
|
||||||
|
if (value === '' || value === null || value === undefined) return '0'
|
||||||
|
return String(finiteNumber(value, 0))
|
||||||
|
}
|
||||||
|
return String(row?.[field] ?? '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function csvSafe (value) {
|
||||||
|
let text = String(value ?? '').replaceAll('\r', ' ').replaceAll('\n', ' ').trim()
|
||||||
|
if (text.includes(';') || text.includes('"')) {
|
||||||
|
text = `"${text.replaceAll('"', '""')}"`
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportCurrentView () {
|
||||||
|
const cols = exportableColumns.value
|
||||||
|
const list = exportedRows.value
|
||||||
|
if (cols.length === 0 || list.length === 0) return
|
||||||
|
|
||||||
|
const lines = [cols.map(col => csvSafe(col.label)).join(';')]
|
||||||
|
for (const row of list) {
|
||||||
|
lines.push(cols.map(col => csvSafe(exportCellValue(row, col.field))).join(';'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const bom = '\uFEFF'
|
||||||
|
const blob = new Blob([bom + lines.join('\n')], { type: 'text/csv;charset=utf-8;' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `pricing_rules_${new Date().toISOString().slice(0, 10)}.csv`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
a.remove()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportAllFiltered () {
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
askili_yan: (columnFilters.value.askili_yan || []).join(','),
|
||||||
|
kategori: (columnFilters.value.kategori || []).join(','),
|
||||||
|
urun_ilk_grubu: (columnFilters.value.urun_ilk_grubu || []).join(','),
|
||||||
|
urun_ana_grubu: (columnFilters.value.urun_ana_grubu || []).join(','),
|
||||||
|
urun_alt_grubu: (columnFilters.value.urun_alt_grubu || []).join(','),
|
||||||
|
icerik: (columnFilters.value.icerik || []).join(','),
|
||||||
|
marka: (columnFilters.value.marka || []).join(','),
|
||||||
|
brand_code: (columnFilters.value.brand_code || []).join(','),
|
||||||
|
brand_group: (columnFilters.value.brand_group || []).join(','),
|
||||||
|
sort_by: tablePagination.value?.sortBy || '',
|
||||||
|
desc: tablePagination.value?.descending ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of numericFields) {
|
||||||
|
const min = String(numberRangeFilters.value[field]?.min || '').trim()
|
||||||
|
const max = String(numberRangeFilters.value[field]?.max || '').trim()
|
||||||
|
if (min) params[`${field}_min`] = min
|
||||||
|
if (max) params[`${field}_max`] = max
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await download('/pricing/pricing-rules/export-all', params)
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `pricing_rules_all_${new Date().toISOString().slice(0, 10)}.csv`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
a.remove()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
} catch (err) {
|
||||||
|
Notify.create({ type: 'negative', message: err?.message || 'Tum filtre export alinamadi' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openImportDialog () {
|
||||||
|
fileInputRef.value?.click?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCsvRows (text) {
|
||||||
|
const clean = String(text || '').replace(/^\uFEFF/, '')
|
||||||
|
const rows = []
|
||||||
|
let cell = ''
|
||||||
|
let row = []
|
||||||
|
let inQuotes = false
|
||||||
|
|
||||||
|
for (let i = 0; i < clean.length; i++) {
|
||||||
|
const ch = clean[i]
|
||||||
|
if (inQuotes) {
|
||||||
|
if (ch === '"') {
|
||||||
|
if (clean[i + 1] === '"') {
|
||||||
|
cell += '"'
|
||||||
|
i += 1
|
||||||
|
} else {
|
||||||
|
inQuotes = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cell += ch
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch === '"') {
|
||||||
|
inQuotes = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ch === ';') {
|
||||||
|
row.push(cell)
|
||||||
|
cell = ''
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ch === '\n') {
|
||||||
|
row.push(cell)
|
||||||
|
rows.push(row)
|
||||||
|
row = []
|
||||||
|
cell = ''
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ch !== '\r') {
|
||||||
|
cell += ch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cell !== '' || row.length > 0) {
|
||||||
|
row.push(cell)
|
||||||
|
rows.push(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeImportText (value) {
|
||||||
|
return String(value ?? '').trim().toLocaleUpperCase('tr')
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildImportRowKeyFromObject (row) {
|
||||||
|
return importKeyFieldLabels
|
||||||
|
.map(([field]) => normalizeImportText(row?.[field] ?? ''))
|
||||||
|
.join('|')
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseImportedNumber (value) {
|
||||||
|
const text = String(value ?? '').trim().replace(/\s/g, '')
|
||||||
|
if (!text) return 0
|
||||||
|
|
||||||
|
const lastComma = text.lastIndexOf(',')
|
||||||
|
const lastDot = text.lastIndexOf('.')
|
||||||
|
let normalized = text
|
||||||
|
|
||||||
|
if (lastComma >= 0 && lastDot >= 0) {
|
||||||
|
if (lastComma > lastDot) normalized = text.replace(/\./g, '').replace(',', '.')
|
||||||
|
else normalized = text.replace(/,/g, '')
|
||||||
|
} else if (lastComma >= 0) {
|
||||||
|
normalized = text.replace(/\./g, '').replace(',', '.')
|
||||||
|
} else {
|
||||||
|
normalized = text.replace(/,/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(normalized)
|
||||||
|
return Number.isFinite(parsed) ? parsed : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseImportedBoolean (value) {
|
||||||
|
const normalized = normalizeImportText(value)
|
||||||
|
if (!normalized) return null
|
||||||
|
if (['AKTIF', 'TRUE', '1', 'EVET', 'YES'].includes(normalized)) return true
|
||||||
|
if (['PASIF', 'FALSE', '0', 'HAYIR', 'NO'].includes(normalized)) return false
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onImportFileChange (event) {
|
||||||
|
const input = event?.target
|
||||||
|
const file = input?.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = await file.text()
|
||||||
|
const matrix = parseCsvRows(text).filter(row => row.some(cell => String(cell || '').trim() !== ''))
|
||||||
|
if (matrix.length < 2) {
|
||||||
|
Notify.create({ type: 'negative', message: 'CSV bos veya gecersiz' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = matrix[0].map(cell => String(cell || '').trim())
|
||||||
|
const keyHeaderIndexes = {}
|
||||||
|
for (const [, label] of importKeyFieldLabels) {
|
||||||
|
keyHeaderIndexes[label] = headers.indexOf(label)
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingKeyHeaders = importKeyFieldLabels
|
||||||
|
.map(([, label]) => label)
|
||||||
|
.filter(label => keyHeaderIndexes[label] < 0)
|
||||||
|
if (missingKeyHeaders.length > 0) {
|
||||||
|
Notify.create({ type: 'negative', message: `Eksik kolon: ${missingKeyHeaders.join(', ')}` })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowMap = new Map(rows.value.map(row => [buildImportRowKeyFromObject(row), row]))
|
||||||
|
let matched = 0
|
||||||
|
let updated = 0
|
||||||
|
let unmatched = 0
|
||||||
|
|
||||||
|
for (let i = 1; i < matrix.length; i++) {
|
||||||
|
const csvRow = matrix[i]
|
||||||
|
const identity = {}
|
||||||
|
for (const [field, label] of importKeyFieldLabels) {
|
||||||
|
identity[field] = csvRow[keyHeaderIndexes[label]] ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = rowMap.get(buildImportRowKeyFromObject(identity))
|
||||||
|
if (!target) {
|
||||||
|
unmatched += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
matched += 1
|
||||||
|
|
||||||
|
let changed = false
|
||||||
|
for (const [headerLabel, field] of Object.entries(importFieldMap)) {
|
||||||
|
const idx = headers.indexOf(headerLabel)
|
||||||
|
if (idx < 0) continue
|
||||||
|
const rawValue = csvRow[idx] ?? ''
|
||||||
|
|
||||||
|
if (field === 'is_active') {
|
||||||
|
const next = parseImportedBoolean(rawValue)
|
||||||
|
if (next === null || next === target.is_active) continue
|
||||||
|
target.is_active = next
|
||||||
|
changed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = parseImportedNumber(rawValue)
|
||||||
|
if (Number(target[field] || 0) === next) continue
|
||||||
|
target[field] = next
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
markDirty(target)
|
||||||
|
updated += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated === 0 && matched === 0) {
|
||||||
|
Notify.create({ type: 'warning', message: 'CSV satirlari ekrandaki kurallarla eslesmedi' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: `CSV yüklendi. Eslesen: ${matched}, guncellenen: ${updated}, eslesmeyen: ${unmatched}`
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
Notify.create({ type: 'negative', message: err?.message || 'CSV okunamadi' })
|
||||||
|
} finally {
|
||||||
|
if (input) input.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copySelectedToSelected () {
|
||||||
|
const keys = [...copySelectedKeys.value]
|
||||||
|
if (keys.length < 2) return
|
||||||
|
const sourceKey = keys[0]
|
||||||
|
const source = rows.value.find(row => row._row_key === sourceKey)
|
||||||
|
if (!source) return
|
||||||
|
|
||||||
|
for (let i = 1; i < keys.length; i++) {
|
||||||
|
const target = rows.value.find(row => row._row_key === keys[i])
|
||||||
|
if (!target) continue
|
||||||
|
target.is_active = Boolean(source.is_active)
|
||||||
|
for (const field of numericFields) {
|
||||||
|
target[field] = source[field]
|
||||||
|
}
|
||||||
|
markDirty(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
Notify.create({ type: 'positive', message: 'Kopyalama tamamlandi' })
|
||||||
|
}
|
||||||
|
|
||||||
function isHeaderFilterField (field) {
|
function isHeaderFilterField (field) {
|
||||||
return headerFilterFieldSet.has(field)
|
return headerFilterFieldSet.has(field)
|
||||||
}
|
}
|
||||||
@@ -576,7 +1035,8 @@ async function loadRows () {
|
|||||||
timeout: 180000
|
timeout: 180000
|
||||||
})
|
})
|
||||||
rows.value = (Array.isArray(res?.data) ? res.data : []).map(normalizeWorksheetRow)
|
rows.value = (Array.isArray(res?.data) ? res.data : []).map(normalizeWorksheetRow)
|
||||||
selected.value = []
|
selectedKeyMap.value = {}
|
||||||
|
copySelectedKeys.value = []
|
||||||
if (rows.value.length === 0) {
|
if (rows.value.length === 0) {
|
||||||
emptyRetryTimer = setTimeout(loadRows, 10000)
|
emptyRetryTimer = setTimeout(loadRows, 10000)
|
||||||
}
|
}
|
||||||
@@ -588,7 +1048,7 @@ async function loadRows () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveSelected () {
|
async function saveSelected () {
|
||||||
const dirty = selected.value.filter(row => row?._dirty)
|
const dirty = rows.value.filter(row => selectedKeyMap.value?.[row._row_key] && row?._dirty)
|
||||||
if (dirty.length === 0) return
|
if (dirty.length === 0) return
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
@@ -789,6 +1249,16 @@ onBeforeUnmount(() => {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selection-header-copy {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #bf5b04;
|
||||||
|
}
|
||||||
|
|
||||||
.excel-filter-menu {
|
.excel-filter-menu {
|
||||||
min-width: 230px;
|
min-width: 230px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@@ -829,6 +1299,24 @@ onBeforeUnmount(() => {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rules-table :deep(th.copy-selection-col),
|
||||||
|
.rules-table :deep(td.copy-selection-col) {
|
||||||
|
background: #fff3e8;
|
||||||
|
color: #bf5b04;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-table :deep(th.save-selection-col),
|
||||||
|
.rules-table :deep(td.save-selection-col) {
|
||||||
|
background: #eef9ef;
|
||||||
|
color: #1b7f3a;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
.rules-table :deep(th.selection-col),
|
.rules-table :deep(th.selection-col),
|
||||||
.rules-table :deep(td.selection-col) {
|
.rules-table :deep(td.selection-col) {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -843,6 +1331,20 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copy-cell-wrap {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-table :deep(.rule-select-checkbox) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-table :deep(.rule-select-checkbox .q-checkbox__inner) {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.rules-table :deep(th.usd-col),
|
.rules-table :deep(th.usd-col),
|
||||||
.rules-table :deep(td.usd-col) {
|
.rules-table :deep(td.usd-col) {
|
||||||
background: #ecf9f0;
|
background: #ecf9f0;
|
||||||
@@ -889,4 +1391,13 @@ onBeforeUnmount(() => {
|
|||||||
outline: none;
|
outline: none;
|
||||||
border-color: #1976d2;
|
border-color: #1976d2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-legend :deep(.q-chip) {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -90,6 +90,23 @@
|
|||||||
:disable="selectedRowCount === 0"
|
:disable="selectedRowCount === 0"
|
||||||
@click="bulkDialogOpen = true"
|
@click="bulkDialogOpen = true"
|
||||||
/>
|
/>
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
flat
|
||||||
|
icon="download"
|
||||||
|
label="Sayfayi Excel'e Aktar"
|
||||||
|
:disable="filteredRows.length === 0"
|
||||||
|
@click="exportCurrentView"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
outline
|
||||||
|
icon="download_for_offline"
|
||||||
|
label="Tum Filtreyi Excel'e Aktar"
|
||||||
|
:disable="filteredRows.length === 0 || exportAllLoading"
|
||||||
|
:loading="exportAllLoading"
|
||||||
|
@click="exportAllFiltered"
|
||||||
|
/>
|
||||||
<q-pagination
|
<q-pagination
|
||||||
v-model="currentPage"
|
v-model="currentPage"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -491,8 +508,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
import { Notify } from 'quasar'
|
||||||
import { useProductPricingStore } from 'src/stores/ProductPricingStore'
|
import { useProductPricingStore } from 'src/stores/ProductPricingStore'
|
||||||
import api from 'src/services/api'
|
import api, { download } from 'src/services/api'
|
||||||
|
|
||||||
const store = useProductPricingStore()
|
const store = useProductPricingStore()
|
||||||
const PAGE_LIMIT = 250
|
const PAGE_LIMIT = 250
|
||||||
@@ -742,6 +760,7 @@ const bulkDialogOpen = ref(false)
|
|||||||
const bulkField = ref('expenseForBasePrice')
|
const bulkField = ref('expenseForBasePrice')
|
||||||
const bulkValue = ref('')
|
const bulkValue = ref('')
|
||||||
const selectedCurrencies = ref(['USD', 'EUR', 'TRY'])
|
const selectedCurrencies = ref(['USD', 'EUR', 'TRY'])
|
||||||
|
const exportAllLoading = ref(false)
|
||||||
const showSelectedOnly = ref(false)
|
const showSelectedOnly = ref(false)
|
||||||
|
|
||||||
const editableColumns = [
|
const editableColumns = [
|
||||||
@@ -859,6 +878,8 @@ const visibleColumns = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const exportableColumns = computed(() => visibleColumns.value.filter((col) => col.name !== 'select' && col.name !== 'calcAction'))
|
||||||
|
|
||||||
const stickyLeftMap = computed(() => {
|
const stickyLeftMap = computed(() => {
|
||||||
const map = {}
|
const map = {}
|
||||||
let left = 0
|
let left = 0
|
||||||
@@ -1205,6 +1226,100 @@ function formatDateDisplay (val) {
|
|||||||
return `${day}.${month}.${year}`
|
return `${day}.${month}.${year}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
return String(row?.[field] ?? '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function csvSafe (value) {
|
||||||
|
let text = String(value ?? '').replaceAll('\r', ' ').replaceAll('\n', ' ').trim()
|
||||||
|
if (text.includes(';') || text.includes('"')) {
|
||||||
|
text = `"${text.replaceAll('"', '""')}"`
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportCurrentView () {
|
||||||
|
const cols = exportableColumns.value
|
||||||
|
const list = filteredRows.value
|
||||||
|
if (cols.length === 0 || list.length === 0) return
|
||||||
|
|
||||||
|
const lines = [cols.map((col) => csvSafe(col.label)).join(';')]
|
||||||
|
for (const row of list) {
|
||||||
|
lines.push(cols.map((col) => csvSafe(exportCellValue(row, col.field))).join(';'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const bom = '\uFEFF'
|
||||||
|
const blob = new Blob([bom + lines.join('\n')], { type: 'text/csv;charset=utf-8;' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `product_pricing_${new Date().toISOString().slice(0, 10)}.csv`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
a.remove()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportAllFiltered () {
|
||||||
|
exportAllLoading.value = true
|
||||||
|
try {
|
||||||
|
const filters = buildServerFilters()
|
||||||
|
const params = {
|
||||||
|
product_code: (filters.product_code || []).join(','),
|
||||||
|
brand_group_selection: (filters.brand_group_selection || []).join(','),
|
||||||
|
marka: (filters.marka || []).join(','),
|
||||||
|
askili_yan: (filters.askili_yan || []).join(','),
|
||||||
|
kategori: (filters.kategori || []).join(','),
|
||||||
|
urun_ilk_grubu: (filters.urun_ilk_grubu || []).join(','),
|
||||||
|
urun_ana_grubu: (filters.urun_ana_grubu || []).join(','),
|
||||||
|
urun_alt_grubu: (filters.urun_alt_grubu || []).join(','),
|
||||||
|
icerik: (filters.icerik || []).join(','),
|
||||||
|
karisim: (filters.karisim || []).join(','),
|
||||||
|
sort_by: tablePagination.value?.sortBy || '',
|
||||||
|
desc: tablePagination.value?.descending ? 1 : 0,
|
||||||
|
currencies: (selectedCurrencies.value || []).join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of valueFilterFields) {
|
||||||
|
const values = valueFilters.value[field] || []
|
||||||
|
if (values.length > 0) {
|
||||||
|
params[`vf_${field}`] = values.join(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stockQtyMin = String(numberRangeFilters.value.stockQty?.min || '').trim()
|
||||||
|
const stockQtyMax = String(numberRangeFilters.value.stockQty?.max || '').trim()
|
||||||
|
if (stockQtyMin) params.stock_qty_min = stockQtyMin
|
||||||
|
if (stockQtyMax) params.stock_qty_max = stockQtyMax
|
||||||
|
|
||||||
|
const stockEntryFrom = String(dateRangeFilters.value.stockEntryDate?.from || '').trim()
|
||||||
|
const stockEntryTo = String(dateRangeFilters.value.stockEntryDate?.to || '').trim()
|
||||||
|
const lastPricingFrom = String(dateRangeFilters.value.lastPricingDate?.from || '').trim()
|
||||||
|
const lastPricingTo = String(dateRangeFilters.value.lastPricingDate?.to || '').trim()
|
||||||
|
if (stockEntryFrom) params.stock_entry_from = stockEntryFrom
|
||||||
|
if (stockEntryTo) params.stock_entry_to = stockEntryTo
|
||||||
|
if (lastPricingFrom) params.last_pricing_from = lastPricingFrom
|
||||||
|
if (lastPricingTo) params.last_pricing_to = lastPricingTo
|
||||||
|
|
||||||
|
const blob = await download('/pricing/products/export-all', params)
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `product_pricing_all_${new Date().toISOString().slice(0, 10)}.csv`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
a.remove()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
} catch (err) {
|
||||||
|
Notify.create({ type: 'negative', message: err?.message || 'Tum filtre export alinamadi' })
|
||||||
|
} finally {
|
||||||
|
exportAllLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function needsRepricing (row) {
|
function needsRepricing (row) {
|
||||||
const stockEntryDate = String(row?.stockEntryDate || '').trim()
|
const stockEntryDate = String(row?.stockEntryDate || '').trim()
|
||||||
const lastPricingDate = String(row?.lastPricingDate || '').trim()
|
const lastPricingDate = String(row?.lastPricingDate || '').trim()
|
||||||
|
|||||||
@@ -569,7 +569,8 @@ async function handleDownload() {
|
|||||||
dateFrom.value, // startDate
|
dateFrom.value, // startDate
|
||||||
dateTo.value, // endDate
|
dateTo.value, // endDate
|
||||||
selectedMonType.value, // <-- eklendi (['1','2'] veya ['1','3'])
|
selectedMonType.value, // <-- eklendi (['1','2'] veya ['1','3'])
|
||||||
localeStore.backendLangCode
|
localeStore.backendLangCode,
|
||||||
|
statementheaderStore.headers
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log("[DEBUG] Store’dan gelen result:", result)
|
console.log("[DEBUG] Store’dan gelen result:", result)
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
// src/stores/downloadstpdfStore.js
|
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { download, extractApiErrorDetail } from 'src/services/api'
|
import api, { download, extractApiErrorDetail } from 'src/services/api'
|
||||||
|
|
||||||
export const useDownloadstpdfStore = defineStore('downloadstpdf', {
|
export const useDownloadstpdfStore = defineStore('downloadstpdf', {
|
||||||
actions: {
|
actions: {
|
||||||
/* ==========================================================
|
async downloadPDF(accountCode, startDate, endDate, parislemler = [], langcode = 'TR', rows = []) {
|
||||||
📄 PDF İNDİR / AÇ
|
|
||||||
========================================================== */
|
|
||||||
async downloadPDF(accountCode, startDate, endDate, parislemler = [], langcode = 'TR') {
|
|
||||||
try {
|
try {
|
||||||
// 🔹 Query params
|
let pdfBlob
|
||||||
|
|
||||||
|
if (Array.isArray(rows) && rows.length > 0) {
|
||||||
|
const response = await api.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/export-pdf',
|
||||||
|
data: {
|
||||||
|
account_code: accountCode,
|
||||||
|
start_date: startDate,
|
||||||
|
end_date: endDate,
|
||||||
|
lang_code: langcode || 'TR',
|
||||||
|
rows
|
||||||
|
},
|
||||||
|
responseType: 'blob',
|
||||||
|
timeout: 180000
|
||||||
|
})
|
||||||
|
pdfBlob = response.data
|
||||||
|
} else {
|
||||||
const params = {
|
const params = {
|
||||||
accountcode: accountCode,
|
accountcode: accountCode,
|
||||||
startdate: startDate,
|
startdate: startDate,
|
||||||
@@ -23,30 +36,24 @@ export const useDownloadstpdfStore = defineStore('downloadstpdf', {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 MERKEZİ API — BLOB
|
pdfBlob = await download('/export-pdf', params)
|
||||||
const blob = await download('/export-pdf', params)
|
}
|
||||||
|
|
||||||
// 🔹 Blob → URL
|
|
||||||
const pdfUrl = window.URL.createObjectURL(
|
const pdfUrl = window.URL.createObjectURL(
|
||||||
new Blob([blob], { type: 'application/pdf' })
|
new Blob([pdfBlob], { type: 'application/pdf' })
|
||||||
)
|
)
|
||||||
|
|
||||||
// 🔹 Yeni sekmede aç
|
|
||||||
window.open(pdfUrl, '_blank')
|
window.open(pdfUrl, '_blank')
|
||||||
|
|
||||||
console.log('✅ PDF yeni sekmede açıldı')
|
return { ok: true, message: 'PDF hazirlandi' }
|
||||||
return { ok: true, message: '📄 PDF hazırlandı' }
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const detail = await extractApiErrorDetail(err)
|
const detail = await extractApiErrorDetail(err)
|
||||||
const status = err?.status || err?.response?.status || '-'
|
const status = err?.status || err?.response?.status || '-'
|
||||||
console.error(`? PDF a<EFBFBD>ma hatas<EFBFBD> [${status}] /export-pdf: ${detail}`)
|
console.error(`PDF acma hatasi [${status}] /export-pdf: ${detail}`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
message:
|
message: detail || 'PDF alinamadi'
|
||||||
detail ||
|
|
||||||
'PDF al<61>namad<61>'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user