CMMS on CAP-686: UI sample vs built
Branch CAP-686 (with development merged) in backendApi, capExpertApp and dbMigrations, compared with the Captain V11 UI sample · checked 14 Oct 2026
Status: most of the CMMS tab is built and runs on real data. CAP-686 brings the CMMS tab back on Inventory and wires every surface to real API endpoints. Of the 17 pieces in the UI sample, 9 are built, 5 are partial and 3 are not built. There is one schema migration.
Before merge: regenerate the swagger client (the local copy is out of date, so the front end won’t compile without it), fix the bugs listed under Issues found, and add tests. The branch has none.
work_order_activities), 13 new columns on work_orders, and new columns on 3 other tablesAbout the UI sample. The Vercel site is a design sample, not built from our code. Every number in it is mock data. Here it is only the target design that our code is compared against.
On this page
- UI sample vs CAP-686, piece by piece
- Values
- Database · all tables
- API endpoints
- Front end
- Issues found
- The UI sample
- History
UI sample vs CAP-686, piece by piece
Built in place Built, differs in place with a deliberate difference Partial part of it Not built nothing yet
| # | UI sample piece | Status | On CAP-686 |
|---|---|---|---|
| 1 | CMMS tab under Inventory | Built | Visible to accounts with the existing camp.manage_inventory.biomed feature, which existing accounts already have. Deep link ?view=biomed; a work-order notification opens ?view=biomed&wo=<id>. |
| 2 | KPI tiles | Partial | Total Assets (OEM / HTM), Contract Coverage %, PM Overdue (within 30 / 60 days), High-Risk Assets (≥ 40%). The fifth tile is Open Work Orders instead of No WO History. Every tile drills down when clicked. All tiles count only assets that have a work order, not the whole fleet. |
| 3 | Warranty banner with Review coverage options | Built, differs | Also counts service contracts: “N OEM warranties and N service contracts ending in the next 90 days”. The button opens the asset list filtered to expiring coverage. |
| 4 | Assets / Work Orders toggle and the Grouped accordions | Partial | 4 groups: PM Overdue, High-Risk, HTM Covered, OEM Covered. There is no No WO History group. Each group shows 8 rows, then “+N more”. Assets list columns: Tag, Asset, Facility, Coverage, Coverage ends, PM, Replace. |
| 5 | Work order table: WO #, Asset, Type, Status, Stage, Assigned, Facility, Due | Built | All 8 columns, with the 3-dot stage. Server-side paging and sorting. |
| 6 | Saved views | Built, differs | All, Critical, Escalated, Assigned to me, This week’s PM, plus an extra Unassigned view. They are real filters: Critical means Urgent, Escalated, or overdue and not closed. Assigned to me means the signed-in user. |
| 7 | Type and Status filters, search, “Filtered by” chips | Built | Single-select header filters. Search covers WO number, description, asset, model, manufacturer, serial, assignee and facility. |
| 8 | Work order drawer | Partial | Built: stage stepper, asset tag and location (facility, floor, department, room), requester, details, PM schedule with the electrical safety result, SLA window, service contract, activity timeline, parts and materials, Add to calendar, Re-assign, Update Status. Uptime YTD is not built. “Last-seen location” is the asset’s assigned location, not a sighting. |
| 9 | Update Status | Partial | Status, stage, “Closed because” (completed / cancelled) and a note, which goes into the timeline. Of the quick actions, only Order part is built, and it only opens the drawer’s parts section. User manual and Intended use are not built. |
| 10 | New Work Order | Built, differs | Asset search (top 6), free-text asset, facility, type, priority, due date, description. It adds PM and contract cards, a warning when the asset already has an open WO, and a revenue-driver warning. Assigned To is optional: Unassigned, the 3 least-loaded techs, or any other tech from a paged list. Tech chips show open and active counts, not Light / Steady / Heavy. |
| 11 | Parts & POs | Not built | No screen; the old sub-tab was removed. The /parts-pos API is still there, and parts show read-only in the drawer. |
| 12 | Team Stats panel | Partial | SLA at risk, open / active / done this week, WOs closed per day over 7 days, tech cards, team performance (avg close time, first-fix rate, PM on-time, backlog) against fixed targets, and backlog by type. No Light / Steady / Heavy label. A Manage button for user managers. |
| 13 | Intake Asset | Built | All fields, with a floor → department → room picker. Creates a real inventory row. |
| 14 | Retire Assets | Built | Paste or CSV, up to 500 IDs. A live preview warns about an active contract, open WOs, recalls and overdue PM. Records retired_at / retired_by plus a history row. The “schedule a reminder” and “email the list” options are not built. |
| 15 | Generate Report | Built | All 6 reports on the existing report pipeline, in the same formats as the sample: Joint Commission (PDF, XLSX), DNV GL (PDF, XLSX), State Survey (PDF), 90-day PM compliance (PDF, XLSX, CSV), FDA recalls (PDF, XLSX), contract coverage (XLSX, CSV). |
| 16 | “Viewing as” role switcher | Not built | Access is controlled by the feature flag instead. |
| 17 | /cmms-dashboard | Not built | No page. Team Stats covers part of it: first-fix rate, PM on-time, average close time and team load. Missing: SLA compliance %, PM completion %, labor cost per hour (no labor data in the schema), and the action feed. |
| — | Asset check (nurse, read-only) | Not built | It was added on this branch and then removed. GET /cmms/asset-check remains, used only by New Work Order. |
| — | CMMS map (not in the sample) | Built | The Map view on the CMMS tab shows facilities whose assets have work orders. |
Values
The work-order values now match the UI sample. The migration converts any existing rows: Repair → CM, Calibration → IN, In Progress → Open at First Touch, Completed or Cancelled → Closed with a reason.
| Field | Values | vs UI sample |
|---|---|---|
| Type | PM, CM, IN, Install, Project, Other (default PM) | Same |
| Status | Open, Under Customer, Escalated, Closed | Same |
| Stage | Assigned, First Touch, Resolution (default Assigned) | Same |
| Priority | Urgent, High, Medium, Low | Same |
| Closed reason | completed, cancelled | New; the sample has none |
| Activity action | created, assigned, reassigned, stage_changed, status_changed, note, escalated, closed, reopened | Matches the sample’s timeline |
| WO number | WO-{accountId}-00001 | Sample uses WO-2401 |
| PO status | Ordered, Backordered, Delivered, Cancelled | Sample has no Cancelled |
Database
Full table changes and structures: CMMS tables →
One migration, 20261009104038-cmms-tab.js, runs in a single transaction and has a full down. It was applied cleanly to the local database on 14 Oct. No later development migration touches the risk functions it rewrites.
| Table | Change |
|---|---|
work_order_activities | New. The drawer’s activity timeline, append-only. |
work_orders | 13 new columns: stage, closer, requester, SLA and typed-in asset. Values converted as above. |
users | is_technician, technician_title, technician_capacity. Users who already have work orders assigned are flagged. |
service_contracts | coverage_source: oem, third_party or self. Empty means derive it from vendor vs manufacturer. |
camp_inventory_items | retired_at, retired_by. The risk function returns Retired for assets the bulk retire marks. |
parts_pos | Unchanged. vendor_name is still free text. |
work_orders (30 columns)
| Column | Type | Null | Default | Notes |
|---|---|---|---|---|
id | integer | no | serial | PK |
account_id | integer | no | FK → accounts | |
inventory_id | integer | yes | FK → camp_inventory_items (the asset), on delete set null | |
asset_name new | varchar(255) | yes | the asset as typed; the only reference when there is no inventory link | |
work_order_number | varchar(64) | no | unique per account | |
type | varchar(48) | no | 'PM' | PM, CM, IN, Install, Project, Other |
priority | varchar(24) | no | 'Medium' | Urgent, High, Medium, Low |
status | varchar(24) | no | 'Open' | Open, Under Customer, Escalated, Closed |
stage new | varchar(32) | no | 'Assigned' | Assigned, First Touch, Resolution |
assignee_user_id | integer | yes | FK → users (the technician) | |
opened_at | timestamptz | no | now | |
due_at | timestamptz | yes | ||
closed_at | timestamptz | yes | ||
closed_reason new | varchar(32) | yes | completed or cancelled | |
closed_by new | integer | yes | FK → users, on delete set null | |
requested_by_user_id new | integer | yes | FK → users, on delete set null | |
requested_by_name new | varchar(150) | yes | ||
requested_by_title new | varchar(150) | yes | ||
requested_by_email new | varchar(255) | yes | ||
requested_by_phone new | varchar(50) | yes | ||
sla_hours new | integer | yes | hours from opened_at until the SLA window closes (API accepts 1–720) | |
escalated_at new | timestamptz | yes | ||
escalation_target new | varchar(120) | yes | for example “OEM · Level 2 Tech” | |
description | text | yes | ||
notes | text | yes | ||
created_by | integer | yes | FK → users | |
updated_by new | integer | yes | FK → users, on delete set null | |
created_at, updated_at | timestamptz | no | now | |
deleted_at | timestamptz | yes | soft delete |
work_order_activities (new, 9 columns)
| Column | Type | Null | Default | Notes |
|---|---|---|---|---|
id | bigint | no | serial | PK |
account_id | integer | no | FK → accounts, on delete cascade | |
work_order_id | integer | no | FK → work_orders, on delete cascade | |
action | varchar(32) | no | created, assigned, reassigned, stage_changed, status_changed, note, escalated, closed, reopened | |
from_value, to_value | text | yes | the value before and after | |
note | text | yes | ||
actor_user_id | integer | yes | FK → users, on delete set null | |
created_at | timestamptz | no | now |
The analytics role reads both work_orders and work_order_activities per account only.
parts_pos (unchanged, 19 columns)
| Column | Type | Null | Default | Notes |
|---|---|---|---|---|
id | integer | no | serial | PK |
account_id | integer | no | FK → accounts | |
po_number | varchar(64) | no | unique per account | |
part_number | varchar(128) | yes | ||
description | text | yes | ||
vendor_name | varchar(255) | yes | free text; there is no vendors table | |
quantity | integer | no | 1 | |
unit_cost | decimal(12,2) | yes | ||
status | varchar(24) | no | 'Ordered' | Ordered, Backordered, Delivered, Cancelled |
ordered_at | timestamptz | no | now | |
expected_at, received_at | timestamptz | yes | ||
linked_inventory_id | integer | yes | FK → camp_inventory_items, on delete set null | |
linked_work_order_id | integer | yes | FK → work_orders, on delete set null | |
notes | text | yes | ||
created_by | integer | yes | FK → users | |
created_at, updated_at | timestamptz | no | now | |
deleted_at | timestamptz | yes | soft delete |
New columns on other tables
| Column | Type | Null | Default | Notes |
|---|---|---|---|---|
users.is_technician | boolean | no | false | shows the user in technician pickers and Team Stats |
users.technician_title | varchar(100) | yes | for example “Sr. Biomed Tech”; falls back to the job title | |
users.technician_capacity | smallint | no | 6 | open + active WOs that count as a full load; stored but not used by any endpoint yet |
service_contracts.coverage_source | varchar(20) | yes | oem, third_party, self | |
camp_inventory_items.retired_at | timestamptz | yes | when the CMMS retire archived the asset | |
camp_inventory_items.retired_by | integer | yes | FK → users, on delete set null |
Reused, not new
- PM schedule: the PM columns on
camp_inventory_items(pm_schedule,pm_interval,pm_status,pm_vendor_name,last_service_date,next_service_dateand others). PM Overdue usesnext_service_date. - Location: the existing location, floor, department and room data. Asset tag: the asset’s QR code or biomed ID.
- Service history:
service_histories, imported from customers’ own CMMS exports. - Retire audit: one
inventory_historical_recordsrow per retired asset.
API endpoints
All are in the primary role, behind sign-in plus the camp.manage_inventory.biomed feature. All are backed by the database; none is stubbed.
Work orders
| Endpoint | On this branch | What it does |
|---|---|---|
GET /work-orders | changed | Paged list: search, saved view, type, status, stage, priority, assignee, facility, due range, sorting |
POST /work-orders | changed | Create. Typed-in asset, requester and SLA fields. Safe numbering under a per-account lock |
GET /work-orders/:id | new | Drawer data: requester, SLA, asset and location, PM, contract, parts, timeline (newest 200) |
POST /work-orders/:id/status | new | Change status (and stage), with a closed reason and a note |
POST /work-orders/:id/assign | new | Assign or reassign a technician; sends a notification |
POST /work-orders/:id/stage | new | Change the stage |
POST /work-orders/:id/notes | new | Add a note to the timeline |
PATCH /work-orders/:id | changed | Edit fields |
POST /work-orders/:id/close | changed | Close as completed |
CMMS (all new)
| Endpoint | What it does |
|---|---|
POST /cmms/summary | KPI tiles, banner counts and group totals |
POST /cmms/assets | Asset list or one group’s rows, paged |
POST /cmms/map | Facilities for the map view |
POST /cmms/assets/search | New Work Order’s asset search (top 6) |
GET /cmms/team-stats | Team Stats panel |
GET /cmms/technicians | Technicians with open / active / done, least loaded first, paged |
GET /cmms/technicians/candidates | Users who can be made technicians (user managers only) |
GET /cmms/technicians/:userId | One technician |
PUT /cmms/technicians/:userId | Turn technician on or off and set the title (user managers only) |
POST /cmms/intake | Create an asset from the Intake dialog |
POST /cmms/retire/preview | Match up to 500 IDs and return warnings |
POST /cmms/retire | Retire the matched assets and record the audit |
GET /cmms/asset-check | One asset’s PM, contract, open WOs, recalls and risk, for New Work Order |
Parts POs and reports
GET/POST /parts-pos,PATCH /parts-pos/:id,POST /parts-pos/:id/receive: unchanged apart from the feature guard and status enum. The UI doesn’t use them.- 6 CMMS reports are registered on the existing report pipeline, so there is no new route. The only parameter is facility scope.
- The older
/manage-inventory-v2/technicians,/vendor-performanceand/pm-scheduleroutes are still there. The UI no longer uses them, and/techniciansoverlaps/cmms/technicians.
Front end
- Where:
src/app/modules/manage-inventory/cmms/, holding the tab, 9 components, 7 dialogs, 4 services andcmms.model.ts. There are no new routes. - Dialogs: New Work Order, Update Status, Re-assign, Intake Asset, Retire Assets, Report, Manage technicians.
- Header buttons: shown in full from 1400 px wide, in a “⋯” menu below that.
- Technicians: flagged in user management (a Technicians filter and a Tech chip). The CMMS settings block is on the user form.
- Removed: the old PM Schedule, Analytics (vendor performance) and Parts & POs sub-tabs, the old New Work Order dialog, and the “coming soon” PM export.
- Data: every number comes from the API. There are no mock arrays, no TODOs and no lint suppressions in the diff.
- Docs:
docs/cmms/README.mdplus nine plan files.
Issues found
| # | Issue | Effect | Where |
|---|---|---|---|
| 1 | Swagger client is out of date | The front end imports CMMS client code that isn’t generated yet. It won’t compile until npm run swagger runs against a CAP-686 API. | capExpertApp src/swagger |
| 2 | No tests | No spec files for cmms, work-orders or parts-pos. One comment even refers to a spec that doesn’t exist. | backendApi |
| 3 | No WO History is always 0 | Every CMMS query first keeps assets that have a work order, then this count asks for assets with none. The UI no longer shows it; the API still returns it. | cmms.service.ts:214, :266; cmms.assets.sql.ts:32-33 |
| 4 | KPIs cover only assets with a work order | A new account, or one with few WOs, sees near-empty tiles, banner and groups. Coverage % and PM Overdue are not for the whole fleet. | cmms.service.ts |
| 5 | A search of only spaces breaks the SQL | POST /cmms/assets/search with q = “ ” builds an empty WHEN and errors. | cmms.service.ts:459-472 |
| 6 | DNV GL report total is mislabelled | The count of overdue assets appears under “Days Overdue”. | dnv-pm-compliance.report.ts:66 |
| 7 | Status, assign, stage and note changes are not in a transaction | A failure can leave the change without its timeline row, or the row without the change. | work-orders.service.ts:209-284 |
| 8 | Retire stamps every matched ID | Assets the bulk retire skipped still get retired_at and a history row, so “retired” can overcount. Retired components get no stamp. An ID shared by two assets matches only the first. | cmms.service.ts:663-728 |
| 9 | Team Stats rules | “SLA at risk” means Escalated or due today: it ignores sla_hours, which the drawer uses, and already-overdue rows. Only the first 20 technicians are listed. | cmms.service.ts:533-569 |
| 10 | PO numbering can collide | It is still the count plus one with no lock (work orders now lock). Unchanged on this branch. | parts-pos.service.ts:170-173 |
| 11 | Intake can create a manufacturer | A free-text manufacturer becomes a new global manufacturer row when no “Other” exists. | cmms.service.ts:629 |
| 12 | Leftovers | The unused PartsPosDataService and five unused data-service methods. Stale comments: “five-bucket”, “No WO History” in the tour, “demo” mentions. Hardcoded team targets (5 days, 75%, 90%, 8). | capExpertApp, backendApi |
The UI sample
Captured from the Vercel UI sample on 14 Oct 2026. It is a design reference, not our code. Its data is mock, and some of its buttons do nothing.










History
| Date | Ticket | Change |
|---|---|---|
| May 2026 | CAP-15 | First CMMS: work_orders and parts_pos tables, their APIs, and a CMMS view with four sub-tabs |
| 12 May 2026 | CAP-2003 | CMMS tab hidden “until the feature is rebuilt” |
| Oct 2026 | CAP-686 | The rebuild: schema migration, CMMS and work-order APIs, six reports, the new CMMS tab, drawer and dialogs, technicians in user management. development merged in. |