feature: Backend as Single Source of Truth for Batch-Type Field Requirements
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,7 @@ public record DraftPayloadResponse(
|
||||
Guid BatchId,
|
||||
string Status,
|
||||
string BatchType,
|
||||
BatchTypeFieldRequirements FieldRequirements,
|
||||
DraftPatientDto? Patient,
|
||||
DraftEncounterDto? Encounter,
|
||||
List<DraftObservationDto> Observations
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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> = {}): 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<BatchDetailResponse> = {}): 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<BatchDetailResponse> = {},
|
||||
) {
|
||||
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"]')
|
||||
|
||||
@@ -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> = {}): 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<BatchDetailResponse> = {}): 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<BatchDetailResponse> = {}): BatchDetailRes
|
||||
}
|
||||
|
||||
function mountWithDraft(batchOverrides: Partial<BatchDetailResponse> = {}) {
|
||||
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<BatchDetailResponse> = {}) {
|
||||
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')
|
||||
|
||||
@@ -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> = {},
|
||||
): BatchDraft {
|
||||
return {
|
||||
batchId: 'b1',
|
||||
status: 'IN_ENTRY',
|
||||
batchType,
|
||||
fieldRequirements: fieldRequirementsForBatchType(batchType),
|
||||
patient: null,
|
||||
encounter: null,
|
||||
observations: [],
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
@@ -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: '',
|
||||
|
||||
@@ -220,10 +220,10 @@ const allergyFields = ref<FieldInfo[]>([])
|
||||
const medicationFields = ref<FieldInfo[]>([])
|
||||
const encounterFields = ref<FieldInfo[]>([])
|
||||
|
||||
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 []
|
||||
|
||||
@@ -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[]
|
||||
@@ -244,3 +267,12 @@ export interface LiveCaptureResponse {
|
||||
criticalAlertCount: number
|
||||
promotedAt: string
|
||||
}
|
||||
|
||||
export interface BatchTypeFieldRequirements {
|
||||
showPatientDemographics: boolean
|
||||
showEncounterContext: boolean
|
||||
showEncounterSummaryFields: boolean
|
||||
showObservations: boolean
|
||||
showAllergies: boolean
|
||||
showMedications: boolean
|
||||
}
|
||||
Reference in New Issue
Block a user