403 lines
10 KiB
Go
403 lines
10 KiB
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 (
|
||
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"
|
||
)
|
||
|
||
// 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 {
|
||
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
|
||
}
|
||
|
||
/* ============================ 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) {
|
||
defer func() {
|
||
if rec := recover(); rec != nil {
|
||
log.Printf("❌ PANIC ExportStatementHeaderReportPDFHandler: %v", rec)
|
||
debug.PrintStack()
|
||
http.Error(w, fmt.Sprintf("header PDF panic: %v", rec), http.StatusInternalServerError)
|
||
}
|
||
}()
|
||
|
||
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)
|
||
}
|
||
|
||
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-header-report.pdf")
|
||
_, _ = w.Write(buf.Bytes())
|
||
|
||
log.Printf("✅ Header-only PDF üretimi tamamlandı (%s)", time.Since(started))
|
||
}
|
||
}
|