diff --git a/vigilcare-records-web/src/__tests__/views/CoverSheetView.test.ts b/vigilcare-records-web/src/__tests__/views/CoverSheetView.test.ts index 77e79a0..e2ead13 100644 --- a/vigilcare-records-web/src/__tests__/views/CoverSheetView.test.ts +++ b/vigilcare-records-web/src/__tests__/views/CoverSheetView.test.ts @@ -28,6 +28,31 @@ vi.mock('@/components/PatientSearch.vue', () => ({ }, })) +vi.mock('@/components/StatusBadge.vue', () => ({ + default: { + props: ['status'], + template: '{{ status }}', + }, +})) + +vi.mock('@/components/EmptyState.vue', () => ({ + default: { + props: ['title', 'description'], + template: '
{{ title }}
', + }, +})) + +vi.mock('@/components/SkeletonBlock.vue', () => ({ + default: { template: '
' }, +})) + +vi.mock('@/components/InlineError.vue', () => ({ + default: { + props: ['title', 'message'], + template: '
{{ message }}
', + }, +})) + import { get, post } from '@/api/client' const mockedGet = vi.mocked(get) @@ -75,18 +100,33 @@ beforeEach(() => { }) describe('CoverSheetView', () => { - it('renders generate form and cover sheet list', async () => { - const wrapper = mount(CoverSheetView) + it('renders generate form, preview, and cover sheet list', async () => { + const wrapper = mount(CoverSheetView, { + global: { + stubs: { + RouterLink: { template: '' }, + }, + }, + }) await flushPromises() expect(wrapper.text()).toContain('Generate Cover Sheets') - expect(wrapper.text()).toContain('Cover Sheet List') + expect(wrapper.text()).toContain('Preview') + expect(wrapper.text()).toContain('Existing Cover Sheets') expect(wrapper.text()).toContain('VCR-CS-AABBCCDD') + expect(wrapper.find('.cover-sheet-preview').exists()).toBe(true) + expect(wrapper.find('[data-testid="status-badge"]').text()).toBe('UNUSED') expect(wrapper.find('select').exists()).toBe(true) }) it('loads entry clerks and cover sheets on mount', async () => { - mount(CoverSheetView) + mount(CoverSheetView, { + global: { + stubs: { + RouterLink: { template: '' }, + }, + }, + }) await flushPromises() expect(mockedGet).toHaveBeenCalledWith('users', { role: 'DATA_ENTRY_CLERK' }) @@ -104,7 +144,13 @@ describe('CoverSheetView', () => { error: null, }) - const wrapper = mount(CoverSheetView) + const wrapper = mount(CoverSheetView, { + global: { + stubs: { + RouterLink: { template: '' }, + }, + }, + }) await flushPromises() const batchTypeSelect = wrapper.findAll('select')[0] @@ -134,7 +180,13 @@ describe('CoverSheetView', () => { error: null, }) - const wrapper = mount(CoverSheetView) + const wrapper = mount(CoverSheetView, { + global: { + stubs: { + RouterLink: { template: '' }, + }, + }, + }) await flushPromises() await wrapper.findAll('select')[0].setValue('LAB_RESULTS') @@ -142,5 +194,6 @@ describe('CoverSheetView', () => { await flushPromises() expect(wrapper.text()).toContain('Print Cover Sheets') + expect(wrapper.find('button.btn-primary').text()).toContain('Generate Cover Sheets') }) }) diff --git a/vigilcare-records-web/src/__tests__/views/FhirExplorerView.test.ts b/vigilcare-records-web/src/__tests__/views/FhirExplorerView.test.ts index 4c9621b..81c35bb 100644 --- a/vigilcare-records-web/src/__tests__/views/FhirExplorerView.test.ts +++ b/vigilcare-records-web/src/__tests__/views/FhirExplorerView.test.ts @@ -9,7 +9,9 @@ vi.mock('@/api/fhirClient', () => ({ })) vi.mock('@/components/AppHeader.vue', () => ({ - default: { template: '
' }, + default: { + template: '
', + }, })) vi.mock('@/components/PatientSearch.vue', () => ({ @@ -20,6 +22,20 @@ vi.mock('@/components/PatientSearch.vue', () => ({ }, })) +vi.mock('@/components/InlineError.vue', () => ({ + default: { + props: ['title', 'message'], + template: '
{{ message }}
', + }, +})) + +vi.mock('@/components/EmptyState.vue', () => ({ + default: { + props: ['title', 'description'], + template: '
{{ title }}
', + }, +})) + import { fhirGet, openFhirJsonInNewTab } from '@/api/fhirClient' const mockedFhirGet = vi.mocked(fhirGet) @@ -76,6 +92,7 @@ describe('FhirExplorerView', () => { expect(wrapper.text()).toContain('Patient $everything') expect(wrapper.find('select').element).toBeTruthy() expect(wrapper.text()).toContain('MRN (identifier)') + expect(wrapper.text()).toContain('CapabilityStatement') }) it('searches FHIR Patient resources and shows results table', async () => { @@ -106,6 +123,7 @@ describe('FhirExplorerView', () => { expect(wrapper.text()).toContain('"resourceType": "Patient"') expect(wrapper.text()).toContain('"id": "patient-1"') + expect(wrapper.find('.fhir-json-panel').exists()).toBe(true) }) it('loads Patient $everything bundle grouped by resource type', async () => { diff --git a/vigilcare-records-web/src/__tests__/views/IntakeView.test.ts b/vigilcare-records-web/src/__tests__/views/IntakeView.test.ts index 6d06902..6ccdd67 100644 --- a/vigilcare-records-web/src/__tests__/views/IntakeView.test.ts +++ b/vigilcare-records-web/src/__tests__/views/IntakeView.test.ts @@ -42,12 +42,20 @@ vi.mock('@/components/AssignClerkDialog.vue', () => ({ }, })) +vi.mock('@/components/InlineError.vue', () => ({ + default: { + props: ['title', 'message'], + template: '

{{ title }}

{{ message }}

', + }, +})) + const uploadBatchMock = vi.fn() vi.mock('@/stores/batches', () => ({ useBatchStore: () => ({ batches: [], loading: false, + error: null, listBatches: vi.fn(), uploadBatch: uploadBatchMock, assignBatch: vi.fn(), @@ -89,7 +97,9 @@ describe('IntakeView cover sheet upload', () => { const wrapper = mount(IntakeView) const input = wrapper.find('input[placeholder="VCR-CS-XXXXXXXX"]') - expect(wrapper.text()).toContain('Quick Upload with Cover Sheet') + expect(wrapper.text()).toContain('Cover Sheet Code') + expect(wrapper.text()).toContain('Upload Scanned Document') + expect(wrapper.find('.upload-dropzone').exists()).toBe(true) expect(input.exists()).toBe(true) expect(input.attributes('autofocus')).toBeDefined() expect(wrapper.find('button.btn-secondary').text()).toBe('Lookup') @@ -147,7 +157,7 @@ describe('IntakeView cover sheet upload', () => { undefined, 'VCR-CS-A3F7B2D1', ) - expect(wrapper.text()).toContain('Upload with Cover Sheet') + expect(wrapper.text()).toContain('Batch uploaded with cover sheet') }) it('keeps manual upload available without a cover sheet', async () => { @@ -155,6 +165,7 @@ describe('IntakeView cover sheet upload', () => { expect(wrapper.text()).toContain('New Batch') expect(wrapper.text()).toContain('Upload and Create Batch') + expect(wrapper.text()).toContain('Recent Uploads') const file = new File(['pdf'], 'scan.pdf', { type: 'application/pdf' }) const fileInput = wrapper.find('input[type="file"]') diff --git a/vigilcare-records-web/src/assets/main.css b/vigilcare-records-web/src/assets/main.css index 3a72849..4f1cdf3 100644 --- a/vigilcare-records-web/src/assets/main.css +++ b/vigilcare-records-web/src/assets/main.css @@ -173,4 +173,29 @@ .ocr-badge-low { @apply bg-clinical-danger-bg text-clinical-danger; } + /* Phase 17 — Intake upload zone */ + .upload-dropzone { + @apply flex items-center justify-center min-h-[200px] sm:min-h-[240px] + rounded-card border-2 border-dashed border-primary-300 bg-primary-50/40 + px-6 py-10 cursor-pointer transition-colors + hover:border-primary-500 hover:bg-primary-50; + } + .upload-dropzone--active { + @apply border-primary-600 bg-primary-50; + } + .upload-dropzone--error { + @apply border-clinical-danger bg-clinical-danger-bg; + } + /* Phase 17 — Cover sheet paper preview */ + .cover-sheet-preview { + @apply bg-white border border-line-strong rounded-sm + aspect-[210/297] max-h-[520px] w-full mx-auto overflow-hidden + flex flex-col; + box-shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 4px 16px rgba(16, 24, 40, 0.08); + } + /* Phase 17 — FHIR raw JSON panel */ + .fhir-json-panel { + @apply bg-navy text-[#D1E0FF] text-[13px] leading-5 font-mono + p-4 rounded-input overflow-x-auto max-h-96 border border-navy; + } } diff --git a/vigilcare-records-web/src/utils/batchStatus.ts b/vigilcare-records-web/src/utils/batchStatus.ts index d6bafaa..e07e21e 100644 --- a/vigilcare-records-web/src/utils/batchStatus.ts +++ b/vigilcare-records-web/src/utils/batchStatus.ts @@ -36,6 +36,9 @@ export const BATCH_STATUS_META: Record = { APPROVED: { label: 'Approved', tone: 'success', icon: 'approve' }, PROMOTED: { label: 'Promoted', tone: 'success', icon: 'done' }, CANCELLED: { label: 'Cancelled', tone: 'muted', icon: 'cancel' }, + /** Cover sheet list statuses */ + UNUSED: { label: 'Unused', tone: 'success', icon: 'check' }, + USED: { label: 'Used', tone: 'muted', icon: 'done' }, } export const BATCH_STATUS_TONE_CLASSES: Record = { diff --git a/vigilcare-records-web/src/views/CoverSheetView.vue b/vigilcare-records-web/src/views/CoverSheetView.vue index 9455290..e429e9f 100644 --- a/vigilcare-records-web/src/views/CoverSheetView.vue +++ b/vigilcare-records-web/src/views/CoverSheetView.vue @@ -2,210 +2,324 @@
-
-

Cover Sheet Management

- - -
-

Generate Cover Sheets

- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- {{ generateError }} -
- -
- - -
-
-
- - -
-
-

Cover Sheet List

- -
-
- - -
-
- - -
-
+
+
+
+

+ Cover Sheet Management +

+

+ Generate printable separators that reconnect scanned paper to batch metadata. +

-
- {{ listError }} -
+ +
+
+

Generate Cover Sheets

-
-
Loading...
-
- No cover sheets found. -
- - - - - - - - - - - - - - - +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+ + +
+ +

-

- - - - - - - - - -
CodeBatch TypeTrackPatientAssigned ToStatusLinked BatchCreated
{{ sheet.code }}{{ formatBatchType(sheet.batchType) }} + Last run: {{ lastGeneratedIds.length }} cover sheet(s) ready to print. +

+ + + + +
+

Preview

+

+ Approximate printable layout for the current form values. +

+ +
+
+ +
+

VigilCare

+

Records · Cover Sheet

+
+
+ +
+ +
- - - - {{ sheet.assignToUserName ?? '—' }} - - - {{ sheet.isUsed ? 'Used' : 'Unused' }} - - - - {{ sheet.batchId.substring(0, 8) }}... - - - - {{ formatDate(sheet.createdAt) }} -
+ v-for="n in 25" + :key="n" + class="bg-ink-strong" + :class="qrCellClass(n)" + /> +
+ +
+

+ VCR-CS-XXXXXXXX +

+

+ {{ previewBatchType }} +

+

+ {{ track === 'BACKFILL' ? 'Backfill (Track A)' : 'Live Capture (Track B)' }} +

+
+ +
+
+
Quantity
+
{{ previewCount }}
+
+
+
Patient
+
+ {{ patientId ? 'Linked' : 'Not linked' }} +
+
+
+
Entry clerk
+
+ {{ assignedClerkLabel }} +
+
+
+
Generated
+
{{ previewTimestamp }}
+
+
+
+
+
-
-

Page {{ page }}

-
- - + +
+
+

Existing Cover Sheets

+ +
+
+ + +
+
+ + +
+
-
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
CodeBatch TypeTrackPatientAssigned ToStatusLinked BatchCreated
{{ sheet.code }}{{ formatBatchType(sheet.batchType) }} + + {{ sheet.track === 'BACKFILL' ? 'Backfill' : 'Live' }} + + + + + + {{ sheet.assignToUserName ?? '—' }} + + + + + {{ sheet.batchId.substring(0, 8) }}… + + + + {{ formatDate(sheet.createdAt) }} +
+
+ +
+

Page {{ page }}

+
+ + +
+
+
@@ -217,6 +331,10 @@ import { get, post, postBlob } from '../api/client' import { useToast } from '../composables/useToast' import AppHeader from '../components/AppHeader.vue' import PatientSearch from '../components/PatientSearch.vue' +import StatusBadge from '../components/StatusBadge.vue' +import EmptyState from '../components/EmptyState.vue' +import SkeletonBlock from '../components/SkeletonBlock.vue' +import InlineError from '../components/InlineError.vue' import type { CoverSheetResponse, UserSummary } from '../types' const toast = useToast() @@ -244,6 +362,25 @@ const filterPatientId = ref() const hasNextPage = computed(() => coverSheets.value.length === pageSize) +const previewCount = computed(() => Math.min(100, Math.max(1, count.value || 1))) + +const previewBatchType = computed(() => + batchType.value ? formatBatchType(batchType.value) : 'Select a batch type', +) + +const assignedClerkLabel = computed(() => { + if (!assignToUserId.value) return 'None' + const clerk = clerks.value.find(c => c.id === assignToUserId.value) + return clerk?.fullName ?? 'Selected' +}) + +const previewTimestamp = computed(() => + new Date().toLocaleString(undefined, { + dateStyle: 'medium', + timeStyle: 'short', + }), +) + function formatBatchType(type: string): string { return type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) } @@ -252,6 +389,15 @@ function formatDate(value: string): string { return new Date(value).toLocaleString() } +/** Sparse checker pattern for QR placeholder cells */ +function qrCellClass(n: number): string { + const row = Math.ceil(n / 5) + const col = ((n - 1) % 5) + 1 + const corner = (row <= 2 && col <= 2) || (row <= 2 && col >= 4) || (row >= 4 && col <= 2) + const mid = (row + col) % 2 === 0 + return corner || mid ? 'opacity-100' : 'opacity-20' +} + async function loadClerks(): Promise { clerksLoading.value = true try { diff --git a/vigilcare-records-web/src/views/FhirExplorerView.vue b/vigilcare-records-web/src/views/FhirExplorerView.vue index 5e0fc64..f26994d 100644 --- a/vigilcare-records-web/src/views/FhirExplorerView.vue +++ b/vigilcare-records-web/src/views/FhirExplorerView.vue @@ -1,223 +1,334 @@