Calendar View Report with FullCalendar.js
Query date-range records from a Fulcrum app and render them as an interactive monthly calendar using the FullCalendar.js library, with color-coded event types and multi-view navigation.
This example shows how to build an HTML Report Builder template that renders Fulcrum records as calendar events using FullCalendar v3. Records with date_from / date_to fields are queried server-side, mapped to FullCalendar event objects, and rendered as a color-coded monthly calendar with week and day views.
Use Cases
- Attendance, leave, and time-off tracking
- Scheduled work orders or inspections across a date range
- Any app where records have start/end dates that benefit from a calendar view
Dependencies
FullCalendar v3 requires jQuery and Moment.js. Load all three from CDN:
<!-- FullCalendar CSS -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.10.2/fullcalendar.min.css" rel="stylesheet" />
<!-- jQuery (required by FullCalendar v3) -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Moment.js (required by FullCalendar v3) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
<!-- FullCalendar JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.10.2/fullcalendar.min.js"></script>EJS Report Template
<%
/**
* Calendar View Report
*
* Queries records from a Fulcrum app and displays them as calendar events.
* Each record must have at minimum:
* - a date_from field (event start)
* - a type or category field (used for color coding)
* - a label field (shown as the event title)
*
* Replace "YOUR_APP_TABLE_NAME" with your app's data_name or form_id.
* Adjust the SELECT columns and field references to match your schema.
*/
const rows = QUERY(`
SELECT
label_field,
type_field,
comment_field,
date_from,
date_to
FROM "YOUR_APP_TABLE_NAME"
`).rows;
%>
<link href="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.10.2/fullcalendar.min.css" rel="stylesheet" />
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.10.2/fullcalendar.min.js"></script>
<div id="calendar"></div>
<script>
$(function () {
// Inject the server-side query results as a JSON array
const records = <%- JSON.stringify(rows) %>;
// Map event type strings to display colors.
// Customize these to match your app's choice field values.
function getColorByType(type) {
const colors = {
'Excused Absence': 'dodgerblue',
'Unexcused Absence': 'orangered',
'Late': 'gold',
'Scheduled Time Off': 'limegreen',
'No Call/No Show': 'crimson',
};
return colors[type] || 'gray'; // Default for unrecognized types
}
// Convert Fulcrum records to FullCalendar event objects
const events = records.map(record => {
const startDate = moment(record.date_from).format('YYYY-MM-DDTHH:mm:ss');
const endDate = record.date_to
? moment(record.date_to).format('YYYY-MM-DDTHH:mm:ss')
: null;
return {
title: record.label_field,
start: startDate,
end: endDate,
overlap: true,
backgroundColor: getColorByType(record.type_field),
description: record.comment_field || '',
allDay: !record.date_from.includes('T'), // No time = all-day
extendedProps: {
type: record.type_field,
},
};
});
// Initialize the FullCalendar widget
$('#calendar').fullCalendar({
defaultView: 'month',
defaultDate: new Date().toISOString().slice(0, 10),
header: {
left: 'prev,next today',
center: 'title',
right: 'month,agendaWeek,agendaDay',
},
events: events,
// Optionally append a tooltip description beneath each event title
eventRender: function (event, element) {
if (event.description) {
// Insert the description as text (not HTML) because it comes from
// record data and could otherwise inject markup into the report
element.append(
$('<div class="fc-description"></div>')
.css({ 'font-size': '0.75em', opacity: 0.85 })
.text(event.description)
);
}
},
});
});
</script>
<style>
html, body {
margin: 0;
padding: 0;
font-family: 'Lucida Grande', Helvetica, Arial, Verdana, sans-serif;
font-size: 14px;
width: 100% !important;
}
#calendar {
max-width: 900px;
margin: 40px auto;
}
</style>How It Works
The QUERY() call runs server-side during EJS rendering and returns an array of row objects. That array is serialized with JSON.stringify() and injected directly into the <script> block as records, so FullCalendar initializes with real data the moment the page loads — no client-side API calls needed.
Each row is mapped to a FullCalendar event object. The allDay flag is set automatically: if date_from contains a time component, the event is timed; otherwise it spans the full day. The getColorByType() function maps choice-field values to colors — adjust the keys to match your app's vocabulary.
FullCalendar renders a standard month grid with prev/next navigation and view toggles (Month / Week / Day).
Customization
Add a legend: Render a color swatch key below the calendar using the same getColorByType map.
Filter by date range: Add a WHERE date_from >= '...' AND date_from <= '...' clause to the QUERY, or use $params.query to accept date range filters via URL parameter (see Dynamic Reports with EJS $params).
FullCalendar v5+: The v3 API shown here uses jQuery. FullCalendar v5+ has a native JavaScript API without jQuery. The event object shape is the same; initialization changes to:
const calendar = new FullCalendar.Calendar(document.getElementById('calendar'), { ... });
calendar.render();Updated about 9 hours ago