Documentation
How data flows between sections
Five different parts of the product create work, and they all converge on one record — the work order. Follow that spine and you can explain almost any number in the system.
On this page
The work-order spine
Whatever the origin, the created work order carries a source field naming
where it came from, a location_node_id, usually an
asset_id, and a generated organization-scoped number. From that point
every module reads the same record.
1. Request to work order
The most common path, and the one to demo first.
Submission — requester portal
A staff member creates a ServiceRequest against a location
node and optionally an asset, with a category, description and priority.
Status submitted. Followers can subscribe to it.
Triage — operations panel
A supervisor approves or rejects. Every decision is written as a
ServiceRequestTriageEvent; clarification questions to the
requester are RequesterMessage records visible on both
sides.
Conversion — the hand-off
Only an approved request converts. The action copies
organization, priority, location and asset onto a new work order with
type=corrective and
source=service_request, sets the request to
converted, and links them one-to-one. Re-running it returns
the existing work order rather than creating a second one.
Assignment — dispatch board
The supervisor assigns a technician, team or vendor and a planned window.
Status moves to assigned and the work order appears in that
technician's workbench and in the mobile app's changes feed.
Execution — workbench or mobile
Start, pause and resume are recorded with timestamps; paused minutes accumulate as hold time and are excluded from actual duration. Tasks, readings, notes, before/during/after photos and field evidence all attach to the work order. Blockers escalate to the supervisor.
Completion, approval, closure
Completion requires an action, a root cause, notes and whatever evidence the completion rules demand. The supervisor approves, then closes. Reopening creates a new history event rather than rewinding the old one.
Back to the requester
Resolution marks the originating request resolved. The
requester confirms it (status confirmed) or reopens it
(status reopened), and can leave or withdraw feedback. That
is the loop closed.
2. Preventive maintenance
A PmSchedule targets an asset or a location node and carries a recurrence
— daily through annual, custom, or meter-based — plus an optional checklist version
and job plan.
- The recurrence calculator produces the next due date. Meter schedules instead watch asset or fleet readings and fire when the interval is crossed.
- Generation is keyed by
(schedule, due key)and recorded inpm_schedule_generationswith a unique constraint, so a retry, a double-click or two concurrent workers all collapse to a single work order. - The generated order has
source=pmand inherits the schedule's target, team and job plan tasks. - Advancing the schedule after generation depends on its completion policy:
fixedkeeps the original cadence,from_completionre-bases the next due date on when the work actually finished. - PM compliance is measured as generated-versus-completed-on-time and surfaces in reports.
3. Inspections and corrective work
Template → version
Checklist templates are edited freely, then published into an
immutable ChecklistTemplateVersion. Inspections always bind
to a version, so editing a template never rewrites history.
Schedule → run
An inspection is scheduled against a location or asset and an inspector.
Starting it opens a ChecklistRun; each answered item becomes
a ChecklistRunResponse with a result of pass, partial, fail
or not applicable.
Score and evidence
The scorer computes a weighted result live, so the inspector sees the
outcome before submitting. Photos and a signature attach as
InspectionEvidence.
Completion raises work
On completion, every failed item becomes one corrective work order,
keyed idempotently by inspection and item position and linked through
InspectionCorrectiveWork. The failed item's prompt becomes
the work summary.
Triage → verification → evidence
Compliance triages failed items, and once the corrective work order is
closed a verifier accepts or rejects the fix as a
CorrectiveVerification. Accepted verifications and the
inspection report feed ComplianceEvidence against an
obligation.
4. Fleet triggers
Fleet vehicles are their own register but they borrow the maintenance spine. Recording
an odometer or engine-hour reading, logging a defect, failing a checkout inspection
or reporting an accident can raise a work order with
type=fleet_maintenance and a source such as fleet_mileage
or fleet_breakdown. Breakdowns and accidents are raised at
urgent priority; the rest at medium.
Each trigger is stored with a trigger_key so the same 10,000 km
milestone can never raise two orders. The vehicle must have a location node —
that is what puts fleet work on the same board as building work.
5. Inventory and cost
- Reserve ties stock to a specific work order before the technician travels.
- Issue consumes it — against the reservation if there is one — and writes
an immutable
StockLedgerEntrythat names the work order. - Return, receive, adjust and transfer are all further ledger entries; balances are derived, never stored as an editable number.
- Issued parts plus approved labor entries are what the cost calculator rolls up onto the work order, and from there onto the asset's lifetime cost.
- Falling below an item's reorder point produces a
ReorderProposal; stocktake sessions reconcile counted against ledger quantities.
6. Mobile and the API
The Flutter app never talks to the web UI; it uses /api/v1 with a Sanctum
token and mirrors the same lifecycle.
| Concern | How the API handles it |
|---|---|
| Sync | GET /work-orders/changes returns what changed since a
cursor, so a device coming back online catches up rather than refetching.
|
| Duplicate submissions | Every mutation runs behind an idempotency key, so a retried start/complete applies once. |
| Stale offline edits | A staleness check rejects a mutation made against a version of the record the server has since moved past. |
| QR scanning | GET /assets/scan/{token} resolves a printed asset label
straight to the asset and its location path. |
| Evidence | POST /work-orders/{id}/evidence uploads photos under a
separate upload rate limit. |
7. Tracing one record end to end
When a UAT participant asks "where did this come from?", follow the chain:
ServiceRequest --(converted)--> WorkOrder.service_request_id
PmSchedule --(generation)--> PmScheduleGeneration.work_order_id
Inspection --(failed item)--> InspectionCorrectiveWork.work_order_id
FleetAsset --(trigger)-----> FleetMaintenanceTrigger.work_order_id
WorkOrder
├─ tasks, labor entries, notes, photos, field evidence, blockers, permits
├─ history events (who moved it, when, from which status)
├─ stock ledger entries (which parts, from which store)
├─ follow-up work orders (parent_work_order_id)
└─ audit events (immutable, exportable)
Every one of those links is a real foreign key you can check in the panel, in the API
response, or with make psql during UAT.