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. |
| Customer / Property Name | Required | Free-text field, not a client picker — see the limitation below. |
| Property Address | Optional | Free-text. |
| Description | Required | Textarea. Placeholder copy changes with the selected Case Type. |
| 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." A photo of the cracked head can be attached afterward from 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.
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, so incident photos go here — there is no dedicated photo field on the case itself.
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 New/Edit Case form is a plain text Input with placeholder "e.g. Sterling Storage", not a client picker, 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.