Files
bssapp/svc/routes/statements_pdf.go

639 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// routes/statements_pdf.go
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/models"
"bssapp-backend/queries"
"bytes"
"database/sql"
"fmt"
"log"
"net/http"
"runtime/debug"
"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"
)
// 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} // secondaryden 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) error {
return registerDejavuFonts(pdf, fontFamilyReg, fontFamilyBold)
}
// 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 {
if logoPath, err := resolvePdfImagePath("Baggi-Tekstil-A.s-Logolu.jpeg"); err == nil {
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)
debug.PrintStack()
http.Error(w, fmt.Sprintf("PDF oluşturulurken panic oluştu: %v", rec), 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)
if err := ensureFonts(pdf); err != nil {
http.Error(w, "PDF font yükleme hatası: "+err.Error(), http.StatusInternalServerError)
return
}
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ı
if err := pdf.Error(); err != nil {
http.Error(w, "PDF render hatası: "+err.Error(), http.StatusInternalServerError)
return
}
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ı
12 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).
*/