Damage Cases
Tracking property damage and warranty claims tied to a job — what a case captures, how cost rolls up, and a real current limitation in how it connects to a client record.
On this page
What a damage case is
A Damage Case is a standalone record for tracking either property damage caused during a job (e.g. a mower striking a client's sprinkler head or fence) or a warranty claim (e.g. a plant that died within its warranty period and needs replacement). Both share the same record shape — a caseType field of "damage" or "warranty" — so the feature is really one case-tracking model with two flavors, not two separate features.
It is not attached to a vehicle-accident or fleet-incident workflow — there is no field for at-fault party, insurance carrier, or claim number anywhere in the schema. What it captures is closer to "an incident happened at a customer's property, here's what it cost to make right."
Where cases live
Damage Cases is a Landscapt tool, not a CMMS feature — it's listed in the crew-hidden Tools sidebar as "Track property damage & warranty". It lives at a single route, /tools/damage-cases; the older /dashboard/damage-cases and /dashboards/damage-cases copies now redirect there.
The page itself has two tabs: Cases (a searchable list — by customer, case #, description, or property address) and Reporting (a chart of year-to-date damage vs. warranty cost by month, with YTD summary tiles).
Opening a case
Cases are opened standalone, from the "Open Case" button on the list page — there is no "log damage" action hanging off a Job, Visit, or Ticket record that pre-fills a case. A crew member or office staffer fills out a plain form:
| Field | Required? | Notes |
|---|---|---|
| Case Type | Required | “Damage” or “Warranty” — a two-value select, defaults to Damage. |
| Date of Incident | Required | Date picker. |
| Find Client | Optional | Search-and-select that fills the name (and address, when blank) below — the case still stores the name as text, see the limitation below. |
| Warranty | Optional | Warranty cases only — links the case to one of your active warranties, fills in the customer, and warns when the incident date is outside its coverage. See the Warranties guide. |
| Customer / Property Name | Required | Free text, so a customer who isn’t a client can be typed directly. |
| Property Address | Optional | Free-text. |
| Description | Required | Textarea. Placeholder copy changes with the selected Case Type. |
| Photos | Optional | New cases only. Take Photo opens the device camera; Choose Photos picks from the gallery (HEIC photos are converted automatically). They upload to the case right after it is created. |
| Resolution Notes | Edit only | Only shown once a case exists — not on initial creation. |
A case number is assigned server-side via the next_damage_case_number() RPC. Creation retries up to three times on a unique-constraint collision (two concurrent submissions computing the same number), rather than surfacing a raw DB error.
Worked example. A mower clips a client's sprinkler head while mowing a back yard. The crew lead (or office, once told) opens a new case: Case Type Damage, Date of Incident set to today, Customer / Property Name typed as "Sterling Storage", Description "Mower struck and cracked a sprinkler head on the west lawn near the fence line." The crew lead snaps a photo of the cracked head on the form, and it lands on the case's Files tab (see below). The case is now visible in the list with status Open and $0.00 total cost until an expense is logged.
Reporting from the field
Crew and anyone with Job Photos access can also open a case without going near the Tools menu: Job Photos → Field → Damage Report is a phone-friendly form with the same fields (report type, date, customer / property, address, what happened). Submitting it opens a new case with the same case number, ticket and automation behavior as the form above. The field form also has the Find Client search and a Take Photo / Choose Photos control; if a photo fails to upload, the case is still created and the form says how many photos need to be added by the office. Employee injuries are tracked separately — see Injury & Near-Miss Cases.
Status
Status is a plain four-value field, changed from a dropdown on the case's status badge — any status can be selected from any other at any time; there is no enforced progression or guard rail in the UI or the mutation.
| Status | Meaning |
|---|---|
| Open | Default status on every new case. |
| In Progress | Being worked — no system-enforced meaning beyond the label. |
| Resolved | Has a dedicated color in the shared StatusBadge palette (teal). |
| Closed | End state. |
Expenses & cost tracking
A case's Total Cost is not a field you set directly — it's derived by summing every non-deleted row in damage_case_expenses for that case, both in the list view and the detail panel.
Continuing the sprinkler-head example: the case's Expenses tab gets an entry — Date, an optional Vendor (picked from the shared Vendors table, or typed free-text if the vendor isn't in the system yet), a Description ("Replacement sprinkler head + labor"), and an Amount in dollars, stored as cents. The case detail panel's Total Cost updates immediately to match.
Each expense row also carries a nullable purchaseOrderId, but the Add Expense form always submits it as null — there is no UI control to set it per-expense today. The only PO link that's actually wired up in the UI is the one at the case level, described next.
Beyond expenses, each case has generic Files, Comments, and Audit Trail tabs shared with other record types across the app (AttachmentsSection, CommentsSection, AuditTrailTab). Files accepts images, PDFs, and common office documents. Photos added when the case was opened, or uploaded later, show as a thumbnail Photos strip at the top of the Files tab; click one to open it full size.
Tickets
Every new case automatically opens a ticket for the office to follow up on, whether it was created from the Open Case dialog or the crew's field form. The ticket is Open, normal priority, in the Damage / Warranty category, with the case number, customer, address, incident date and description in its body, and it is linked back to the case. If the customer name matches exactly one client, the ticket is attached to that client and appears on their activity timeline; otherwise it is created without a client.
The case's Tickets tab lists every ticket linked to it (number, subject, status); click one to open it. You can link more tickets yourself: on the ticket, use Linked Records → Add Link, choose Damage Case, and pick the case.
Linking a Purchase Order
A case can be linked to a single existing Purchase Order via a searchable picker (by PO number or vendor) in the detail panel's header strip, and unlinked with one click. This writes to damage_cases.linkedPoId — it does not create a new PO or requisition from the case, and a case does not spawn a repair Work Order either. If parts or materials are needed to fix the damage, that PO has to already exist (or be created separately in Equipt's PO module) before it can be linked here.
Automations & Zapier
Creating a case fires a damage_case_created event, usable two ways:
- Landscapt's internal Automations — "Damage case was created" is a selectable trigger, filterable by Case Type.
- Zapier — listed as the "New Damage Case" trigger, delivered instantly like the rest of Landscapt's triggers. See the Zapier guide for connection details.
customerName is free text (see below), case creation does an exact-match lookup against clients.display_name (trimmed, lowercased) purely to decide whether to fire the trigger — nothing is written back onto the case either way. If the typed name doesn't match a client exactly, the automation and Zapier trigger silently never fire for that case. A typo, an abbreviation, or a property name instead of the client's name on file is enough to break this.The client-linking limitation
damage_cases.customer_name is a plain text column — there is no foreign key to clients.id. This is confirmed still true directly in the code, not just in CLAUDE.md's general note about informal client-name strings: the type definition has a customerName: string field and no clientId, the Find Client picker on the New Case form only copies the client's display name (and address) into the text fields, and case creation has to run an exact-match text lookup against the clients table after the fact just to fire automations — a workaround that only exists because there's no real relationship to query.
display_name exactly. Per CLAUDE.md, migrating this to a real client_id FK is deferred until after the Landscapt dev/prod split — until then, treat the customer name field as a label, not a relationship.