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:
$variableor${expression}Best for: Complex logic, data manipulation
Java-based: Full access to Java libraries
Velocity
Syntax:
#set($var = value)Output:
$variableBest 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.columnsData 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
Navigate: Administration → Report Designs
Add: Select "HTML/text-based template"
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:
Check syntax - Validate Groovy/HTML syntax
Test incrementally - Start simple, add complexity gradually
Use comments - Comment out sections to isolate issues
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:
Verify data set names - Check exact spelling and case
Debug data access - Add debug output to check data
Handle null values - Use safe navigation and defaults
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:
Include complete HTML structure - DOCTYPE, head, body tags
Embed CSS styles - Use internal stylesheets for reliability
Test across browsers - Check in multiple browsers
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:
Optimize loops - Minimize nested iterations
Cache calculations - Store computed values
Limit data processing - Use report-level filtering
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:
Set template encoding - Use UTF-8 consistently
Include meta charset - Add to HTML head section
Test with special characters - Verify international characters work
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() - 30String 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