// routes/statements_pdf.go package routes import ( "bssapp-backend/auth" "bssapp-backend/models" "bssapp-backend/queries" "bytes" "database/sql" "fmt" "log" "net/http" "os" "path/filepath" "sort" "strings" "time" "github.com/jung-kurt/gofpdf" ) /* ============================ SABİTLER ============================ */ // A4 Landscape (mm) const ( pageWidth = 297.0 pageHeight = 210.0 // Kenar boşlukları (mm) marginL = 8.0 marginT = 8.0 marginR = 8.0 marginB = 15.0 // Satır aralıkları (mm) lineHMain = 6.0 // ana satır biraz iri lineHDetail = 5.2 cellPadX = 2.2 // Satır yükseklikleri (mm) headerRowH = 8.4 subHeaderRowH = 6.2 groupBarH = 9.0 // Çizgi kalınlığı gridLineWidth = 0.3 // Logo genişliği (yükseklik oranlanır) logoW = 42.0 ) // Ana tablo kolonları var mainCols = []string{ "Belge No", "Tarih", "Vade", "İşlem", "Açıklama", "Para", "Borç", "Alacak", "Bakiye", } // Ana tablo kolon genişlikleri (ilk 3 geniş) var mainWbase = []float64{ 34, // Belge No 30, // Tarih 28, // Vade 24, // İşlem 60, // Açıklama (biraz daraltıldı) 20, // Para 36, // Borç (genişletildi) 36, // Alacak (genişletildi) 36, // Bakiye (genişletildi) } // Detay tablo kolonları ve genişlikleri var dCols = []string{ "Ana Grup", "Alt Grup", "Garson", "Fit", "İçerik", "Ürün", "Renk", "Adet", "Fiyat", "Tutar", } var dWbase = []float64{ 30, 28, 22, 20, 56, 30, 22, 20, 20, 26} // Font dosyaları const ( fontFamilyReg = "dejavu" fontFamilyBold = "dejavu-b" fontPathReg = "fonts/DejaVuSans.ttf" fontPathBold = "fonts/DejaVuSans-Bold.ttf" ) // Kurumsal renkler var ( colorPrimary = [3]int{149, 113, 22} // #957116 (altın) colorSecondary = [3]int{218, 193, 151} // #dac197 (bej) colorDetailFill = [3]int{242, 235, 222} // secondary’den daha açık (detay satır zemin) ) /* ============================ HELPERS ============================ */ // Genişlikleri normalize et func normalizeWidths(base []float64, targetTotal float64) []float64 { sum := 0.0 for _, v := range base { sum += v } out := make([]float64, len(base)) if sum <= 0 { each := targetTotal / float64(len(base)) for i := range out { out[i] = each } return out } scale := targetTotal / sum for i, v := range base { out[i] = v * scale } return out } // Türkçe formatlı sayı func formatCurrencyTR(n float64) string { s := fmt.Sprintf("%.2f", n) // "864000.00" parts := strings.Split(s, ".") intPart := parts[0] decPart := parts[1] // Binlik ayırıcı (.) var out strings.Builder for i, c := range intPart { if (len(intPart)-i)%3 == 0 && i != 0 { out.WriteRune('.') } out.WriteRune(c) } return out.String() + "," + decPart } // Fontları yükle func ensureFonts(pdf *gofpdf.Fpdf) { if _, err := os.Stat(fontPathReg); err == nil { pdf.AddUTF8Font(fontFamilyReg, "", fontPathReg) } else { log.Printf("⚠️ Font bulunamadı: %s", fontPathReg) } if _, err := os.Stat(fontPathBold); err == nil { pdf.AddUTF8Font(fontFamilyBold, "", fontPathBold) } else { log.Printf("⚠️ Font bulunamadı: %s", fontPathBold) } } // Güvenli satır kırma func splitLinesSafe(pdf *gofpdf.Fpdf, text string, width float64) [][]byte { if width <= 0 { width = 1 } defer func() { if rec := recover(); rec != nil { log.Printf("⚠️ splitLinesSafe recover: %v", rec) } }() return pdf.SplitLines([]byte(text), width) } // Metin wrap çizimi func drawWrapText(pdf *gofpdf.Fpdf, text string, x, y, width, lineH float64) { if text == "" { return } lines := splitLinesSafe(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 } } // Wrap satır yüksekliği func calcRowHeightForText(pdf *gofpdf.Fpdf, text string, colWidth, lineHeight, padX float64) float64 { if colWidth <= 2*padX { colWidth = 2*padX + 1 } if text == "" { return lineHeight } lines := splitLinesSafe(pdf, text, colWidth-(2*padX)) if len(lines) == 0 { return lineHeight } h := float64(len(lines)) * lineHeight if h < lineHeight { h = lineHeight } return h } // Alt tarafa sığmıyor mu? func needNewPage(pdf *gofpdf.Fpdf, needH float64) bool { return pdf.GetY()+needH+marginB > pageHeight } // NULL/boş’ları uzun çizgiye çevir func nullToDash(s string) string { if strings.TrimSpace(s) == "" { return "—" } return s } /* ============================ HEADER DRAW ============================ */ // Küçük yardımcı: kutu içine otomatik 1-2 satır metin (taşarsa 2) func drawLabeledBox(pdf *gofpdf.Fpdf, x, y, w, h float64, label, value string, align string) { // Çerçeve pdf.Rect(x, y, w, h, "") // İç marj ix := x + 2 iy := y + 1.8 iw := w - 4 // Etiket (kalın, küçük) pdf.SetFont(fontFamilyBold, "", 8) pdf.SetXY(ix, iy) pdf.CellFormat(iw, 4, label, "", 0, "L", false, 0, "") // Değer (normal, 1-2 satır) pdf.SetFont(fontFamilyReg, "", 8) vy := iy + 4.2 lineH := 4.2 lines := splitLinesSafe(pdf, value, iw) if len(lines) > 2 { lines = lines[:2] // en fazla 2 satır göster } for _, ln := range lines { pdf.SetXY(ix, vy) pdf.CellFormat(iw, lineH, string(ln), "", 0, align, false, 0, "") vy += lineH } } func drawPageHeader(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 } /* ============================ GROUP BAR ============================ */ func drawGroupBar(pdf *gofpdf.Fpdf, currency string, sonBakiye float64) { // Kutu alanı (tam genişlik) x := marginL y := pdf.GetY() w := pageWidth - marginL - marginR h := groupBarH // Çerçeve pdf.SetDrawColor(colorPrimary[0], colorPrimary[1], colorPrimary[2]) pdf.SetLineWidth(0.6) pdf.Rect(x, y, w, h, "") // Metinler pdf.SetFont(fontFamilyBold, "", 11.3) // bir tık büyük pdf.SetTextColor(colorPrimary[0], colorPrimary[1], colorPrimary[2]) pdf.SetXY(x+cellPadX+1.0, y+(h-5.0)/2) pdf.CellFormat(w*0.6, 5.0, fmt.Sprintf("%s", currency), "", 0, "L", false, 0, "") txt := "Son Bakiye = " + formatCurrencyTR(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, "") // Renk/kalınlık geri pdf.SetLineWidth(gridLineWidth) pdf.SetDrawColor(200, 200, 200) pdf.SetTextColor(0, 0, 0) pdf.Ln(h) } /* ============================ HEADER ROWS ============================ */ func drawMainHeaderRow(pdf *gofpdf.Fpdf, cols []string, widths []float64) { // Ana başlık: Primary arkaplan, beyaz yazı pdf.SetFont(fontFamilyBold, "", 10.2) pdf.SetTextColor(255, 255, 255) pdf.SetFillColor(colorPrimary[0], colorPrimary[1], colorPrimary[2]) pdf.SetDrawColor(120, 90, 20) x := marginL y := pdf.GetY() for i, c := range cols { pdf.Rect(x, y, widths[i], headerRowH, "DF") pdf.SetXY(x+cellPadX, y+1.6) pdf.CellFormat(widths[i]-2*cellPadX, headerRowH-3.2, c, "", 0, "C", false, 0, "") x += widths[i] } pdf.SetY(y + headerRowH) } func drawDetailHeaderRow(pdf *gofpdf.Fpdf, cols []string, widths []float64) { // Detay başlık: Secondary (daha açık) pdf.SetFont(fontFamilyBold, "", 9.2) pdf.SetFillColor(colorSecondary[0], colorSecondary[1], colorSecondary[2]) pdf.SetTextColor(0, 0, 0) pdf.SetDrawColor(160, 140, 100) x := marginL y := pdf.GetY() for i, c := range cols { pdf.Rect(x, y, widths[i], subHeaderRowH, "DF") pdf.SetXY(x+cellPadX, y+1.2) pdf.CellFormat(widths[i]-2*cellPadX, subHeaderRowH-2.4, c, "", 0, "C", false, 0, "") x += widths[i] } pdf.Ln(subHeaderRowH) } /* ============================ DATA ROWS ============================ */ // Ana veri satırı: daha büyük ve kalın görünsün // Ana veri satırı (zebrasız, sıkı grid – satırlar yapışık) func drawMainDataRow(pdf *gofpdf.Fpdf, row []string, widths []float64, rowH float64) { x := marginL y := pdf.GetY() // Arka plan beyaz, çizgiler gri pdf.SetFont(fontFamilyBold, "", 8.5) pdf.SetDrawColor(210, 210, 210) pdf.SetTextColor(30, 30, 30) pdf.SetFillColor(255, 255, 255) for i, val := range row { // Hücre çerçevesi pdf.Rect(x, y, widths[i], rowH, "") switch { case i == 4: // Açıklama wrap drawWrapText(pdf, val, x+cellPadX, y+0.8, widths[i]-2*cellPadX, lineHMain) case i >= 6: // Sayısal sağ pdf.SetXY(x+cellPadX, y+(rowH-lineHMain)/2) pdf.CellFormat(widths[i]-2*cellPadX, lineHMain, val, "", 0, "R", false, 0, "") default: // Normal sol pdf.SetXY(x+cellPadX, y+(rowH-lineHMain)/2) pdf.CellFormat(widths[i]-2*cellPadX, lineHMain, val, "", 0, "L", false, 0, "") } x += widths[i] } // ❌ pdf.Ln(rowH) yerine: // ✅ bir alt satıra tam yapışık in pdf.SetY(y + rowH) } // Detay veri satırı: açık zemin, biraz küçük; zebra opsiyonel func drawDetailDataRow(pdf *gofpdf.Fpdf, row []string, widths []float64, rowH float64, fillBg bool) { pdf.SetFont(fontFamilyReg, "", 8.0) pdf.SetTextColor(60, 60, 60) pdf.SetDrawColor(220, 220, 220) x := marginL y := pdf.GetY() for i, val := range row { // Zemin if fillBg { pdf.SetFillColor(colorDetailFill[0], colorDetailFill[1], colorDetailFill[2]) pdf.Rect(x, y, widths[i], rowH, "DF") pdf.SetFillColor(255, 255, 255) // geri } else { pdf.Rect(x, y, widths[i], rowH, "") } switch { case i == 4: // İçerik wrap drawWrapText(pdf, val, x+cellPadX, y+1.4, widths[i]-2*cellPadX, lineHDetail) case i >= 7: // Sayısal sağ pdf.SetXY(x+cellPadX, y+(rowH-lineHDetail)/2) pdf.CellFormat(widths[i]-2*cellPadX, lineHDetail, val, "", 0, "R", false, 0, "") default: // Sol pdf.SetXY(x+cellPadX, y+(rowH-lineHDetail)/2) pdf.CellFormat(widths[i]-2*cellPadX, lineHDetail, val, "", 0, "L", false, 0, "") } x += widths[i] } pdf.Ln(rowH) } /* ============================ HANDLER ============================ */ func ExportPDFHandler(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() defer func() { if rec := recover(); rec != nil { log.Printf("❌ PANIC ExportPDFHandler: %v", rec) http.Error(w, "PDF oluşturulurken hata oluştu", http.StatusInternalServerError) } }() accountCode := r.URL.Query().Get("accountcode") startDate := r.URL.Query().Get("startdate") endDate := r.URL.Query().Get("enddate") // parislemler sanitize rawParis := r.URL.Query()["parislemler"] parislemler := make([]string, 0, len(rawParis)) for _, v := range rawParis { v = strings.TrimSpace(v) if v == "" || strings.EqualFold(v, "undefined") || strings.EqualFold(v, "null") { continue } parislemler = append(parislemler, v) } log.Printf("▶️ ExportPDFHandler: account=%s start=%s end=%s parislemler=%v", accountCode, startDate, endDate, parislemler) // 1) Header verileri headers, belgeNos, err := queries.GetStatementsPDF(accountCode, startDate, endDate, parislemler) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } log.Printf("✅ Header verileri alındı: %d kayıt, %d belge no", len(headers), len(belgeNos)) // 2) Detay verileri detailMap, err := queries.GetDetailsMapPDF(belgeNos, startDate, endDate) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } log.Printf("✅ Detay verileri alındı: %d belge için detay var", len(detailMap)) // 3) Gruplama 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 } } // 4) Kolon genişlikleri wAvail := pageWidth - marginL - marginR mainWn := normalizeWidths(mainWbase, wAvail) dWn := normalizeWidths(dWbase, wAvail) // 5) PDF init pdf := gofpdf.New("L", "mm", "A4", "") pdf.SetMargins(marginL, marginT, marginR) pdf.SetAutoPageBreak(false, marginB) ensureFonts(pdf) pdf.SetFont(fontFamilyReg, "", 8.5) pdf.SetTextColor(0, 0, 0) pageNum := 0 cariIsim := "" if len(headers) > 0 { cariIsim = headers[0].CariIsim } // Sayfa başlatıcı (header yüksekliği dinamik) newPage := func() { pageNum++ pdf.AddPage() // drawPageHeader tablo başlangıç yüksekliğini döndürüyor tableTop := drawPageHeader(pdf, accountCode, cariIsim, startDate, endDate) // Sayfa numarası pdf.SetFont(fontFamilyReg, "", 6) pdf.SetXY(pageWidth-marginR-28, pageHeight-marginB+3) pdf.CellFormat(28, 5, fmt.Sprintf("Sayfa %d", pageNum), "", 0, "R", false, 0, "") // Tablo Y konumunu ayarla pdf.SetY(tableTop) } newPage() // 6) Gruplar yaz for _, cur := range order { g := groups[cur] if needNewPage(pdf, groupBarH+headerRowH) { newPage() } drawGroupBar(pdf, cur, g.sonBakiye) drawMainHeaderRow(pdf, mainCols, mainWn) for _, h := range g.rows { row := []string{ h.BelgeNo, h.BelgeTarihi, h.VadeTarihi, h.IslemTipi, h.Aciklama, h.ParaBirimi, formatCurrencyTR(h.Borc), formatCurrencyTR(h.Alacak), formatCurrencyTR(h.Bakiye), } pdf.SetFont(fontFamilyBold, "", 9.6) rh := calcRowHeightForText(pdf, row[4], mainWn[4], lineHMain, cellPadX) if needNewPage(pdf, rh+headerRowH) { newPage() drawGroupBar(pdf, cur, g.sonBakiye) drawMainHeaderRow(pdf, mainCols, mainWn) } drawMainDataRow(pdf, row, mainWn, rh) // detaylar details := detailMap[h.BelgeNo] if len(details) > 0 { if needNewPage(pdf, subHeaderRowH) { newPage() drawGroupBar(pdf, cur, g.sonBakiye) drawMainHeaderRow(pdf, mainCols, mainWn) } drawDetailHeaderRow(pdf, dCols, dWn) for i, d := range details { drow := []string{ nullToDash(d.UrunAnaGrubu), nullToDash(d.UrunAltGrubu), nullToDash(d.YetiskinGarson), nullToDash(d.Fit), nullToDash(d.Icerik), nullToDash(d.UrunKodu), nullToDash(d.UrunRengi), formatCurrencyTR(d.ToplamAdet), formatCurrencyTR(d.ToplamFiyat), formatCurrencyTR(d.ToplamTutar), } pdf.SetFont(fontFamilyReg, "", 8) rh2 := calcRowHeightForText(pdf, drow[4], dWn[4], lineHDetail, cellPadX) if needNewPage(pdf, rh2) { newPage() drawGroupBar(pdf, cur, g.sonBakiye) drawMainHeaderRow(pdf, mainCols, mainWn) drawDetailHeaderRow(pdf, dCols, dWn) } // zebra: çift indekslerde açık zemin fill := (i%2 == 0) drawDetailDataRow(pdf, drow, dWn, rh2, fill) } } } pdf.Ln(3) } // 7) Çıktı 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.pdf") _, _ = w.Write(buf.Bytes()) log.Printf("✅ PDF üretimi tamam: %s", time.Since(started)) } } /* NOTLAR: - Header artık dinamik yüksekliğe sahip (drawPageHeader -> contentTopY döner). - Logo sol üst köşe, başlık “toolbar” gibi; “Cari / Tarih” kutuları 1–2 satır metni çerçeve içinde otomatik kırar. - Grup barı primary renkte çerçeveli ve yazı biraz büyük. - Ana satırlar kalın & bir tık büyük (detaydan farklı). - Detay satırlar açık (bej) zeminle zebra (i%2==0) uygulanır. - SplitLines her yerde splitLinesSafe ile korunuyor (panic yok). - AddPage sadece newPage()’de. - Font bulunamazsa gömme fontlarla devam edilir (log yazar). */