feature: Optional OCR-Assisted Draft Pre-Fill
This commit is contained in:
@@ -79,6 +79,7 @@ export function emptyDraft(
|
||||
status: 'IN_ENTRY',
|
||||
batchType,
|
||||
fieldRequirements: fieldRequirementsForBatchType(batchType),
|
||||
ocrConfidence: null,
|
||||
patient: null,
|
||||
encounter: null,
|
||||
observations: [],
|
||||
|
||||
@@ -52,4 +52,16 @@
|
||||
.status-badge {
|
||||
@apply px-2 py-1 rounded-full text-xs font-medium;
|
||||
}
|
||||
.ocr-banner {
|
||||
@apply bg-blue-50 border border-blue-200 rounded-md p-3 text-sm text-blue-800;
|
||||
}
|
||||
.ocr-high {
|
||||
@apply border-l-[3px] border-l-green-500;
|
||||
}
|
||||
.ocr-medium {
|
||||
@apply border-l-[3px] border-l-yellow-500;
|
||||
}
|
||||
.ocr-low {
|
||||
@apply border-l-[3px] border-l-red-500;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { type ComputedRef } from 'vue'
|
||||
import type { OcrConfidenceMap } from '../types'
|
||||
|
||||
export function useOcrFieldConfidence(
|
||||
ocrConfidence: ComputedRef<OcrConfidenceMap | null | undefined>,
|
||||
) {
|
||||
function fieldConfidenceClass(fieldPath: string): string {
|
||||
if (!ocrConfidence.value) return ''
|
||||
const confidence = ocrConfidence.value.fieldConfidences[fieldPath]
|
||||
if (confidence === undefined) return ''
|
||||
if (confidence >= 0.85) return 'ocr-high'
|
||||
if (confidence >= 0.7) return 'ocr-medium'
|
||||
return 'ocr-low'
|
||||
}
|
||||
|
||||
return { fieldConfidenceClass }
|
||||
}
|
||||
@@ -126,11 +126,19 @@ export interface DraftObservation {
|
||||
note: string | null
|
||||
}
|
||||
|
||||
export interface OcrConfidenceMap {
|
||||
provider: string
|
||||
processedAt: string
|
||||
durationMs: number
|
||||
fieldConfidences: Record<string, number>
|
||||
}
|
||||
|
||||
export interface BatchDraft {
|
||||
batchId: string
|
||||
status: string
|
||||
batchType: string
|
||||
fieldRequirements: BatchTypeFieldRequirements
|
||||
ocrConfidence: OcrConfidenceMap | null
|
||||
patient: DraftPatient | null
|
||||
encounter: DraftEncounter | null
|
||||
observations: DraftObservation[]
|
||||
|
||||
Reference in New Issue
Block a user