Skip to content

Tickets Settings Developer Guide

Ed Mozley edited this page Oct 27, 2026 · 3 revisions

Tickets settings - Developer Guide

A map of tickets/settings/index.php (Tickets -> Settings): how the page is put together, which tab does what, which endpoints each tab calls and which wiki page documents it. Read this before changing the file; read the tab's own page before changing a tab.

In plain words

Tickets -> Settings is one PHP file of about 9,450 lines. It holds 26 tabs, from "a list of words" (ticket types) to "the credentials of the company's mailbox" (mailboxes). Each tab has its own permission, so the page is built from the module's settings manifest: an analyst only gets the tabs they hold, and a tab they lack is never written into the page at all.

The file has four parts, top to bottom:

  1. PHP set-up - login and module checks, the manifest, and a few values worked out on the server so a tab opens on what is really in force (checklist mode, mandatory fields, row display).
  2. The tab bar and the panels - one <div class="tab-content"> per tab, each wrapped in settingsTabVisible().
  3. The modals - written once, after all the panels, outside any tab check.
  4. One big <script> - the loaders, renderers and save handlers for every tab.

How manifests, capabilities and setting keys work in general is in Roles - Developer Guide. This page only says how Tickets uses them.

The tabs

In manifest order, which is the order the tab bar draws them. The panels in index.php are not in the same order (Indexing, for example, sits before Merge behaviour in the markup) - the bar follows the manifest, not the file.

All capabilities are Cap::TICKETS_* cases. Endpoints are under api/tickets/ unless a folder is given.

Tab id What it configures Capability Endpoints Documented in
departments Departments, their default confidentiality, and which teams see them TICKETS_DEPARTMENTS get_departments, save_department, delete_department, get_teams, save_department_teams Admin Access Control, Confidential tickets
ticket-types Ticket types, and hiding a type per company TICKETS_TICKET_TYPES get_ticket_types, save_ticket_type, delete_ticket_type, set_ticket_type_hidden Multi-Tenancy - Developer Guide
ticket-origins Ticket origins, and hiding an origin per company TICKETS_TICKET_ORIGINS get_ticket_origins, save_ticket_origin, delete_ticket_origin, set_ticket_origin_hidden Multi-Tenancy - Developer Guide
categories Categories, sub-categories, resolution codes, and whether each field shows on a ticket TICKETS_CATEGORIES get_ticket_classification, save_ticket_classification_settings, save_ticket_category, delete_ticket_category, save_ticket_resolution_code, delete_ticket_resolution_code Ticket categories - Developer Guide
checklists Whether outstanding mandatory SOP steps block closing TICKETS_CHECKLISTS save_checklist_settings Checklist closure gating - Developer Guide
mandatory-fields Fields that must be filled before closing, and who is emailed TICKETS_MANDATORY_FIELDS (sensitive) save_mandatory_fields_settings Mandatory fields - Developer Guide
custom-fields Custom fields per ticket type, and where the portal shows the answers TICKETS_CUSTOM_FIELDS get_ticket_fields_settings, get_ticket_types, save_ticket_field, delete_ticket_field, reorder_ticket_fields; api/settings/ for ticket_portal_field_answers Custom ticket fields - Developer Guide
statuses Ticket statuses (closed, pauses SLA, default) TICKETS_STATUSES get_ticket_statuses, save_ticket_status, delete_ticket_status Configurable lookups
priorities Ticket priorities TICKETS_PRIORITIES get_ticket_priorities, save_ticket_priority, delete_ticket_priority Configurable lookups
sla SLA targets per priority, calendars and holidays, breach notification rules, cron activity TICKETS_SLA get_sla_settings, save_sla_global_settings, save_priority_sla, list_timezones, get_sla_calendar, save_sla_calendar, delete_sla_calendar, get_sla_notification_rules, save_sla_notification_rule, delete_sla_notification_rule, get_sla_cron_runs SLA Management
rota-locations Rota locations TICKETS_ROTA_LOCATIONS get_rota_locations, save_rota_location, delete_rota_location Rota copy and paste
mailboxes Ticket mailboxes, sign-in, allow list, folder checks, activity log, oversized-email defaults TICKETS_MAILBOXES (sensitive) get_mailboxes, save_mailbox, delete_mailbox, mailbox_oauth_start, mailbox_logout, check_mailbox_email, verify_mailbox_folder, get_mailbox_whitelist, save_mailbox_whitelist, set_mailbox_health_dismissed, get_mailbox_activity, get_mailbox_outbound, oversize_settings; api/system/get_tenants Mailbox authentication, Mailbox providers, Folder verification, Attachment size limits, Email send log
messaging Messaging channels (WhatsApp through Twilio or Meta, Telegram, Teams, Mattermost) and message templates TICKETS_MESSAGING (sensitive) api/messaging/: get_channels, save_channel, delete_channel, test_channel, save_base_url, teams_package, telegram_connect, get_templates?manage=1, save_template, delete_template WhatsApp, Telegram, Teams and Mattermost
webchat Website chat widgets TICKETS_WEBCHAT api/webchat/: get_widgets, save_widget, delete_widget Web Chat - Developer Guide
email-templates Requester email templates, sender rules, the simulator, the public web address TICKETS_EMAIL_TEMPLATES get_email_templates, save_email_template, delete_email_template, simulate_email_template, get_public_base_url, save_public_base_url, set_base_url_warning_dismissed Email template sender rules, Public web address
reply-templates The team's shared reply templates TICKETS_REPLY_TEMPLATES get_reply_templates, save_reply_template, delete_reply_template Canned Responses - Developer Guide
time-tracking Time recording on or off, per company, screen and API separately TICKETS_MANAGE get_time_tracking_settings, save_time_tracking_settings Time tracking - Developer Guide
merge-behaviour What a merge does to references and conversations TICKETS_MERGE api/settings/ (merge_* keys) Merging tickets - Developer Guide
indexing The two switches that drain the attachment text queue TICKETS_INDEXING api/settings/ (attachment_extract_* keys) Attachment text extraction
rota Rota shifts, and whether the rota includes weekends TICKETS_ROTA get_rota_shifts, save_rota_shift, delete_rota_shift; api/settings/ for rota_include_weekends Rota copy and paste
numbering How ticket numbers are made, and renumbering TICKETS_NUMBERING (sensitive) numbering_preview, numbering_renumber; api/settings/ (ticket_number_* keys) Ticket numbering - Developer Guide
general Reopen on reply, snooze wake hour, the ticket workspace, who can export the ticket list, long-message display, long tickets, team folders, the AI reading aids TICKETS_GENERAL api/settings/ (keys below) Reading long tickets, Ticket AI reading, Snoozing tickets, Confidential tickets, Ticket workspace, Ticket table
privacy What a requester sees of their own ticket in the portal TICKETS_PRIVACY api/settings/ (portal_third_party_visibility) Portal privacy
reply-cleanup The reply-cleanup AI provider and key, plus tone and custom instructions TICKETS_REPLY_CLEANUP (sensitive) the shared AI panel (api/system/ai/), get_reply_cleanup_settings, save_reply_cleanup_settings AI providers
csat The customer satisfaction survey TICKETS_CSAT get_csat_settings, save_csat_settings CSAT company scope
row-display What your own ticket rows show none ('cap' => null) save_inbox_display -

api/settings/ means get_system_settings.php to read and save_system_settings.php to write.

How the page is assembled

require_once '../../includes/settings_manifest.php';
requireModuleAccess('tickets');

$settingsManifest = settingsManifestFor('tickets');
$conn             = connectToDatabase();
$visibleTabs      = settingsVisibleTabs($conn, (int) $_SESSION['analyst_id'], $settingsManifest);
$activeTabId      = settingsFirstTabId($visibleTabs);
  • settingsManifestFor('tickets') (includes/capabilities.php) finds tickets/settings/manifest.php among the manifests settingsManifests() globs.
  • settingsVisibleTabs() keeps a tab when its cap is null, when the analyst holds it, or when they hold one of its also_visible_to capabilities. Tickets uses no also_visible_to. A manifest stores a capability as a string - capKey(Cap::TICKETS_SLA) - and settingsVisibleTabs() turns it back with Cap::from() before calling analystHasCapability().
  • renderSettingsTabBar($visibleTabs, $activeTabId) draws the bar. Each button carries data-tab, data-capability (none for a preference tab) and onclick="switchTab('<id>')".
  • Each panel is wrapped like this:
<?php if (settingsTabVisible($visibleTabs, 'sla')): ?>
<div class="tab-content<?php echo $activeTabId === 'sla' ? ' active' : ''; ?>" id="sla-tab"
     data-capability="<?php echo capKey(Cap::TICKETS_SLA); ?>">

A panel's id is always <tab id>-tab. The row-display panel has no data-capability attribute.

Hiding a tab by capability

A tab is hidden by not holding its capability - there is nothing else to switch. To give a role one tab, tick that tab's capability in System -> Roles; TICKETS_MANAGE is the umbrella that holds all of them (and is itself the capability of the Time tracking tab). An administrator holds everything.

Inside a tab, a control can be gated on its own. The Row display tab is everyone's, but its "make this the default for everyone" button is only rendered when analystHasCapability($conn, ..., Cap::TICKETS_MANAGE), and save_inbox_display.php checks the same thing for scope = install.

Setting keys

Several tabs save through the generic writer api/settings/save_system_settings.php. That endpoint checks every posted key before writing any of them:

  1. settingKeyOwner($key) (includes/settings_keys.php) - no owner, and the whole post is refused with "Unknown setting".
  2. analystCanWriteSettingKey($conn, $analystId, $key) - the analyst must hold the owning tab's capability.

The owners come from setting_keys on each manifest tab, gathered by settingKeyOwners(). Tickets declares its keys on these tabs:

Tab Keys
categories ticket_category_enabled, ticket_closure_category_enabled, ticket_resolution_code_enabled
checklists ticket_checklist_closure_mode
mandatory-fields ticket_mandatory_fields, ticket_mandatory_mode, ticket_mandatory_notify, ticket_mandatory_record
custom-fields ticket_portal_field_answers
merge-behaviour merge_reference_mode, merge_originals_mode, merge_ai_summary
indexing attachment_extract_cron, attachment_extract_opportunistic
rota rota_include_weekends
numbering ticket_number_style, ticket_number_format, ticket_number_start, ticket_number_scope, ticket_number_reset, ticket_number_renumber
general reopen_on_customer_reply, snooze_wake_hour, ticket_workspace_mode, ticket_list_export, ticket_collapse_* (5), ticket_group_older, ticket_group_show, ticket_flag_duplicates, ticket_team_folders, ticket_ai_summary_* (4), ticket_ai_read_enabled, ticket_ai_confidential
privacy portal_third_party_visibility
reply-cleanup tickets_reply_cleanup_provider, _model, _api_key, _verify_ssl, _tone

Two Tickets keys are still listed by hand in settingKeyOwners() rather than in the manifest: time_tracking_enabled and time_tracking_api_enabled (owned by TICKETS_MANAGE). The comment there says the manifest is the pattern to copy, not that block.

Adding a setting to a tab that saves through save_system_settings.php: add its key to that tab's setting_keys. Forget, and saving the tab fails with "Unknown setting" - for administrators too.

The Reply cleanup tab's provider and key go through the shared AI panel (renderAiSettingsPanel('tickets_reply_cleanup'), includes/ai_settings_panel.php, assets/js/ai-settings.js), whose endpoints authorise with analystCanManageAiNamespace() - which looks up the owner of tickets_reply_cleanup_api_key.

The JavaScript

All of it is one inline <script> near the end of the file.

Constants

Name What it is
API_BASE '../../api/tickets/'
API_SETTINGS '../../api/settings/'
API_TICKETS '../../api/tickets/' again, used by the Reply cleanup, CSAT and Numbering code
MSG_API '../../api/messaging/'
WEBCHAT_API '../../api/webchat/'
MAILBOX_PROVIDERS json_encode(MailboxProvider::forJs()) - the Mailboxes tab's provider badges. See Mailbox providers
MESSAGING_PROVIDERS json_encode(MessagingProviderKind::forJs()) - the Messaging tab's badges, drawn by messagingProviderBadge(). A channel with no provider saved is drawn as Twilio. The enum has more cases (the web chat widget, Slack) than the channel dialog's provider dropdown offers
EVENT_LABELS The email template events and their English labels

Switching tabs

switchTab(tab) sets currentTab, moves the active class to the button with data-tab="<tab>", and shows #<tab>-tab. It assumes the panel exists.

On load the page opens a tab in two cases:

  • ?oauth=success&mailbox_id=... (back from a mailbox sign-in) opens mailboxes.
  • Otherwise a hash names a tab: settings/#sla opens SLA. It only switches if the tab button is shown and #<id>-tab exists, so a hash for a tab the analyst lacks leaves the page on its first tab. Feature Bingo's "Take me there" links use this.

Loading

There is no per-tab lazy loading. Two DOMContentLoaded handlers start the loaders for every tab when the page opens - the first calls loadTeams() then loadDepartments(), and loadTicketTypes(), loadTicketOrigins(), loadCustomFields(), loadTicketStatuses(), loadTicketPriorities(), loadRotaLocations(), loadMailboxes(), loadChannels(), loadWidgets(), loadMsgTemplates(), loadEmailTemplates(), loadReplyTemplates(), loadRotaShifts(), loadRotaWeekendSetting(), loadSlaTab(); the second calls loadGeneralSettings(), loadPrivacySettings(), loadMergeSettings(), loadReplyCleanupSettings(), loadCsatSettings(). loadOversizeDefaults() and loadIndexingSettings() are called directly as the script runs.

Because those run whatever the analyst can see, a loader must return early when its panel is not there. loadWidgets() (if (!tbody) return;), loadMergeSettings() and loadIndexingSettings() show the pattern.

Several tabs read the whole of get_system_settings.php and pick their keys out of data.settings; there is no key filter.

The newer tabs - Time tracking, Categories, and the close-gate block (Checklists and Mandatory fields) - are self-contained IIFEs that look for their own panel or button and return when it is absent. The close-gate block is deliberately separate from the Categories one: the Categories IIFE returns early when its tab is absent, so a role holding Checklists but not Categories once had a Save button that did nothing.

Checklists and Mandatory fields have nothing to load in JavaScript: their controls are rendered already set from $checklistClosureMode and $mandatoryCfg, which the PHP at the top reads. Row display is the same, from inboxDisplayForAnalyst().

Saving

Each tab saves with its own fetch() - a JSON POST, then showToast() (from assets/js/toast.js) with the result. There is no shared save helper. Forms (generalSettingsForm, csatSettingsForm, mergeBehaviourForm and so on) use a submit listener with preventDefault(); the newer tabs use a button click listener.

Modals

The modals are written once, after the last panel and outside every tab check, so they are in the page for everyone. Each is a <div class="modal" id="..."> opened by adding the active class.

  • editModal is shared by the simple lookups. openAddModal(type), editItem(type, id) and deleteItem(type, id, name) take a type of department, ticket-type, ticket-origin, status, priority or rota-location and pick the endpoint from a map; configureModalFields(type) shows the fields that type has. The maps also still hold a team entry from before teams moved to System -> Teams.
  • The rest belong to one tab each: tfModal (custom fields), tcCategoryModal and tcCodeModal (categories), mailboxModal, channelModal, widgetModal, msgTemplateModal, plus the email template, reply template, rota shift and SLA calendar modals. slaNotifModal is the exception - it sits inside the SLA panel.

renderOversizeFields($px, $withLimits) is a PHP function at the top of the file that draws the oversized-email fields twice: the install defaults on the Mailboxes tab (prefix ovd) and a mailbox's own rules in its dialog (prefix ovm). The script reads and fills them with oversizeFields(px) and setOversizeFields(px, s).

Traps

There are no TRAP: comments in this file. These are the warnings its comments carry, and what they protect.

  • The script runs even when a tab is not rendered. The panel is capability-gated; the <script> is not. One addEventListener on a null element throws, and every top-level statement after it in that script never runs - so a broken guard on one tab can silently break Save on tabs further down. Guard every DOM lookup (if (!el) return;), or put the tab's code in an IIFE that checks for its panel first. The submit listeners use ?.addEventListener, and the start-up block calls each tab's loader only when hasTab('<id>') finds its <id>-tab panel - add a new tab's loader the same way.
  • Make active conditional on $activeTabId. Hard-coding class="tab-content active" on the first panel leaves an analyst who cannot see that tab looking at no panel at all. See Roles - Developer Guide.
  • An unloaded checkbox looks like OFF. The Categories block stays locked until its load succeeds, because its three switches are drawn unticked and a failed load would otherwise save "all off" as fact. The AI summary and AI reading switches on General default to false on purpose: for those, OFF is the safe way to be wrong.
  • A saved 0 is not "not set". ticket_ai_summary_auto_after is read with Number.isInteger(), not || N, because 0 means "never refresh by itself".
  • Defaults on screen must match the engine's. loadGeneralSettings(), loadMergeSettings() and loadPrivacySettings() each name the PHP function whose default they mirror (customerReplyReopensTickets(), mergeSettings(), portalThirdPartyPolicy() and others). Change one, change both. The workspace (Off) and export (Administrators only) dropdowns fall back the same way as ticketWorkspaceMode() and ticketListCanExport() when a value is missing or unknown.
  • An enum setting is checked before it is stored. ticket_workspace_mode and ticket_list_export must be a case of TicketWorkspaceMode / TicketListExport; api/settings/save_system_settings.php refuses the whole save otherwise (its $enumKeys table - add a row there for the next one). Their dropdowns are built from ::cases(), so a new case appears without touching the page.
  • Click handlers on icon buttons use closest(). The buttons hold an inline <svg>, so a click lands on the svg or its <path>; testing e.target.classList silently does nothing.
  • The Row display preview is a second renderer. It emits the real inbox class names against the real inbox.css, but it is not inbox.js. Rename a class in one and rename it in the other.
  • A mailbox field hidden for one provider must lose required. A hidden required input still blocks the form; toggleProviderFields() and toggleAuthModeFields() switch required to match.

Related pages

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally