@7365admin1/layer-common 3.2.7 → 3.2.8-staging.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/assets/css/primitives.css +1113 -0
  2. package/assets/css/screens.css +1097 -0
  3. package/assets/css/shell.css +258 -0
  4. package/assets/css/tokens.css +236 -0
  5. package/components/AcceptDialog.vue +17 -18
  6. package/components/AccessCard/AvailableStats.vue +24 -16
  7. package/components/AccessCardAddForm.vue +42 -42
  8. package/components/AccessCardAssignToUnitForm.vue +13 -21
  9. package/components/AccessCardDeleteDialog.vue +41 -39
  10. package/components/AccessCardDetailsDialog.vue +125 -79
  11. package/components/AccessCardPreviewDialog.vue +58 -84
  12. package/components/AccessCardQrTagging.vue +60 -63
  13. package/components/AccessCardReplaceForm.vue +27 -23
  14. package/components/AccessManagement.vue +86 -85
  15. package/components/AddEqupmentForm.vue +18 -22
  16. package/components/AddPassKeyToVisitor.vue +95 -73
  17. package/components/AppButton.vue +62 -0
  18. package/components/AppCard.vue +20 -0
  19. package/components/AppDateField.vue +49 -0
  20. package/components/AppField.vue +76 -0
  21. package/components/AppKpiTile.vue +65 -0
  22. package/components/AppSegmented.vue +38 -0
  23. package/components/AppSelect.vue +247 -0
  24. package/components/AreaChecklistHistoryLogs.vue +51 -44
  25. package/components/AreaChecklistHistoryMain.vue +14 -7
  26. package/components/AreaFormDialog.vue +71 -59
  27. package/components/AreaMain.vue +73 -107
  28. package/components/AttachmentsViewer.vue +84 -22
  29. package/components/AttendanceCheckInOutDialog.vue +85 -88
  30. package/components/AttendanceDetailsDialog.vue +57 -63
  31. package/components/AttendanceMain.vue +4 -3
  32. package/components/AttendanceMapSearchDialog.vue +42 -44
  33. package/components/AttendanceSettingsDialog.vue +79 -63
  34. package/components/Avatar/Main.vue +54 -2
  35. package/components/BillingMain.vue +26 -30
  36. package/components/BtnUploadFile.vue +53 -11
  37. package/components/BuildingForm.vue +34 -27
  38. package/components/BuildingManagement/buildings.vue +39 -39
  39. package/components/BuildingManagement/units.vue +26 -42
  40. package/components/BuildingUnitFormAdd.vue +20 -23
  41. package/components/BuildingUnitFormEdit.vue +8 -4
  42. package/components/BulletinBoardForm.vue +19 -24
  43. package/components/BulletinBoardManagement.vue +49 -57
  44. package/components/BulletinBoardView.vue +9 -6
  45. package/components/BulletinExpirationChip.vue +13 -4
  46. package/components/Button/Close.vue +2 -2
  47. package/components/CameraForm.vue +21 -22
  48. package/components/CameraMain.vue +50 -49
  49. package/components/Card/DeleteConfirmation.vue +24 -22
  50. package/components/Card/MemberInfoSummary.vue +28 -3
  51. package/components/Card/Toggle.vue +15 -3
  52. package/components/CardToolbar.vue +37 -0
  53. package/components/Carousel.vue +56 -36
  54. package/components/Chat/Bubbles.vue +43 -14
  55. package/components/Chat/Information.vue +49 -32
  56. package/components/Chat/ListCard.vue +38 -9
  57. package/components/Chat/Message.vue +41 -15
  58. package/components/Chat/Navigation.vue +27 -15
  59. package/components/Chat/SkeletonLoader.vue +9 -2
  60. package/components/ChecklistItemRow.vue +25 -1
  61. package/components/CleaningScheduleMain.vue +44 -49
  62. package/components/ClientDetailForm.vue +81 -56
  63. package/components/ClientMain.vue +92 -82
  64. package/components/ConfirmDialog.vue +14 -8
  65. package/components/Container/Standard.vue +9 -7
  66. package/components/CreateSubsciptionPlan.vue +148 -89
  67. package/components/DashboardEmptyState.vue +48 -3
  68. package/components/DashboardMain.vue +924 -676
  69. package/components/DashboardPanel.vue +84 -21
  70. package/components/DeliveryCompany.vue +34 -20
  71. package/components/Dialog/DeleteConfirmation.vue +20 -21
  72. package/components/Dialog/ReplaceAutofillPrompt.vue +19 -26
  73. package/components/Dialog/ReusablePrompt.vue +30 -29
  74. package/components/Dialog/UpdateMoreAction.vue +62 -59
  75. package/components/DocumentForm.vue +14 -21
  76. package/components/DocumentManagement.vue +162 -108
  77. package/components/DocumentViewer.vue +29 -12
  78. package/components/DrawImage.vue +29 -21
  79. package/components/EntryPass/QrTemplatePreview.vue +27 -16
  80. package/components/EntryPassInformation.vue +40 -10
  81. package/components/EntryPassMain.vue +49 -27
  82. package/components/EquipmentItemMain.vue +84 -97
  83. package/components/EquipmentManagementMain.vue +81 -103
  84. package/components/Facility/BookingSetup.vue +43 -52
  85. package/components/Feedback/Form.vue +26 -12
  86. package/components/FeedbackMain.vue +35 -49
  87. package/components/FileInput.vue +75 -58
  88. package/components/FileInputWithList.vue +59 -12
  89. package/components/FormDialog.vue +60 -21
  90. package/components/HidAccessLogDashboard.vue +512 -490
  91. package/components/HidCameraCaptureDialog.vue +216 -0
  92. package/components/HidEnabledGate.vue +67 -9
  93. package/components/HidIntercomManagement.vue +1322 -653
  94. package/components/HidQrCodeConfiguration.vue +1030 -258
  95. package/components/HidReaderForm.vue +142 -59
  96. package/components/HidReaderManagement.vue +211 -323
  97. package/components/HidServiceSettingsPanel.vue +44 -13
  98. package/components/HidUserEnrollment.vue +1185 -412
  99. package/components/HygieneUpdateMoreAction.vue +44 -44
  100. package/components/IncidentReport/Authorities.vue +30 -18
  101. package/components/IncidentReport/IncidentInformation.vue +10 -10
  102. package/components/IncidentReport/IncidentInformationDownload.vue +3 -4
  103. package/components/IncidentReport/affectedEntities.vue +41 -20
  104. package/components/Input/Date.vue +3 -0
  105. package/components/Input/DatePicker.vue +1 -2
  106. package/components/Input/DateTimePicker.vue +1 -1
  107. package/components/Input/File.vue +55 -10
  108. package/components/Input/FileV2.vue +39 -6
  109. package/components/Input/InputPhoneNumberV2.vue +2 -1
  110. package/components/Input/ListGroupSelection.vue +56 -8
  111. package/components/Input/NRICNumber.vue +1 -0
  112. package/components/Input/NewDate.vue +1 -0
  113. package/components/Input/Number.vue +1 -0
  114. package/components/Input/Password.vue +1 -0
  115. package/components/Input/PhoneNumber.vue +21 -1
  116. package/components/Input/VehicleNumber.vue +1 -0
  117. package/components/InputLabel.vue +2 -2
  118. package/components/InvitationClientForm.vue +21 -19
  119. package/components/InvitationForm.vue +15 -21
  120. package/components/InvitationMain.vue +46 -61
  121. package/components/Layout/Header.vue +263 -36
  122. package/components/Layout/NavigationDrawer.vue +300 -30
  123. package/components/ListItem.vue +33 -3
  124. package/components/LocalPagination.vue +27 -3
  125. package/components/ManageChecklistMain.vue +136 -86
  126. package/components/MemberInformation.vue +32 -16
  127. package/components/MemberMain.vue +113 -93
  128. package/components/MyAttendanceMain.vue +3 -5
  129. package/components/NavigationItem.vue +83 -3
  130. package/components/Nfc/NFCPatrolReportMain.vue +47 -33
  131. package/components/Nfc/NFCPatrolRouteForm.vue +18 -20
  132. package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
  133. package/components/Nfc/NFCPatrolSettings.vue +14 -4
  134. package/components/Nfc/NFCTagForm.vue +22 -8
  135. package/components/Nfc/NFCTagMain.vue +41 -43
  136. package/components/Nfc/PatrolReport/DailyReportTab.vue +8 -1
  137. package/components/Nfc/PatrolReport/MonthlyReportTab.vue +8 -1
  138. package/components/Nfc/PatrolReport/MonthlyReportTable.vue +5 -3
  139. package/components/Nfc/PatrolReport/PatrolActivityTable.vue +26 -6
  140. package/components/Nfc/PatrolReport/PatrolReportTab.vue +8 -0
  141. package/components/Nfc/PatrolReport/PatrolRouteSummary.vue +41 -9
  142. package/components/Nfc/PatrolReport/RemarksDialog.vue +39 -9
  143. package/components/Nfc/PatrolReport/ReportEmptyState.vue +29 -9
  144. package/components/Nfc/PatrolReport/ReportFilters.vue +19 -10
  145. package/components/Nfc/PatrolReport/ReportLocationHeader.vue +32 -6
  146. package/components/Nfc/PatrolReport/SummaryReportTable.vue +28 -6
  147. package/components/NotificationSettings.vue +373 -0
  148. package/components/NumberSettingField.vue +9 -2
  149. package/components/OnlineFormConfigurationForm.vue +725 -237
  150. package/components/OnlineFormFill.vue +213 -106
  151. package/components/OnlineFormRenderPreview.vue +25 -30
  152. package/components/OnlineFormsConfiguration.vue +113 -109
  153. package/components/OrgViewDialog.vue +34 -16
  154. package/components/OvernightParkingAvailability.vue +49 -26
  155. package/components/OvernightParkingManagement.vue +78 -18
  156. package/components/PageHeader.vue +43 -0
  157. package/components/PassInformation.vue +109 -53
  158. package/components/PasswordConfirmation.vue +44 -26
  159. package/components/PeopleForm.vue +41 -20
  160. package/components/PhotoUpload.vue +110 -48
  161. package/components/PlaceholderComponent.vue +35 -19
  162. package/components/PlanActionsMenu.vue +12 -2
  163. package/components/PlanStatusBadge.vue +7 -8
  164. package/components/PlateNumberDisplay.vue +9 -6
  165. package/components/QrTemplate/PrintDialog.vue +66 -43
  166. package/components/RolePermissionFormCreate.vue +104 -79
  167. package/components/RolePermissionFormPreviewUpdate.vue +111 -109
  168. package/components/RolePermissionMain.vue +183 -81
  169. package/components/ScanVisitorQRCode.vue +99 -35
  170. package/components/ScheduleAreaMain.vue +34 -59
  171. package/components/ScheduleTaskForm.vue +61 -42
  172. package/components/ScheduleTaskMain.vue +56 -69
  173. package/components/SearchVehicleNumberUser.vue +39 -37
  174. package/components/ServiceProviderFormCreate.vue +29 -20
  175. package/components/ServiceProviderInvitationPrompt.vue +155 -0
  176. package/components/ServiceProviderMain.vue +601 -432
  177. package/components/Signature.vue +40 -34
  178. package/components/SignaturePad.vue +5 -29
  179. package/components/SiteSettings.vue +41 -11
  180. package/components/SiteSettingsWorkOrder.vue +61 -30
  181. package/components/SlideCardGroup.vue +35 -11
  182. package/components/Snackbar.vue +36 -3
  183. package/components/SpecificAttr.vue +18 -8
  184. package/components/StatusChip.vue +156 -0
  185. package/components/StockCard.vue +17 -12
  186. package/components/SubscriptionPlanTable.vue +207 -140
  187. package/components/SwitchContext.vue +201 -9
  188. package/components/TableHygiene.vue +36 -46
  189. package/components/TableList.vue +10 -10
  190. package/components/TableListSecondary.vue +105 -54
  191. package/components/TableMain.vue +251 -78
  192. package/components/TableV3.vue +61 -70
  193. package/components/TiptapEditor.vue +33 -40
  194. package/components/TiptapEditorQr.vue +5 -2
  195. package/components/Tooltip/Info.vue +69 -24
  196. package/components/UnitMain.vue +40 -60
  197. package/components/UnitPersonCard.vue +42 -13
  198. package/components/VehicleAddSelection.vue +48 -32
  199. package/components/VehicleForm.vue +112 -129
  200. package/components/VehicleManagement.vue +26 -12
  201. package/components/VehicleUpdateMoreAction.vue +59 -46
  202. package/components/VisitorDataFromScannedQRCodeForm.vue +90 -65
  203. package/components/VisitorForm.vue +774 -210
  204. package/components/VisitorFormSelection.vue +40 -37
  205. package/components/VisitorManagement.vue +239 -82
  206. package/components/VisitorPassKeyQRScanner.vue +65 -18
  207. package/components/VisitorSearchResultSummary.vue +60 -16
  208. package/components/VisitorSocketPopUp.vue +22 -14
  209. package/components/VisitorsReportPreview.vue +44 -29
  210. package/components/WorkOrder/Create.vue +26 -17
  211. package/components/WorkOrder/ListView.vue +38 -7
  212. package/components/WorkOrder/Main.vue +62 -59
  213. package/composables/useFeedback.ts +3 -3
  214. package/composables/useHidAmico.ts +269 -0
  215. package/composables/useHidNavigation.ts +2 -9
  216. package/composables/useMember.ts +3 -3
  217. package/composables/useNFCPatrolDailyReport.ts +13 -0
  218. package/composables/useNFCPatrolMonthlyReport.ts +22 -56
  219. package/composables/useNFCPatrolReport.ts +13 -0
  220. package/composables/useNotificationPreference.ts +87 -0
  221. package/composables/useOnlineFormPages.ts +2 -0
  222. package/composables/useRole.ts +22 -0
  223. package/composables/useServiceProvider.ts +3 -3
  224. package/composables/useServiceProviderInvitation.ts +145 -0
  225. package/composables/useSipWebPhone.ts +336 -0
  226. package/composables/useSubscriptionPlan.ts +62 -0
  227. package/composables/useThemePreference.ts +63 -0
  228. package/composables/useVisitor.ts +6 -1
  229. package/composables/useWebUsb.ts +127 -37
  230. package/composables/useWorkOrder.ts +17 -3
  231. package/error.vue +30 -6
  232. package/nuxt.config.ts +36 -0
  233. package/package.json +25 -4
  234. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +2 -2
  235. package/pages/[org]/[site]/access-mgmt/{identity-mapping → hid-cards}/index.vue +2 -2
  236. package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
  237. package/pages/notification-settings.vue +19 -0
  238. package/pages/payment-method-linked.vue +34 -10
  239. package/pages/require-customer.vue +48 -10
  240. package/pages/require-organization-membership.vue +32 -8
  241. package/pages/unauthorized.vue +29 -4
  242. package/plugins/secure-member.client.ts +21 -56
  243. package/plugins/vuetify.ts +69 -2
  244. package/types/nfc-patrol-report.ts +3 -2
  245. package/types/site.d.ts +71 -0
  246. package/types/subscription.ts +18 -11
  247. package/utils/breadcrumb.test.ts +108 -0
  248. package/utils/breadcrumb.ts +82 -0
  249. package/utils/dashboard.test.ts +118 -0
  250. package/utils/dashboard.ts +53 -0
  251. package/utils/data.test.ts +95 -0
  252. package/utils/data.ts +40 -2
  253. package/utils/hid-discover.test.ts +102 -0
  254. package/utils/list-page.test.ts +60 -0
  255. package/utils/list-page.ts +38 -0
  256. package/utils/materialSymbols.ts +363 -0
  257. package/utils/nav-sections.test.ts +190 -0
  258. package/utils/nav-sections.ts +151 -0
  259. package/utils/nfc-patrol-report.test.ts +130 -0
  260. package/utils/nfc-patrol-report.ts +101 -0
  261. package/utils/role.test.ts +69 -0
  262. package/utils/role.ts +10 -0
  263. package/utils/route-name.test.ts +85 -0
  264. package/utils/route-name.ts +24 -0
  265. package/utils/status.test.ts +110 -0
  266. package/utils/status.ts +135 -0
  267. package/utils/table-card.test.ts +84 -0
  268. package/utils/theme-aa-ledger.ts +148 -0
  269. package/utils/theme.test.ts +485 -7
  270. package/utils/theme.ts +295 -110
  271. package/.changeset/README.md +0 -8
  272. package/.changeset/config.json +0 -11
  273. package/.editorconfig +0 -12
  274. package/.github/workflows/main.yml +0 -17
  275. package/.github/workflows/publish-staging.yml +0 -47
  276. package/.github/workflows/publish.yml +0 -39
  277. package/PUBLISHING.md +0 -269
  278. package/components/HidIdentityMapping.vue +0 -975
  279. package/components/ScheduleTaskAreaFormDialog.vue +0 -144
  280. package/components/ScheduleTaskAreaUpdateMoreAction.vue +0 -109
  281. package/components/ScheduleTastTicketMain.vue +0 -182
  282. package/components/TableWithButton.vue +0 -94
  283. package/test/visitor-socket.test.mjs +0 -36
@@ -1,159 +1,164 @@
1
+ <!--
2
+ HID READER MANAGEMENT - the readers on this site's doors.
3
+
4
+ Was a bespoke screen: an above-card toolbar, a raw `v-table` and about
5
+ twenty-five hardcoded hexes. Those hexes are why this screen had no dark
6
+ theme - `#111827` on the dark card measures **1.06:1**, and `#475467` 2.17:1.
7
+
8
+ It is the design's standard table screen, so it is drawn with the shared one:
9
+ `TableMain` supplies the page heading, the card, the in-card toolbar, the
10
+ uppercase header row, the 49px rows and the pager. Every column, action,
11
+ dialog and handler is the same - the table's own `filteredReaders` still
12
+ feeds it, the overflow menu still opens the same five dialogs.
13
+
14
+ The handoff draws no HID table, so it states no column proportions for it -
15
+ the table takes the shared automatic layout every other `TableMain` caller
16
+ uses, and scrolls inside the card at 360/768 the way the other wide tables
17
+ do. See `readerHeaders` for why the screen's own fixed percentages were
18
+ measured and dropped rather than carried over.
19
+ -->
1
20
  <template>
2
21
  <section class="hid-reader-management">
3
- <div class="toolbar">
4
- <v-btn
5
- rounded="xl"
6
- class="text-none add-button"
7
- variant="flat"
8
- prepend-icon="mdi-plus"
9
- @click="openAdd"
10
- >
11
- Add HID Reader
12
- </v-btn>
13
-
14
- <div class="toolbar-spacer" />
15
-
16
- <v-text-field
17
- v-model="search"
18
- placeholder="Search"
19
- variant="outlined"
20
- density="compact"
21
- hide-details
22
- class="search-input"
23
- prepend-inner-icon="mdi-magnify"
24
- />
25
-
26
- <v-select
27
- v-model="status"
28
- :items="statusOptions"
29
- variant="outlined"
30
- density="compact"
31
- hide-details
32
- class="status-input"
33
- />
34
- </div>
35
-
36
- <v-card flat border class="table-card">
37
- <div class="table-refresh">
38
- <v-btn icon="mdi-refresh" variant="text" density="comfortable" :loading="loading" @click="loadReaders" />
39
- </div>
40
-
41
- <v-table class="reader-table">
42
- <thead>
43
- <tr>
44
- <th class="name-column">Name</th>
45
- <th class="location-column">Location</th>
46
- <th class="device-column">Device ID</th>
47
- <th class="status-column">Status</th>
48
- <th class="sync-at-column">Last sync at</th>
49
- <th class="message-column">Last sync message</th>
50
- <th class="action-column"></th>
51
- </tr>
52
- </thead>
53
- <tbody>
54
- <tr v-for="reader in filteredReaders" :key="reader._id">
55
- <td class="name-cell">
56
- <span class="cell-strong">{{ reader.name || "N/A" }}</span>
57
- </td>
58
- <td class="location-cell">{{ reader.location || "N/A" }}</td>
59
- <td class="device-cell">{{ reader.deviceId || "N/A" }}</td>
60
- <td class="status-cell">
61
- <v-chip
62
- size="small"
63
- variant="flat"
64
- class="status-chip"
65
- :class="getStatusClass(resolveReaderStatus(reader))"
66
- >
67
- {{ formatReaderStatus(resolveReaderStatus(reader)) }}
68
- </v-chip>
69
- </td>
70
- <td class="sync-at-cell">{{ formatDate(reader.lastSyncAt) }}</td>
71
- <td class="message-cell">
72
- <span class="message-text" :title="reader.lastSyncMessage || 'N/A'">
73
- {{ reader.lastSyncMessage || "N/A" }}
74
- </span>
75
- </td>
76
- <td class="action-cell">
77
- <v-menu>
78
- <template #activator="{ props: menuProps }">
79
- <v-btn
80
- v-bind="menuProps"
81
- icon="mdi-dots-vertical"
82
- variant="text"
83
- density="comfortable"
84
- class="action-menu-button"
85
- />
86
- </template>
87
-
88
- <v-list density="compact" min-width="180">
89
- <v-list-item title="View" @click="openView(reader)" />
90
- <v-list-item title="Sync HID Reader" @click="openSyncConfirm(reader)" />
91
- <v-list-item title="Test Connection" @click="openTestConfirm(reader)" />
92
- <v-list-item title="Edit" @click="openEdit(reader)" />
93
- <v-list-item title="Delete" class="text-error" @click="openDeleteConfirm(reader)" />
94
- </v-list>
95
- </v-menu>
96
- </td>
97
- </tr>
98
- </tbody>
99
- </v-table>
100
-
101
- <div v-if="!filteredReaders.length" class="empty-state">
102
- No HID devices added yet.
103
- </div>
104
- </v-card>
22
+ <TableMain
23
+ title="HID Readers"
24
+ :headers="readerHeaders"
25
+ :items="filteredReaders"
26
+ :loading="loading"
27
+ :items-per-page="-1"
28
+ :page-range="readerRange"
29
+ show-header
30
+ @refresh="loadReaders"
31
+ >
32
+ <template #actions>
33
+ <AppButton icon="mdi-plus" @click="openAdd">Add HID Reader</AppButton>
34
+ </template>
35
+
36
+ <!-- The design puts filters on their own full-width row under the
37
+ toolbar, which is what `extension` is. -->
38
+ <template #extension>
39
+ <div class="app-filter-row hid-reader-filters">
40
+ <AppField v-model="search" search placeholder="Search" />
41
+ <AppSelect
42
+ v-model="status"
43
+ :items="statusOptions"
44
+ placeholder="Status"
45
+ clearable
46
+ />
47
+ </div>
48
+ </template>
49
+
50
+ <template #[`item.name`]="{ item }">
51
+ <span class="app-cell--strong">{{ item.name || "N/A" }}</span>
52
+ </template>
53
+
54
+ <template #[`item.location`]="{ item }">
55
+ <span class="app-cell--muted">{{ item.location || "N/A" }}</span>
56
+ </template>
57
+
58
+ <template #[`item.deviceId`]="{ item }">
59
+ <span class="app-cell--muted">{{ item.deviceId || "N/A" }}</span>
60
+ </template>
61
+
62
+ <!-- The shared chip, so "Active" is the same green here as on a work
63
+ order. `utils/status.ts` is the mapping; this screen's own
64
+ `getStatusClass` was a fourth private copy of it. -->
65
+ <template #[`item.status`]="{ item }">
66
+ <StatusChip
67
+ :status="resolveReaderStatus(item)"
68
+ :label="formatReaderStatus(resolveReaderStatus(item))"
69
+ />
70
+ </template>
71
+
72
+ <template #[`item.lastSyncAt`]="{ item }">
73
+ <span class="app-cell--num">{{ formatDate(item.lastSyncAt) }}</span>
74
+ </template>
75
+
76
+ <template #[`item.lastSyncMessage`]="{ item }">
77
+ <span class="app-cell--muted" :title="item.lastSyncMessage || 'N/A'">
78
+ {{ item.lastSyncMessage || "N/A" }}
79
+ </span>
80
+ </template>
81
+
82
+ <template #[`item.action-table`]="{ item }">
83
+ <v-menu>
84
+ <template #activator="{ props: menuProps }">
85
+ <AppButton v-bind="menuProps" variant="row" icon="mdi-dots-vertical" />
86
+ </template>
87
+
88
+ <v-list density="compact" min-width="180">
89
+ <v-list-item title="View" @click="openView(item)" />
90
+ <v-list-item title="Sync HID Reader" @click="openSyncConfirm(item)" />
91
+ <v-list-item title="Test Connection" @click="openTestConfirm(item)" />
92
+ <v-list-item title="Edit" @click="openEdit(item)" />
93
+ <v-list-item title="Delete" class="text-error" @click="openDeleteConfirm(item)" />
94
+ </v-list>
95
+ </v-menu>
96
+ </template>
97
+
98
+ <!-- The screen's own sentence, kept: the shared empty state says "No data
99
+ available", which is less useful on a screen whose whole point is
100
+ that no device has been added yet. -->
101
+ <template #no-data>
102
+ <div class="table-card__empty">
103
+ <v-icon icon="mdi-card-account-details-outline" size="32" />
104
+ <span>No HID devices added yet.</span>
105
+ </div>
106
+ </template>
107
+ </TableMain>
105
108
 
106
109
  <HidReaderForm
107
110
  v-model="formDialog"
108
111
  :mode="selectedReader ? 'edit' : 'add'"
109
112
  :reader="selectedReader"
110
113
  :loading="saving"
114
+ :site="site"
111
115
  @submit="saveReader"
112
116
  />
113
117
 
114
118
  <v-dialog v-model="viewDialog" max-width="390">
115
119
  <v-card rounded="lg">
116
- <v-card-title class="small-title">{{ selectedReader?.name || "HID Reader" }}</v-card-title>
120
+ <v-card-title class="screen-card-title">{{ selectedReader?.name || "HID Reader" }}</v-card-title>
117
121
  <v-card-text>
118
122
  <div class="detail-grid">
119
123
  <span>HID reader name</span><strong>{{ selectedReader?.name || "N/A" }}</strong>
120
124
  <span>Device ID</span><strong>{{ selectedReader?.deviceId || "N/A" }}</strong>
121
125
  <span>Location</span><strong>{{ selectedReader?.location || "N/A" }}</strong>
126
+ <span>HID Portal</span><strong>{{ selectedReader?.portalName || selectedReader?.portalId || "N/A" }}</strong>
122
127
  <span>Monitor Path</span><strong>{{ selectedReader?.monitorPath || "N/A" }}</strong>
123
128
  <span>Status</span><strong>{{ formatReaderStatus(resolveReaderStatus(selectedReader)) }}</strong>
124
129
  <span>Username</span><strong>{{ selectedReader?.username || "N/A" }}</strong>
125
130
  </div>
126
131
 
127
132
  <div class="dialog-actions-inline">
128
- <v-btn rounded="xl" class="text-none" variant="tonal" @click="openTestConfirm(selectedReader)">Test Connection</v-btn>
129
- <v-btn rounded="xl" class="text-none" variant="tonal" @click="openSyncConfirm(selectedReader)">Sync Reader</v-btn>
133
+ <AppButton variant="ghost" @click="openTestConfirm(selectedReader)">Test Connection</AppButton>
134
+ <AppButton variant="ghost" @click="openSyncConfirm(selectedReader)">Sync Reader</AppButton>
130
135
  </div>
131
136
  </v-card-text>
132
137
  <v-card-actions>
133
138
  <v-spacer />
134
- <v-btn class="text-none" variant="text" @click="viewDialog = false">Close</v-btn>
139
+ <AppButton variant="ghost" @click="viewDialog = false">Close</AppButton>
135
140
  </v-card-actions>
136
141
  </v-card>
137
142
  </v-dialog>
138
143
 
139
144
  <v-dialog v-model="confirmDialog" max-width="390" persistent>
140
145
  <v-card rounded="lg" class="reader-action-dialog">
141
- <v-card-title class="small-title">{{ confirmTitle }}</v-card-title>
146
+ <v-card-title class="screen-card-title">{{ confirmTitle }}</v-card-title>
142
147
 
143
148
  <v-card-text v-if="pendingAction === 'test'" class="reader-action-content">
144
- <v-icon icon="mdi-database-sync-outline" color="#1976d2" size="48" />
149
+ <v-icon icon="mdi-database-sync-outline" class="dialog-mark" size="48" />
145
150
  <h3>Test Device Connection</h3>
146
151
  <p>You are about to test the connection for {{ selectedReader?.name || "this HID Reader" }}.</p>
147
152
  </v-card-text>
148
153
 
149
154
  <v-card-text v-else-if="pendingAction === 'sync' && actionStage === 'confirm'" class="reader-action-content">
150
- <v-icon icon="mdi-database-sync-outline" color="#1976d2" size="48" />
155
+ <v-icon icon="mdi-database-sync-outline" class="dialog-mark" size="48" />
151
156
  <h3>Sync Data</h3>
152
157
  <p>You're about to sync data with {{ selectedReader?.name || "this HID Reader" }}.</p>
153
158
  </v-card-text>
154
159
 
155
160
  <v-card-text v-else-if="pendingAction === 'sync' && actionStage === 'review'" class="reader-action-content reader-sync-review">
156
- <v-icon icon="mdi-database-sync-outline" color="#1976d2" size="48" />
161
+ <v-icon icon="mdi-database-sync-outline" class="dialog-mark" size="48" />
157
162
  <h3>Ready to Synchronize</h3>
158
163
  <div class="sync-summary-list">
159
164
  <div>
@@ -181,7 +186,7 @@
181
186
  </v-card-text>
182
187
 
183
188
  <v-card-text v-else-if="pendingAction === 'sync' && actionStage === 'loading'" class="reader-action-content">
184
- <v-progress-circular indeterminate color="#8a8f98" size="42" width="5" />
189
+ <v-progress-circular indeterminate class="dialog-spinner" size="42" width="5" />
185
190
  <h3>Sync Data to {{ selectedReader?.name || "HID Reader" }}...</h3>
186
191
  </v-card-text>
187
192
 
@@ -207,7 +212,7 @@
207
212
 
208
213
  <v-dialog v-model="resultDialog" max-width="390">
209
214
  <v-card rounded="lg">
210
- <v-card-title v-if="resultReaderName" class="small-title">{{ resultReaderName }}</v-card-title>
215
+ <v-card-title v-if="resultReaderName" class="screen-card-title">{{ resultReaderName }}</v-card-title>
211
216
  <v-card-text class="result-content" :class="{ 'result-with-stats': resultStats }">
212
217
  <v-icon :icon="resultSuccess ? 'mdi-check-circle' : 'mdi-alert-circle'" :color="resultSuccess ? 'success' : 'warning'" size="42" />
213
218
  <h3>{{ resultTitle }}</h3>
@@ -257,7 +262,7 @@ const {
257
262
 
258
263
  const readers = ref<Record<string, any>[]>([]);
259
264
  const search = ref("");
260
- const status = ref("Status");
265
+ const status = ref("");
261
266
  const loading = ref(false);
262
267
  const saving = ref(false);
263
268
  const actionLoading = ref(false);
@@ -281,7 +286,42 @@ const syncPreviewStats = ref({
281
286
  });
282
287
  const resultStats = ref<typeof syncPreviewStats.value | null>(null);
283
288
 
284
- const statusOptions = ["Status", "active", "inactive", "deleted", "offline"];
289
+ const statusOptions = [
290
+ { title: "Active", value: "active" },
291
+ { title: "Inactive", value: "inactive" },
292
+ { title: "Offline", value: "offline" },
293
+ ];
294
+
295
+ /**
296
+ * The columns. NO `weight`, on purpose.
297
+ *
298
+ * The old CSS pinned them at 19/14/15/12/15/20/5 % with `table-layout: fixed`,
299
+ * and `TableMain`'s `weight` would say exactly that - but it was measured and
300
+ * it is worse than the automatic layout, in two ways the design cares about:
301
+ *
302
+ * * at 360 the six percentages resolve against a ~330px card, so every cell
303
+ * truncates to "Mai...", the headers overlap and the status chip is cut in
304
+ * half. The old fixed grid only worked because it also carried
305
+ * `min-width: 920px`, which is the automatic layout's behaviour anyway.
306
+ * * a 5% action column is ~29px of content once the shared 20px cell padding
307
+ * is taken out, so the 30px row button overflows by half a pixel and the
308
+ * fixed-layout ellipsis rule paints a "..." beside every button.
309
+ *
310
+ * The handoff states `fr` proportions for the tables it draws (Feedbacks,
311
+ * Units, Pass & Key) and draws no HID table at all, so there is no design
312
+ * number to honour here. Automatic is what the other 39 `TableMain` callers
313
+ * use. `sortable: false` keeps the previous behaviour: the raw `v-table` never
314
+ * sorted.
315
+ */
316
+ const readerHeaders = [
317
+ { title: "Name", value: "name", sortable: false },
318
+ { title: "Location", value: "location", sortable: false },
319
+ { title: "Device ID", value: "deviceId", sortable: false },
320
+ { title: "Status", value: "status", sortable: false },
321
+ { title: "Last sync at", value: "lastSyncAt", sortable: false },
322
+ { title: "Last sync message", value: "lastSyncMessage", sortable: false },
323
+ { title: "", value: "action-table", sortable: false },
324
+ ];
285
325
 
286
326
  const filteredReaders = computed(() => {
287
327
  const query = search.value.trim().toLowerCase();
@@ -294,12 +334,21 @@ const filteredReaders = computed(() => {
294
334
  resolveReaderStatus(reader),
295
335
  ].some((value) => String(value ?? "").toLowerCase().includes(query));
296
336
 
297
- const matchesStatus = status.value === "Status" || resolveReaderStatus(reader) === status.value;
337
+ const matchesStatus = !status.value || resolveReaderStatus(reader) === status.value;
298
338
 
299
339
  return matchesSearch && matchesStatus;
300
340
  });
301
341
  });
302
342
 
343
+ /**
344
+ * The design's `1-6 of 6` count on the toolbar row. This screen loads every
345
+ * reader in one call and has never paged, so the range is simply what is on
346
+ * screen - stated rather than left as `TableMain`'s `-- - -- of --` placeholder.
347
+ */
348
+ const readerRange = computed(() =>
349
+ filteredReaders.value.length ? `1 - ${filteredReaders.value.length} of ${filteredReaders.value.length}` : "0 of 0"
350
+ );
351
+
303
352
  const confirmTitle = computed(() => {
304
353
  if (pendingAction.value === "sync" && actionStage.value === "loading") return "Test Connection...";
305
354
  if (pendingAction.value === "sync") return "Sync Data";
@@ -915,13 +964,6 @@ function formatDate(value?: string) {
915
964
  return Number.isNaN(date.getTime()) ? "N/A" : date.toLocaleString();
916
965
  }
917
966
 
918
- function getStatusClass(value?: string) {
919
- if (value === "active") return "status-success";
920
- if (value === "inactive") return "status-warning";
921
- if (value === "deleted") return "status-error";
922
- return "status-muted";
923
- }
924
-
925
967
  function resolveReaderStatus(reader?: Record<string, any> | null) {
926
968
  if (!reader) return "";
927
969
  if (reader.lastSyncStatus === "failed" && reader.status === "active") return "offline";
@@ -942,208 +984,49 @@ function formatReaderStatus(value?: string) {
942
984
  }
943
985
  </script>
944
986
 
987
+
945
988
  <style scoped>
989
+ /*
990
+ WHAT IS LEFT AFTER THE CARD AND TABLE MOVED TO `TableMain`.
991
+
992
+ The toolbar, the card, the `reader-table` grid, the seven column widths, the
993
+ header band, the row height, the hover, the empty state and the four
994
+ `status-*` chip palettes are all gone - they were a private copy of the shared
995
+ table, drawn in about twenty-five literal hexes. What remains is the two
996
+ dialogs, and every colour in them is now a token, so the dark theme reaches
997
+ them for the first time.
998
+ */
946
999
  .hid-reader-management {
947
1000
  padding: 24px 0;
948
1001
  width: 100%;
949
1002
  min-width: 0;
950
1003
  }
951
1004
 
952
- .toolbar {
953
- display: flex;
954
- flex-wrap: wrap;
955
- gap: 12px;
956
- align-items: center;
957
- margin-bottom: 16px;
958
- }
959
-
960
- .toolbar-spacer {
961
- flex: 1;
962
- }
963
-
964
- .add-button {
965
- min-width: 170px;
966
- background: #e4e4e4;
967
- color: #0a2638;
1005
+ .hid-reader-filters {
1006
+ /* The filter row sits INSIDE the card's extension, so it needs the card's
1007
+ own side padding rather than the row's default bottom margin. */
1008
+ margin: 0;
1009
+ padding: 10px 14px;
968
1010
  }
969
1011
 
970
- .search-input {
1012
+ .hid-reader-filters :deep(.app-field) {
971
1013
  max-width: 240px;
972
1014
  }
973
1015
 
974
- .status-input {
1016
+ .hid-reader-filters :deep(.app-select) {
975
1017
  max-width: 160px;
976
1018
  }
977
1019
 
978
- .table-card {
979
- min-height: 430px;
980
- overflow-x: auto;
981
- overflow-y: hidden;
982
- }
983
-
984
- .reader-table :deep(table) {
985
- table-layout: fixed;
986
- width: 100%;
987
- min-width: 920px;
988
- border-collapse: separate;
989
- border-spacing: 0;
990
- }
991
-
992
- .reader-table :deep(th) {
993
- height: 44px;
994
- background: #f8fafc;
995
- color: #344054;
996
- font-size: 12px;
997
- font-weight: 700;
998
- letter-spacing: 0;
999
- text-transform: none;
1000
- border-bottom: 1px solid #e7ebef;
1001
- }
1002
-
1003
- .reader-table :deep(td) {
1004
- height: 64px;
1005
- color: #1f2937;
1006
- font-size: 13px;
1007
- border-bottom: 1px solid #edf0f3;
1008
- vertical-align: middle;
1009
- }
1010
-
1011
- .reader-table :deep(tbody tr) {
1012
- transition: background-color 0.15s ease;
1013
- }
1014
-
1015
- .reader-table :deep(tbody tr:hover) {
1016
- background: #fbfcfe;
1017
- }
1018
-
1019
- .reader-table :deep(th),
1020
- .reader-table :deep(td) {
1021
- padding: 0 14px !important;
1022
- white-space: nowrap;
1023
- }
1024
-
1025
- .name-column,
1026
- .name-cell {
1027
- width: 19%;
1028
- }
1029
-
1030
- .location-column,
1031
- .location-cell {
1032
- width: 14%;
1033
- }
1034
-
1035
- .device-column,
1036
- .device-cell {
1037
- width: 15%;
1038
- }
1039
-
1040
- .status-column,
1041
- .status-cell {
1042
- width: 12%;
1043
- }
1044
-
1045
- .sync-at-column,
1046
- .sync-at-cell {
1047
- width: 15%;
1048
- }
1049
-
1050
- .message-column,
1051
- .message-cell {
1052
- width: 20%;
1053
- min-width: 0;
1054
- }
1055
-
1056
- .action-column,
1057
- .action-cell {
1058
- width: 5%;
1059
- text-align: right;
1060
- }
1061
-
1062
- .cell-strong {
1063
- display: block;
1064
- overflow: hidden;
1065
- color: #111827;
1066
- font-weight: 600;
1067
- text-overflow: ellipsis;
1068
- }
1069
-
1070
- .location-cell,
1071
- .device-cell,
1072
- .sync-at-cell {
1073
- overflow: hidden;
1074
- color: #475467;
1075
- text-overflow: ellipsis;
1076
- }
1077
-
1078
- .message-text {
1079
- display: block;
1080
- max-width: 100%;
1081
- overflow: hidden;
1082
- color: #475467;
1083
- line-height: 1.35;
1084
- text-overflow: ellipsis;
1085
- }
1086
-
1087
- .action-menu-button {
1088
- color: #344054;
1089
- }
1090
-
1091
- .table-refresh {
1092
- display: flex;
1093
- justify-content: flex-start;
1094
- padding: 10px 14px;
1095
- border-bottom: 1px solid #e7ebef;
1096
- background: #ffffff;
1097
- }
1098
-
1099
- .empty-state {
1100
- min-height: 280px;
1101
- display: grid;
1102
- place-items: center;
1103
- color: #687382;
1104
- font-size: 13px;
1105
- }
1106
-
1107
- .status-chip {
1108
- min-width: 86px;
1109
- justify-content: center;
1110
- font-weight: 600;
1111
- }
1112
-
1113
- .status-success {
1114
- background: #dff7e7 !important;
1115
- color: #1b7f3a !important;
1116
- }
1117
-
1118
- .status-warning {
1119
- background: #fff0cc !important;
1120
- color: #9a6700 !important;
1121
- }
1122
-
1123
- .status-error {
1124
- background: #fde2e1 !important;
1125
- color: #b42318 !important;
1126
- }
1127
-
1128
- .status-muted {
1129
- background: #e9edf2 !important;
1130
- color: #475467 !important;
1131
- }
1132
-
1133
- .small-title {
1134
- font-size: 14px;
1135
- font-weight: 700;
1136
- }
1137
-
1138
1020
  .detail-grid {
1139
1021
  display: grid;
1140
1022
  grid-template-columns: 1fr 1fr;
1141
1023
  gap: 8px 16px;
1142
1024
  font-size: 13px;
1025
+ color: var(--text);
1143
1026
  }
1144
1027
 
1145
1028
  .detail-grid span {
1146
- color: #667085;
1029
+ color: var(--muted);
1147
1030
  }
1148
1031
 
1149
1032
  .dialog-actions-inline {
@@ -1168,7 +1051,7 @@ function formatReaderStatus(value?: string) {
1168
1051
 
1169
1052
  .reader-action-content h3 {
1170
1053
  margin: 4px 0 0;
1171
- color: #111827;
1054
+ color: var(--text);
1172
1055
  font-size: 15px;
1173
1056
  font-weight: 700;
1174
1057
  line-height: 1.3;
@@ -1176,11 +1059,21 @@ function formatReaderStatus(value?: string) {
1176
1059
 
1177
1060
  .reader-action-content p {
1178
1061
  margin: 0;
1179
- color: #667085;
1062
+ color: var(--muted);
1180
1063
  font-size: 13px;
1181
1064
  line-height: 1.45;
1182
1065
  }
1183
1066
 
1067
+ /* Was `color="#1976d2"` and `color="#8a8f98"` on the elements themselves - the
1068
+ same blue and the same grey on both themes. */
1069
+ .dialog-mark {
1070
+ color: var(--accent);
1071
+ }
1072
+
1073
+ .dialog-spinner {
1074
+ color: var(--muted);
1075
+ }
1076
+
1184
1077
  .reader-sync-review {
1185
1078
  justify-items: stretch;
1186
1079
  text-align: left;
@@ -1204,24 +1097,32 @@ function formatReaderStatus(value?: string) {
1204
1097
  align-items: center;
1205
1098
  justify-content: space-between;
1206
1099
  gap: 16px;
1207
- color: #667085;
1100
+ color: var(--muted);
1208
1101
  font-size: 13px;
1209
1102
  }
1210
1103
 
1211
1104
  .sync-summary-list strong {
1212
- color: #111827;
1105
+ color: var(--text);
1213
1106
  font-weight: 700;
1214
1107
  }
1215
1108
 
1109
+ /* The split footer bar. The handoff does not draw one, so the SHAPE is the
1110
+ screen's own; only the colours change - `#062d42` was a fixed navy that sat
1111
+ on the dark card unchanged. */
1216
1112
  .action-cancel,
1217
1113
  .action-submit {
1218
1114
  flex: 1;
1219
1115
  border-radius: 0;
1220
1116
  }
1221
1117
 
1118
+ .action-cancel {
1119
+ background: var(--card);
1120
+ color: var(--text2);
1121
+ }
1122
+
1222
1123
  .action-submit {
1223
- background: #062d42;
1224
- color: #ffffff;
1124
+ background: var(--accent-strong);
1125
+ color: var(--on-accent-strong);
1225
1126
  }
1226
1127
 
1227
1128
  .result-content {
@@ -1245,24 +1146,11 @@ function formatReaderStatus(value?: string) {
1245
1146
  }
1246
1147
 
1247
1148
  @media (max-width: 960px) {
1248
- .toolbar {
1249
- align-items: stretch;
1250
- }
1251
-
1252
- .toolbar-spacer {
1253
- display: none;
1254
- }
1255
-
1256
- .add-button,
1257
- .search-input,
1258
- .status-input {
1149
+ .hid-reader-filters :deep(.app-field),
1150
+ .hid-reader-filters :deep(.app-select) {
1259
1151
  max-width: none;
1260
1152
  width: 100%;
1261
1153
  }
1262
-
1263
- .table-card {
1264
- min-height: 360px;
1265
- }
1266
1154
  }
1267
1155
 
1268
1156
  @media (max-width: 600px) {
@@ -1274,7 +1162,7 @@ function formatReaderStatus(value?: string) {
1274
1162
  grid-template-columns: 1fr;
1275
1163
  }
1276
1164
 
1277
- .dialog-actions-inline .v-btn {
1165
+ .dialog-actions-inline .app-btn {
1278
1166
  width: 100%;
1279
1167
  }
1280
1168