Files
honeyDueAPI/internal/services/pdf_service.go
Trey t 739b245ee6 Fix PDF report UTF-8 encoding for residence names and task fields
Add UnicodeTranslatorFromDescriptor to convert UTF-8 strings to
Windows-1252 for gofpdf built-in fonts. Prevents garbled characters
in residence names, task titles, categories, priorities, and statuses.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 11:23:44 -05:00

183 lines
4.9 KiB
Go

package services
import (
"bytes"
"fmt"
"time"
"github.com/jung-kurt/gofpdf"
)
// PDFService handles PDF generation
type PDFService struct{}
// NewPDFService creates a new PDF service
func NewPDFService() *PDFService {
return &PDFService{}
}
// GenerateTasksReportPDF generates a PDF report from task report data
func (s *PDFService) GenerateTasksReportPDF(report *TasksReportResponse) ([]byte, error) {
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.SetMargins(15, 15, 15)
pdf.AddPage()
// Translate UTF-8 strings to Windows-1252 for built-in fonts
tr := pdf.UnicodeTranslatorFromDescriptor("")
// Header
pdf.SetFont("Arial", "B", 20)
pdf.SetTextColor(51, 51, 51)
pdf.Cell(0, 12, "Tasks Report")
pdf.Ln(14)
// Residence name
pdf.SetFont("Arial", "", 14)
pdf.SetTextColor(102, 102, 102)
pdf.Cell(0, 8, tr(report.ResidenceName))
pdf.Ln(10)
// Generated date
pdf.SetFont("Arial", "", 10)
pdf.Cell(0, 6, fmt.Sprintf("Generated: %s", report.GeneratedAt.Format("January 2, 2006 at 3:04 PM")))
pdf.Ln(12)
// Summary section
pdf.SetFont("Arial", "B", 14)
pdf.SetTextColor(51, 51, 51)
pdf.Cell(0, 8, "Summary")
pdf.Ln(10)
// Summary box
pdf.SetFillColor(248, 249, 250)
pdf.Rect(15, pdf.GetY(), 180, 30, "F")
y := pdf.GetY() + 5
pdf.SetXY(20, y)
pdf.SetFont("Arial", "B", 12)
pdf.SetTextColor(51, 51, 51)
// Summary columns
colWidth := 45.0
pdf.Cell(colWidth, 6, "Total Tasks")
pdf.Cell(colWidth, 6, "Completed")
pdf.Cell(colWidth, 6, "Pending")
pdf.Cell(colWidth, 6, "Overdue")
pdf.Ln(8)
pdf.SetX(20)
pdf.SetFont("Arial", "", 16)
pdf.Cell(colWidth, 8, fmt.Sprintf("%d", report.TotalTasks))
pdf.SetTextColor(40, 167, 69) // Green
pdf.Cell(colWidth, 8, fmt.Sprintf("%d", report.Completed))
pdf.SetTextColor(255, 193, 7) // Yellow/Orange
pdf.Cell(colWidth, 8, fmt.Sprintf("%d", report.Pending))
pdf.SetTextColor(220, 53, 69) // Red
pdf.Cell(colWidth, 8, fmt.Sprintf("%d", report.Overdue))
pdf.Ln(25)
// Tasks table
pdf.SetTextColor(51, 51, 51)
pdf.SetFont("Arial", "B", 14)
pdf.Cell(0, 8, "Tasks")
pdf.Ln(10)
if len(report.Tasks) == 0 {
pdf.SetFont("Arial", "I", 11)
pdf.SetTextColor(128, 128, 128)
pdf.Cell(0, 8, "No tasks found for this residence.")
} else {
// Table header
pdf.SetFont("Arial", "B", 10)
pdf.SetFillColor(233, 236, 239)
pdf.SetTextColor(51, 51, 51)
pdf.CellFormat(70, 8, "Title", "1", 0, "L", true, 0, "")
pdf.CellFormat(30, 8, "Category", "1", 0, "C", true, 0, "")
pdf.CellFormat(25, 8, "Priority", "1", 0, "C", true, 0, "")
pdf.CellFormat(25, 8, "Status", "1", 0, "C", true, 0, "")
pdf.CellFormat(30, 8, "Due Date", "1", 0, "C", true, 0, "")
pdf.Ln(-1)
// Table rows
pdf.SetFont("Arial", "", 9)
for _, task := range report.Tasks {
// Check if we need a new page
if pdf.GetY() > 270 {
pdf.AddPage()
// Repeat header
pdf.SetFont("Arial", "B", 10)
pdf.SetFillColor(233, 236, 239)
pdf.CellFormat(70, 8, "Title", "1", 0, "L", true, 0, "")
pdf.CellFormat(30, 8, "Category", "1", 0, "C", true, 0, "")
pdf.CellFormat(25, 8, "Priority", "1", 0, "C", true, 0, "")
pdf.CellFormat(25, 8, "Status", "1", 0, "C", true, 0, "")
pdf.CellFormat(30, 8, "Due Date", "1", 0, "C", true, 0, "")
pdf.Ln(-1)
pdf.SetFont("Arial", "", 9)
}
// Determine row color based on status
if task.IsCancelled {
pdf.SetFillColor(248, 215, 218) // Light red
} else if task.IsCompleted {
pdf.SetFillColor(212, 237, 218) // Light green
} else if task.IsArchived {
pdf.SetFillColor(226, 227, 229) // Light gray
} else {
pdf.SetFillColor(255, 255, 255) // White
}
// Title (truncate if too long, use runes to avoid cutting multi-byte UTF-8 characters)
title := task.Title
titleRunes := []rune(title)
if len(titleRunes) > 35 {
title = string(titleRunes[:32]) + "..."
}
// Status text
var status string
if task.IsCancelled {
status = "Cancelled"
} else if task.IsCompleted {
status = "Completed"
} else if task.IsArchived {
status = "Archived"
} else {
status = task.Status
}
// Due date
dueDate := "-"
if task.DueDate != nil {
dueDate = task.DueDate.Format("Jan 2, 2006")
}
pdf.SetTextColor(51, 51, 51)
pdf.CellFormat(70, 7, tr(title), "1", 0, "L", true, 0, "")
pdf.CellFormat(30, 7, tr(task.Category), "1", 0, "C", true, 0, "")
pdf.CellFormat(25, 7, tr(task.Priority), "1", 0, "C", true, 0, "")
pdf.CellFormat(25, 7, tr(status), "1", 0, "C", true, 0, "")
pdf.CellFormat(30, 7, dueDate, "1", 0, "C", true, 0, "")
pdf.Ln(-1)
}
}
// Footer
pdf.SetY(-25)
pdf.SetFont("Arial", "I", 9)
pdf.SetTextColor(128, 128, 128)
pdf.Cell(0, 10, tr(fmt.Sprintf("honeyDue - Tasks Report for %s", report.ResidenceName)))
pdf.Ln(5)
pdf.Cell(0, 10, fmt.Sprintf("Generated on %s", time.Now().UTC().Format("2006-01-02 15:04:05 UTC")))
// Output to buffer
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return nil, fmt.Errorf("failed to generate PDF: %w", err)
}
return buf.Bytes(), nil
}