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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user