package routes import ( "bssapp-backend/queries" "bytes" "database/sql" "fmt" "net/http" "sort" "strconv" "strings" "time" "github.com/jung-kurt/gofpdf" ) type orderListPDFRow struct { OrderNumber string OrderDate string TerminTarihi string CurrAccCode string CurrAccDescription string MusteriTemsilcisi string Piyasa string DocCurrencyCode string TotalAmount float64 TotalAmountUSD float64 PackedUSD float64 PackedRatePct float64 HasUretimUrunu bool Description string } type pdfColumn struct { Header string Width float64 Align string Wrap bool MaxLines int } func OrderListPDFRoute(db *sql.DB) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { search := strings.TrimSpace(r.URL.Query().Get("search")) currAcc := strings.TrimSpace(r.URL.Query().Get("CurrAccCode")) orderDate := strings.TrimSpace(r.URL.Query().Get("OrderDate")) sortBy := strings.TrimSpace(r.URL.Query().Get("sort_by")) descending := parseBool(r.URL.Query().Get("descending")) rows, err := queries.GetOrderListExcel(db, search, currAcc, orderDate) if err != nil { http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError) return } defer rows.Close() out := make([]orderListPDFRow, 0, 256) for rows.Next() { var ( id, no, date, termin, code, name string rep, piyasa, cur string total float64 totalUSD float64 packedAmount float64 packedUSD float64 packedRatePct float64 hasUretim bool desc string usdRate float64 ) if err := rows.Scan( &id, &no, &date, &termin, &code, &name, &rep, &piyasa, &cur, &total, &totalUSD, &packedAmount, &packedUSD, &packedRatePct, &hasUretim, &desc, &usdRate, ); err != nil { http.Error(w, "scan error: "+err.Error(), http.StatusInternalServerError) return } _ = id _ = packedAmount _ = usdRate out = append(out, orderListPDFRow{ OrderNumber: no, OrderDate: date, TerminTarihi: termin, CurrAccCode: code, CurrAccDescription: name, MusteriTemsilcisi: rep, Piyasa: piyasa, DocCurrencyCode: cur, TotalAmount: total, TotalAmountUSD: totalUSD, PackedUSD: packedUSD, PackedRatePct: packedRatePct, HasUretimUrunu: hasUretim, Description: desc, }) } applyOrderListSort(out, sortBy, descending) 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 } drawOrderListPDF(pdf, out, search, currAcc, orderDate, sortBy, descending) 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=\"siparis-listesi.pdf\"") _, _ = w.Write(buf.Bytes()) }) } func drawOrderListPDF( pdf *gofpdf.Fpdf, rows []orderListPDFRow, search, currAcc, orderDate, sortBy string, desc bool, ) { cols := []pdfColumn{ {Header: "Siparis No", Width: 20, Align: "L", Wrap: false, MaxLines: 1}, {Header: "Tarih", Width: 12, Align: "C", Wrap: false, MaxLines: 1}, {Header: "Termin", Width: 12, Align: "C", Wrap: false, MaxLines: 1}, {Header: "Cari Kod", Width: 15, Align: "L", Wrap: true, MaxLines: 2}, {Header: "Cari Adi", Width: 27, Align: "L", Wrap: true, MaxLines: 3}, {Header: "Temsilci", Width: 18, Align: "L", Wrap: true, MaxLines: 2}, {Header: "Piyasa", Width: 13, Align: "L", Wrap: true, MaxLines: 2}, {Header: "PB", Width: 8, Align: "C", Wrap: false, MaxLines: 1}, {Header: "Tutar", Width: 15, Align: "R", Wrap: false, MaxLines: 1}, {Header: "USD", Width: 15, Align: "R", Wrap: false, MaxLines: 1}, {Header: "Paket USD", Width: 15, Align: "R", Wrap: false, MaxLines: 1}, {Header: "%", Width: 8, Align: "R", Wrap: false, MaxLines: 1}, {Header: "Uretim", Width: 24, Align: "L", Wrap: true, MaxLines: 3}, {Header: "Aciklama", Width: 52, Align: "L", Wrap: true, MaxLines: 3}, } pageW, pageH := pdf.GetPageSize() marginL, marginT, marginR := 8.0, 8.0, 8.0 bottomLimit := pageH - 10.0 lineH := 3.8 cellPadX := 1.1 cellPadY := 0.6 drawHeader := func() { pdf.AddPage() pdf.SetFont("dejavu", "B", 12) pdf.CellFormat(0, 7, "Siparis Listesi", "", 1, "L", false, 0, "") pdf.SetFont("dejavu", "", 8) sortText := "TotalAmountUSD DESC (varsayilan)" if sortBy != "" { dir := "ASC" if desc { dir = "DESC" } sortText = fmt.Sprintf("%s %s", sortBy, dir) } filterLine := fmt.Sprintf( "Filtreler -> Arama: %s | Cari: %s | Siparis Tarihi: %s | Siralama: %s | Olusturma: %s", emptyDash(search), emptyDash(currAcc), emptyDash(orderDate), sortText, time.Now().Format("2006-01-02 15:04"), ) pdf.CellFormat(0, 5, filterLine, "", 1, "L", false, 0, "") pdf.Ln(1) pdf.SetFont("dejavu", "B", 8) pdf.SetFillColor(240, 240, 240) for _, c := range cols { pdf.CellFormat(c.Width, 6, c.Header, "1", 0, "C", true, 0, "") } pdf.Ln(-1) pdf.SetFont("dejavu", "", 7) } buildCells := func(row orderListPDFRow) []string { uretim := "" if row.HasUretimUrunu { uretim = "Uretime Verilecek Urunu Var" } return []string{ row.OrderNumber, row.OrderDate, row.TerminTarihi, row.CurrAccCode, row.CurrAccDescription, row.MusteriTemsilcisi, row.Piyasa, row.DocCurrencyCode, fmt.Sprintf("%.2f", row.TotalAmount), fmt.Sprintf("%.2f", row.TotalAmountUSD), fmt.Sprintf("%.2f", row.PackedUSD), fmt.Sprintf("%.2f", row.PackedRatePct), uretim, row.Description, } } calcLineCount := func(col pdfColumn, text string) int { if !col.Wrap { return 1 } lines := pdf.SplitLines([]byte(strings.TrimSpace(text)), col.Width-(cellPadX*2)) n := len(lines) if n < 1 { n = 1 } if col.MaxLines > 0 && n > col.MaxLines { n = col.MaxLines } return n } drawWrappedCell := func(x, y, w, h float64, text, align string, maxLines int) { pdf.Rect(x, y, w, h, "D") lines := pdf.SplitLines([]byte(strings.TrimSpace(text)), w-(cellPadX*2)) if len(lines) == 0 { lines = [][]byte{[]byte("")} } if maxLines > 0 && len(lines) > maxLines { lines = lines[:maxLines] } ty := y + cellPadY + 2.8 for _, ln := range lines { pdf.SetXY(x+cellPadX, ty-2.8) pdf.CellFormat(w-(cellPadX*2), 3.8, string(ln), "", 0, align, false, 0, "") ty += 3.8 } } drawHeader() for _, row := range rows { cells := buildCells(row) maxLines := 1 for i, c := range cols { n := calcLineCount(c, cells[i]) if n > maxLines { maxLines = n } } rowH := (float64(maxLines) * lineH) + (cellPadY * 2) if pdf.GetY()+rowH > bottomLimit { drawHeader() } x := marginL y := pdf.GetY() for i, c := range cols { if c.Wrap { drawWrappedCell(x, y, c.Width, rowH, cells[i], c.Align, c.MaxLines) } else { pdf.SetXY(x, y) pdf.CellFormat(c.Width, rowH, cells[i], "1", 0, c.Align, false, 0, "") } x += c.Width } pdf.SetXY(marginL, y+rowH) } _ = pageW _ = marginT _ = marginR } func applyOrderListSort(rows []orderListPDFRow, sortBy string, desc bool) { if len(rows) <= 1 { return } field := strings.TrimSpace(sortBy) if field == "" { field = "TotalAmountUSD" desc = true } lessText := func(a, b string) bool { aa := strings.ToLower(strings.TrimSpace(a)) bb := strings.ToLower(strings.TrimSpace(b)) if desc { return aa > bb } return aa < bb } lessNum := func(a, b float64) bool { if desc { return a > b } return a < b } lessBool := func(a, b bool) bool { av, bv := 0, 0 if a { av = 1 } if b { bv = 1 } if desc { return av > bv } return av < bv } sort.SliceStable(rows, func(i, j int) bool { a := rows[i] b := rows[j] switch field { case "OrderNumber": return lessText(a.OrderNumber, b.OrderNumber) case "OrderDate": return lessText(a.OrderDate, b.OrderDate) case "TerminTarihi": return lessText(a.TerminTarihi, b.TerminTarihi) case "CurrAccCode": return lessText(a.CurrAccCode, b.CurrAccCode) case "CurrAccDescription": return lessText(a.CurrAccDescription, b.CurrAccDescription) case "MusteriTemsilcisi": return lessText(a.MusteriTemsilcisi, b.MusteriTemsilcisi) case "Piyasa": return lessText(a.Piyasa, b.Piyasa) case "DocCurrencyCode": return lessText(a.DocCurrencyCode, b.DocCurrencyCode) case "TotalAmount": return lessNum(a.TotalAmount, b.TotalAmount) case "PackedUSD": return lessNum(a.PackedUSD, b.PackedUSD) case "PackedRatePct": return lessNum(a.PackedRatePct, b.PackedRatePct) case "HasUretimUrunu": return lessBool(a.HasUretimUrunu, b.HasUretimUrunu) case "Description": return lessText(a.Description, b.Description) default: return lessNum(a.TotalAmountUSD, b.TotalAmountUSD) } }) } func emptyDash(v string) string { if strings.TrimSpace(v) == "" { return "-" } return v } func parseBool(v string) bool { b, err := strconv.ParseBool(strings.TrimSpace(v)) if err == nil { return b } switch strings.TrimSpace(strings.ToLower(v)) { case "1", "yes", "on": return true default: return false } }