# CRH HMIS — Module-by-Module QA Audit

**System:** Clara Rosa Hospital HMIS (Laravel 11 / Blade / Tailwind / MySQL) — `go.crh.co.ke`
**Audit date:** 4 June 2026
**Audit type:** Static source audit of Blade views, controllers, routes, CSS and JS in the deployed `public_html` tree.

---

## 1. Scope, method and honest limitations

This audit was performed by reading the **source code**, not by driving the live site in a browser. That shapes what it can and cannot assert:

- **No screenshots.** Every finding instead carries a precise `file:line` reference, which is more actionable for fixing than an image. The "Screenshot reference" column your brief asked for is therefore replaced by **Evidence (file:line)**.
- **Console errors / failed API calls** cannot be observed at runtime here. What *can* be done — and was — is to flag the **code patterns that cause them** (e.g. `fetch()` with no `.catch`, swallowed error responses).
- **Route integrity (broken links / blank pages) could not be definitively verified.** A reliable check requires booting the framework (`php artisan route:list`), and `vendor/` is not in this archive. A naive static diff produced ~120 "missing" route names that turned out to be **false positives** from `Route::resource` and nested `->name()` group prefixes. Rather than report phantom broken links, this audit states the check as *inconclusive* and gives you the authoritative way to run it (Appendix A).
- **RBAC** is audited from route middleware and the `CheckRole` middleware, not by logging in as each role.
- **Audit-log coverage** is measured by cross-referencing write operations (`::create`, `->update`, `->delete`) against `AuditService::log` calls per controller. The write counts are a heuristic (they include relation/builder writes), so figures are framed as *candidate gaps to confirm*, not absolute counts.

The headline reality: **the most important issues are system-wide, not module-specific.** The same five root causes reappear in nearly every module, so the bulk of the value is in fixing them once (Section 3 and the Global Design System, Section 5) rather than page by page.

---

## 2. Evidence summary (measured across the codebase)

| Metric | Value | What it means |
|---|---|---|
| Blade view files | 296 | |
| Inline `style="…"` attributes in views | **2,282** | Page-specific styling instead of shared classes |
| Distinct hardcoded inline font sizes | **24** (7px → 48px, incl. 9.5/10.5/11.5/12.5/13.5px) | No type scale is enforced |
| Distinct inline `border-radius` values | **14** (2px → 999px) | No radius scale is enforced |
| Inline-styled `<th>` / `<td>` (bypass table system) | **217 / 307** | Row height, padding and font drift between tables |
| Shared `.btn-*` usages vs inline-styled `<button>` | 678 vs 40 | Buttons mostly consistent; 40 exceptions |
| Reusable Blade components defined | **0** | `resources/views/components` is empty |
| Files with tables but **no** horizontal-scroll wrapper | **116 of 193** | Wide tables overflow on mobile |
| Views using any responsive (`sm:`/`md:`/`lg:`) class | 132 of 296 (~45%) | Over half have no responsive rules |
| Route definitions / role-gated groups | 447 / 83 | RBAC structure is broadly present |
| `AuditService::log` calls total | 160 | |
| Controllers with writes but **zero** audit logging | 19 | Compliance gaps (Section 3.4) |
| Tailwind delivery | **`cdn.tailwindcss.com`** (dev CDN) | Production performance + console warning |

---

## 3. System-wide findings (cross-cutting — highest priority)

These are listed once here; they recur in every module's table below by reference.

### SW-1 — Tailwind loaded from the development CDN  *(Severity: High · Performance / UI)*
`layouts/app.blade.php:16` loads `https://cdn.tailwindcss.com`. This build is explicitly not for production: it compiles utility classes in the browser on every page load (slower first paint), has no purge, and prints a console warning on every page (`cdn.tailwindcss.com should not be used in production`). On a clinic network this adds avoidable latency to every screen.
**Fix:** Ship a pre-built Tailwind stylesheet. Because deployment is cPanel-ZIP with no build step, the pragmatic path is a one-time local build of a purged `app.css` committed to `public/css/`, or — given how little raw Tailwind is actually used versus the custom classes — fold the needed utilities into the design-system CSS (Section 5) and drop the CDN script.

### SW-2 — 2,282 inline styles and a 24-value font "scale"  *(Severity: High · UI / UX)*
Styling is overwhelmingly inline and per-page. The layout *does* define good shared classes (`.btn-primary`, `.form-input`, `.table-header`, `.table-cell`, `.stat-card`) but pages routinely ignore or override them. Concrete examples:
- Sub-legible 9px label text: `partials/_patient-balance-summary.blade.php:66,72,123,131` ("Patient Owes" / "Insurance Pending").
- 9.5px text: `partials/_encounter-styles.blade.php:542`, `partials/_payment-section.blade.php:212`.
- Shared button class then overridden inline, defeating standardisation: `dental/consult.blade.php:323,366,416,464` (`class="btn-primary" style="padding:7px 14px"`).
Distribution is very uneven — `billing` (240 inline styles / 67 font sizes) and `visits` (214 / 63) are the worst; `finance`, `patients`, `feedback`, `crm`, `admin` are nearly clean. The clean modules prove the shared classes are sufficient; the messy ones simply didn't use them.
**Fix:** Global design system + incremental migration (Section 5). No business logic changes.

### SW-3 — 524 tables bypass the table system; 116 tables overflow on mobile  *(Severity: High · UI / UX, Medium for mobile)*
`.table-header`/`.table-cell` are used 2,997 times (good), but 524 `<th>`/`<td>` carry inline styles that re-set padding/font/height, so adjacent tables don't match. Separately, 116 of 193 table-bearing views have no `overflow-x-auto` wrapper; HMIS tables are wide (many columns, plus inline `width:200px` etc.), so on a phone they push the layout sideways or clip.
**Fix:** A single `<x-ui.table>` component that always wraps in a horizontal-scroll container and applies the standard header/cell styles (Section 5).

### SW-4 — Audit-trail gaps in 19 controllers  *(Severity: High · Security / Data — compliance)*
Billing, payments and voids **are** correctly audited (`payment_recorded`, void logging confirmed). But several controllers perform create/update/delete with **no** `AuditService::log` call. Highest-confidence gaps (many writes, zero audit calls):

| Controller | Writes (heuristic) | Audit calls | Notable unlogged actions |
|---|---|---|---|
| `ImagingController` | 13 | 0 | imaging order create / result post |
| `AppointmentController` | 11 | 0 | create / reschedule / cancel / no-show |
| `ProcurementRequestController` | 12 | 0 | request create / approve |
| `CorporateMemberController` | 6 | 0 | member add / update / remove |
| `CorporateAccountController` | 5 | 0 | account create / update / toggle |
| `ReferralController` | 5 | 0 | referral create / approve |
| `InventoryController` | 3 | 0 | item create / stock change |
| `SickLeaveController` | 1 | 0 | sick-leave issue |
| `FeedbackController` | 2 | 0 | feedback create / status change |

**Fix:** Add `AuditService::log(action, entity, id, before, after)` on every create/update/delete/approve in these controllers. This is functional/compliance work, not cosmetic — do it as a deliberate pass. (Confirm each heuristic write is a real state change before adding a log line.)

### SW-5 — Latent secret-exposure risk in the web-root layout  *(Severity: High · Security)*
The domain document root is `public_html/` and the root `.htaccess` (`RewriteRule ^(.*)$ public/$1 [L]`) forwards requests into `public/`. This *currently* hides `.env`, `composer.lock`, `storage/logs`, `sql/` and the loose `crh-*.zip` files sitting in the app root — but only while `mod_rewrite` is enabled and that `.htaccess` is intact. One config change, server migration, or accidental deletion of that file exposes `.env` (which contains `APP_KEY` and `DB_PASSWORD`) to the public internet.
**Fix (best):** Point the domain's document root directly at `public_html/public` in cPanel, so the Laravel root is never web-served. **Fix (interim):** add an explicit deny for dotfiles and archives in the root `.htaccess`, and delete the loose `*.zip` / stray `*.sql` from the app root. Also remove stale `app.blade.php.*-backup` and `web.php.*-backup` files (`layouts/`, `routes/`) — clutter, and confusing during future edits.

### SW-6 — Errors swallowed into generic "Failed to save"  *(Severity: Medium · UX)*
Several AJAX handlers discard the server's real message and show a generic failure. The visit billing Save button is the confirmed case (`visits/show.blade.php`, now patched in the prior fix). The same anti-pattern appears wherever a `fetch().then(r=>r.json())` only checks `data.success`. Users (and you, when supporting them) lose the actual reason — a closed-visit lock, a validation error, a 419 session timeout all look identical.
**Fix:** Standardise a small `postJson()` helper that surfaces `data.message` / validation errors and distinguishes 419 (session) from network failure. Roll it into the autosave JS already in `public/js/hmis-autosave.js`.

### SW-7 — `fetch()` calls with no error handling  *(Severity: Medium · Functional / UX)*
Files where `fetch` count exceeds `.catch` count (unhandled rejection → silent no-op or console error if the call fails):
`crm/follow-ups.blade.php` (4/0), `corporate/referrals/referrals/create.blade.php` (2/0), `corporate/referrals/index.blade.php` (2/0), `corporate/accounts/show.blade.php` (1/0), `pharmacy/drugs-categorize.blade.php` (1/0), `pharmacy/drugs-bulk-edit.blade.php` (1/0).
**Fix:** Same `postJson()` helper (SW-6) gives every call a uniform failure path.

---

## 4. Module-by-module findings

Severity: **Critical** (data loss / outage / security breach) · **High** (blocks a workflow or compliance) · **Medium** (degraded UX, no data risk) · **Low** (cosmetic / hygiene).
Category: UI · UX · Functional · Data · Security · Performance.

### 4.1 Patient Care
*Views: `clinical/`, `consultation` (`partials/_encounter-styles`), `triage`, `outpatient/`, `inpatient/`, `maternity/`, `anc/`, `direct-encounters/`, `dental/`*

| ID | Page/submenu | Exact issue (evidence) | Severity | Category | Recommended fix |
|---|---|---|---|---|---|
| PC-1 | Encounter (all) | Heavy inline styling in the encounter partials; 9.5px header text `partials/_encounter-styles.blade.php:542` is below legible minimum | High | UI/UX | Adopt design-system type scale (min 11px); SW-2 |
| PC-2 | Inpatient show | `fetch` without `.catch` in `inpatient/*` (admit/show) — failed save of vitals/notes shows nothing | Medium | Functional | SW-7 helper |
| PC-3 | Dental consult | `btn-primary` overridden with inline `padding:7px 14px` 4× (`dental/consult.blade.php:323,366,416,464`); inline-styled action buttons `:254,:476` | Medium | UI | Use `<x-ui.button>` (Section 5) |
| PC-4 | Maternity (24 views) | Largest clinical module; mixed table styling, several wide tables without scroll wrapper | Medium | UI/UX (mobile) | `<x-ui.table>` wrapper; SW-3 |
| PC-5 | Direct encounters | `AuditService` present but thin (2 logs / 7 writes) — confirm result-edit and convert are logged | Medium | Data | Audit pass; SW-4 |
| PC-6 | ANC consults | `AncConsultController` 4 writes / 0 audit | High | Data | Add audit logging |

### 4.2 Patient Visits
*Views: `visits/`, `appointments/`, queue (`VisitController@queueManagement`)*

| ID | Page/submenu | Exact issue (evidence) | Severity | Category | Recommended fix |
|---|---|---|---|---|---|
| PV-1 | Encounter → Billing | Save button swallowed the lock/validation error (`visits/show.blade.php`) | High | UX/Functional | **Fixed** in prior patch; generalise via SW-6 |
| PV-2 | Visits (module) | Worst styling debt after billing: 214 inline styles, 63 hardcoded font sizes, 7 inline-styled buttons | High | UI | SW-2 migration |
| PV-3 | Appointments | `AppointmentController` 11 writes / 0 audit (create, reschedule, cancel, no-show all unlogged) | High | Data/Security | Add audit logging; SW-4 |
| PV-4 | Queue management | Confirm wide queue table has scroll wrapper on mobile | Medium | UI (mobile) | SW-3 |
| PV-5 | Visit lifecycle | Closed-visit lock is correct but invisible until SW-6 fix; verify "Reactivate" is reachable for Admin only | Low | UX | Already role-gated; surface message (done) |

### 4.3 Pharmacy
*Views: `pharmacy/` (28), `inventory/`, `procurement/`, `suppliers/`*

| ID | Page/submenu | Exact issue (evidence) | Severity | Category | Recommended fix |
|---|---|---|---|---|---|
| PH-1 | Drugs categorize / bulk edit | `fetch` with no `.catch` (`drugs-categorize.blade.php`, `drugs-bulk-edit.blade.php`) — silent failure on bulk save | High | Functional | SW-7 helper |
| PH-2 | Pharmacy (module) | 129 inline styles / 39 font sizes — third worst | Medium | UI | SW-2 migration |
| PH-3 | Procurement requests | `ProcurementRequestController` 12 writes / 0 audit (request, approve) | High | Data/Security | Add audit logging |
| PH-4 | Inventory items | `InventoryController` 3 writes / 0 audit (stock adjustments unlogged — fraud/loss vector) | High | Security/Data | Add audit logging |
| PH-5 | Stock take / receipt | Good audit coverage already (`StockReceiptController` 5 logs) — use as the template | — | — | Reference pattern |

### 4.4 Diagnostics
*Views: `lab/` (7), `imaging/` (4)*

| ID | Page/submenu | Exact issue (evidence) | Severity | Category | Recommended fix |
|---|---|---|---|---|---|
| DX-1 | Imaging (all) | `ImagingController` 13 writes / 0 audit — order creation and result posting entirely unlogged | High | Security/Data | Add audit logging; SW-4 |
| DX-2 | Lab + Imaging | 23 + 16 hardcoded font sizes; inline-styled result tables | Medium | UI | SW-2 / SW-3 |
| DX-3 | Result entry | Confirm result tables wrap for mobile (radiographers often on tablets) | Medium | UI (mobile) | SW-3 |
| DX-4 | Lab orders | `VisitLockService` correctly blocks edits on closed visits — verify lab result post returns a clear message, not generic failure | Medium | UX | SW-6 |

### 4.5 Finance & Billing
*Views: `billing/` (9), `finance/` (6), `expenses/`, `cash-recon/`, `insurance/`, `price-list/`*

| ID | Page/submenu | Exact issue (evidence) | Severity | Category | Recommended fix |
|---|---|---|---|---|---|
| FB-1 | Record payment | `mpesa_code` overflow → 500 on long entries | High | Functional/Data | **Fixed** (widen column + validation cap) |
| FB-2 | Billing (module) | **Worst** styling debt: 240 inline styles, 67 font sizes | High | UI | SW-2 migration — start here |
| FB-3 | Finance (module) | Near-zero inline styling — **the model module**; mirror its approach elsewhere | — | — | Reference pattern |
| FB-4 | Payments / voids | Audit logging confirmed present (`payment_recorded`, void) — good | — | Security | Maintain |
| FB-5 | Credit notes | `CreditNoteController` 4 logs / 3 writes — good coverage | — | — | — |
| FB-6 | Expenses | `ExpensesController` 4 logs / 7 writes — confirm `updatePayment` and pay are logged | Medium | Data | Verify/extend audit |
| FB-7 | Cash recon | Cashier closing is sensitive money-handling — verify every close/approve is audited | High | Security | Confirm audit on `cash-recon` actions |

### 4.6 Corporate / SACCO
*Views: `corporate/` (31), routes in `routes/corporate.php`*

| ID | Page/submenu | Exact issue (evidence) | Severity | Category | Recommended fix |
|---|---|---|---|---|---|
| CO-1 | Accounts / Members | `CorporateAccountController` (5 writes/0 audit) and `CorporateMemberController` (6/0) — membership and account changes unlogged (eligibility & money implications) | High | Security/Data | Add audit logging; SW-4 |
| CO-2 | Referrals / payouts | `ReferralController` 5 writes / 0 audit; rider-referral payouts are payments — must be audited | High | Security/Data | Add audit logging |
| CO-3 | Referrals create / index | `fetch` without `.catch` (`referrals/referrals/create.blade.php`, `referrals/index.blade.php`, `accounts/show.blade.php`) | Medium | Functional | SW-7 helper |
| CO-4 | Corporate (module) | 74 inline styles | Medium | UI | SW-2 migration |
| CO-5 | Premiums | `CorporatePremiumController` post/reverse — confirm audit (money posting) | High | Security | Verify audit |
| CO-6 | Public enrolment | Public-facing enrolment form — verify rate-limiting and validation (untrusted input) | Medium | Security | Confirm throttle + validation |

### 4.7 Human Resource
*Views: `hr/` (10), `payroll/` (8), `locum/` (12), `sick-leave/`*

| ID | Page/submenu | Exact issue (evidence) | Severity | Category | Recommended fix |
|---|---|---|---|---|---|
| HR-1 | Payroll | `PayrollController` 21 writes / 2 audit — run approve/lock/reverse/paid are money actions; confirm all are logged | High | Security/Data | Audit pass |
| HR-2 | Locum payments | `LocumController` 29 writes / 1 audit — payment marking largely unlogged | High | Security/Data | Add audit logging |
| HR-3 | Sick leave | `SickLeaveController` 1 write / 0 audit | Medium | Data | Add audit logging |
| HR-4 | HR (module) | 58 inline styles / 11 font sizes; 2 inline-styled buttons | Medium | UI | SW-2 migration |
| HR-5 | Payroll settings | `PayrollSettingsController` 2 writes / 0 audit (rates/deductions config changes) | High | Security | Add audit logging |

### 4.8 CRM & Feedback
*Views: `crm/` (2), `feedback/` (2)*

| ID | Page/submenu | Exact issue (evidence) | Severity | Category | Recommended fix |
|---|---|---|---|---|---|
| CR-1 | Follow-ups | `crm/follow-ups.blade.php` 4 `fetch` / 0 `.catch` — failed status updates silently lost | High | Functional | SW-7 helper |
| CR-2 | Feedback | `FeedbackController` 2 writes / 0 audit | Medium | Data | Add audit logging |
| CR-3 | Both modules | Styling already clean (0–2 inline styles) — low effort to bring fully onto components | Low | UI | Quick win |

### 4.9 Admin
*Views: `admin/` (9), `reports/` (40), `analytics/`*

| ID | Page/submenu | Exact issue (evidence) | Severity | Category | Recommended fix |
|---|---|---|---|---|---|
| AD-1 | Admin (module) | Styling clean (3 inline styles); good baseline | Low | UI | Adopt components opportunistically |
| AD-2 | Reports (40 views) | Largest view folder; check each report table wraps for mobile and uses shared table styles | Medium | UI (mobile) | SW-3 |
| AD-3 | Roles / Users / Settings | Verify these admin actions (`admin.users.toggle`, `roles.store`, `settings.update`) are audited — they govern access | High | Security | Confirm audit on `AdminController` |
| AD-4 | Audit log viewer | Confirm there *is* a UI to read the audit trail (route `admin.audit` referenced) and that it is admin-gated | Medium | Security | Verify page renders + RBAC |
| AD-5 | Scheduler | `proc_open` disabled → `schedule:run` errors every 15 min (`storage/logs`) | Medium | Performance/Functional | Disable unused scheduler cron, or run individual commands via cron |

---

## 5. Global design-system fix (proposal)

**Goal:** replace page-specific styling with a small set of shared, reusable pieces so font sizes, buttons, tables, cards, forms, tabs, modals and sidebar items are defined **once**. **No business logic changes** — this is purely the presentation layer.

### 5.1 What ships (all in this delivery)
1. **`public/css/crh-design-system.css`** — design *tokens* as CSS custom properties (one type scale, one colour palette built from your existing `cr` magenta, one spacing scale, one radius scale, elevation, transitions) plus standardised component classes (`.crh-btn`, `.crh-card`, `.crh-table`, `.crh-field`, `.crh-modal`, `.crh-tabs`, `.crh-badge`). Plain CSS — **no build step**, works with cPanel-ZIP deploys.
2. **Blade components** in `resources/views/components/ui/` so pages write intent, not pixels:
   - `<x-ui.button variant="success" size="sm">Save</x-ui.button>`
   - `<x-ui.card>…</x-ui.card>`
   - `<x-ui.table :headers="[...]">…rows…</x-ui.table>` (always mobile-scroll-wrapped)
   - `<x-ui.input label="Amount" name="amount" required />`
   - `<x-ui.select label="Method" :options="[...]" />`
   - `<x-ui.badge tone="green">Paid</x-ui.badge>`
   - `<x-ui.modal id="pay">…</x-ui.modal>`
   - `<x-ui.page-header title="Payments" />`
3. **`public/js/crh-postjson.js`** — the shared `postJson()` helper (fixes SW-6/SW-7 uniformly).
4. **`docs/Design-System-Adoption.md`** — the migration playbook.

### 5.2 The type scale (replaces the 24 ad-hoc sizes)
`--fs-2xs:11px` · `--fs-xs:12px` · `--fs-sm:13px` · `--fs-base:14px` · `--fs-lg:16px` · `--fs-xl:18px` · `--fs-2xl:22px` · `--fs-3xl:28px`. Minimum body/label size is **11px** — anything currently 7–10.5px (the balance-summary labels, encounter headers, notification badge) maps up to `--fs-2xs`.

### 5.3 The radius scale (replaces the 14 ad-hoc values)
`--radius-sm:4px` (chips) · `--radius-md:8px` (buttons, inputs) · `--radius-lg:12px` (cards) · `--radius-pill:9999px` (status pills). The two "fully round" spellings (`99px` and `999px`) both collapse to `--radius-pill`.

### 5.4 Migration strategy (no logic touched, lowest risk first)
1. **Link the CSS** in `layouts/app.blade.php` and **delete the dev-CDN Tailwind** (SW-1). The new classes are additive; existing pages keep working because the design-system class names are *new* (they don't clash with current ones).
2. **Migrate worst-first:** `billing` → `visits` → `pharmacy` → `corporate` → `hr` → `lab`/`imaging`. `finance`/`patients`/`admin`/`crm`/`feedback` are already clean and can be left or converted opportunistically.
3. **Mechanical replacements** (find-and-replace, no behaviour change):
   - `<button class="btn-success" style="…">` → `<x-ui.button variant="success">`
   - raw `<table>…<th style>…<td style>` → `<x-ui.table>`
   - inline `font-size:NNpx` → remove; let the component/scale govern
4. **Switch AJAX handlers** to `postJson()` as you touch each page (folds in SW-6/SW-7).
5. Re-measure: target inline `style=` count trending from 2,282 toward < 300 (icons/one-offs only).

### 5.5 Definition of done (per module)
No inline `font-size`; all buttons via `<x-ui.button>`; all tables via `<x-ui.table>` (mobile-wrapped); all forms via `<x-ui.input>/<x-ui.select>`; all AJAX via `postJson()`; all create/update/delete/void/approve actions call `AuditService::log`.

---

## Appendix A — Authoritative route-integrity check (run on server)
Static analysis could not confirm broken links. To get the real answer, on the server (cPanel Terminal if available, or a temporary admin-only route) run:
```
php artisan route:list --json > routes.json
```
then compare against route names referenced in views:
```
grep -rohE "route\('[^']+'" resources/views --include=*.blade.php | sed -E "s/route\('//; s/'//" | sort -u
```
Any referenced name absent from `route:list` is a genuine broken link. (The ~120 names this audit's static diff flagged are produced by `Route::resource` and `->name('prefix.')` groups and are **not** broken.)

## Appendix B — Reference (clean) modules to copy from
`finance` (0 inline styles), `patients` (4), `admin` (3), `feedback` (0), `crm` (2). These already follow the shared-class approach and should be the template the rest of the system converges on.
