Tickets - edmozley/freeitsm GitHub Wiki
Folder: tickets/
Colour: Blue #0078d4
The primary module β an Outlook-style three-panel ticket inbox with email integration, departments, teams, audit trails, AI reply cleanup, drag-and-drop triage, time recording, right-click quick actions, business-hours-aware SLAs, a customisable dashboard, and a staff rota.
π SLA Management β business calendars, per-priority response and resolution targets, pause-the-clock statuses, breach notifications, and cron worker setup. Meaty enough to deserve its own wiki page. The full engine design is also written up in
docs/sla.mdin the repo.
-
Left panel β folder tree with a By Department / By Analyst toggle. Choose between grouping tickets by department + status (default) or by assigned analyst + status. The choice is persisted per-analyst via
user_preferencesso each analyst keeps their own view across sessions. The Unassigned folder is contextual: in Department view it lists tickets with no department; in Analyst view it lists tickets with no assignee β making it easy to see what needs picking up. - Middle panel β ticket list, searchable
- Right panel β reading pane with the full email thread, internal notes, attachments, audit trail
- Create tickets (from email or manually), reply / forward emails, attachments, internal notes
- Audit trail of every change (assignee, status, priority, department, owner, etc.)
- Team-based filtering of departments
- Scheduling via
work_start_datetimeand a ticket calendar with month / week / day views (see Calendar below)
Drag any ticket from the list onto a folder to update it:
- Department view: drop on a department folder to reassign; drop on a department + status subfolder to update both; drop on Unassigned to clear the department.
-
Analyst view: drop on an analyst folder to assign that analyst as owner (sets both
assigned_analyst_idandowner_idso the right-pane Owner field stays in sync); drop on analyst + status to update both; drop on Unassigned to clear the analyst.
Hovering a collapsed folder during a drag auto-expands it (Outlook-style spring-loaded folders) so any nested status is reachable.
You can also drag a ticket onto the Trash folder to soft-delete it (see Trash below).
Deleting a ticket is no longer permanent. The reading-pane Delete button is now "Move to trash" β a soft-delete that sets deleted_datetime / deleted_by and removes nothing.
- Trash folder β pinned to the bottom of the inbox folder list with a count. It lists trashed tickets, each with per-row β© Restore and β Delete forever buttons.
-
Hidden everywhere else β trashed tickets are filtered out of the inbox list, all folder / department / analyst counts, search, the calendar, per-user ticket counts, dashboard widgets, and the System Topology / Orphaned-tickets screens (a
deleted_datetime IS NULLfilter was added to all of them). They appear only in the Trash folder. - In-pane banner β opening a trashed ticket shows a "This ticket is in the trash" banner with Restore / Delete-forever and greys out the normal workflow actions until it's restored.
- SLA cron β the breach cron skips trashed tickets, so no notifications fire on a deleted ticket.
Four ways to trash a ticket:
- the reading-pane Delete (Move to trash) button
- drag a ticket onto the Trash folder
- right-click a ticket β Move to trash
- right-click the Trash folder β Empty trash β permanently purges all trashed tickets in the active company (confirm prompt + count toast)
Every trash and restore writes a ticket_audit entry. Permanent deletion (the old irreversible removal + attachment-file cleanup, behind Delete forever / Empty trash) only ever acts on a ticket that's already in the trash.
Endpoints: delete_ticket.php (now soft-deletes), restore_ticket.php, permanently_delete_ticket.php, empty_trash.php; get_ticket_counts.php returns trash_count. Schema adds tickets.deleted_datetime + deleted_by.
Granular per-ticket time logging in the reading pane. Inline form (minutes + optional description) appends entries to a list showing who logged what and when, with a running total at the top. Each entry is soft-deletable but only by the analyst who logged it, so deleted rows stay in the audit history.
- Backdating β the right-click context menu's Record time modal includes a datetime picker, so you can log time you forgot earlier in the day
-
Storage β plain integer minutes (no decimal-hour rounding pain); rendered as
45mor1h 30min the UI - Ownership β your own entries are deletable from the inline list; other analysts' entries are read-only
-
Per-analyst timesheet view β not in the UI yet, but the data is keyed by
analyst_idandentry_datetimeready for one
Backed by the ticket_time_entries table.
tickets/calendar.php plots every ticket with a work_start_datetime as a coloured block at its scheduled time. Red = High priority, green = Low, blue = anything else. The header layout matches the standalone Calendar module β Today button + chevron Prev / Next + title on the left, Month / Week / Day toggle on the right β and the Prev / Next buttons step by month, week or day depending on which view is active.
π Scheduled work in your own calendar β getting these into the calendar an analyst actually uses. A subscription link that works with any calendar app, or real appointments written into an Exchange mailbox over Graph, with changes coming back the other way. Setup, the choices involved, and the Developer Guide.
- Month view β the classic 6-row grid. Weekday header strip (MonβSun, light grey to match the standalone Calendar module), each day cell shows up to three ticket pills with their start time, plus a +N more link that drops you straight into the Day view for that date. Today is highlighted with a circular accent on the day number.
- Week view β MonβSun across the top over a 24-hour vertical timeline. The day-strip header is a blue band; today's column gets a slightly darker shade plus an inverted (white circle on blue) day number. Tickets render as 1-hour blocks positioned at their start hour (with minute offset). The weekday header stays fixed while the hour grid scrolls.
-
Day view β single column for the chosen day with the same 24-hour timeline. Tickets render as larger blocks showing
ticket_number β subjectand start time.
The API endpoint (api/tickets/get_scheduled_tickets.php) accepts either the legacy ?year=...&month=... form or, for week / day views, an explicit ?start=YYYY-MM-DD&end=YYYY-MM-DD range. JS computes the right range per view and renders client-side.
Click any ticket block in any view to open a read-only detail modal with ticket number, subject, scheduled datetime, status, priority, requester, department, owner, plus an Open in inbox link to jump back to the full inbox view.
Right-click any ticket in the list to open a popover at the cursor. It has grown into the main way to work a queue without opening tickets one at a time β most of what you can do to a ticket, you can do from here.
On a phone (2.10.0) there is no right button, so press and hold a ticket for half a second. The same menu opens as a full-screen panel with big, thumb-sized rows. A row with more options behind it β Set status, Set priority, Assign to, Snooze and the rest β slides the menu off to the left and its options in from the right, with Back to return and β to close. A finger that moves while held is scrolling the list, so the menu does not open; and the press never also opens the ticket underneath. How it is built: Mobile: Tickets Β§ The right-click menu on a phone.
The popover acts on the right-clicked ticket without changing what's open in the reading pane β handy when you're reading ticket A but need to log time against ticket B without losing your place. Its header shows which ticket (or how many) it is about to act on.
Triage β set a field without opening the ticket. Each opens a flyout, rebuilt every time the menu opens so newly-added entries appear without a refresh, with a β against the current value when you right-clicked the ticket that's open in the reading pane:
- Set status Β· Set priority Β· Set department Β· Set type
- Assign to β picking an analyst sets the assignee and the owner, so the Owner field can't drift out of step (the ticket service applies that pairing, so it holds however the assignee was changed β including a drag onto an analyst folder)
- Move to company β multi-company installs only; hidden entirely at N=1
Every flyout except Set status opens with a (no priority) / (no department) / (no type) / (unassigned) row that clears the field β status is the exception, because a ticket always has one.
Linking β Link CMDB object, Link to problem, Link to change, Link to ticket, Link to task. Each opens a search-as-you-type picker. The CMDB one keeps a "Recently linked (this session)" footer log so you can link several objects in one sitting without reopening it. See Linking tickets.
Time β Record time opens minutes + description + a datetime picker defaulting to now, so backdating is easy.
Getting it out of the way β Snooze (a flyout of preset wake times plus Pick a dateβ¦), Wake now, and Move to trash (a soft delete β see Trash).
Housekeeping β Change subject renames the ticket everywhere it appears (the list, the reading-pane header, triage), recording old β new in the audit trail and deliberately not bumping the ticket's last-updated time. Merge folds the selected tickets together β see Merging tickets.
Rather than showing greyed-out rows, the menu hides what wouldn't work:
| Item | Shown when |
|---|---|
| Merge | two or more tickets are selected β merging one ticket isn't a thing |
| Change subject | exactly one ticket is targeted β the inverse of Merge; renaming a whole selection to one subject isn't a thing either |
| Wake now | the target is actually asleep (on a selection: any of them is) |
| Move to company | the install has more than one company |
The menu follows Outlook's rule, which most people expect without being able to name it:
- right-click a row inside the current selection β the action applies to the whole selection
- right-click a row outside it β the selection is dropped and the action applies to that row only
Getting this backwards is how a bulk action hits the wrong set, so it's worth knowing which way round it is. With several selected, the header reads "N tickets selected" and the triage items, snooze and trash all fan out across them β each ticket audited individually. See Selecting several tickets.
- The Trash folder β Empty trash (permanently purges the trashed tickets in the active company, with a counted confirmation)
- A message header in the conversation β opens the Split dialog for that message, the same as its β button β see Splitting tickets
The popover closes on outside-click, Escape, window blur, or scroll. If the ticket it acted on happens to be the one open in the reading pane, the relevant section there refreshes itself.
Icon button on the ticket subject row toggles a popout mode where the folder list and email list disappear and the reading pane fills the full width, with the ticket properties container floating as a 340px right-hand panel. Same renderTicket() markup β just a body class (.ticket-popout) toggled by JS, with the CSS doing the layout work via display: none on the side panes and position: absolute on the properties panel.
Double-click an email row to open it straight into popout mode without changing the persisted preference. The dedicated popout icon persists across reloads via localStorage tied to actual reading-pane state.
When typing a reply, click the β¨ Cleanup button to have Claude rewrite the rough draft as a properly formatted email β adds a Dear [Name], greeting from the requester, fixes grammar, applies the configured tone, and signs off Kind regards,. The prompt is locked down so it won't invent technical details, fabricate apologies, or pad the content beyond what was written. Streams live into the editor; an Undo link appears for 30 seconds in case Claude butchers the rewrite.
Configured under Tickets β Settings β Reply Cleanup with its own Anthropic API key (separate from RFP AI / Knowledge AI) for granular billing visibility. Choose the model (Haiku 4.5 default, Sonnet 4.6, Opus 4.7) and tone (Friendly / Formal / Brief).
Button in ticket detail view opens a slide-in chat panel that searches the Knowledge base for relevant articles using ticket context. Useful for "have we seen this before?" on a fresh incident.
A ticket can be linked to a problem, a change, another ticket (Related / Duplicate of / Parent-Child), or a CMDB object. Problems, changes and ticket links live together in the reading pane's Links section β one Link toβ¦ menu picks the type, then a searchable picker β with the same actions on the ticket right-click menu; CMDB objects have their own Affected CMDB Objects search area. Ticket-to-ticket links are directional (they read correctly from both ends), same-company only on multi-company installs, and informational in v1 (display + audit, no automatic state changes).
See the dedicated Linking tickets page for the full walkthrough β relationship types, cardinality rules, reverse views on the problem/change/CMDB side, multi-tenancy scoping, and where each link is stored.
Separately from CMDB objects, a ticket can record which asset it is about β the actual laptop, monitor or screen. The picker opens showing the requester's own equipment and searches the whole estate, including by location, which is how shared kit gets found. Requesters can attach their own equipment from the self-service portal.
See Linking equipment to tickets, which also covers the Tickets tab this creates on the asset.
A ticket often leads to work that is not the ticket itself β order the part, book the engineer, write it up. That work lives as tasks attached to the ticket, shown as pills in the same Links bar, β while outstanding and β once done, with a fraction such as 2/5 when the task has subtasks.
Link toβ¦ β Task opens one box that does both jobs: it searches your existing tasks and offers to create one with exactly what you typed. Seeing the near matches as you type is what stops a second copy of a task that already exists. A task created here is assigned to you and takes the ticket's company. The right-click menu has Link to taskβ¦ as well, so a task can be attached without opening the ticket.
A ticket can have any number of tasks; a task belongs to at most one ticket. The β unlinks rather than deletes β the task stays in the Tasks module. Closing a ticket that still has unfinished tasks warns but does not block.
See Tasks for the task's own side of this, and the developer guide for how it is built.
The reply window has a Templates button that inserts a saved reply at the cursor β rich text, with the same [merge_code] placeholders as the automated email templates, resolved against the real ticket as you insert them. Two kinds: shared team templates, curated in Tickets β Settings β Reply templates behind a permission, and each analyst's own private ones, saved straight from the reply window with no permission needed (so having a personal template doesn't depend on someone granting settings access). Private templates are visible only to their owner β not to colleagues, and not to administrators.
See the dedicated Canned responses page for the merge-code table, saving and editing your own, and multi-company scoping β plus the Developer Guide for how the two permission scopes share one table.
The ticket list works the way Outlook and File Explorer do β click for one, Ctrl+click to toggle tickets in and out, Shift+click for a block, Ctrl+Shift+click to extend one. No checkboxes. It works from the keyboard too (arrows, Shift+arrows, Ctrl+arrows, Space, Ctrl+A, Escape). With several selected, the right-click menu acts on the whole selection β status, priority, department, type and assignee β and you can drag the set onto a folder or onto Trash, with a stack-of-pages drag image showing the count. What the screen does while several are selected is a per-analyst preference (summary panel, keep the ticket open, or a bar above the list).
When two people report the same thing, select the tickets and right-click β Merge. The conversations come together, and if an AI provider is configured FreeITSM writes a short briefing of what has been asked and what has been done so whoever picks the ticket up doesn't have to read every thread. A Tickets β Settings β Merge behaviour tab decides whether the reference stays with an existing ticket or a brand-new one is minted, and whether the original conversations move across, are attached as HTML files, or both. Nothing is deleted β a merged-away ticket keeps its reference forever, stays searchable, shows where it went, and a customer replying to an old email about it still reaches you.
See the dedicated Merging tickets page for the settings and their trade-offs β plus the Developer Guide for why merged tickets are never deleted.
The opposite job: when a customer raises a second, unrelated thing mid-conversation, click β Split on that message (or right-click its header). It moves to a brand-new ticket, optionally taking every newer message with it, and the original keeps a marker showing where they went. Both tickets stay open, linked as Related. Splits can be undone while nobody has worked on the new ticket yet β and merges can be undone too.
See the dedicated Splitting tickets page β plus the Developer Guide for why undoing a split refuses on activity while undoing a merge does not.
See the dedicated Selecting several tickets page for the full key map and the three display modes β plus the Developer Guide for why bulk writes loop the ticket service rather than issuing a single UPDATE.
A large part of any queue is waiting on somebody else β the user who hasn't sent the screenshot, the supplier who ships on Tuesday, the change that goes in at the weekend. Those tickets aren't work, but they sit in the inbox looking exactly like work, so every morning starts by re-reading them to re-decide they still can't be touched.
Right-click a ticket β Snooze, then pick In 3 hours, This evening, Tomorrow, Next week, or Pick a dateβ¦ for an exact time. Each option shows the moment it actually means ("Tomorrow Β· Wed 09:00"), and an option disappears once its name would stop being true β This evening isn't offered after 18:00. You can add a short note of what you're waiting for.
What then happens:
- It leaves the queue. The ticket drops out of every folder and every folder count, and appears in the Snoozed folder (above Trash), listed in the order the tickets come back.
- It comes back on its own, with nothing scheduled to run β so it returns on time even on an install with the crons switched off.
- A customer reply wakes it immediately, whichever channel they use β email, the self-service portal, web chat or WhatsApp β because their reply is usually the very thing you were waiting for.
- A sleeping ticket says so. Opening it shows a banner with the wake time, your note, who snoozed it, and a Wake now button. Right-click offers Wake now too, and snoozing works on a whole multi-select at once.
Tomorrow and Next week land on the start of the working day set under Tickets β Settings β General (default 09:00), read in each analyst's own timezone β so a desk spread across zones each gets their own morning.
Two things snoozing deliberately does not do:
- SLA clocks keep running. The customer's target isn't suspended because the desk decided to look away; snoozing your way out of a breach would make the numbers meaningless. Use a status flagged Pause SLA if the clock genuinely should stop (see SLA Management).
- It doesn't hide the ticket from search, or from the command palette. Snoozing takes a ticket out of your folders, not out of the product.
Snooze is a fact about the ticket, not a private reminder β unlike the snooze in a mail client, it applies for the whole desk, which is why the banner names who set it and why.
Developers: see the Snoozing tickets β Developer Guide for why waking is the clock rather than a cron job, the four inbound doors that wake a ticket, and the schema gate that keeps the inbox working before Database Verification has run.
A ticket can be Normal or Confidential (the Sensitivity field in its properties). Confidential tickets are kept from the requester's managers; the service desk sees them as usual. A red Confidential marker sits in the properties bar even when it is collapsed, and a padlock marks them in the list. Tickets also become confidential on their own: through a confidential mailbox (an HR mailbox), in a confidential department, when the requester turns it on in the portal, or when split from or merged with a confidential ticket. Nothing automatic ever makes one Normal again. See Confidential tickets.
Opening a ticket is recorded. The ticket's Audit window lists who viewed it and how many times each day, service desk and self-service portal alike. The person who raised it sees portal viewers only, such as a manager, on the ticket itself. See Who has seen a ticket.
Two analysts answering the same customer is the oldest service-desk annoyance there is β and until now nothing warned you. Open a ticket a colleague already has open and a line appears under the header: "James Smith is also viewing this ticket." If they are actually writing β a reply, a forward or a note β it turns amber and says so, and clicking Reply repeats the warning inside the composer.
It warns and never blocks: two people may legitimately both need to write (a holding reply while someone else escalates), so nothing is greyed out and the decision stays yours. It updates live in both directions, and anyone whose browser goes quiet simply stops being shown a few seconds later β so a closed tab or a dropped connection never leaves a ghost sitting on a ticket.
See the dedicated Collision detection page for what the two states mean and what it deliberately doesn't do (it is not a record of who read what) β plus the Developer Guide for the heartbeat design and why it needs nothing scheduled.
- Microsoft 365 (via Microsoft Graph API) and Google Workspace (via Gmail API)
- OAuth 2.0 authentication β no plaintext mailbox passwords. Microsoft mailboxes support two modes β delegated (sign in as the mailbox) and app-only (client credentials, no sign-in) β plus safeguards that ensure you're reading the right inbox. See Mailbox Authentication for when to use which, Azure setup, an OAuth scopes/permissions 101, and troubleshooting.
- Basic IMAP / SMTP β a third provider option for plain username/password mailboxes (hosting/cPanel inboxes, Fastmail, etc.) with no OAuth app registration. See Basic IMAP mailboxes.
- Mailbox whitelist β per-mailbox domain and email-address whitelisting; non-whitelisted senders are rejected and logged
- Email actions β configurable per-mailbox: reject actions (delete / move to Deleted Items / mark as read) and import actions (delete / move to folder) with folder verification
-
Email templates β automated responses triggered by ticket events (new ticket from email, assigned, closed) with merge codes:
[ticket_reference],[requester_name],[analyst_name], etc. The editor has an Edit / Preview tab so you can see the rendered email with sample merge values β HTML bodies are supported, so templates can include styled buttons.[ticket_url]links straight to the requester's own view of the ticket, which needs the public web address set. Each template can be limited to particular senders β an address or a whole domain β with the most specific template winning, never the order they are listed in.
-
check_email.phpruns (scheduled, or via a manual trigger) - For each active mailbox: refreshes the OAuth token, calls the provider's API, and imports new emails
- Creates a new ticket or matches an existing one based on subject / requester
- Downloads attachments and logs the results
Every 60 seconds an open inbox checks the mailboxes and then asks api/tickets/get_inbox_activity.php for one number: the id of the newest message on any ticket the analyst can see (their companies only). When it goes up, the inbox runs the same refresh as the β» button: the list and folder counts, never the reading pane or a reply being typed. Before 3.1.0 only mail found by the inbox's own check triggered a refresh, so chat messages (they arrive by webhook) and mail collected by the scheduled task sat unseen until someone pressed refresh. The first look after the page loads only sets the baseline. The endpoint returns nothing but the number.
The New Ticket modal's requester is a single searchable field: type a name or an email address and pick the person, with their company shown beside them. Anyone not on the system yet is added inline, keeping whatever you already typed. See Raising a ticket for someone else β and the Developer Guide for the request flow, the two authorisation points, and the company-scoping fix that came with it.
A ticket's send-from mailbox is resolved from its emails' mailbox_id. Emailed tickets get this for free, but a manually-raised ("on behalf of someone") ticket used to leave it NULL β so replying failed with "Could not determine mailbox for this ticket."
The New Ticket modal now has a "Send replies from" dropdown so a manual ticket is paired with a mailbox up front. create_ticket.php stamps the chosen mailbox_id on the initial email, so replies just work.
- The list (via
api/tickets/get_sendable_mailboxes.php) is scoped to the company the ticket is being raised in β its pinned mailbox(es) plus shared-intake ones β and only those that are active and signed-in (so the choice can actually send). - Multi-tenant safe β a read-only company label sits next to the field at N>1 so it's clear which company's mailboxes these are, and the server rejects a mailbox that doesn't belong to that company.
- N=1 unaffected β it simply lists all active, signed-in mailboxes.
- If there are no sendable mailboxes the ticket can still be created; it just can't email a reply yet.
Laid out to give the message the room (2.10.0):
- To and Cc each have their label beside the box rather than above it, and browser autocomplete is off (no saved-value dropdowns over them).
- There is no Message label over the editor and no Attachments label over the drop zone β both said what was already obvious β and the editor is taller (440px).
- Templates and Signature sit at the bottom left of the footer, the same height as Send. Their menus open upwards, and opening one closes the other.
For developers: the markup is in tickets/index.php (the reply modal), the menus and their mutual exclusion in assets/js/inbox.js, the look in assets/css/inbox.css.
(This used to say "matching the New Ticket modal's requester name / email row". That row no longer exists β those two boxes were replaced by a single searchable requester field. See Raising a ticket for someone else.)
The ticketing system handles email correspondence as a flat thread β each email in a ticket is stored and displayed as its own standalone entry, newest first, with no nesting or indentation.
When a user replies to an email, their email client (Gmail, Outlook, etc.) automatically appends the entire previous conversation as a quoted block below their new content. If you simply store the full email body, each reply contains every previous message nested inside it, creating an ever-growing blob of duplicated content. Displaying these naively produces deeply indented, confusing threads with coloured borders and boxes-within-boxes.
The reply flow separates what the recipient sees from what gets saved to the database:
- Reply editor is empty β when an analyst clicks Reply, TinyMCE opens with a blank editor. No quoted thread, no markers, just a clean box for typing.
-
Server assembles the full email β when the analyst clicks Send, only their typed content is sent to the server. The server (
api/tickets/send_email.php) then:- Fetches all previous emails for the ticket from the database
- Builds a quoted thread (each email as "On [date], [name] wrote:" + blockquote)
- Inserts a visible reply marker: β Please reply above this line β
- Constructs the full email: analyst's reply + marker + quoted thread
- Sends this assembled email to the recipient via the ticket's send-from mailbox
- Only the analyst's content is saved β the database stores just what the analyst typed, not the full assembled email. This prevents thread duplication in the DB.
The visible text β Please reply above this line β serves as the primary anchor for stripping. It is:
- Plain Unicode text (em dashes + words) that survives every email client's HTML processing
- Wrapped in a
<div>withdata-reply-marker="true"as a secondary signal - Displayed to the recipient as a subtle grey line between the analyst's reply and the quoted thread
When a user replies back, their email contains the full thread (their reply + our marker + quoted history). The stripping functions extract only the user's new content by looking for these anchors in order:
-
Our visible marker text β
β Please reply above this line β(primary, works with all email clients) -
Our
data-reply-markerdiv β backup if the HTML attribute survives -
Legacy SDREF marker β
[*** SDREF:XXX-000-00000 REPLY ABOVE THIS LINE ***]for older emails -
Generic blockquote fallback β takes content before the first
<blockquote>tag - Attribution line cleanup β removes trailing "On [date], [name] wrote:" lines that email clients add before quoted blocks
Stripping happens at two points:
-
Import time (
check_mailbox_email.phpβstripInboundThread()) β cleans the body before saving to the DB -
Display time (
get_ticket_thread.phpβstripQuotedThread()) β safety net for legacy emails already in the DB
The correspondence thread in the reading pane renders emails as a flat list:
- Newest email at the top, oldest at the bottom
- Each email separated by a thin horizontal line
- Direction badge (Received/Sent) with sender name, email address, and timestamp
- CSS overrides (
!important) to kill any inline styles, blockquote indentation, or coloured borders from email HTML that leak through stripping
| File | Role |
|---|---|
assets/js/inbox.js |
Reply/forward modals (empty editor), sendEmail() passes type param, loadCorrespondenceThread() renders the flat thread |
api/tickets/send_email.php |
buildFullEmailBody() assembles the full email for the recipient, saves only the analyst's content to the DB |
api/tickets/get_ticket_thread.php |
stripQuotedThread() strips quoted content at display time |
api/tickets/check_mailbox_email.php |
stripInboundThread() strips quoted content at import time |
assets/css/inbox.css |
Flat thread styles, inline-HTML overrides |
Email isn't the only way into the inbox. Customers can also reach the desk over WhatsApp, Telegram and through an embeddable web chat widget β each conversation becomes a normal ticket, tagged with its origin and answered from the same reading pane. Both are managed under Tickets β Settings (Messaging and Web chat respectively) and each has its own wiki page:
-
WhatsApp channel β Twilio or the Meta WhatsApp Cloud API behind one provider-agnostic interface (
includes/messaging/), webhook ingress in direct or relay mode, the 24-hour service window with pre-approved templates, AI summarise / suggest helpers, inbound images and files attached to the ticket, and a 15-second live refresh on open channel tickets - Telegram channel (2.10.0, contributed by Andrew Turbay in PR #159) β a bot as a channel: paste its token, press Connect, and messages to the bot become tickets. No 24-hour reply window, replies in each person's Telegram language, a once-only Share phone number step that links the chat to a known person, and pictures and documents both ways. The developer side: Telegram channel - Developer Guide
- Web chat channel β public widget key + copy-paste embed snippet, Shadow-DOM widget, per-conversation tokens, origin allowlist, office hours, and AI answers grounded strictly in published public Knowledge articles, with assist / deflect modes and escalation
Sometimes the closing email needs one sentence that belongs to that ticket alone β "the spare charger is on your desk", "closing as agreed on the phone" β and nothing about it is worth storing or configuring.
Put [ticket_closed_message] anywhere in an active Ticket closed template:
Hi [requester_first_name],
Your ticket [ticket_reference] has been closed.
[ticket_closed_message]
If this isn't resolved, just reply to reopen it.
From then on, setting a ticket to a closed status asks the analyst for a note, and whatever they type lands exactly where you put the placeholder β so you keep control of the wording around it. Type nothing and the line simply is not there: no gap, no stray punctuation.
π The prompt appears only when the placeholder is actually in an active template. Two reasons. Without it, anything an analyst typed would be silently thrown away and they would have no way of knowing. And every desk that does not want this would otherwise gain an extra click on every single close, forever. So it switches itself on when you configure it, and stays out of the way when you have not.
< or pressing Enter cannot mangle the message. Closes that cannot prompt anybody β a bulk close, a REST API close β send the code as empty rather than leaving the literal text in the email. A workflow's Set ticket status action sends no closing email at all.
Asked for by mbsouth.
If the Checklists & SOPs module is in use, a ticket can carry procedure steps marked mandatory. Tickets β Settings β Checklists decides what happens when somebody closes a ticket with one still outstanding:
| Setting | What happens |
|---|---|
| Warn (default) | The ticket closes, and an internal note records which mandatory steps were outstanding, who closed it, and whether they did it from the web interface or the API. |
| Block | The close is refused until those steps are done, and the message names them. |
π Whichever you choose is enforced at the point every close goes through, not in the browser β so bulk actions, the REST API and workflow automation obey it too. A gate that can be walked around reports control that is not there.
Tickets β Settings β Mandatory fields lists the ticket's fields (priority, category, resolution code, owner and the rest), each with a tick for must be filled in before closing. Choose what happens when a ticket is closed with a ticked field empty: warn, warn and email named people such as a service delivery manager, or refuse the close. A separate switch records it on the ticket as an internal note. The same rule applies on the ticket screen, in bulk actions, through the REST API and in workflows. A field switched off for a company is never required, and merging never triggers it. Full detail: Mandatory fields.
When a requester comes back on a ticket whose status is flagged is_closed, the ticket returns to your first open status (includes/ticket_reply.php; toggle under Tickets β Settings β General). It's one rule shared by every channel a customer can arrive on β the self-service portal, a reply to a notification email, WhatsApp, and web chat β because "reply to a finished ticket" means the same thing whichever door they used, and two copies of the rule would drift into a ticket that reopens by email but not from the portal.
- Defaults to on when the setting has never been saved, matching what customers were already being told β the requester-facing email template promises "just reply to this email and it will be reopened".
-
Status names are never hardcoded β it reads
ticket_statuses.is_closedand reopens to the first active non-closed status (preferring one called "Open", then the install default), so a renamed workflow still lands somewhere sane. - Resolved vs Closed β the shipped defaults flag only Closed as closed, so out of the box a reply to a Resolved ticket appends without reopening; mark Resolved as closed under Statuses if you want it included.
- Non-fatal by design β a failed reopen can never cost you the customer's message. Reopening is non-destructive and visible, whereas a reply silently landing on a closed ticket nobody looks at again is not.
- It also wakes a snoozed ticket. The same four doors carry a second rule β if the ticket was snoozed, the customer's reply brings it straight back to the queue, because their reply is usually what the snooze was waiting for. The two rules are independent: a reply can wake a ticket without reopening it, and vice versa.
Per-analyst customisable dashboard with Chart.js widgets.
- Widget library with 15 pre-built charts (bar, pie, doughnut, line) covering status, priority, department, type, analyst, origin, first-time-fix, time-series
- Time-series widgets with configurable grouping: day, month, or year
- Multi-series: stacked bar charts broken down by status or priority; created-vs-closed comparison line charts
- Date range filter: last 7/30 days, this month, last 3/6/12 months, this year, all time
- Department filter: scope any widget to specific departments
- Each analyst picks widgets for their own dashboard
- Status filtering on supported widgets
- Inline editing: cog icon on each widget opens a modal to edit properties without leaving the dashboard
- Drag-and-drop reordering
Weekly grid showing analyst shift patterns, WFH/office location, and on-call status. Per-day entry management, configurable shift definitions, and a Sunday/Monday week-start toggle.
One cell: right-click it and choose Copy, then right-click another and choose Paste. If the target already has a shift you are asked first, and the question names both β "James Smith already has Early (AM-PM) on Monday 7 Sep. Replace it with Late?" The same menu has Clear for removing a shift without opening the editor.
Several cells: once something is on the clipboard there are three ways to aim it at more than one cell, because three different jobs want different ones.
- Drag across a block of cells to select a rectangle, or hold Ctrl (Cmd on a Mac) and click cells one at a time to pick out scattered ones. Then right-click anywhere inside the selection.
- Hover a day heading and that whole column lights up β everybody, on one day. Right-click the heading to paste into all of it.
- Hover an analyst's name and their whole row lights up β one person, all week. Right-click the name to paste into all of it.
A plain click still opens the shift editor, which is what it has always done and is the commoner action. Selecting is a gesture the grid can tell apart: a drag across cells, or a Ctrl-click. A press and release inside a single cell is never a selection, and the click that finishes a drag does not open anything.
If every cell you are pasting into is empty it simply happens. If some of them already have a shift, the menu offers both ways by name and you choose before you commit:
Paste into all 7 Paste into 6 empty cells
The second only appears when there is genuinely a mix β with nothing in the way, or with nothing empty, it would be the same action under two names. Pasting into all of them asks you to confirm and says how many shifts it replaces; filling the gaps just happens, because nothing is lost.
Empty is worked out on the server at the moment of writing, not from the grid your browser last drew. If a colleague filled one of those cells while you were deciding, it is left alone β which is the whole reason the option exists.
The same two exclusions as a week paste apply, and for the same reason: a cell belonging to a deactivated analyst, or one falling on a weekend while weekends are switched off, is not written. You are told how many were refused rather than left to notice.
Copying a whole day, or one person's whole week: right-click a day heading and choose Copy to lift every shift on that day; right-click an analyst's name to lift their whole week. Paste it onto another day, or another analyst, and that line is made to match the one you copied β cells the copy did not have are cleared, so you get the day you copied rather than a mixture of the two. The confirmation says how many shifts arrive and how many go.
A day can only be pasted onto another day, and an analyst only onto another analyst. A day is one shift per person and a week is one shift per day, so there is no honest way to lay one over the other β and quietly doing something plausible instead would be worse than saying no. The menu says so where you clicked, and says where it can go:
A day cannot go onto an analyst β you copied a whole day, which is one shift per analyst. It can only be pasted onto another day heading.
The same applies to dropping a whole line into a single cell, and to pasting a line back onto the one it came from.
Clearing: the same three menus β a day heading, an analyst's name, a selection β will also empty what is there. It always asks, and always names the number: "This removes the 5 shifts on Monday 7 Jun. It cannot be undone." Clearing respects the same two exclusions as everything else here: a deactivated analyst's entries and hidden weekend entries are never touched.
A whole week: Copy week puts the week on the clipboard and a Paste week button appears, telling you which week it is holding. Navigate to the week you want and press it. Entries are remembered as a day of the week rather than a date, so Monday lands on Monday.
Pasting a week replaces it rather than merging into it β a merge would leave you with a week matching neither the one you copied nor the one you had. If the target week already has anything, you are told how much before it happens: "This week has 5 entries already. Pasting replaces the whole week with the 5 you copied, so the 5 currently here are removed."
Two things it will not touch, because you cannot see them on the grid you are pasting over:
- Entries belonging to deactivated analysts. The rota only lists active analysts, so those rows are already invisible and are left alone.
- Weekend entries, when weekends are switched off. With a Monday-to-Friday grid, a paste covers Monday to Friday; anything on the Saturday or Sunday survives.
The clipboard lives in the browser tab and is not saved, so it is gone when you close the rota. If a shift is deleted between copying and pasting, those entries cannot be pasted and you are told how many rather than losing them silently.
Each module has an interactive help guide at tickets/help.php β scroll-spy sidebar, sectioned content. 11 sections: Overview, The inbox, Working with tickets (including drag-and-drop triage, full-screen view, right-click menu with Status / Priority / Assign-to / Link-CMDB / Record-time submenus, time recording), Comments & attachments, AI tools (Reply Cleanup β¨ + Ask AI π€), CSAT surveys (both trigger modes, the analytics page, attribution semantics), User management (Add / Edit / Delete on tickets/users.php), Dashboard, Calendar & rota, Settings (with a prominent callout to the dedicated SLA Management help page), Quick tips.
Tickets β Settings:
- Departments
- Ticket types
- Ticket origins
- Statuses (with Pause-SLA flag per status β see SLA Management)
- Priorities (with response and resolution targets per priority β see SLA Management)
- Rota locations
- Mailboxes (M365 + Google) β OAuth tokens, whitelists, import/reject actions, folder verification. Plain Basic IMAP / SMTP mailboxes are supported too β see Basic IMAP mailboxes
- Messaging β WhatsApp channels, message templates and the Public base URL β see WhatsApp channel
-
Web chat β embeddable chat widgets (its own
tickets.webchatcapability, separate from the WhatsApp credentials tab) β see Web chat channel - Email templates
-
Reply templates β the team's shared canned responses (its own
tickets.reply_templatescapability; an analyst's private templates are saved from the reply window and never appear here) β see Canned responses - Rota shifts
- General (department-default mapping; reopen a closed ticket when the requester replies β one rule shared by the portal, email, WhatsApp and web chat; and the start of the working day that Tomorrow / Next week snoozes wake to, read in each analyst's own timezone β see Snoozing tickets)
- π Privacy β what a requester sees of their own ticket in the self-service portal. Forwards to suppliers, copied-in colleagues and their replies all land on the same ticket; this decides whether the requester sees them. Defaults to hiding them
- Reply Cleanup AI (key, model, tone)
- CSAT β survey mode (off / auto on close / manual only), delay, one-survey-per-ticket toggle, scale (stars or emojis) β see Customer Satisfaction Surveys below
- SLA β business calendars, per-priority targets, behaviour settings, breach notification rules, cron activity log (see SLA Management)
Analysts and teams are no longer here. They're app-wide β analysts work across every module, and teams are used by tickets, tasks, contracts and the workflow engine β so they moved to System β Analysts and System β Teams (see System). Ticket departments stay under Tickets β Settings; the departmentβteam link is editable from both the Departments tab here and System β Teams.
The tickets/users.php page lists every end user known to the system (people who raise tickets, not analysts) alongside their ticket history. Users land in the users table four reactive ways: portal self-registration, inbound email from a new sender, the mailbox poller, and the workflow engine creating one from a triggered ticket. That covers most cases, but analysts also need to be able to pre-create a user (a new starter who's about to need access, or a contractor) and edit or delete existing rows β which is what this surface adds.
Add user β Add button in the users-list header opens the person editor (includes/person_editor.php). It is the same editor Assets β Users opens, so a person is added and changed the same way whichever screen you are on. It has:
- Email (optional - people who sign in through a directory may have no mailbox; give an email or a display name. If given: validated, unique, and not an analyst's)
- Display name
- Preferred name
- Job title, Department, Office, Phone, Mobile, Employee ID, Manager (all optional)
- Password (optional)
- Company (multi-company installs only β hidden at N=1)
Those seven middle fields describe the human rather than the account, and they are the
same columns Directory sync writes β job_title, department,
office, phone, mobile, employee_id, manager_id on users, added by slice 1 in
August 2026.
asset-management/users.php - at two
different depths, and they drifted: Assets had no preferred name, password or company,
cleared a manager it could not see, and posted to a Tickets-only endpoint. Since
discussion #62 both screens open
one shared editor, reading the person from api/tickets/get_person.php, and
save_user.php accepts either module. Delete stays here; Mark as left / Reactivate stay on
Assets.
Three of them earn their place beyond tidiness:
-
Office says which site somebody is at, and a directory fills it in for you from
physicalDeliveryOfficeName.β οΈ It does not feed the ticket asset picker βapi/tickets/search_linkable_assets.phpsearchesasset_locations.namethrough the asset's ownlocation_idand never readsusers.office. The schema comment beside the column is easy to read as saying otherwise. -
Manager records the reporting line, and - once portal managers
are switched on - decides who can see this person's tickets in the self-service
portal. It is a type-ahead: type part of a name.
β οΈ It does not route approvals: catalogue request approvals go to a designated analyst, andincludes/catalogue_approvals.phpsays in as many words that manager-based routing is "a later slice". A loop is refused server-side byuserManagerIsSafe()β A managing B managing A is not hypothetical, it happens whenever two people cover for each other, and anything walking the chain for an approver would walk it forever. - Employee ID is the join key when reconciling against a payroll system that has never heard of an email address.
Where a directory owns the record (users.is_managed = 1), all seven are shown
disabled with one note saying why. This is not decoration: api/tickets/save_user.php
refuses the entire save if a managed record's request mentions any of them β an edit
accepted now and reverted by the next sync is how somebody concludes FreeITSM lost their
change β so a form that posted them anyway would turn "I changed their company" into an
error about a job title. The detail pane names the source - "Details from: Contoso AD -
directory", or "Baikal - address book" - so the reason is visible before the modal
is opened; someone who only signs in through a provider shows "Signs in with" instead.
Everything else on the record, company and password included, stays editable. (An
address book with write-back on locks nothing: edits are sent to the card.)
The Source filter, under the search box, lists everyone, only the people added in FreeITSM, or only those from one directory or address book, with a count beside each. It appears once anybody is linked to a source.
Add to address book. Somebody who emailed in exists here but in no address book. If an administrator has switched on Let analysts add people to this address book on that address book, the person gets an Add to address book button beside Edit: it asks first, creates their card, and links this record rather than making a copy. It refuses if the book already has their email address, and checks the next import will pick the card up. See CardDAV contact sync.
The manager type-ahead is scoped, twice over. It searches api/tickets/get_users.php
(?search=&limit=20), which is scoped to the companies you can reach - so you can only pick
somebody you are allowed to see, and save_user.php checks again. And manager_id itself is
not tenant-scoped, so the current manager's name comes from get_person.php only when
analystCanAccessUser() allows it - never a LEFT JOIN users mgr, which would hand an
analyst scoped to one company a name from another. A manager outside reach shows as
"Someone you cannot see - left as it is unless you choose another"; the id is parked and
the key is omitted from the payload, because posting nothing would clear a reporting line
the analyst was never shown - and take away that manager's view of the person's tickets.
Manager access. Beside Edit, Manager access opens a full-screen page for what this person can see as a portal manager: what gives them access, who they can see (click anyone for why), and - for those allowed - adding people, groups or departments to their team, or excluding them.
π The list of field names is emitted into the editor's JS from USER_PERSON_FIELDS in
includes/users.php rather than retyped. That file says plainly that a list duplicated
across writers "is a list that will disagree with itself within a month"; with one shared
editor it is now written in exactly one place (the Assets copy that hardcoded it twice is
gone).
π Full treatment, including what a portal user may change about themselves and why three of the seven are withheld from them: Contact details, with its developer guide.
Leaving the password blank creates a passwordless account β exactly the same state inbound-ticket users start in. The user can then later claim the account by visiting self-service/register.php with that email and choosing their own password (the register endpoint detects the existing passwordless row and updates it rather than creating a duplicate). Setting the password from the analyst side is the shortcut path β useful when you want to hand a fresh login over to someone in person. Note that the portal's register flow now confirms ownership by email before the password is set β see Self-Service Portal.
On a multi-company install the Company picker decides where that person's self-service tickets are filed. Its blank option means different things in the two modes, and the label says so: on a new person it reads "Work it out from their email address" (the payload omits the field and the server infers the company), while on an existing one it reads "No company" β a deliberate choice that is sent and stored.
Edit user β Edit button on the user detail panel re-opens the same modal pre-filled. Saving without a password leaves the existing hash untouched; supplying one resets it.
Delete user β Delete button next to Edit. The API (api/tickets/delete_user.php) is FK-safe: it refuses to delete if the user is the requester on any tickets or has any rows in users_assets, returning a clear "Cannot delete: this user is the requester on N ticket(s). Reassign or close those tickets first" message. Audit history can't silently break, but it also means you can't tidy up a user account if they have ticket history β soft-delete is on the roadmap if that becomes painful.
The modal disables browser autocomplete on every field (including autocomplete="new-password" on the password input, which Chrome respects for create-account flows) so the analyst's own email / saved password isn't suggested into a form whose entire purpose is creating someone else's record.
Email a 1β5 satisfaction survey to the requester when a ticket is resolved, capture the response on a no-login landing page, and track per-analyst trends. Off by default; the master switch lives under Tickets β Settings β CSAT.
-
Send mode β
off(disabled),auto on close(survey is queued automatically when the ticket transitions into any closed status), ormanual only(analyst clicks the β Request feedback button in the ticket toolbar when they want to ask). -
Delay (minutes) β
0β10080. Wait this many minutes after close before sending. v1 stores the value but always sends immediately; honouring the delay needs a worker, deferred to a follow-up. - One survey per ticket β default on; stops survey-spamming a flaky ticket that reopens and re-closes several times. The manual button always bypasses this guard.
-
Survey scale β
stars(5 outline stars with classic fill-on-hover trailing behaviour, click to lock) oremojis(5 greyscale faces π‘ π π π π; hovered/selected one returns to full colour at 1.25Γ scale). Both modes store the same 1β5 number so dashboards and averages work identically. - Ask chat customers in their chat (3.1.0) β a customer who wrote in on WhatsApp, Telegram, Slack, Microsoft Teams or Mattermost is asked in that chat instead of by email: five buttons on Telegram, Teams and Mattermost, "reply with a single digit" elsewhere. WhatsApp is only asked while its 24-hour window is open; outside it, or if the chat message fails, the email survey is used. A digit counts as a rating only straight after the question β within a week, and before the customer has written anything else β so an answer to an analyst's question is never mistaken for one. Off after upgrading (on for a new install). The ratings land in the same place as emailed ones. How it works: Teams and Mattermost β Developer Guide.
The survey email content comes from the existing Email templates machinery β create a template with event CSAT survey (a new option alongside New ticket from email, Ticket assigned, Ticket closed) and embed the new [csat_link] merge code wherever you want the rating URL. Standard merge codes ([requester_name], [ticket_reference], [analyst_name], etc.) are available as usual.
The button in the ticket toolbar (and the auto-trigger in assign_ticket.php) refuse with a clear error if no active template exists β you can't silently produce dead survey links.
Each survey request creates a row in ticket_csat_responses with a per-response token (24 random bytes hex-encoded, 48 chars). The public landing page (/csat.php?token=β¦) looks the token up β an invalid or already-responded token shows a friendly generic error so token existence isn't leaked. Tokens are one-shot: once responded_datetime is set, the same URL refuses subsequent submissions. A per-install HMAC secret (csat_token_secret in system_settings, auto-generated on db_verify) is reserved for verification flows where the DB isn't queried.
-
Auto β fires from
api/tickets/assign_ticket.phpin the same block that sends theticket_closedtemplate email, but only when the status transitions FROM not-closed TO closed andcsat_mode === 'auto'. Pre-flight checks (template exists, mailbox reachable) run BEFORE the row is inserted, so failed sends don't leave orphan tokens. -
Manual β
api/tickets/request_csat.phppowers the β Request feedback button in the toolbar. Bypasses both the close-transition trigger and the one-per-ticket guard (an analyst clicking the button is a deliberate ask), but still respectscsat_mode = offas the master switch.
/csat.php?token=β¦ β deliberately gorgeous, full-bleed gradient background, no analyst chrome. The rating is the UI: borderless, no surrounding box, no per-item labels.
- Stars mode β 5 outline SVG stars in a row. Hovering star #3 fills #1+#2+#3 in gold (classic fill-trail). Click locks the score in; the hover effect clears on the next move.
- Emojis mode β 5 faces π‘ π π π π in greyscale at 40% opacity. The hovered or active one returns to full colour at 1.25Γ scale (the fill-trail model doesn't make conceptual sense for distinct icons).
- Live caption under the rating updates on hover ("3 / 5 β Neutral") and locks on click.
- Submit enables on first click; optional comment box underneath.
- Thanks screen on submit is branded and matches the survey page's gradient.
- Error states β invalid token, already-responded token, or expired template all render the same friendly "link not valid" page so token existence is never leaked.
tickets/csat/ β reachable from the β CSAT button in the Tickets nav. Shows:
- KPI tiles: average rating (out of 5), response count, response rate (responded Γ· sent)
- Score distribution: bar chart of 1β5 with percentages
-
Per-analyst leaderboard: avg rating + response count, ordered by avg desc. Attribution survives ticket reassignment because
analyst_idis denormalised onto the response row at survey-send time. - Responses: the ratings newest first, 25 a page, with their free-text comments and the customer who left them, linked back to the source ticket
Window selectable: 7 / 30 / 90 / 365 days, or a custom From / To range. The three tiles count surveys sent in the period, so the response rate compares like with like; the bars, the leaderboard and the list count responses received in it. A survey sent on the 30th and answered on the 2nd can therefore be in one month's tiles and the next month's list.
Who a rating belongs to: the analyst is fixed when the survey is sent (the one who closed the ticket, or who pressed Request feedback), so reassigning the ticket afterwards does not move it. The customer is the ticket's requester as it stands now.
Filters (added for #157): Rating (Positive 4β5, Neutral 3, Negative 1β2), Analyst and Customer, alongside the date range. Analyst and customer narrow every figure on the page; the rating band narrows only the responses list, because averaging only the 5s would always give 5. The Analyst and Customer lists offer everyone who has been sent a survey in the selected company, whatever the dates - so a choice does not vanish when you change the period. Every filter is a URL parameter, so a filtered view can be bookmarked or shared. On a phone the filters live behind a sticky Filters button at the bottom of the screen, with a badge counting the filters in force, and open full-screen.
On a multi-company install every figure follows the company switcher in the header, like the ticket list: the company picked there, or every company you may see under All companies. An analyst restricted to some companies never sees another company's ratings or comments. This is company scope only: an analyst whose view of the inbox is narrowed by team still sees every rating in their company here. How it is built: CSAT company scope and filters - Developer Guide. (Added for #157; single-company installs are unchanged.)
v1 ships as a standalone page rather than a dashboard widget because the existing widget library models "group X by Y" which doesn't fit CSAT's average-of-ratings semantics cleanly. Widget-library integration is a follow-up.
Searchable, paginated log of imported and rejected emails per mailbox with clickable processing log details. Stored in mailbox_activity_log.
| Table | Purpose |
|---|---|
tickets |
Core ticket record (ticket_number YYYYMMDD-XXXX format) |
emails |
Email messages linked to tickets (incoming/outgoing/manual) |
target_mailboxes |
Configured mailboxes with OAuth credentials |
mailbox_email_whitelist |
Per-mailbox sender whitelist |
mailbox_activity_log |
Audit of imports and rejections |
ticket_email_templates |
Auto-response templates with merge codes β now includes the csat_request event for survey emails |
ticket_csat_responses |
One row per CSAT survey sent (token, sent / responded timestamps, 1β5 rating, free-text comment, denormalised analyst_id) |
ticket_audit |
Field-level change history |
ticket_notes |
Internal notes |
ticket_time_entries |
Granular time logs (analyst + minutes + datetime + notes, plus an is_active flag for soft delete) |
users |
End users (ticket requesters) β tickets.user_id FK; created reactively (portal, inbound email, workflow) or manually via tickets/users.php
|
user_preferences |
Per-analyst settings (e.g. inbox grouping, popout mode) |
SLA-related tables (sla_calendars, sla_calendar_hours, sla_calendar_holidays, sla_notification_rules, sla_notifications_sent, sla_cron_runs) are documented separately on the SLA Management page.
- Record previews β the β beside every linked record, and what each card shows