Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-06-04 14:33:10 +03:00
parent 00626152c2
commit 7b1588d69d
11 changed files with 2065 additions and 78 deletions

View File

@@ -579,6 +579,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"finance", "export",
wrapV3(routes.ExportPDFHandler(mssql)),
)
bindV3(r, pgDB,
"/api/export-pdf", "POST",
"finance", "export",
wrapV3(routes.ExportPDFHandler(mssql)),
)
bindV3(r, pgDB,
"/api/exportstamentheaderreport-pdf", "GET",
@@ -795,6 +800,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"pricing", "view",
wrapV3(http.HandlerFunc(routes.GetProductPricingFilterOptionsHandler)),
)
bindV3(r, pgDB,
"/api/pricing/products/export-all", "GET",
"pricing", "view",
wrapV3(http.HandlerFunc(routes.ExportAllProductPricingHandler)),
)
bindV3(r, pgDB,
"/api/pricing/brand-classification/lookups", "GET",
"pricing", "view",
@@ -840,6 +850,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"pricing", "view",
wrapV3(routes.GetPricingParameterRulesHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/pricing-rules/export-all", "GET",
"pricing", "view",
wrapV3(routes.ExportPricingRulesHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/production-product-costing/no-cost-products", "GET",
"costing", "view",

View File

@@ -36,6 +36,583 @@ type ProductPricingPage struct {
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) {
result := ProductPricingPage{
Rows: []models.ProductPricing{},

View File

@@ -5,9 +5,12 @@ import (
"bssapp-backend/utils"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
)
// 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 {
return queries.PricingRuleOptionFilters{
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 {
raw = strings.TrimSpace(raw)
if raw == "" {

View File

@@ -2,10 +2,12 @@ package routes
import (
"bssapp-backend/auth"
"bssapp-backend/models"
"bssapp-backend/queries"
"context"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strconv"
@@ -211,6 +213,392 @@ func GetProductPricingFilterOptionsHandler(w http.ResponseWriter, r *http.Reques
_ = 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 {
if r != nil {
if id := strings.TrimSpace(r.Header.Get("X-Request-ID")); id != "" {

View File

@@ -8,6 +8,7 @@ import (
"bssapp-backend/queries"
"bytes"
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
@@ -19,6 +20,28 @@ import (
"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 ============================ */
// A4 Landscape (mm)
@@ -468,11 +491,25 @@ func ExportPDFHandler(mssql *sql.DB) http.HandlerFunc {
log.Printf("▶️ ExportPDFHandler: account=%s start=%s end=%s parislemler=%v",
accountCode, startDate, endDate, parislemler)
// 1) Header verileri
headers, belgeNos, err := queries.GetStatementsPDF(r.Context(), accountCode, startDate, endDate, langCode, parislemler)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
var (
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 {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
log.Printf("✅ Header verileri alındı: %d kayıt, %d belge no", len(headers), len(belgeNos))
@@ -506,10 +543,7 @@ func ExportPDFHandler(mssql *sql.DB) http.HandlerFunc {
for _, k := range order {
sort.SliceStable(groups[k].rows, func(i, j int) bool {
ri, rj := groups[k].rows[i], groups[k].rows[j]
if ri.BelgeTarihi == rj.BelgeTarihi {
return ri.BelgeNo < rj.BelgeNo
}
return ri.BelgeTarihi < rj.BelgeTarihi
return parseStatementHeaderDate(ri.BelgeTarihi).Before(parseStatementHeaderDate(rj.BelgeTarihi))
})
if n := len(groups[k].rows); n > 0 {
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:
- Header artık dinamik yüksekliğe sahip (drawPageHeader -> contentTopY döner).

View File

@@ -395,20 +395,6 @@ function toggleFiltersCollapsed() {
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() {
if (!canExportFinance.value) {
$q.notify({ type: 'negative', message: 'PDF export yetkiniz yok', position: 'top-right' })
@@ -420,7 +406,17 @@ async function downloadAgingScreenPDF() {
}
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' }))
window.open(pdfUrl, '_blank')
} catch (err) {

View File

@@ -3,9 +3,6 @@
<div class="top-bar row items-center justify-between q-mb-xs">
<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>
<q-btn
flat
@@ -19,9 +16,17 @@
<div class="action-bar row items-center justify-between q-mb-xs">
<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-xs">
<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>
<q-btn
flat
color="primary"
@@ -30,6 +35,38 @@
:disable="!hasAnyFilter"
@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
color="primary"
unelevated
@@ -39,6 +76,13 @@
:label="`Kaydet (${selectedDirtyCount})`"
@click="saveSelected"
/>
<input
ref="fileInputRef"
type="file"
accept=".csv,text/csv"
class="hidden-file-input"
@change="onImportFileChange"
>
</div>
</div>
@@ -75,10 +119,17 @@
<q-checkbox
v-if="col.name === 'select'"
dense
size="24px"
class="rule-select-checkbox"
color="positive"
:model-value="allSelectedVisible"
:indeterminate="someSelectedVisible && !allSelectedVisible"
@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">
<span>{{ col.label }}</span>
<q-btn
@@ -183,9 +234,32 @@
:class="[col.classes, { 'sticky-col': isStickyCol(col.name), 'sticky-boundary': isStickyBoundary(col.name) }]"
:style="getBodyCellStyle(col)"
>
<div
v-if="col.name === 'copy_select'"
class="row items-center no-wrap justify-center copy-cell-wrap"
>
<q-checkbox
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-if="col.name === 'select'"
v-else-if="col.name === 'select'"
dense
size="24px"
class="rule-select-checkbox"
color="positive"
:model-value="isRowSelected(props.row)"
@update:model-value="(value) => setRowSelected(props.row, value)"
/>
@@ -219,7 +293,7 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { Notify } from 'quasar'
import api from 'src/services/api'
import api, { download } from 'src/services/api'
import { usePermissionStore } from 'stores/permissionStore'
const perm = usePermissionStore()
@@ -228,7 +302,9 @@ const canUpdate = computed(() => perm.hasApiPermission('pricing:update'))
const loading = ref(false)
const saving = ref(false)
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 })
let emptyRetryTimer = null
@@ -238,6 +314,46 @@ const numericFields = new Set([
'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 = [
'has_rule', 'is_active', 'askili_yan', 'kategori', 'urun_ilk_grubu', 'urun_ana_grubu',
'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group'
@@ -265,7 +381,8 @@ function col (name, label, field, width, extra = {}) {
}
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('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' }),
@@ -307,7 +424,7 @@ const columns = [
]
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'
]
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 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 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(() => {
return [...headerFilterFieldSet].some(field => hasFilter(field))
})
@@ -456,21 +594,54 @@ function getBodyCellStyle (column) {
}
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) {
const key = row?._row_key
if (!key) return
const next = { ...(selectedKeyMap.value || {}) }
if (value) {
if (!isRowSelected(row)) selected.value = [...selected.value, row]
return
next[key] = true
} 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) {
const keys = new Set(visibleRowKeys.value)
const remaining = selected.value.filter(row => !keys.has(row._row_key))
selected.value = value ? [...remaining, ...filteredRows.value] : remaining
const next = { ...(selectedKeyMap.value || {}) }
for (const key of visibleRowKeys.value) {
if (value) next[key] = true
else delete next[key]
}
selectedKeyMap.value = next
}
function selectDirtyRow (row) {
@@ -487,6 +658,294 @@ function updateNumber (row, field, value) {
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) {
return headerFilterFieldSet.has(field)
}
@@ -576,7 +1035,8 @@ async function loadRows () {
timeout: 180000
})
rows.value = (Array.isArray(res?.data) ? res.data : []).map(normalizeWorksheetRow)
selected.value = []
selectedKeyMap.value = {}
copySelectedKeys.value = []
if (rows.value.length === 0) {
emptyRetryTimer = setTimeout(loadRows, 10000)
}
@@ -588,7 +1048,7 @@ async function loadRows () {
}
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
saving.value = true
try {
@@ -789,6 +1249,16 @@ onBeforeUnmount(() => {
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 {
min-width: 230px;
padding: 8px;
@@ -829,6 +1299,24 @@ onBeforeUnmount(() => {
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(td.selection-col) {
background: #fff;
@@ -843,6 +1331,20 @@ onBeforeUnmount(() => {
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(td.usd-col) {
background: #ecf9f0;
@@ -889,4 +1391,13 @@ onBeforeUnmount(() => {
outline: none;
border-color: #1976d2;
}
.action-legend :deep(.q-chip) {
font-size: 11px;
font-weight: 700;
}
.hidden-file-input {
display: none;
}
</style>

View File

@@ -90,6 +90,23 @@
:disable="selectedRowCount === 0"
@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
v-model="currentPage"
color="primary"
@@ -491,8 +508,9 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { Notify } from 'quasar'
import { useProductPricingStore } from 'src/stores/ProductPricingStore'
import api from 'src/services/api'
import api, { download } from 'src/services/api'
const store = useProductPricingStore()
const PAGE_LIMIT = 250
@@ -742,6 +760,7 @@ const bulkDialogOpen = ref(false)
const bulkField = ref('expenseForBasePrice')
const bulkValue = ref('')
const selectedCurrencies = ref(['USD', 'EUR', 'TRY'])
const exportAllLoading = ref(false)
const showSelectedOnly = ref(false)
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 map = {}
let left = 0
@@ -1205,6 +1226,100 @@ function formatDateDisplay (val) {
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) {
const stockEntryDate = String(row?.stockEntryDate || '').trim()
const lastPricingDate = String(row?.lastPricingDate || '').trim()

View File

@@ -569,7 +569,8 @@ async function handleDownload() {
dateFrom.value, // startDate
dateTo.value, // endDate
selectedMonType.value, // <-- eklendi (['1','2'] veya ['1','3'])
localeStore.backendLangCode
localeStore.backendLangCode,
statementheaderStore.headers
)
console.log("[DEBUG] Storedan gelen result:", result)

View File

@@ -1,52 +1,59 @@
// src/stores/downloadstpdfStore.js
import { defineStore } from 'pinia'
import { download, extractApiErrorDetail } from 'src/services/api'
import api, { download, extractApiErrorDetail } from 'src/services/api'
export const useDownloadstpdfStore = defineStore('downloadstpdf', {
actions: {
/* ==========================================================
📄 PDF İNDİR / AÇ
========================================================== */
async downloadPDF(accountCode, startDate, endDate, parislemler = [], langcode = 'TR') {
async downloadPDF(accountCode, startDate, endDate, parislemler = [], langcode = 'TR', rows = []) {
try {
// 🔹 Query params
const params = {
accountcode: accountCode,
startdate: startDate,
enddate: endDate,
langcode: langcode || 'TR'
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 = {
accountcode: accountCode,
startdate: startDate,
enddate: endDate,
langcode: langcode || 'TR'
}
if (Array.isArray(parislemler) && parislemler.length > 0) {
params.parislemler = parislemler.filter(
p => p !== undefined && p !== null && p !== ''
)
}
pdfBlob = await download('/export-pdf', params)
}
if (Array.isArray(parislemler) && parislemler.length > 0) {
params.parislemler = parislemler.filter(
p => p !== undefined && p !== null && p !== ''
)
}
// 🔥 MERKEZİ API — BLOB
const blob = await download('/export-pdf', params)
// 🔹 Blob → URL
const pdfUrl = window.URL.createObjectURL(
new Blob([blob], { type: 'application/pdf' })
new Blob([pdfBlob], { type: 'application/pdf' })
)
// 🔹 Yeni sekmede aç
window.open(pdfUrl, '_blank')
console.log('✅ PDF yeni sekmede açıldı')
return { ok: true, message: '📄 PDF hazırlandı' }
return { ok: true, message: 'PDF hazirlandi' }
} catch (err) {
const detail = await extractApiErrorDetail(err)
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 {
ok: false,
message:
detail ||
'PDF al<61>namad<61>'
message: detail || 'PDF alinamadi'
}
}
}