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 agingDetailPDF struct { FaturaCari string OdemeCari string Doviz string FaturaRef string OdemeRef string FaturaTarihi string OdemeTarihi string OdemeDocDate string EslesenTutar float64 UsdTutar float64 TryTutar float64 Aciklama string Gun int GunBelge int GunKur float64 odemeDateParsed time.Time odemeDateEmpty bool } type agingCurrencyPDF struct { Key string Cari8 string CariDetay string Doviz string AcikKalemTutar float64 AcikKalemUSD float64 AcikKalemTRY float64 OrtGun int OrtBelgeGun int weightedBase float64 weightedGunSum float64 weightedDocSum float64 Details []agingDetailPDF } type agingMasterPDF struct { Key string Cari8 string CariDetay string AcikKalemUSD float64 AcikKalemTRY float64 AcikKalemOrtVadeGun int AcikKalemOrtBelge int NormalUSD float64 NormalTRY float64 OrtalamaVadeGun int OrtalamaBelgeGun int weightedAllBase float64 weightedAllGunSum float64 weightedAllDocSum float64 weightedOpenBase float64 weightedOpenGunSum float64 weightedOpenDocSum float64 Currencies []agingCurrencyPDF } func ExportStatementAgingPDFHandler(_ *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 } params := models.StatementAgingParams{ AccountCode: strings.TrimSpace(r.URL.Query().Get("accountcode")), EndDate: strings.TrimSpace(r.URL.Query().Get("enddate")), Parislemler: r.URL.Query()["parislemler"], } if params.AccountCode == "" || params.EndDate == "" { http.Error(w, "accountcode and enddate are required", http.StatusBadRequest) return } rows, err := queries.GetStatementAging(params) if err != nil { http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError) return } masters := buildAgingPDFData(rows) pdf := gofpdf.New("L", "mm", "A4", "") 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 } drawStatementAgingPDF(pdf, params, masters) 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-detailed.pdf"`) _, _ = w.Write(buf.Bytes()) } } func buildAgingPDFData(rows []map[string]interface{}) []agingMasterPDF { masterMap := make(map[string]*agingMasterPDF) currMap := make(map[string]*agingCurrencyPDF) for _, row := range rows { cari8 := strings.TrimSpace(readStrAny(row["Cari8"], row["cari8"])) if cari8 == "" { continue } cariDetay := strings.TrimSpace(readStrAny(row["CariDetay"], row["cari_detay"])) doviz := strings.ToUpper(strings.TrimSpace(readStrAny(row["DocCurrencyCode"], row["doc_currency_code"]))) if doviz == "" { doviz = "TRY" } aciklama := strings.ToUpper(strings.TrimSpace(readStrAny(row["Aciklama"], row["aciklama"]))) isAcik := aciklama == "ACIKKALEM" eslesen := readFloatAny(row["EslesenTutar"], row["eslesen_tutar"]) usd := readFloatAny(row["UsdTutar"], row["usd_tutar"]) tryVal := readFloatAny(row["TryTutar"], row["try_tutar"]) gun := readIntAny(row["GunSayisi"], row["gun_sayisi"]) gunBelge := readIntAny(row["GunSayisi_DocDate"], row["gun_sayisi_docdate"]) absTry := math.Abs(tryVal) m := masterMap[cari8] if m == nil { m = &agingMasterPDF{Key: cari8, Cari8: cari8, CariDetay: cariDetay} masterMap[cari8] = m } if m.CariDetay == "" && cariDetay != "" { m.CariDetay = cariDetay } ckey := cari8 + "|" + doviz c := currMap[ckey] if c == nil { c = &agingCurrencyPDF{ Key: ckey, Cari8: cari8, CariDetay: cariDetay, Doviz: doviz, Details: make([]agingDetailPDF, 0, 128), } currMap[ckey] = c } if c.CariDetay == "" && cariDetay != "" { c.CariDetay = cariDetay } if isAcik { m.AcikKalemUSD += usd m.AcikKalemTRY += tryVal c.AcikKalemTutar += eslesen c.AcikKalemUSD += usd c.AcikKalemTRY += tryVal } else { m.NormalUSD += usd m.NormalTRY += tryVal } if absTry > 0 { m.weightedAllBase += absTry m.weightedAllGunSum += absTry * float64(gun) m.weightedAllDocSum += absTry * float64(gunBelge) if isAcik { m.weightedOpenBase += absTry m.weightedOpenGunSum += absTry * float64(gun) m.weightedOpenDocSum += absTry * float64(gunBelge) c.weightedBase += absTry c.weightedGunSum += absTry * float64(gun) c.weightedDocSum += absTry * float64(gunBelge) } } odemeTar := readStrAny(row["OdemeTarihi"], row["odeme_tarihi"]) odemeParsed, ok := parseYMD(odemeTar) detail := agingDetailPDF{ FaturaCari: readStrAny(row["FaturaCari"], row["fatura_cari"]), OdemeCari: readStrAny(row["OdemeCari"], row["odeme_cari"]), Doviz: doviz, FaturaRef: readStrAny(row["FaturaRef"], row["fatura_ref"]), OdemeRef: readStrAny(row["OdemeRef"], row["odeme_ref"]), FaturaTarihi: readStrAny(row["FaturaTarihi"], row["fatura_tarihi"]), OdemeTarihi: odemeTar, OdemeDocDate: readStrAny(row["OdemeDocDate"], row["odeme_doc_date"]), EslesenTutar: eslesen, UsdTutar: usd, TryTutar: tryVal, Aciklama: readStrAny(row["Aciklama"], row["aciklama"]), Gun: gun, GunBelge: gunBelge, GunKur: readFloatAny(row["GunKur"], row["gun_kur"]), odemeDateParsed: odemeParsed, odemeDateEmpty: !ok, } c.Details = append(c.Details, detail) } masters := make([]agingMasterPDF, 0, len(masterMap)) for _, m := range masterMap { if m.weightedOpenBase > 0 { m.AcikKalemOrtVadeGun = int(math.Ceil(m.weightedOpenGunSum / m.weightedOpenBase)) m.AcikKalemOrtBelge = int(math.Ceil(m.weightedOpenDocSum / m.weightedOpenBase)) } if m.weightedAllBase > 0 { m.OrtalamaVadeGun = int(math.Ceil(m.weightedAllGunSum / m.weightedAllBase)) m.OrtalamaBelgeGun = int(math.Ceil(m.weightedAllDocSum / m.weightedAllBase)) } currs := make([]agingCurrencyPDF, 0, len(currMap)) for _, c := range currMap { if c.Cari8 != m.Cari8 { continue } if c.weightedBase > 0 { c.OrtGun = int(math.Ceil(c.weightedGunSum / c.weightedBase)) c.OrtBelgeGun = int(math.Ceil(c.weightedDocSum / c.weightedBase)) } sort.SliceStable(c.Details, func(i, j int) bool { ai := c.Details[i] aj := c.Details[j] if ai.odemeDateEmpty && !aj.odemeDateEmpty { return true } if !ai.odemeDateEmpty && aj.odemeDateEmpty { return false } if ai.odemeDateEmpty && aj.odemeDateEmpty { return false } return ai.odemeDateParsed.After(aj.odemeDateParsed) }) currs = append(currs, *c) } sort.SliceStable(currs, func(i, j int) bool { return currs[i].Doviz < currs[j].Doviz }) m.Currencies = currs masters = append(masters, *m) } sort.SliceStable(masters, func(i, j int) bool { return masters[i].Cari8 < masters[j].Cari8 }) return masters } func drawStatementAgingPDF(pdf *gofpdf.Fpdf, p models.StatementAgingParams, masters []agingMasterPDF) { pageW, pageH := pdf.GetPageSize() marginL, marginR, marginT, marginB := 8.0, 8.0, 8.0, 10.0 tableW := pageW - marginL - marginR colorPrimary := [3]int{149, 113, 22} colorLevel2 := [3]int{76, 95, 122} colorLevel3 := [3]int{31, 59, 91} level1Cols := []string{"Ana Cari Kod", "Ana Cari Detay", "Açık Kalem USD", "Açık Kalem TRY", "Açık Kalem Ort Vade", "Açık Kalem Ort Belge", "Normal USD", "Normal TRY", "Ortalama Vade", "Ortalama Belge"} level1W := normalizeWidths([]float64{20, 46, 18, 18, 15, 15, 16, 16, 15, 15}, tableW) level2Cols := []string{"Ana Cari Kod", "Ana Cari Detay", "Döviz", "Açık Kalem", "Açık Kalem USD", "Açık Kalem TRY", "Ort Gün", "Ort Belge Gün"} level2W := normalizeWidths([]float64{20, 52, 12, 24, 24, 24, 16, 18}, tableW) level3Cols := []string{"Fatura Cari", "Ödeme Cari", "Döviz", "Fatura Ref", "Ödeme Ref", "Fatura Tarihi", "Ödeme Vade", "Ödeme DocDate", "Eşleşen", "USD", "TRY", "Açıklama", "Gün", "Gün Belge", "Gün Kur"} level3W := normalizeWidths([]float64{15, 15, 10, 15, 15, 13, 13, 13, 14, 12, 14, 14, 8, 10, 10}, tableW) pageHeader := func() { pdf.AddPage() pdf.SetFont("dejavu", "B", 15) pdf.SetTextColor(colorPrimary[0], colorPrimary[1], colorPrimary[2]) pdf.SetXY(marginL, marginT) pdf.CellFormat(150, 7, "Cari Yaşlandırmalı Ekstre", "", 0, "L", false, 0, "") pdf.SetFont("dejavu", "", 9) pdf.SetTextColor(20, 20, 20) pdf.SetXY(pageW-marginR-95, marginT+1) pdf.CellFormat(95, 5, "Son Tarih: "+p.EndDate, "", 0, "R", false, 0, "") pdf.SetXY(pageW-marginR-95, marginT+6) pdf.CellFormat(95, 5, "Cari: "+p.AccountCode, "", 0, "R", false, 0, "") mode := "1_2" if len(p.Parislemler) > 0 { mode = strings.Join(p.Parislemler, ",") } pdf.SetXY(pageW-marginR-95, marginT+11) pdf.CellFormat(95, 5, "Parasal İşlem: "+mode, "", 0, "R", false, 0, "") pdf.SetDrawColor(colorPrimary[0], colorPrimary[1], colorPrimary[2]) pdf.Line(marginL, marginT+16, pageW-marginR, marginT+16) pdf.SetDrawColor(210, 210, 210) pdf.SetY(marginT + 19) } needPage := func(need float64) bool { return pdf.GetY()+need+marginB > pageH } drawHeaderRow := func(cols []string, widths []float64, rgb [3]int, h float64) { pdf.SetFont("dejavu", "B", 8.4) pdf.SetFillColor(rgb[0], rgb[1], rgb[2]) pdf.SetTextColor(255, 255, 255) y := pdf.GetY() x := marginL for i, c := range cols { pdf.Rect(x, y, widths[i], h, "DF") pdf.SetXY(x+0.8, y+1.0) pdf.CellFormat(widths[i]-1.6, h-2.0, c, "", 0, "C", false, 0, "") x += widths[i] } pdf.SetY(y + h) } drawRow := func(vals []string, widths []float64, h float64, fill bool, fillRGB [3]int, centerCols map[int]bool, rightCols map[int]bool) { pdf.SetFont("dejavu", "", 7.6) pdf.SetTextColor(20, 20, 20) y := pdf.GetY() x := marginL for i, v := range vals { if fill { pdf.SetFillColor(fillRGB[0], fillRGB[1], fillRGB[2]) pdf.Rect(x, y, widths[i], h, "DF") } else { pdf.Rect(x, y, widths[i], h, "") } align := "L" if rightCols[i] { align = "R" } else if centerCols[i] { align = "C" } pdf.SetXY(x+0.8, y+0.8) pdf.CellFormat(widths[i]-1.6, h-1.6, v, "", 0, align, false, 0, "") x += widths[i] } pdf.SetY(y + h) } format2 := func(v float64) string { return trFormat(v, 2) } pageHeader() for _, m := range masters { if needPage(7 + 7 + 6) { pageHeader() } drawHeaderRow(level1Cols, level1W, colorPrimary, 7) drawRow( []string{ m.Cari8, m.CariDetay, format2(m.AcikKalemUSD), format2(m.AcikKalemTRY), fmt.Sprintf("%d", m.AcikKalemOrtVadeGun), fmt.Sprintf("%d", m.AcikKalemOrtBelge), format2(m.NormalUSD), format2(m.NormalTRY), fmt.Sprintf("%d", m.OrtalamaVadeGun), fmt.Sprintf("%d", m.OrtalamaBelgeGun), }, level1W, 6.4, true, [3]int{250, 246, 238}, map[int]bool{0: true, 4: true, 5: true, 8: true, 9: true}, map[int]bool{2: true, 3: true, 6: true, 7: true}, ) for _, c := range m.Currencies { if needPage(6 + 6 + 6) { pageHeader() drawHeaderRow(level1Cols, level1W, colorPrimary, 7) drawRow( []string{ m.Cari8, m.CariDetay, format2(m.AcikKalemUSD), format2(m.AcikKalemTRY), fmt.Sprintf("%d", m.AcikKalemOrtVadeGun), fmt.Sprintf("%d", m.AcikKalemOrtBelge), format2(m.NormalUSD), format2(m.NormalTRY), fmt.Sprintf("%d", m.OrtalamaVadeGun), fmt.Sprintf("%d", m.OrtalamaBelgeGun), }, level1W, 6.4, true, [3]int{250, 246, 238}, map[int]bool{0: true, 4: true, 5: true, 8: true, 9: true}, map[int]bool{2: true, 3: true, 6: true, 7: true}, ) } drawHeaderRow(level2Cols, level2W, colorLevel2, 6) drawRow( []string{ c.Cari8, c.CariDetay, c.Doviz, format2(c.AcikKalemTutar), format2(c.AcikKalemUSD), format2(c.AcikKalemTRY), fmt.Sprintf("%d", c.OrtGun), fmt.Sprintf("%d", c.OrtBelgeGun), }, level2W, 5.8, true, [3]int{236, 240, 247}, map[int]bool{0: true, 2: true, 6: true, 7: true}, map[int]bool{3: true, 4: true, 5: true}, ) if needPage(5.8) { pageHeader() } drawHeaderRow(level3Cols, level3W, colorLevel3, 5.8) for _, d := range c.Details { if needPage(5.2) { pageHeader() drawHeaderRow(level3Cols, level3W, colorLevel3, 5.8) } drawRow( []string{ d.FaturaCari, d.OdemeCari, d.Doviz, d.FaturaRef, d.OdemeRef, d.FaturaTarihi, d.OdemeTarihi, d.OdemeDocDate, format2(d.EslesenTutar), format2(d.UsdTutar), format2(d.TryTutar), d.Aciklama, fmt.Sprintf("%d", d.Gun), fmt.Sprintf("%d", d.GunBelge), trFormat(d.GunKur, 2), }, level3W, 5.2, false, [3]int{}, map[int]bool{2: true, 5: true, 6: true, 7: true, 11: true, 12: true, 13: true, 14: true}, map[int]bool{8: true, 9: true, 10: true}, ) } pdf.Ln(1.2) } pdf.Ln(1.8) } } func trFormat(v float64, frac int) string { neg := v < 0 if neg { v = -v } pow := math.Pow(10, float64(frac)) rounded := math.Round(v*pow) / pow intPart := int64(rounded) decPart := int64(math.Round((rounded - float64(intPart)) * pow)) intStr := fmt.Sprintf("%d", intPart) var grouped strings.Builder for i, r := range intStr { if i > 0 && (len(intStr)-i)%3 == 0 { grouped.WriteString(".") } grouped.WriteRune(r) } out := grouped.String() if frac > 0 { decFmt := fmt.Sprintf("%%0%dd", frac) out += "," + fmt.Sprintf(decFmt, decPart) } if neg { return "-" + out } return out } func readStrAny(v ...interface{}) string { for _, x := range v { switch t := x.(type) { case nil: case string: if strings.TrimSpace(t) != "" { return t } case []byte: s := strings.TrimSpace(string(t)) if s != "" { return s } case time.Time: return t.Format("2006-01-02") default: s := strings.TrimSpace(fmt.Sprint(t)) if s != "" && s != "" { return s } } } return "" } func readFloatAny(v ...interface{}) float64 { for _, x := range v { if x == nil { continue } switch t := x.(type) { case float64: return t case float32: return float64(t) case int: return float64(t) case int32: return float64(t) case int64: return float64(t) case string: if n, ok := parseNumberFlexible(t); ok { return n } case []byte: if n, ok := parseNumberFlexible(string(t)); ok { return n } default: if n, ok := parseNumberFlexible(fmt.Sprint(t)); ok { return n } } } return 0 } func readIntAny(v ...interface{}) int { return int(math.Ceil(readFloatAny(v...))) } func parseNumberFlexible(s string) (float64, bool) { s = strings.TrimSpace(s) if s == "" { return 0, false } 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, false } return n, true } func parseYMD(v string) (time.Time, bool) { v = strings.TrimSpace(v) if v == "" { return time.Time{}, false } t, err := time.Parse("2006-01-02", v) if err != nil { return time.Time{}, false } return t, true }