Skip to content

Commit 3214cf9

Browse files
committed
fix: compiled generic_report.jrxml manually using docker exec unlockedv2-server-1 sh -c cd /app/backend/src/templates && /opt/jasperstarter/bin/jasperstarter compile generic_report.jrxml
1 parent fb97c24 commit 3214cf9

File tree

7 files changed

+521
-144
lines changed

7 files changed

+521
-144
lines changed

backend/go.mod

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,7 @@ require (
2626
gorm.io/gorm v1.25.12
2727
)
2828

29-
require (
30-
github.com/go-pdf/fpdf v0.9.0
31-
github.com/xuri/excelize/v2 v2.9.0
32-
)
29+
require github.com/xuri/excelize/v2 v2.9.0
3330

3431
require (
3532
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect

backend/go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM
5757
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
5858
github.com/go-co-op/gocron/v2 v2.16.1 h1:ux/5zxVRveCaCuTtNI3DiOk581KC1KpJbpJFYUEVYwo=
5959
github.com/go-co-op/gocron/v2 v2.16.1/go.mod h1:opexeOFy5BplhsKdA7bzY9zeYih8I8/WNJ4arTIFPVc=
60-
github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw=
61-
github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
6260
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
6361
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
6462
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=

backend/src/handlers/reports_handler.go

Lines changed: 18 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package handlers
22

33
import (
4-
"UnlockEdv2/src"
4+
"UnlockEdv2/src/jasper"
55
"UnlockEdv2/src/models"
6-
"bytes"
76
"context"
87
"encoding/csv"
98
"encoding/json"
@@ -13,106 +12,15 @@ import (
1312
"strings"
1413
"time"
1514

16-
"github.com/go-pdf/fpdf"
1715
"github.com/sirupsen/logrus"
1816
"github.com/xuri/excelize/v2"
1917
)
2018

2119
const (
22-
pdfMaxWidth = 280.0
23-
pdfSampleSize = 100
2420
maxDateRangeDays = 90
2521
maxFacilitiesInList = 50
2622
)
2723

28-
func calculateOptimalColumnWidths(config *ColumnWidthConfig) []float64 {
29-
colWidths := make([]float64, len(config.Headers))
30-
copy(colWidths, config.MinWidths)
31-
32-
sampleSize := config.SampleSize
33-
if sampleSize > len(config.Data) {
34-
sampleSize = len(config.Data)
35-
}
36-
37-
for i := 0; i < sampleSize; i++ {
38-
for j, cell := range config.Data[i] {
39-
if j < len(colWidths) {
40-
contentWidth := config.PDF.GetStringWidth(cell) + 4
41-
if contentWidth > colWidths[j] {
42-
colWidths[j] = contentWidth
43-
}
44-
}
45-
}
46-
}
47-
48-
totalWidth := 0.0
49-
for _, width := range colWidths {
50-
totalWidth += width
51-
}
52-
53-
if totalWidth > config.MaxWidth {
54-
scale := config.MaxWidth / totalWidth
55-
for i := range colWidths {
56-
colWidths[i] *= scale
57-
}
58-
}
59-
60-
return colWidths
61-
}
62-
63-
func renderPDFTable(config *PDFTableConfig) {
64-
config.PDF.SetFont("Arial", "B", config.HeaderFontSize)
65-
for i, header := range config.Headers {
66-
config.PDF.CellFormat(config.ColumnWidths[i], 7, header, "1", 0, "C", false, 0, "")
67-
}
68-
config.PDF.Ln(-1)
69-
70-
config.PDF.SetFont("Arial", "", config.DataFontSize)
71-
for _, row := range config.Data {
72-
for i, cell := range row {
73-
if i < len(config.ColumnWidths) {
74-
alignment := "L"
75-
if i < len(config.Alignments) {
76-
alignment = config.Alignments[i]
77-
}
78-
config.PDF.CellFormat(config.ColumnWidths[i], 6, cell, "1", 0, alignment, false, 0, "")
79-
}
80-
}
81-
config.PDF.Ln(-1)
82-
}
83-
}
84-
85-
func renderPDFHeader(pdf *fpdf.Fpdf, title string, filterSummary []models.PDFFilterLine) {
86-
pdf.RegisterImageOptionsReader("logo", fpdf.ImageOptions{ImageType: "PNG"}, bytes.NewReader(src.UnlockedLogoImg))
87-
pdf.ImageOptions("logo", 10, 10, 25, 0, false, fpdf.ImageOptions{ImageType: "PNG"}, 0, "")
88-
89-
pdf.SetFont("Arial", "B", 16)
90-
pdf.SetXY(40, 12)
91-
pdf.Cell(0, 8, title+" Report")
92-
93-
pdf.SetFont("Arial", "", 9)
94-
pdf.SetXY(40, 21)
95-
pdf.Cell(0, 5, "Generated: "+time.Now().Format("January 2, 2006 at 3:04 PM"))
96-
97-
if len(filterSummary) > 0 {
98-
pdf.SetXY(10, 38)
99-
pdf.SetFont("Arial", "B", 10)
100-
pdf.Cell(0, 5, "Report Filters:")
101-
pdf.Ln(6)
102-
103-
pdf.SetFont("Arial", "", 9)
104-
for _, filter := range filterSummary {
105-
pdf.SetX(15)
106-
pdf.Cell(30, 5, filter.Label+":")
107-
pdf.Cell(0, 5, filter.Value)
108-
pdf.Ln(5)
109-
}
110-
pdf.Ln(5)
111-
} else {
112-
pdf.Ln(30)
113-
}
114-
}
115-
11624
func buildFilterSummary(req *models.ReportGenerateRequest, facilityName, residentName string) []models.PDFFilterLine {
11725
var filters []models.PDFFilterLine
11826

@@ -245,25 +153,6 @@ func (srv *Server) handleGenerateReport(w http.ResponseWriter, r *http.Request,
245153
}
246154
}
247155

248-
type PDFTableConfig struct {
249-
PDF *fpdf.Fpdf
250-
Headers []string
251-
ColumnWidths []float64
252-
Alignments []string
253-
Data [][]string
254-
HeaderFontSize float64
255-
DataFontSize float64
256-
}
257-
258-
type ColumnWidthConfig struct {
259-
PDF *fpdf.Fpdf
260-
Headers []string
261-
Data [][]string
262-
MinWidths []float64
263-
MaxWidth float64
264-
SampleSize int
265-
}
266-
267156
type reportExporter interface {
268157
ToCSV() ([][]string, error)
269158
ToExcel() (*excelize.File, error)
@@ -412,39 +301,30 @@ func (srv *Server) exportReport(w http.ResponseWriter, report reportExporter, fo
412301

413302
filterSummary := buildFilterSummary(req, facilityName, residentName)
414303

415-
pdf := fpdf.New("L", "mm", "A4", "")
416-
pdf.AddPage()
417-
418-
renderPDFHeader(pdf, config.Title, filterSummary)
419-
420-
pdf.SetFont("Arial", "", config.DataFontSize)
421-
422-
colWidths := calculateOptimalColumnWidths(&ColumnWidthConfig{
423-
PDF: pdf,
424-
Headers: config.Headers,
425-
Data: config.Data,
426-
MinWidths: config.MinWidths,
427-
MaxWidth: pdfMaxWidth,
428-
SampleSize: pdfSampleSize,
429-
})
304+
var pdfBytes []byte
305+
switch req.Type {
306+
case models.AttendanceReport:
307+
pdfBytes, err = jasper.GenerateAttendanceReportPDF(config, filterSummary)
308+
case models.ProgramOutcomesReport:
309+
pdfBytes, err = jasper.GenerateProgramOutcomesReportPDF(config, filterSummary)
310+
case models.FacilityComparisonReport:
311+
pdfBytes, err = jasper.GenerateFacilityComparisonReportPDF(config, filterSummary)
312+
default:
313+
return newBadRequestServiceError(errors.New("unsupported report type"), "unsupported report type for PDF generation")
314+
}
430315

431-
renderPDFTable(&PDFTableConfig{
432-
PDF: pdf,
433-
Headers: config.Headers,
434-
ColumnWidths: colWidths,
435-
Alignments: config.Alignments,
436-
Data: config.Data,
437-
HeaderFontSize: config.HeaderFontSize,
438-
DataFontSize: config.DataFontSize,
439-
})
316+
if err != nil {
317+
logrus.WithError(err).Error("Failed to generate PDF with Jasper")
318+
return newInternalServerServiceError(err, "failed to generate PDF")
319+
}
440320

441321
filename := fmt.Sprintf("%s-Report-%s.pdf", config.Title, time.Now().Format("2006-01-02"))
442322
w.Header().Set("Content-Type", "application/pdf")
443323
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
444324
w.WriteHeader(http.StatusOK)
445325

446-
if err := pdf.Output(w); err != nil {
447-
return newInternalServerServiceError(err, "failed to generate PDF")
326+
if _, err := w.Write(pdfBytes); err != nil {
327+
return newInternalServerServiceError(err, "failed to write PDF")
448328
}
449329
return nil
450330

backend/src/jasper/jasper_service.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os"
1111
"os/exec"
1212
"path/filepath"
13+
"strings"
1314
"time"
1415

1516
"github.com/evertonvps/go-jasper"
@@ -182,3 +183,119 @@ func GenerateUsageReportPDF(db *database.DB, queryCtx *models.QueryContext, hasP
182183
service := newJasperServiceWithContext(db, queryCtx, hasProgramAccess)
183184
return service.generateUsageReportPDF(userID)
184185
}
186+
187+
func generateReportPDF(config models.PDFConfig, filterSummary []models.PDFFilterLine, templateName string) ([]byte, error) {
188+
type ReportData struct {
189+
Rows [][]string `json:"rows"`
190+
}
191+
192+
rows := config.Data
193+
if len(rows) == 0 {
194+
rows = [][]string{}
195+
}
196+
197+
reportData := ReportData{
198+
Rows: rows,
199+
}
200+
201+
jsonData, err := json.Marshal(reportData)
202+
if err != nil {
203+
return nil, fmt.Errorf("failed to marshal report data: %w", err)
204+
}
205+
206+
title := config.Title
207+
if title == "" {
208+
title = "Report"
209+
}
210+
211+
filterSummaryText := ""
212+
if len(filterSummary) > 0 {
213+
var filterLines []string
214+
for _, filter := range filterSummary {
215+
filterLines = append(filterLines, fmt.Sprintf("%s: %s", filter.Label, filter.Value))
216+
}
217+
filterSummaryText = strings.Join(filterLines, "\n")
218+
}
219+
220+
params := []jasper.Parameter{
221+
{Key: "ReportTitle", Value: title},
222+
{Key: "GeneratedDate", Value: time.Now().Format("January 2, 2006 at 3:04 PM")},
223+
{Key: "LogoImage", Value: base64.StdEncoding.EncodeToString(src.UnlockedLogoImg)},
224+
{Key: "FilterSummary", Value: filterSummaryText},
225+
}
226+
227+
tempDir := os.Getenv("JASPER_TEMP_DIR")
228+
if tempDir == "" {
229+
tempDir = filepath.Join(os.TempDir(), "jasper-reports")
230+
if err := os.MkdirAll(tempDir, 0755); err != nil {
231+
return nil, fmt.Errorf("failed to create temp directory: %w", err)
232+
}
233+
logrus.WithField("temp_dir", tempDir).Info("JASPER_TEMP_DIR not set, using fallback directory")
234+
}
235+
236+
outputFileName := fmt.Sprintf("%s_%s", templateName, uuid.New().String())
237+
outputFilePath := filepath.Join(tempDir, outputFileName)
238+
jsonFileName := fmt.Sprintf("%s_%s.json", templateName, uuid.New().String())
239+
jsonFilePath := filepath.Join(tempDir, jsonFileName)
240+
241+
if err := os.WriteFile(jsonFilePath, jsonData, 0600); err != nil {
242+
return nil, fmt.Errorf("failed to write temporary data file: %w", err)
243+
}
244+
245+
defer func() {
246+
if err := os.Remove(jsonFilePath); err != nil {
247+
logrus.WithFields(logrus.Fields{
248+
"json_file": jsonFilePath,
249+
"error": err,
250+
}).Warn("Failed to remove temporary JSON file")
251+
}
252+
253+
if err := os.Remove(outputFilePath + ".pdf"); err != nil {
254+
logrus.WithFields(logrus.Fields{
255+
"pdf_file": outputFilePath + ".pdf",
256+
"error": err,
257+
}).Warn("Failed to remove temporary PDF file")
258+
}
259+
}()
260+
261+
gj := jasper.NewGoJasperJsonData(jsonFilePath, "", params, "pdf", outputFilePath)
262+
gj.Output = outputFilePath
263+
264+
if path, err := exec.LookPath("jasperstarter"); err == nil {
265+
gj.Executable = path
266+
} else {
267+
logrus.Info("jasperstarter not found in PATH, falling back to default path")
268+
gj.Executable = "/opt/jasperstarter/bin/jasperstarter"
269+
}
270+
271+
templateDir := os.Getenv("JASPER_TEMPLATE_DIR")
272+
if templateDir == "" {
273+
templateDir = "/templates"
274+
}
275+
compiledTemplatePath := filepath.Join(templateDir, templateName+".jasper")
276+
277+
pdfBytes, err := gj.Process(compiledTemplatePath)
278+
if err != nil {
279+
logrus.WithFields(logrus.Fields{
280+
"template_path": compiledTemplatePath,
281+
"template_dir": templateDir,
282+
"error": err,
283+
}).Error("Failed to process compiled template")
284+
285+
return nil, fmt.Errorf("failed to process template: %w", err)
286+
}
287+
288+
return pdfBytes, nil
289+
}
290+
291+
func GenerateAttendanceReportPDF(config models.PDFConfig, filterSummary []models.PDFFilterLine) ([]byte, error) {
292+
return generateReportPDF(config, filterSummary, "attendance_report")
293+
}
294+
295+
func GenerateProgramOutcomesReportPDF(config models.PDFConfig, filterSummary []models.PDFFilterLine) ([]byte, error) {
296+
return generateReportPDF(config, filterSummary, "program_outcomes_report")
297+
}
298+
299+
func GenerateFacilityComparisonReportPDF(config models.PDFConfig, filterSummary []models.PDFFilterLine) ([]byte, error) {
300+
return generateReportPDF(config, filterSummary, "facility_comparison_report")
301+
}
48.5 KB
Binary file not shown.

0 commit comments

Comments
 (0)