feature: Optional OCR-Assisted Draft Pre-Fill

This commit is contained in:
voltsrage
2026-06-28 01:28:54 +08:00
parent 5039dbb979
commit c160c5f912
42 changed files with 3592 additions and 33 deletions
@@ -10,6 +10,11 @@
</span>
</div>
<div v-if="ocrConfidence" class="ocr-banner">
Pre-filled by OCR ({{ ocrConfidence.provider }}).
Review all values against the scan before submitting.
</div>
<!-- Patient section -->
<fieldset class="border border-gray-200 rounded-md p-4">
<legend class="text-sm font-medium text-gray-700 px-2">Patient Demographics</legend>
@@ -20,7 +25,7 @@
v-model="patient.fullName"
@blur="savePatient"
type="text"
class="form-input text-sm"
:class="['form-input', 'text-sm', fieldConfidenceClass('patient.fullName')]"
/>
</div>
<div>
@@ -29,12 +34,16 @@
v-model="patient.dateOfBirth"
@blur="savePatient"
type="date"
class="form-input text-sm"
:class="['form-input', 'text-sm', fieldConfidenceClass('patient.dateOfBirth')]"
/>
</div>
<div>
<label class="block text-xs text-gray-500">Sex</label>
<select v-model="patient.sex" @change="savePatient" class="form-input text-sm">
<select
v-model="patient.sex"
@change="savePatient"
:class="['form-input', 'text-sm', fieldConfidenceClass('patient.sex')]"
>
<option value="">Select...</option>
<option value="male">Male</option>
<option value="female">Female</option>
@@ -146,12 +155,16 @@
v-model="encounter.admissionDate"
@blur="saveEncounter"
type="datetime-local"
class="form-input text-sm"
:class="['form-input', 'text-sm', fieldConfidenceClass('encounter.admissionDate')]"
/>
</div>
<div>
<label class="block text-xs text-gray-500">Department</label>
<select v-model="encounter.department" @change="saveEncounter" class="form-input text-sm">
<select
v-model="encounter.department"
@change="saveEncounter"
:class="['form-input', 'text-sm', fieldConfidenceClass('encounter.department')]"
>
<option value=""></option>
<option v-for="dept in departments" :key="dept" :value="dept">{{ dept }}</option>
</select>
@@ -162,7 +175,7 @@
v-model="encounter.roomBed"
@blur="saveEncounter"
type="text"
class="form-input text-sm"
:class="['form-input', 'text-sm', fieldConfidenceClass('encounter.roomBed')]"
/>
</div>
<div>
@@ -171,7 +184,7 @@
v-model="encounter.admissionReason"
@blur="saveEncounter"
type="text"
class="form-input text-sm"
:class="['form-input', 'text-sm', fieldConfidenceClass('encounter.admissionReason')]"
/>
</div>
<div v-if="showEncounterSummary">
@@ -203,6 +216,7 @@
v-for="obs in observations"
:key="obs.id"
:observation="obs"
:value-input-class="observationValueConfidenceClass(obs.observationCode)"
@update="(field, value) => handleObsUpdate(obs.id, field, value)"
@delete="handleObsDelete"
/>
@@ -233,6 +247,7 @@
import { ref, reactive, computed, watch } from 'vue'
import { useBatchStore } from '../stores/batches'
import { useToast } from '../composables/useToast'
import { useOcrFieldConfidence } from '../composables/useOcrFieldConfidence'
import ObservationRow from '../components/ObservationRow.vue'
import type { BatchDetailResponse, DraftObservation } from '../types'
@@ -271,6 +286,14 @@ const departments = [
]
const fieldReqs = computed(() => batchStore.currentDraft?.fieldRequirements)
const ocrConfidence = computed(() => batchStore.currentDraft?.ocrConfidence ?? null)
const { fieldConfidenceClass } = useOcrFieldConfidence(ocrConfidence)
function observationValueConfidenceClass(observationCode: string): string {
if (!observationCode) return ''
return fieldConfidenceClass(`observation.${observationCode}.value`)
}
const showAllergies = computed(() => fieldReqs.value?.showAllergies ?? false)
const showMedications = computed(() => fieldReqs.value?.showMedications ?? false)
const showEncounterSummary = computed(() => fieldReqs.value?.showEncounterSummaryFields ?? false)
@@ -29,7 +29,7 @@
@change="update('value', parseFloat(($event.target as HTMLInputElement).value))"
type="number"
step="0.01"
class="form-input text-sm"
:class="['form-input', 'text-sm', valueInputClass]"
:disabled="readonly"
/>
</div>
@@ -96,6 +96,7 @@ const props = defineProps<{
readonly?: boolean
showVerified?: boolean
verified?: boolean
valueInputClass?: string
}>()
const emit = defineEmits<{
@@ -12,6 +12,11 @@
<p class="text-sm text-red-700">{{ batch.rejectionReason }}</p>
</div>
<div v-if="ocrConfidence" class="ocr-banner">
Values pre-filled by OCR ({{ ocrConfidence.provider }}).
Colored borders indicate extraction confidence verify each value against the scan.
</div>
<!-- Patient review -->
<fieldset class="border border-gray-200 rounded-md p-4">
<legend class="text-sm font-medium text-gray-700 px-2">Patient Demographics</legend>
@@ -26,7 +31,10 @@
/>
<label class="text-xs text-gray-500">{{ field.label }}</label>
</div>
<p class="text-sm mt-2 pl-8 font-medium">
<p
class="text-sm mt-2 pl-8 font-medium"
:class="fieldConfidenceClass(field.path)"
>
{{ field.value || '(empty)' }}
</p>
</div>
@@ -47,7 +55,10 @@
/>
<label class="text-xs text-gray-500">{{ field.label }}</label>
</div>
<p class="text-sm mt-2 pl-8 font-medium">
<p
class="text-sm mt-2 pl-8 font-medium"
:class="fieldConfidenceClass(field.path)"
>
{{ field.value || '(empty)' }}
</p>
</div>
@@ -68,7 +79,10 @@
/>
<label class="text-xs text-gray-500">{{ field.label }}</label>
</div>
<p class="text-sm mt-2 pl-8 font-medium">
<p
class="text-sm mt-2 pl-8 font-medium"
:class="fieldConfidenceClass(field.path)"
>
{{ field.value || '(empty)' }}
</p>
</div>
@@ -89,7 +103,10 @@
/>
<label class="text-xs text-gray-500">{{ field.label }}</label>
</div>
<p class="text-sm mt-2 pl-8 font-medium">
<p
class="text-sm mt-2 pl-8 font-medium"
:class="fieldConfidenceClass(field.path)"
>
{{ field.value || '(empty)' }}
</p>
</div>
@@ -109,6 +126,7 @@
:readonly="true"
:show-verified="true"
:verified="fieldChecks[`observations[${index}].value`] ?? false"
:value-input-class="observationValueConfidenceClass(obs.observationCode)"
@verify="(_obsId, passed) => toggleCheck(`observations[${index}].value`, passed)"
/>
</div>
@@ -190,6 +208,7 @@ import { ref, computed, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useBatchStore } from '../stores/batches'
import { useToast } from '../composables/useToast'
import { useOcrFieldConfidence } from '../composables/useOcrFieldConfidence'
import ObservationRow from '../components/ObservationRow.vue'
import type { BatchDetailResponse, DraftObservation } from '../types'
@@ -221,6 +240,14 @@ const medicationFields = ref<FieldInfo[]>([])
const encounterFields = ref<FieldInfo[]>([])
const fieldReqs = computed(() => batchStore.currentDraft?.fieldRequirements)
const ocrConfidence = computed(() => batchStore.currentDraft?.ocrConfidence ?? null)
const { fieldConfidenceClass } = useOcrFieldConfidence(ocrConfidence)
function observationValueConfidenceClass(observationCode: string): string {
if (!observationCode) return ''
return fieldConfidenceClass(`observation.${observationCode}.value`)
}
const showAllergies = computed(() => fieldReqs.value?.showAllergies ?? false)
const showMedications = computed(() => fieldReqs.value?.showMedications ?? false)
const showEncounterSummary = computed(() => fieldReqs.value?.showEncounterSummaryFields ?? false)