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 agingScreenPDFRow struct { Cari8 string CariDetay string FaturaCari string OdemeCari string FaturaRef string OdemeRef string FaturaTarihi string OdemeTarihi string OdemeDocDate string EslesenTutar float64 UsdTutar float64 CurrencyUsdRate float64 GunSayisi float64 GunSayisiDocDate float64 Aciklama string DocCurrencyCode string } func ExportStatementAgingScreenPDFHandler(_ *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("enddate")) if selectedDate == "" { selectedDate = strings.TrimSpace(r.URL.Query().Get("selected_date")) } if selectedDate == "" { selectedDate = time.Now().Format("2006-01-02") } params := models.StatementAgingParams{ AccountCode: strings.TrimSpace(r.URL.Query().Get("accountcode")), EndDate: selectedDate, Parislemler: r.URL.Query()["parislemler"], } if err := queries.RebuildStatementAgingCache(r.Context()); err != nil { http.Error(w, "Error rebuilding aging cache: "+err.Error(), http.StatusInternalServerError) return } rawRows, err := queries.GetStatementAging(params) if err != nil { http.Error(w, "Error fetching aging statement: "+err.Error(), http.StatusInternalServerError) return } rows := make([]agingScreenPDFRow, 0, len(rawRows)) for _, r := range rawRows { rows = append(rows, agingScreenPDFRow{ Cari8: pickString(r, "Cari8", "cari8"), CariDetay: pickString(r, "CariDetay", "cari_detay"), FaturaCari: pickString(r, "FaturaCari", "fatura_cari"), OdemeCari: pickString(r, "OdemeCari", "odeme_cari"), FaturaRef: pickString(r, "FaturaRef", "fatura_ref"), OdemeRef: pickString(r, "OdemeRef", "odeme_ref"), FaturaTarihi: pickString(r, "FaturaTarihi", "fatura_tarihi"), OdemeTarihi: pickString(r, "OdemeTarihi", "odeme_tarihi"), OdemeDocDate: pickString(r, "OdemeDocDate", "odeme_doc_date"), EslesenTutar: pickFloat(r, "EslesenTutar", "eslesen_tutar"), UsdTutar: pickFloat(r, "UsdTutar", "usd_tutar"), CurrencyUsdRate: pickFloat(r, "CurrencyUsdRate", "currency_usd_rate", "CurrencyTryRate", "currency_try_rate"), GunSayisi: pickFloat(r, "GunSayisi", "gun_sayisi"), GunSayisiDocDate: pickFloat(r, "GunSayisi_DocDate", "gun_sayisi_docdate"), Aciklama: pickString(r, "Aciklama", "aciklama"), DocCurrencyCode: pickString(r, "DocCurrencyCode", "doc_currency_code"), }) } pdf := gofpdf.New("L", "mm", "A3", "") pdf.SetMargins(8, 8, 8) pdf.SetAutoPageBreak(false, 10) if err := registerDejavuFonts(pdf, "dejavu"); err != nil { http.Error(w, "pdf font error: "+err.Error(), http.StatusInternalServerError) return } drawStatementAgingScreenPDF(pdf, selectedDate, params.AccountCode, rows) 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 } w.Header().Set("Content-Type", "application/pdf") w.Header().Set("Content-Disposition", `inline; filename="account-aging-screen.pdf"`) _, _ = w.Write(buf.Bytes()) } } type agingScreenMasterPDF struct { GroupKey string Cari8 string CariDetay string SatirSayisi int ToplamUSD float64 NormalUSD float64 AcikKalemUSD float64 KurWeightedBase float64 KurWeightedSum float64 KurFallback float64 WeightedBase float64 WeightedGunSum float64 WeightedGunDocSum float64 } type agingScreenCurrencyPDF struct { GroupKey string MasterKey string DocCurrencyCode string SatirSayisi int ToplamTutar float64 ToplamUSD float64 NormalTutar float64 AcikKalemTutar float64 KurWeightedBase float64 KurWeightedSum float64 KurFallback float64 WeightedBase float64 WeightedGunSum float64 WeightedGunDocSum float64 } func drawStatementAgingScreenPDF(pdf *gofpdf.Fpdf, selectedDate, accountCode string, rows []agingScreenPDFRow) { masters, currenciesByMaster, detailsByCurrency := buildStatementAgingScreenPDFData(rows) pageW, pageH := pdf.GetPageSize() marginL, marginT, marginR, marginB := 8.0, 8.0, 8.0, 10.0 tableW := pageW - marginL - marginR masterCols := []string{"Ana Cari Kod", "Ana Cari Detay", "Satir", "Toplam USD", "Normal USD", "Acik Kalem USD", "Ort. Gun", "Ort. Gun (DocDate)", "Kur"} masterW := normalizeWidths([]float64{22, 40, 12, 18, 18, 20, 14, 18, 12}, tableW) currencyCols := []string{"Doviz", "Satir", "Toplam Tutar", "Toplam USD", "Normal", "Acik Kalem", "Kur", "Ort. Gun", "Ort. Gun (DocDate)"} currencyW := normalizeWidths([]float64{16, 12, 20, 18, 18, 18, 12, 14, 18}, tableW) detailCols := []string{"Fatura Cari", "Odeme Cari", "Fatura Ref", "Odeme Ref", "Fatura Tarihi", "Odeme Vade", "Odeme DocDate", "Eslesen Tutar", "USD Tutar", "Kur", "Gun", "Gun (DocDate)", "Aciklama", "Doviz"} detailW := normalizeWidths([]float64{18, 18, 22, 22, 16, 16, 18, 18, 16, 12, 10, 13, 30, 11}, tableW) header := func() { pdf.AddPage() pdf.SetFont("dejavu", "B", 14) pdf.SetTextColor(149, 113, 22) pdf.SetXY(marginL, marginT) pdf.CellFormat(150, 6, "Cari Yaslandirmali Ekstre", "", 0, "L", false, 0, "") pdf.SetFont("dejavu", "", 8.5) pdf.SetTextColor(30, 30, 30) pdf.SetXY(pageW-marginR-90, marginT+0.5) pdf.CellFormat(90, 4.8, "Son Tarih: "+formatDateTR(selectedDate), "", 0, "R", false, 0, "") if strings.TrimSpace(accountCode) != "" { pdf.SetXY(pageW-marginR-90, marginT+5) pdf.CellFormat(90, 4.8, "Cari: "+accountCode, "", 0, "R", false, 0, "") } pdf.SetDrawColor(149, 113, 22) pdf.Line(marginL, marginT+10.5, pageW-marginR, marginT+10.5) pdf.SetDrawColor(200, 200, 200) pdf.SetY(marginT + 13) } needPage := func(needH float64) bool { return pdf.GetY()+needH+marginB > pageH } drawHeaderRow := func(cols []string, widths []float64, h float64, r, g, b int, fontSize float64) { pdf.SetFont("dejavu", "B", fontSize) pdf.SetTextColor(255, 255, 255) pdf.SetFillColor(r, g, b) y := pdf.GetY() x := marginL for i, c := range cols { pdf.Rect(x, y, widths[i], h, "DF") pdf.SetXY(x+0.8, y+0.9) pdf.CellFormat(widths[i]-1.6, h-1.8, c, "", 0, "C", false, 0, "") x += widths[i] } pdf.SetY(y + h) } header() drawHeaderRow(masterCols, masterW, 6.2, 149, 113, 22, 7.2) pdf.SetFont("dejavu", "", 7) pdf.SetTextColor(25, 25, 25) for _, m := range masters { masterLine := []string{ m.Cari8, m.CariDetay, strconv.Itoa(m.SatirSayisi), formatMoneyPDF(m.ToplamUSD), formatMoneyPDF(m.NormalUSD), formatMoneyPDF(m.AcikKalemUSD), fmt.Sprintf("%.0f", statementAgingAvg(m.WeightedGunSum, m.WeightedBase)), fmt.Sprintf("%.0f", statementAgingAvg(m.WeightedGunDocSum, m.WeightedBase)), formatMoneyPDF(statementAgingAvg(m.KurWeightedSum, m.KurWeightedBase, m.KurFallback)), } rowH := calcPDFRowHeight(pdf, masterLine, masterW, map[int]bool{1: true}, 5.8, 3.3) if needPage(rowH) { header() drawHeaderRow(masterCols, masterW, 6.2, 149, 113, 22, 7.2) pdf.SetFont("dejavu", "", 7) pdf.SetTextColor(25, 25, 25) } y := pdf.GetY() x := marginL for i, v := range masterLine { pdf.Rect(x, y, masterW[i], rowH, "") align := "L" if i >= 2 { align = "R" } if i == 6 || i == 7 { align = "C" } drawPDFCellWrapped(pdf, v, x, y, masterW[i], rowH, align, 3.3) x += masterW[i] } pdf.SetY(y + rowH) for _, c := range currenciesByMaster[m.GroupKey] { if needPage(11.2) { header() drawHeaderRow(masterCols, masterW, 6.2, 149, 113, 22, 7.2) pdf.SetFont("dejavu", "", 7) pdf.SetTextColor(25, 25, 25) } pdf.SetFont("dejavu", "B", 7) drawHeaderRow(currencyCols, currencyW, 5.6, 76, 95, 122, 6.8) pdf.SetFont("dejavu", "", 6.8) pdf.SetTextColor(35, 35, 35) currencyLine := []string{ c.DocCurrencyCode, strconv.Itoa(c.SatirSayisi), formatMoneyPDF(c.ToplamTutar), formatMoneyPDF(c.ToplamUSD), formatMoneyPDF(c.NormalTutar), formatMoneyPDF(c.AcikKalemTutar), formatMoneyPDF(statementAgingAvg(c.KurWeightedSum, c.KurWeightedBase, c.KurFallback)), fmt.Sprintf("%.0f", statementAgingAvg(c.WeightedGunSum, c.WeightedBase)), fmt.Sprintf("%.0f", statementAgingAvg(c.WeightedGunDocSum, c.WeightedBase)), } cRowH := 5.4 y := pdf.GetY() x := marginL for i, v := range currencyLine { pdf.Rect(x, y, currencyW[i], cRowH, "") align := "R" if i == 0 { align = "L" } if i == 7 || i == 8 { align = "C" } drawPDFCellWrapped(pdf, v, x, y, currencyW[i], cRowH, align, 3.2) x += currencyW[i] } pdf.SetY(y + cRowH) drawHeaderRow(detailCols, detailW, 5.6, 31, 59, 91, 6.8) pdf.SetFont("dejavu", "", 6.6) pdf.SetTextColor(30, 30, 30) for _, d := range detailsByCurrency[c.GroupKey] { line := []string{ d.FaturaCari, d.OdemeCari, d.FaturaRef, d.OdemeRef, formatDateTR(d.FaturaTarihi), formatDateTR(d.OdemeTarihi), formatDateTR(d.OdemeDocDate), formatMoneyPDF(d.EslesenTutar), formatMoneyPDF(d.UsdTutar), formatMoneyPDF(d.CurrencyUsdRate), fmt.Sprintf("%.0f", d.GunSayisi), fmt.Sprintf("%.0f", d.GunSayisiDocDate), d.Aciklama, d.DocCurrencyCode, } rowH := calcPDFRowHeight(pdf, line, detailW, map[int]bool{0: true, 1: true, 2: true, 3: true, 12: true}, 5.4, 3.1) if needPage(rowH) { header() drawHeaderRow(masterCols, masterW, 6.2, 149, 113, 22, 7.2) pdf.SetFont("dejavu", "B", 7) drawHeaderRow(currencyCols, currencyW, 5.6, 76, 95, 122, 6.8) pdf.SetFont("dejavu", "", 6.8) pdf.SetTextColor(35, 35, 35) y = pdf.GetY() x = marginL for i, v := range currencyLine { pdf.Rect(x, y, currencyW[i], cRowH, "") align := "R" if i == 0 { align = "L" } if i == 7 || i == 8 { align = "C" } drawPDFCellWrapped(pdf, v, x, y, currencyW[i], cRowH, align, 3.2) x += currencyW[i] } pdf.SetY(y + cRowH) drawHeaderRow(detailCols, detailW, 5.6, 31, 59, 91, 6.8) pdf.SetFont("dejavu", "", 6.6) pdf.SetTextColor(30, 30, 30) } rowY := pdf.GetY() rowX := marginL for i, v := range line { pdf.Rect(rowX, rowY, detailW[i], rowH, "") align := "L" if i >= 7 && i <= 9 { align = "R" } if i == 10 || i == 11 { align = "C" } drawPDFCellWrapped(pdf, v, rowX, rowY, detailW[i], rowH, align, 3.1) rowX += detailW[i] } pdf.SetY(rowY + rowH) } pdf.Ln(1) } pdf.Ln(1.2) } } func statementAgingAvg(sum, base float64, fallback ...float64) float64 { if base > 0 { return sum / base } if len(fallback) > 0 { return fallback[0] } return 0 } func buildStatementAgingScreenPDFData(rows []agingScreenPDFRow) ([]agingScreenMasterPDF, map[string][]agingScreenCurrencyPDF, map[string][]agingScreenPDFRow) { masterMap := map[string]*agingScreenMasterPDF{} currencyMap := map[string]*agingScreenCurrencyPDF{} detailsByCurrency := map[string][]agingScreenPDFRow{} for _, row := range rows { masterKey := strings.TrimSpace(row.Cari8) if masterKey == "" { continue } curr := strings.ToUpper(strings.TrimSpace(row.DocCurrencyCode)) if curr == "" { curr = "N/A" } currencyKey := masterKey + "|" + curr aciklama := strings.ToUpper(strings.TrimSpace(row.Aciklama)) absUsd := absFloatExcel(row.UsdTutar) m := masterMap[masterKey] if m == nil { m = &agingScreenMasterPDF{ GroupKey: masterKey, Cari8: masterKey, CariDetay: strings.TrimSpace(row.CariDetay), } masterMap[masterKey] = m } if m.CariDetay == "" { m.CariDetay = strings.TrimSpace(row.CariDetay) } c := currencyMap[currencyKey] if c == nil { c = &agingScreenCurrencyPDF{ GroupKey: currencyKey, MasterKey: masterKey, DocCurrencyCode: curr, } currencyMap[currencyKey] = c } m.SatirSayisi++ m.ToplamUSD += row.UsdTutar if aciklama == "ACIKKALEM" { m.AcikKalemUSD += row.UsdTutar } else { m.NormalUSD += row.UsdTutar } if absUsd > 0 { m.WeightedBase += absUsd m.WeightedGunSum += absUsd * row.GunSayisi m.WeightedGunDocSum += absUsd * row.GunSayisiDocDate if row.CurrencyUsdRate > 0 { m.KurWeightedBase += absUsd m.KurWeightedSum += absUsd * row.CurrencyUsdRate } } if row.CurrencyUsdRate > 0 { m.KurFallback = row.CurrencyUsdRate } c.SatirSayisi++ c.ToplamTutar += row.EslesenTutar c.ToplamUSD += row.UsdTutar if aciklama == "ACIKKALEM" { c.AcikKalemTutar += row.EslesenTutar } else { c.NormalTutar += row.EslesenTutar } if absUsd > 0 { c.WeightedBase += absUsd c.WeightedGunSum += absUsd * row.GunSayisi c.WeightedGunDocSum += absUsd * row.GunSayisiDocDate if row.CurrencyUsdRate > 0 { c.KurWeightedBase += absUsd c.KurWeightedSum += absUsd * row.CurrencyUsdRate } } if row.CurrencyUsdRate > 0 { c.KurFallback = row.CurrencyUsdRate } detailsByCurrency[currencyKey] = append(detailsByCurrency[currencyKey], row) } masterKeys := make([]string, 0, len(masterMap)) for k := range masterMap { masterKeys = append(masterKeys, k) } sort.SliceStable(masterKeys, func(i, j int) bool { return strings.ToUpper(masterKeys[i]) < strings.ToUpper(masterKeys[j]) }) masters := make([]agingScreenMasterPDF, 0, len(masterKeys)) currenciesByMaster := make(map[string][]agingScreenCurrencyPDF, len(masterKeys)) for _, mk := range masterKeys { masters = append(masters, *masterMap[mk]) } for _, c := range currencyMap { currenciesByMaster[c.MasterKey] = append(currenciesByMaster[c.MasterKey], *c) } for mk := range currenciesByMaster { sort.SliceStable(currenciesByMaster[mk], func(i, j int) bool { return strings.ToUpper(currenciesByMaster[mk][i].DocCurrencyCode) < strings.ToUpper(currenciesByMaster[mk][j].DocCurrencyCode) }) } for k := range detailsByCurrency { sort.SliceStable(detailsByCurrency[k], func(i, j int) bool { a := detailsByCurrency[k][i] b := detailsByCurrency[k][j] if strings.TrimSpace(a.FaturaCari) == strings.TrimSpace(b.FaturaCari) { if strings.TrimSpace(a.OdemeCari) == strings.TrimSpace(b.OdemeCari) { if strings.TrimSpace(a.FaturaRef) == strings.TrimSpace(b.FaturaRef) { return strings.TrimSpace(a.OdemeRef) < strings.TrimSpace(b.OdemeRef) } return strings.TrimSpace(a.FaturaRef) < strings.TrimSpace(b.FaturaRef) } return strings.TrimSpace(a.OdemeCari) < strings.TrimSpace(b.OdemeCari) } return strings.TrimSpace(a.FaturaCari) < strings.TrimSpace(b.FaturaCari) }) } return masters, currenciesByMaster, detailsByCurrency } func pickString(m map[string]interface{}, keys ...string) string { for _, k := range keys { if v, ok := m[k]; ok { return strings.TrimSpace(toStringValue(v)) } } return "" } func pickFloat(m map[string]interface{}, keys ...string) float64 { for _, k := range keys { if v, ok := m[k]; ok { return toFloat64Value(v) } } return 0 } func toStringValue(v interface{}) string { switch x := v.(type) { case nil: return "" case string: return x case []byte: return string(x) default: return fmt.Sprint(x) } } func toFloat64Value(v interface{}) float64 { switch x := v.(type) { case nil: return 0 case float64: return x case float32: return float64(x) case int: return float64(x) case int64: return float64(x) case int32: return float64(x) case string: return parseFloatValue(x) case []byte: return parseFloatValue(string(x)) default: return parseFloatValue(fmt.Sprint(x)) } } func parseFloatValue(s string) float64 { s = strings.TrimSpace(s) if s == "" { return 0 } hasComma := strings.Contains(s, ",") hasDot := strings.Contains(s, ".") if hasComma && hasDot { if strings.LastIndex(s, ",") > strings.LastIndex(s, ".") { s = strings.ReplaceAll(s, ".", "") s = strings.Replace(s, ",", ".", 1) } else { s = strings.ReplaceAll(s, ",", "") } } else if hasComma { s = strings.ReplaceAll(s, ".", "") s = strings.Replace(s, ",", ".", 1) } n, err := strconv.ParseFloat(s, 64) if err != nil { return 0 } return n }