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

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