feature: Design System, App Shell, and Login Redesign
@@ -213,7 +213,8 @@ VigilCareRecords/
|
|||||||
│ ├── run-vigilcare-records-phase-13-verification.sh # OCR config, ocrConfidence API, optional live OCR polling
|
│ ├── run-vigilcare-records-phase-13-verification.sh # OCR config, ocrConfidence API, optional live OCR polling
|
||||||
│ └── fixtures/test-scan.pdf # Sample PDF for upload verification scripts
|
│ └── fixtures/test-scan.pdf # Sample PDF for upload verification scripts
|
||||||
└── docs/
|
└── docs/
|
||||||
├── plans/ # Phase 1–13 implementation guides
|
├── plans/ # Phase 14–18 UI redesign guides (1–13 historical; not in repo)
|
||||||
|
├── designs/ # UI/UX design-doc + mockup PNGs
|
||||||
├── digitization-workstation-guide.md # Clinical scenarios and clerk workflow reference
|
├── digitization-workstation-guide.md # Clinical scenarios and clerk workflow reference
|
||||||
├── vigilcare-records-gap-analysis.md # Known gaps and hardening backlog
|
├── vigilcare-records-gap-analysis.md # Known gaps and hardening backlog
|
||||||
└── vigilcare-records-prd.md # Product requirements and phase roadmap
|
└── vigilcare-records-prd.md # Product requirements and phase roadmap
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
**Phases 1–13 are complete.** Post-phase hardening (health checks, user management, auth rate limiting, document access audit, batch cancellation, list sorting, unified promotion retry, normalized patient deduplication, assignment-time `IN_ENTRY` transitions, API-proxied document streaming, CORS) is also done. See the [gap analysis](vigilcare-records-gap-analysis.md) summary matrix for remaining open items.
|
**Phases 1–13 are complete.** Post-phase hardening (health checks, user management, auth rate limiting, document access audit, batch cancellation, list sorting, unified promotion retry, normalized patient deduplication, assignment-time `IN_ENTRY` transitions, API-proxied document streaming, CORS) is also done. See the [gap analysis](vigilcare-records-gap-analysis.md) summary matrix for remaining open items.
|
||||||
|
|
||||||
|
**Phases 14–18 (UI redesign)** are planned: restyle and re-layout `vigilcare-records-web` against [designs/design-doc.md](designs/design-doc.md) without new backend behavior. Implementation guides: [plans/](plans/).
|
||||||
|
|
||||||
| Phase | Scope | Status |
|
| Phase | Scope | Status |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 1 | Schema, auth, roles, batch CRUD, MinIO upload, status machine | Done |
|
| 1 | Schema, auth, roles, batch CRUD, MinIO upload, status machine | Done |
|
||||||
@@ -19,8 +21,13 @@
|
|||||||
| 11 | HL7 FHIR R4 read-only API and FHIR Explorer UI | Done |
|
| 11 | HL7 FHIR R4 read-only API and FHIR Explorer UI | Done |
|
||||||
| 12 | Backend-driven batch-type field requirements (`fieldRequirements` metadata) | Done |
|
| 12 | Backend-driven batch-type field requirements (`fieldRequirements` metadata) | Done |
|
||||||
| 13 | Optional OCR-assisted draft pre-fill (Azure or Tesseract; disabled by default) | Done |
|
| 13 | Optional OCR-assisted draft pre-fill (Azure or Tesseract; disabled by default) | Done |
|
||||||
|
| 14 | UI redesign: design tokens, app shell (sidebar), login | Planned |
|
||||||
|
| 15 | UI redesign: shared UX primitives (status, OCR badges, SoD, sticky actions) | Planned |
|
||||||
|
| 16 | UI redesign: Entry / Verification / Clinical Approval workstation layouts | Planned |
|
||||||
|
| 17 | UI redesign: Intake, Cover Sheets, Live Capture, History, Dashboard, FHIR Explorer | Planned |
|
||||||
|
| 18 | UI redesign: surface unused APIs (batch events, work queues, Users admin) | Planned |
|
||||||
|
|
||||||
**Verification scripts:** `./scripts/run-vigilcare-records-verification-p9.sh` (full workflow + metrics), `./scripts/run-vigilcare-records-phase-10-verification.sh`, `./scripts/run-vigilcare-records-phase-11-verification.sh`, `./scripts/run-vigilcare-records-phase-13-verification.sh`.
|
**Verification scripts:** `./scripts/run-vigilcare-records-verification-p9.sh` (full workflow + metrics), `./scripts/run-vigilcare-records-phase-10-verification.sh`, `./scripts/run-vigilcare-records-phase-11-verification.sh`, `./scripts/run-vigilcare-records-phase-13-verification.sh`. Phases 14–18 verify via Vitest + manual checklists in each plan.
|
||||||
|
|
||||||
See [README.md](../README.md) for API reference, quick start, and [digitization-workstation-guide.md](digitization-workstation-guide.md) for operator workflows.
|
See [README.md](../README.md) for API reference, quick start, and [digitization-workstation-guide.md](digitization-workstation-guide.md) for operator workflows.
|
||||||
|
|
||||||
@@ -568,7 +575,7 @@ SMART on FHIR authorization and FHIR write operations remain out of scope.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Digitization Workstation UI — *implemented (Phases 7, 10, 11, 12, 13)*
|
## Digitization Workstation UI — *implemented (Phases 7, 10, 11, 12, 13); redesign Planned (Phases 14–18)*
|
||||||
|
|
||||||
Vue 3 SPA at `vigilcare-records-web/` (dev server port **3028**, proxies `/api` and `/fhir` → API on **5217**). Role-based routing with JWT refresh.
|
Vue 3 SPA at `vigilcare-records-web/` (dev server port **3028**, proxies `/api` and `/fhir` → API on **5217**). Role-based routing with JWT refresh.
|
||||||
|
|
||||||
@@ -586,6 +593,20 @@ Vue 3 SPA at `vigilcare-records-web/` (dev server port **3028**, proxies `/api`
|
|||||||
|
|
||||||
Scan viewer loads documents via authenticated `GET /digitization-batches/:id/document` blob URLs (avoids cross-origin MinIO iframe issues). Not a full EMR UI — clinical alerting views remain in VigilCareClinical's ward dashboard.
|
Scan viewer loads documents via authenticated `GET /digitization-batches/:id/document` blob URLs (avoids cross-origin MinIO iframe issues). Not a full EMR UI — clinical alerting views remain in VigilCareClinical's ward dashboard.
|
||||||
|
|
||||||
|
### UI redesign roadmap (Phases 14–18)
|
||||||
|
|
||||||
|
Constrained to **existing API capabilities**. Design source: [designs/design-doc.md](designs/design-doc.md) (section 36 corrections override mockups). Plans: [plans/README.md](plans/README.md).
|
||||||
|
|
||||||
|
| Phase | Focus |
|
||||||
|
|---|---|
|
||||||
|
| 14 | Navy/blue design tokens, role-filtered sidebar shell, split login (no role picker); keep Public Sans |
|
||||||
|
| 15 | Shared primitives: status badges, empty/loading/error, sticky action bar, OCR % badges, SoD banner, confirm dialogs |
|
||||||
|
| 16 | Dense Entry / Verification / Clinical Approval workstation layouts |
|
||||||
|
| 17 | Restyle Intake, Cover Sheets, Live Capture, Patient History, Queue Dashboard, FHIR Explorer |
|
||||||
|
| 18 | Wire unused APIs: batch events panel, `work-queue/*` lists, administrator Users page |
|
||||||
|
|
||||||
|
Deferred mockup items (global search, notifications, Reports/Master Data, SSO, OCR region highlight, infra health widgets) are listed in [plans/README.md](plans/README.md).
|
||||||
|
|
||||||
See [digitization-workstation-guide.md](digitization-workstation-guide.md) for operator workflows and clinical scenarios.
|
See [digitization-workstation-guide.md](digitization-workstation-guide.md) for operator workflows and clinical scenarios.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -666,12 +687,17 @@ If VigilCareClinical is unreachable in split deployment, batch remains `approved
|
|||||||
| 11 | HL7 FHIR R4 read-only API and FHIR Explorer UI | Done |
|
| 11 | HL7 FHIR R4 read-only API and FHIR Explorer UI | Done |
|
||||||
| 12 | Backend-driven batch-type field requirements | Done |
|
| 12 | Backend-driven batch-type field requirements | Done |
|
||||||
| 13 | Optional OCR-assisted draft pre-fill | Done |
|
| 13 | Optional OCR-assisted draft pre-fill | Done |
|
||||||
|
| 14 | UI redesign: tokens, app shell, login | Planned |
|
||||||
|
| 15 | UI redesign: shared UX primitives | Planned |
|
||||||
|
| 16 | UI redesign: Entry / Verification / Approval workstation | Planned |
|
||||||
|
| 17 | UI redesign: supporting screens + dashboard | Planned |
|
||||||
|
| 18 | UI redesign: surface unused existing APIs | Planned |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step-by-Step Guide
|
## Step-by-Step Guide
|
||||||
|
|
||||||
Complete phases in order. Promotion (Phase 4) must not be built until the draft state machine and separation of duties are correct — debugging promotion bugs alongside workflow bugs is painful.
|
Complete phases in order. Promotion (Phase 4) must not be built until the draft state machine and separation of duties are correct — debugging promotion bugs alongside workflow bugs is painful. For Phases 14–18, complete each UI plan before starting the next; do not invent backend features to match mockups.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -796,6 +822,46 @@ This phase connects Records to Clinical. Run against a VigilCareClinical Phase 2
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Phase 14 — UI Design System, Shell, Login
|
||||||
|
|
||||||
|
**What to do:** Map design-doc color tokens into Tailwind (keep Public Sans); add role-filtered `AppShell` sidebar for existing routes; redesign login as split brand + form without a role selector.
|
||||||
|
|
||||||
|
**Plan:** [plans/phase-14-plan.md](plans/phase-14-plan.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 15 — Shared UX Primitives
|
||||||
|
|
||||||
|
**What to do:** Status badges, empty/loading/error patterns, sticky workstation action bar, OCR percentage badges, separation-of-duties banner, confirmation dialogs for irreversible actions.
|
||||||
|
|
||||||
|
**Plan:** [plans/phase-15-plan.md](plans/phase-15-plan.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 16 — Workstation Layouts (Entry, Verification, Approval)
|
||||||
|
|
||||||
|
**What to do:** Dense scan-first layouts with sticky CTAs and design-doc action labels; apply SoD and OCR primitives; no API contract changes.
|
||||||
|
|
||||||
|
**Plan:** [plans/phase-16-plan.md](plans/phase-16-plan.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 17 — Supporting Screens
|
||||||
|
|
||||||
|
**What to do:** Restyle Intake, Cover Sheets, Live Capture, Patient History, Queue Dashboard (overview metrics only — no infra health), and FHIR Explorer.
|
||||||
|
|
||||||
|
**Plan:** [plans/phase-17-plan.md](plans/phase-17-plan.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 18 — Surface Unused APIs in the UI
|
||||||
|
|
||||||
|
**What to do:** Batch events audit panel; prefer `work-queue/entry|verification|clinical-approval` for queues; administrator Users page via existing `UsersController`.
|
||||||
|
|
||||||
|
**Plan:** [plans/phase-18-plan.md](plans/phase-18-plan.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Deployment Notes (Small Island Context)
|
## Deployment Notes (Small Island Context)
|
||||||
|
|
||||||
- **Single-site tenant:** One hospital or health district per deployment. No cross-island federation in v1.
|
- **Single-site tenant:** One hospital or health district per deployment. No cross-island federation in v1.
|
||||||
@@ -838,7 +904,8 @@ This phase connects Records to Clinical. Run against a VigilCareClinical Phase 2
|
|||||||
|
|
||||||
- [README.md](../README.md) — API reference, quick start, verification scripts, data models
|
- [README.md](../README.md) — API reference, quick start, verification scripts, data models
|
||||||
- [digitization-workstation-guide.md](digitization-workstation-guide.md) — clinical scenarios (backfill, live capture, corrections)
|
- [digitization-workstation-guide.md](digitization-workstation-guide.md) — clinical scenarios (backfill, live capture, corrections)
|
||||||
- [plans/](plans/) — phase-by-phase implementation guides (Phases 1–13)
|
- [plans/](plans/) — phase implementation guides (Phases 14–18 UI redesign; Phase 1–13 plans not in repo)
|
||||||
|
- [designs/design-doc.md](designs/design-doc.md) — UI/UX design specification for the workstation redesign
|
||||||
- [vigilcare-records-gap-analysis.md](vigilcare-records-gap-analysis.md) — post-phase hardening tracker and remaining open items
|
- [vigilcare-records-gap-analysis.md](vigilcare-records-gap-analysis.md) — post-phase hardening tracker and remaining open items
|
||||||
- [vigilcare-clinical-api-prd.md](vigilcare-clinical-api-prd.md) — downstream alerting and observation ingest
|
- [vigilcare-clinical-api-prd.md](vigilcare-clinical-api-prd.md) — downstream alerting and observation ingest
|
||||||
- [Completed/VigilCareClinicalAPI/VigilCare-Partner-Brief.md](Completed/VigilCareClinicalAPI/VigilCare-Partner-Brief.md) — clinical positioning and scope boundaries
|
- [Completed/VigilCareClinicalAPI/VigilCare-Partner-Brief.md](Completed/VigilCareClinicalAPI/VigilCare-Partner-Brief.md) — clinical positioning and scope boundaries
|
||||||
|
|||||||
@@ -1397,9 +1397,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||||
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
|
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2768,9 +2768,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.15",
|
"version": "3.3.18",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||||
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -3005,9 +3005,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.15",
|
"version": "8.5.26",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3024,7 +3024,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.12",
|
"nanoid": "^3.3.17",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -3780,9 +3780,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "7.28.0",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
|
||||||
"integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
|
"integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -4,75 +4,100 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vc-navy: #08254A;
|
||||||
|
--vc-primary: #155EEF;
|
||||||
|
--vc-primary-hover: #0B4FD6;
|
||||||
|
--vc-soft-blue: #EEF4FF;
|
||||||
|
--vc-bg: #F7F9FC;
|
||||||
|
--vc-surface: #FFFFFF;
|
||||||
|
--vc-text-strong: #101828;
|
||||||
|
--vc-text: #344054;
|
||||||
|
--vc-text-secondary: #667085;
|
||||||
|
--vc-text-disabled: #98A2B3;
|
||||||
|
--vc-border: #E4E7EC;
|
||||||
|
--vc-border-strong: #D0D5DD;
|
||||||
|
--vc-success: #079455;
|
||||||
|
--vc-success-bg: #ECFDF3;
|
||||||
|
--vc-warning: #DC6803;
|
||||||
|
--vc-warning-bg: #FFFAEB;
|
||||||
|
--vc-error: #D92D20;
|
||||||
|
--vc-error-bg: #FEF3F2;
|
||||||
|
--vc-critical: #B42318;
|
||||||
|
--vc-critical-bg: #FEE4E2;
|
||||||
|
--vc-radius-input: 8px;
|
||||||
|
--vc-radius-card: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
@apply font-sans;
|
@apply font-sans text-ink bg-canvas;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
@apply bg-primary-600 text-white px-4 py-2 rounded-md
|
@apply bg-primary-600 text-white px-4 py-2 rounded-input
|
||||||
hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed
|
hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed
|
||||||
transition-colors;
|
transition-colors;
|
||||||
}
|
}
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
@apply bg-white text-primary-700 border border-primary-300 px-4 py-2 rounded-md
|
@apply bg-surface text-primary-700 border border-primary-300 px-4 py-2 rounded-input
|
||||||
hover:bg-primary-50 disabled:opacity-50 disabled:cursor-not-allowed
|
hover:bg-primary-50 disabled:opacity-50 disabled:cursor-not-allowed
|
||||||
transition-colors;
|
transition-colors;
|
||||||
}
|
}
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
@apply bg-clinical-danger text-white px-4 py-2 rounded-md
|
@apply bg-clinical-danger text-white px-4 py-2 rounded-input
|
||||||
hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed
|
hover:bg-clinical-critical disabled:opacity-50 disabled:cursor-not-allowed
|
||||||
transition-colors;
|
transition-colors;
|
||||||
}
|
}
|
||||||
.form-input {
|
.form-input {
|
||||||
@apply block w-full rounded-md border border-gray-300 px-4 py-2
|
@apply block w-full rounded-input border border-line-strong px-4 py-2 text-ink
|
||||||
focus:border-primary-500 focus:ring-2 focus:ring-primary-500
|
focus:border-primary-500 focus:ring-2 focus:ring-primary-500
|
||||||
disabled:bg-gray-100 disabled:text-gray-500;
|
disabled:bg-canvas disabled:text-ink-disabled;
|
||||||
}
|
}
|
||||||
.page-container {
|
.page-container {
|
||||||
@apply p-4 sm:p-6 lg:p-8 max-w-4xl mx-auto;
|
@apply p-4 sm:p-6 lg:p-8 max-w-4xl mx-auto;
|
||||||
}
|
}
|
||||||
.app-header {
|
.app-header {
|
||||||
@apply flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between
|
@apply flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between
|
||||||
px-4 py-4 sm:px-6 sm:py-4 bg-white border-b shadow-sm;
|
px-4 py-3 sm:px-6 h-auto min-h-16 bg-surface border-b border-line;
|
||||||
}
|
}
|
||||||
.app-header-title {
|
.app-header-title {
|
||||||
@apply flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4;
|
@apply flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4 min-w-0;
|
||||||
}
|
}
|
||||||
.app-header-actions {
|
.app-header-actions {
|
||||||
@apply flex flex-wrap items-center gap-2 sm:gap-4;
|
@apply flex flex-wrap items-center gap-2 sm:gap-4;
|
||||||
}
|
}
|
||||||
.card {
|
.card {
|
||||||
@apply bg-white rounded-md border border-gray-200 p-4 sm:p-6;
|
@apply bg-surface rounded-card border border-line p-4 sm:p-6 shadow-card;
|
||||||
}
|
}
|
||||||
.card-elevated {
|
.card-elevated {
|
||||||
@apply bg-white rounded-lg shadow-md p-4 sm:p-6;
|
@apply bg-surface rounded-card border border-line p-4 sm:p-6 shadow-card;
|
||||||
}
|
}
|
||||||
.split-pane {
|
.split-pane {
|
||||||
@apply grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-8
|
@apply grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-8
|
||||||
h-auto lg:h-[calc(100vh-4rem)] min-h-0;
|
h-auto lg:flex-1 lg:min-h-0 min-h-0;
|
||||||
}
|
}
|
||||||
.nav-link {
|
.nav-link {
|
||||||
@apply text-sm text-gray-500 hover:text-gray-800 transition-colors;
|
@apply text-sm text-ink-secondary hover:text-ink-strong transition-colors;
|
||||||
}
|
}
|
||||||
.nav-link.router-link-active {
|
.nav-link.router-link-active {
|
||||||
@apply text-gray-900 font-medium;
|
@apply text-ink-strong font-medium;
|
||||||
}
|
}
|
||||||
.status-badge {
|
.status-badge {
|
||||||
@apply px-2 py-1 rounded-full text-xs font-medium;
|
@apply px-2 py-1 rounded-control text-xs font-medium;
|
||||||
}
|
}
|
||||||
.ocr-banner {
|
.ocr-banner {
|
||||||
@apply bg-blue-50 border border-blue-200 rounded-md p-3 text-sm text-blue-800;
|
@apply bg-primary-50 border border-primary-100 rounded-input p-3 text-sm text-primary-800;
|
||||||
}
|
}
|
||||||
.ocr-high {
|
.ocr-high {
|
||||||
@apply border-l-[3px] border-l-green-500;
|
@apply border-l-[3px] border-l-clinical-safe;
|
||||||
}
|
}
|
||||||
.ocr-medium {
|
.ocr-medium {
|
||||||
@apply border-l-[3px] border-l-yellow-500;
|
@apply border-l-[3px] border-l-clinical-warning;
|
||||||
}
|
}
|
||||||
.ocr-low {
|
.ocr-low {
|
||||||
@apply border-l-[3px] border-l-red-500;
|
@apply border-l-[3px] border-l-clinical-danger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-header">
|
<div class="app-header">
|
||||||
<div class="app-header-title">
|
<div class="app-header-title">
|
||||||
<router-link to="/" class="flex items-center gap-2 shrink-0">
|
<h1 class="text-lg font-semibold text-ink-strong">{{ title }}</h1>
|
||||||
<svg viewBox="0 0 32 32" class="w-6 h-6" aria-hidden="true">
|
|
||||||
<path fill="currentColor" class="text-primary-700" d="M16 2 4 6.5v8.2c0 8 5.1 14.6 12 15.3 6.9-.7 12-7.3 12-15.3V6.5z"/>
|
|
||||||
<path fill="white" d="M16 8.5c-3.6 0-6.6 2.8-6.6 6.3 0 3.4 2.7 6.4 6.6 9.6 3.9-3.2 6.6-6.2 6.6-9.6 0-3.5-3-6.3-6.6-6.3m0 3.4c1.7 0 3.1 1.3 3.1 2.9s-1.4 2.9-3.1 2.9-3.1-1.3-3.1-2.9 1.4-2.9 3.1-2.9"/>
|
|
||||||
</svg>
|
|
||||||
<span class="font-bold text-primary-800 tracking-tight">VigilCare</span>
|
|
||||||
</router-link>
|
|
||||||
<h1 class="text-lg font-semibold">{{ title }}</h1>
|
|
||||||
<nav class="flex gap-3 ml-4">
|
|
||||||
<router-link v-if="auth.canIntake" to="/intake" class="nav-link">Intake</router-link>
|
|
||||||
<router-link v-if="auth.canIntake" to="/cover-sheets" class="nav-link">Cover Sheets</router-link>
|
|
||||||
<router-link v-if="auth.canEntry" to="/entry" class="nav-link">Entry</router-link>
|
|
||||||
<router-link v-if="auth.canVerify" to="/verification" class="nav-link">Verification</router-link>
|
|
||||||
<router-link v-if="auth.canApprove" to="/approval" class="nav-link">Approval</router-link>
|
|
||||||
<router-link v-if="auth.canLiveCapture" to="/live-capture" class="nav-link">Live Capture</router-link>
|
|
||||||
<router-link to="/patients" class="nav-link">Patient History</router-link>
|
|
||||||
<router-link v-if="auth.canSupervise" to="/dashboard" class="nav-link">Queue Dashboard</router-link>
|
|
||||||
<router-link v-if="auth.canSupervise" to="/fhir-explorer" class="nav-link">FHIR Explorer</router-link>
|
|
||||||
</nav>
|
|
||||||
<slot name="subtitle" />
|
<slot name="subtitle" />
|
||||||
</div>
|
</div>
|
||||||
<div class="app-header-actions">
|
<div class="app-header-actions">
|
||||||
<slot name="actions" />
|
<slot name="actions" />
|
||||||
<span class="text-sm text-gray-600">{{ auth.userFullName }}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
@click="auth.logout()"
|
|
||||||
class="text-sm text-gray-500 hover:text-gray-800"
|
|
||||||
>
|
|
||||||
Sign Out
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAuthStore } from '../stores/auth'
|
|
||||||
|
|
||||||
defineProps<{ title: string }>()
|
defineProps<{ title: string }>()
|
||||||
|
|
||||||
const auth = useAuthStore()
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-screen min-h-0 bg-canvas text-ink">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside
|
||||||
|
:class="[
|
||||||
|
'flex flex-col shrink-0 bg-navy text-white transition-[width] duration-200 ease-out',
|
||||||
|
collapsed ? 'w-16' : 'w-60',
|
||||||
|
]"
|
||||||
|
aria-label="Main navigation"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'flex items-center h-16 border-b border-white/10 shrink-0',
|
||||||
|
collapsed ? 'justify-center px-2' : 'gap-2.5 px-4',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 32 32" class="w-7 h-7 shrink-0" aria-hidden="true">
|
||||||
|
<path fill="#155EEF" d="M16 2 4 6.5v8.2c0 8 5.1 14.6 12 15.3 6.9-.7 12-7.3 12-15.3V6.5z"/>
|
||||||
|
<path fill="white" d="M16 8.5c-3.6 0-6.6 2.8-6.6 6.3 0 3.4 2.7 6.4 6.6 9.6 3.9-3.2 6.6-6.2 6.6-9.6 0-3.5-3-6.3-6.6-6.3m0 3.4c1.7 0 3.1 1.3 3.1 2.9s-1.4 2.9-3.1 2.9-3.1-1.3-3.1-2.9 1.4-2.9 3.1-2.9"/>
|
||||||
|
</svg>
|
||||||
|
<div v-if="!collapsed" class="min-w-0 leading-tight">
|
||||||
|
<p class="font-bold tracking-tight text-sm truncate">VigilCare</p>
|
||||||
|
<p class="text-[11px] text-white/60 truncate">Records</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="flex-1 overflow-y-auto py-3 px-2 space-y-4">
|
||||||
|
<div v-if="workspaceItems.length">
|
||||||
|
<p
|
||||||
|
v-if="!collapsed"
|
||||||
|
class="px-2 mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-white/40"
|
||||||
|
>
|
||||||
|
Workspace
|
||||||
|
</p>
|
||||||
|
<ul class="space-y-0.5">
|
||||||
|
<li v-for="item in workspaceItems" :key="item.to">
|
||||||
|
<router-link
|
||||||
|
:to="item.to"
|
||||||
|
:title="collapsed ? item.label : undefined"
|
||||||
|
:class="navItemClass(item.to)"
|
||||||
|
>
|
||||||
|
<span class="shrink-0 w-5 h-5 flex items-center justify-center" aria-hidden="true">
|
||||||
|
<component :is="item.icon" />
|
||||||
|
</span>
|
||||||
|
<span v-if="!collapsed" class="truncate">{{ item.label }}</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="adminItems.length">
|
||||||
|
<p
|
||||||
|
v-if="!collapsed"
|
||||||
|
class="px-2 mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-white/40"
|
||||||
|
>
|
||||||
|
Administration
|
||||||
|
</p>
|
||||||
|
<ul class="space-y-0.5">
|
||||||
|
<li v-for="item in adminItems" :key="item.to">
|
||||||
|
<router-link
|
||||||
|
:to="item.to"
|
||||||
|
:title="collapsed ? item.label : undefined"
|
||||||
|
:class="navItemClass(item.to)"
|
||||||
|
>
|
||||||
|
<span class="shrink-0 w-5 h-5 flex items-center justify-center" aria-hidden="true">
|
||||||
|
<component :is="item.icon" />
|
||||||
|
</span>
|
||||||
|
<span v-if="!collapsed" class="truncate">{{ item.label }}</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="shrink-0 border-t border-white/10 p-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex w-full items-center gap-2.5 rounded-input px-2.5 py-2 text-sm text-white/70 hover:bg-white/10 hover:text-white transition-colors"
|
||||||
|
:class="collapsed ? 'justify-center' : ''"
|
||||||
|
:aria-expanded="!collapsed"
|
||||||
|
:aria-label="collapsed ? 'Expand sidebar' : 'Collapse sidebar'"
|
||||||
|
@click="toggleCollapsed"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 shrink-0 transition-transform"
|
||||||
|
:class="collapsed ? 'rotate-180' : ''"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span v-if="!collapsed">Collapse</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main column -->
|
||||||
|
<div class="flex flex-col flex-1 min-w-0 min-h-0">
|
||||||
|
<header class="flex items-center justify-end gap-4 h-16 shrink-0 px-4 sm:px-6 bg-surface border-b border-line">
|
||||||
|
<span class="text-sm text-ink-secondary truncate">{{ auth.userFullName }}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-sm text-ink-secondary hover:text-ink-strong transition-colors"
|
||||||
|
@click="auth.logout()"
|
||||||
|
>
|
||||||
|
Sign Out
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="flex-1 min-h-0 overflow-auto">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, h, ref, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
|
const COLLAPSE_KEY = 'vigilcare_sidebar_collapsed'
|
||||||
|
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const collapsed = ref(localStorage.getItem(COLLAPSE_KEY) === '1')
|
||||||
|
|
||||||
|
watch(collapsed, (value) => {
|
||||||
|
localStorage.setItem(COLLAPSE_KEY, value ? '1' : '0')
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleCollapsed() {
|
||||||
|
collapsed.value = !collapsed.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function iconPath(d: string) {
|
||||||
|
return () =>
|
||||||
|
h(
|
||||||
|
'svg',
|
||||||
|
{ viewBox: '0 0 20 20', fill: 'currentColor', class: 'w-5 h-5' },
|
||||||
|
[h('path', { 'fill-rule': 'evenodd', 'clip-rule': 'evenodd', d })],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
intake: iconPath(
|
||||||
|
'M3 4.5A1.5 1.5 0 014.5 3h11A1.5 1.5 0 0117 4.5v11a1.5 1.5 0 01-1.5 1.5h-11A1.5 1.5 0 013 15.5v-11zM5.5 6a.5.5 0 000 1h9a.5.5 0 000-1h-9zm0 3a.5.5 0 000 1h9a.5.5 0 000-1h-9zm0 3a.5.5 0 000 1h6a.5.5 0 000-1h-6z',
|
||||||
|
),
|
||||||
|
cover: iconPath(
|
||||||
|
'M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm1 3.5A.5.5 0 015.5 6h9a.5.5 0 010 1h-9A.5.5 0 015 6.5zM5.5 9a.5.5 0 000 1h5a.5.5 0 000-1h-5z',
|
||||||
|
),
|
||||||
|
entry: iconPath(
|
||||||
|
'M3.5 3A1.5 1.5 0 002 4.5v11A1.5 1.5 0 003.5 17h13a1.5 1.5 0 001.5-1.5v-11A1.5 1.5 0 0016.5 3h-13zM5 7.5A.5.5 0 015.5 7h9a.5.5 0 010 1h-9A.5.5 0 015 7.5zm0 3A.5.5 0 015.5 10h9a.5.5 0 010 1h-9A.5.5 0 015 10.5zm0 3A.5.5 0 015.5 13h5a.5.5 0 010 1h-5A.5.5 0 015 13.5z',
|
||||||
|
),
|
||||||
|
verify: iconPath(
|
||||||
|
'M10 2a8 8 0 100 16 8 8 0 000-16zm3.53 6.47a.75.75 0 00-1.06-1.06L9 10.88 7.53 9.41a.75.75 0 10-1.06 1.06l2 2a.75.75 0 001.06 0l4-4z',
|
||||||
|
),
|
||||||
|
approve: iconPath(
|
||||||
|
'M10 1.5l7.5 3v5.25c0 4.5-2.9 8.25-7.5 9.25-4.6-1-7.5-4.75-7.5-9.25V4.5L10 1.5zm3.03 6.03a.75.75 0 00-1.06-1.06L9 9.44 7.53 7.97a.75.75 0 10-1.06 1.06l2 2a.75.75 0 001.06 0l3.5-3.5z',
|
||||||
|
),
|
||||||
|
live: iconPath(
|
||||||
|
'M4 5a2 2 0 012-2h8a2 2 0 012 2v6a2 2 0 01-2 2H9.414l-2.707 2.707A1 1 0 015 15.293V13H6a2 2 0 01-2-2V5zm3.5 2.5a.75.75 0 000 1.5h5a.75.75 0 000-1.5h-5z',
|
||||||
|
),
|
||||||
|
patients: iconPath(
|
||||||
|
'M10 2a4 4 0 100 8 4 4 0 000-8zM3.5 16.5a6.5 6.5 0 0113 0 .75.75 0 01-.75.75h-11.5a.75.75 0 01-.75-.75z',
|
||||||
|
),
|
||||||
|
dashboard: iconPath(
|
||||||
|
'M3 4.5A1.5 1.5 0 014.5 3h3A1.5 1.5 0 019 4.5v3A1.5 1.5 0 017.5 9h-3A1.5 1.5 0 013 7.5v-3zM11 4.5A1.5 1.5 0 0112.5 3h3A1.5 1.5 0 0117 4.5v3A1.5 1.5 0 0115.5 9h-3A1.5 1.5 0 0111 7.5v-3zM3 12.5A1.5 1.5 0 014.5 11h3A1.5 1.5 0 019 12.5v3A1.5 1.5 0 017.5 17h-3A1.5 1.5 0 013 15.5v-3zM11 12.5a1.5 1.5 0 011.5-1.5h3a1.5 1.5 0 011.5 1.5v3a1.5 1.5 0 01-1.5 1.5h-3a1.5 1.5 0 01-1.5-1.5v-3z',
|
||||||
|
),
|
||||||
|
fhir: iconPath(
|
||||||
|
'M4.5 3A1.5 1.5 0 003 4.5v11A1.5 1.5 0 004.5 17h11a1.5 1.5 0 001.5-1.5v-11A1.5 1.5 0 0015.5 3h-11zM6 7.25a.75.75 0 01.75-.75h6.5a.75.75 0 010 1.5h-6.5A.75.75 0 016 7.25zm0 3a.75.75 0 01.75-.75h6.5a.75.75 0 010 1.5h-6.5A.75.75 0 016 10.25zm0 3a.75.75 0 01.75-.75h3.5a.75.75 0 010 1.5h-3.5A.75.75 0 016 13.25z',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
label: string
|
||||||
|
to: string
|
||||||
|
icon: () => ReturnType<typeof h>
|
||||||
|
show: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceItems = computed<NavItem[]>(() =>
|
||||||
|
[
|
||||||
|
{ label: 'Intake', to: '/intake', icon: icons.intake, show: auth.canIntake },
|
||||||
|
{ label: 'Cover Sheets', to: '/cover-sheets', icon: icons.cover, show: auth.canIntake },
|
||||||
|
{ label: 'Data Entry', to: '/entry', icon: icons.entry, show: auth.canEntry },
|
||||||
|
{ label: 'Verification', to: '/verification', icon: icons.verify, show: auth.canVerify },
|
||||||
|
{ label: 'Clinical Approval', to: '/approval', icon: icons.approve, show: auth.canApprove },
|
||||||
|
{ label: 'Live Capture', to: '/live-capture', icon: icons.live, show: auth.canLiveCapture },
|
||||||
|
{ label: 'Patient History', to: '/patients', icon: icons.patients, show: auth.isAuthenticated },
|
||||||
|
].filter((item) => item.show),
|
||||||
|
)
|
||||||
|
|
||||||
|
const adminItems = computed<NavItem[]>(() =>
|
||||||
|
[
|
||||||
|
{ label: 'Queue Dashboard', to: '/dashboard', icon: icons.dashboard, show: auth.canSupervise },
|
||||||
|
{ label: 'FHIR Explorer', to: '/fhir-explorer', icon: icons.fhir, show: auth.canSupervise },
|
||||||
|
].filter((item) => item.show),
|
||||||
|
)
|
||||||
|
|
||||||
|
function isActive(path: string): boolean {
|
||||||
|
if (path === '/patients') {
|
||||||
|
return route.path === '/patients' || route.path.startsWith('/patients/')
|
||||||
|
}
|
||||||
|
if (path === '/entry') {
|
||||||
|
return route.path === '/entry' || route.path.startsWith('/entry/')
|
||||||
|
}
|
||||||
|
if (path === '/verification') {
|
||||||
|
return route.path === '/verification' || route.path.startsWith('/verification/')
|
||||||
|
}
|
||||||
|
if (path === '/approval') {
|
||||||
|
return route.path === '/approval' || route.path.startsWith('/approval/')
|
||||||
|
}
|
||||||
|
return route.path === path || route.path.startsWith(`${path}/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function navItemClass(path: string): string {
|
||||||
|
const base =
|
||||||
|
'flex items-center gap-2.5 rounded-input text-sm transition-colors ' +
|
||||||
|
(collapsed.value ? 'justify-center px-2 py-2.5' : 'px-2.5 py-2')
|
||||||
|
if (isActive(path)) {
|
||||||
|
return `${base} bg-primary-600 text-white font-medium`
|
||||||
|
}
|
||||||
|
return `${base} text-white/75 hover:bg-white/10 hover:text-white`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<AppShell>
|
||||||
|
<router-view />
|
||||||
|
</AppShell>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import AppShell from '../components/AppShell.vue'
|
||||||
|
</script>
|
||||||
@@ -9,32 +9,46 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: { requiresAuth: false },
|
meta: { requiresAuth: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/intake',
|
path: '/',
|
||||||
|
component: () => import('../layouts/AuthenticatedLayout.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirect: () => {
|
||||||
|
const auth = useAuthStore()
|
||||||
|
return auth.isAuthenticated
|
||||||
|
? getDefaultRouteForRole(auth.userRole)
|
||||||
|
: '/login'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'intake',
|
||||||
name: 'Intake',
|
name: 'Intake',
|
||||||
component: () => import('../views/IntakeView.vue'),
|
component: () => import('../views/IntakeView.vue'),
|
||||||
meta: { requiresAuth: true, roles: ['INTAKE_CLERK', 'ADMINISTRATOR'] },
|
meta: { requiresAuth: true, roles: ['INTAKE_CLERK', 'ADMINISTRATOR'] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/cover-sheets',
|
path: 'cover-sheets',
|
||||||
name: 'CoverSheets',
|
name: 'CoverSheets',
|
||||||
component: () => import('../views/CoverSheetView.vue'),
|
component: () => import('../views/CoverSheetView.vue'),
|
||||||
meta: { requiresAuth: true, roles: ['INTAKE_CLERK', 'ADMINISTRATOR'] },
|
meta: { requiresAuth: true, roles: ['INTAKE_CLERK', 'ADMINISTRATOR'] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/entry',
|
path: 'entry',
|
||||||
name: 'EntryQueue',
|
name: 'EntryQueue',
|
||||||
component: () => import('../views/EntryView.vue'),
|
component: () => import('../views/EntryView.vue'),
|
||||||
meta: { requiresAuth: true, roles: ['DATA_ENTRY_CLERK', 'ADMINISTRATOR'] },
|
meta: { requiresAuth: true, roles: ['DATA_ENTRY_CLERK', 'ADMINISTRATOR'] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/entry/:batchId',
|
path: 'entry/:batchId',
|
||||||
name: 'EntryBatch',
|
name: 'EntryBatch',
|
||||||
component: () => import('../views/EntryView.vue'),
|
component: () => import('../views/EntryView.vue'),
|
||||||
meta: { requiresAuth: true, roles: ['DATA_ENTRY_CLERK', 'ADMINISTRATOR'] },
|
meta: { requiresAuth: true, roles: ['DATA_ENTRY_CLERK', 'ADMINISTRATOR'] },
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/verification',
|
path: 'verification',
|
||||||
name: 'VerificationQueue',
|
name: 'VerificationQueue',
|
||||||
component: () => import('../views/VerificationView.vue'),
|
component: () => import('../views/VerificationView.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
@@ -43,7 +57,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/verification/:batchId',
|
path: 'verification/:batchId',
|
||||||
name: 'VerificationBatch',
|
name: 'VerificationBatch',
|
||||||
component: () => import('../views/VerificationView.vue'),
|
component: () => import('../views/VerificationView.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
@@ -53,7 +67,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/approval',
|
path: 'approval',
|
||||||
name: 'ApprovalQueue',
|
name: 'ApprovalQueue',
|
||||||
component: () => import('../views/ApprovalView.vue'),
|
component: () => import('../views/ApprovalView.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
@@ -62,7 +76,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/approval/:batchId',
|
path: 'approval/:batchId',
|
||||||
name: 'ApprovalBatch',
|
name: 'ApprovalBatch',
|
||||||
component: () => import('../views/ApprovalView.vue'),
|
component: () => import('../views/ApprovalView.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
@@ -72,38 +86,40 @@ const routes: RouteRecordRaw[] = [
|
|||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/patients/:patientId/history',
|
path: 'patients/:patientId/history',
|
||||||
name: 'PatientHistory',
|
name: 'PatientHistory',
|
||||||
component: () => import('../views/PatientHistoryView.vue'),
|
component: () => import('../views/PatientHistoryView.vue'),
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/patients',
|
path: 'patients',
|
||||||
name: 'PatientSearch',
|
name: 'PatientSearch',
|
||||||
component: () => import('../views/PatientHistoryView.vue'),
|
component: () => import('../views/PatientHistoryView.vue'),
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/live-capture',
|
path: 'live-capture',
|
||||||
name: 'LiveCapture',
|
name: 'LiveCapture',
|
||||||
component: () => import('../views/LiveCaptureView.vue'),
|
component: () => import('../views/LiveCaptureView.vue'),
|
||||||
meta: { requiresAuth: true, roles: ['CLINICIAN', 'ADMINISTRATOR'] },
|
meta: { requiresAuth: true, roles: ['CLINICIAN', 'ADMINISTRATOR'] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/dashboard',
|
path: 'dashboard',
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
component: () => import('../views/QueueDashboardView.vue'),
|
component: () => import('../views/QueueDashboardView.vue'),
|
||||||
meta: { requiresAuth: true, roles: ['ADMINISTRATOR'] },
|
meta: { requiresAuth: true, roles: ['ADMINISTRATOR'] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/fhir-explorer',
|
path: 'fhir-explorer',
|
||||||
name: 'FhirExplorer',
|
name: 'FhirExplorer',
|
||||||
component: () => import('../views/FhirExplorerView.vue'),
|
component: () => import('../views/FhirExplorerView.vue'),
|
||||||
meta: { requiresAuth: true, roles: ['ADMINISTRATOR'] },
|
meta: { requiresAuth: true, roles: ['ADMINISTRATOR'] },
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/:pathMatch(.*)*',
|
||||||
redirect: '/login',
|
redirect: '/login',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen lg:h-screen flex flex-col">
|
<div class="h-full min-h-0 flex flex-col">
|
||||||
<AppHeader title="Clinical Approval">
|
<AppHeader title="Clinical Approval">
|
||||||
<template #subtitle>
|
<template #subtitle>
|
||||||
<span v-if="currentBatch" class="text-sm text-gray-500">
|
<span v-if="currentBatch" class="text-sm text-gray-500">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen flex flex-col">
|
<div class="h-full min-h-0 flex flex-col">
|
||||||
<AppHeader title="Cover Sheets" />
|
<AppHeader title="Cover Sheets" />
|
||||||
|
|
||||||
<div class="p-4 sm:p-6 lg:p-8 max-w-6xl mx-auto flex-1 w-full">
|
<div class="p-4 sm:p-6 lg:p-8 max-w-6xl mx-auto flex-1 w-full">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen lg:h-screen flex flex-col">
|
<div class="h-full min-h-0 flex flex-col">
|
||||||
<AppHeader title="Data Entry">
|
<AppHeader title="Data Entry">
|
||||||
<template #subtitle>
|
<template #subtitle>
|
||||||
<span v-if="currentBatch" class="text-sm text-gray-500">
|
<span v-if="currentBatch" class="text-sm text-gray-500">
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<!-- Queue view (no batch selected) -->
|
<!-- Queue view (no batch selected) -->
|
||||||
<div v-if="!batchId" class="flex-1 p-4 sm:p-6">
|
<div v-if="!batchId" class="flex-1 p-4 sm:p-6">
|
||||||
<h2 class="text-xl font-semibold mb-4">Entry Queue</h2>
|
<h2 class="text-xl font-semibold mb-4">Data Entry Queue</h2>
|
||||||
<BatchList
|
<BatchList
|
||||||
:batches="batchStore.batches"
|
:batches="batchStore.batches"
|
||||||
:loading="batchStore.loading"
|
:loading="batchStore.loading"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen flex flex-col">
|
<div class="h-full min-h-0 flex flex-col">
|
||||||
<AppHeader title="FHIR Explorer" />
|
<AppHeader title="FHIR Explorer" />
|
||||||
|
|
||||||
<div class="p-4 sm:p-6 lg:p-8 max-w-6xl mx-auto flex-1 w-full space-y-6">
|
<div class="p-4 sm:p-6 lg:p-8 max-w-6xl mx-auto flex-1 w-full space-y-6">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen flex flex-col">
|
<div class="h-full min-h-0 flex flex-col">
|
||||||
<AppHeader title="Intake" />
|
<AppHeader title="Intake" />
|
||||||
|
|
||||||
<div class="page-container flex-1">
|
<div class="page-container flex-1">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen flex flex-col">
|
<div class="h-full min-h-0 flex flex-col">
|
||||||
<AppHeader title="Live Capture" />
|
<AppHeader title="Live Capture" />
|
||||||
|
|
||||||
<div class="flex-1 p-4 sm:p-6 lg:p-8 max-w-5xl mx-auto w-full">
|
<div class="flex-1 p-4 sm:p-6 lg:p-8 max-w-5xl mx-auto w-full">
|
||||||
|
|||||||
@@ -1,33 +1,92 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 px-4 py-8">
|
<div class="min-h-screen flex flex-col lg:flex-row bg-canvas">
|
||||||
<div class="max-w-md w-full bg-white rounded-lg shadow-md p-6 sm:p-8">
|
<!-- Brand panel -->
|
||||||
<div class="text-center mb-8">
|
<aside
|
||||||
<h1 class="text-2xl font-bold text-gray-900">VigilCare Records</h1>
|
class="relative flex flex-col justify-between bg-navy text-white px-8 py-10 lg:w-[45%] lg:min-h-screen lg:px-12 lg:py-14"
|
||||||
<p class="text-gray-600 mt-2">Digitization Workstation</p>
|
>
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute inset-0 opacity-[0.07]"
|
||||||
|
aria-hidden="true"
|
||||||
|
style="background-image: radial-gradient(circle at 20% 20%, #155EEF 0%, transparent 45%), radial-gradient(circle at 80% 70%, #EEF4FF 0%, transparent 40%);"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<svg viewBox="0 0 32 32" class="w-9 h-9 shrink-0" aria-hidden="true">
|
||||||
|
<path fill="#155EEF" d="M16 2 4 6.5v8.2c0 8 5.1 14.6 12 15.3 6.9-.7 12-7.3 12-15.3V6.5z"/>
|
||||||
|
<path fill="white" d="M16 8.5c-3.6 0-6.6 2.8-6.6 6.3 0 3.4 2.7 6.4 6.6 9.6 3.9-3.2 6.6-6.2 6.6-9.6 0-3.5-3-6.3-6.6-6.3m0 3.4c1.7 0 3.1 1.3 3.1 2.9s-1.4 2.9-3.1 2.9-3.1-1.3-3.1-2.9 1.4-2.9 3.1-2.9"/>
|
||||||
|
</svg>
|
||||||
|
<div class="leading-tight">
|
||||||
|
<p class="font-bold text-lg tracking-tight">VigilCare</p>
|
||||||
|
<p class="text-sm text-white/60">Records</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form @submit.prevent="handleLogin" class="space-y-4">
|
<div class="mt-10 lg:mt-20 max-w-md">
|
||||||
|
<h1 class="text-[2rem] sm:text-[2.5rem] lg:text-[2.625rem] font-bold leading-tight tracking-tight">
|
||||||
|
Digitize. Verify. Trust.
|
||||||
|
</h1>
|
||||||
|
<p class="mt-4 text-base text-white/70 leading-relaxed">
|
||||||
|
Scanned clinical records become governed, structured data your teams can verify and trust.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Restrained flow hint (desktop) -->
|
||||||
|
<div
|
||||||
|
class="relative hidden lg:flex items-center gap-3 mt-16 text-sm text-white/50"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<span class="rounded-input border border-white/15 bg-white/5 px-3 py-1.5">Paper record</span>
|
||||||
|
<span class="text-white/30">→</span>
|
||||||
|
<span class="rounded-input border border-white/15 bg-white/5 px-3 py-1.5">Verified digital</span>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Auth panel -->
|
||||||
|
<div class="flex flex-1 items-center justify-center px-4 py-10 sm:px-8">
|
||||||
|
<div class="w-full max-w-[440px] bg-surface border border-line rounded-card shadow-card p-6 sm:p-8">
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-[1.75rem] font-semibold text-ink-strong leading-tight">Sign in</h2>
|
||||||
|
<p class="mt-2 text-sm text-ink-secondary">
|
||||||
|
Use your VigilCare workstation credentials.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleLogin" class="space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Username</label>
|
<label for="login-username" class="block text-sm font-semibold text-ink mb-2">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
|
id="login-username"
|
||||||
v-model="username"
|
v-model="username"
|
||||||
type="text"
|
type="text"
|
||||||
|
autocomplete="username"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
placeholder="e.g. intake1, entry1, verifier1"
|
placeholder="e.g. intake1, entry1, verifier1"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Password</label>
|
<label for="login-password" class="block text-sm font-semibold text-ink mb-2">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
|
id="login-password"
|
||||||
v-model="password"
|
v-model="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="errorMessage" class="text-clinical-danger text-sm">
|
<div
|
||||||
|
v-if="errorMessage"
|
||||||
|
class="rounded-input bg-clinical-danger-bg border border-clinical-danger/20 px-3 py-2 text-sm text-clinical-danger"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -37,6 +96,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen flex flex-col">
|
<div class="h-full min-h-0 flex flex-col">
|
||||||
<AppHeader title="Patient History" />
|
<AppHeader title="Patient History" />
|
||||||
|
|
||||||
<div class="flex-1 p-4 sm:p-6 lg:p-8 max-w-5xl mx-auto w-full">
|
<div class="flex-1 p-4 sm:p-6 lg:p-8 max-w-5xl mx-auto w-full">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen lg:h-screen flex flex-col">
|
<div class="h-full min-h-0 flex flex-col">
|
||||||
<AppHeader title="Supervisor Dashboard">
|
<AppHeader title="Queue Dashboard">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen lg:h-screen flex flex-col">
|
<div class="h-full min-h-0 flex flex-col">
|
||||||
<AppHeader title="Verification">
|
<AppHeader title="Verification">
|
||||||
<template #subtitle>
|
<template #subtitle>
|
||||||
<span v-if="currentBatch" class="text-sm text-gray-500">
|
<span v-if="currentBatch" class="text-sm text-gray-500">
|
||||||
|
|||||||
@@ -7,20 +7,35 @@ export default {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
|
navy: '#08254A',
|
||||||
primary: {
|
primary: {
|
||||||
50: '#eff9f8',
|
50: '#EEF4FF',
|
||||||
100: '#d9f0ee',
|
100: '#D1E0FF',
|
||||||
300: '#92d2ca',
|
300: '#84ADFF',
|
||||||
400: '#5cb9ae',
|
400: '#528BFF',
|
||||||
500: '#2f9d91',
|
500: '#155EEF',
|
||||||
600: '#1f8075',
|
600: '#155EEF',
|
||||||
700: '#17655d',
|
700: '#0B4FD6',
|
||||||
800: '#124e48',
|
800: '#0A3FAA',
|
||||||
},
|
},
|
||||||
|
surface: '#FFFFFF',
|
||||||
|
canvas: '#F7F9FC',
|
||||||
|
// Flat keys so @apply text-ink / border-line resolve (nested DEFAULT breaks @apply)
|
||||||
|
ink: '#344054',
|
||||||
|
'ink-strong': '#101828',
|
||||||
|
'ink-secondary': '#667085',
|
||||||
|
'ink-disabled': '#98A2B3',
|
||||||
|
line: '#E4E7EC',
|
||||||
|
'line-strong': '#D0D5DD',
|
||||||
clinical: {
|
clinical: {
|
||||||
safe: '#2f9e58',
|
safe: '#079455',
|
||||||
warning: '#c98a1b',
|
'safe-bg': '#ECFDF3',
|
||||||
danger: '#c2293f',
|
warning: '#DC6803',
|
||||||
|
'warning-bg': '#FFFAEB',
|
||||||
|
danger: '#D92D20',
|
||||||
|
'danger-bg': '#FEF3F2',
|
||||||
|
critical: '#B42318',
|
||||||
|
'critical-bg': '#FEE4E2',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
@@ -32,6 +47,16 @@ export default {
|
|||||||
'sans-serif',
|
'sans-serif',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
borderRadius: {
|
||||||
|
control: '6px',
|
||||||
|
input: '8px',
|
||||||
|
card: '10px',
|
||||||
|
panel: '12px',
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
card: '0 1px 2px rgba(16, 24, 40, 0.04)',
|
||||||
|
dialog: '0 8px 24px rgba(16, 24, 40, 0.12)',
|
||||||
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
18: '4.5rem',
|
18: '4.5rem',
|
||||||
22: '5.5rem',
|
22: '5.5rem',
|
||||||
|
|||||||