Files
bssapp/svc/routes/customer_balance_pdf.go
2026-03-17 12:04:56 +03:00

903 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/models"
"bssapp-backend/queries"
"bytes"
"database/sql"
"fmt"
"math"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/jung-kurt/gofpdf"
)
type balanceSummaryPDF struct {
AnaCariKodu string
AnaCariAdi string
Piyasa string
Temsilci string
RiskDurumu string
Bakiye12Map map[string]float64
Bakiye13Map map[string]float64
USDBakiye12 float64
TLBakiye12 float64
USDBakiye13 float64
TLBakiye13 float64
VadeGun float64
VadeBelge float64
}
func ExportCustomerBalancePDFHandler(_ *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
selectedDate := strings.TrimSpace(r.URL.Query().Get("selected_date"))
if selectedDate == "" {
selectedDate = time.Now().Format("2006-01-02")
}
params := models.CustomerBalanceListParams{
SelectedDate: selectedDate,
CariSearch: strings.TrimSpace(r.URL.Query().Get("cari_search")),
CariIlkGrup: strings.TrimSpace(r.URL.Query().Get("cari_ilk_grup")),
Piyasa: strings.TrimSpace(r.URL.Query().Get("piyasa")),
Temsilci: strings.TrimSpace(r.URL.Query().Get("temsilci")),
RiskDurumu: strings.TrimSpace(r.URL.Query().Get("risk_durumu")),
IslemTipi: strings.TrimSpace(r.URL.Query().Get("islem_tipi")),
Ulke: strings.TrimSpace(r.URL.Query().Get("ulke")),
Il: strings.TrimSpace(r.URL.Query().Get("il")),
Ilce: strings.TrimSpace(r.URL.Query().Get("ilce")),
}
detailed := parseBoolQuery(r.URL.Query().Get("detailed"))
excludeZero12 := parseBoolQuery(r.URL.Query().Get("exclude_zero_12"))
excludeZero13 := parseBoolQuery(r.URL.Query().Get("exclude_zero_13"))
rows, err := queries.GetCustomerBalanceList(r.Context(), params)
if err != nil {
http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError)
return
}
rows = filterCustomerBalanceRowsForPDF(rows, excludeZero12, excludeZero13)
summaries, detailsByMaster := buildCustomerBalancePDFData(rows)
pdf := gofpdf.New("L", "mm", "A4", "")
pdf.SetMargins(8, 8, 8)
pdf.SetAutoPageBreak(false, 12)
if err := registerDejavuFonts(pdf, "dejavu"); err != nil {
http.Error(w, "pdf font error: "+err.Error(), http.StatusInternalServerError)
return
}
if err := safeDrawCustomerBalancePDF(
pdf,
selectedDate,
params.CariSearch,
detailed,
"Cari Bakiye Listesi",
false,
summaries,
detailsByMaster,
); err != nil {
pdf = gofpdf.New("L", "mm", "A4", "")
pdf.SetMargins(8, 8, 8)
pdf.SetAutoPageBreak(true, 12)
if ferr := registerDejavuFonts(pdf, "dejavu"); ferr != nil {
http.Error(w, "pdf font error: "+ferr.Error(), http.StatusInternalServerError)
return
}
drawCustomerBalancePDFFallback(pdf, selectedDate, params.CariSearch, "Cari Bakiye Listesi", summaries, false)
}
if err := pdf.Error(); err != nil {
http.Error(w, "pdf render error: "+err.Error(), http.StatusInternalServerError)
return
}
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
http.Error(w, "pdf output error: "+err.Error(), http.StatusInternalServerError)
return
}
filename := "customer-balance-summary.pdf"
if detailed {
filename = "customer-balance-detailed.pdf"
}
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", filename))
_, _ = w.Write(buf.Bytes())
}
}
func parseBoolQuery(v string) bool {
switch strings.ToLower(strings.TrimSpace(v)) {
case "1", "true", "yes", "on":
return true
default:
return false
}
}
func filterCustomerBalanceRowsForPDF(rows []models.CustomerBalanceListRow, excludeZero12, excludeZero13 bool) []models.CustomerBalanceListRow {
out := make([]models.CustomerBalanceListRow, 0, len(rows))
for _, row := range rows {
if excludeZero12 && row.Bakiye12 == 0 {
continue
}
if excludeZero13 && row.Bakiye13 == 0 {
continue
}
out = append(out, row)
}
return out
}
func buildCustomerBalancePDFData(rows []models.CustomerBalanceListRow) ([]balanceSummaryPDF, map[string][]models.CustomerBalanceListRow) {
summaryMap := make(map[string]*balanceSummaryPDF)
detailsByMaster := make(map[string][]models.CustomerBalanceListRow)
vadeWeightMap := make(map[string]float64)
vadeGunSumMap := make(map[string]float64)
vadeBelgeSumMap := make(map[string]float64)
for _, row := range rows {
master := strings.TrimSpace(row.AnaCariKodu)
if master == "" {
master = strings.TrimSpace(row.CariKodu)
}
if master == "" {
continue
}
s := summaryMap[master]
if s == nil {
s = &balanceSummaryPDF{
AnaCariKodu: master,
AnaCariAdi: strings.TrimSpace(row.AnaCariAdi),
Piyasa: strings.TrimSpace(row.Piyasa),
Temsilci: strings.TrimSpace(row.Temsilci),
RiskDurumu: strings.TrimSpace(row.RiskDurumu),
Bakiye12Map: map[string]float64{},
Bakiye13Map: map[string]float64{},
}
summaryMap[master] = s
}
if s.AnaCariAdi == "" && strings.TrimSpace(row.AnaCariAdi) != "" {
s.AnaCariAdi = strings.TrimSpace(row.AnaCariAdi)
}
if s.Piyasa == "" && strings.TrimSpace(row.Piyasa) != "" {
s.Piyasa = strings.TrimSpace(row.Piyasa)
}
if s.Temsilci == "" && strings.TrimSpace(row.Temsilci) != "" {
s.Temsilci = strings.TrimSpace(row.Temsilci)
}
if s.RiskDurumu == "" && strings.TrimSpace(row.RiskDurumu) != "" {
s.RiskDurumu = strings.TrimSpace(row.RiskDurumu)
}
curr := strings.ToUpper(strings.TrimSpace(row.CariDoviz))
if curr == "" {
curr = "N/A"
}
s.Bakiye12Map[curr] += row.Bakiye12
s.Bakiye13Map[curr] += row.Bakiye13
s.USDBakiye12 += row.USDBakiye12
s.TLBakiye12 += row.TLBakiye12
s.USDBakiye13 += row.USDBakiye13
s.TLBakiye13 += row.TLBakiye13
w := absFloatExcel(row.USDBakiye12) + absFloatExcel(row.TLBakiye12) + absFloatExcel(row.USDBakiye13) + absFloatExcel(row.TLBakiye13)
if w > 0 {
vadeWeightMap[master] += w
vadeGunSumMap[master] += row.VadeGun * w
vadeBelgeSumMap[master] += row.VadeBelgeGun * w
}
detailsByMaster[master] = append(detailsByMaster[master], row)
}
masters := make([]string, 0, len(summaryMap))
for m := range summaryMap {
masters = append(masters, m)
}
sort.Strings(masters)
summaries := make([]balanceSummaryPDF, 0, len(masters))
for _, m := range masters {
if base := vadeWeightMap[m]; base > 0 {
summaryMap[m].VadeGun = vadeGunSumMap[m] / base
summaryMap[m].VadeBelge = vadeBelgeSumMap[m] / base
}
summaries = append(summaries, *summaryMap[m])
d := detailsByMaster[m]
sort.SliceStable(d, func(i, j int) bool {
if d[i].CariKodu == d[j].CariKodu {
if d[i].CariDoviz == d[j].CariDoviz {
si, _ := strconv.Atoi(d[i].Sirket)
sj, _ := strconv.Atoi(d[j].Sirket)
return si < sj
}
return d[i].CariDoviz < d[j].CariDoviz
}
return d[i].CariKodu < d[j].CariKodu
})
detailsByMaster[m] = d
}
return summaries, detailsByMaster
}
func drawCustomerBalancePDF(
pdf *gofpdf.Fpdf,
selectedDate string,
searchText string,
detailed bool,
reportTitle string,
includeVadeColumns bool,
summaries []balanceSummaryPDF,
detailsByMaster map[string][]models.CustomerBalanceListRow,
) {
pageW, pageH := pdf.GetPageSize()
marginL, marginT, marginR, marginB := 8.0, 8.0, 8.0, 12.0
tableW := pageW - marginL - marginR
pageNoColor := [3]int{90, 90, 90}
pdf.SetFooterFunc(func() {
pdf.SetY(-8)
pdf.SetFont("dejavu", "", 8)
pdf.SetTextColor(pageNoColor[0], pageNoColor[1], pageNoColor[2])
pdf.CellFormat(0, 4, fmt.Sprintf("Sayfa %d", pdf.PageNo()), "", 0, "R", false, 0, "")
})
summaryCols := []string{"Ana Cari Kod", "Ana Cari Detay", "Piyasa", "Temsilci", "Risk", "1_2 Pr.Br", "1_3 Pr.Br", "1_2 USD", "1_2 TRY", "1_3 USD", "1_3 TRY"}
summaryWeights := []float64{18, 42, 16, 16, 14, 24, 24, 14, 14, 14, 14}
if includeVadeColumns {
// Aging raporu (A4 dikey) için kolonlar sıkıştırılır.
summaryCols = append(summaryCols, "Vade Gun", "Belge Tarihi Gun")
summaryWeights = []float64{14, 28, 10, 10, 10, 13, 13, 9, 9, 9, 9, 8, 10}
}
summaryW := normalizeWidths(summaryWeights, tableW)
detailCols := []string{"Cari Kod", "Cari Detay", "Sirket", "Muhasebe", "Doviz", "1_2 Pr.Br", "1_3 Pr.Br", "1_2 USD", "1_2 TRY", "1_3 USD", "1_3 TRY"}
detailWeights := []float64{23, 40, 9, 18, 9, 20, 20, 13, 13, 13, 13}
if includeVadeColumns {
detailCols = append(detailCols, "Vade Gun", "Belge Tarihi Gun")
detailWeights = append(detailWeights, 11, 14)
}
detailW := normalizeWidths(detailWeights, tableW)
header := func() {
pdf.AddPage()
titleX := marginL
if logoPath, err := resolvePdfImagePath("Baggi-Tekstil-A.s-Logolu.jpeg"); err == nil {
pdf.ImageOptions(logoPath, marginL, marginT-1, 34, 0, false, gofpdf.ImageOptions{}, 0, "")
titleX = marginL + 38
}
pdf.SetFont("dejavu", "B", 15)
pdf.SetTextColor(149, 113, 22)
pdf.SetXY(titleX, marginT)
title := strings.TrimSpace(reportTitle)
if title == "" {
title = "Cari Bakiye Listesi"
}
pdf.CellFormat(140, 7, title, "", 0, "L", false, 0, "")
pdf.SetFont("dejavu", "", 9)
pdf.SetTextColor(20, 20, 20)
pdf.SetXY(pageW-marginR-80, marginT+1)
pdf.CellFormat(80, 5, "Tarih: "+formatDateTR(selectedDate), "", 0, "R", false, 0, "")
mode := "Detaysiz"
if detailed {
mode = "Detayli"
}
pdf.SetXY(pageW-marginR-80, marginT+6)
pdf.CellFormat(80, 5, "Mod: "+mode, "", 0, "R", false, 0, "")
if strings.TrimSpace(searchText) != "" {
pdf.SetXY(titleX, marginT+8)
pdf.CellFormat(tableW-(titleX-marginL), 5, "Arama: "+searchText, "", 0, "L", false, 0, "")
}
pdf.SetDrawColor(149, 113, 22)
pdf.Line(marginL, marginT+14, pageW-marginR, marginT+14)
pdf.SetDrawColor(210, 210, 210)
pdf.SetY(marginT + 17)
}
needPage := func(needH float64) bool {
return pdf.GetY()+needH+marginB > pageH
}
drawSummaryHeader := func() {
pdf.SetFont("dejavu", "B", 7.5)
pdf.SetFillColor(149, 113, 22)
pdf.SetTextColor(255, 255, 255)
y := pdf.GetY()
x := marginL
for i, c := range summaryCols {
if i >= len(summaryW) {
break
}
pdf.Rect(x, y, summaryW[i], 7, "DF")
pdf.SetXY(x+1, y+1.2)
pdf.CellFormat(summaryW[i]-2, 4.6, c, "", 0, "C", false, 0, "")
x += summaryW[i]
}
pdf.SetY(y + 7)
}
drawDetailHeader := func() {
pdf.SetFont("dejavu", "B", 7.2)
pdf.SetFillColor(149, 113, 22)
pdf.SetTextColor(255, 255, 255)
y := pdf.GetY()
x := marginL
for i, c := range detailCols {
if i >= len(detailW) {
break
}
pdf.Rect(x, y, detailW[i], 6, "DF")
pdf.SetXY(x+1, y+1)
pdf.CellFormat(detailW[i]-2, 4, c, "", 0, "C", false, 0, "")
x += detailW[i]
}
pdf.SetY(y + 6)
}
header()
drawSummaryHeader()
pdf.SetFont("dejavu", "", 7.2)
pdf.SetTextColor(20, 20, 20)
totalUSD12, totalTRY12 := 0.0, 0.0
totalUSD13, totalTRY13 := 0.0, 0.0
totalPrBr12 := map[string]float64{}
totalPrBr13 := map[string]float64{}
totalVadeBase, totalVadeSum, totalVadeBelgeSum := 0.0, 0.0, 0.0
for _, s := range summaries {
totalUSD12 += s.USDBakiye12
totalTRY12 += s.TLBakiye12
totalUSD13 += s.USDBakiye13
totalTRY13 += s.TLBakiye13
for k, v := range s.Bakiye12Map {
totalPrBr12[k] += v
}
for k, v := range s.Bakiye13Map {
totalPrBr13[k] += v
}
w := absFloatExcel(s.USDBakiye12) + absFloatExcel(s.TLBakiye12) + absFloatExcel(s.USDBakiye13) + absFloatExcel(s.TLBakiye13)
if w > 0 {
totalVadeBase += w
totalVadeSum += s.VadeGun * w
totalVadeBelgeSum += s.VadeBelge * w
}
}
totalsRow := []string{
"TOPLAM",
"",
"",
"",
"",
formatCurrencyMapPDF(totalPrBr12),
formatCurrencyMapPDF(totalPrBr13),
formatMoneyPDF(totalUSD12),
formatMoneyPDF(totalTRY12),
formatMoneyPDF(totalUSD13),
formatMoneyPDF(totalTRY13),
}
if includeVadeColumns {
totalVade, totalVadeBelge := 0.0, 0.0
if totalVadeBase > 0 {
totalVade = totalVadeSum / totalVadeBase
totalVadeBelge = totalVadeBelgeSum / totalVadeBase
}
totalsRow = append(totalsRow, formatDayUpPDF(totalVade), formatDayUpPDF(totalVadeBelge))
}
totalH := calcPDFRowHeightCapped(pdf, totalsRow, summaryW, map[int]int{0: 1, 1: 1, 2: 1, 3: 1, 5: 2, 6: 2}, 6.2, 3.4)
if needPage(totalH) {
header()
drawSummaryHeader()
}
pdf.SetFont("dejavu", "B", 6.8)
pdf.SetFillColor(218, 193, 151)
pdf.SetTextColor(20, 20, 20)
totalY := pdf.GetY()
totalX := marginL
for i, v := range totalsRow {
if i >= len(summaryW) {
break
}
pdf.Rect(totalX, totalY, summaryW[i], totalH, "FD")
align := "L"
if i >= 7 {
align = "R"
}
if includeVadeColumns && (i == len(totalsRow)-1 || i == len(totalsRow)-2) {
align = "C"
}
if i == 5 || i == 6 {
drawPDFCellWrappedCapped(pdf, v, totalX, totalY, summaryW[i], totalH, align, 3.4, 2)
} else {
drawPDFCellWrapped(pdf, v, totalX, totalY, summaryW[i], totalH, align, 3.4)
}
totalX += summaryW[i]
}
pdf.SetY(totalY + totalH)
pdf.SetFont("dejavu", "", 7.2)
pdf.SetTextColor(20, 20, 20)
for _, s := range summaries {
row := []string{
s.AnaCariKodu,
s.AnaCariAdi,
s.Piyasa,
s.Temsilci,
s.RiskDurumu,
formatCurrencyMapPDF(s.Bakiye12Map),
formatCurrencyMapPDF(s.Bakiye13Map),
formatMoneyPDF(s.USDBakiye12),
formatMoneyPDF(s.TLBakiye12),
formatMoneyPDF(s.USDBakiye13),
formatMoneyPDF(s.TLBakiye13),
}
if includeVadeColumns {
row = append(row, formatDayUpPDF(s.VadeGun), formatDayUpPDF(s.VadeBelge))
}
rowH := calcPDFRowHeightCapped(pdf, row, summaryW, map[int]int{0: 3, 1: 3, 2: 3, 3: 3}, 6.2, 3.6)
if needPage(rowH) {
header()
drawSummaryHeader()
pdf.SetFont("dejavu", "", 7.2)
pdf.SetTextColor(20, 20, 20)
}
y := pdf.GetY()
x := marginL
for i, v := range row {
if i >= len(summaryW) {
break
}
pdf.Rect(x, y, summaryW[i], rowH, "")
align := "L"
if i >= 7 {
align = "R"
}
if includeVadeColumns && (i == len(row)-1 || i == len(row)-2) {
align = "C"
}
if i <= 3 {
drawPDFCellWrappedCapped(pdf, v, x, y, summaryW[i], rowH, align, 3.6, 3)
} else {
drawPDFCellWrapped(pdf, v, x, y, summaryW[i], rowH, align, 3.6)
}
x += summaryW[i]
}
pdf.SetY(y + rowH)
}
if !detailed {
return
}
pdf.Ln(1.8)
for _, s := range summaries {
rows := detailsByMaster[s.AnaCariKodu]
if len(rows) == 0 {
continue
}
if needPage(12.4) {
header()
}
pdf.SetFont("dejavu", "B", 8)
pdf.SetFillColor(218, 193, 151)
pdf.SetTextColor(20, 20, 20)
y := pdf.GetY()
pdf.Rect(marginL, y, tableW, 6.2, "DF")
pdf.SetXY(marginL+1.5, y+1)
pdf.CellFormat(tableW-3, 4.2, "Detay: "+s.AnaCariKodu, "", 0, "L", false, 0, "")
pdf.SetY(y + 6.2)
drawDetailHeader()
pdf.SetFont("dejavu", "", 7)
pdf.SetTextColor(40, 40, 40)
for _, r := range rows {
line := []string{
r.CariKodu,
r.CariDetay,
r.Sirket,
r.MuhasebeKodu,
r.CariDoviz,
formatMoneyPDF(r.Bakiye12),
formatMoneyPDF(r.Bakiye13),
formatMoneyPDF(r.USDBakiye12),
formatMoneyPDF(r.TLBakiye12),
formatMoneyPDF(r.USDBakiye13),
formatMoneyPDF(r.TLBakiye13),
}
if includeVadeColumns {
line = append(line, formatDayUpPDF(r.VadeGun), formatDayUpPDF(r.VadeBelgeGun))
}
rowH := calcPDFRowHeight(pdf, line, detailW, map[int]bool{1: true}, 5.8, 3.3)
if needPage(rowH) {
header()
pdf.SetFont("dejavu", "B", 8)
pdf.SetFillColor(218, 193, 151)
pdf.SetTextColor(20, 20, 20)
y := pdf.GetY()
pdf.Rect(marginL, y, tableW, 6.2, "DF")
pdf.SetXY(marginL+1.5, y+1)
pdf.CellFormat(tableW-3, 4.2, "Detay: "+s.AnaCariKodu, "", 0, "L", false, 0, "")
pdf.SetY(y + 6.2)
drawDetailHeader()
pdf.SetFont("dejavu", "", 7)
pdf.SetTextColor(40, 40, 40)
}
rowY := pdf.GetY()
rowX := marginL
for i, v := range line {
if i >= len(detailW) {
break
}
pdf.Rect(rowX, rowY, detailW[i], rowH, "")
align := "L"
if i >= 5 {
align = "R"
}
if includeVadeColumns && (i == len(line)-1 || i == len(line)-2) {
align = "C"
}
drawPDFCellWrapped(pdf, v, rowX, rowY, detailW[i], rowH, align, 3.3)
rowX += detailW[i]
}
pdf.SetY(rowY + rowH)
}
pdf.Ln(1.2)
}
}
func safeDrawCustomerBalancePDF(
pdf *gofpdf.Fpdf,
selectedDate string,
searchText string,
detailed bool,
reportTitle string,
includeVadeColumns bool,
summaries []balanceSummaryPDF,
detailsByMaster map[string][]models.CustomerBalanceListRow,
) (err error) {
defer func() {
if rec := recover(); rec != nil {
err = fmt.Errorf("draw panic: %v", rec)
}
}()
drawCustomerBalancePDF(pdf, selectedDate, searchText, detailed, reportTitle, includeVadeColumns, summaries, detailsByMaster)
return nil
}
func drawCustomerBalancePDFFallback(
pdf *gofpdf.Fpdf,
selectedDate string,
searchText string,
reportTitle string,
summaries []balanceSummaryPDF,
includeVadeColumns bool,
) {
pdf.AddPage()
pdf.SetFont("dejavu", "B", 13)
pdf.SetTextColor(149, 113, 22)
pdf.CellFormat(0, 8, reportTitle, "", 1, "L", false, 0, "")
pdf.SetFont("dejavu", "", 9)
pdf.SetTextColor(20, 20, 20)
pdf.CellFormat(0, 5, "Tarih: "+formatDateTR(selectedDate), "", 1, "L", false, 0, "")
if strings.TrimSpace(searchText) != "" {
pdf.CellFormat(0, 5, "Arama: "+searchText, "", 1, "L", false, 0, "")
}
pdf.Ln(1)
header := []string{"Ana Cari Kod", "Ana Cari Detay", "Piyasa", "Temsilci", "Risk", "1_2 USD", "1_2 TRY", "1_3 USD", "1_3 TRY"}
widths := normalizeWidths([]float64{18, 34, 12, 12, 12, 10, 10, 10, 10}, 281)
if includeVadeColumns {
header = append(header, "Vade Gun", "Belge Tarihi Gun")
widths = normalizeWidths([]float64{17, 28, 10, 10, 10, 10, 10, 10, 10, 8, 10}, 281)
}
pdf.SetFont("dejavu", "B", 8)
pdf.SetFillColor(149, 113, 22)
pdf.SetTextColor(255, 255, 255)
x, y := 8.0, pdf.GetY()
for i, h := range header {
pdf.Rect(x, y, widths[i], 6, "DF")
pdf.SetXY(x+1, y+1)
pdf.CellFormat(widths[i]-2, 4, h, "", 0, "C", false, 0, "")
x += widths[i]
}
pdf.SetY(y + 6)
pdf.SetFont("dejavu", "", 7.4)
pdf.SetTextColor(20, 20, 20)
for _, s := range summaries {
row := []string{
s.AnaCariKodu,
s.AnaCariAdi,
s.Piyasa,
s.Temsilci,
s.RiskDurumu,
formatMoneyPDF(s.USDBakiye12),
formatMoneyPDF(s.TLBakiye12),
formatMoneyPDF(s.USDBakiye13),
formatMoneyPDF(s.TLBakiye13),
}
if includeVadeColumns {
row = append(row, formatDayUpPDF(s.VadeGun), formatDayUpPDF(s.VadeBelge))
}
if pdf.GetY()+6 > 198 {
pdf.AddPage()
pdf.SetY(8)
}
x = 8
y = pdf.GetY()
for i, v := range row {
pdf.Rect(x, y, widths[i], 6, "")
align := "L"
if i >= 2 {
align = "R"
}
if includeVadeColumns && (i == len(row)-1 || i == len(row)-2) {
align = "C"
}
pdf.SetXY(x+1, y+1)
pdf.CellFormat(widths[i]-2, 4, v, "", 0, align, false, 0, "")
x += widths[i]
}
pdf.SetY(y + 6)
}
}
func formatDateTR(v string) string {
s := strings.TrimSpace(v)
if s == "" {
return s
}
if t, err := time.Parse("2006-01-02", s); err == nil {
return t.Format("02.01.2006")
}
if t, err := time.Parse(time.RFC3339, s); err == nil {
return t.Format("02.01.2006")
}
return s
}
func calcPDFRowHeight(pdf *gofpdf.Fpdf, row []string, widths []float64, wrapIdx map[int]bool, minH, lineH float64) float64 {
maxLines := 1
for i, v := range row {
if !wrapIdx[i] {
continue
}
if i >= len(widths) || widths[i] <= 2 {
continue
}
lines := safeSplitLinesPDF(pdf, strings.TrimSpace(v), widths[i]-2)
if len(lines) > maxLines {
maxLines = len(lines)
}
}
h := float64(maxLines)*lineH + 2
if h < minH {
return minH
}
return h
}
func drawPDFCellWrapped(pdf *gofpdf.Fpdf, value string, x, y, w, h float64, align string, lineH float64) {
if w <= 2 || h <= 0 {
return
}
text := strings.TrimSpace(value)
lines := safeSplitLinesPDF(pdf, text, w-2)
if len(lines) == 0 {
lines = [][]byte{[]byte("")}
}
startY := y + (h-(float64(len(lines))*lineH))/2
if startY < y+0.7 {
startY = y + 0.7
}
for _, ln := range lines {
pdf.SetXY(x+1, startY)
pdf.CellFormat(w-2, lineH, string(ln), "", 0, align, false, 0, "")
startY += lineH
}
}
func safeSplitLinesPDF(pdf *gofpdf.Fpdf, text string, width float64) (lines [][]byte) {
if width <= 0 {
width = 1
}
defer func() {
if recover() != nil {
lines = [][]byte{[]byte(text)}
}
}()
lines = pdf.SplitLines([]byte(text), width)
return lines
}
func calcPDFRowHeightCapped(pdf *gofpdf.Fpdf, row []string, widths []float64, wrapMax map[int]int, minH, lineH float64) float64 {
maxLines := 1
for i, v := range row {
limit, ok := wrapMax[i]
if !ok || limit <= 0 {
continue
}
if i >= len(widths) || widths[i] <= 2 {
continue
}
lines := safeSplitLinesPDF(pdf, strings.TrimSpace(v), widths[i]-2)
lineCount := len(lines)
if lineCount > limit {
lineCount = limit
}
if lineCount > maxLines {
maxLines = lineCount
}
}
h := float64(maxLines)*lineH + 2
if h < minH {
return minH
}
return h
}
func drawPDFCellWrappedCapped(pdf *gofpdf.Fpdf, value string, x, y, w, h float64, align string, lineH float64, maxLines int) {
if w <= 2 || h <= 0 {
return
}
text := strings.TrimSpace(value)
lines := safeSplitLinesPDF(pdf, text, w-2)
if len(lines) == 0 {
lines = [][]byte{[]byte("")}
}
clipped := false
if maxLines > 0 && len(lines) > maxLines {
lines = lines[:maxLines]
clipped = true
}
if clipped && len(lines) > 0 {
last := string(lines[len(lines)-1])
lines[len(lines)-1] = []byte(fitTextWithSuffixPDF(pdf, last, w-2, "..."))
}
startY := y + (h-(float64(len(lines))*lineH))/2
if startY < y+0.7 {
startY = y + 0.7
}
for _, ln := range lines {
pdf.SetXY(x+1, startY)
pdf.CellFormat(w-2, lineH, string(ln), "", 0, align, false, 0, "")
startY += lineH
}
}
func fitTextWithSuffixPDF(pdf *gofpdf.Fpdf, text string, width float64, suffix string) string {
txt := strings.TrimSpace(text)
if txt == "" {
return suffix
}
if pdf.GetStringWidth(txt) <= width {
return txt
}
allowed := width - pdf.GetStringWidth(suffix)
if allowed <= 0 {
return suffix
}
runes := []rune(txt)
for len(runes) > 0 && pdf.GetStringWidth(string(runes)) > allowed {
runes = runes[:len(runes)-1]
}
if len(runes) == 0 {
return suffix
}
return string(runes) + suffix
}
func formatDayUpPDF(v float64) string {
return formatIntPDF(int64(math.Ceil(v)))
}
func formatIntPDF(v int64) string {
s := strconv.FormatInt(v, 10)
sign := ""
if strings.HasPrefix(s, "-") {
sign = "-"
s = strings.TrimPrefix(s, "-")
}
var out []string
for len(s) > 3 {
out = append([]string{s[len(s)-3:]}, out...)
s = s[:len(s)-3]
}
if s != "" {
out = append([]string{s}, out...)
}
return sign + strings.Join(out, ".")
}
func formatCurrencyMapPDF(m map[string]float64) string {
if len(m) == 0 {
return "-"
}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, k := range keys {
if m[k] == 0 {
continue
}
parts = append(parts, k+": "+formatMoneyPDF(m[k]))
}
if len(parts) == 0 {
return "-"
}
return strings.Join(parts, " | ")
}
func formatMoneyPDF(v float64) string {
s := fmt.Sprintf("%.2f", v)
parts := strings.SplitN(s, ".", 2)
intPart, decPart := parts[0], "00"
if len(parts) == 2 {
decPart = parts[1]
}
sign := ""
if strings.HasPrefix(intPart, "-") {
sign = "-"
intPart = strings.TrimPrefix(intPart, "-")
}
var out []string
for len(intPart) > 3 {
out = append([]string{intPart[len(intPart)-3:]}, out...)
intPart = intPart[:len(intPart)-3]
}
if intPart != "" {
out = append([]string{intPart}, out...)
}
return sign + strings.Join(out, ".") + "," + decPart
}