瀏覽代碼

新增时区选择器组件及相关逻辑,允许用户选择时区并在扫描记录中格式化时间。更新多个视图以集成时区选择器,提升用户体验。

wuyi 2 周之前
父節點
當前提交
71db2988b0
共有 4 個文件被更改,包括 355 次插入58 次删除
  1. 84 0
      src/components/TimeZoneSelector.vue
  2. 78 0
      src/composables/useTimeZone.js
  3. 117 29
      src/views/JumpView.vue
  4. 76 29
      src/views/ScanView.vue

+ 84 - 0
src/components/TimeZoneSelector.vue

@@ -0,0 +1,84 @@
+<script setup>
+import { onMounted, onUnmounted } from 'vue'
+import { useTimeZone, timeZones } from '@/composables/useTimeZone'
+
+const {
+  selectedTimeZone,
+  showTimeZoneSelector,
+  changeTimeZone,
+  currentTimeZoneLabel
+} = useTimeZone()
+
+// 点击外部关闭时区选择器
+const handleClickOutside = (event) => {
+  if (!event.target.closest('.timezone-selector-container')) {
+    showTimeZoneSelector.value = false
+  }
+}
+
+onMounted(() => {
+  document.addEventListener('click', handleClickOutside)
+})
+
+onUnmounted(() => {
+  document.removeEventListener('click', handleClickOutside)
+})
+</script>
+
+<template>
+  <div class="timezone-selector-container relative">
+    <button
+      class="flex items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50 hover:border-slate-300 transition-colors"
+      @click.stop="showTimeZoneSelector = !showTimeZoneSelector"
+      :title="currentTimeZoneLabel"
+    >
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        fill="none"
+        viewBox="0 0 24 24"
+        stroke-width="1.5"
+        stroke="currentColor"
+        class="h-3.5 w-3.5"
+      >
+        <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
+      </svg>
+      <span>{{ timeZones.find(tz => tz.value === selectedTimeZone)?.short || 'ET' }}</span>
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        fill="none"
+        viewBox="0 0 24 24"
+        stroke-width="1.5"
+        stroke="currentColor"
+        class="h-3 w-3"
+      >
+        <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
+      </svg>
+    </button>
+    
+    <!-- 时区下拉菜单 -->
+    <div
+      v-if="showTimeZoneSelector"
+      class="absolute right-0 top-full mt-2 w-64 rounded-xl border border-slate-200 bg-white shadow-lg z-50"
+      @click.stop
+    >
+      <div class="p-2">
+        <div class="mb-2 px-3 py-2 text-xs font-semibold text-slate-500 uppercase">Select Time Zone</div>
+        <div class="space-y-1">
+          <button
+            v-for="tz in timeZones"
+            :key="tz.value"
+            class="w-full rounded-lg px-3 py-2 text-left text-sm font-medium transition-colors"
+            :class="selectedTimeZone === tz.value 
+              ? 'bg-slate-900 text-white' 
+              : 'text-slate-700 hover:bg-slate-50'"
+            @click="changeTimeZone(tz.value)"
+          >
+            <div class="font-semibold">{{ tz.label }}</div>
+            <div class="text-xs opacity-80">{{ tz.abbr }}</div>
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+

+ 78 - 0
src/composables/useTimeZone.js

@@ -0,0 +1,78 @@
+import { computed, ref } from 'vue'
+
+// 时区选项
+export const timeZones = [
+  { value: 'America/New_York', label: 'Eastern Time (ET)', abbr: 'EST/EDT', short: 'ET' },
+  { value: 'America/Chicago', label: 'Central Time (CT)', abbr: 'CST/CDT', short: 'CT' },
+  { value: 'America/Denver', label: 'Mountain Time (MT)', abbr: 'MST/MDT', short: 'MT' },
+  { value: 'America/Los_Angeles', label: 'Pacific Time (PT)', abbr: 'PST/PDT', short: 'PT' }
+]
+
+/**
+ * 时区管理的 composable
+ * @returns {Object} 时区相关的状态和方法
+ */
+export function useTimeZone() {
+  // 从localStorage读取保存的时区,默认为东部时间
+  const selectedTimeZone = ref(localStorage.getItem('selectedTimeZone') || 'America/New_York')
+
+  // 时区选择器显示状态
+  const showTimeZoneSelector = ref(false)
+
+  // 切换时区
+  const changeTimeZone = (timeZone) => {
+    selectedTimeZone.value = timeZone
+    localStorage.setItem('selectedTimeZone', timeZone)
+    showTimeZoneSelector.value = false
+  }
+
+  // 获取当前选中的时区标签
+  const currentTimeZoneLabel = computed(() => {
+    const tz = timeZones.find(tz => tz.value === selectedTimeZone.value)
+    return tz ? `${tz.label} (${tz.abbr})` : 'Eastern Time (ET)'
+  })
+
+  // 格式化日期时间为选中的时区
+  const formatDateTime = (dateString) => {
+    if (!dateString) return '-'
+    try {
+      let date
+      
+      // API返回的格式是 2025-12-24T09:23:25.063Z,标记为UTC但实际是北京时间
+      // 将Z替换为+08:00表示这是北京时间
+      if (dateString.endsWith('Z')) {
+        date = new Date(dateString.replace('Z', '+08:00'))
+      } else if (/[+-]\d{2}:?\d{2}$/.test(dateString)) {
+        // 已经有明确的时区信息,直接解析
+        date = new Date(dateString)
+      } else {
+        // 没有时区信息,假设是北京时间(UTC+8),添加 +08:00
+        date = new Date(dateString + '+08:00')
+      }
+      
+      // 转换为选中的美国时区
+      return date.toLocaleString('en-US', {
+        timeZone: selectedTimeZone.value,
+        year: 'numeric',
+        month: '2-digit',
+        day: '2-digit',
+        hour: '2-digit',
+        minute: '2-digit',
+        second: '2-digit',
+        hour12: true
+      })
+    } catch {
+      return dateString
+    }
+  }
+
+  return {
+    timeZones,
+    selectedTimeZone,
+    showTimeZoneSelector,
+    changeTimeZone,
+    currentTimeZoneLabel,
+    formatDateTime
+  }
+}
+

+ 117 - 29
src/views/JumpView.vue

@@ -3,6 +3,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { useToast } from 'primevue/usetoast'
 import Dialog from 'primevue/dialog'
+import TimeZoneSelector from '@/components/TimeZoneSelector.vue'
 import {
     fetchQrInfoApi,
     updateLinkInfoApi,
@@ -10,6 +11,7 @@ import {
     fetchRecentScanRecordsApi
 } from '@/services/api'
 import { saveMaintenanceCode, getMaintenanceCode } from '@/utils/maintenanceCodeCache'
+import { useTimeZone } from '@/composables/useTimeZone'
 
 const route = useRoute()
 const router = useRouter()
@@ -43,6 +45,7 @@ const scanRecordsMaintenanceCode = ref('')
 const verifiedMaintenanceCode = ref('')
 const scanRecords = ref(null)
 const loadingScanRecords = ref(false)
+const hasCachedScanRecordsMaintenanceCode = ref(false)
 
 const formData = reactive({
     jumpUrl: '',
@@ -280,25 +283,41 @@ const fetchScanRecords = async (maintenanceCode) => {
 }
 
 const handleVerifyScanRecordsMaintenance = async () => {
-    if (!qrCode.value || !scanRecordsMaintenanceCode.value) {
+    if (!qrCode.value) {
+        toast.add({ severity: 'warn', summary: 'Notice', detail: 'QR code is required.', life: 2400 })
+        return
+    }
+    // 如果有缓存的维护码,直接使用;否则使用用户输入的
+    let codeToUse = scanRecordsMaintenanceCode.value.trim()
+    if (!codeToUse && hasCachedScanRecordsMaintenanceCode.value) {
+        const cachedCode = getMaintenanceCode(qrCode.value)
+        if (cachedCode) {
+            codeToUse = cachedCode
+        }
+    }
+    if (!codeToUse) {
         toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter the maintenance code.', life: 2400 })
         return
     }
-    await fetchScanRecords(scanRecordsMaintenanceCode.value)
+    await fetchScanRecords(codeToUse)
 }
 
-const formatDateTime = (dateString) => {
-    if (!dateString) return '-'
-    const date = new Date(dateString)
-    return date.toLocaleString('en-US', {
-        year: 'numeric',
-        month: '2-digit',
-        day: '2-digit',
-        hour: '2-digit',
-        minute: '2-digit',
-        second: '2-digit'
-    })
-}
+// 使用时区 composable
+const { selectedTimeZone, formatDateTime } = useTimeZone()
+
+// 格式化后的扫描记录(响应式,当时区变化时自动更新)
+const formattedScanRecords = computed(() => {
+    if (!scanRecords.value || !scanRecords.value.records) {
+        return null
+    }
+    return {
+        ...scanRecords.value,
+        records: scanRecords.value.records.map(record => ({
+            ...record,
+            formattedScanTime: formatDateTime(record.scanTime)
+        }))
+    }
+})
 
 const openRecordLocation = (record) => {
     if (record.latitude && record.longitude) {
@@ -397,10 +416,26 @@ watch(qrCode, (newQrCode) => {
     if (newQrCode) {
         const cachedCode = getMaintenanceCode(newQrCode)
         hasCachedMaintenanceCode.value = !!cachedCode
+        hasCachedScanRecordsMaintenanceCode.value = !!cachedCode
         if (cachedCode) {
             maintenanceCode.value = cachedCode
+            scanRecordsMaintenanceCode.value = cachedCode
         } else {
             maintenanceCode.value = ''
+            scanRecordsMaintenanceCode.value = ''
+        }
+    }
+})
+
+// 当打开扫描记录维护码对话框时,自动填充缓存的维护码
+watch(showScanRecordsMaintenanceDialog, (isOpen) => {
+    if (isOpen && qrCode.value) {
+        const cachedCode = getMaintenanceCode(qrCode.value)
+        hasCachedScanRecordsMaintenanceCode.value = !!cachedCode
+        if (cachedCode) {
+            scanRecordsMaintenanceCode.value = cachedCode
+        } else {
+            scanRecordsMaintenanceCode.value = ''
         }
     }
 })
@@ -708,12 +743,20 @@ onMounted(() => {
             :style="{ width: '90vw', maxWidth: '400px' }" class="p-fluid">
             <div class="space-y-4">
                 <p class="text-sm text-slate-600">Enter the maintenance code to view scan records.</p>
-                <div class="space-y-2">
+                <div v-if="!hasCachedScanRecordsMaintenanceCode" class="space-y-2">
                     <label class="block text-sm font-medium text-slate-700">Maintenance code</label>
                     <input v-model="scanRecordsMaintenanceCode" type="text" maxlength="8"
                         class="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-900 focus:border-cyan-400 focus:outline-none focus:ring-2 focus:ring-cyan-400/30"
                         placeholder="Enter the maintenance code" @keyup.enter="handleVerifyScanRecordsMaintenance" />
                 </div>
+                <div v-else class="rounded-xl border border-emerald-200 bg-emerald-50 p-3">
+                    <div class="flex items-start gap-2">
+                        <i class="pi pi-check-circle text-sm text-emerald-600" />
+                        <p class="text-xs text-emerald-700">
+                            Maintenance code has been entered. You can click the button to proceed.
+                        </p>
+                    </div>
+                </div>
                 <div class="flex justify-end gap-3 pt-2">
                     <button type="button"
                         class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50"
@@ -722,7 +765,7 @@ onMounted(() => {
                     </button>
                     <button type="button"
                         class="rounded-xl bg-cyan-500 px-4 py-2 text-sm font-semibold text-white transition hover:bg-cyan-600 disabled:opacity-50"
-                        :disabled="loadingScanRecords || !scanRecordsMaintenanceCode"
+                        :disabled="loadingScanRecords || (!hasCachedScanRecordsMaintenanceCode && !scanRecordsMaintenanceCode)"
                         @click="handleVerifyScanRecordsMaintenance">
                         <i v-if="loadingScanRecords" class="pi pi-spin pi-spinner mr-2" />
                         <i v-else class="pi pi-check mr-2" />
@@ -733,23 +776,43 @@ onMounted(() => {
         </Dialog>
 
         <!-- Scan records dialog -->
-        <Dialog v-model:visible="showScanRecordsDialog" modal header="Scan Records"
-            :style="{ width: '90vw', maxWidth: '600px' }" class="p-fluid">
-            <div v-if="loadingScanRecords" class="py-8 text-center text-slate-600">
-                <i class="pi pi-spin pi-spinner text-2xl" />
-                <p class="mt-2">Loading...</p>
-            </div>
-            <div v-else-if="scanRecords">
-                <div v-if="scanRecords.records && scanRecords.records.length > 0"
+        <Dialog v-model:visible="showScanRecordsDialog" modal :closable="true" :closeOnEscape="true"
+            :dismissableMask="true" :style="{ width: '90vw', maxWidth: '800px' }" :draggable="false">
+            <template #header>
+                <div class="flex items-center justify-between gap-3">
+                    <div class="flex items-center gap-3">
+                        <div class="flex h-10 w-10 items-center justify-center rounded-full bg-cyan-100">
+                            <i class="pi pi-history text-lg text-cyan-600" />
+                        </div>
+                        <div>
+                            <h3 class="text-lg font-semibold text-slate-900">Recent Scan Records</h3>
+                            <p class="text-sm text-slate-500">
+                                QR Code: <span class="font-mono">{{ qrCode }}</span>
+                            </p>
+                        </div>
+                    </div>
+                    <!-- 时区选择器 -->
+                    <TimeZoneSelector />
+                </div>
+            </template>
+
+            <div class="space-y-4 py-4">
+                <div v-if="loadingScanRecords" class="flex items-center justify-center py-8">
+                    <i class="pi pi-spin pi-spinner text-3xl text-slate-400" />
+                    <span class="ml-3 text-slate-600">Loading...</span>
+                </div>
+
+                <div v-else-if="formattedScanRecords">
+                <div v-if="formattedScanRecords.records && formattedScanRecords.records.length > 0"
                     class="space-y-3 max-h-[60vh] overflow-y-auto">
-                    <div v-for="record in scanRecords.records" :key="record.id"
+                    <div v-for="record in formattedScanRecords.records" :key="`${record.id}-${selectedTimeZone}`"
                         class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm hover:shadow-md transition-shadow">
                         <div class="flex items-start justify-between gap-4">
                             <div class="flex-1 space-y-2">
                                 <div class="flex items-center gap-2">
                                     <i class="pi pi-clock text-sm text-slate-500" />
                                     <span class="text-sm font-medium text-slate-700">Scan Time</span>
-                                    <span class="text-sm text-slate-600">{{ formatDateTime(record.scanTime) }}</span>
+                                    <span class="text-sm text-slate-600">{{ record.formattedScanTime }}</span>
                                 </div>
 
                                 <div v-if="record.address" class="flex items-start gap-2">
@@ -781,11 +844,35 @@ onMounted(() => {
                         </div>
                     </div>
                 </div>
-                <div v-else class="py-8 text-center text-slate-500">
-                    <i class="pi pi-inbox text-3xl mb-2" />
-                    <p>No scan records</p>
+                <div v-else class="rounded-xl border border-slate-200 bg-slate-50 p-8 text-center">
+                    <i class="pi pi-inbox text-4xl text-slate-400 mb-3" />
+                    <p class="text-sm text-slate-600">No scan records</p>
+                </div>
+                </div>
+
+                <div v-else class="rounded-xl border border-red-200 bg-red-50 p-4">
+                    <div class="flex items-center gap-2">
+                        <i class="pi pi-exclamation-triangle text-red-600" />
+                        <span class="text-sm text-red-700">Failed to load, please try again later</span>
+                    </div>
                 </div>
             </div>
+
+            <template #footer>
+                <div class="flex justify-end gap-3">
+                    <button type="button"
+                        class="rounded-xl border border-slate-300 px-5 py-2.5 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
+                        @click="showScanRecordsDialog = false">
+                        Close
+                    </button>
+                    <button type="button"
+                        class="rounded-xl bg-cyan-600 px-6 py-2.5 text-sm font-semibold text-white transition hover:bg-cyan-700 disabled:opacity-50"
+                        :disabled="loadingScanRecords || !verifiedMaintenanceCode" @click="fetchScanRecords(verifiedMaintenanceCode)">
+                        <i class="pi pi-refresh mr-2" :class="{ 'pi-spin': loadingScanRecords }" />
+                        Refresh
+                    </button>
+                </div>
+            </template>
         </Dialog>
     </div>
 </template>
@@ -798,3 +885,4 @@ onMounted(() => {
         linear-gradient(135deg, #020617, #030712);
 }
 </style>
+

+ 76 - 29
src/views/ScanView.vue

@@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
 import { useToast } from 'primevue/usetoast'
 import Dialog from 'primevue/dialog'
 import LocationPicker from '@/components/LocationPicker.vue'
+import TimeZoneSelector from '@/components/TimeZoneSelector.vue'
 import {
   fetchQrInfoApi,
   updatePersonProfileApi,
@@ -17,6 +18,7 @@ import {
 } from '@/services/api'
 import { saveMaintenanceCode, getMaintenanceCode } from '@/utils/maintenanceCodeCache'
 import { useUserStore } from '@/stores/user'
+import { useTimeZone } from '@/composables/useTimeZone'
 
 const route = useRoute()
 const router = useRouter()
@@ -57,6 +59,7 @@ const scanRecordsMaintenanceCode = ref('')
 const verifiedMaintenanceCode = ref('')
 const scanRecords = ref(null)
 const loadingScanRecords = ref(false)
+const hasCachedScanRecordsMaintenanceCode = ref(false)
 const showMaintenanceCodeHintDialog = ref(false)
 
 const FORM_KEY_MAP = Object.freeze({
@@ -641,12 +644,28 @@ watch(qrCode, (newQrCode) => {
   if (newQrCode) {
     const cachedCode = getMaintenanceCode(newQrCode)
     hasCachedMaintenanceCode.value = !!cachedCode
+    hasCachedScanRecordsMaintenanceCode.value = !!cachedCode
     if (cachedCode) {
       maintenanceCode.value = cachedCode
       viewMaintenanceCode.value = cachedCode
+      scanRecordsMaintenanceCode.value = cachedCode
     } else {
       maintenanceCode.value = ''
       viewMaintenanceCode.value = ''
+      scanRecordsMaintenanceCode.value = ''
+    }
+  }
+})
+
+// 当打开扫描记录维护码对话框时,自动填充缓存的维护码
+watch(showScanRecordsMaintenanceDialog, (isOpen) => {
+  if (isOpen && qrCode.value) {
+    const cachedCode = getMaintenanceCode(qrCode.value)
+    hasCachedScanRecordsMaintenanceCode.value = !!cachedCode
+    if (cachedCode) {
+      scanRecordsMaintenanceCode.value = cachedCode
+    } else {
+      scanRecordsMaintenanceCode.value = ''
     }
   }
 })
@@ -708,25 +727,41 @@ const fetchScanRecords = async (maintenanceCode) => {
 }
 
 const handleVerifyScanRecordsMaintenance = async () => {
-  if (!qrCode.value || !scanRecordsMaintenanceCode.value) {
+  if (!qrCode.value) {
+    toast.add({ severity: 'warn', summary: 'Notice', detail: 'QR code is required.', life: 2400 })
+    return
+  }
+  // 如果有缓存的维护码,直接使用;否则使用用户输入的
+  let codeToUse = scanRecordsMaintenanceCode.value.trim()
+  if (!codeToUse && hasCachedScanRecordsMaintenanceCode.value) {
+    const cachedCode = getMaintenanceCode(qrCode.value)
+    if (cachedCode) {
+      codeToUse = cachedCode
+    }
+  }
+  if (!codeToUse) {
     toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter the maintenance code.', life: 2400 })
     return
   }
-  await fetchScanRecords(scanRecordsMaintenanceCode.value)
+  await fetchScanRecords(codeToUse)
 }
 
-const formatDateTime = (dateString) => {
-  if (!dateString) return '-'
-  const date = new Date(dateString)
-  return date.toLocaleString('en-US', {
-    year: 'numeric',
-    month: '2-digit',
-    day: '2-digit',
-    hour: '2-digit',
-    minute: '2-digit',
-    second: '2-digit'
-  })
-}
+// 使用时区 composable
+const { selectedTimeZone, formatDateTime } = useTimeZone()
+
+// 格式化后的扫描记录(响应式,当时区变化时自动更新)
+const formattedScanRecords = computed(() => {
+  if (!scanRecords.value || !scanRecords.value.records) {
+    return null
+  }
+  return {
+    ...scanRecords.value,
+    records: scanRecords.value.records.map(record => ({
+      ...record,
+      formattedScanTime: formatDateTime(record.scanTime)
+    }))
+  }
+})
 
 const formatRemark = (remark) => {
   if (!remark) return 'No extra details'
@@ -1466,16 +1501,20 @@ onMounted(() => {
     <Dialog v-model:visible="showScanRecordsDialog" modal :closable="true" :closeOnEscape="true"
       :dismissableMask="true" :style="{ width: '90vw', maxWidth: '800px' }" :draggable="false">
       <template #header>
-        <div class="flex items-center gap-3">
-          <div class="flex h-10 w-10 items-center justify-center rounded-full bg-cyan-100">
-            <i class="pi pi-history text-lg text-cyan-600" />
-          </div>
-          <div>
-            <h3 class="text-lg font-semibold text-slate-900">Recent Scan Records</h3>
-            <p class="text-sm text-slate-500">
-              QR Code: <span class="font-mono">{{ qrCode }}</span>
-            </p>
+        <div class="flex items-center justify-between gap-3">
+          <div class="flex items-center gap-3">
+            <div class="flex h-10 w-10 items-center justify-center rounded-full bg-cyan-100">
+              <i class="pi pi-history text-lg text-cyan-600" />
+            </div>
+            <div>
+              <h3 class="text-lg font-semibold text-slate-900">Recent Scan Records</h3>
+              <p class="text-sm text-slate-500">
+                QR Code: <span class="font-mono">{{ qrCode }}</span>
+              </p>
+            </div>
           </div>
+          <!-- 时区选择器 -->
+          <TimeZoneSelector />
         </div>
       </template>
 
@@ -1485,17 +1524,17 @@ onMounted(() => {
           <span class="ml-3 text-slate-600">Loading...</span>
         </div>
 
-        <div v-else-if="scanRecords">
+        <div v-else-if="formattedScanRecords">
 
-          <div v-if="scanRecords.records && scanRecords.records.length > 0" class="space-y-3 max-h-[60vh] overflow-y-auto">
-            <div v-for="record in scanRecords.records" :key="record.id"
+          <div v-if="formattedScanRecords.records && formattedScanRecords.records.length > 0" class="space-y-3 max-h-[60vh] overflow-y-auto">
+            <div v-for="record in formattedScanRecords.records" :key="`${record.id}-${selectedTimeZone}`"
               class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm hover:shadow-md transition-shadow">
               <div class="flex items-start justify-between gap-4">
                 <div class="flex-1 space-y-2">
                   <div class="flex items-center gap-2">
                     <i class="pi pi-clock text-sm text-slate-500" />
                     <span class="text-sm font-medium text-slate-700">Scan Time</span>
-                    <span class="text-sm text-slate-600">{{ formatDateTime(record.scanTime) }}</span>
+                    <span class="text-sm text-slate-600">{{ record.formattedScanTime }}</span>
                   </div>
 
                   <div v-if="record.address" class="flex items-start gap-2">
@@ -1585,12 +1624,20 @@ onMounted(() => {
       </template>
 
       <div class="space-y-4 py-4">
-        <div>
+        <div v-if="!hasCachedScanRecordsMaintenanceCode">
           <label class="mb-2 block text-sm font-medium text-slate-700">Maintenance code</label>
           <input v-model="scanRecordsMaintenanceCode" type="text" maxlength="8"
             class="w-full rounded-xl border border-slate-300 px-4 py-3 text-slate-900 placeholder:text-slate-400 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20"
             placeholder="Enter the maintenance code" @keyup.enter="handleVerifyScanRecordsMaintenance" autofocus />
         </div>
+        <div v-else class="rounded-xl border border-emerald-200 bg-emerald-50 p-3">
+          <div class="flex items-start gap-2">
+            <i class="pi pi-check-circle text-sm text-emerald-600" />
+            <p class="text-xs text-emerald-700">
+              Maintenance code has been entered. You can click the button to proceed.
+            </p>
+          </div>
+        </div>
       </div>
 
       <template #footer>
@@ -1602,7 +1649,7 @@ onMounted(() => {
           </button>
           <button type="button"
             class="rounded-xl bg-cyan-600 px-6 py-2.5 text-sm font-semibold text-white transition hover:bg-cyan-700 disabled:opacity-50"
-            :disabled="loadingScanRecords || !scanRecordsMaintenanceCode" @click="handleVerifyScanRecordsMaintenance">
+            :disabled="loadingScanRecords || (!hasCachedScanRecordsMaintenanceCode && !scanRecordsMaintenanceCode)" @click="handleVerifyScanRecordsMaintenance">
             {{ loadingScanRecords ? 'Verifying...' : 'Verify' }}
           </button>
         </div>