From 5039dbb97975ee3bec866b08b8c7d7a4b052d903 Mon Sep 17 00:00:00 2001 From: voltsrage Date: Sat, 27 Jun 2026 23:47:56 +0800 Subject: [PATCH] feature: Backend as Single Source of Truth for Batch-Type Field Requirements --- .../Records/Batch/BatchDetailResponse.cs | 2 + .../Batch/BatchTypeFieldRequirements.cs | 70 +++++++++++++++ .../Records/Batch/DraftPayloadResponse.cs | 1 + VigilCareRecordsAPI/Services/DraftService.cs | 1 + .../__tests__/components/EntryForm.test.ts | 53 ++++++----- .../components/VerificationForm.test.ts | 21 +++-- .../__tests__/helpers/fieldRequirements.ts | 87 +++++++++++++++++++ .../src/components/EntryForm.vue | 14 +-- .../src/components/VerificationForm.vue | 8 +- vigilcare-records-web/src/types/index.ts | 32 +++++++ 10 files changed, 245 insertions(+), 44 deletions(-) create mode 100644 VigilCareRecordsAPI/Models/Records/Batch/BatchTypeFieldRequirements.cs create mode 100644 vigilcare-records-web/src/__tests__/helpers/fieldRequirements.ts diff --git a/VigilCareRecordsAPI/Models/Records/Batch/BatchDetailResponse.cs b/VigilCareRecordsAPI/Models/Records/Batch/BatchDetailResponse.cs index a50c518..37dee66 100644 --- a/VigilCareRecordsAPI/Models/Records/Batch/BatchDetailResponse.cs +++ b/VigilCareRecordsAPI/Models/Records/Batch/BatchDetailResponse.cs @@ -7,6 +7,7 @@ public record BatchDetailResponse( string Status, string BatchType, string Track, + BatchTypeFieldRequirements FieldRequirements, Guid? PatientId, string DocumentRef, string? DocumentUrl, @@ -34,6 +35,7 @@ public record BatchDetailResponse( batch.Status.ToDbString(), batch.BatchType.ToDbString(), batch.Track.ToDbString(), + BatchTypeFieldRequirements.ForBatchType(batch.BatchType), batch.PatientId, batch.DocumentRef, documentUrl, diff --git a/VigilCareRecordsAPI/Models/Records/Batch/BatchTypeFieldRequirements.cs b/VigilCareRecordsAPI/Models/Records/Batch/BatchTypeFieldRequirements.cs new file mode 100644 index 0000000..f1d326f --- /dev/null +++ b/VigilCareRecordsAPI/Models/Records/Batch/BatchTypeFieldRequirements.cs @@ -0,0 +1,70 @@ +public record BatchTypeFieldRequirements( + bool ShowPatientDemographics, + bool ShowEncounterContext, + bool ShowEncounterSummaryFields, + bool ShowObservations, + bool ShowAllergies, + bool ShowMedications +) +{ + public static BatchTypeFieldRequirements ForBatchType(BatchType batchType) => batchType switch + { + BatchType.PatientRegistration => new( + ShowPatientDemographics: true, + ShowEncounterContext: false, + ShowEncounterSummaryFields: false, + ShowObservations: false, + ShowAllergies: false, + ShowMedications: false), + + BatchType.VitalsSheet => new( + ShowPatientDemographics: true, + ShowEncounterContext: true, + ShowEncounterSummaryFields: false, + ShowObservations: true, + ShowAllergies: false, + ShowMedications: false), + + BatchType.LabResults => new( + ShowPatientDemographics: true, + ShowEncounterContext: true, + ShowEncounterSummaryFields: false, + ShowObservations: true, + ShowAllergies: false, + ShowMedications: false), + + BatchType.AllergyUpdate => new( + ShowPatientDemographics: true, + ShowEncounterContext: false, + ShowEncounterSummaryFields: false, + ShowObservations: false, + ShowAllergies: true, + ShowMedications: false), + + BatchType.EncounterSummary => new( + ShowPatientDemographics: true, + ShowEncounterContext: true, + ShowEncounterSummaryFields: true, + ShowObservations: false, + ShowAllergies: false, + ShowMedications: false), + + BatchType.MedicationList => new( + ShowPatientDemographics: true, + ShowEncounterContext: false, + ShowEncounterSummaryFields: false, + ShowObservations: false, + ShowAllergies: false, + ShowMedications: true), + + BatchType.Mixed => new( + ShowPatientDemographics: true, + ShowEncounterContext: true, + ShowEncounterSummaryFields: true, + ShowObservations: true, + ShowAllergies: true, + ShowMedications: true), + + _ => throw new ArgumentOutOfRangeException(nameof(batchType)) + }; +} \ No newline at end of file diff --git a/VigilCareRecordsAPI/Models/Records/Batch/DraftPayloadResponse.cs b/VigilCareRecordsAPI/Models/Records/Batch/DraftPayloadResponse.cs index 9bb10e1..919415d 100644 --- a/VigilCareRecordsAPI/Models/Records/Batch/DraftPayloadResponse.cs +++ b/VigilCareRecordsAPI/Models/Records/Batch/DraftPayloadResponse.cs @@ -2,6 +2,7 @@ public record DraftPayloadResponse( Guid BatchId, string Status, string BatchType, + BatchTypeFieldRequirements FieldRequirements, DraftPatientDto? Patient, DraftEncounterDto? Encounter, List Observations diff --git a/VigilCareRecordsAPI/Services/DraftService.cs b/VigilCareRecordsAPI/Services/DraftService.cs index 3c1c130..61fafd5 100644 --- a/VigilCareRecordsAPI/Services/DraftService.cs +++ b/VigilCareRecordsAPI/Services/DraftService.cs @@ -35,6 +35,7 @@ public class DraftService : IDraftService batch.Id, batch.Status.ToDbString(), batch.BatchType.ToDbString(), + BatchTypeFieldRequirements.ForBatchType(batch.BatchType), batch.DraftPatient is not null ? MapPatient(batch.DraftPatient) : null, batch.DraftEncounter is not null ? MapEncounter(batch.DraftEncounter) : null, batch.DraftObservations.Select(MapObservation).OrderBy(o => o.RecordedAt).ToList() diff --git a/vigilcare-records-web/src/__tests__/components/EntryForm.test.ts b/vigilcare-records-web/src/__tests__/components/EntryForm.test.ts index acc2954..1542947 100644 --- a/vigilcare-records-web/src/__tests__/components/EntryForm.test.ts +++ b/vigilcare-records-web/src/__tests__/components/EntryForm.test.ts @@ -4,6 +4,10 @@ import { setActivePinia, createPinia } from 'pinia' import EntryForm from '@/components/EntryForm.vue' import { useBatchStore } from '@/stores/batches' import type { BatchDetailResponse } from '@/types' +import { + emptyDraft, + fieldRequirementsForBatchType, +} from '@/__tests__/helpers/fieldRequirements' vi.mock('@/api/client', () => ({ get: vi.fn(), @@ -24,11 +28,13 @@ vi.mock('@/composables/useToast', () => ({ })) function makeBatch(overrides: Partial = {}): BatchDetailResponse { + const batchType = overrides.batchType ?? 'VITALS' return { id: 'b1', status: 'IN_ENTRY', - batchType: 'VITALS', + batchType, track: 'TRACK_A', + fieldRequirements: fieldRequirementsForBatchType(batchType), patientId: null, documentRef: 'docs/scan.pdf', documentUrl: null, @@ -41,12 +47,25 @@ function makeBatch(overrides: Partial = {}): BatchDetailRes promotionEncounterId: null, supersedesBatchId: null, clinicianAttestation: false, + isCorrection: false, + supersession: null, createdAt: '2026-06-27T10:00:00Z', updatedAt: '2026-06-27T10:00:00Z', ...overrides, } } +function mountWithDraft( + batchOverrides: Partial = {}, +) { + const batch = makeBatch(batchOverrides) + const store = useBatchStore() + store.currentDraft = emptyDraft(batch.batchType) + return mount(EntryForm, { + props: { batch, batchId: 'b1' }, + }) +} + beforeEach(() => { setActivePinia(createPinia()) vi.clearAllMocks() @@ -103,55 +122,41 @@ describe('EntryForm', () => { describe('conditional sections by batch type', () => { it('hides allergies section for VITALS batch', () => { - const wrapper = mount(EntryForm, { - props: { batch: makeBatch({ batchType: 'VITALS' }), batchId: 'b1' }, - }) + const wrapper = mountWithDraft({ batchType: 'VITALS' }) expect(wrapper.text()).not.toContain('Allergies') }) it('shows allergies section for ALLERGY_UPDATE batch', () => { - const wrapper = mount(EntryForm, { - props: { batch: makeBatch({ batchType: 'ALLERGY_UPDATE' }), batchId: 'b1' }, - }) + const wrapper = mountWithDraft({ batchType: 'ALLERGY_UPDATE' }) expect(wrapper.text()).toContain('Allergies') expect(wrapper.text()).toContain('No known allergies') }) it('hides medications section for VITALS batch', () => { - const wrapper = mount(EntryForm, { - props: { batch: makeBatch({ batchType: 'VITALS' }), batchId: 'b1' }, - }) + const wrapper = mountWithDraft({ batchType: 'VITALS' }) expect(wrapper.text()).not.toContain('Medications') }) it('shows medications section for MEDICATION_LIST batch', () => { - const wrapper = mount(EntryForm, { - props: { batch: makeBatch({ batchType: 'MEDICATION_LIST' }), batchId: 'b1' }, - }) + const wrapper = mountWithDraft({ batchType: 'MEDICATION_LIST' }) expect(wrapper.text()).toContain('Medications') expect(wrapper.text()).toContain('No active medications') }) it('shows both allergies and medications for MIXED batch', () => { - const wrapper = mount(EntryForm, { - props: { batch: makeBatch({ batchType: 'MIXED' }), batchId: 'b1' }, - }) + const wrapper = mountWithDraft({ batchType: 'MIXED' }) expect(wrapper.text()).toContain('Allergies') expect(wrapper.text()).toContain('Medications') }) it('hides encounter summary fields for VITALS batch', () => { - const wrapper = mount(EntryForm, { - props: { batch: makeBatch({ batchType: 'VITALS' }), batchId: 'b1' }, - }) + const wrapper = mountWithDraft({ batchType: 'VITALS' }) expect(wrapper.text()).not.toContain('Encounter Status') expect(wrapper.text()).not.toContain('Discharge Diagnosis') }) it('shows encounter summary fields for ENCOUNTER_SUMMARY batch', () => { - const wrapper = mount(EntryForm, { - props: { batch: makeBatch({ batchType: 'ENCOUNTER_SUMMARY' }), batchId: 'b1' }, - }) + const wrapper = mountWithDraft({ batchType: 'ENCOUNTER_SUMMARY' }) expect(wrapper.text()).toContain('Encounter Status') expect(wrapper.text()).toContain('Discharge Diagnosis') }) @@ -164,7 +169,7 @@ describe('EntryForm', () => { }) const store = useBatchStore() - store.currentDraft = { + store.currentDraft = emptyDraft('VITALS', { patient: { id: 'dp1', batchId: 'b1', @@ -180,7 +185,7 @@ describe('EntryForm', () => { }, encounter: null, observations: [], - } + }) await wrapper.vm.$nextTick() const nameInput = wrapper.find('input[type="text"]') diff --git a/vigilcare-records-web/src/__tests__/components/VerificationForm.test.ts b/vigilcare-records-web/src/__tests__/components/VerificationForm.test.ts index 70f4125..8bc0e25 100644 --- a/vigilcare-records-web/src/__tests__/components/VerificationForm.test.ts +++ b/vigilcare-records-web/src/__tests__/components/VerificationForm.test.ts @@ -4,6 +4,10 @@ import { setActivePinia, createPinia } from 'pinia' import VerificationForm from '@/components/VerificationForm.vue' import { useBatchStore } from '@/stores/batches' import type { BatchDetailResponse } from '@/types' +import { + emptyDraft, + fieldRequirementsForBatchType, +} from '@/__tests__/helpers/fieldRequirements' vi.mock('@/api/client', () => ({ get: vi.fn(), @@ -30,11 +34,13 @@ vi.mock('vue-router', () => ({ })) function makeBatch(overrides: Partial = {}): BatchDetailResponse { + const batchType = overrides.batchType ?? 'VITALS' return { id: 'b1', status: 'PENDING_VERIFICATION', - batchType: 'VITALS', + batchType, track: 'TRACK_A', + fieldRequirements: fieldRequirementsForBatchType(batchType), patientId: 'p1', documentRef: 'docs/scan.pdf', documentUrl: null, @@ -47,6 +53,8 @@ function makeBatch(overrides: Partial = {}): BatchDetailRes promotionEncounterId: null, supersedesBatchId: null, clinicianAttestation: false, + isCorrection: false, + supersession: null, createdAt: '2026-06-27T10:00:00Z', updatedAt: '2026-06-27T10:00:00Z', ...overrides, @@ -54,12 +62,13 @@ function makeBatch(overrides: Partial = {}): BatchDetailRes } function mountWithDraft(batchOverrides: Partial = {}) { + const batch = makeBatch(batchOverrides) const wrapper = mount(VerificationForm, { - props: { batch: makeBatch(batchOverrides), batchId: 'b1' }, + props: { batch, batchId: 'b1' }, }) const store = useBatchStore() - store.currentDraft = { + store.currentDraft = emptyDraft(batch.batchType, { patient: { id: 'dp1', batchId: 'b1', @@ -103,7 +112,7 @@ function mountWithDraft(batchOverrides: Partial = {}) { note: null, }, ], - } + }) return { wrapper, store } } @@ -350,7 +359,7 @@ describe('VerificationForm', () => { }) const store = useBatchStore() - store.currentDraft = { + store.currentDraft = emptyDraft('ALLERGY_UPDATE', { patient: { id: 'dp1', batchId: 'b1', @@ -375,7 +384,7 @@ describe('VerificationForm', () => { status: null, }, observations: [], - } + }) await wrapper.vm.$nextTick() expect(wrapper.text()).toContain('No Known Allergies') diff --git a/vigilcare-records-web/src/__tests__/helpers/fieldRequirements.ts b/vigilcare-records-web/src/__tests__/helpers/fieldRequirements.ts new file mode 100644 index 0000000..c518cc0 --- /dev/null +++ b/vigilcare-records-web/src/__tests__/helpers/fieldRequirements.ts @@ -0,0 +1,87 @@ +import type { BatchDraft, BatchTypeFieldRequirements } from '@/types' + +export function fieldRequirementsForBatchType( + batchType: string, +): BatchTypeFieldRequirements { + switch (batchType) { + case 'PATIENT_REGISTRATION': + return { + showPatientDemographics: true, + showEncounterContext: false, + showEncounterSummaryFields: false, + showObservations: false, + showAllergies: false, + showMedications: false, + } + case 'ALLERGY_UPDATE': + return { + showPatientDemographics: true, + showEncounterContext: false, + showEncounterSummaryFields: false, + showObservations: false, + showAllergies: true, + showMedications: false, + } + case 'ENCOUNTER_SUMMARY': + return { + showPatientDemographics: true, + showEncounterContext: true, + showEncounterSummaryFields: true, + showObservations: false, + showAllergies: false, + showMedications: false, + } + case 'MEDICATION_LIST': + return { + showPatientDemographics: true, + showEncounterContext: false, + showEncounterSummaryFields: false, + showObservations: false, + showAllergies: false, + showMedications: true, + } + case 'MIXED': + return { + showPatientDemographics: true, + showEncounterContext: true, + showEncounterSummaryFields: true, + showObservations: true, + showAllergies: true, + showMedications: true, + } + case 'LAB_RESULTS': + return { + showPatientDemographics: true, + showEncounterContext: true, + showEncounterSummaryFields: false, + showObservations: true, + showAllergies: false, + showMedications: false, + } + default: + return { + showPatientDemographics: true, + showEncounterContext: true, + showEncounterSummaryFields: false, + showObservations: true, + showAllergies: false, + showMedications: false, + } + } +} + +export function emptyDraft( + batchType: string, + overrides: Partial = {}, +): BatchDraft { + return { + batchId: 'b1', + status: 'IN_ENTRY', + batchType, + fieldRequirements: fieldRequirementsForBatchType(batchType), + patient: null, + encounter: null, + observations: [], + ...overrides, + } +} diff --git a/vigilcare-records-web/src/components/EntryForm.vue b/vigilcare-records-web/src/components/EntryForm.vue index 52fb6b1..48588c7 100644 --- a/vigilcare-records-web/src/components/EntryForm.vue +++ b/vigilcare-records-web/src/components/EntryForm.vue @@ -270,16 +270,10 @@ const departments = [ 'Anesthesiology', ] -const batchType = computed(() => props.batch?.batchType ?? '') -const showAllergies = computed(() => - ['ALLERGY_UPDATE', 'MIXED'].includes(batchType.value) -) -const showMedications = computed(() => - ['MEDICATION_LIST', 'MIXED'].includes(batchType.value) -) -const showEncounterSummary = computed(() => - ['ENCOUNTER_SUMMARY', 'MIXED'].includes(batchType.value) -) +const fieldReqs = computed(() => batchStore.currentDraft?.fieldRequirements) +const showAllergies = computed(() => fieldReqs.value?.showAllergies ?? false) +const showMedications = computed(() => fieldReqs.value?.showMedications ?? false) +const showEncounterSummary = computed(() => fieldReqs.value?.showEncounterSummaryFields ?? false) const patient = reactive({ fullName: '', diff --git a/vigilcare-records-web/src/components/VerificationForm.vue b/vigilcare-records-web/src/components/VerificationForm.vue index bc92092..b9bfdd0 100644 --- a/vigilcare-records-web/src/components/VerificationForm.vue +++ b/vigilcare-records-web/src/components/VerificationForm.vue @@ -220,10 +220,10 @@ const allergyFields = ref([]) const medicationFields = ref([]) const encounterFields = ref([]) -const batchType = computed(() => props.batch?.batchType ?? '') -const showAllergies = computed(() => ['ALLERGY_UPDATE', 'MIXED'].includes(batchType.value)) -const showMedications = computed(() => ['MEDICATION_LIST', 'MIXED'].includes(batchType.value)) -const showEncounterSummary = computed(() => ['ENCOUNTER_SUMMARY', 'MIXED'].includes(batchType.value)) +const fieldReqs = computed(() => batchStore.currentDraft?.fieldRequirements) +const showAllergies = computed(() => fieldReqs.value?.showAllergies ?? false) +const showMedications = computed(() => fieldReqs.value?.showMedications ?? false) +const showEncounterSummary = computed(() => fieldReqs.value?.showEncounterSummaryFields ?? false) function parseJsonList(json: string | null): string[] { if (!json) return [] diff --git a/vigilcare-records-web/src/types/index.ts b/vigilcare-records-web/src/types/index.ts index 7ef3c06..d2ad17f 100644 --- a/vigilcare-records-web/src/types/index.ts +++ b/vigilcare-records-web/src/types/index.ts @@ -36,11 +36,28 @@ export interface ApiError { code: string } +export interface BatchTypeFieldRequirements { + showPatientDemographics: boolean + showEncounterContext: boolean + showEncounterSummaryFields: boolean + showObservations: boolean + showAllergies: boolean + showMedications: boolean +} + +export interface SupersessionInfo { + originalBatchId: string + originalBatchStatus: string + originalPromotedAt: string + originalObservationCount: number +} + export interface BatchDetailResponse { id: string status: string batchType: string track: string + fieldRequirements: BatchTypeFieldRequirements patientId: string | null documentRef: string documentUrl: string | null @@ -53,6 +70,8 @@ export interface BatchDetailResponse { promotionEncounterId: string | null supersedesBatchId: string | null clinicianAttestation: boolean + isCorrection: boolean + supersession: SupersessionInfo | null createdAt: string updatedAt: string } @@ -108,6 +127,10 @@ export interface DraftObservation { } export interface BatchDraft { + batchId: string + status: string + batchType: string + fieldRequirements: BatchTypeFieldRequirements patient: DraftPatient | null encounter: DraftEncounter | null observations: DraftObservation[] @@ -243,4 +266,13 @@ export interface LiveCaptureResponse { observations: LiveCaptureObservationResponse[] criticalAlertCount: number promotedAt: string +} + +export interface BatchTypeFieldRequirements { + showPatientDemographics: boolean + showEncounterContext: boolean + showEncounterSummaryFields: boolean + showObservations: boolean + showAllergies: boolean + showMedications: boolean } \ No newline at end of file