Building Custom HTML and Text Reports

Building Custom HTML and Text Reports

This page is derived from older reporting documentation and may not reflect the current behavior of the reporting module. Contributions to align it with the latest version are welcome.

Create custom HTML, XML, and text-based reports with complete control over layout and formatting.

Text templates give you ultimate flexibility to create custom report outputs in any text-based format. Using scripting languages like Groovy and Velocity, you can build everything from patient summaries and clinical reports to custom HTML dashboards and XML data feeds.

Table of Contents


When to Use Text Templates

Ideal Use Cases

📄 Patient Summaries

  • Discharge summaries with custom formatting

  • Clinical reports for referrals

  • Patient history summaries

  • Treatment plans and care coordination

🌐 Custom HTML Reports

  • Interactive web dashboards

  • Reports with custom styling and branding

  • Mobile-friendly layouts

  • Embedded charts and visualizations

🔗 System Integration

  • XML feeds for external systems

  • JSON outputs for APIs

  • Custom CSV formats with specific layouts

  • FHIR-compliant health information exchange

📊 Specialized Layouts

  • Multi-column layouts not possible in Excel

  • Complex nested data structures

  • Dynamic content based on data conditions

  • Reports requiring custom calculations

When NOT to Use Text Templates

Use standard formats instead when:

  • Simple tabular data export is sufficient

  • Users need Excel functionality (sorting, filtering)

  • No custom formatting requirements

  • Limited technical resources for template maintenance


Template Basics

Understanding Template Structure

Basic Anatomy

Static Content: Regular HTML/text that appears as-is Dynamic Content: <% scripting code %> Variable Output: $variableName or ${expression} Comments: <%-- This is a comment --%>

Simple Example

<!DOCTYPE html> <html> <head> <title>Patient Summary</title> </head> <body> <h1>Patient Summary Report</h1> <p>Generated on: <%= new Date() %></p> <p>Total Patients: $data.totalPatients</p> </body> </html>

Available Scripting Languages

Groovy (Recommended)

  • Syntax: <% groovy code %>

  • Output: $variable or ${expression}

  • Best for: Complex logic, data manipulation

  • Java-based: Full access to Java libraries

Velocity

  • Syntax: #set($var = value)

  • Output: $variable

  • Best for: Simple templating, familiar syntax

  • Lightweight: Faster for simple operations

Accessing Report Data

Main Data Object

// Get the complete report data reportData = context.reportData // Access specific data sets dataset = reportData.dataSets.get("myDataSet") // Get metadata about columns columns = dataset.metaData.columns

Data Set Types

Row-Per-Patient Data

dataset = reportData.dataSets.get("patientList") for (row in dataset) { patientId = row.getColumnValue("patientId") name = row.getColumnValue("patientName") age = row.getColumnValue("age") }

Indicator Data

dataset = reportData.dataSets.get("indicators") for (row in dataset) { indicator = row.getColumnValue("indicator") value = row.getColumnValue("value") }

Scripting with Groovy

Basic Groovy Syntax

Variables and Operations

<% // Variables patientCount = data.totalPatients percentMale = (data.malePatients / patientCount) * 100 // String manipulation facilityName = data.facilityName.toUpperCase() reportDate = new Date().format("dd/MMM/yyyy") // Conditionals status = (percentMale > 50) ? "Male Majority" : "Female Majority" %> <p>Facility: $facilityName</p> <p>Report Date: $reportDate</p> <p>Gender Distribution: $status</p>

Loops and Iteration

<% dataset = reportData.dataSets.get("patientList") %> <table> <tr><th>Name</th><th>Age</th><th>Gender</th></tr> <% for (row in dataset) { %> <tr> <td>$row.getColumnValue("patientName")</td> <td>$row.getColumnValue("age")</td> <td>$row.getColumnValue("gender")</td> </tr> <% } %> </table>

Working with Data

Accessing Patient Data

<% // Get patient list dataset patients = reportData.dataSets.get("patientDemographics") // Process each patient for (patient in patients) { // Basic demographics id = patient.getColumnValue("patientId") name = patient.getColumnValue("patientName") age = patient.getColumnValue("age") gender = patient.getColumnValue("gender") // Handle dates birthdate = patient.getColumnValue("birthdate") if (birthdate) { formattedBirthdate = util.format(birthdate, "dd/MMM/yyyy") } // Handle null values phone = patient.getColumnValue("phoneNumber") ?: "Not provided" %> <div class="patient"> <h3>$name (ID: $id)</h3> <p>Age: $age | Gender: $gender</p> <% if (birthdate) { %> <p>Birth Date: $formattedBirthdate</p> <% } %> <p>Phone: $phone</p> </div> <% } %>

Data Calculations

<% // Get indicator data indicators = reportData.dataSets.get("monthlyIndicators") // Calculate totals and percentages totalPatients = 0 malePatients = 0 for (row in indicators) { indicator = row.getColumnValue("indicator") value = row.getColumnValue("value") if (indicator == "Total Patients") { totalPatients = value } else if (indicator == "Male Patients") { malePatients = value } } // Calculate percentage if (totalPatients > 0) { malePercentage = Math.round((malePatients / totalPatients) * 100) femalePercentage = 100 - malePercentage } %> <div class="summary"> <h2>Gender Distribution</h2> <p>Total Patients: $totalPatients</p> <p>Male: $malePatients ($malePercentage%)</p> <p>Female: ${totalPatients - malePatients} ($femalePercentage%)</p> </div>

Utility Functions

Built-in Utilities

<% // Date formatting today = new Date() formattedDate = util.format(today, "dd/MMM/yyyy") // Number formatting percentage = 0.847 formattedPercentage = util.format(percentage, "0.0%") // String formatting patientCount = 1234 formattedCount = String.format("%,d", patientCount) // Conditional formatting status = util.format(completionRate > 0.8 ? "Good" : "Needs Improvement") %> <p>Report Date: $formattedDate</p> <p>Completion Rate: $formattedPercentage</p> <p>Patient Count: $formattedCount</p> <p>Status: $status</p>

Custom Utility Functions

<% // Define reusable functions def formatPatientName(firstName, lastName) { if (firstName && lastName) { return "$lastName, $firstName" } else if (firstName) { return firstName } else if (lastName) { return lastName } else { return "Unknown" } } def getAgeGroup(age) { if (age < 5) return "0-4" if (age < 15) return "5-14" if (age < 50) return "15-49" return "50+" } // Use functions patients = reportData.dataSets.get("patientList") for (patient in patients) { firstName = patient.getColumnValue("givenName") lastName = patient.getColumnValue("familyName") age = patient.getColumnValue("age") fullName = formatPatientName(firstName, lastName) ageGroup = getAgeGroup(age) %> <p>$fullName - Age Group: $ageGroup</p> <% } %>

Common Patterns

Patient Summary Template

Clinical Summary Layout

<!DOCTYPE html> <html> <head> <title>Patient Clinical Summary</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } .header { border-bottom: 2px solid #333; padding-bottom: 10px; } .section { margin: 20px 0; } .vital-signs { background: #f5f5f5; padding: 10px; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } th { background-color: #f2f2f2; } </style> </head> <body> <% // Get patient data patient = reportData.dataSets.get("patientSummary").iterator().next() encounters = reportData.dataSets.get("recentEncounters") vitals = reportData.dataSets.get("vitalSigns") %> <div class="header"> <h1>Clinical Summary</h1> <h2>${patient.getColumnValue("patientName")} (ID: ${patient.getColumnValue("patientId")})</h2> <p><strong>DOB:</strong> ${util.format(patient.getColumnValue("birthdate"), "dd/MMM/yyyy")} | <strong>Age:</strong> ${patient.getColumnValue("age")} | <strong>Gender:</strong> ${patient.getColumnValue("gender")}</p> </div> <div class="section"> <h3>Recent Vital Signs</h3> <div class="vital-signs"> <% for (vital in vitals) { %> <p><strong>${vital.getColumnValue("vitalType")}:</strong> ${vital.getColumnValue("value")} ${vital.getColumnValue("units")} (${util.format(vital.getColumnValue("dateRecorded"), "dd/MMM/yyyy")})</p> <% } %> </div> </div> <div class="section"> <h3>Recent Encounters</h3> <table> <tr> <th>Date</th> <th>Type</th> <th>Provider</th> <th>Diagnosis</th> </tr> <% for (encounter in encounters) { %> <tr> <td>${util.format(encounter.getColumnValue("encounterDate"), "dd/MMM/yyyy")}</td> <td>${encounter.getColumnValue("encounterType")}</td> <td>${encounter.getColumnValue("provider")}</td> <td>${encounter.getColumnValue("diagnosis") ?: "Not recorded"}</td> </tr> <% } %> </table> </div> </body> </html>

Dashboard Template

Program Monitoring Dashboard

<!DOCTYPE html> <html> <head> <title>HIV Program Dashboard</title> <style> body { font-family: Arial, sans-serif; margin: 0; background: #f4f4f4; } .container { max-width: 1200px; margin: 0 auto; padding: 20px; } .header { background: #2c3e50; color: white; padding: 20px; text-align: center; } .metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin: 20px 0; } .metric-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .metric-value { font-size: 2em; font-weight: bold; color: #2c3e50; } .metric-label { color: #7f8c8d; margin-top: 5px; } .status-good { color: #27ae60; } .status-warning { color: #f39c12; } .status-critical { color: #e74c3c; } .chart-container { background: white; padding: 20px; border-radius: 8px; margin: 20px 0; } </style> </head> <body> <% // Get data indicators = reportData.dataSets.get("hivIndicators") trends = reportData.dataSets.get("monthlyTrends") // Process indicators indicatorMap = [:] for (row in indicators) { indicatorMap[row.getColumnValue("indicator")] = row.getColumnValue("value") } // Calculate status def getStatus(actual, target) { if (actual >= target) return "status-good" if (actual >= target * 0.8) return "status-warning" return "status-critical" } %> <div class="header"> <h1>HIV Program Dashboard</h1> <p>Reporting Period: ${util.format(reportData.context.startDate, "MMM yyyy")}</p> <p>Facility: ${reportData.context.location}</p> </div> <div class="container"> <div class="metrics"> <% def metrics = [ [label: "Active on ART", key: "activeOnART", target: 450], [label: "New Enrollments", key: "newEnrollments", target: 50], [label: "Viral Load Suppression", key: "vlSuppression", target: 85], [label: "Retention Rate", key: "retentionRate", target: 90] ] for (metric in metrics) { def value = indicatorMap[metric.key] ?: 0 def statusClass = getStatus(value, metric.target) %> <div class="metric-card"> <div class="metric-value ${statusClass}">${value}</div> <div class="metric-label">${metric.label}</div> <div style="font-size: 0.9em; color: #95a5a6;">Target: ${metric.target}</div> </div> <% } %> </div> <div class="chart-container"> <h3>Monthly Trends</h3> <table style="width: 100%; border-collapse: collapse;"> <tr style="background: #ecf0f1;"> <th style="padding: 10px; border: 1px solid #ddd;">Month</th> <th style="padding: 10px; border: 1px solid #ddd;">New Enrollments</th> <th style="padding: 10px; border: 1px solid #ddd;">Active Patients</th> <th style="padding: 10px; border: 1px solid #ddd;">Retention %</th> </tr> <% for (trend in trends) { %> <tr> <td style="padding: 10px; border: 1px solid #ddd;">${util.format(trend.getColumnValue("month"), "MMM yyyy")}</td> <td style="padding: 10px; border: 1px solid #ddd;">${trend.getColumnValue("newEnrollments")}</td> <td style="padding: 10px; border: 1px solid #ddd;">${trend.getColumnValue("activePatients")}</td> <td style="padding: 10px; border: 1px solid #ddd;">${trend.getColumnValue("retentionRate")}%</td> </tr> <% } %> </table> </div> </div> </body> </html>

XML Data Export

FHIR-like Patient Export

<?xml version="1.0" encoding="UTF-8"?> <% patients = reportData.dataSets.get("patientExport") %> <PatientBundle> <metadata> <exportDate>${util.format(new Date(), "yyyy-MM-dd'T'HH:mm:ss'Z'")}</exportDate> <facility>${reportData.context.location}</facility> <totalPatients>${patients.size()}</totalPatients> </metadata> <patients> <% for (patient in patients) { %> <patient> <identifier>${patient.getColumnValue("patientId")}</identifier> <name> <given>${patient.getColumnValue("givenName")}</given> <family>${patient.getColumnValue("familyName")}</family> </name> <gender>${patient.getColumnValue("gender")}</gender> <birthDate>${util.format(patient.getColumnValue("birthdate"), "yyyy-MM-dd")}</birthDate> <% if (patient.getColumnValue("phoneNumber")) { %> <telecom> <system>phone</system> <value>${patient.getColumnValue("phoneNumber")}</value> </telecom> <% } %> <% if (patient.getColumnValue("address")) { %> <address> <text>${patient.getColumnValue("address")}</text> </address> <% } %> </patient> <% } %> </patients> </PatientBundle>

Advanced Techniques

Data Aggregation and Analysis

Grouping and Counting

<% // Group patients by age groups patients = reportData.dataSets.get("patientList") ageGroups = [:] for (patient in patients) { age = patient.getColumnValue("age") group = age < 18 ? "Pediatric" : age < 65 ? "Adult" : "Elderly" if (!ageGroups[group]) { ageGroups[group] = [] } ageGroups[group] << patient } %> <h2>Patients by Age Group</h2> <% ageGroups.each { group, patientList -> %> <h3>${group} (${patientList.size()} patients)</h3> <ul> <% patientList.each { patient -> %> <li>${patient.getColumnValue("patientName")} - Age ${patient.getColumnValue("age")}</li> <% } %> </ul> <% } %>

Statistical Calculations

<% // Calculate statistics observations = reportData.dataSets.get("labResults") values = [] for (obs in observations) { value = obs.getColumnValue("numericValue") if (value != null) { values << value } } if (values.size() > 0) { // Basic statistics total = values.sum() average = total / values.size() minimum = values.min() maximum = values.max() // Median calculation sortedValues = values.sort() median = sortedValues.size() % 2 == 0 ? (sortedValues[sortedValues.size()/2 - 1] + sortedValues[sortedValues.size()/2]) / 2 : sortedValues[sortedValues.size()/2] } %> <div class="statistics"> <h3>Lab Result Statistics</h3> <p>Count: ${values.size()}</p> <p>Average: ${String.format("%.2f", average)}</p> <p>Median: ${String.format("%.2f", median)}</p> <p>Range: ${minimum} - ${maximum}</p> </div>

Conditional Content

Dynamic Sections Based on Data

<% // Check data availability hivData = reportData.dataSets.get("hivProgram") tbData = reportData.dataSets.get("tbProgram") mchData = reportData.dataSets.get("mchProgram") %> <h1>Program Summary Report</h1> <% if (hivData && hivData.size() > 0) { %> <div class="program-section"> <h2>HIV Program</h2> <% for (row in hivData) { %> <p>${row.getColumnValue("indicator")}: ${row.getColumnValue("value")}</p> <% } %> </div> <% } else { %> <div class="no-data"> <p><em>No HIV program data available for this period.</em></p> </div> <% } %> <% if (tbData && tbData.size() > 0) { %> <div class="program-section"> <h2>TB Program</h2> <% for (row in tbData) { %> <p>${row.getColumnValue("indicator")}: ${row.getColumnValue("value")}</p> <% } %> </div> <% } %>

Alert and Warning Systems

<% // Define thresholds indicators = reportData.dataSets.get("qualityIndicators") alerts = [] for (indicator in indicators) { name = indicator.getColumnValue("indicator") value = indicator.getColumnValue("value") target = indicator.getColumnValue("target") if (value < target * 0.7) { alerts << [level: "critical", message: "$name is critically low: $value (target: $target)"] } else if (value < target * 0.9) { alerts << [level: "warning", message: "$name is below target: $value (target: $target)"] } } %> <% if (alerts.size() > 0) { %> <div class="alerts"> <h2>⚠️ Alerts and Warnings</h2> <% alerts.each { alert -> %> <div class="alert-${alert.level}"> <p><strong>${alert.level.toUpperCase()}:</strong> ${alert.message}</p> </div> <% } %> </div> <% } else { %> <div class="status-good"> <p>✅ All indicators are meeting targets.</p> </div> <% } %>

Performance Optimization

Efficient Data Processing

<% // Cache frequently used data patients = reportData.dataSets.get("patientList") patientMap = [:] // Build lookup map for efficiency for (patient in patients) { patientId = patient.getColumnValue("patientId") patientMap[patientId] = patient } // Process encounters efficiently encounters = reportData.dataSets.get("encounters") %> <h2>Patient Encounter Summary</h2> <% def processedPatients = new HashSet() for (encounter in encounters) { patientId = encounter.getColumnValue("patientId") // Only process each patient once if (!processedPatients.contains(patientId)) { patient = patientMap[patientId] if (patient) { %> <div class="patient-summary"> <h3>${patient.getColumnValue("patientName")}</h3> <p>Latest Encounter: ${util.format(encounter.getColumnValue("encounterDate"), "dd/MMM/yyyy")}</p> </div> <% processedPatients.add(patientId) } } } %>

Examples Library

Example 1: Medication List Report

<!DOCTYPE html> <html> <head> <title>Patient Medication List</title> <style> .medication { border-left: 4px solid #3498db; padding: 10px; margin: 10px 0; background: #f8f9fa; } .active { border-left-color: #27ae60; } .discontinued { border-left-color: #e74c3c; } </style> </head> <body> <% patient = reportData.dataSets.get("patientInfo").iterator().next() medications = reportData.dataSets.get("medications") %> <h1>Medication List</h1> <h2>${patient.getColumnValue("patientName")} (ID: ${patient.getColumnValue("patientId")})</h2> <% def activeMeds = medications.findAll { it.getColumnValue("status") == "ACTIVE" } def discontinuedMeds = medications.findAll { it.getColumnValue("status") != "ACTIVE" } %> <h3>Active Medications (${activeMeds.size()})</h3> <% for (med in activeMeds) { %> <div class="medication active"> <strong>${med.getColumnValue("drugName")}</strong> - ${med.getColumnValue("dose")} <br><small>Started: ${util.format(med.getColumnValue("startDate"), "dd/MMM/yyyy")}</small> <% if (med.getColumnValue("instructions")) { %> <br><em>Instructions: ${med.getColumnValue("instructions")}</em> <% } %> </div> <% } %> <% if (discontinuedMeds.size() > 0) { %> <h3>Previous Medications (${discontinuedMeds.size()})</h3> <% for (med in discontinuedMeds) { %> <div class="medication discontinued"> <strong>${med.getColumnValue("drugName")}</strong> - ${med.getColumnValue("dose")} <br><small> ${util.format(med.getColumnValue("startDate"), "dd/MMM/yyyy")} - ${util.format(med.getColumnValue("endDate"), "dd/MMM/yyyy")} </small> <br><span style="color: #e74c3c;">Reason: ${med.getColumnValue("discontinueReason")}</span> </div> <% } %> <% } %> </body> </html>

Example 2: Quality Metrics Dashboard

<!DOCTYPE html> <html> <head> <title>Quality Metrics Dashboard</title> <style> .dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; } .metric-box { border: 1px solid #ddd; padding: 15px; border-radius: 5px; } .metric-title { font-weight: bold; margin-bottom: 10px; } .metric-value { font-size: 1.5em; margin: 10px 0; } .progress-bar { width: 100%; height: 20px; background: #ecf0f1; border-radius: 10px; overflow: hidden; } .progress-fill { height: 100%; transition: width 0.3s ease; } .excellent { background: #27ae60; } .good { background: #2ecc71; } .fair { background: #f39c12; } .poor { background: #e74c3c; } </style> </head> <body> <% metrics = reportData.dataSets.get("qualityMetrics") def getGradeClass(percentage) { if (percentage >= 95) return "excellent" if (percentage >= 85) return "good" if (percentage >= 70) return "fair" return "poor" } def getGradeText(percentage) { if (percentage >= 95) return "Excellent" if (percentage >= 85) return "Good" if (percentage >= 70) return "Fair" return "Needs Improvement" } %> <h1>Quality Metrics Dashboard</h1> <p>Reporting Period: ${util.format(reportData.context.startDate, "MMM yyyy")}</p> <div class="dashboard"> <% for (metric in metrics) { def name = metric.getColumnValue("metricName") def numerator = metric.getColumnValue("numerator") def denominator = metric.getColumnValue("denominator") def percentage = denominator > 0 ? (numerator / denominator * 100) : 0 def gradeClass = getGradeClass(percentage) def gradeText = getGradeText(percentage) %> <div class="metric-box"> <div class="metric-title">${name}</div> <div class="metric-value ${gradeClass}"> ${String.format("%.1f", percentage)}% </div> <div class="progress-bar"> <div class="progress-fill ${gradeClass}" style="width: ${percentage}%;"></div> </div> <div style="margin-top: 10px;"> <small>${numerator} of ${denominator} patients</small> <br><strong>${gradeText}</strong> </div> </div> <% } %> </div> </body> </html>

Testing and Deployment

Testing Strategy

1. Template Validation

// Test basic syntax <% try { testData = reportData.dataSets.get("testDataSet") if (testData) { out.println("✅ Data access working") } else { out.println("❌ No data found") } } catch (Exception e) { out.println("❌ Error: " + e.getMessage()) } %>

2. Data Edge Cases

  • Empty datasets - Template should handle gracefully

  • Null values - Use safe navigation and defaults

  • Large datasets - Test performance with realistic data volumes

  • Missing columns - Handle missing data gracefully

3. Output Validation

  • HTML validation - Check for valid HTML structure

  • Cross-browser testing - Verify rendering in different browsers

  • Mobile responsiveness - Test on mobile devices

  • Print formatting - Verify print layouts work correctly

Deployment Process

1. Template Upload

  1. Navigate: Administration → Report Designs

  2. Add: Select "HTML/text-based template"

  3. Configure:

    • Name: User-friendly display name

    • Report Definition: Link to your report

    • Template Content: Paste your template code directly

    • Description: Brief explanation of output format

2. Template Configuration

Template Engine: Groovy (recommended) or Velocity Content Type: text/html, text/xml, text/plain, etc. Character Encoding: UTF-8 (recommended)

3. Permission Setup

  • Verify user roles can access the template

  • Test with different user accounts

  • Confirm template appears in output format list

4. Documentation

Create user guide:

  • What the template produces

  • When to use this format vs others

  • How to interpret the output

  • Troubleshooting common issues

Version Control

Template Management

<%-- Template Version: 2.1 Last Updated: January 2024 Changes: Added medication allergy section Author: Data Team --%>

Change Tracking

  • Comment all changes in template code

  • Test thoroughly before deployment

  • Backup previous versions before updates

  • Communicate changes to end users


Troubleshooting

Common Issues and Solutions

🚫 Template Syntax Errors

Symptoms: Template fails to render, shows error messages

Common Causes:

  • Unclosed script tags <% %>

  • Missing semicolons in Groovy code

  • Incorrect variable references

  • Malformed HTML structure

Solutions:

  1. Check syntax - Validate Groovy/HTML syntax

  2. Test incrementally - Start simple, add complexity gradually

  3. Use comments - Comment out sections to isolate issues

  4. Check logs - Review OpenMRS error logs for details

Example Fix:

// ❌ Incorrect - missing closing tag <% for (patient in patients) { out.println(patient.name) // ✅ Correct - proper closing <% for (patient in patients) { %> <p>${patient.getColumnValue("name")}</p> <% } %>

📊 Data Access Problems

Symptoms: Variables show as null, empty output sections

Common Causes:

  • Wrong data set names

  • Incorrect column names

  • Missing data in report

  • Case sensitivity issues

Solutions:

  1. Verify data set names - Check exact spelling and case

  2. Debug data access - Add debug output to check data

  3. Handle null values - Use safe navigation and defaults

  4. Check report configuration - Ensure data sets are properly linked

Debug Template:

<% // Debug: List all available data sets out.println("<h3>Available Data Sets:</h3>") reportData.dataSets.keySet().each { key -> out.println("<p>Dataset: $key</p>") dataset = reportData.dataSets.get(key) if (dataset) { out.println("<ul>") dataset.metaData.columns.each { column -> out.println("<li>Column: ${column.label}</li>") } out.println("</ul>") } } %>

🎨 Formatting and Display Issues

Symptoms: Poor layout, missing styles, incorrect formatting

Common Causes:

  • CSS conflicts

  • HTML structure problems

  • Browser compatibility issues

  • Missing character encoding

Solutions:

  1. Include complete HTML structure - DOCTYPE, head, body tags

  2. Embed CSS styles - Use internal stylesheets for reliability

  3. Test across browsers - Check in multiple browsers

  4. Set character encoding - Use UTF-8 encoding

Template Structure Best Practices:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Report Title</title> <style> /* Embed all CSS here for reliability */ body { font-family: Arial, sans-serif; margin: 20px; } </style> </head> <body> <!-- Template content here --> </body> </html>

Performance Issues

Symptoms: Template takes too long to render, timeouts

Common Causes:

  • Inefficient loops and data processing

  • Large datasets without pagination

  • Complex calculations in templates

  • Memory-intensive operations

Solutions:

  1. Optimize loops - Minimize nested iterations

  2. Cache calculations - Store computed values

  3. Limit data processing - Use report-level filtering

  4. Consider pagination - Break large lists into sections

Performance Optimization Example:

<% // ❌ Inefficient - multiple data set lookups for (patient in patients) { encounters = reportData.dataSets.get("encounters") patientEncounters = encounters.findAll { it.getColumnValue("patientId") == patient.getColumnValue("patientId") } // Process encounters... } // ✅ Efficient - single lookup with grouping encounters = reportData.dataSets.get("encounters") encountersByPatient = encounters.groupBy { it.getColumnValue("patientId") } for (patient in patients) { patientId = patient.getColumnValue("patientId") patientEncounters = encountersByPatient[patientId] ?: [] // Process encounters... } %>

🔤 Character Encoding Problems

Symptoms: Special characters display incorrectly, corrupted text

Common Causes:

  • Missing UTF-8 encoding

  • Database encoding issues

  • Browser interpretation problems

Solutions:

  1. Set template encoding - Use UTF-8 consistently

  2. Include meta charset - Add to HTML head section

  3. Test with special characters - Verify international characters work

  4. Check database settings - Ensure UTF-8 storage

Advanced Debugging

Template Debugging Techniques

Add Debug Output:

<% // Enable debug mode debugMode = true if (debugMode) { out.println("<div style='background: #f0f0f0; padding: 10px; margin: 10px 0; border: 1px solid #ccc;'>") out.println("<h4>Debug Information</h4>") // Show available data out.println("<p>Report Data Sets: ${reportData.dataSets.keySet()}</p>") out.println("<p>Report Context: ${reportData.context}</p>") // Show first few rows of data patients = reportData.dataSets.get("patientList") if (patients) { out.println("<p>Sample patient data:</p>") count = 0 for (patient in patients) { if (count++ >= 3) break out.println("<p>Patient: ${patient.getColumnValue("patientId")} - ${patient.getColumnValue("patientName")}</p>") } } out.println("</div>") } %>

Error Handling Wrapper:

<% def safeOutput(closure) { try { closure() } catch (Exception e) { out.println("<div style='color: red; background: #ffe6e6; padding: 10px; margin: 5px;'>") out.println("<strong>Error:</strong> ${e.getMessage()}") out.println("</div>") } } // Use wrapper for risky operations safeOutput { // Your template code here patients = reportData.dataSets.get("patientList") for (patient in patients) { out.println("<p>${patient.getColumnValue("patientName")}</p>") } } %>

Quick Reference

Essential Groovy Patterns

Data Access

// Get dataset dataset = reportData.dataSets.get("datasetName") // Iterate through rows for (row in dataset) { value = row.getColumnValue("columnName") } // Safe value access with default value = row.getColumnValue("columnName") ?: "Default Value" // Check for null/empty if (value && value.trim()) { // Process non-empty value }

Date Formatting

// Format date formattedDate = util.format(dateValue, "dd/MMM/yyyy") // Current date today = new Date() todayFormatted = util.format(today, "dd/MM/yyyy HH:mm") // Date calculations lastMonth = new Date() - 30

String Operations

// String formatting formatted = String.format("%.2f%%", percentage * 100) // String manipulation upperCase = text.toUpperCase() trimmed = text.trim() replaced = text.replace("old", "new") // Null-safe operations safeText = text?.trim() ?: "N/A"

Collections and Grouping

// Filter data filteredData = dataset.findAll { it.getColumnValue("status") == "Active" } // Group by field grouped = dataset.groupBy { it.getColumnValue("gender") } // Count occurrences counts = dataset.countBy { it.getColumnValue("ageGroup") } // Sort data sorted = dataset.sort { it.getColumnValue("name") }

HTML Template Patterns

Basic Structure

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Report Title</title> <style>/* CSS here */</style> </head> <body> <!-- Template content --> </body> </html>

Common CSS Classes

.header { background: #333; color: white; padding: 20px; } .metric { border: 1px solid #ddd; padding: 15px; margin: 10px; } .status-good { color: #27ae60; } .status-warning { color: #f39c12; } .status-critical { color: #e74c3c; } .table { width: 100%; border-collapse: collapse; } .table th, .table td { border: 1px solid #ddd; padding: 8px; }

Responsive Design

@media (max-width: 768px) { .dashboard { grid-template-columns: 1fr; } .metric { margin: 5px 0; } table { font-size: 0.9em; } }

Ready for more advanced topics?

  • 📈 Excel Template Design - Create professional Excel reports

  • 🔧 Report Design Manager - Manage multiple output formats

  • ⚙️ Custom Renderers - Build your own output processors