Files
bssapp/svc/routes/customer_balance_pdf.go
2026-03-03 11:26:52 +03:00

549 lines
14 KiB
Go

package routes
import (
"bssapp-backend/auth"
"bssapp-backend/models"
"bssapp-backend/queries"
"bytes"
"database/sql"
"fmt"
"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
VadeBase 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
}
drawCustomerBalancePDF(
pdf,
selectedDate,
params.CariSearch,
detailed,
summaries,
detailsByMaster,
)
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)
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 := absFloat(row.Bakiye12) + absFloat(row.Bakiye13)
if w > 0 {
s.VadeBase += w
s.VadeGun += row.VadeGun * w
s.VadeBelge += 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 {
s := summaryMap[m]
if s != nil && s.VadeBase > 0 {
s.VadeGun = s.VadeGun / s.VadeBase
s.VadeBelge = s.VadeBelge / s.VadeBase
}
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,
summaries []balanceSummaryPDF,
detailsByMaster map[string][]models.CustomerBalanceListRow,
) {
pageW, _ := pdf.GetPageSize()
marginL, marginT, marginR, marginB := 8.0, 8.0, 8.0, 12.0
tableW := pageW - marginL - marginR
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", "Vade Gun", "Belge Gun"}
summaryW := normalizeWidths([]float64{18, 46, 14, 18, 12, 20, 20, 12, 12, 12, 12, 10, 10}, 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", "Vade Gun", "Belge Gun"}
detailW := normalizeWidths([]float64{22, 40, 9, 16, 8, 20, 20, 12, 12, 12, 12, 9, 9}, tableW)
header := func() {
pdf.AddPage()
pdf.SetFont("dejavu", "B", 15)
pdf.SetTextColor(149, 113, 22)
pdf.SetXY(marginL, marginT)
pdf.CellFormat(120, 7, "Cari Bakiye Listesi", "", 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: "+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(marginL, marginT+8)
pdf.CellFormat(tableW, 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 > 210.0
}
wrappedLines := func(text string, w float64) [][]byte {
t := strings.TrimSpace(text)
if t == "" {
t = "-"
}
return splitLinesSafe(pdf, t, w)
}
calcWrappedRowHeight := func(row []string, widths []float64, wrapIdx map[int]bool, lineH float64, minH float64) float64 {
maxLines := 1
for i, v := range row {
if !wrapIdx[i] {
continue
}
ln := len(wrappedLines(v, widths[i]-2))
if ln > maxLines {
maxLines = ln
}
}
h := float64(maxLines)*lineH + 1.2
if h < minH {
return minH
}
return h
}
drawWrapped := func(text string, x, y, w, rowH, lineH float64, align string) {
lines := wrappedLines(text, w-2)
total := float64(len(lines)) * lineH
startY := y + (rowH-total)/2
cy := startY
for _, ln := range lines {
pdf.SetXY(x+1, cy)
pdf.CellFormat(w-2, lineH, string(ln), "", 0, align, false, 0, "")
cy += lineH
}
}
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 {
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 {
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)
drawSummaryRow := func(s balanceSummaryPDF) {
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),
formatMoneyPDF(s.VadeGun),
formatMoneyPDF(s.VadeBelge),
}
wrapCols := map[int]bool{1: true, 3: true}
rowH := calcWrappedRowHeight(row, summaryW, wrapCols, 3.2, 6.2)
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 detailed {
pdf.SetFillColor(246, 241, 231)
pdf.Rect(x, y, summaryW[i], rowH, "FD")
} else {
pdf.Rect(x, y, summaryW[i], rowH, "")
}
align := "L"
if i >= 7 {
align = "R"
}
if wrapCols[i] {
drawWrapped(v, x, y, summaryW[i], rowH, 3.2, "L")
} else {
pdf.SetXY(x+1, y+(rowH-4.2)/2)
pdf.CellFormat(summaryW[i]-2, 4.2, v, "", 0, align, false, 0, "")
}
x += summaryW[i]
}
pdf.SetY(y + rowH)
}
if !detailed {
for _, s := range summaries {
drawSummaryRow(s)
}
return
}
for _, s := range summaries {
drawSummaryRow(s)
pdf.Ln(1.2)
rows := detailsByMaster[s.AnaCariKodu]
if len(rows) == 0 {
pdf.Ln(1.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),
formatMoneyPDF(r.VadeGun),
formatMoneyPDF(r.VadeBelgeGun),
}
detailWrapCols := map[int]bool{1: true}
rowH := calcWrappedRowHeight(line, detailW, detailWrapCols, 3.0, 5.8)
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 {
pdf.Rect(rowX, rowY, detailW[i], rowH, "")
align := "L"
if i >= 5 {
align = "R"
}
if detailWrapCols[i] {
drawWrapped(v, rowX, rowY, detailW[i], rowH, 3.0, "L")
} else {
pdf.SetXY(rowX+1, rowY+(rowH-4.0)/2)
pdf.CellFormat(detailW[i]-2, 4.0, v, "", 0, align, false, 0, "")
}
rowX += detailW[i]
}
pdf.SetY(rowY + rowH)
}
pdf.Ln(1.2)
}
}
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
}
func absFloat(v float64) float64 {
if v < 0 {
return -v
}
return v
}