Fix responsiveness
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
Use Playwright MCP as a visual QA and frontend polish tool for this page.
|
||||||
|
|
||||||
|
Inspect the page at mobile, tablet, and desktop breakpoints, identify styling/layout issues, and fix them directly in the code.
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
- spacing
|
||||||
|
- sizing
|
||||||
|
- alignment
|
||||||
|
- element positions
|
||||||
|
- typography scaling
|
||||||
|
- card proportions
|
||||||
|
- image/text balance
|
||||||
|
- overflow/clipping
|
||||||
|
- responsiveness
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- keep Tailwind CSS
|
||||||
|
- preserve the current design intent
|
||||||
|
- improve the page until it looks polished and production-ready
|
||||||
|
- verify each fix visually with Playwright MCP
|
||||||
|
- iterate until no obvious visual issues remain
|
||||||
|
|
||||||
|
|
||||||
|
Test at:
|
||||||
|
- 375x812
|
||||||
|
- 768x1024
|
||||||
|
- 1280x800
|
||||||
|
|
||||||
|
Use VigilCareRecordsAPI/Data/Seed/DataSeeder.cs to get the logins
|
||||||
|
|
||||||
|
Do not only report problems.
|
||||||
|
Make the fixes, re-test, and then provide a short summary of what was improved.
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
Here's the updated prompt:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Role
|
||||||
|
Act as a senior QA engineer testing a Vue 3 + Node.js web application for logic errors, bugs, and edge cases.
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
A plain text report of all issues found, grouped by file or feature domain, with severity level per issue (Critical / High / Medium / Low).
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Test the provided functionality, section, or domain by reading the code and mentally executing it across normal, boundary, and failure scenarios. Report every defect found. When testing a view or feature, trace execution downward through all underlying components, composables, and backend routes and controllers that the feature depends on.
|
||||||
|
|
||||||
|
## Grounding Rules
|
||||||
|
- Stay within the confines of the provided code — do not invent features, routes, or behaviors that are not present.
|
||||||
|
- Do not hallucinate API responses, database states, or UI interactions not inferable from the code.
|
||||||
|
- Do not suggest third-party testing tools or libraries unless already present in the codebase.
|
||||||
|
- If a behavior is ambiguous, flag it as a question rather than assuming intent.
|
||||||
|
- When a view or component calls a composable, follow that composable's logic as part of the same test pass.
|
||||||
|
- When a composable or service makes an API call, follow the corresponding backend route, middleware, and controller as part of the same test pass.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
Test each provided file or domain in this order of priority:
|
||||||
|
|
||||||
|
1. **Logic correctness** — Does the code do what it is clearly intended to do?
|
||||||
|
2. **Edge cases** — Empty inputs, null/undefined values, empty arrays, zero, negative numbers, max-length strings, concurrent calls.
|
||||||
|
3. **Error handling** — Are errors caught? Are failure states handled gracefully? Do error messages leak sensitive data?
|
||||||
|
4. **Reactivity correctness** (frontend) — Does state update when it should? Can stale state be observed?
|
||||||
|
5. **Data flow** — Are values passed, transformed, or mutated in ways that could produce unexpected results downstream? Trace data from the frontend input all the way to the database query and back.
|
||||||
|
6. **Boundary conditions** — Off-by-one errors, pagination limits, permission boundaries, rate limits.
|
||||||
|
7. **Race conditions** — Async operations that could resolve out of order or leave state inconsistent.
|
||||||
|
8. **Contract mismatches** — Does the frontend expect a response shape the backend does not guarantee? Are required fields missing, optional fields assumed present, or error codes unhandled?
|
||||||
|
9. **Login/Auth Requirements** - If auth or login is required use the following credentials email: bradleystorm.sevt@mockinbox.com and password: Password123!
|
||||||
|
|
||||||
|
**Conflict resolution:** If a behavior could be either a bug or an intentional design choice, report it as a flagged ambiguity rather than a confirmed defect. Do not silently assume either way.
|
||||||
|
|
||||||
|
**Priority hierarchy:** Logic correctness > Error handling > Edge cases > Data flow > Contract mismatches > Boundary conditions > Race conditions > Reactivity.
|
||||||
|
|
||||||
|
## Trace Depth
|
||||||
|
When a file is provided as the entry point for testing, automatically include in scope:
|
||||||
|
- All composables imported and called by that file
|
||||||
|
- All child components rendered by that file
|
||||||
|
- All backend routes, middleware, and controllers called by those composables or services
|
||||||
|
- All database queries executed by those controllers
|
||||||
|
|
||||||
|
Report issues at the layer where they originate, not just where their effect is observed.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
**Bad output (do not produce this):**
|
||||||
|
```
|
||||||
|
- The login form might have issues.
|
||||||
|
- Consider adding more validation.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Good output (produce this):**
|
||||||
|
```
|
||||||
|
FILE: src/composables/useAuth.js
|
||||||
|
SEVERITY: Critical
|
||||||
|
ISSUE: If `refreshToken()` is called while a refresh is already in flight, two concurrent requests are fired. The second response overwrites the token set by the first, leaving the app in a potentially invalid auth state.
|
||||||
|
REPRODUCTION: Trigger two API calls simultaneously on a near-expired token.
|
||||||
|
FIX RECOMMENDATION: Guard the refresh call with an in-flight flag or return the existing promise if one is pending.
|
||||||
|
|
||||||
|
FILE: backend/controllers/authController.js
|
||||||
|
SEVERITY: High
|
||||||
|
ISSUE: The refresh token is not invalidated after use. A leaked token can be replayed indefinitely until expiry.
|
||||||
|
REPRODUCTION: Capture the refresh token from a valid session and reuse it after the session has been refreshed.
|
||||||
|
FIX RECOMMENDATION: Implement refresh token rotation — invalidate the used token and issue a new one on each refresh.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Context / Input
|
||||||
|
Paste files in this order, highest reliability first:
|
||||||
|
1. Backend routes, middleware, and controllers
|
||||||
|
2. Composables and services
|
||||||
|
3. Components and views
|
||||||
|
|
||||||
|
## Final Reminder
|
||||||
|
- Do not fabricate bugs. Every reported issue must be traceable to a specific line or code path in the provided files.
|
||||||
|
- Do not skip files because they look simple — shallow files are common sources of silent failures.
|
||||||
|
- Ambiguity is a valid finding. Flag it rather than resolve it silently.
|
||||||
|
- Always trace execution through the full stack — frontend to composable to backend to database — before closing a test pass on any feature.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
Plain text only. No markdown formatting, no bullet symbols, no headers with hashes. Group findings by file. For each issue state: FILE, SEVERITY, ISSUE, REPRODUCTION STEPS, FIX RECOMMENDATION. If a file has no issues, write the filename followed by "No issues found." Restate this format requirement if the session resets mid-task.
|
||||||
@@ -98,7 +98,7 @@ describe('IntakeView cover sheet upload', () => {
|
|||||||
const input = wrapper.find('input[placeholder="VCR-CS-XXXXXXXX"]')
|
const input = wrapper.find('input[placeholder="VCR-CS-XXXXXXXX"]')
|
||||||
|
|
||||||
expect(wrapper.text()).toContain('Cover Sheet Code')
|
expect(wrapper.text()).toContain('Cover Sheet Code')
|
||||||
expect(wrapper.text()).toContain('Upload Scanned Document')
|
expect(wrapper.text()).toContain('New Batch')
|
||||||
expect(wrapper.find('.upload-dropzone').exists()).toBe(true)
|
expect(wrapper.find('.upload-dropzone').exists()).toBe(true)
|
||||||
expect(input.exists()).toBe(true)
|
expect(input.exists()).toBe(true)
|
||||||
expect(input.attributes('autofocus')).toBeDefined()
|
expect(input.attributes('autofocus')).toBeDefined()
|
||||||
|
|||||||
@@ -19,7 +19,15 @@ vi.mock('@/composables/useToast', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/components/AppHeader.vue', () => ({
|
vi.mock('@/components/AppHeader.vue', () => ({
|
||||||
default: { template: '<div data-testid="app-header" />' },
|
default: {
|
||||||
|
props: ['title', 'description'],
|
||||||
|
template: `
|
||||||
|
<div data-testid="app-header" :title="title" :description="description">
|
||||||
|
<slot name="actions" />
|
||||||
|
<slot name="subtitle" />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/components/EmptyState.vue', () => ({
|
vi.mock('@/components/EmptyState.vue', () => ({
|
||||||
|
|||||||
@@ -60,14 +60,14 @@
|
|||||||
@apply p-4 sm:p-6 lg:p-8 max-w-4xl mx-auto;
|
@apply p-4 sm:p-6 lg:p-8 max-w-4xl mx-auto;
|
||||||
}
|
}
|
||||||
.app-header {
|
.app-header {
|
||||||
@apply flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between
|
@apply flex flex-row items-start justify-between gap-3
|
||||||
px-4 py-3 sm:px-6 h-auto min-h-16 bg-surface border-b border-line;
|
px-4 py-3 sm:px-6 sm:items-center min-h-14 sm:min-h-16 bg-surface border-b border-line;
|
||||||
}
|
}
|
||||||
.app-header-title {
|
.app-header-title {
|
||||||
@apply flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4 min-w-0;
|
@apply flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-4 min-w-0;
|
||||||
}
|
}
|
||||||
.app-header-actions {
|
.app-header-actions {
|
||||||
@apply flex flex-wrap items-center gap-2 sm:gap-4;
|
@apply flex flex-wrap items-center justify-end gap-2 sm:gap-4 shrink-0;
|
||||||
}
|
}
|
||||||
.card {
|
.card {
|
||||||
@apply bg-surface rounded-card border border-line p-4 sm:p-6 shadow-card;
|
@apply bg-surface rounded-card border border-line p-4 sm:p-6 shadow-card;
|
||||||
@@ -92,7 +92,8 @@
|
|||||||
@apply flex flex-col min-h-0 overflow-hidden bg-surface;
|
@apply flex flex-col min-h-0 overflow-hidden bg-surface;
|
||||||
}
|
}
|
||||||
.workstation-scan {
|
.workstation-scan {
|
||||||
@apply flex flex-col min-h-[40vh] xl:min-h-0 p-3 xl:p-4 bg-canvas border-b xl:border-b-0 xl:border-r border-line;
|
@apply flex flex-col min-h-[13rem] sm:min-h-[18rem] xl:min-h-0
|
||||||
|
p-3 xl:p-4 bg-canvas border-b xl:border-b-0 xl:border-r border-line;
|
||||||
}
|
}
|
||||||
.workstation-form {
|
.workstation-form {
|
||||||
@apply flex flex-col min-h-0 overflow-hidden bg-surface;
|
@apply flex flex-col min-h-0 overflow-hidden bg-surface;
|
||||||
|
|||||||
@@ -1,15 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-header">
|
<div class="app-header">
|
||||||
<div class="app-header-title">
|
<div class="app-header-title min-w-0 flex-1">
|
||||||
<h1 class="text-lg font-semibold text-ink-strong">{{ title }}</h1>
|
<div class="min-w-0">
|
||||||
|
<h1 class="text-lg sm:text-xl font-semibold text-ink-strong leading-tight">
|
||||||
|
{{ title }}
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
v-if="description"
|
||||||
|
class="mt-0.5 text-sm text-ink-secondary leading-snug max-w-2xl"
|
||||||
|
>
|
||||||
|
{{ description }}
|
||||||
|
</p>
|
||||||
<slot name="subtitle" />
|
<slot name="subtitle" />
|
||||||
</div>
|
</div>
|
||||||
<div class="app-header-actions">
|
</div>
|
||||||
|
<div class="app-header-actions self-start">
|
||||||
<slot name="actions" />
|
<slot name="actions" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{ title: string }>()
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
}>(),
|
||||||
|
{ description: undefined },
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,33 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-screen min-h-0 bg-canvas text-ink">
|
<div class="flex h-screen min-h-0 bg-canvas text-ink">
|
||||||
|
<!-- Mobile backdrop -->
|
||||||
|
<div
|
||||||
|
v-if="mobileOpen"
|
||||||
|
class="fixed inset-0 z-40 bg-navy/40 lg:hidden"
|
||||||
|
aria-hidden="true"
|
||||||
|
@click="closeMobile"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<aside
|
<aside
|
||||||
:class="[
|
:class="[
|
||||||
'flex flex-col shrink-0 bg-navy text-white transition-[width] duration-200 ease-out',
|
'flex flex-col shrink-0 bg-navy text-white transition-[width,transform] duration-200 ease-out shadow-xl lg:shadow-none',
|
||||||
collapsed ? 'w-16' : 'w-60',
|
'fixed inset-y-0 left-0 z-50 lg:static lg:z-auto',
|
||||||
|
mobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0',
|
||||||
|
sidebarCollapsed ? 'w-16' : 'w-60',
|
||||||
]"
|
]"
|
||||||
aria-label="Main navigation"
|
aria-label="Main navigation"
|
||||||
|
:aria-hidden="navAriaHidden"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
'flex items-center h-16 border-b border-white/10 shrink-0',
|
'flex items-center h-14 sm:h-16 border-b border-white/10 shrink-0',
|
||||||
collapsed ? 'justify-center px-2' : 'gap-2.5 px-4',
|
sidebarCollapsed ? 'justify-center px-2' : 'gap-2.5 px-4',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 32 32" class="w-7 h-7 shrink-0" aria-hidden="true">
|
<svg viewBox="0 0 32 32" class="w-7 h-7 shrink-0" aria-hidden="true">
|
||||||
<path fill="#155EEF" d="M16 2 4 6.5v8.2c0 8 5.1 14.6 12 15.3 6.9-.7 12-7.3 12-15.3V6.5z"/>
|
<path fill="#155EEF" d="M16 2 4 6.5v8.2c0 8 5.1 14.6 12 15.3 6.9-.7 12-7.3 12-15.3V6.5z"/>
|
||||||
<path fill="white" d="M16 8.5c-3.6 0-6.6 2.8-6.6 6.3 0 3.4 2.7 6.4 6.6 9.6 3.9-3.2 6.6-6.2 6.6-9.6 0-3.5-3-6.3-6.6-6.3m0 3.4c1.7 0 3.1 1.3 3.1 2.9s-1.4 2.9-3.1 2.9-3.1-1.3-3.1-2.9 1.4-2.9 3.1-2.9"/>
|
<path fill="white" d="M16 8.5c-3.6 0-6.6 2.8-6.6 6.3 0 3.4 2.7 6.4 6.6 9.6 3.9-3.2 6.6-6.2 6.6-9.6 0-3.5-3-6.3-6.6-6.3m0 3.4c1.7 0 3.1 1.3 3.1 2.9s-1.4 2.9-3.1 2.9-3.1-1.3-3.1-2.9 1.4-2.9 3.1-2.9"/>
|
||||||
</svg>
|
</svg>
|
||||||
<div v-if="!collapsed" class="min-w-0 leading-tight">
|
<div v-if="!sidebarCollapsed" class="min-w-0 leading-tight">
|
||||||
<p class="font-bold tracking-tight text-sm truncate">VigilCare</p>
|
<p class="font-bold tracking-tight text-sm truncate">VigilCare</p>
|
||||||
<p class="text-[11px] text-white/60 truncate">Records</p>
|
<p class="text-[11px] text-white/60 truncate">Records</p>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ml-auto lg:hidden rounded-input p-1.5 text-white/70 hover:bg-white/10 hover:text-white"
|
||||||
|
aria-label="Close navigation"
|
||||||
|
@click="closeMobile"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="flex-1 overflow-y-auto py-3 px-2 space-y-4">
|
<nav class="flex-1 overflow-y-auto py-3 px-2 space-y-4">
|
||||||
<div v-if="workspaceItems.length">
|
<div v-if="workspaceItems.length">
|
||||||
<p
|
<p
|
||||||
v-if="!collapsed"
|
v-if="!sidebarCollapsed"
|
||||||
class="px-2 mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-white/40"
|
class="px-2 mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-white/40"
|
||||||
>
|
>
|
||||||
Workspace
|
Workspace
|
||||||
@@ -36,13 +57,14 @@
|
|||||||
<li v-for="item in workspaceItems" :key="item.to">
|
<li v-for="item in workspaceItems" :key="item.to">
|
||||||
<router-link
|
<router-link
|
||||||
:to="item.to"
|
:to="item.to"
|
||||||
:title="collapsed ? item.label : undefined"
|
:title="sidebarCollapsed ? item.label : undefined"
|
||||||
:class="navItemClass(item.to)"
|
:class="navItemClass(item.to)"
|
||||||
|
@click="closeMobile"
|
||||||
>
|
>
|
||||||
<span class="shrink-0 w-5 h-5 flex items-center justify-center" aria-hidden="true">
|
<span class="shrink-0 w-5 h-5 flex items-center justify-center" aria-hidden="true">
|
||||||
<component :is="item.icon" />
|
<component :is="item.icon" />
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!collapsed" class="truncate">{{ item.label }}</span>
|
<span v-if="!sidebarCollapsed" class="truncate">{{ item.label }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -50,7 +72,7 @@
|
|||||||
|
|
||||||
<div v-if="adminItems.length">
|
<div v-if="adminItems.length">
|
||||||
<p
|
<p
|
||||||
v-if="!collapsed"
|
v-if="!sidebarCollapsed"
|
||||||
class="px-2 mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-white/40"
|
class="px-2 mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-white/40"
|
||||||
>
|
>
|
||||||
Administration
|
Administration
|
||||||
@@ -59,20 +81,21 @@
|
|||||||
<li v-for="item in adminItems" :key="item.to">
|
<li v-for="item in adminItems" :key="item.to">
|
||||||
<router-link
|
<router-link
|
||||||
:to="item.to"
|
:to="item.to"
|
||||||
:title="collapsed ? item.label : undefined"
|
:title="sidebarCollapsed ? item.label : undefined"
|
||||||
:class="navItemClass(item.to)"
|
:class="navItemClass(item.to)"
|
||||||
|
@click="closeMobile"
|
||||||
>
|
>
|
||||||
<span class="shrink-0 w-5 h-5 flex items-center justify-center" aria-hidden="true">
|
<span class="shrink-0 w-5 h-5 flex items-center justify-center" aria-hidden="true">
|
||||||
<component :is="item.icon" />
|
<component :is="item.icon" />
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!collapsed" class="truncate">{{ item.label }}</span>
|
<span v-if="!sidebarCollapsed" class="truncate">{{ item.label }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="shrink-0 border-t border-white/10 p-2">
|
<div class="shrink-0 border-t border-white/10 p-2 hidden lg:block">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex w-full items-center gap-2.5 rounded-input px-2.5 py-2 text-sm text-white/70 hover:bg-white/10 hover:text-white transition-colors"
|
class="flex w-full items-center gap-2.5 rounded-input px-2.5 py-2 text-sm text-white/70 hover:bg-white/10 hover:text-white transition-colors"
|
||||||
@@ -97,15 +120,33 @@
|
|||||||
|
|
||||||
<!-- Main column -->
|
<!-- Main column -->
|
||||||
<div class="flex flex-col flex-1 min-w-0 min-h-0">
|
<div class="flex flex-col flex-1 min-w-0 min-h-0">
|
||||||
<header class="flex items-center justify-end gap-4 h-16 shrink-0 px-4 sm:px-6 bg-surface border-b border-line">
|
<header
|
||||||
<span class="text-sm text-ink-secondary truncate">{{ auth.userFullName }}</span>
|
class="flex items-center justify-between gap-3 h-14 sm:h-16 shrink-0 px-3 sm:px-6 bg-surface border-b border-line"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-sm text-ink-secondary hover:text-ink-strong transition-colors"
|
class="lg:hidden inline-flex items-center justify-center rounded-input p-2 text-ink-secondary hover:bg-canvas hover:text-ink-strong"
|
||||||
|
aria-label="Open navigation"
|
||||||
|
:aria-expanded="mobileOpen"
|
||||||
|
@click="mobileOpen = true"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 5.25a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10zm0 5.25a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end gap-3 sm:gap-4 min-w-0 ml-auto">
|
||||||
|
<span class="text-sm text-ink-secondary truncate max-w-[9rem] sm:max-w-none">
|
||||||
|
{{ auth.userFullName }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 text-sm text-ink-secondary hover:text-ink-strong transition-colors"
|
||||||
@click="auth.logout()"
|
@click="auth.logout()"
|
||||||
>
|
>
|
||||||
Sign Out
|
Sign Out
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="flex-1 min-h-0 overflow-auto">
|
<main class="flex-1 min-h-0 overflow-auto">
|
||||||
@@ -116,7 +157,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, h, ref, watch } from 'vue'
|
import { computed, h, onUnmounted, ref, watch } from 'vue'
|
||||||
|
import { useMediaQuery } from '@vueuse/core'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
@@ -124,13 +166,40 @@ const COLLAPSE_KEY = 'vigilcare_sidebar_collapsed'
|
|||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const isDesktopNav = useMediaQuery('(min-width: 1024px)')
|
||||||
|
|
||||||
const collapsed = ref(localStorage.getItem(COLLAPSE_KEY) === '1')
|
const collapsed = ref(localStorage.getItem(COLLAPSE_KEY) === '1')
|
||||||
|
const mobileOpen = ref(false)
|
||||||
|
|
||||||
|
/** On mobile the drawer is always expanded; desktop uses the collapse preference. */
|
||||||
|
const sidebarCollapsed = computed(() => collapsed.value && !mobileOpen.value)
|
||||||
|
|
||||||
|
/** Hide off-canvas nav from AT on small screens when closed. */
|
||||||
|
const navAriaHidden = computed(() => !isDesktopNav.value && !mobileOpen.value)
|
||||||
|
|
||||||
watch(collapsed, (value) => {
|
watch(collapsed, (value) => {
|
||||||
localStorage.setItem(COLLAPSE_KEY, value ? '1' : '0')
|
localStorage.setItem(COLLAPSE_KEY, value ? '1' : '0')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.fullPath,
|
||||||
|
() => {
|
||||||
|
closeMobile()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(mobileOpen, (open) => {
|
||||||
|
document.body.style.overflow = open ? 'hidden' : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
function closeMobile() {
|
||||||
|
mobileOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
function toggleCollapsed() {
|
function toggleCollapsed() {
|
||||||
collapsed.value = !collapsed.value
|
collapsed.value = !collapsed.value
|
||||||
}
|
}
|
||||||
@@ -223,7 +292,7 @@ function isActive(path: string): boolean {
|
|||||||
function navItemClass(path: string): string {
|
function navItemClass(path: string): string {
|
||||||
const base =
|
const base =
|
||||||
'flex items-center gap-2.5 rounded-input text-sm transition-colors ' +
|
'flex items-center gap-2.5 rounded-input text-sm transition-colors ' +
|
||||||
(collapsed.value ? 'justify-center px-2 py-2.5' : 'px-2.5 py-2')
|
(sidebarCollapsed.value ? 'justify-center px-2 py-2.5' : 'px-2.5 py-2')
|
||||||
if (isActive(path)) {
|
if (isActive(path)) {
|
||||||
return `${base} bg-primary-600 text-white font-medium`
|
return `${base} bg-primary-600 text-white font-medium`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<span class="evidence-level-mark" aria-hidden="true">2</span>
|
<span class="evidence-level-mark" aria-hidden="true">2</span>
|
||||||
Verified draft
|
Verified draft
|
||||||
</p>
|
</p>
|
||||||
<h2 class="text-base font-semibold text-ink-strong">Clinical Approval</h2>
|
<h2 class="text-base font-semibold text-ink-strong">Sign-off review</h2>
|
||||||
<p class="mt-1 text-xs text-ink-secondary max-w-md">
|
<p class="mt-1 text-xs text-ink-secondary max-w-md">
|
||||||
Clinical sign-off before promotion to live clinical tables. Review the source scan and verified draft.
|
Clinical sign-off before promotion to live clinical tables. Review the source scan and verified draft.
|
||||||
</p>
|
</p>
|
||||||
@@ -125,11 +125,13 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="batch?.verifiedByUserId"
|
v-if="batch?.verifiedByUserId || batch?.verifiedByUserName"
|
||||||
class="rounded-input border border-line bg-canvas px-3 py-2 text-sm text-ink-secondary"
|
class="rounded-input border border-line bg-canvas px-3 py-2 text-sm"
|
||||||
>
|
>
|
||||||
<span class="text-xs uppercase tracking-wide font-semibold">Verified by</span>
|
<span class="text-xs uppercase tracking-wide font-semibold text-ink-secondary">Verified by</span>
|
||||||
<p class="font-mono text-ink mt-0.5">{{ batch.verifiedByUserId.substring(0, 8) }}…</p>
|
<p class="text-ink-strong mt-0.5">
|
||||||
|
{{ batch.verifiedByUserName?.trim() || 'Prior verifier' }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Level 4 decision controls (copy weight below draft) -->
|
<!-- Level 4 decision controls (copy weight below draft) -->
|
||||||
@@ -230,7 +232,7 @@
|
|||||||
<template #left>
|
<template #left>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-danger"
|
class="btn-danger w-full sm:w-auto"
|
||||||
:disabled="processing || !!promotionResult"
|
:disabled="processing || !!promotionResult"
|
||||||
data-testid="reject-batch"
|
data-testid="reject-batch"
|
||||||
@click="showRejectDialog = true"
|
@click="showRejectDialog = true"
|
||||||
@@ -241,7 +243,7 @@
|
|||||||
<template #primary>
|
<template #primary>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-primary"
|
class="btn-primary w-full sm:w-auto"
|
||||||
:disabled="processing || !!promotionResult"
|
:disabled="processing || !!promotionResult"
|
||||||
data-testid="approve-promote"
|
data-testid="approve-promote"
|
||||||
@click="showApproveConfirm = true"
|
@click="showApproveConfirm = true"
|
||||||
@@ -252,7 +254,7 @@
|
|||||||
<template #right>
|
<template #right>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-secondary"
|
class="btn-secondary w-full sm:w-auto"
|
||||||
:disabled="processing"
|
:disabled="processing"
|
||||||
@click="$emit('back')"
|
@click="$emit('back')"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<slot name="emptyAction" />
|
<slot name="emptyAction" />
|
||||||
</template>
|
</template>
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
<table v-else class="w-full min-w-[640px] text-sm">
|
<table v-else class="w-full min-w-[36rem] text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b text-left text-ink-secondary">
|
<tr class="border-b text-left text-ink-secondary">
|
||||||
<th class="py-2 px-4">ID</th>
|
<th class="py-2 px-4">ID</th>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<span class="evidence-level-mark" aria-hidden="true">2</span>
|
<span class="evidence-level-mark" aria-hidden="true">2</span>
|
||||||
Structured draft
|
Structured draft
|
||||||
</p>
|
</p>
|
||||||
<h2 class="text-base font-semibold text-ink-strong">Data Entry</h2>
|
<h2 class="text-base font-semibold text-ink-strong">Draft fields</h2>
|
||||||
<p
|
<p
|
||||||
class="mt-1 text-xs tabular-nums"
|
class="mt-1 text-xs tabular-nums"
|
||||||
:class="{
|
:class="{
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col lg:flex-row lg:items-start gap-4 p-4 bg-gray-50 rounded-md">
|
<div
|
||||||
<div class="flex-1 grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-5 gap-4">
|
class="flex flex-col xl:flex-row xl:items-start gap-3 p-3 rounded-input border border-line bg-canvas"
|
||||||
|
data-testid="observation-row"
|
||||||
|
>
|
||||||
|
<div class="flex-1 grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-5 gap-3 min-w-0">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs text-gray-500">Code</label>
|
<label class="workstation-field-label">Code</label>
|
||||||
<select
|
<select
|
||||||
:value="observation.observationCode"
|
:value="observation.observationCode"
|
||||||
@change="update('observationCode', ($event.target as HTMLSelectElement).value)"
|
@change="update('observationCode', ($event.target as HTMLSelectElement).value)"
|
||||||
class="form-input text-sm"
|
class="form-input text-sm py-1.5"
|
||||||
:disabled="readonly"
|
:disabled="readonly"
|
||||||
>
|
>
|
||||||
<option value="">Select...</option>
|
<option value="">Select...</option>
|
||||||
@@ -23,7 +26,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="flex items-center gap-2 text-xs text-gray-500">
|
<label class="workstation-field-label">
|
||||||
Value
|
Value
|
||||||
<OcrConfidenceBadge :label="ocrLabel" :level="ocrLevel" />
|
<OcrConfidenceBadge :label="ocrLabel" :level="ocrLevel" />
|
||||||
</label>
|
</label>
|
||||||
@@ -32,59 +35,63 @@
|
|||||||
@change="update('value', parseFloat(($event.target as HTMLInputElement).value))"
|
@change="update('value', parseFloat(($event.target as HTMLInputElement).value))"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
:class="['form-input', 'text-sm', valueInputClass]"
|
:class="['form-input', 'text-sm', 'py-1.5', valueInputClass]"
|
||||||
:disabled="readonly"
|
:disabled="readonly"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs text-gray-500">Unit</label>
|
<label class="workstation-field-label">Unit</label>
|
||||||
<input
|
<input
|
||||||
:value="observation.unit"
|
:value="observation.unit"
|
||||||
@change="update('unit', ($event.target as HTMLInputElement).value)"
|
@change="update('unit', ($event.target as HTMLInputElement).value)"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-input text-sm"
|
class="form-input text-sm py-1.5"
|
||||||
:disabled="readonly"
|
:disabled="readonly"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs text-gray-500">Recorded At</label>
|
<label class="workstation-field-label">Recorded At</label>
|
||||||
<input
|
<input
|
||||||
:value="observation.recordedAt?.substring(0, 16)"
|
:value="observation.recordedAt?.substring(0, 16)"
|
||||||
@change="update('recordedAt', ($event.target as HTMLInputElement).value)"
|
@change="update('recordedAt', ($event.target as HTMLInputElement).value)"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
class="form-input text-sm"
|
class="form-input text-sm py-1.5"
|
||||||
:disabled="readonly"
|
:disabled="readonly"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs text-gray-500">Note</label>
|
<label class="workstation-field-label">Note</label>
|
||||||
<input
|
<input
|
||||||
:value="observation.note"
|
:value="observation.note"
|
||||||
@change="update('note', ($event.target as HTMLInputElement).value)"
|
@change="update('note', ($event.target as HTMLInputElement).value)"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-input text-sm"
|
class="form-input text-sm py-1.5"
|
||||||
placeholder="Optional"
|
placeholder="Optional"
|
||||||
:disabled="readonly"
|
:disabled="readonly"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Verification checkbox (only in verification mode) -->
|
<div
|
||||||
<div v-if="showVerified" class="flex items-center lg:mt-8">
|
v-if="showVerified"
|
||||||
|
class="flex items-center shrink-0 xl:mt-7"
|
||||||
|
>
|
||||||
|
<label class="inline-flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="verified"
|
:checked="verified"
|
||||||
@change="$emit('verify', observation.id, ($event.target as HTMLInputElement).checked)"
|
@change="$emit('verify', observation.id, ($event.target as HTMLInputElement).checked)"
|
||||||
class="w-4 h-4 text-clinical-safe rounded"
|
class="w-4 h-4 text-clinical-safe rounded"
|
||||||
/>
|
/>
|
||||||
<span class="ml-2 text-xs text-gray-500">OK</span>
|
<span class="text-xs text-ink-secondary">OK</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete button (entry mode only) -->
|
|
||||||
<button
|
<button
|
||||||
v-if="!readonly && !showVerified"
|
v-if="!readonly && !showVerified"
|
||||||
|
type="button"
|
||||||
@click="$emit('delete', observation.id)"
|
@click="$emit('delete', observation.id)"
|
||||||
class="lg:mt-8 text-clinical-danger hover:text-red-800 text-sm"
|
class="self-start xl:mt-7 text-clinical-danger hover:text-clinical-critical text-sm font-medium"
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
>
|
>
|
||||||
<!-- Toolbar — light chrome, no decorative overlays on the page -->
|
<!-- Toolbar — light chrome, no decorative overlays on the page -->
|
||||||
<div
|
<div
|
||||||
class="flex flex-wrap items-center gap-1 px-3 py-2 bg-surface border-b border-line text-sm text-ink shrink-0"
|
class="flex flex-wrap items-center gap-0.5 sm:gap-1 px-2 sm:px-3 py-2 bg-surface border-b border-line text-sm text-ink shrink-0"
|
||||||
data-testid="scan-viewer-toolbar"
|
data-testid="scan-viewer-toolbar"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="error" class="flex h-full items-center justify-center p-6">
|
<div v-else-if="error" class="flex min-h-[10rem] items-center justify-center p-4 sm:p-6">
|
||||||
<InlineError
|
<InlineError
|
||||||
class="max-w-md w-full"
|
class="max-w-md w-full"
|
||||||
title="Could not load document"
|
title="Could not load document"
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
<SeparationOfDutiesBanner
|
<SeparationOfDutiesBanner
|
||||||
v-model:blocked="sodBlocked"
|
v-model:blocked="sodBlocked"
|
||||||
:entered-by-user-id="batch?.enteredByUserId"
|
:entered-by-user-id="batch?.enteredByUserId"
|
||||||
|
:entered-by-user-name="enteredByDisplayName"
|
||||||
:current-user-id="auth.userId"
|
:current-user-id="auth.userId"
|
||||||
:current-user-name="auth.userFullName"
|
:current-user-name="auth.userFullName"
|
||||||
/>
|
/>
|
||||||
@@ -117,7 +118,10 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- Encounter review -->
|
<!-- Encounter review -->
|
||||||
<fieldset class="workstation-form-section">
|
<fieldset
|
||||||
|
v-if="showEncounterContext && encounterFields.length > 0"
|
||||||
|
class="workstation-form-section"
|
||||||
|
>
|
||||||
<legend class="workstation-form-legend">Encounter Context</legend>
|
<legend class="workstation-form-legend">Encounter Context</legend>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<div v-for="field in encounterFields" :key="field.path">
|
<div v-for="field in encounterFields" :key="field.path">
|
||||||
@@ -145,11 +149,20 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- Observations review -->
|
<!-- Observations review -->
|
||||||
<fieldset class="workstation-form-section">
|
<fieldset
|
||||||
|
v-if="showObservations"
|
||||||
|
class="workstation-form-section"
|
||||||
|
>
|
||||||
<legend class="workstation-form-legend">
|
<legend class="workstation-form-legend">
|
||||||
Observations ({{ observations.length }})
|
Observations ({{ observations.length }})
|
||||||
</legend>
|
</legend>
|
||||||
<div class="space-y-2">
|
<p
|
||||||
|
v-if="observations.length === 0"
|
||||||
|
class="text-sm text-ink-secondary"
|
||||||
|
>
|
||||||
|
No observations recorded.
|
||||||
|
</p>
|
||||||
|
<div v-else class="space-y-2">
|
||||||
<ObservationRow
|
<ObservationRow
|
||||||
v-for="(obs, index) in observations"
|
v-for="(obs, index) in observations"
|
||||||
:key="obs.id"
|
:key="obs.id"
|
||||||
@@ -325,6 +338,15 @@ function observationValueConfidenceClass(observationCode: string): string {
|
|||||||
const showAllergies = computed(() => fieldReqs.value?.showAllergies ?? false)
|
const showAllergies = computed(() => fieldReqs.value?.showAllergies ?? false)
|
||||||
const showMedications = computed(() => fieldReqs.value?.showMedications ?? false)
|
const showMedications = computed(() => fieldReqs.value?.showMedications ?? false)
|
||||||
const showEncounterSummary = computed(() => fieldReqs.value?.showEncounterSummaryFields ?? false)
|
const showEncounterSummary = computed(() => fieldReqs.value?.showEncounterSummaryFields ?? false)
|
||||||
|
const showEncounterContext = computed(() => fieldReqs.value?.showEncounterContext ?? false)
|
||||||
|
const showObservations = computed(() => fieldReqs.value?.showObservations ?? false)
|
||||||
|
|
||||||
|
const enteredByDisplayName = computed(() => {
|
||||||
|
const name = props.batch?.enteredByUserName?.trim()
|
||||||
|
if (name) return name
|
||||||
|
const fromQueue = batchStore.batches.find((b) => b.id === props.batchId)
|
||||||
|
return fromQueue?.enteredByUserName?.trim() || null
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => batchStore.currentDraft,
|
() => batchStore.currentDraft,
|
||||||
@@ -385,7 +407,7 @@ watch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (draft.encounter) {
|
if (draft.encounter && showEncounterContext.value) {
|
||||||
encounterFields.value = [
|
encounterFields.value = [
|
||||||
{ path: 'encounter.admissionDate', label: 'Admission Date', value: draft.encounter.admissionDate ?? '' },
|
{ path: 'encounter.admissionDate', label: 'Admission Date', value: draft.encounter.admissionDate ?? '' },
|
||||||
{ path: 'encounter.department', label: 'Department', value: draft.encounter.department ?? '' },
|
{ path: 'encounter.department', label: 'Department', value: draft.encounter.department ?? '' },
|
||||||
@@ -398,6 +420,12 @@ watch(
|
|||||||
{ path: 'encounter.dischargeDiagnosis', label: 'Discharge Diagnosis', value: draft.encounter.dischargeDiagnosis ?? '' },
|
{ path: 'encounter.dischargeDiagnosis', label: 'Discharge Diagnosis', value: draft.encounter.dischargeDiagnosis ?? '' },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
encounterFields.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!showObservations.value) {
|
||||||
|
observations.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldChecks.value = {}
|
fieldChecks.value = {}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="sticky bottom-0 z-10 -mx-4 mt-auto border-t border-line bg-surface/95 px-4 py-3 backdrop-blur-sm sm:-mx-0 sm:rounded-b-card"
|
class="sticky bottom-0 z-10 border-t border-line bg-surface/95 px-3 py-3 backdrop-blur-sm sm:px-4"
|
||||||
data-testid="workstation-action-bar"
|
data-testid="workstation-action-bar"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
|
class="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between"
|
||||||
>
|
>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2 min-w-0">
|
||||||
<slot name="left" />
|
<slot name="left" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center justify-center gap-2">
|
<div class="flex flex-wrap items-center gap-2 min-w-0 sm:justify-center">
|
||||||
<slot name="center" />
|
<slot name="center" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
<div class="flex flex-wrap items-center gap-2 sm:justify-end">
|
||||||
<slot name="primary" />
|
<slot name="primary" />
|
||||||
<slot name="right" />
|
<slot name="right" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ function mapWorkQueueItem(item: WorkQueueItemResponse): BatchDetailResponse {
|
|||||||
documentUrl: null,
|
documentUrl: null,
|
||||||
enableRetroactiveAlerts: false,
|
enableRetroactiveAlerts: false,
|
||||||
enteredByUserId: item.enteredByUserId,
|
enteredByUserId: item.enteredByUserId,
|
||||||
|
enteredByUserName: item.enteredByUserName,
|
||||||
verifiedByUserId: null,
|
verifiedByUserId: null,
|
||||||
approvedByUserId: null,
|
approvedByUserId: null,
|
||||||
rejectionReason: item.rejectionReason,
|
rejectionReason: item.rejectionReason,
|
||||||
@@ -225,7 +226,16 @@ export const useBatchStore = defineStore('batches', () => {
|
|||||||
`digitization-batches/${id}`
|
`digitization-batches/${id}`
|
||||||
)
|
)
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
currentBatch.value = response.data
|
const fromQueue = batches.value.find((b) => b.id === id)
|
||||||
|
currentBatch.value = {
|
||||||
|
...response.data,
|
||||||
|
enteredByUserName:
|
||||||
|
response.data.enteredByUserName ?? fromQueue?.enteredByUserName ?? null,
|
||||||
|
verifiedByUserName:
|
||||||
|
response.data.verifiedByUserName ?? fromQueue?.verifiedByUserName ?? null,
|
||||||
|
approvedByUserName:
|
||||||
|
response.data.approvedByUserName ?? fromQueue?.approvedByUserName ?? null,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to load batch'
|
error.value = e instanceof Error ? e.message : 'Failed to load batch'
|
||||||
|
|||||||
@@ -63,8 +63,12 @@ export interface BatchDetailResponse {
|
|||||||
documentUrl: string | null
|
documentUrl: string | null
|
||||||
enableRetroactiveAlerts: boolean
|
enableRetroactiveAlerts: boolean
|
||||||
enteredByUserId: string | null
|
enteredByUserId: string | null
|
||||||
|
/** Present on work-queue items; optional on batch detail until API includes it */
|
||||||
|
enteredByUserName?: string | null
|
||||||
verifiedByUserId: string | null
|
verifiedByUserId: string | null
|
||||||
|
verifiedByUserName?: string | null
|
||||||
approvedByUserId: string | null
|
approvedByUserId: string | null
|
||||||
|
approvedByUserName?: string | null
|
||||||
rejectionReason: string | null
|
rejectionReason: string | null
|
||||||
promotedAt: string | null
|
promotedAt: string | null
|
||||||
promotionEncounterId: string | null
|
promotionEncounterId: string | null
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
<AppHeader title="Clinical Approval">
|
<AppHeader title="Clinical Approval">
|
||||||
<template #subtitle>
|
<template #subtitle>
|
||||||
<span v-if="currentBatch" class="text-sm text-ink-secondary">
|
<span v-if="currentBatch" class="text-sm text-ink-secondary">
|
||||||
Batch: {{ currentBatch.id.substring(0, 8) }}...
|
Batch {{ currentBatch.id.substring(0, 8) }}…
|
||||||
| Type: {{ formatBatchType(currentBatch.batchType) }}
|
· {{ formatBatchType(currentBatch.batchType) }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</AppHeader>
|
</AppHeader>
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 flex flex-col">
|
<div class="h-full min-h-0 flex flex-col">
|
||||||
<AppHeader title="Cover Sheets" />
|
<AppHeader
|
||||||
|
title="Cover Sheets"
|
||||||
|
description="Generate printable separators that reconnect scanned paper to batch metadata."
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
<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 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">
|
|
||||||
Cover Sheet Management
|
|
||||||
</h1>
|
|
||||||
<p class="mt-1 text-sm text-ink-secondary">
|
|
||||||
Generate printable separators that reconnect scanned paper to batch metadata.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Top: 50 / 50 generation + preview -->
|
<!-- Top: 50 / 50 generation + preview -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-8 items-start">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-8 items-start">
|
||||||
<section class="card">
|
<section class="card">
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
<AppHeader title="Data Entry">
|
<AppHeader title="Data Entry">
|
||||||
<template #subtitle>
|
<template #subtitle>
|
||||||
<span v-if="currentBatch" class="text-sm text-ink-secondary">
|
<span v-if="currentBatch" class="text-sm text-ink-secondary">
|
||||||
Batch: {{ currentBatch.id.substring(0, 8) }}...
|
Batch {{ currentBatch.id.substring(0, 8) }}…
|
||||||
| Type: {{ currentBatch.batchType.replace(/_/g, ' ') }}
|
· {{ currentBatch.batchType.replace(/_/g, ' ') }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</AppHeader>
|
</AppHeader>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 flex flex-col">
|
<div class="h-full min-h-0 flex flex-col">
|
||||||
<AppHeader title="FHIR Explorer">
|
<AppHeader
|
||||||
|
title="FHIR Explorer"
|
||||||
|
description="Read-only inspection of exposed FHIR resources for administrators and integration staff."
|
||||||
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<button type="button" class="btn-secondary text-sm" @click="openMetadata">
|
<button type="button" class="btn-secondary text-sm" @click="openMetadata">
|
||||||
CapabilityStatement
|
CapabilityStatement
|
||||||
@@ -10,15 +13,6 @@
|
|||||||
|
|
||||||
<div class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
<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">
|
<div class="max-w-7xl mx-auto space-y-6">
|
||||||
<div>
|
|
||||||
<h1 class="text-[1.75rem] font-semibold text-ink-strong leading-tight tracking-tight">
|
|
||||||
FHIR R4 Explorer
|
|
||||||
</h1>
|
|
||||||
<p class="mt-1 text-sm text-ink-secondary">
|
|
||||||
Read-only inspection of exposed FHIR resources for administrators and integration staff.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Resource browser -->
|
<!-- Resource browser -->
|
||||||
<section class="card space-y-5">
|
<section class="card space-y-5">
|
||||||
<header>
|
<header>
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 flex flex-col">
|
<div class="h-full min-h-0 flex flex-col">
|
||||||
<AppHeader title="Intake" />
|
<AppHeader
|
||||||
|
title="Intake"
|
||||||
|
description="Create a digitization batch from a PDF or image scan."
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
<div class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
||||||
<div class="max-w-7xl mx-auto">
|
<div class="max-w-7xl mx-auto">
|
||||||
<div class="mb-6">
|
|
||||||
<h1 class="text-[1.75rem] font-semibold text-ink-strong leading-tight tracking-tight">
|
|
||||||
Upload Scanned Document
|
|
||||||
</h1>
|
|
||||||
<p class="mt-1 text-sm text-ink-secondary">
|
|
||||||
Create a digitization batch from a PDF or image scan.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 xl:grid-cols-[minmax(0,0.7fr)_minmax(0,0.3fr)] gap-6 lg:gap-8 items-start">
|
<div class="grid grid-cols-1 xl:grid-cols-[minmax(0,0.7fr)_minmax(0,0.3fr)] gap-6 lg:gap-8 items-start">
|
||||||
<!-- Main — ~70%: upload + batch details -->
|
<!-- Main — ~70%: upload + batch details -->
|
||||||
<div class="space-y-6 min-w-0">
|
<div class="space-y-6 min-w-0">
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 flex flex-col">
|
<div class="h-full min-h-0 flex flex-col">
|
||||||
<AppHeader title="Live Capture" />
|
<AppHeader
|
||||||
|
title="Live Capture"
|
||||||
|
description="Bedside observation recording — lighter than backfill digitization."
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
<div class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
||||||
<div class="max-w-5xl mx-auto space-y-6">
|
<div class="max-w-5xl mx-auto space-y-6">
|
||||||
<div>
|
|
||||||
<h1 class="text-[1.75rem] font-semibold text-ink-strong leading-tight tracking-tight">
|
|
||||||
Live Capture
|
|
||||||
</h1>
|
|
||||||
<p class="mt-1 text-sm text-ink-secondary">
|
|
||||||
Bedside observation recording — lighter than backfill digitization.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mode selector -->
|
<!-- Mode selector -->
|
||||||
<div class="flex border-b border-line" role="tablist">
|
<div class="flex border-b border-line" role="tablist">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="min-h-screen flex flex-col lg:flex-row bg-canvas">
|
<div class="min-h-screen flex flex-col lg:flex-row bg-canvas">
|
||||||
<!-- Brand panel -->
|
<!-- Brand panel -->
|
||||||
<aside
|
<aside
|
||||||
class="relative flex flex-col justify-between bg-navy text-white px-8 py-10 lg:w-[45%] lg:min-h-screen lg:px-12 lg:py-14"
|
class="relative flex flex-col justify-between bg-navy text-white px-6 py-8 sm:px-8 sm:py-10 lg:w-[45%] lg:min-h-screen lg:px-12 lg:py-14"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none absolute inset-0 opacity-[0.07]"
|
class="pointer-events-none absolute inset-0 opacity-[0.07]"
|
||||||
@@ -22,11 +22,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-10 lg:mt-20 max-w-md">
|
<div class="mt-6 sm:mt-10 lg:mt-20 max-w-md">
|
||||||
<h1 class="text-[2rem] sm:text-[2.5rem] lg:text-[2.625rem] font-bold leading-tight tracking-tight">
|
<h1 class="text-[1.75rem] sm:text-[2.5rem] lg:text-[2.625rem] font-bold leading-tight tracking-tight">
|
||||||
Digitize. Verify. Trust.
|
Digitize. Verify. Trust.
|
||||||
</h1>
|
</h1>
|
||||||
<p class="mt-4 text-base text-white/70 leading-relaxed">
|
<p class="mt-3 sm:mt-4 text-sm sm:text-base text-white/70 leading-relaxed">
|
||||||
Scanned clinical records become governed, structured data your teams can verify and trust.
|
Scanned clinical records become governed, structured data your teams can verify and trust.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,9 +44,9 @@
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Auth panel -->
|
<!-- Auth panel -->
|
||||||
<div class="flex flex-1 items-center justify-center px-4 py-10 sm:px-8">
|
<div class="flex flex-1 items-center justify-center px-4 py-8 sm:px-8 sm:py-10">
|
||||||
<div class="w-full max-w-[440px] bg-surface border border-line rounded-card shadow-card p-6 sm:p-8">
|
<div class="w-full max-w-[440px] bg-surface border border-line rounded-card shadow-card p-5 sm:p-8">
|
||||||
<div class="mb-8">
|
<div class="mb-6 sm:mb-8">
|
||||||
<h2 class="text-[1.75rem] font-semibold text-ink-strong leading-tight">Sign in</h2>
|
<h2 class="text-[1.75rem] font-semibold text-ink-strong leading-tight">Sign in</h2>
|
||||||
<p class="mt-2 text-sm text-ink-secondary">
|
<p class="mt-2 text-sm text-ink-secondary">
|
||||||
Use your VigilCare workstation credentials.
|
Use your VigilCare workstation credentials.
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 flex flex-col">
|
<div class="h-full min-h-0 flex flex-col">
|
||||||
<AppHeader title="Patient History" />
|
<AppHeader
|
||||||
|
title="Patient History"
|
||||||
|
description="Digitization lineage, promotion attribution, and audit trail for a patient."
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
<div class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
||||||
<div class="max-w-5xl mx-auto space-y-6">
|
<div class="max-w-5xl mx-auto space-y-6">
|
||||||
<div>
|
|
||||||
<h1 class="text-[1.75rem] font-semibold text-ink-strong leading-tight tracking-tight">
|
|
||||||
Patient History
|
|
||||||
</h1>
|
|
||||||
<p class="mt-1 text-sm text-ink-secondary">
|
|
||||||
Digitization lineage, promotion attribution, and audit trail for a patient.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Search when no patient selected -->
|
<!-- Search when no patient selected -->
|
||||||
<section v-if="!patientId" class="card space-y-4">
|
<section v-if="!patientId" class="card space-y-4">
|
||||||
<h2 class="text-base font-semibold text-ink-strong">Find Patient</h2>
|
<h2 class="text-base font-semibold text-ink-strong">Find Patient</h2>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 flex flex-col">
|
<div class="h-full min-h-0 flex flex-col">
|
||||||
<AppHeader title="Queue Dashboard">
|
<AppHeader
|
||||||
|
title="Queue Dashboard"
|
||||||
|
description="Supervisor view of digitization backlog, aging work, and reject rate."
|
||||||
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -15,15 +18,6 @@
|
|||||||
|
|
||||||
<div class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
<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 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>
|
|
||||||
|
|
||||||
<InlineError
|
<InlineError
|
||||||
v-if="overviewError"
|
v-if="overviewError"
|
||||||
title="Could not load queue overview"
|
title="Could not load queue overview"
|
||||||
@@ -34,7 +28,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Priority work metrics -->
|
<!-- Priority work metrics -->
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-5 gap-3 sm:gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3 sm:gap-4">
|
||||||
<div class="card-elevated">
|
<div class="card-elevated">
|
||||||
<p class="text-xs font-semibold uppercase tracking-wide text-ink-secondary">
|
<p class="text-xs font-semibold uppercase tracking-wide text-ink-secondary">
|
||||||
Pending Entry
|
Pending Entry
|
||||||
@@ -85,18 +79,16 @@
|
|||||||
{{ ((overview?.rejectRate ?? 0) * 100).toFixed(1) }}%
|
{{ ((overview?.rejectRate ?? 0) * 100).toFixed(1) }}%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="card-elevated">
|
||||||
|
|
||||||
<!-- Secondary age metric -->
|
|
||||||
<div class="card-elevated max-w-md">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-wide text-ink-secondary">
|
<p class="text-xs font-semibold uppercase tracking-wide text-ink-secondary">
|
||||||
Average Time in Queue
|
Average Time in Queue
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 text-2xl font-bold tabular-nums text-ink-strong">
|
<p class="mt-2 text-3xl font-bold tabular-nums text-ink-strong">
|
||||||
{{ formatMinutes(overview?.averageTimeInQueueMinutes ?? 0) }}
|
{{ formatMinutes(overview?.averageTimeInQueueMinutes ?? 0) }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-ink-secondary mt-2">Target: under 24 hours</p>
|
<p class="text-xs text-ink-secondary mt-2">Target: under 24 hours</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Status breakdown from overview.statusCounts -->
|
<!-- Status breakdown from overview.statusCounts -->
|
||||||
<section class="card">
|
<section class="card">
|
||||||
@@ -113,18 +105,18 @@
|
|||||||
<div
|
<div
|
||||||
v-for="{ status, count } in statusEntries"
|
v-for="{ status, count } in statusEntries"
|
||||||
:key="status"
|
:key="status"
|
||||||
class="flex items-center gap-3 sm:gap-4"
|
class="flex items-center gap-2 sm:gap-4 min-w-0"
|
||||||
>
|
>
|
||||||
<div class="w-44 shrink-0">
|
<div class="w-[7.5rem] sm:w-44 shrink-0 min-w-0">
|
||||||
<StatusBadge :status="status" />
|
<StatusBadge :status="status" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 h-2.5 rounded-full bg-canvas border border-line overflow-hidden">
|
<div class="flex-1 min-w-0 h-2.5 rounded-full bg-canvas border border-line overflow-hidden">
|
||||||
<div
|
<div
|
||||||
class="h-full rounded-full bg-primary-500"
|
class="h-full rounded-full bg-primary-500"
|
||||||
:style="{ width: `${(count / maxCount) * 100}%` }"
|
:style="{ width: `${(count / maxCount) * 100}%` }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm font-semibold tabular-nums text-ink-strong w-8 text-right">
|
<span class="text-sm font-semibold tabular-nums text-ink-strong w-6 sm:w-8 text-right shrink-0">
|
||||||
{{ count }}
|
{{ count }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0 flex flex-col" data-testid="users-view">
|
<div class="h-full min-h-0 flex flex-col" data-testid="users-view">
|
||||||
<AppHeader title="Users" />
|
<AppHeader
|
||||||
|
title="Users"
|
||||||
<div class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
description="Create accounts, update roles, and reset passwords for active staff."
|
||||||
<div class="max-w-7xl mx-auto space-y-6">
|
>
|
||||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
<template #actions>
|
||||||
<div>
|
|
||||||
<h1 class="text-[1.75rem] font-semibold text-ink-strong leading-tight tracking-tight">
|
|
||||||
Users
|
|
||||||
</h1>
|
|
||||||
<p class="mt-1 text-sm text-ink-secondary">
|
|
||||||
Create accounts, update roles, and reset passwords for active staff.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-primary"
|
class="btn-primary"
|
||||||
@@ -21,7 +13,11 @@
|
|||||||
>
|
>
|
||||||
{{ showCreate ? 'Cancel' : 'Create user' }}
|
{{ showCreate ? 'Cancel' : 'Create user' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</template>
|
||||||
|
</AppHeader>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
|
||||||
<section v-if="showCreate" class="card space-y-4" data-testid="users-create-form">
|
<section v-if="showCreate" class="card space-y-4" data-testid="users-create-form">
|
||||||
<header>
|
<header>
|
||||||
@@ -135,7 +131,7 @@
|
|||||||
description="Create a user or clear the role filter."
|
description="Create a user or clear the role filter."
|
||||||
/>
|
/>
|
||||||
<div v-else class="overflow-x-auto">
|
<div v-else class="overflow-x-auto">
|
||||||
<table class="w-full min-w-[640px] text-sm">
|
<table class="w-full min-w-[36rem] text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-line text-left text-ink-secondary">
|
<tr class="border-b border-line text-left text-ink-secondary">
|
||||||
<th class="py-2 px-3 font-medium">Full name</th>
|
<th class="py-2 px-3 font-medium">Full name</th>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
<AppHeader title="Verification">
|
<AppHeader title="Verification">
|
||||||
<template #subtitle>
|
<template #subtitle>
|
||||||
<span v-if="currentBatch" class="text-sm text-ink-secondary">
|
<span v-if="currentBatch" class="text-sm text-ink-secondary">
|
||||||
Batch: {{ currentBatch.id.substring(0, 8) }}...
|
Batch {{ currentBatch.id.substring(0, 8) }}…
|
||||||
| Entered by: {{ currentBatch.enteredByUserId?.substring(0, 8) }}...
|
<span v-if="enteredByLabel"> · Entered by {{ enteredByLabel }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</AppHeader>
|
</AppHeader>
|
||||||
@@ -79,6 +79,15 @@ const batchId = computed(() => props.batchId ?? (route.params.batchId as string
|
|||||||
const currentBatch = computed(() => batchStore.currentBatch)
|
const currentBatch = computed(() => batchStore.currentBatch)
|
||||||
const { documentUrl, documentError, documentLoading, refreshUrl } = usePresignedUrl(batchId)
|
const { documentUrl, documentError, documentLoading, refreshUrl } = usePresignedUrl(batchId)
|
||||||
|
|
||||||
|
const enteredByLabel = computed(() => {
|
||||||
|
const batch = currentBatch.value
|
||||||
|
if (!batch) return null
|
||||||
|
if (batch.enteredByUserName?.trim()) return batch.enteredByUserName.trim()
|
||||||
|
const fromQueue = batchStore.batches.find((b) => b.id === batch.id)
|
||||||
|
if (fromQueue?.enteredByUserName?.trim()) return fromQueue.enteredByUserName.trim()
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
function openBatch(id: string) {
|
function openBatch(id: string) {
|
||||||
router.push(`/verification/${id}`)
|
router.push(`/verification/${id}`)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user