# Technical Specification — Breakdown Reporting Module

**GPS Renewables · Operations & Maintenance Technology**

| Field | Detail |
|---|---|
| Version | Draft v0.1 |
| Derived from | `Breakdown Reporting.dc.html` (client-side prototype: template + logic class, in-memory state, seeded mock data) |
| Companion document | SRS — Breakdown Reporting Module v0.1 (functional requirements) |
| Date | 23 August 2026 |

> This spec describes the **current prototype's implementation** and, in §7, what changes when it becomes a real, persisted, multi-user system. It is written for an engineer picking up the build, not as a from-scratch architecture proposal.

---

## 1. Architecture overview

The prototype is a single client-rendered screen with four top-level views (Dashboard, Breakdowns list, PIR detail, RCA Approvals, General Tasks) plus five modals, all driven from one in-memory state object. There is no backend: all data is seeded, all writes are local `setState` calls, and all users are simulated via a "View as" role switch in the top bar. There is no routing — `state.screen` selects the visible view; there is no persistence — a page reload resets to the seed data.

**Layout shell:** fixed 212px sidebar (nav: Dashboard, Breakdowns, RCA Approvals with pending-count badge, General Tasks) + top bar (site chip, search, role switcher, current-user avatar) + scrollable main content. Same visual language as the Maintenance Platform build (Inter, `letter-spacing:0.5px`, `#5fa414` primary, pill buttons, 8px-radius cards, MDI icons via CDN).

---

## 2. State model

Top-level state (`Component.state`):

```
role, screen, pirId,              // active role (simulated identity) + navigation
q, fStage, fSev, fType, mine,     // list filters
sortKey, sortDir,                 // list sort
pirs: [ PIR ],                    // all breakdown records
tasks: [ GeneralTask ],           // all general tasks (cross-PIR)
feed: { [pirId]: [LogEntry] },    // activity log, keyed by PIR id
modal, noteOpen, noteText, toast, stageMenu, cancelText, taskWho,
form: {...}, immText, immRca, actForm: {...}, decideMode, decideText   // transient modal/form state
```

### PIR record shape

```js
{
  id, title, equip, type, sev, cbg,             // report basics
  when, raised, sic, eng,                       // reported time, shift-in-charge (reporter), assigned engineer
  stage,                                        // one of STAGES[].key, or 'cancelled'
  immHrs,                                       // hours-to-immediate-action (for the dashboard KPI)
  desc,
  immediate, immBy, immAt,                      // immediate action text + author + time
  rcaReq,                                       // true / false / null (undecided)
  rca: { whys[5], category, factors[], medium, long, file, approver, status, days, note, decidedBy },
  actions: [ { id, title, kind:'Corrective', owner, due, status } ],
  cancelReason                                  // set only when stage === 'cancelled'
}
```

`rca.whys` is always a fixed 5-element array (`{why, ans}` pairs) regardless of whether RCA is required — it is simply not rendered when `rcaReq !== true`.

General tasks are stored separately (`state.tasks`), each carrying its own `pir` back-reference, rather than nested under the PIR — this is what makes the cross-PIR "General Tasks" list (§S5) a single flat query instead of a flatten-and-map over every PIR.

### Reference data (module-level constants)

`PEOPLE` (id → name/color/role), `ROLES`, `ME` (role → default acting person), `TYPES`, `SEVS` + `SEV_C` (severity → color triad: chip bg/fg, bar color), `STAGES` (ordered array of `{key,label,icon,bg,fg}`), `EQUIP`, `CAUSE_CATS`, `FACTORS`. A separate `CANCELLED` stage-meta object is consulted via `stageMeta(key)` rather than living in the `STAGES` array, since Cancelled is reachable from any stage rather than sitting at a fixed position in the sequence.

---

## 3. Stage machine

`STAGES` is an ordered list; a PIR's position (`idx = STAGES.findIndex(...)`) drives:
- the header **stage rail** (§UI, done/current/upcoming coloring),
- which stages render **struck through** when `rcaReq === false` (`rca` and `approved` are skipped visually, not removed — see `rcaSkipped` in `buildDetail()`),
- which single **contextual action button** appears for the assigned engineer (`acts` array in `buildDetail()`), evaluated as a chain of stage/permission conditions rather than a lookup table.

**Transitions** happen three ways:
1. **Contextual action buttons** — each calls `update(pirId, mutatorFn, logEntry)`, which both mutates the PIR and pushes a system log entry atomically.
2. **Manual stage-menu override** — any role with `perms().setStage` can jump directly to any stage via the dropdown (`stageOptions` in `buildDetail()`); this does not run gate checks (see §6, gap G1).
3. **Automatic closure** — `autoClose(p)` runs after every corrective-action completion: if `rcaOk` (RCA not required, or approved) and every corrective action is Done, stage flips to `closed`. This is the **only** path to Closed; there is no manual "close" button.

Cancellation is a special-cased transition available from the header (`perms().cancel`) whenever stage is not already `closed`/`cancelled`, going through its own modal that requires a non-empty reason.

---

## 4. Permission model

`perms()` computes a flat capability object from `state.role` on every render (not memoized — cheap enough at this data volume):

```js
{
  raise:    Admin | Shift In Charge | Maintenance Engineer
  engineer: Admin | Maintenance Engineer          // gates §5 "isEng" (acting-on-this-PIR checks)
  approve:  Admin | Central Team
  tasks:    Admin | Central Team | Plant Manager
  assign:   Admin | Shift In Charge | Central Team
  cancel:   Admin | Plant Manager | Central Team
  setStage: Admin | Plant Manager | Shift In Charge | Maintenance Engineer | Central Team
}
```

`isEng` in `buildDetail()` further narrows `engineer` to "and I am the assigned engineer for *this* PIR (or Admin), and it isn't cancelled" — this is what actually gates the immediate-action/RCA/CAPA action buttons, as opposed to the coarser role-level `engineer` flag. `perms()` itself has no per-record awareness (e.g. `cancel` doesn't check whether the record is already closed — that check lives separately in the button-render condition).

All permission checks are **client-side only** — this is the single highest-priority item to close before production use (see §7).

---

## 5. Key computed views (`renderVals()` pipeline)

`renderVals()` composes the full template data from three builder methods plus shell/modal wiring:

- **`buildDash()`** — KPI math (RCA-on-time %, CAPA closure %, overdue count, immediate-≤6h %), grouped bar data (by equipment top-6, by type), severity-mix segments, and the overdue-actions list. All KPIs are computed by filtering/reducing `state.pirs` fresh on every render; there is no caching or incremental update.
- **`buildList()`** — applies the four filters (search text, stage, severity, type, "mine") in sequence, then a single comparator-based sort keyed by `sortKey`/`sortDir` with a stable secondary sort on report date. Column header click toggles direction if already the active sort key, else switches key and defaults to descending.
- **`buildDetail()`** — the largest builder: stage rail, header action buttons, RCA panel (5-Why rows, category, factor chips, medium/long text, attach/submit/approve/reject controls, all permission- and status-gated), corrective-action table (with `onComplete` wired per-row), general-task table (same pattern), and the activity-log feed (grouped by day via a single linear scan of `feed[pirId]`).

Every mutation (`update()`, `setForm()`, `setAct()`) goes through `Object.assign`/array-copy patterns to keep state immutable-by-convention, matching the Maintenance Platform build's style.

---

## 6. Known simplifications in the current build

| Gap | Detail | Risk if shipped as-is |
|---|---|---|
| G1 — Stage override bypasses gates | The manual stage dropdown lets any `setStage` role jump straight to CAPA or Closed without RCA approval or without any corrective actions existing. | Undermines the RCA-approval gate that is the module's core control. |
| G2 — No real Maintenance Platform integration | "Create corrective action" mutates local state only; the `ACT-####` id is a random number, not a platform-issued id. | No real activity is created; nothing shows up on the Maintenance Platform. |
| G3 — RCA file attach is fake | `attachRca()` fabricates a filename/size; no file is actually uploaded anywhere. | No real evidence trail. |
| G4 — Single-site, single-tenant data | `siteName` is hardcoded; no site-scoping guard exists on `state.pirs`. | Cannot support multiple plants without rework. |
| G5 — "View as" is identity | Role and acting-user are chosen from a dropdown, not derived from a session. | Not a security boundary — fine for a prototype, unacceptable in production. |
| G6 — PIR id generation is fragile | New PIR ids are computed from `1056 + count - 8 + 1` against the seed data length, not a real sequence. | Will collide/break once seed data changes. |

---

## 7. Productionization checklist

1. **Persistence & multi-user backend.** Move `pirs`, `tasks`, `feed` to a real datastore with optimistic-locking or event-sourced writes (the log-entry-per-mutation pattern already used client-side maps cleanly onto an append-only audit log).
2. **Server-side gate enforcement (closes G1).** RCA-approval-before-CAPA, mandatory-reason-on-cancel/reject, and role checks must be re-validated server-side, not trusted from the client.
3. **Real Maintenance Platform integration (closes G2).** Corrective-action creation should call the Maintenance Platform's activity-creation API (or a shared event bus) with `origin='Breakdown'` and a back-reference; completion status should sync bidirectionally so `autoClose()` reacts to activities closed from the *other* application, not just from this one.
4. **File storage (closes G3).** RCA attachments need real object storage (S3-compatible) with a signed-URL upload flow.
5. **SSO identity (closes G5).** Replace `ME`/role-switcher with Keycloak-derived identity, role, and site, shared with the Maintenance Platform session.
6. **ID issuance (closes G6).** PIR, corrective-action, and general-task ids should come from the backend, not be computed client-side from array lengths.
7. **Site scoping (closes G4).** Every query (`buildDash`, `buildList`, approvals, tasks) needs a site/portfolio filter consistent with the Maintenance Platform's `FR-ACC-01/04`.
8. **Notifications.** RCA-submitted-for-approval and RCA-sent-back are the two events most worth wiring to email/in-app notification first (per the SRS's `NFR` and open item O4).

---

*End of Technical Spec Draft v0.1 — GPS Renewables Breakdown Reporting Module.*
