Tickets: Lifecycle & Management
Tickets are the heart of the ERPat Helpdesk. This guide walks the full support-ticket lifecycle — from creation and assignment through comments, status transitions, and closure — and every management surface around it: ticket types and their SLA durations, routing groups, canned templates, agent signatures, collaborators, labels, attachments, and the three ways a ticket can be submitted.
contact-support portal are documented separately on Tickets — Email & Automation. The Permissions Reference and Glossary & Legends hold the master tables.Who uses tickets
The ticketing pillar serves several distinct roles. Access to any surface requires both the module_ticket pillar toggle to be enabled and the relevant permission grant on the user's role.
Support Agent
Works the Browse queue, replies with comments, changes status, and is assigned tickets to resolve.
Helpdesk Admin
Defines ticket types, groups, labels, and templates; manages assignment; edits or deletes tickets and comments.
Client Contact
Raises and follows their own tickets from the client portal; a client reply flips the ticket to client_replied.
Anonymous Requester
Submits from the public contact-support portal after verifying an email OTP — no login required.
Status legend
Every ticket carries exactly one status value in the tickets table. The five statuses below are the complete set the module uses. On the list table the status label is colour-coded, and on the ticket detail view it renders as a pill (status-new, status-open, and so on).
| Status | Meaning | How a ticket reaches it |
|---|---|---|
| new | A freshly created, un-worked ticket. On creation the module logs "Ticket marked as new". Unassigned new tickets show the Assign to me / Assign to user options. | Set implicitly when a ticket is first saved (create path in Tickets/save and the inbound channels). |
| open | The ticket is actively being worked. This is the only status in which the in-page comment composer is shown, and the only one from which Mark as On Hold / Mark as Closed are offered. | Mark as Open action, self/other assignment (tickets/assign_to forces open), or a staff comment on a new/closed ticket by the assignee/collaborator/admin. |
| on_hold | Paused / parked — waiting on something. Excluded from overdue calculations. No SLA due label is shown while on hold. | Mark as On Hold action from an open ticket (staff only). |
| client_replied | A client contact posted a comment, signalling the agent that the customer responded. Internal signal only. | Set automatically when a user whose user_type is client saves a comment on the ticket. |
| closed | Resolved. The module stamps closed_at with the UTC close time and logs the closure. Excluded from overdue calculations. | Mark as Closed action from an open ticket (staff only). |
Browse table opens with new and open pre-checked and on_hold / closed unchecked. If a ticket "disappears" after you close or hold it, it hasn't gone anywhere — tick those boxes in the Status multi-select to see it again.The ticket lifecycle
From first submission to resolution, a ticket flows through a predictable pipeline. Statuses can loop back — a closed ticket can be re-opened, and a client reply re-engages an agent at any point.
1. Create
A ticket is created via the Add Ticket modal (staff or client), the client portal, or an inbound channel. A ticket type is mandatory. On save the ticket is new, a consolidated "ticket created" summary comment is logged, and the description becomes the first comment.
2. Assign
Assignment can be automatic (a ticket type carries a default assigned_to user), manual (Assign to user or Assign to me), or by routing bucket (ticket groups). Assigning always forces the status to open and notifies the new assignee.
3. Respond & comment
Agents and clients converse through the comment thread. Each comment stamps last_activity_at. A staff comment on a new or closed ticket (by the assignee, a collaborator, or an admin) reopens it to open. A client comment sets client_replied.
4. On hold (optional)
When work is blocked, an agent marks the open ticket on_hold. Overdue tracking pauses while held.
5. Resolve / close
Mark as Closed sets closed, records closed_at, logs the closure, and fires a ticket_closed notification.
6. Reopen
A closed, held, or new ticket can be re-opened with Mark as Open; the module logs "Ticket reopened" and notifies via ticket_reopened. A fresh comment on a closed ticket by the assignee/collaborator/admin also reopens it automatically.
Status action matrix
The row-level and detail-view action menus are permission- and status-aware. The table below reflects the exact conditions in the controller.
| Action | Endpoint | Shown when | Resulting status |
|---|---|---|---|
| Assign to me | tickets/assign_to/{id} | staff, ticket is new and assigned_to = 0, with ticket_manage | open |
| Assign to user | tickets/assign_to/{id}/other_user | staff, ticket is new or open, with ticket_manage | open |
| Mark as Open | tickets/save_ticket_status/{id}/open | staff, ticket is new, on_hold, or closed | open |
| Mark as On Hold | tickets/save_ticket_status/{id}/on_hold | staff, ticket is open | on_hold |
| Mark as Closed | tickets/save_ticket_status/{id}/closed | staff, ticket is open | closed |
| Quick comment | tickets/quick_comment_modal_form | ticket is open, with ticket_comment | unchanged |
ticket_staff_update (they call save_ticket_status, which gates on it) and either ticket_manage or being the ticket's assignee. There is no direct "reassign to another ticket type" status transition — that happens through the edit modal, not the status endpoints.Creating a ticket
The Add Ticket button opens the modal served by tickets/modal_form and posts to tickets/save. What appears in the form depends on who you are and your permissions.
Staff see the full form: title (unless hide_ticket_title is on), optional client, Requested by, Collaborators, optional project reference, ticket type, description, and — depending on grants — Assign to (needs ticket_manage), a due date (needs override_ticket_due_date), and labels (needs add_ticket_label). Selecting a client dynamically reloads the requested-by, collaborators, and project dropdowns for that client.
- Selecting a ticket type shows its description as a tooltip (via
ticket_types/get_tooltip_description). - If no ticket type is available to you, the modal warns you and creating may conflict — request ticket type access from an admin.
- Files can be attached only at creation time (the dropzone is hidden when editing).
Client contacts get a trimmed form. Their client_id is forced to their own company, assigned_to is set to none, and Requested by is silently set to themselves. Clients cannot set an assignee, due date, labels, or collaborators. They need the ticket_create permission (creating) or ticket_update (editing) — see Permissions.
description field is only editable while creating. When you edit an existing ticket, the description (the opening message) is intentionally not shown — subsequent context belongs in comments, not the original request.Ticket types & SLAs
A ticket type classifies a request (for example "Bug", "Access request", "Billing"). Types are managed from the Types tab (tickets/view_types → the ticket_types controller). Every ticket must have a type, and the type drives the SLA, default assignee, points, and labels.
/ticket_typesticket_types/list_dataticket_types/modal_formticket_types/saveType fields
| Field | Purpose |
|---|---|
title | Display name of the type (required). |
duration | The SLA window, stored in seconds. The modal captures hours/minutes/seconds and multiplies to seconds (hours×3600 + mins×60 + sec). |
points | Effort/weight value shown next to the type (e.g. "3 points"). |
assigned_to | Default agent a new ticket of this type is auto-assigned to when no explicit assignee is chosen (and the ticket has no client). |
labels | Labels attached to the type itself (rendered beside the type in the queue). |
description | Free text shown as a tooltip on the create form. |
How the SLA due date is derived
The queue shows a due date and an OVERDUE flag using this precedence:
An explicit due_date (set on a ticket by a user with override_ticket_due_date) always wins. Otherwise, the effective due time is the ticket's created_at plus the type's duration seconds. A ticket is flagged overdue only if it is neither closed nor on_hold.
override_ticket_type unlocks all ticket types in the create/group pickers (bypassing the per-user allowed-types restriction). override_ticket_due_date reveals the due-date & time fields on the ticket form so an agent can set a custom SLA deadline independent of the type duration.Ticket groups
A ticket group is a routing/assignment bucket that bundles several ticket types together, so a queue can be filtered to (say) "Finance requests" or "IT requests" in one click. Groups live in the Groups tab and are managed by the ticket_groups controller.
ticket_groups/indexticket_groups/list_dataticket_groups/modal_formticket_groups/list_ticket_types_modalGroup fields & behaviour
| Field | Purpose |
|---|---|
title | Group name (required). |
description | Optional description. |
ticket_types_id | Comma-separated list of the ticket type IDs the group contains (required). The queue's group filter shows only tickets whose type is in this set (matched via FIND_IN_SET). |
accessible_by | Who the group is exposed to (rendered as info labels on the list). |
status | active / inactive — the group list defaults to showing active groups. |
The types count on the group row is a modal link (list_ticket_types_modal) that lists exactly which types are mapped, each shown with its duration and points. Filtering the ticket queue by a group narrows results to tickets whose type belongs to that group.
override_ticket_type.Templates & signatures (canned replies)
Templates are reusable comment/reply bodies. They speed up common answers and live in the Templates tab (visible to admins). Each agent also has a personal signature that pre-fills the comment composer.
Managing templates
| Action | Endpoint |
|---|---|
| List | tickets/view_templates / tickets/ticket_template_list_data |
| Add / edit form | tickets/ticket_template_modal_form |
| Save | tickets/save_ticket_template |
| View | tickets/ticket_template_view/{id} |
| Delete | tickets/delete_ticket_template |
A template has a title, a description (the body), an optional ticket_type_id binding, and a private flag. Templates with ticket_type_id = 0 apply to any type. Only the creator or an admin can edit or delete a template; a private template is visible only to its creator, while public templates are shared with all agents.
Inserting a template or KB article into a reply
Inside the open-ticket comment composer, staff with ticket_manage get two "insert" buttons:
Insert Template
tickets/insert_template_modal_form lists templates matching the ticket's type (plus type-agnostic ones). Clicking a row pastes its HTML body into the reply editor and closes the modal.
Insert Knowledge Base Content
tickets/insert_knowledge_base_content_modal_form lists your accessible KB articles (respecting knowledge_base_categories access). Clicking a row pastes the article body straight into the reply — a fast way to answer with an existing published answer.
Per-agent signature
The Signature button (tickets/settings_modal_form) opens a rich-text editor saved via tickets/save_settings to the user setting user_{id}_signature. When you open an open ticket, the comment box is pre-filled with your signature so every reply is consistently signed.
Collaborators
Collaborators are extra people looped in on a ticket beyond the requester and assignee. They are stored as a comma-separated list in the ticket's collaborators column and gated by the ticket_collaborator permission (which controls who can be picked as a collaborator).
Add / remove
On the ticket edit modal (staff), the Collaborators multi-select lets you add or remove people. Saving diffs the old vs new list.
Notifications
Added collaborators receive an added_as_a_ticket_collaborator notification; removed ones get removed_as_a_ticket_collaborator. Changes are also written to the ticket activity log ("Added collaborators: …" / "Removed collaborators: …").
Access & commenting
Being a collaborator grants access to a ticket even when the base ticket-type access wouldn't. Collaborators can post comments even without the blanket ticket_comment permission, and can be @mentioned.
Viewing the list
tickets/collaborators_modal and tickets/list_collaborators/{id} render the current collaborator roster with avatars.
Comments, mentions & activity log
The conversation thread on a ticket is a series of comments stored in ticket_comments. The composer is shown only while the ticket is open and you may comment.
Who can comment
You can post a comment if you hold the ticket_comment permission or you are the ticket's creator, requested-by, assignee, or a collaborator. Comments save via tickets/save_comment.
Comment features
| Feature | How it works |
|---|---|
| Attachments | Files are uploaded to a temp dir then moved to the timeline path; the serialized file list is stored in the comment's files column. Never clean/escape that serialized value. |
| @Mentions | Typing @ pulls the ticket's members (tickets/get_allowed_members_to_mention). Mentioned users receive a mentioned_on_a_ticket notification. |
| Quick comment | From the queue's row menu (open tickets), tickets/quick_comment_modal_form lets you reply without opening the ticket. |
| Edit comment | tickets/update_comment_modal_form → tickets/update_comment. Requires ticket_staff_update. Static/system comments cannot be edited. |
| Delete comment | tickets/delete_comment — requires the ticket_staff_delete permit (not admin-only). Static/system comments can never be deleted. |
| Auto-refresh | When allow_ticket_comment_auto_refresh is on, the thread polls tickets/get_latest_comments on an interval and can be paused. |
| Sort order | The show_recent_ticket_comments_at_the_top setting flips the thread to newest-first and moves the composer accordingly. |
Ticket activity log & flood control
Field-level changes (type, project, client, title, requester, assignee, collaborators, labels) are written to the ticket timeline as human-readable module log entries. On creation, instead of one log per field, the module posts a single consolidated "ticket created" summary comment to avoid flooding the timeline.
Separately, the ticket_logs table records submission events with the requester's IP; its submission_count query counts submissions from the same IP + user in the last 30 minutes for flood control on inbound/public submissions. That anti-abuse machinery is covered on Tickets — Email & Automation.
Labels & priority
Labels are lightweight coloured tags used for triage and priority signalling. A ticket's labels are stored as a comma-separated list of label IDs in the labels column and matched with FIND_IN_SET when filtering the queue.
- Adding labels to a ticket via the edit form requires the
add_ticket_labelpermission (the labels field is hidden otherwise). - Label definitions (name + colour) are managed through the shared Labels manager (
labels/modal_form) withtype=ticketfor ticket labels andtype=ticket_typefor labels attached to ticket types. - The queue's Label filter dropdown lets you narrow to a single label.
File attachments & the viewer
Attachments ride along with comments (and the first comment created from a new ticket's description). Uploads are storage-aware: files land in the configured timeline file path, which may be local or S3-backed, so the same viewer/download logic works in both.
Upload
The dropzone posts to tickets/upload_file (temp) and tickets/validate_ticket_file validates it; on save the files are moved to permanent storage and serialized into the comment.
Preview vs viewer
Images, video, PDF, TXT and CSS render inline in a lightbox. Documents like spreadsheets and .docx open the rich in-modal file viewer (tickets/file_viewer/{comment_id}?file_name=…) — spreadsheets via SheetJS, docx via Mammoth, text via highlight.js, with a download fallback.
Download
tickets/download_comment_files/{id} zips and downloads all files on a comment. Access to the viewer is validated against the parent ticket exactly like the detail view.
Submission channels
Tickets can enter the system three ways. All three land in the same tickets table and lifecycle, but the entry surface and identity handling differ.
Agents work tickets from the internal /tickets page. It's a tabbed panel: Browse (the live DataTable queue), Templates (admin), Types, and Groups. The Browse table supports status multi-select, filters (type, group, label, assigned-to, requested-by, collaborators, created date), an Overdue checkbox, a display toggle (picture/name), auto-refresh, and per-row action menus. Staff create tickets with the full modal and can assign, label, set due dates, and manage collaborators.
Client contacts see a trimmed ticket list rendered from the client area (clients/tickets/index). They can raise a ticket for their own company with ticket_create, follow the thread, and reply — a client reply flips the ticket to client_replied for the agent. Clients never see the client_replied label themselves, nor assignment/label/collaborator controls.
The public contact-support portal (the Ticketing Portal button on the queue) is an anonymous, marketing-styled form. The requester chooses a destination — the main platform or a specific tenant via a verified company key — picks a ticket type, writes a description, and, if not logged in, verifies their email with a one-time code before submitting. reCAPTCHA and flood control protect it. The full external + email pipeline is documented on Tickets — Email & Automation.
Routes & endpoints
Ticket routes are declared in the module's config/routes.php and merged into the core route table. The tickets/(:any) chains cover the multi-segment methods; only the notable linkable entry points are listed below.
| Route | Resolves to | Purpose |
|---|---|---|
/tickets | Tickets/index | Main tabbed ticket workspace (Browse / Templates / Types / Groups). |
tickets/view_browse | Tickets/view_browse | The live queue DataTable partial. |
tickets/view/{id} | Tickets/view/$1 | Single-ticket detail view with the comment thread. |
tickets/modal_form | Tickets/modal_form | Add/edit ticket modal. |
tickets/save | Tickets/save | Create/update a ticket. |
tickets/list_data | Tickets/list_data | DataTable JSON for the staff queue. |
tickets/save_comment | Tickets/save_comment | Post a comment (may change status). |
tickets/save_ticket_status/{id}/{status} | Tickets/save_ticket_status | Open / on_hold / closed transition. |
tickets/assign_to/{id}[/other_user] | Tickets/assign_to | Self- or other-user assignment (forces open). |
tickets/delete | Tickets/delete | Soft-delete a ticket and its comments. |
tickets/file_viewer/{comment_id} | Tickets/file_viewer | Rich in-modal attachment viewer. |
/ticket_types | Ticket_types/index | Ticket type management. |
ticket_types/save | Ticket_types/save | Create/update a type (duration in seconds). |
ticket_groups/index | Ticket_groups/index | Ticket group management (default routing dispatch). |
ticket_groups/list_ticket_types_modal | Ticket_groups/list_ticket_types_modal | Show the types inside a group. |
contact-support | public portal | Anonymous/external ticket submission (see email page). |
css route also maps to tickets/index and is preserved for backward compatibility — you can ignore it in day-to-day use.Permissions (RBAC)
These are the exact ticket-related permission keys the module surfaces to the Roles UI (from config/permissions.php). They appear only when module_helpdesk is enabled. Keys marked specific render an all/specific dropdown; the rest render an enable toggle.
| Permission key | Label | What it gates |
|---|---|---|
ticket specific | Ticket Type (Access) | Base access to the tickets pillar and which ticket types you may work (all vs specific). |
ticket_create | Create Ticket | Create a ticket type; also the client-side create-ticket permit. |
ticket_update | Edit Ticket | Edit a ticket type; also the client-side edit-ticket permit. |
ticket_delete | Remove Ticket | Delete a ticket type; also the client-side delete permit. |
ticket_staff specific | Ticket Staff (Access) | Which staff/members a user can see and act on across tickets (assignment/requester scope). |
ticket_staff_create | Create Ticket Staff | Staff permission to create a ticket from the internal form. |
ticket_staff_update | Edit Ticket Staff | Staff permission to edit a ticket, change status, edit comments. |
ticket_staff_delete | Remove Ticket Staff | Staff permission to delete a ticket and to delete comments. |
ticket_collaborator specific | Ticket Collaborators (Access) | Who can be selected as a collaborator on tickets. |
ticket_manage | Manage Tickets | Assign, edit, and run the full action menu; shows the assignee field and insert-template buttons. |
ticket_comment | Ticket Comments | Post comments on any accessible ticket (participants can comment without it). |
add_ticket_label | Add Ticket Label | Reveals the labels field on the ticket form so an agent can tag tickets. |
override_ticket_due_date | Override Ticket Due Date | Shows the custom due-date & time fields, overriding the type's SLA duration. |
override_ticket_type | Override Ticket Type | Unlocks all ticket types in pickers, bypassing the per-user allowed-types restriction. |
ticket_groups | Ticket Groups | Access to the Groups tab and group filtering. |
ticket_groups_create | Create Ticket Group | Create a new routing group. |
ticket_groups_update | Edit Ticket Group | Edit an existing group's title/types/access. |
ticket_groups_delete | Remove Ticket Group | Delete a group. |
module_ticket pillar sub-toggle is enabled and their role grants the matching permit. Turning off module_ticket (or the master module_helpdesk) hides the whole pillar regardless of role.Standard operating procedures
- Open
/ticketsand confirm the Browse tab shows new tickets (default filter). - Click the ticket ID or title to open the detail view; read the original description (first comment).
- If it is unassigned, use Assign to me (or Assign to user) — this moves it to open.
- Post your first reply in the composer. Your signature is pre-filled; use Insert Template or Insert Knowledge Base Content for canned answers.
- Attach any needed files to the comment (uploader lives in the composer, not the edit modal).
- Add labels for triage if you have
add_ticket_label, and loop in collaborators as needed. - Watch the SLA due date; if you can't resolve promptly, either keep working or set On Hold with a comment explaining why.
- From the queue row menu or the ticket's Actions menu, choose Assign to user (requires
ticket_manage). - Pick the target agent from the Select2 dropdown and save — the assignee changes and the ticket goes/stays open.
- The new assignee is notified (
ticket_assigned); the previous assignee, if any, is notified they were removed. - Add a collaborator instead if you want a specialist to advise without owning the ticket — they'll be able to comment and are notified.
- For cross-type routing, edit the ticket and change its Ticket Type; this may re-scope who can see it (an agent without access to the new type loses the row).
- Confirm the resolution in a final comment while the ticket is open.
- Choose Mark as Closed — the ticket becomes closed,
closed_atis stamped, and aticket_closednotification fires. - To reopen: tick closed in the status filter to find it, then use Mark as Open. The module logs "Ticket reopened" and notifies via
ticket_reopened. - Alternatively, when the assignee/collaborator/admin posts a new comment on a closed ticket, it reopens automatically to open.
Use cases
Internal IT desk
Employees raise "Bug" and "Access request" types; groups route them to the right agents; SLA durations flag overdue work in red.
Client support
Client contacts open tickets from the portal; a client reply surfaces client_replied so agents know a customer is waiting.
Public intake
Prospects and non-users submit via contact-support with email OTP verification — no account needed.
SLA tracking
Type durations (in seconds) drive due dates; the Overdue filter surfaces breaches, and On Hold pauses the clock.
Gotchas & troubleshooting
duration is seconds, not hours. The modal captures hours/minutes/seconds and converts. If SLAs look wildly off, verify the saved seconds value rather than assuming hours.The Browse queue defaults to showing only new and open. Closed and on-hold tickets are hidden until you tick them in the Status multi-select. Changing a ticket's type can also drop it from your view if you don't have access to the new type.
The composer only appears when the ticket is open and you may comment. If it's new, on hold, or closed, open it first (or use Quick comment from the row menu on open tickets). You must also hold ticket_comment or be a participant (creator/requester/assignee/collaborator).
Those fields are permission-gated: assignee needs ticket_manage, the due-date fields need override_ticket_due_date, and labels need add_ticket_label. Clients never see any of them. Ask an admin to grant the permit.
Your role's ticket access is specific and no ticket types are in your allowed list. Ask an admin to grant you the relevant types, or the override_ticket_type permit to see all types.
By design. The description is the original request and is only editable at creation. Add context through comments instead.
Comment deletion requires the ticket_staff_delete permit — it is not restricted to admins. Static/system-generated comments (like the creation summary) can't be edited or deleted by anyone.
Only images, video, PDF, TXT and CSS preview in the lightbox. Spreadsheets, .docx, and other documents open the rich in-modal file viewer, which always offers a download fallback.
Terminology
The classification a ticket must have. Carries the SLA duration (seconds), points, default assignee, and labels.
A routing bucket bundling several ticket types (comma-separated ticket_types_id) for one-click queue filtering.
An extra participant on a ticket (CSV collaborators column) who gains access and can comment even without the blanket comment permit.
A reusable reply body, optionally bound to a type and optionally private to its creator, inserted into the comment editor.
A per-agent block (user_{id}_signature) that pre-fills the reply composer.
A non-closed, non-held ticket whose effective due date (explicit due_date, else created_at + type duration) is in the past.
The person the ticket is raised on behalf of (requested_by), distinct from created_by and the assigned_to agent.