fix: Add No clinical approval view + No live capture view for bedside data entry + EntryForm missing allergy, medication, and discharge fields

This commit is contained in:
voltsrage
2026-06-27 16:27:58 +08:00
parent 1c7e7fee7d
commit efd3974d1f
13 changed files with 1184 additions and 12 deletions
@@ -2,6 +2,14 @@
<div class="app-header">
<div class="app-header-title">
<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.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 v-if="auth.canSupervise" to="/dashboard" class="nav-link">Dashboard</router-link>
</nav>
<slot name="subtitle" />
</div>
<div class="app-header-actions">
@@ -60,6 +60,82 @@
</div>
</fieldset>
<!-- Allergies section (ALLERGY_UPDATE or MIXED) -->
<fieldset v-if="showAllergies" class="border border-gray-200 rounded-md p-4">
<legend class="text-sm font-medium text-gray-700 px-2">Allergies</legend>
<label class="flex items-center gap-2 mb-3 cursor-pointer">
<input
v-model="patient.noKnownAllergies"
@change="onNoKnownAllergiesChange"
type="checkbox"
class="w-4 h-4 text-clinical-safe rounded"
/>
<span class="text-sm">No known allergies (NKA)</span>
</label>
<div v-if="!patient.noKnownAllergies" class="space-y-2">
<div
v-for="(allergy, idx) in allergies"
:key="idx"
class="flex items-center gap-2"
>
<input
v-model="allergies[idx]"
@blur="saveAllergies"
type="text"
class="form-input text-sm flex-1"
placeholder="Allergy (e.g. Penicillin)"
/>
<button
@click="removeAllergy(idx)"
class="text-clinical-danger hover:text-red-800 text-sm"
>
Remove
</button>
</div>
<button @click="addAllergy" class="text-sm text-primary-600 hover:text-primary-800">
+ Add Allergy
</button>
</div>
</fieldset>
<!-- Medications section (MEDICATION_LIST or MIXED) -->
<fieldset v-if="showMedications" class="border border-gray-200 rounded-md p-4">
<legend class="text-sm font-medium text-gray-700 px-2">Medications</legend>
<label class="flex items-center gap-2 mb-3 cursor-pointer">
<input
v-model="patient.noActiveMedications"
@change="onNoActiveMedicationsChange"
type="checkbox"
class="w-4 h-4 text-clinical-safe rounded"
/>
<span class="text-sm">No active medications</span>
</label>
<div v-if="!patient.noActiveMedications" class="space-y-2">
<div
v-for="(med, idx) in medications"
:key="idx"
class="flex items-center gap-2"
>
<input
v-model="medications[idx]"
@blur="saveMedications"
type="text"
class="form-input text-sm flex-1"
placeholder="Medication (e.g. Metoprolol 50mg BID)"
/>
<button
@click="removeMedication(idx)"
class="text-clinical-danger hover:text-red-800 text-sm"
>
Remove
</button>
</div>
<button @click="addMedication" class="text-sm text-primary-600 hover:text-primary-800">
+ Add Medication
</button>
</div>
</fieldset>
<!-- Encounter section -->
<fieldset class="border border-gray-200 rounded-md p-4">
<legend class="text-sm font-medium text-gray-700 px-2">Encounter Context</legend>
@@ -98,6 +174,24 @@
class="form-input text-sm"
/>
</div>
<div v-if="showEncounterSummary">
<label class="block text-xs text-gray-500">Encounter Status</label>
<select v-model="encounter.status" @change="saveEncounter" class="form-input text-sm">
<option value=""></option>
<option value="active">Active</option>
<option value="discharged">Discharged</option>
</select>
</div>
<div v-if="showEncounterSummary" class="col-span-2">
<label class="block text-xs text-gray-500">Discharge Diagnosis</label>
<textarea
v-model="encounter.dischargeDiagnosis"
@blur="saveEncounter"
class="form-input text-sm"
rows="2"
placeholder="Discharge diagnosis..."
/>
</div>
</div>
</fieldset>
@@ -136,7 +230,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import { ref, reactive, computed, watch } from 'vue'
import { useBatchStore } from '../stores/batches'
import ObservationRow from '../components/ObservationRow.vue'
import type { BatchDetailResponse, DraftObservation } from '../types'
@@ -174,25 +268,53 @@ 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 patient = reactive({
fullName: '',
dateOfBirth: '',
sex: '',
bloodType: '',
emergencyContact: '',
noKnownAllergies: false,
noActiveMedications: false,
})
const allergies = ref<string[]>([])
const medications = ref<string[]>([])
const encounter = reactive({
admissionDate: '',
department: '',
roomBed: '',
admissionReason: '',
dischargeDiagnosis: '',
status: '',
})
const observations = ref<DraftObservation[]>([])
const statusColor = ref('bg-gray-100 text-gray-800')
function parseJsonList(json: string | null): string[] {
if (!json) return []
try {
const parsed = JSON.parse(json)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
// Load draft data when batch changes
watch(
() => batchStore.currentDraft,
@@ -205,7 +327,11 @@ watch(
sex: draft.patient.sex ?? '',
bloodType: draft.patient.bloodType ?? '',
emergencyContact: draft.patient.emergencyContact ?? '',
noKnownAllergies: draft.patient.noKnownAllergies ?? false,
noActiveMedications: draft.patient.noActiveMedications ?? false,
})
allergies.value = parseJsonList(draft.patient.allergiesJson)
medications.value = parseJsonList(draft.patient.medicationsJson)
}
if (draft.encounter) {
Object.assign(encounter, {
@@ -213,6 +339,8 @@ watch(
department: draft.encounter.department ?? '',
roomBed: draft.encounter.roomBed ?? '',
admissionReason: draft.encounter.admissionReason ?? '',
dischargeDiagnosis: draft.encounter.dischargeDiagnosis ?? '',
status: draft.encounter.status ?? '',
})
}
observations.value = draft.observations ?? []
@@ -220,9 +348,67 @@ watch(
{ immediate: true }
)
function addAllergy() {
allergies.value.push('')
}
function removeAllergy(idx: number) {
allergies.value.splice(idx, 1)
saveAllergies()
}
async function saveAllergies() {
try {
await batchStore.saveDraftPatient(props.batchId, {
...patient,
allergiesJson: JSON.stringify(allergies.value.filter(a => a.trim())),
})
} catch (e: unknown) {
errorMessage.value = e instanceof Error ? e.message : 'Failed to save allergies'
}
}
function onNoKnownAllergiesChange() {
if (patient.noKnownAllergies) {
allergies.value = []
}
savePatient()
}
function addMedication() {
medications.value.push('')
}
function removeMedication(idx: number) {
medications.value.splice(idx, 1)
saveMedications()
}
async function saveMedications() {
try {
await batchStore.saveDraftPatient(props.batchId, {
...patient,
medicationsJson: JSON.stringify(medications.value.filter(m => m.trim())),
})
} catch (e: unknown) {
errorMessage.value = e instanceof Error ? e.message : 'Failed to save medications'
}
}
function onNoActiveMedicationsChange() {
if (patient.noActiveMedications) {
medications.value = []
}
savePatient()
}
async function savePatient() {
try {
await batchStore.saveDraftPatient(props.batchId, patient)
await batchStore.saveDraftPatient(props.batchId, {
...patient,
allergiesJson: patient.noKnownAllergies ? null : JSON.stringify(allergies.value.filter(a => a.trim())),
medicationsJson: patient.noActiveMedications ? null : JSON.stringify(medications.value.filter(m => m.trim())),
})
} catch (e: unknown) {
errorMessage.value = e instanceof Error ? e.message : 'Failed to save patient'
}
@@ -33,6 +33,48 @@
</div>
</fieldset>
<!-- Allergies review (ALLERGY_UPDATE or MIXED) -->
<fieldset v-if="allergyFields.length > 0" class="border border-gray-200 rounded-md p-4">
<legend class="text-sm font-medium text-gray-700 px-2">Allergies</legend>
<div class="space-y-3">
<div v-for="field in allergyFields" :key="field.path">
<div class="flex items-center gap-2">
<input
type="checkbox"
:checked="fieldChecks[field.path]"
@change="toggleCheck(field.path)"
class="w-4 h-4 text-clinical-safe rounded"
/>
<label class="text-xs text-gray-500">{{ field.label }}</label>
</div>
<p class="text-sm mt-2 pl-8 font-medium">
{{ field.value || '(empty)' }}
</p>
</div>
</div>
</fieldset>
<!-- Medications review (MEDICATION_LIST or MIXED) -->
<fieldset v-if="medicationFields.length > 0" class="border border-gray-200 rounded-md p-4">
<legend class="text-sm font-medium text-gray-700 px-2">Medications</legend>
<div class="space-y-3">
<div v-for="field in medicationFields" :key="field.path">
<div class="flex items-center gap-2">
<input
type="checkbox"
:checked="fieldChecks[field.path]"
@change="toggleCheck(field.path)"
class="w-4 h-4 text-clinical-safe rounded"
/>
<label class="text-xs text-gray-500">{{ field.label }}</label>
</div>
<p class="text-sm mt-2 pl-8 font-medium">
{{ field.value || '(empty)' }}
</p>
</div>
</div>
</fieldset>
<!-- Encounter review -->
<fieldset class="border border-gray-200 rounded-md p-4">
<legend class="text-sm font-medium text-gray-700 px-2">Encounter Context</legend>
@@ -172,8 +214,25 @@ interface FieldInfo {
}
const patientFields = ref<FieldInfo[]>([])
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))
function parseJsonList(json: string | null): string[] {
if (!json) return []
try {
const parsed = JSON.parse(json)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
watch(
() => batchStore.currentDraft,
(draft) => {
@@ -190,6 +249,50 @@ watch(
{ path: 'patient.bloodType', label: 'Blood Type', value: draft.patient.bloodType ?? '' },
{ path: 'patient.emergencyContact', label: 'Emergency Contact', value: draft.patient.emergencyContact ?? '' },
]
// Build allergy fields
allergyFields.value = []
if (showAllergies.value) {
if (draft.patient.noKnownAllergies) {
allergyFields.value = [
{ path: 'patient.noKnownAllergies', label: 'No Known Allergies', value: 'Yes (NKA)' },
]
} else {
const items = parseJsonList(draft.patient.allergiesJson)
allergyFields.value = items.map((item, i) => ({
path: `patient.allergies[${i}]`,
label: `Allergy ${i + 1}`,
value: item,
}))
if (items.length === 0) {
allergyFields.value = [
{ path: 'patient.allergiesJson', label: 'Allergies', value: '(none entered)' },
]
}
}
}
// Build medication fields
medicationFields.value = []
if (showMedications.value) {
if (draft.patient.noActiveMedications) {
medicationFields.value = [
{ path: 'patient.noActiveMedications', label: 'No Active Medications', value: 'Yes' },
]
} else {
const items = parseJsonList(draft.patient.medicationsJson)
medicationFields.value = items.map((item, i) => ({
path: `patient.medications[${i}]`,
label: `Medication ${i + 1}`,
value: item,
}))
if (items.length === 0) {
medicationFields.value = [
{ path: 'patient.medicationsJson', label: 'Medications', value: '(none entered)' },
]
}
}
}
}
// Build encounter field list
@@ -200,11 +303,19 @@ watch(
{ path: 'encounter.roomBed', label: 'Room / Bed', value: draft.encounter.roomBed ?? '' },
{ path: 'encounter.admissionReason', label: 'Admission Reason', value: draft.encounter.admissionReason ?? '' },
]
if (showEncounterSummary.value) {
encounterFields.value.push(
{ path: 'encounter.status', label: 'Encounter Status', value: draft.encounter.status ?? '' },
{ path: 'encounter.dischargeDiagnosis', label: 'Discharge Diagnosis', value: draft.encounter.dischargeDiagnosis ?? '' },
)
}
}
// Initialize all checks to false
fieldChecks.value = {}
patientFields.value.forEach((f) => { fieldChecks.value[f.path] = false })
allergyFields.value.forEach((f) => { fieldChecks.value[f.path] = false })
medicationFields.value.forEach((f) => { fieldChecks.value[f.path] = false })
encounterFields.value.forEach((f) => { fieldChecks.value[f.path] = false })
observations.value.forEach((_, i) => { fieldChecks.value[`observations[${i}].value`] = false })
},
@@ -212,7 +323,8 @@ watch(
)
const totalFields = computed(
() => patientFields.value.length + encounterFields.value.length + observations.value.length
() => patientFields.value.length + allergyFields.value.length + medicationFields.value.length
+ encounterFields.value.length + observations.value.length
)
const checkedCount = computed(
() => Object.values(fieldChecks.value).filter(Boolean).length