package routes import ( "bssapp-backend/auth" "bssapp-backend/models" "bssapp-backend/queries" "bytes" "database/sql" "fmt" "log" "net/http" "path/filepath" "sort" "strings" "time" "github.com/jung-kurt/gofpdf" ) /* ============================ SABİTLER ============================ */ // A4 Landscape (mm) const ( hPageWidth = 297.0 hPageHeight = 210.0 hMarginL = 8.0 hMarginT = 8.0 hMarginR = 8.0 hMarginB = 15.0 hLineHMain = 5.2 hCellPadX = 2.0 hHeaderRowH = 8.4 hGroupBarH = 9.0 hGridLineWidth = 0.3 hLogoW = 42.0 ) // Kolonlar var hMainCols = []string{ "Belge No", "Tarih", "Vade", "İşlem", "Açıklama", "Para", "Borç", "Alacak", "Bakiye", } var hMainWbase = []float64{ 34, 30, 28, 24, 60, 20, 36, 36, 36, } // Font dosyaları const ( hFontFamilyReg = "dejavu" hFontFamilyBold = "dejavu-b" ) // Renkler var ( hColorPrimary = [3]int{149, 113, 22} // #957116 hColorSecondary = [3]int{218, 193, 151} // #dac197 (Baggi secondary) ) /* ============================ FONT / FORMAT ============================ */ func hEnsureFonts(pdf *gofpdf.Fpdf) error { return registerDejavuFonts(pdf, hFontFamilyReg, hFontFamilyBold) } func hNormalizeWidths(base []float64, targetTotal float64) []float64 { sum := 0.0 for _, v := range base { sum += v } scale := targetTotal / sum out := make([]float64, len(base)) for i, v := range base { out[i] = v * scale } return out } func hFormatCurrencyTR(val float64) string { s := fmt.Sprintf("%.2f", val) parts := strings.Split(s, ".") intPart, decPart := parts[0], parts[1] 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 strings.Join(out, ".") + "," + decPart } func hNeedNewPage(pdf *gofpdf.Fpdf, needH float64) bool { return pdf.GetY()+needH+hMarginB > hPageHeight } /* ============================ YARDIMCI FONKSİYONLAR ============================ */ func hSplitLinesSafe(pdf *gofpdf.Fpdf, text string, width float64) [][]byte { if width <= 0 { width = 1 } return pdf.SplitLines([]byte(text), width) } func hDrawWrapText(pdf *gofpdf.Fpdf, text string, x, y, width, lineH float64) { if text == "" { return } lines := hSplitLinesSafe(pdf, text, width) cy := y for _, ln := range lines { pdf.SetXY(x, cy) pdf.CellFormat(width, lineH, string(ln), "", 0, "L", false, 0, "") cy += lineH } } func hCalcRowHeightForText(pdf *gofpdf.Fpdf, text string, colWidth, lineHeight, padX float64) float64 { if text == "" { return lineHeight } lines := hSplitLinesSafe(pdf, text, colWidth-(2*padX)) h := float64(len(lines)) * lineHeight if h < lineHeight { h = lineHeight } return h } /* ============================ HEADER ============================ */ func hDrawPageHeader(pdf *gofpdf.Fpdf, cariKod, cariIsim, start, end string) float64 { logoPath, _ := filepath.Abs("./public/Baggi-Tekstil-A.s-Logolu.jpeg") // Logo pdf.ImageOptions(logoPath, hMarginL, 2, hLogoW, 0, false, gofpdf.ImageOptions{}, 0, "") // Başlıklar pdf.SetFont(hFontFamilyBold, "", 16) pdf.SetTextColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2]) pdf.SetXY(hMarginL+hLogoW+8, hMarginT+2) pdf.CellFormat(120, 7, "Baggi Software System", "", 0, "L", false, 0, "") pdf.SetFont(hFontFamilyBold, "", 12) pdf.SetXY(hMarginL+hLogoW+8, hMarginT+10) pdf.CellFormat(120, 6, "Cari Hesap Raporu", "", 0, "L", false, 0, "") // Bugünün tarihi (sağ üst) today := time.Now().Format("02.01.2006") pdf.SetFont(hFontFamilyReg, "", 9) pdf.SetXY(hPageWidth-hMarginR-40, hMarginT+3) pdf.CellFormat(40, 6, "Tarih: "+today, "", 0, "R", false, 0, "") // Cari & Tarih kutuları (daha yukarı taşındı) boxY := hMarginT + hLogoW - 6 pdf.SetFont(hFontFamilyBold, "", 10) pdf.Rect(hMarginL, boxY, 140, 11, "") pdf.SetXY(hMarginL+2, boxY+3) pdf.CellFormat(136, 5, fmt.Sprintf("Cari: %s — %s", cariKod, cariIsim), "", 0, "L", false, 0, "") pdf.Rect(hPageWidth-hMarginR-140, boxY, 140, 11, "") pdf.SetXY(hPageWidth-hMarginR-138, boxY+3) pdf.CellFormat(136, 5, fmt.Sprintf("Tarih Aralığı: %s → %s", start, end), "", 0, "R", false, 0, "") // Alt çizgi y := boxY + 13 pdf.SetDrawColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2]) pdf.Line(hMarginL, y, hPageWidth-hMarginR, y) pdf.SetDrawColor(200, 200, 200) return y + 4 } /* ============================ TABLO ============================ */ func hDrawGroupBar(pdf *gofpdf.Fpdf, currency string, sonBakiye float64) { x := hMarginL y := pdf.GetY() w := hPageWidth - hMarginL - hMarginR h := hGroupBarH pdf.SetDrawColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2]) pdf.SetLineWidth(0.6) pdf.Rect(x, y, w, h, "") pdf.SetFont(hFontFamilyBold, "", 11.3) pdf.SetTextColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2]) pdf.SetXY(x+hCellPadX+1.0, y+(h-5.0)/2) pdf.CellFormat(w*0.6, 5.0, currency, "", 0, "L", false, 0, "") txt := "Son Bakiye = " + hFormatCurrencyTR(sonBakiye) pdf.SetXY(x+w*0.4, y+(h-5.0)/2) pdf.CellFormat(w*0.6-2.0, 5.0, txt, "", 0, "R", false, 0, "") pdf.SetLineWidth(hGridLineWidth) pdf.SetDrawColor(200, 200, 200) pdf.SetTextColor(0, 0, 0) pdf.Ln(h) } func hDrawMainHeaderRow(pdf *gofpdf.Fpdf, cols []string, widths []float64) { pdf.SetFont(hFontFamilyBold, "", 10.2) pdf.SetTextColor(255, 255, 255) pdf.SetFillColor(hColorPrimary[0], hColorPrimary[1], hColorPrimary[2]) pdf.SetDrawColor(120, 90, 20) x := hMarginL y := pdf.GetY() for i, c := range cols { pdf.Rect(x, y, widths[i], hHeaderRowH, "DF") pdf.SetXY(x+hCellPadX, y+1.6) pdf.CellFormat(widths[i]-2*hCellPadX, hHeaderRowH-3.2, c, "", 0, "C", false, 0, "") x += widths[i] } pdf.Ln(hHeaderRowH) } func hDrawMainDataRow(pdf *gofpdf.Fpdf, row []string, widths []float64, rowH float64, rowIndex int) { x := hMarginL y := pdf.GetY() // ✅ Zebra efekti wTotal := hPageWidth - hMarginL - hMarginR if rowIndex%2 == 1 { pdf.SetFillColor(hColorSecondary[0], hColorSecondary[1], hColorSecondary[2]) pdf.Rect(x, y, wTotal, rowH, "F") } pdf.SetFont(hFontFamilyReg, "", 8.2) pdf.SetDrawColor(242, 235, 222) pdf.SetTextColor(30, 30, 30) for i, val := range row { pdf.Rect(x, y, widths[i], rowH, "") switch { case i == 4: // Açıklama wrap hDrawWrapText(pdf, val, x+hCellPadX, y+0.5, widths[i]-2*hCellPadX, hLineHMain) case i >= 6: pdf.SetXY(x+hCellPadX, y+(rowH-hLineHMain)/2) pdf.CellFormat(widths[i]-2*hCellPadX, hLineHMain, val, "", 0, "R", false, 0, "") default: pdf.SetXY(x+hCellPadX, y+(rowH-hLineHMain)/2) pdf.CellFormat(widths[i]-2*hCellPadX, hLineHMain, val, "", 0, "L", false, 0, "") } x += widths[i] } // ❌ pdf.Ln(rowH) YOK // ✅ Manuel olarak Y'yi arttırıyoruz pdf.SetY(y + rowH) } /* ============================ MAIN HANDLER ============================ */ func ExportStatementHeaderReportPDFHandler(mssql *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 } started := time.Now() accountCode := r.URL.Query().Get("accountcode") startDate := r.URL.Query().Get("startdate") endDate := r.URL.Query().Get("enddate") rawParis := r.URL.Query()["parislemler"] var parislemler []string for _, v := range rawParis { v = strings.TrimSpace(v) if v != "" && v != "undefined" && v != "null" { parislemler = append(parislemler, v) } } headers, _, err := queries.GetStatementsHPDF(accountCode, startDate, endDate, parislemler) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } type grp struct { code string rows []models.StatementHeader sonBakiye float64 } order := []string{} groups := map[string]*grp{} for _, h := range headers { g, ok := groups[h.ParaBirimi] if !ok { g = &grp{code: h.ParaBirimi} groups[h.ParaBirimi] = g order = append(order, h.ParaBirimi) } g.rows = append(g.rows, h) } 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 }) if n := len(groups[k].rows); n > 0 { groups[k].sonBakiye = groups[k].rows[n-1].Bakiye } } pdf := gofpdf.New("L", "mm", "A4", "") pdf.SetMargins(hMarginL, hMarginT, hMarginR) pdf.SetAutoPageBreak(false, hMarginB) if err := hEnsureFonts(pdf); err != nil { http.Error(w, "PDF font yükleme hatası: "+err.Error(), http.StatusInternalServerError) return } wAvail := hPageWidth - hMarginL - hMarginR mainWn := hNormalizeWidths(hMainWbase, wAvail) cariIsim := "" if len(headers) > 0 { cariIsim = headers[0].CariIsim } pageNum := 0 newPage := func() { pageNum++ pdf.AddPage() tableTop := hDrawPageHeader(pdf, accountCode, cariIsim, startDate, endDate) pdf.SetY(tableTop) } newPage() for _, cur := range order { g := groups[cur] hDrawGroupBar(pdf, cur, g.sonBakiye) hDrawMainHeaderRow(pdf, hMainCols, mainWn) rowIndex := 0 for _, h := range g.rows { row := []string{ h.BelgeNo, h.BelgeTarihi, h.VadeTarihi, h.IslemTipi, h.Aciklama, h.ParaBirimi, hFormatCurrencyTR(h.Borc), hFormatCurrencyTR(h.Alacak), hFormatCurrencyTR(h.Bakiye), } rh := hCalcRowHeightForText(pdf, row[4], mainWn[4], hLineHMain, hCellPadX) if hNeedNewPage(pdf, rh+hHeaderRowH) { newPage() hDrawGroupBar(pdf, cur, g.sonBakiye) hDrawMainHeaderRow(pdf, hMainCols, mainWn) } hDrawMainDataRow(pdf, row, mainWn, rh, rowIndex) rowIndex++ } pdf.Ln(1) } var buf bytes.Buffer if err := pdf.Output(&buf); err != nil { http.Error(w, "PDF oluşturulamadı: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/pdf") w.Header().Set("Content-Disposition", "inline; filename=statement-header-report.pdf") _, _ = w.Write(buf.Bytes()) log.Printf("✅ Header-only PDF üretimi tamamlandı (%s)", time.Since(started)) } }