feature: Digitization Workstation UI

This commit is contained in:
voltsrage
2026-06-27 12:15:43 +08:00
parent 88e70b3dbe
commit e22d33b654
55 changed files with 6411 additions and 143 deletions
@@ -0,0 +1,141 @@
<template>
<div class="min-h-screen lg:h-screen flex flex-col">
<AppHeader title="Supervisor Dashboard">
<template #actions>
<button
type="button"
@click="refreshData"
class="text-sm text-primary-600 hover:text-primary-800"
>
Refresh
</button>
</template>
</AppHeader>
<div class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8 space-y-6 lg:space-y-8">
<!-- Summary cards -->
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 lg:gap-8">
<div class="card">
<p class="text-sm text-gray-500">Pending Entry</p>
<p class="text-3xl font-bold text-yellow-600">
{{ overview?.statusCounts?.UPLOADED ?? 0 }}
</p>
</div>
<div class="card">
<p class="text-sm text-gray-500">In Entry</p>
<p class="text-3xl font-bold text-blue-600">
{{ overview?.statusCounts?.IN_ENTRY ?? 0 }}
</p>
</div>
<div class="card">
<p class="text-sm text-gray-500">Pending Verification</p>
<p class="text-3xl font-bold text-orange-600">
{{ overview?.statusCounts?.PENDING_VERIFICATION ?? 0 }}
</p>
</div>
<div class="card">
<p class="text-sm text-gray-500">Reject Rate</p>
<p
class="text-3xl font-bold"
:class="(overview?.rejectRate ?? 0) > 15
? 'text-clinical-danger'
: 'text-clinical-safe'"
>
{{ ((overview?.rejectRate ?? 0) * 100).toFixed(1) }}%
</p>
</div>
</div>
<!-- Queue age and throughput -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-8">
<div class="card">
<h3 class="text-sm font-medium text-gray-700 mb-2">Average Time in Queue</h3>
<p class="text-2xl font-bold">
{{ formatMinutes(overview?.averageTimeInQueueMinutes ?? 0) }}
</p>
<p class="text-xs text-gray-500 mt-2">
Target: &lt; 24 hours
</p>
</div>
<div class="card">
<h3 class="text-sm font-medium text-gray-700 mb-2">
Oldest Pending Verification
</h3>
<p
class="text-2xl font-bold"
:class="(overview?.oldestPendingVerificationMinutes ?? 0) > 1440
? 'text-clinical-danger'
: 'text-gray-900'"
>
{{ formatMinutes(overview?.oldestPendingVerificationMinutes ?? 0) }}
</p>
</div>
</div>
<!-- Status breakdown -->
<div class="card">
<h3 class="text-sm font-medium text-gray-700 mb-4">Batches by Status</h3>
<div class="space-y-2">
<div
v-for="(count, status) in overview?.statusCounts ?? {}"
:key="status"
class="flex items-center gap-4"
>
<span class="w-40 shrink-0 text-sm text-gray-600">
{{ String(status).replace(/_/g, ' ') }}
</span>
<div class="flex-1 bg-gray-100 rounded-full h-4">
<div
class="h-4 rounded-full bg-primary-500"
:style="{ width: `${(Number(count) / maxCount) * 100}%` }"
/>
</div>
<span class="text-sm font-medium w-8 text-right">{{ count }}</span>
</div>
</div>
</div>
<!-- Recent activity -->
<div class="card">
<h3 class="text-sm font-medium text-gray-700 mb-4">All Batches</h3>
<BatchList
:batches="batchStore.batches"
:loading="batchStore.loading"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useBatchStore } from '../stores/batches'
import { get } from '../api/client'
import AppHeader from '../components/AppHeader.vue'
import BatchList from '../components/BatchList.vue'
import type { WorkQueueOverview } from '../types'
const batchStore = useBatchStore()
const overview = ref<WorkQueueOverview | null>(null)
const maxCount = computed(() => {
if (!overview.value?.statusCounts) return 1
return Math.max(...Object.values(overview.value.statusCounts).map(Number), 1)
})
function formatMinutes(minutes: number): string {
if (minutes < 60) return `${Math.round(minutes)}m`
if (minutes < 1440) return `${(minutes / 60).toFixed(1)}h`
return `${(minutes / 1440).toFixed(1)}d`
}
async function refreshData() {
const response = await get<WorkQueueOverview>('work-queue/overview')
if (response.success && response.data) {
overview.value = response.data
}
await batchStore.listBatches({ page: 1, pageSize: 50 })
}
onMounted(refreshData)
</script>