Tickets
Support and service tickets — where they come from, how they're worked, and how they connect to the rest of a client's record.
On this page
What a ticket is
A ticket (crm_tickets) is Landscapt's general-purpose customer-service record — a note, call, or event tied to a client that needs to be tracked, assigned, and resolved. Every ticket has a sequential ticket_number, a status, a priority, an optional category, and an optional client and assignee.
Tickets are shown as three different lists in the app — Tickets, Calls, and Events — but they're all the same underlying table and component (TicketsList), just filtered by type (note / call / event).
Not the same as CMMS Maintenance Requests
Status, priority & type reference
These are literal string enums, not free text — the UI only ever writes one of these.
| Status | Value | Meaning |
|---|---|---|
| Open | open | Default status on every new ticket — including every ticket a public form creates. |
| On Hold | on_hold | Waiting on something outside your control (a callback, a part, a client reply). Counts as the same “Open” stage on the status flow indicator. |
| Pending | pending | Being actively worked — the middle step on the status flow (“In Progress”). |
| Closed | closed | Resolved. Sets closed_at and fires the Ticket Closed trigger (Zapier + internal automations). |
The detail sheet's status flow indicator only shows three visual steps — Open, In Progress, Closed — because on_hold maps to the same step index as open. Under the hood it's still a distinct, filterable status.
| Priority | Notes |
|---|---|
| Low | Shown with no badge in the list view — only High and Urgent get a colored pill. |
| Normal | The default on every new ticket, including form-created ones. |
| High | Orange badge in the list. |
| Urgent | Red badge in the list. |
| Type | Notes |
|---|---|
| Note | The default type — a written ticket. What Forms always create. |
| Call | Same record shape, filtered into a dedicated Calls list (typeFilter="call") elsewhere in the app. |
| Event | Same record shape, filtered into an Events list (typeFilter="event"). |
Category is a free-form org-configured list (crm_list_options under key ticket_categories), not a hard-coded enum. If your org hasn't configured any, the New Ticket dialog and edit form fall back to a default set: Uncategorized, Estimate, Billing, Change Service, Complaint, Other.
Creating a ticket
Three ways a ticket comes into existence:
- Staff, manually — the Add Ticket (or Add Call / Add Event, depending on which list you're on) button opens
NewTicketDialog, letting staff set type, client, category, subject, body, status, priority, assignee, and due date directly. - A public form submission, automatically — every submission on a
crm_formsform creates a ticket, no matter what else the form does or how it handles the matched/created client. The ticket always starts asstatus: "open",priority: "normal",type: "note", withcategoryset to the form's own name and the body built from the submitter's name/email/phone/message plus every raw field on the form. - A Communication Automation Event action — the Ticket action step lets an automation sequence create a ticket with a configurable title, description, priority (Low/Normal/High/Urgent), and assignee whenever it runs.
Bulk creation is also available via CSV import (Actions menu → Import), which requires only a Subject column.
Assignment
A ticket can be assigned to exactly one user at a time, via assigned_to (a display-name string) and assigned_to_id (a real crm_employees.id, resolved from the assignee-picker's selected employee). Both are set together whenever the UI's Assigned To dropdown is used — in the New Ticket dialog, the edit form, or the list's bulk Reassign action (select one or more rows → Actions → Reassign).
resolveAssigneeId() in src/lib/ticket-notify.ts turns that crm_employees.id into a login user for notifications by looking up crm_employees.user_id. Tickets created before assigned_to_id existed, or created through a path that never had a real id (the public form path always leaves the assignee blank; the Zapier "Create Ticket" action has no assignee field either), only have the name string — resolveAssigneeId() falls back to fuzzy-matching that name against crm_employees so assignment notifications still work — but if the name doesn't match an employee exactly, assignment notifications are silently skipped rather than failing.Worked example: a form-to-close lifecycle
- A visitor submits a contact form on your public site.
submitFormResponse()matches or creates a client, then unconditionally inserts acrm_ticketsrow: open, normal priority, type note, category = the form's name, body = name/email/phone/ message plus every raw field. notifyStaffOfNewTicket()runs immediately — it notifies the org's configured New Ticket recipients (no assignee yet, so no one else is added).- Because the ticket has a client,
ticket_createdfires for any Communication Automation sequence watching that trigger (optionally filtered to this form's category), and for any Zapier "New Ticket" trigger. - A manager opens the ticket from the Tickets list, clicks Reassign (or edits the ticket directly) and assigns it to a rep. This sets both
assigned_toandassigned_to_id, and fires aticket_assignednotification to that rep alone. - The rep works the ticket — adds comments (Comments & History tab, backed by
CommentsSection), which notify the assignee if someone else comments, and sets status to Pending while they investigate. - The rep resolves it and clicks Close Ticket (
useCloseTicket). This setsstatus: "closed"andclosed_at, then firesticket_closedfor both internal automations and Zapier. - If the client later replies and staff change the status away from Closed,
useUpdateTicketdetects the prior status was closed and firesticket_reopened.
The ticket detail sheet
Clicking any ticket row opens TicketDetailSheet, with five tabs:
- Details — status flow buttons, the client/category/assignee/dates meta grid, body text, and a Linked Records picker that can attach the ticket to an Estimate, Invoice, or Job belonging to the same client.
- Comments & History — threaded comments via the shared
CommentsSection(recordType="ticket"). - Files — attachments via the shared
AttachmentsSection. - Contributors — additional staff CC'd on the ticket as participants, distinct from the single assignee. Managed by
use-ticket-contributors.ts. - Audit Trail — the shared
AuditTrailTabchange history.
The header also has a PDF button that opens a print-formatted view in a new window — all ticket fields are HTML-escaped before being written into that window, since subject/body/names can originate from an unsanitized public form submission.
Client link & activity timeline
client_id is nullable — a ticket doesn't strictly require a client — but whenever one is set at creation, useCreateTicket also inserts a matching client_activity row (activity_type: "ticket") so the ticket shows up on that client's unified Activity Timeline alongside notes, calls, invoices, and estimates.
A ticket created from a public form submission doesn't go through useCreateTicket — it's a direct server-side insert in submit-form-response.ts — but it logs the same shape of client_activity row (activity_type: "ticket", linked via ref_id/ref_table), so it shows up and deep-links on the timeline exactly like a manually-created ticket does.
A ticket can also carry an SMS-consent warning: if a form submission checked SMS consent but captured no phone number, sms_consent_pending_phone is set true and the detail sheet shows a dismissible amber banner until a phone number is collected and the warning is cleared.
Field upsells from crews
When a crew spots work a property needs, they send it in from the crew app and it arrives here as an ordinary ticket with the category Upsell — so it inherits assignment, due dates, priority, comments, automations and the audit trail without being a separate thing to check.
The crew's side of this — where the Suggest work button lives and what they see — is in the Crew App guide. Each one carries the crew's note, who spotted it, the date, the property, and the service they suggested. Any photo they took is on the Files tab, which is usually enough to price the work without a trip out to look. The ticket is linked back to the job the crew was on.
To take one forward, click Create estimate on the ticket. That makes a draft estimate for the client, puts the suggested service on it at your catalog rate, links the two together and opens the estimate ready to price — so there's nothing to retype and nothing to remember to link afterwards. The button needs the Add Estimate permission, the same one every other way of creating an estimate requires, and it disappears once the ticket has been converted.
Re-quoting is still fine. You can link additional estimates by hand from Linked Records — useful if you quoted the work before the suggestion came in, or the client asked for a revised price. The reports read the most recent estimate linked to the ticket.
That link is what turns the suggestion into a number you can report on: the estimate carries the real sales pipeline (Sent → Accepted → Won/Lost), so a crew's suggestion reaching Won is measurable, while the ticket's own open/closed only ever means whether the office has dealt with it.
Two reports in the Report Center read this: Field Upsells lists every suggestion with its outcome, and Upsell Conversion by Crew Member totals what each person submitted against the revenue it won. Both are under the Service section, and each has its own permission so you can decide who sees them.
Automations, Zapier & notifications
Communication Automations can trigger on four ticket events:
| Trigger | Fires when… |
|---|---|
| Ticket was created | Fires when any ticket is created with a client attached — includes form-created tickets, since submit-form-response.ts links the ticket to the matched/created client. |
| Ticket was closed | Fires when a ticket's status changes to closed, from useCloseTicket or a bulk “Mark Closed” action. |
| Ticket was reopened | Fires when an update changes status away from "closed" to anything else — i.e. the ticket's prior status was closed. |
| Ticket past due | Not a moment-it-happened event — evaluated against due_date via the ticket_past_due_days condition field (days since due date). |
The same events are exposed to Zapier as instant triggers — New Ticket and Ticket Closed — and Zapier can also create tickets via the Create Ticket action (requires Client + Subject; accepts Body, Priority, Category, Type, and Due Date). See the Zapier guide for the full trigger/action reference.
Three notification preferences exist per user, each with separate email and in-app toggles:
| Preference | Keys | Who receives it |
|---|---|---|
| New Ticket | inAppNewTicket / emailNewTicket | Sent to the org's configured “New Ticket Recipients” plus the resolved assignee (if any), excluding whoever created the ticket. Also fires for anonymous form submissions (no creator to exclude). |
| Ticket Assigned | inAppTicketAssigned / emailTicketAssigned | Sent only to the newly-assigned user — skipped if they assigned it to themselves. |
| Ticket Comment | inAppTicketComment / emailTicketComment | Sent only to the resolved assignee — skipped if the assignee is the one commenting. |