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).