feature: Barcode/QR Cover Sheet System

This commit is contained in:
voltsrage
2026-06-27 21:50:32 +08:00
parent 5db60f46eb
commit 756cff332c
43 changed files with 8203 additions and 21 deletions
+170 -6
View File
@@ -5,6 +5,80 @@
<div class="page-container flex-1">
<h1 class="text-2xl font-bold mb-6">Upload Scanned Document</h1>
<!-- Barcode-assisted upload -->
<div class="card mb-6">
<h2 class="text-lg font-semibold mb-4">Quick Upload with Cover Sheet</h2>
<p class="text-sm text-gray-600 mb-4">
Scan or type a cover sheet barcode to auto-populate batch details.
</p>
<div class="flex gap-4 items-end">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 mb-2">
Cover Sheet Code
</label>
<input
v-model="coverSheetCode"
@keydown.enter.prevent="lookupCoverSheet"
type="text"
class="form-input"
placeholder="VCR-CS-XXXXXXXX"
autofocus
/>
</div>
<button
type="button"
@click="lookupCoverSheet"
class="btn-secondary"
:disabled="!coverSheetCode.trim() || lookupLoading"
>
{{ lookupLoading ? 'Looking up...' : 'Lookup' }}
</button>
</div>
<div v-if="lookupError" class="text-clinical-danger text-sm mt-3">
{{ lookupError }}
</div>
<div
v-if="resolvedCoverSheet"
class="mt-4 bg-green-50 border border-green-200 rounded-md p-4"
>
<p class="text-sm font-medium text-green-800">Cover Sheet Found</p>
<dl class="mt-2 text-sm text-green-700 space-y-1">
<div>
<dt class="inline font-medium">Type:</dt>
<dd class="inline">{{ formatBatchType(resolvedCoverSheet.batchType) }}</dd>
</div>
<div>
<dt class="inline font-medium">Track:</dt>
<dd class="inline">{{ formatTrack(resolvedCoverSheet.track) }}</dd>
</div>
<div v-if="resolvedCoverSheet.patientName">
<dt class="inline font-medium">Patient:</dt>
<dd class="inline">
{{ resolvedCoverSheet.patientName }}
({{ resolvedCoverSheet.patientMrn }})
</dd>
</div>
<div v-if="resolvedCoverSheet.assignToUserName">
<dt class="inline font-medium">Assign to:</dt>
<dd class="inline">{{ resolvedCoverSheet.assignToUserName }}</dd>
</div>
</dl>
<p class="text-sm text-green-600 mt-3">
Select a file below and click "Upload with Cover Sheet" to create the batch.
</p>
<button
type="button"
@click="clearCoverSheet"
class="text-xs text-green-700 hover:text-green-900 mt-2"
>
Clear cover sheet (use manual upload)
</button>
</div>
</div>
<!-- Upload form -->
<div class="card mb-6">
<h2 class="text-lg font-semibold mb-4">New Batch</h2>
@@ -35,7 +109,11 @@
<!-- Batch type -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Batch Type</label>
<select v-model="batchType" class="form-input" required>
<select
v-model="batchType"
class="form-input"
:required="!resolvedCoverSheet"
>
<option value="">Select batch type...</option>
<option value="PATIENT_REGISTRATION">Patient Registration</option>
<option value="ENCOUNTER_SUMMARY">Encounter Summary</option>
@@ -72,6 +150,7 @@
<span class="font-mono">{{ supersedesBatchId.substring(0, 8) }}...</span>
</p>
<button
type="button"
@click="clearCorrection"
class="text-xs text-blue-600 hover:text-blue-800 mt-2"
>
@@ -86,9 +165,9 @@
<button
type="submit"
class="btn-primary"
:disabled="!selectedFile || !batchType || batchStore.loading"
:disabled="!canUpload"
>
{{ batchStore.loading ? 'Uploading...' : 'Upload and Create Batch' }}
{{ uploadButtonLabel }}
</button>
</form>
</div>
@@ -118,14 +197,16 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { get } from '../api/client'
import { useBatchStore } from '../stores/batches'
import { useToast } from '../composables/useToast'
import AppHeader from '../components/AppHeader.vue'
import PatientSearch from '../components/PatientSearch.vue'
import BatchList from '../components/BatchList.vue'
import AssignClerkDialog from '../components/AssignClerkDialog.vue'
import type { CoverSheetResponse } from '../types'
const route = useRoute()
const batchStore = useBatchStore()
@@ -141,13 +222,87 @@ const assignError = ref('')
const assignDialogOpen = ref(false)
const assignBatchId = ref<string | null>(null)
const coverSheetCode = ref('')
const resolvedCoverSheet = ref<CoverSheetResponse | null>(null)
const lookupLoading = ref(false)
const lookupError = ref('')
const canUpload = computed(() => {
if (!selectedFile.value || batchStore.loading) return false
if (resolvedCoverSheet.value) return true
return !!batchType.value
})
const uploadButtonLabel = computed(() => {
if (batchStore.loading) return 'Uploading...'
if (resolvedCoverSheet.value) return 'Upload with Cover Sheet'
return 'Upload and Create Batch'
})
watch(coverSheetCode, () => {
if (resolvedCoverSheet.value && coverSheetCode.value.trim().toUpperCase() !== resolvedCoverSheet.value.code) {
resolvedCoverSheet.value = null
lookupError.value = ''
}
})
function formatBatchType(type: string): string {
return type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
}
function formatTrack(value: string): string {
return value === 'BACKFILL' ? 'Backfill (Track A)' : 'Live Capture (Track B)'
}
function onFileChange(event: Event) {
const input = event.target as HTMLInputElement
selectedFile.value = input.files?.[0] ?? null
}
async function lookupCoverSheet(): Promise<void> {
const code = coverSheetCode.value.trim().toUpperCase()
if (!code) return
lookupLoading.value = true
lookupError.value = ''
resolvedCoverSheet.value = null
try {
const response = await get<CoverSheetResponse>(
`cover-sheets/lookup/${encodeURIComponent(code)}`,
)
if (!response.success || !response.data) {
throw new Error(response.error?.message ?? 'Cover sheet not found')
}
if (response.data.isUsed) {
throw new Error('Cover sheet has already been used')
}
resolvedCoverSheet.value = response.data
coverSheetCode.value = response.data.code
batchType.value = response.data.batchType
track.value = response.data.track
patientId.value = response.data.patientId ?? undefined
toast.success('Cover sheet found')
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : 'Lookup failed'
lookupError.value = msg
toast.error(msg)
} finally {
lookupLoading.value = false
}
}
function clearCoverSheet(): void {
coverSheetCode.value = ''
resolvedCoverSheet.value = null
lookupError.value = ''
}
async function handleUpload() {
if (!selectedFile.value || !batchType.value) return
if (!selectedFile.value || !canUpload.value) return
uploadError.value = ''
try {
@@ -157,15 +312,24 @@ async function handleUpload() {
track.value,
patientId.value,
supersedesBatchId.value,
resolvedCoverSheet.value?.code,
)
if (batch) {
const isCorrection = !!supersedesBatchId.value
const usedCoverSheet = !!resolvedCoverSheet.value
selectedFile.value = null
batchType.value = ''
track.value = 'BACKFILL'
patientId.value = undefined
supersedesBatchId.value = undefined
toast.success(isCorrection ? 'Correction batch created' : 'Batch uploaded successfully')
clearCoverSheet()
toast.success(
isCorrection
? 'Correction batch created'
: usedCoverSheet
? 'Batch uploaded with cover sheet'
: 'Batch uploaded successfully',
)
await loadRecent()
}
} catch (e: unknown) {