fix: Optional OCR-Assisted Draft Pre-Fill
This commit is contained in:
@@ -129,31 +129,42 @@ public class DigitizationBatchesController : ControllerBase
|
|||||||
var batch = await _batches.GetByIdAsync(id);
|
var batch = await _batches.GetByIdAsync(id);
|
||||||
var presignedUrl = await _storage.GetPresignedUrlAsync(batch.DocumentRef);
|
var presignedUrl = await _storage.GetPresignedUrlAsync(batch.DocumentRef);
|
||||||
|
|
||||||
var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
await RecordDocumentAccessAsync(id);
|
||||||
var cutoff = DateTimeOffset.UtcNow.AddMinutes(-5);
|
|
||||||
var recentAccess = await _db.DigitizationEvents.AnyAsync(e =>
|
|
||||||
e.BatchId == id &&
|
|
||||||
e.EventType == DigitizationEventType.DocumentAccessed &&
|
|
||||||
e.ActorUserId == userId &&
|
|
||||||
e.OccurredAt >= cutoff);
|
|
||||||
|
|
||||||
if (!recentAccess)
|
|
||||||
{
|
|
||||||
_db.DigitizationEvents.Add(new DigitizationEvent
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
BatchId = id,
|
|
||||||
EventType = DigitizationEventType.DocumentAccessed,
|
|
||||||
ActorUserId = userId,
|
|
||||||
OccurredAt = DateTimeOffset.UtcNow
|
|
||||||
});
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(ApiResponse<BatchDetailResponse>.Ok(
|
return Ok(ApiResponse<BatchDetailResponse>.Ok(
|
||||||
BatchDetailResponse.FromEntity(batch, presignedUrl)));
|
BatchDetailResponse.FromEntity(batch, presignedUrl)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Streams the scanned document for in-app viewing (same auth and audit as GET batch).
|
||||||
|
/// Proxied through the API so the workstation can render PDFs without cross-origin iframe issues.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("{id:guid}/document")]
|
||||||
|
[Produces("application/pdf", "image/jpeg", "image/png", "application/octet-stream")]
|
||||||
|
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> GetDocument(Guid id)
|
||||||
|
{
|
||||||
|
var batch = await _batches.GetByIdAsync(id);
|
||||||
|
|
||||||
|
if (batch.DocumentRef == "live-capture")
|
||||||
|
return NotFound(ApiResponse<object>.Fail(
|
||||||
|
404, "This batch has no scanned document.", "NO_DOCUMENT"));
|
||||||
|
|
||||||
|
var scanned = await _db.ScannedDocuments
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(d => d.BatchId == id);
|
||||||
|
|
||||||
|
var contentType = scanned?.ContentType ?? "application/octet-stream";
|
||||||
|
|
||||||
|
await RecordDocumentAccessAsync(id);
|
||||||
|
|
||||||
|
var stream = await _storage.DownloadAsync(batch.DocumentRef);
|
||||||
|
Response.Headers.CacheControl = "private, max-age=300";
|
||||||
|
return File(stream, contentType);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lists batches with optional filters and pagination.
|
/// Lists batches with optional filters and pagination.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -265,4 +276,27 @@ public class DigitizationBatchesController : ControllerBase
|
|||||||
var result = await _batchEventService.GetEventsAsync(id, afterCursor, pageSize);
|
var result = await _batchEventService.GetEventsAsync(id, afterCursor, pageSize);
|
||||||
return Ok(ApiResponse<CursorPagedResult<BatchEventResponse>>.Ok(result));
|
return Ok(ApiResponse<CursorPagedResult<BatchEventResponse>>.Ok(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task RecordDocumentAccessAsync(Guid batchId)
|
||||||
|
{
|
||||||
|
var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||||
|
var cutoff = DateTimeOffset.UtcNow.AddMinutes(-5);
|
||||||
|
var recentAccess = await _db.DigitizationEvents.AnyAsync(e =>
|
||||||
|
e.BatchId == batchId &&
|
||||||
|
e.EventType == DigitizationEventType.DocumentAccessed &&
|
||||||
|
e.ActorUserId == userId &&
|
||||||
|
e.OccurredAt >= cutoff);
|
||||||
|
|
||||||
|
if (recentAccess) return;
|
||||||
|
|
||||||
|
_db.DigitizationEvents.Add(new DigitizationEvent
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
BatchId = batchId,
|
||||||
|
EventType = DigitizationEventType.DocumentAccessed,
|
||||||
|
ActorUserId = userId,
|
||||||
|
OccurredAt = DateTimeOffset.UtcNow
|
||||||
|
});
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,8 @@ public class BatchService : IBatchService
|
|||||||
patientId ??= supersededBatch.PatientId;
|
patientId ??= supersededBatch.PatientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
var (objectKey, sha256, fileSize) = await _storage.UploadAsync(fileStream, contentType, Guid.NewGuid());
|
var batchId = Guid.NewGuid();
|
||||||
|
var (objectKey, sha256, fileSize) = await _storage.UploadAsync(fileStream, contentType, batchId);
|
||||||
|
|
||||||
// Duplicate detection: same SHA-256 for same patient within 24 hours
|
// Duplicate detection: same SHA-256 for same patient within 24 hours
|
||||||
if (patientId.HasValue)
|
if (patientId.HasValue)
|
||||||
@@ -101,7 +102,6 @@ public class BatchService : IBatchService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var batchId = Guid.NewGuid();
|
|
||||||
var batch = new DigitizationBatch
|
var batch = new DigitizationBatch
|
||||||
{
|
{
|
||||||
Id = batchId,
|
Id = batchId,
|
||||||
|
|||||||
@@ -178,9 +178,9 @@ describe('EntryForm', () => {
|
|||||||
sex: 'male',
|
sex: 'male',
|
||||||
bloodType: 'O+',
|
bloodType: 'O+',
|
||||||
emergencyContact: '555-1234',
|
emergencyContact: '555-1234',
|
||||||
allergiesJson: null,
|
allergies: null,
|
||||||
noKnownAllergies: false,
|
noKnownAllergies: false,
|
||||||
medicationsJson: null,
|
medications: null,
|
||||||
noActiveMedications: false,
|
noActiveMedications: false,
|
||||||
},
|
},
|
||||||
encounter: null,
|
encounter: null,
|
||||||
|
|||||||
@@ -77,9 +77,9 @@ function mountWithDraft(batchOverrides: Partial<BatchDetailResponse> = {}) {
|
|||||||
sex: 'female',
|
sex: 'female',
|
||||||
bloodType: 'A+',
|
bloodType: 'A+',
|
||||||
emergencyContact: '555-1234',
|
emergencyContact: '555-1234',
|
||||||
allergiesJson: null,
|
allergies: null,
|
||||||
noKnownAllergies: false,
|
noKnownAllergies: false,
|
||||||
medicationsJson: null,
|
medications: null,
|
||||||
noActiveMedications: false,
|
noActiveMedications: false,
|
||||||
},
|
},
|
||||||
encounter: {
|
encounter: {
|
||||||
@@ -340,7 +340,7 @@ describe('VerificationForm', () => {
|
|||||||
it('shows allergy fields for ALLERGY_UPDATE batch', async () => {
|
it('shows allergy fields for ALLERGY_UPDATE batch', async () => {
|
||||||
const { wrapper } = mountWithDraft({ batchType: 'ALLERGY_UPDATE' })
|
const { wrapper } = mountWithDraft({ batchType: 'ALLERGY_UPDATE' })
|
||||||
const store = useBatchStore()
|
const store = useBatchStore()
|
||||||
store.currentDraft!.patient!.allergiesJson = JSON.stringify(['Penicillin', 'Latex'])
|
store.currentDraft!.patient!.allergies = ['Penicillin', 'Latex']
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
// Re-trigger the watcher by resetting draft
|
// Re-trigger the watcher by resetting draft
|
||||||
@@ -368,9 +368,9 @@ describe('VerificationForm', () => {
|
|||||||
sex: 'female',
|
sex: 'female',
|
||||||
bloodType: null,
|
bloodType: null,
|
||||||
emergencyContact: null,
|
emergencyContact: null,
|
||||||
allergiesJson: null,
|
allergies: null,
|
||||||
noKnownAllergies: true,
|
noKnownAllergies: true,
|
||||||
medicationsJson: null,
|
medications: null,
|
||||||
noActiveMedications: false,
|
noActiveMedications: false,
|
||||||
},
|
},
|
||||||
encounter: {
|
encounter: {
|
||||||
|
|||||||
@@ -237,6 +237,14 @@ describe('useBatchStore', () => {
|
|||||||
|
|
||||||
expect(mockedPut).toHaveBeenCalledWith('digitization-batches/b1/draft/patient', {
|
expect(mockedPut).toHaveBeenCalledWith('digitization-batches/b1/draft/patient', {
|
||||||
fullName: 'John Doe',
|
fullName: 'John Doe',
|
||||||
|
dateOfBirth: null,
|
||||||
|
sex: null,
|
||||||
|
bloodType: null,
|
||||||
|
emergencyContact: null,
|
||||||
|
allergies: null,
|
||||||
|
noKnownAllergies: false,
|
||||||
|
medications: null,
|
||||||
|
noActiveMedications: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -183,4 +183,10 @@ export async function postBlob(url: string, data?: unknown): Promise<Blob> {
|
|||||||
return response.data as Blob
|
return response.data as Blob
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** GET and receive a binary response (e.g. scanned document). */
|
||||||
|
export async function getBlob(url: string): Promise<Blob> {
|
||||||
|
const response = await apiClient.get(url, { responseType: 'blob' })
|
||||||
|
return response.data as Blob
|
||||||
|
}
|
||||||
|
|
||||||
export default apiClient
|
export default apiClient
|
||||||
@@ -349,8 +349,8 @@ watch(
|
|||||||
noKnownAllergies: draft.patient.noKnownAllergies ?? false,
|
noKnownAllergies: draft.patient.noKnownAllergies ?? false,
|
||||||
noActiveMedications: draft.patient.noActiveMedications ?? false,
|
noActiveMedications: draft.patient.noActiveMedications ?? false,
|
||||||
})
|
})
|
||||||
allergies.value = parseJsonList(draft.patient.allergiesJson)
|
allergies.value = draft.patient.allergies ?? []
|
||||||
medications.value = parseJsonList(draft.patient.medicationsJson)
|
medications.value = draft.patient.medications ?? []
|
||||||
}
|
}
|
||||||
if (draft.encounter) {
|
if (draft.encounter) {
|
||||||
Object.assign(encounter, {
|
Object.assign(encounter, {
|
||||||
@@ -380,7 +380,7 @@ async function saveAllergies() {
|
|||||||
try {
|
try {
|
||||||
await batchStore.saveDraftPatient(props.batchId, {
|
await batchStore.saveDraftPatient(props.batchId, {
|
||||||
...patient,
|
...patient,
|
||||||
allergiesJson: JSON.stringify(allergies.value.filter(a => a.trim())),
|
allergies: allergies.value.filter(a => a.trim()),
|
||||||
})
|
})
|
||||||
toast.success('Allergies saved')
|
toast.success('Allergies saved')
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
@@ -410,7 +410,7 @@ async function saveMedications() {
|
|||||||
try {
|
try {
|
||||||
await batchStore.saveDraftPatient(props.batchId, {
|
await batchStore.saveDraftPatient(props.batchId, {
|
||||||
...patient,
|
...patient,
|
||||||
medicationsJson: JSON.stringify(medications.value.filter(m => m.trim())),
|
medications: medications.value.filter(m => m.trim()),
|
||||||
})
|
})
|
||||||
toast.success('Medications saved')
|
toast.success('Medications saved')
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
@@ -431,8 +431,8 @@ async function savePatient() {
|
|||||||
try {
|
try {
|
||||||
await batchStore.saveDraftPatient(props.batchId, {
|
await batchStore.saveDraftPatient(props.batchId, {
|
||||||
...patient,
|
...patient,
|
||||||
allergiesJson: patient.noKnownAllergies ? null : JSON.stringify(allergies.value.filter(a => a.trim())),
|
allergies: patient.noKnownAllergies ? null : allergies.value.filter(a => a.trim()),
|
||||||
medicationsJson: patient.noActiveMedications ? null : JSON.stringify(medications.value.filter(m => m.trim())),
|
medications: patient.noActiveMedications ? null : medications.value.filter(m => m.trim()),
|
||||||
})
|
})
|
||||||
toast.success('Patient demographics saved')
|
toast.success('Patient demographics saved')
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
|
|||||||
@@ -34,12 +34,13 @@
|
|||||||
transition: isPanning ? 'none' : 'transform 0.2s',
|
transition: isPanning ? 'none' : 'transform 0.2s',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<!-- PDF rendering via iframe for simplicity; production would use pdf.js -->
|
<!-- PDF/image rendered from same-origin blob URL (avoids cross-origin MinIO iframe issues) -->
|
||||||
<iframe
|
<iframe
|
||||||
v-if="isPdf"
|
v-if="isPdf"
|
||||||
:src="url"
|
:src="url"
|
||||||
class="w-[800px] h-[1100px] bg-white"
|
class="w-[800px] h-[1100px] bg-white"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
|
title="Scanned document"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ watch(
|
|||||||
{ path: 'patient.noKnownAllergies', label: 'No Known Allergies', value: 'Yes (NKA)' },
|
{ path: 'patient.noKnownAllergies', label: 'No Known Allergies', value: 'Yes (NKA)' },
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
const items = parseJsonList(draft.patient.allergiesJson)
|
const items = draft.patient.allergies ?? []
|
||||||
allergyFields.value = items.map((item, i) => ({
|
allergyFields.value = items.map((item, i) => ({
|
||||||
path: `patient.allergies[${i}]`,
|
path: `patient.allergies[${i}]`,
|
||||||
label: `Allergy ${i + 1}`,
|
label: `Allergy ${i + 1}`,
|
||||||
@@ -295,7 +295,7 @@ watch(
|
|||||||
}))
|
}))
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
allergyFields.value = [
|
allergyFields.value = [
|
||||||
{ path: 'patient.allergiesJson', label: 'Allergies', value: '(none entered)' },
|
{ path: 'patient.allergies', label: 'Allergies', value: '(none entered)' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,7 +309,7 @@ watch(
|
|||||||
{ path: 'patient.noActiveMedications', label: 'No Active Medications', value: 'Yes' },
|
{ path: 'patient.noActiveMedications', label: 'No Active Medications', value: 'Yes' },
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
const items = parseJsonList(draft.patient.medicationsJson)
|
const items = draft.patient.medications ?? []
|
||||||
medicationFields.value = items.map((item, i) => ({
|
medicationFields.value = items.map((item, i) => ({
|
||||||
path: `patient.medications[${i}]`,
|
path: `patient.medications[${i}]`,
|
||||||
label: `Medication ${i + 1}`,
|
label: `Medication ${i + 1}`,
|
||||||
@@ -317,7 +317,7 @@ watch(
|
|||||||
}))
|
}))
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
medicationFields.value = [
|
medicationFields.value = [
|
||||||
{ path: 'patient.medicationsJson', label: 'Medications', value: '(none entered)' },
|
{ path: 'patient.medications', label: 'Medications', value: '(none entered)' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,56 @@
|
|||||||
import { computed, watch, onUnmounted, type Ref } from 'vue'
|
import { ref, watch, onUnmounted, type Ref } from 'vue'
|
||||||
import { useIntervalFn } from '@vueuse/core'
|
import { getBlob } from '../api/client'
|
||||||
import { useBatchStore } from '../stores/batches'
|
import { useBatchStore } from '../stores/batches'
|
||||||
|
|
||||||
const PRESIGNED_URL_TTL_MS = 15 * 60 * 1000
|
|
||||||
const REFRESH_BUFFER_MS = 60_000
|
|
||||||
|
|
||||||
export function usePresignedUrl(batchId: Ref<string | undefined>) {
|
export function usePresignedUrl(batchId: Ref<string | undefined>) {
|
||||||
const batchStore = useBatchStore()
|
const batchStore = useBatchStore()
|
||||||
|
const documentUrl = ref<string | null>(null)
|
||||||
|
const documentError = ref<string | null>(null)
|
||||||
|
|
||||||
const documentUrl = computed(() => batchStore.documentUrl)
|
let currentBlobUrl: string | null = null
|
||||||
|
|
||||||
|
function revokeBlobUrl(): void {
|
||||||
|
if (currentBlobUrl) {
|
||||||
|
URL.revokeObjectURL(currentBlobUrl)
|
||||||
|
currentBlobUrl = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshUrl(): Promise<void> {
|
async function refreshUrl(): Promise<void> {
|
||||||
if (batchId.value) {
|
revokeBlobUrl()
|
||||||
await batchStore.getBatch(batchId.value)
|
documentUrl.value = null
|
||||||
}
|
documentError.value = null
|
||||||
}
|
|
||||||
|
|
||||||
const { pause, resume } = useIntervalFn(
|
if (!batchId.value) return
|
||||||
() => { void refreshUrl() },
|
|
||||||
PRESIGNED_URL_TTL_MS - REFRESH_BUFFER_MS,
|
try {
|
||||||
{ immediate: false }
|
await batchStore.getBatch(batchId.value)
|
||||||
)
|
const blob = await getBlob(`digitization-batches/${batchId.value}/document`)
|
||||||
|
if (!blob.size) {
|
||||||
|
throw new Error('Document is empty')
|
||||||
|
}
|
||||||
|
currentBlobUrl = URL.createObjectURL(blob)
|
||||||
|
documentUrl.value = currentBlobUrl
|
||||||
|
} catch (e: unknown) {
|
||||||
|
documentError.value = e instanceof Error ? e.message : 'Failed to load document'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
batchId,
|
batchId,
|
||||||
async (id) => {
|
async (id) => {
|
||||||
pause()
|
|
||||||
if (id) {
|
if (id) {
|
||||||
await refreshUrl()
|
await refreshUrl()
|
||||||
resume()
|
} else {
|
||||||
|
revokeBlobUrl()
|
||||||
|
documentUrl.value = null
|
||||||
|
documentError.value = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
onUnmounted(pause)
|
onUnmounted(revokeBlobUrl)
|
||||||
|
|
||||||
return { documentUrl, refreshUrl }
|
return { documentUrl, documentError, refreshUrl }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,52 @@ import type {
|
|||||||
PatientDigitizationHistoryResponse,
|
PatientDigitizationHistoryResponse,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
|
||||||
|
function nullIfEmpty(value: string | null | undefined): string | null {
|
||||||
|
if (value == null) return null
|
||||||
|
const trimmed = value.trim()
|
||||||
|
return trimmed === '' ? null : trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonStringArray(json: string): string[] | null {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(json)
|
||||||
|
return Array.isArray(parsed) ? parsed : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUpsertDraftPatientRequest(
|
||||||
|
patient: Partial<DraftPatient> & {
|
||||||
|
allergiesJson?: string | null
|
||||||
|
medicationsJson?: string | null
|
||||||
|
}
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const allergies = Array.isArray(patient.allergies)
|
||||||
|
? patient.allergies
|
||||||
|
: patient.allergiesJson
|
||||||
|
? parseJsonStringArray(patient.allergiesJson)
|
||||||
|
: null
|
||||||
|
|
||||||
|
const medications = Array.isArray(patient.medications)
|
||||||
|
? patient.medications
|
||||||
|
: patient.medicationsJson
|
||||||
|
? parseJsonStringArray(patient.medicationsJson)
|
||||||
|
: null
|
||||||
|
|
||||||
|
return {
|
||||||
|
fullName: nullIfEmpty(patient.fullName),
|
||||||
|
dateOfBirth: nullIfEmpty(patient.dateOfBirth),
|
||||||
|
sex: nullIfEmpty(patient.sex),
|
||||||
|
bloodType: nullIfEmpty(patient.bloodType),
|
||||||
|
emergencyContact: nullIfEmpty(patient.emergencyContact),
|
||||||
|
allergies: allergies?.length ? allergies : null,
|
||||||
|
noKnownAllergies: patient.noKnownAllergies ?? false,
|
||||||
|
medications: medications?.length ? medications : null,
|
||||||
|
noActiveMedications: patient.noActiveMedications ?? false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const useBatchStore = defineStore('batches', () => {
|
export const useBatchStore = defineStore('batches', () => {
|
||||||
const batches = ref<BatchDetailResponse[]>([])
|
const batches = ref<BatchDetailResponse[]>([])
|
||||||
const currentBatch = ref<BatchDetailResponse | null>(null)
|
const currentBatch = ref<BatchDetailResponse | null>(null)
|
||||||
@@ -124,11 +170,14 @@ async function assignBatch(batchId: string, entryClerkUserId: string): Promise<v
|
|||||||
|
|
||||||
async function saveDraftPatient(
|
async function saveDraftPatient(
|
||||||
batchId: string,
|
batchId: string,
|
||||||
patient: Partial<DraftPatient>
|
patient: Partial<DraftPatient> & {
|
||||||
|
allergiesJson?: string | null
|
||||||
|
medicationsJson?: string | null
|
||||||
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await put<DraftPatient>(
|
await put<DraftPatient>(
|
||||||
`digitization-batches/${batchId}/draft/patient`,
|
`digitization-batches/${batchId}/draft/patient`,
|
||||||
patient
|
toUpsertDraftPatientRequest(patient)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,16 +93,16 @@ export interface UserProfileResponse {
|
|||||||
|
|
||||||
export interface DraftPatient {
|
export interface DraftPatient {
|
||||||
id: string
|
id: string
|
||||||
batchId: string
|
|
||||||
fullName: string | null
|
fullName: string | null
|
||||||
dateOfBirth: string | null
|
dateOfBirth: string | null
|
||||||
sex: string | null
|
sex: string | null
|
||||||
bloodType: string | null
|
bloodType: string | null
|
||||||
emergencyContact: string | null
|
emergencyContact: string | null
|
||||||
allergiesJson: string | null
|
allergies: string[] | null
|
||||||
noKnownAllergies: boolean
|
noKnownAllergies: boolean
|
||||||
medicationsJson: string | null
|
medications: string[] | null
|
||||||
noActiveMedications: boolean
|
noActiveMedications: boolean
|
||||||
|
updatedAt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DraftEncounter {
|
export interface DraftEncounter {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
:url="documentUrl"
|
:url="documentUrl"
|
||||||
/>
|
/>
|
||||||
<div v-else class="flex items-center justify-center bg-gray-100 rounded-lg">
|
<div v-else class="flex items-center justify-center bg-gray-100 rounded-lg">
|
||||||
<p class="text-gray-500">Loading document...</p>
|
<p class="text-gray-500">{{ documentError ?? 'Loading document...' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Approval panel -->
|
<!-- Approval panel -->
|
||||||
@@ -238,7 +238,7 @@ const toast = useToast()
|
|||||||
const batchId = computed(() => props.batchId ?? (route.params.batchId as string | undefined))
|
const batchId = computed(() => props.batchId ?? (route.params.batchId as string | undefined))
|
||||||
const currentBatch = computed(() => batchStore.currentBatch)
|
const currentBatch = computed(() => batchStore.currentBatch)
|
||||||
const draft = computed(() => batchStore.currentDraft)
|
const draft = computed(() => batchStore.currentDraft)
|
||||||
const { documentUrl } = usePresignedUrl(batchId)
|
const { documentUrl, documentError } = usePresignedUrl(batchId)
|
||||||
|
|
||||||
const processing = ref(false)
|
const processing = ref(false)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
:url="documentUrl"
|
:url="documentUrl"
|
||||||
/>
|
/>
|
||||||
<div v-else class="flex items-center justify-center bg-gray-100 rounded-lg">
|
<div v-else class="flex items-center justify-center bg-gray-100 rounded-lg">
|
||||||
<p class="text-gray-500">Loading document...</p>
|
<p class="text-gray-500">{{ documentError ?? 'Loading document...' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EntryForm
|
<EntryForm
|
||||||
@@ -57,7 +57,7 @@ const router = useRouter()
|
|||||||
|
|
||||||
const batchId = computed(() => props.batchId ?? (route.params.batchId as string | undefined))
|
const batchId = computed(() => props.batchId ?? (route.params.batchId as string | undefined))
|
||||||
const currentBatch = computed(() => batchStore.currentBatch)
|
const currentBatch = computed(() => batchStore.currentBatch)
|
||||||
const { documentUrl } = usePresignedUrl(batchId)
|
const { documentUrl, documentError } = usePresignedUrl(batchId)
|
||||||
|
|
||||||
async function openBatch(id: string) {
|
async function openBatch(id: string) {
|
||||||
router.push(`/entry/${id}`)
|
router.push(`/entry/${id}`)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
:url="documentUrl"
|
:url="documentUrl"
|
||||||
/>
|
/>
|
||||||
<div v-else class="flex items-center justify-center bg-gray-100 rounded-lg">
|
<div v-else class="flex items-center justify-center bg-gray-100 rounded-lg">
|
||||||
<p class="text-gray-500">Loading document...</p>
|
<p class="text-gray-500">{{ documentError ?? 'Loading document...' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VerificationForm
|
<VerificationForm
|
||||||
@@ -58,7 +58,7 @@ const router = useRouter()
|
|||||||
|
|
||||||
const batchId = computed(() => props.batchId ?? (route.params.batchId as string | undefined))
|
const batchId = computed(() => props.batchId ?? (route.params.batchId as string | undefined))
|
||||||
const currentBatch = computed(() => batchStore.currentBatch)
|
const currentBatch = computed(() => batchStore.currentBatch)
|
||||||
const { documentUrl } = usePresignedUrl(batchId)
|
const { documentUrl, documentError } = usePresignedUrl(batchId)
|
||||||
|
|
||||||
function openBatch(id: string) {
|
function openBatch(id: string) {
|
||||||
router.push(`/verification/${id}`)
|
router.push(`/verification/${id}`)
|
||||||
|
|||||||
@@ -10,9 +10,12 @@ export default {
|
|||||||
primary: {
|
primary: {
|
||||||
50: '#eff6ff',
|
50: '#eff6ff',
|
||||||
100: '#dbeafe',
|
100: '#dbeafe',
|
||||||
|
300: '#93c5fd',
|
||||||
|
400: '#60a5fa',
|
||||||
500: '#3b82f6',
|
500: '#3b82f6',
|
||||||
600: '#2563eb',
|
600: '#2563eb',
|
||||||
700: '#1d4ed8',
|
700: '#1d4ed8',
|
||||||
|
800: '#1e40af',
|
||||||
},
|
},
|
||||||
clinical: {
|
clinical: {
|
||||||
safe: '#16a34a',
|
safe: '#16a34a',
|
||||||
|
|||||||
Reference in New Issue
Block a user