feature: Supporting Screens
This commit is contained in:
@@ -4,108 +4,145 @@
|
||||
<template #actions>
|
||||
<button
|
||||
type="button"
|
||||
class="text-sm font-medium text-primary-700 hover:text-primary-800"
|
||||
:disabled="refreshing"
|
||||
@click="refreshData"
|
||||
class="text-sm text-primary-600 hover:text-primary-800"
|
||||
>
|
||||
Refresh
|
||||
{{ refreshing ? 'Refreshing…' : '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-elevated">
|
||||
<p class="text-sm text-gray-500">Pending Entry</p>
|
||||
<p class="text-3xl font-bold text-yellow-600">
|
||||
{{ overview?.statusCounts?.UPLOADED ?? 0 }}
|
||||
<div class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
||||
<div class="max-w-7xl mx-auto space-y-6 lg:space-y-8">
|
||||
<div>
|
||||
<h1 class="text-[1.75rem] font-semibold text-ink-strong leading-tight tracking-tight">
|
||||
Queue Dashboard
|
||||
</h1>
|
||||
<p class="mt-1 text-sm text-ink-secondary">
|
||||
Supervisor view of digitization backlog, aging work, and reject rate.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-elevated">
|
||||
<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-elevated">
|
||||
<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-elevated">
|
||||
<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-elevated">
|
||||
<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: < 24 hours
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-elevated">
|
||||
<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"
|
||||
:error="batchStore.error"
|
||||
empty-title="No batches in the queue."
|
||||
empty-description="Batches appear here as they move through digitization."
|
||||
<InlineError
|
||||
v-if="overviewError"
|
||||
title="Could not load queue overview"
|
||||
:message="overviewError"
|
||||
preserved="Previously loaded metrics are shown when available."
|
||||
retry-label="Retry"
|
||||
@retry="refreshData"
|
||||
/>
|
||||
|
||||
<!-- Priority work metrics -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-5 gap-3 sm:gap-4">
|
||||
<div class="card-elevated">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-ink-secondary">
|
||||
Pending Entry
|
||||
</p>
|
||||
<p class="mt-2 text-3xl font-bold tabular-nums text-clinical-warning">
|
||||
{{ overview?.statusCounts?.UPLOADED ?? 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-elevated">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-ink-secondary">
|
||||
In Entry
|
||||
</p>
|
||||
<p class="mt-2 text-3xl font-bold tabular-nums text-primary-700">
|
||||
{{ overview?.statusCounts?.IN_ENTRY ?? 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-elevated">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-ink-secondary">
|
||||
Pending Verification
|
||||
</p>
|
||||
<p class="mt-2 text-3xl font-bold tabular-nums text-clinical-warning">
|
||||
{{ overview?.statusCounts?.PENDING_VERIFICATION ?? 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-elevated">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-ink-secondary">
|
||||
Oldest Pending Verification
|
||||
</p>
|
||||
<p
|
||||
class="mt-2 text-3xl font-bold tabular-nums"
|
||||
:class="(overview?.oldestPendingVerificationMinutes ?? 0) > 1440
|
||||
? 'text-clinical-danger'
|
||||
: 'text-ink-strong'"
|
||||
>
|
||||
{{ formatMinutes(overview?.oldestPendingVerificationMinutes ?? 0) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-elevated">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-ink-secondary">
|
||||
Reject Rate
|
||||
</p>
|
||||
<p
|
||||
class="mt-2 text-3xl font-bold tabular-nums"
|
||||
:class="(overview?.rejectRate ?? 0) > 0.15
|
||||
? 'text-clinical-danger'
|
||||
: 'text-clinical-safe'"
|
||||
>
|
||||
{{ ((overview?.rejectRate ?? 0) * 100).toFixed(1) }}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Secondary age metric -->
|
||||
<div class="card-elevated max-w-md">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-ink-secondary">
|
||||
Average Time in Queue
|
||||
</p>
|
||||
<p class="mt-2 text-2xl font-bold tabular-nums text-ink-strong">
|
||||
{{ formatMinutes(overview?.averageTimeInQueueMinutes ?? 0) }}
|
||||
</p>
|
||||
<p class="text-xs text-ink-secondary mt-2">Target: under 24 hours</p>
|
||||
</div>
|
||||
|
||||
<!-- Status breakdown from overview.statusCounts -->
|
||||
<section class="card">
|
||||
<h2 class="text-base font-semibold text-ink-strong mb-4">Batches by Status</h2>
|
||||
<div v-if="!overview && refreshing" class="py-2">
|
||||
<SkeletonBlock variant="row" :rows="5" />
|
||||
</div>
|
||||
<EmptyState
|
||||
v-else-if="statusEntries.length === 0"
|
||||
title="No status counts yet."
|
||||
description="Refresh to load work-queue overview metrics."
|
||||
/>
|
||||
<div v-else class="space-y-3">
|
||||
<div
|
||||
v-for="{ status, count } in statusEntries"
|
||||
:key="status"
|
||||
class="flex items-center gap-3 sm:gap-4"
|
||||
>
|
||||
<div class="w-44 shrink-0">
|
||||
<StatusBadge :status="status" />
|
||||
</div>
|
||||
<div class="flex-1 h-2.5 rounded-full bg-canvas border border-line overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full bg-primary-500"
|
||||
:style="{ width: `${(count / maxCount) * 100}%` }"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-sm font-semibold tabular-nums text-ink-strong w-8 text-right">
|
||||
{{ count }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Existing batch list via batch list API -->
|
||||
<section class="card">
|
||||
<h2 class="text-base font-semibold text-ink-strong mb-4">All Batches</h2>
|
||||
<BatchList
|
||||
:batches="batchStore.batches"
|
||||
:loading="batchStore.loading"
|
||||
:error="batchStore.error"
|
||||
empty-title="No batches in the queue."
|
||||
empty-description="Batches appear here as they move through digitization."
|
||||
@retry="refreshData"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,16 +154,28 @@ import { useBatchStore } from '../stores/batches'
|
||||
import { get } from '../api/client'
|
||||
import AppHeader from '../components/AppHeader.vue'
|
||||
import BatchList from '../components/BatchList.vue'
|
||||
import StatusBadge from '../components/StatusBadge.vue'
|
||||
import EmptyState from '../components/EmptyState.vue'
|
||||
import SkeletonBlock from '../components/SkeletonBlock.vue'
|
||||
import InlineError from '../components/InlineError.vue'
|
||||
import type { WorkQueueOverview } from '../types'
|
||||
|
||||
const batchStore = useBatchStore()
|
||||
const overview = ref<WorkQueueOverview | null>(null)
|
||||
const overviewError = ref('')
|
||||
const refreshing = ref(false)
|
||||
|
||||
const maxCount = computed(() => {
|
||||
if (!overview.value?.statusCounts) return 1
|
||||
return Math.max(...Object.values(overview.value.statusCounts).map(Number), 1)
|
||||
const statusEntries = computed(() => {
|
||||
const counts = overview.value?.statusCounts ?? {}
|
||||
return Object.entries(counts)
|
||||
.map(([status, count]) => ({ status, count: Number(count) }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
})
|
||||
|
||||
const maxCount = computed(() =>
|
||||
Math.max(...statusEntries.value.map(e => e.count), 1),
|
||||
)
|
||||
|
||||
function formatMinutes(minutes: number): string {
|
||||
if (minutes < 60) return `${Math.round(minutes)}m`
|
||||
if (minutes < 1440) return `${(minutes / 60).toFixed(1)}h`
|
||||
@@ -134,12 +183,22 @@ function formatMinutes(minutes: number): string {
|
||||
}
|
||||
|
||||
async function refreshData() {
|
||||
const response = await get<WorkQueueOverview>('work-queue/overview')
|
||||
if (response.success && response.data) {
|
||||
overview.value = response.data
|
||||
refreshing.value = true
|
||||
overviewError.value = ''
|
||||
try {
|
||||
const response = await get<WorkQueueOverview>('work-queue/overview')
|
||||
if (response.success && response.data) {
|
||||
overview.value = response.data
|
||||
} else {
|
||||
overviewError.value = response.error?.message ?? 'Failed to load work-queue overview'
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
overviewError.value = e instanceof Error ? e.message : 'Failed to load work-queue overview'
|
||||
}
|
||||
|
||||
await batchStore.listBatches({ page: 1, pageSize: 50 })
|
||||
refreshing.value = false
|
||||
}
|
||||
|
||||
onMounted(refreshData)
|
||||
</script>
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user