Skip to content

Ticket Dashboard Developer Guide

Ed Mozley edited this page Nov 7, 2026 · 1 revision

Ticket dashboard - Developer Guide

How Tickets β†’ Dashboard and its Widget Library work underneath: the pages, the shared widget editor, the endpoints and the two tables. For what an analyst sees, see Tickets β†’ Dashboard.

Read this first if you touch assets/js/widget-editor.js. One script drives every way of making or changing a widget - the cog on the dashboard, and New widget and Edit in the library. An error anywhere in it stops all three at once. That is what happened between 2.5.0 and 4.10.0 (#260).


The shape of it

There are two kinds of thing:

  • A widget - one chart definition: what to count, how to draw it, which dates and departments. Widgets live in one shared library for the whole install.
  • A dashboard - one analyst's choice of library widgets, in their own order.

So adding, removing and reordering are personal. Editing a widget's definition is not: it changes the library row, and every analyst who has that widget sees the change.

ticket_dashboard_widgets            (the library - one row per widget, shared)
        β–² widget_id
analyst_ticket_dashboard_widgets    (who has which widget, in what order)

Files

File What it does
tickets/dashboard/index.php The dashboard. It draws each card with Chart.js, handles drag and drop, the status dropdown on a card, and the cog that opens the edit modal
tickets/dashboard/library.php The Widget Library: table of every widget, New widget, Edit, Duplicate, Delete, Add to dashboard
tickets/dashboard/includes/widget_edit_form.php The edit form's markup. It is included by both pages, so the fields are written once
assets/js/widget-editor.js The form's behaviour, shared by both pages as window.WidgetEditor (below)
assets/js/chart.min.js Chart.js, loaded by the dashboard only
lang/en/tickets.php β†’ tickets.dashboard.* Every string, including the auto-written description's sentence parts (auto_desc.*)

Both pages load the editor as widget-editor.js?v=N. Bump N on both pages whenever the script changes, or browsers keep the old copy.


widget-editor.js - the shared editor

One IIFE that exposes window.WidgetEditor:

Function Called by What it does
init(apiBase) both pages, once on load Loads departments and wires the form's change handlers
populateForm(widget) the cog, library Edit Fills the form from a widget, then calls onPropertyChange()
resetForm() library New widget Clears the form, then calls onPropertyChange()
onPropertyChange() the two above, and the What to count dropdown Rebuilds the Series and Chart type options and shows or hides Time grouping to suit the property
collectFormData() / validateForm() saveWidget() Reads the form into the save payload; a time property must have a grouping
saveWidget() the modal's and the library's Save Posts to save_ticket_dashboard_widget.php, returns {success, widget} or {success:false, error}
generateDescription() as the form changes Writes the automatic description as one translated sentence, until the analyst types their own

The rules that decide what the form offers:

  • TIME_AGGREGATES = created, closed, created_vs_closed - these need a Time grouping (day, month, year).
  • SERIES_RULES - which breakdown each property allows. status cannot be broken down by status; first_time_fix, training_provided and created_vs_closed take no series.
  • getValidChartTypes() - with a series or a time property, bar or line; otherwise bar, doughnut or pie.

The server repeats these lists in save_ticket_dashboard_widget.php ($allowed_properties, $allowed_chart_types, $allowed_series, $allowed_date_ranges, $allowed_groupings). Adding a property, range or grouping means changing both, plus get_ticket_widget_data.php which has to know how to count it.

TRAP: the escape helper in this file is escapeHtml(), not esc(). Many FreeITSM scripts call esc(), so it is the name your fingers type. Here it does not exist, and onPropertyChange() - which every open-the-form path calls - threw esc is not defined and stopped the modal opening (#260). tests/escape-helpers-quotes.php now fails any script in assets/js/ that calls esc() without defining or importing it.


Opening the edit modal (the cog)

cog button  onclick="openWidgetEditModal(id)"         tickets/dashboard/index.php
  └─ WidgetEditor.populateForm(widget)                 assets/js/widget-editor.js
       └─ onPropertyChange()                           rebuilds Series / Chart type
  └─ #widgetEditModal.classList.add('active')          the modal appears

The modal is added after populateForm() returns. So any exception inside populateForm() or onPropertyChange() leaves the page looking as if the click did nothing. The only sign is in the browser console. When the cog "does nothing", open the console first.

On Save, handleWidgetSave() copies the saved fields onto the card's in-memory widget and redraws that one card. It does not reload the page.


Endpoints (api/tickets/)

Every one starts with requireModuleAccessJson('tickets'). There is no admin check: any analyst with Tickets access can create, edit or delete library widgets.

Endpoint Table touched Scope
get_ticket_dashboard.php reads both, WHERE analyst_id = ? AND is_active = 1 personal
add_ticket_dashboard_widget.php INSERT IGNORE into analyst_ticket_dashboard_widgets personal
remove_ticket_dashboard_widget.php DELETE ... WHERE analyst_id = ? AND widget_id = ? personal
reorder_ticket_dashboard_widgets.php UPDATE sort_order ... WHERE analyst_id = ? personal
get_ticket_widget_library.php reads ticket_dashboard_widgets WHERE is_active = 1 shared
save_ticket_dashboard_widget.php UPDATE or INSERT on ticket_dashboard_widgets shared - the cog edits everyone's copy
delete_ticket_dashboard_widget.php removes it from every dashboard, then is_active = 0 shared, soft delete
get_ticket_widget_data.php reads tickets the numbers for one card

get_ticket_widget_data.php - the numbers

It takes widget_id and an optional status, reads the widget's definition, then builds one WHERE that every chart path shares:

  • the date range (buildDateRangeWhere()), on the created or closed date as the property needs;
  • the department filter (buildDepartmentWhere(), the widget's JSON list of ids);
  • the status, if the widget allows it, otherwise its default_status;
  • the company scope - ticketTenantFilter() for the analyst's active company. It is a no-op with one company;
  • t.deleted_datetime IS NULL, so trashed tickets never count.

Then one of three paths: categorical (getCategoricalData()), categorical with a series (getCategoricalWithSeries()), or time-based, which fills in every day, month or year in the range with getTimeLabels() so empty periods draw as zero rather than vanishing. Time labels are formatted with DateFmt, so they follow the analyst's date format.


Tables

ticket_dashboard_widgets - the library. title (100), description (255), chart_type, aggregate_property, series_property, is_status_filterable, default_status, date_range, department_filter (JSON array of department ids), time_grouping, display_order, is_active (soft delete), is_demo. Seeded with the stock widgets in database/freeitsm.sql.

analyst_ticket_dashboard_widgets - one row per analyst per widget. UNIQUE (analyst_id, widget_id), so adding twice is harmless. sort_order, status_filter, is_demo. It is in the analyst lifecycle map as personal, so the rows go with the analyst.


Known gaps (not bugs, not built)

  • Editing is shared, not personal. The help says the cog lets you "customise" a widget, but saving changes the library row for everyone who uses it. To change a widget just for yourself, Duplicate it in the library and add the copy.
  • The status dropdown on a card is not remembered. It reloads that card's numbers, but nothing writes analyst_ticket_dashboard_widgets.status_filter, so after a refresh the card is back to All statuses. The column is read but never written.

Testing a change here

  1. php tests/escape-helpers-quotes.php - includes the esc() check.
  2. In a browser, with the console open: click a cog, then in the library click New widget and Edit. All three go through onPropertyChange(). Change What to count to a time property and back, and Save.
  3. Bump widget-editor.js?v= on both pages.

Related

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally