@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,46 +1,23 @@
1
1
  <template>
2
- <v-card width="100%">
3
- <v-toolbar>
4
- <v-row no-gutters class="fill-height px-6" align="center">
5
- <span class="font-weight-bold text-h5 text-capitalize">
6
- {{ mode }} Visitor
7
- </span>
8
- </v-row>
9
- </v-toolbar>
2
+ <v-card width="100%" class="screen-modal visitor-select">
3
+ <v-card-title class="text-capitalize">{{ mode }} Visitor</v-card-title>
10
4
 
11
- <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-5">
12
- <span class="text-subtitle-2 w-100 font-weight-medium d-flex justify-center mb-3">Please Select Visitor Type</span>
5
+ <div class="screen-modal__body visitor-select__body">
6
+ <p class="visitor-select__hint">Please Select Visitor Type</p>
13
7
  <template v-for="item in selection" :key="item.value">
14
- <v-btn
8
+ <AppButton
15
9
  v-if="showSelection(item)"
16
- color="primary-button"
17
- block
18
- variant="flat"
19
- rounded="md"
20
- size="48"
21
- :text="item.label"
10
+ class="visitor-select__option"
22
11
  @click="select(item.value)"
23
- class="my-2 text-capitalize text-subtitle-2"
24
- />
12
+ >
13
+ {{ item.label }}
14
+ </AppButton>
25
15
  </template>
26
- </v-card-text>
16
+ </div>
27
17
 
28
- <v-toolbar density="compact">
29
- <v-row no-gutters>
30
- <v-col cols="12">
31
- <v-btn
32
- tile
33
- block
34
- variant="text"
35
- class="text-none"
36
- size="48"
37
- @click="cancel"
38
- >
39
- Cancel
40
- </v-btn>
41
- </v-col>
42
- </v-row>
43
- </v-toolbar>
18
+ <div class="visitor-select__footer">
19
+ <AppButton variant="ghost" @click="cancel">Cancel</AppButton>
20
+ </div>
44
21
  </v-card>
45
22
  </template>
46
23
 
@@ -69,6 +46,32 @@ function showSelection(item: any) {
69
46
  if(item.value === 'walk-in' && prop.mode === 'register') {
70
47
  return false ;
71
48
  } return true;
72
-
49
+
73
50
  }
74
51
  </script>
52
+
53
+ <style scoped>
54
+ /* Same shape as the Add Vehicle prompt: full-width choices, one per row. */
55
+ .visitor-select__body {
56
+ padding: 20px;
57
+ }
58
+
59
+ .visitor-select__hint {
60
+ margin: 0 0 14px;
61
+ text-align: center;
62
+ font-size: var(--fs-cell);
63
+ color: var(--muted);
64
+ }
65
+
66
+ .visitor-select__option {
67
+ display: flex;
68
+ width: 100%;
69
+ margin-bottom: 10px;
70
+ }
71
+
72
+ .visitor-select__footer {
73
+ display: flex;
74
+ justify-content: flex-end;
75
+ padding: 0 20px 20px;
76
+ }
77
+ </style>
@@ -1,50 +1,28 @@
1
1
  <template>
2
2
  <v-row no-gutters>
3
- <TableMain
4
- :headers="headers"
5
- :items="items"
6
- :loading="getVisitorPending"
7
- :page="page"
8
- :pages="pages"
9
- :extension-height="110"
10
- :offset="300"
11
- :pageRange="pageRange"
12
- @refresh="getVisitorRefresh"
13
- @update:page="handleUpdatePage"
14
- show-header
15
- @row-click="handleRowClick"
16
- >
17
- <template #actions>
18
- <v-row no-gutters>
19
- <v-btn
20
- v-if="canAddVisitor"
21
- class="text-none"
22
- rounded="pill"
23
- variant="tonal"
24
- size="large"
25
- @click="handleAddNew"
26
- text="Add Visitor"
27
- />
28
- <v-btn
3
+ <!--
4
+ THE DESIGN'S PAGE TITLE ROW (Phase 9).
5
+
6
+ Drawn here rather than through `TableMain`'s `title` prop because the
7
+ design puts the screen's actions on the title line, and `TableMain`
8
+ renders its `actions` slot as a separate row under the heading. Same
9
+ three buttons, same handlers, same permission gates - moved, not changed.
10
+ -->
11
+ <v-col cols="12">
12
+ <PageHeader title="Visitor Mgmt">
13
+ <template #actions>
14
+ <AppButton
29
15
  v-if="canScanVisitorQRCode"
30
- text="Scan QR Code"
31
- class="text-none ml-2"
32
- rounded="pill"
33
- variant="tonal"
34
- size="large"
16
+ variant="ghost"
35
17
  @click="dialog.scanVisitorQRCode = true"
36
- />
18
+ >
19
+ Scan QR Code
20
+ </AppButton>
37
21
  <v-menu>
38
22
  <template #activator="{ props }">
39
- <v-btn
40
- v-bind="props"
41
- text="Download Report"
42
- class="text-none ml-2"
43
- prepend-icon="mdi-download"
44
- rounded="pill"
45
- variant="tonal"
46
- size="large"
47
- />
23
+ <AppButton v-bind="props" variant="ghost" icon="mdi-download">
24
+ Download Report
25
+ </AppButton>
48
26
  </template>
49
27
  <v-list>
50
28
  <v-list-item
@@ -59,8 +37,27 @@
59
37
  />
60
38
  </v-list>
61
39
  </v-menu>
62
- </v-row>
63
- </template>
40
+ <AppButton v-if="canAddVisitor" icon="mdi-plus" @click="handleAddNew">
41
+ Add Visitor
42
+ </AppButton>
43
+ </template>
44
+ </PageHeader>
45
+ </v-col>
46
+
47
+ <TableMain
48
+ :headers="headers"
49
+ :items="items"
50
+ :loading="getVisitorPending"
51
+ :page="page"
52
+ :pages="pages"
53
+ :extension-height="110"
54
+ :offset="300"
55
+ :pageRange="pageRange"
56
+ @refresh="getVisitorRefresh"
57
+ @update:page="handleUpdatePage"
58
+ show-header
59
+ @row-click="handleRowClick"
60
+ >
64
61
  <template #extension>
65
62
  <v-row no-gutters class="w-100 d-flex flex-column">
66
63
  <v-tabs
@@ -109,32 +106,18 @@
109
106
  hide-details
110
107
  />
111
108
  <InputDatePicker v-model="dateTo" density="compact" hide-details />
112
- <v-select
109
+ <AppSelect
113
110
  v-if="activeTab == 'registered'"
114
111
  v-model="filterTypes"
115
112
  label="Filter by types"
116
113
  item-title="label"
117
114
  item-value="value"
118
115
  :items="visitorSelection"
119
- density="compact"
116
+ class="visitor-type-filter"
117
+ compact
120
118
  clearable
121
119
  multiple
122
- max-width="200"
123
- hide-details
124
- >
125
- <template v-slot:selection="{ item, index }">
126
- <div class="d-flex align-center text-caption text-nowrap">
127
- <v-chip
128
- v-if="index === 0"
129
- color="error"
130
- :text="filterTypeSelectionLabel()"
131
- size="x-small"
132
- variant="tonal"
133
- class="ml-2"
134
- />
135
- </div>
136
- </template>
137
- </v-select>
120
+ />
138
121
  </v-card>
139
122
  </v-row>
140
123
  </template>
@@ -303,6 +286,18 @@
303
286
  >
304
287
  </div>
305
288
 
289
+ <v-btn
290
+ v-if="hidQrEnabled"
291
+ icon="mdi-qrcode"
292
+ size="x-small"
293
+ variant="text"
294
+ color="primary"
295
+ title="View HID QR code"
296
+ aria-label="View HID QR code"
297
+ class="align-self-start mt-1"
298
+ @click.stop="openVisitorHidQr(item)"
299
+ />
300
+
306
301
  <v-menu :close-on-content-click="true">
307
302
  <template v-slot:activator="{ props: menuProps }">
308
303
  <div
@@ -618,15 +613,21 @@
618
613
  class="d-flex align-center"
619
614
  >
620
615
  <strong>{{ label }}:</strong>
621
- <v-btn
622
- size="x-small"
623
- class="ml-3 text-capitalize"
624
- color="success"
625
- text="Checkin"
616
+ <!-- The design's check-in / check-out pair. Was Vuetify's
617
+ `color="success"` beside a hardcoded `color="red"` - two
618
+ different systems on one field list, and neither of them a
619
+ token. `:loading` is dropped rather than re-invented: the
620
+ same flag already drives `:disabled`, which is the
621
+ precedent set when the scanned-visitor form converted. -->
622
+ <AppButton
623
+ variant="ok"
624
+ icon="mdi-check"
625
+ class="ml-3"
626
626
  :disabled="isVisitorDataFromScannedQRCodeCheckingInOut"
627
627
  @click="handleCheckin(selectedVisitorDataObject)"
628
- :loading="isVisitorDataFromScannedQRCodeCheckingInOut"
629
- />
628
+ >
629
+ Check In
630
+ </AppButton>
630
631
  </span>
631
632
  <span
632
633
  v-if="
@@ -638,14 +639,15 @@
638
639
  class="d-flex align-center"
639
640
  >
640
641
  <strong>{{ label }}:</strong>
641
- <v-btn
642
- size="x-small"
643
- class="ml-3 text-capitalize"
644
- color="red"
645
- text="Checkout"
642
+ <AppButton
643
+ variant="err"
644
+ icon="mdi-close"
645
+ class="ml-3"
646
646
  :disabled="loading.checkingOut"
647
647
  @click="handleCheckout(selectedVisitorId as string)"
648
- />
648
+ >
649
+ Check Out
650
+ </AppButton>
649
651
  </span>
650
652
 
651
653
  <span
@@ -1177,12 +1179,52 @@
1177
1179
  </v-card>
1178
1180
  </v-dialog>
1179
1181
 
1182
+ <v-dialog v-model="hidQrDialog" max-width="440" persistent>
1183
+ <v-card>
1184
+ <v-toolbar color="transparent" density="comfortable" class="px-2">
1185
+ <v-toolbar-title class="text-subtitle-1 font-weight-bold">Visitor QR Code</v-toolbar-title>
1186
+ <v-btn icon="mdi-close" variant="text" title="Close" @click="hidQrDialog = false" />
1187
+ </v-toolbar>
1188
+ <v-card-text class="px-8 pb-6 text-center">
1189
+ <v-progress-circular v-if="hidQrLoading" indeterminate color="primary" class="my-10" />
1190
+ <template v-else-if="hidQrImageUrl">
1191
+ <div class="text-h6 font-weight-bold">{{ hidQrVisitor?.name || "Visitor Access Pass" }}</div>
1192
+ <div class="visitor-hid-qr my-5">
1193
+ <v-img :src="hidQrImageUrl" width="280" height="280" alt="Visitor HID QR code" />
1194
+ </div>
1195
+ <div class="text-caption text-medium-emphasis">
1196
+ Valid until {{ formatHidQrExpiry(hidQrExpiresAt) }}
1197
+ </div>
1198
+ <v-chip class="mt-2" size="small" :color="hidQrExpired ? 'error' : 'success'" variant="tonal">
1199
+ {{ hidQrExpired ? "Expired" : `Expires in ${formatHidQrCountdown(hidQrRemainingSeconds)}` }}
1200
+ </v-chip>
1201
+ </template>
1202
+ <v-alert v-if="hidQrError" type="warning" variant="tonal" density="compact" class="mt-4 text-left">
1203
+ {{ hidQrError }}
1204
+ </v-alert>
1205
+ <v-btn
1206
+ v-if="!hidQrLoading && (!hidQrImageUrl || hidQrExpired)"
1207
+ block
1208
+ color="primary-button"
1209
+ variant="flat"
1210
+ class="mt-4 text-none"
1211
+ prepend-icon="mdi-refresh"
1212
+ :loading="hidQrGenerating"
1213
+ @click="generateVisitorHidQr"
1214
+ >
1215
+ {{ hidQrExpired ? "Generate QR again" : "Generate HID QR code" }}
1216
+ </v-btn>
1217
+ </v-card-text>
1218
+ </v-card>
1219
+ </v-dialog>
1220
+
1180
1221
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
1181
1222
  </v-row>
1182
1223
  </template>
1183
1224
 
1184
1225
  <script setup lang="ts">
1185
1226
  import moment from "moment-timezone";
1227
+ import QRCode from "qrcode";
1186
1228
 
1187
1229
  definePageMeta({
1188
1230
  middleware: ["01-auth", "02-org"],
@@ -1244,6 +1286,8 @@ const {
1244
1286
  } = useUtils();
1245
1287
  const { formatLocation } = useSecurityUtils();
1246
1288
  const { getFileUrlAnpr } = useFile();
1289
+ const { getSiteById } = useSite();
1290
+ const { getIdentities, issueVisitorQr } = useHidAmico();
1247
1291
  // const { status: visitorStatus, search } = useRoute().query as { status: string, search: string};
1248
1292
 
1249
1293
  const routeName = route.name;
@@ -1255,6 +1299,106 @@ const pageRange = ref("-- - -- of --");
1255
1299
  const activeTab = ref("registered");
1256
1300
  const activeVisitorFormType = ref<TVisitorType | null>(null);
1257
1301
  const selectedVisitorId = ref<string | null>(""); // selected visitor for viewing/actions
1302
+ const hidQrDialog = ref(false);
1303
+ const hidQrLoading = ref(false);
1304
+ const hidQrGenerating = ref(false);
1305
+ const hidQrError = ref("");
1306
+ const hidQrImageUrl = ref("");
1307
+ const hidQrExpiresAt = ref("");
1308
+ const hidQrNow = ref(Date.now());
1309
+ const hidQrVisitor = ref<Record<string, any> | null>(null);
1310
+ const hidQrConfig = ref({ enabled: false, readerId: "", validityMinutes: 60 });
1311
+ let hidQrTimer: ReturnType<typeof setInterval> | null = null;
1312
+ const hidQrEnabled = computed(() => hidQrConfig.value.enabled && Boolean(hidQrConfig.value.readerId));
1313
+ const hidQrRemainingSeconds = computed(() => {
1314
+ const expiry = new Date(hidQrExpiresAt.value).getTime();
1315
+ return Number.isFinite(expiry) ? Math.max(0, Math.ceil((expiry - hidQrNow.value) / 1000)) : 0;
1316
+ });
1317
+ const hidQrExpired = computed(() => Boolean(hidQrExpiresAt.value) && hidQrRemainingSeconds.value === 0);
1318
+
1319
+ async function loadHidQrConfig() {
1320
+ try {
1321
+ const response: any = await getSiteById(siteId);
1322
+ const site = response?.data ?? response;
1323
+ const config = site?.metadata?.hidQrCodePass ?? {};
1324
+ hidQrConfig.value = {
1325
+ enabled: Boolean(config.enabled && config.onlineMode !== false),
1326
+ readerId: String(config.readerId || ""),
1327
+ validityMinutes: Number(config.validityMinutes) > 0 ? Number(config.validityMinutes) : 60,
1328
+ };
1329
+ } catch {
1330
+ hidQrConfig.value = { enabled: false, readerId: "", validityMinutes: 60 };
1331
+ }
1332
+ }
1333
+
1334
+ function extractIdentityItems(response: any) {
1335
+ return response?.items ?? response?.data?.items ?? [];
1336
+ }
1337
+
1338
+ async function openVisitorHidQr(visitor: Record<string, any>) {
1339
+ hidQrVisitor.value = visitor;
1340
+ hidQrDialog.value = true;
1341
+ hidQrLoading.value = true;
1342
+ hidQrError.value = "";
1343
+ hidQrImageUrl.value = "";
1344
+ hidQrExpiresAt.value = "";
1345
+ try {
1346
+ const response = await getIdentities(hidQrConfig.value.readerId, { type: "visitor", limit: 100 });
1347
+ const identity = extractIdentityItems(response).find(
1348
+ (item: any) => String(item.visitor || item.metadata?.visitor || "") === String(visitor._id),
1349
+ );
1350
+ const qrValue = String(identity?.metadata?.qrValue || identity?.cardNo || "");
1351
+ hidQrExpiresAt.value = String(identity?.metadata?.expiresAt || "");
1352
+ if (!qrValue || !hidQrExpiresAt.value) {
1353
+ hidQrError.value = "This visitor does not have an HID QR code yet.";
1354
+ return;
1355
+ }
1356
+ hidQrImageUrl.value = await QRCode.toDataURL(qrValue, { width: 360, margin: 2, errorCorrectionLevel: "M" });
1357
+ hidQrNow.value = Date.now();
1358
+ } catch (error: unknown) {
1359
+ hidQrError.value = errorConverter(error) || "Unable to load the visitor HID QR code.";
1360
+ } finally {
1361
+ hidQrLoading.value = false;
1362
+ }
1363
+ }
1364
+
1365
+ async function generateVisitorHidQr() {
1366
+ if (!hidQrVisitor.value?._id || !hidQrConfig.value.readerId) return;
1367
+ hidQrGenerating.value = true;
1368
+ hidQrError.value = "";
1369
+ try {
1370
+ const response = await issueVisitorQr(hidQrConfig.value.readerId, {
1371
+ visitorId: String(hidQrVisitor.value._id),
1372
+ validityMinutes: hidQrConfig.value.validityMinutes,
1373
+ });
1374
+ const credential = "data" in response ? response.data : response;
1375
+ hidQrImageUrl.value = await QRCode.toDataURL(credential.qrValue, { width: 360, margin: 2, errorCorrectionLevel: "M" });
1376
+ hidQrExpiresAt.value = credential.expiresAt;
1377
+ hidQrNow.value = Date.now();
1378
+ } catch (error: unknown) {
1379
+ hidQrError.value = errorConverter(error) || "Unable to generate the visitor HID QR code.";
1380
+ } finally {
1381
+ hidQrGenerating.value = false;
1382
+ }
1383
+ }
1384
+
1385
+ function formatHidQrExpiry(value: string) {
1386
+ const date = new Date(value);
1387
+ return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
1388
+ }
1389
+
1390
+ function formatHidQrCountdown(totalSeconds: number) {
1391
+ const safe = Math.max(0, totalSeconds);
1392
+ const hours = Math.floor(safe / 3600);
1393
+ const minutes = Math.floor((safe % 3600) / 60);
1394
+ const seconds = safe % 60;
1395
+ return [hours, minutes, seconds].map((part) => String(part).padStart(2, "0")).join(":");
1396
+ }
1397
+
1398
+ watch(hidQrDialog, (open) => {
1399
+ if (hidQrTimer) clearInterval(hidQrTimer);
1400
+ hidQrTimer = open ? setInterval(() => { hidQrNow.value = Date.now(); }, 1000) : null;
1401
+ });
1258
1402
 
1259
1403
  //filter states
1260
1404
  const searchInput = ref("");
@@ -1505,11 +1649,6 @@ const formattedFields = {
1505
1649
  attachments: "Attachments",
1506
1650
  } as const;
1507
1651
 
1508
- function filterTypeSelectionLabel() {
1509
- const length = filterTypes.value.length;
1510
- return `${length} selected ${length === 1 ? "type" : "types"}` as string;
1511
- }
1512
-
1513
1652
  const passStatusOptions = [
1514
1653
  { label: "Returned", value: "Returned" },
1515
1654
  { label: "Not Returned", value: "In Use" },
@@ -1601,7 +1740,6 @@ const {
1601
1740
  watch(getVisitorReq, (newData: any) => {
1602
1741
  if (newData) {
1603
1742
  items.value = newData.items ?? [];
1604
- // items.value = [{ _id: "testid", name: "John Doe", type: "Contractor", company: "ABC Corp", location: "Block A, Level 1, Unit 101", contact: "12345678", plateNumber: "SGX1234A", checkIn: new Date().toISOString(), checkOut: null, status: "registered", visitorPass: [{ _id: "1", keyId: "pass1", prefixAndName: "VIP Pass" }, { _id: "2", keyId: "pass2", prefixAndName: "VIP Pass 2" }], passKeys: [{ _id: "1", keyId: "key2", prefixAndName: "Master Key" }] }];
1605
1743
  pages.value = newData.pages ?? 0;
1606
1744
  pageRange.value = newData?.pageRange ?? "-- - -- of --";
1607
1745
  }
@@ -2347,7 +2485,14 @@ const syncVisitorFiltersFromRoute = () => {
2347
2485
  }
2348
2486
  };
2349
2487
 
2350
- onMounted(syncVisitorFiltersFromRoute);
2488
+ onMounted(() => {
2489
+ syncVisitorFiltersFromRoute();
2490
+ void loadHidQrConfig();
2491
+ });
2492
+
2493
+ onBeforeUnmount(() => {
2494
+ if (hidQrTimer) clearInterval(hidQrTimer);
2495
+ });
2351
2496
 
2352
2497
  watch(() => route.query, syncVisitorFiltersFromRoute, { deep: true });
2353
2498
 
@@ -2398,4 +2543,16 @@ watchEffect(async () => {
2398
2543
  });
2399
2544
  </script>
2400
2545
 
2401
- <style scoped></style>
2546
+ <style scoped>
2547
+ /* The width the type filter carried as `max-width="200"` on the v-select. */
2548
+ .visitor-type-filter {
2549
+ max-width: 200px;
2550
+ }
2551
+
2552
+ .visitor-hid-qr {
2553
+ display: inline-flex;
2554
+ padding: 12px;
2555
+ border: 1px solid #dce3e8;
2556
+ background: #fff;
2557
+ }
2558
+ </style>
@@ -3,26 +3,27 @@
3
3
  @after-enter="handleScanVisitorQRCode">
4
4
  <v-container class="d-flex justify-center" max-height="90vh">
5
5
  <!-- QR code reader container -->
6
- <v-card elevation="2" class="d-flex flex-column align-center pa-2 w-100 h-100">
7
- <v-toolbar>
8
- <v-card-title class="text-h5"> Scan QR Code </v-card-title>
9
- <v-spacer />
10
- <v-btn color="grey-darken-1" icon="mdi-close" @click="closeDialog()" />
11
- </v-toolbar>
12
-
13
- <div id="reader" class="d-flex justify-center align-center w-100 h-100"
14
- style="height: calc(100vh - 64px)">
15
- <!-- The QR code scanner box -->
16
- <!-- This is where the QR code scanner will be displayed -->
6
+ <v-card class="screen-modal qr-scanner">
7
+ <div class="qr-scanner__head">
8
+ <v-card-title>Scan QR Code</v-card-title>
9
+ <AppButton variant="icon" icon="mdi-close" @click="closeDialog()" />
17
10
  </div>
18
- <!-- Show Error Messages -->
19
- <div v-if="showNotFoundMessage" class="text-error mt-8">
20
- <p>{{ errorMessage }}</p>
11
+
12
+ <div class="screen-modal__body qr-scanner__body">
13
+ <div id="reader" class="qr-scanner__reader">
14
+ <!-- The QR code scanner box -->
15
+ <!-- This is where the QR code scanner will be displayed -->
16
+ </div>
17
+ <!-- Show Error Messages -->
18
+ <p v-if="showNotFoundMessage" class="qr-scanner__error">{{ errorMessage }}</p>
19
+ <!-- Switch camera button inside the reader box -->
20
+ <AppButton
21
+ icon="mdi-camera-switch"
22
+ class="qr-scanner__switch"
23
+ aria-label="Switch camera"
24
+ @click="switchCamera()"
25
+ />
21
26
  </div>
22
- <!-- Switch camera button inside the reader box -->
23
- <v-btn color="primary" icon class="mt-4" @click="switchCamera()">
24
- <v-icon icon="mdi-camera-switch" large />
25
- </v-btn>
26
27
  </v-card>
27
28
  </v-container>
28
29
  </v-dialog>
@@ -136,3 +137,49 @@ const stopScanner = () => {
136
137
  });
137
138
  };
138
139
  </script>
140
+
141
+ <style scoped>
142
+ /* The card used to be `w-100` inside the centring container; keep that width
143
+ or the dialog collapses to the width of the reader box. */
144
+ .qr-scanner {
145
+ width: 100%;
146
+ }
147
+
148
+ .qr-scanner__head {
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: space-between;
152
+ padding-right: 12px;
153
+ }
154
+
155
+ .qr-scanner__body {
156
+ display: flex;
157
+ flex-direction: column;
158
+ align-items: center;
159
+ gap: 16px;
160
+ padding: 4px 20px 20px;
161
+ }
162
+
163
+ /* The scanner paints its own video into this box; only the frame is ours. */
164
+ .qr-scanner__reader {
165
+ display: flex;
166
+ align-items: center;
167
+ justify-content: center;
168
+ width: 100%;
169
+ min-height: 320px;
170
+ border: 1px solid var(--border);
171
+ border-radius: var(--r-inner);
172
+ background: var(--thead);
173
+ }
174
+
175
+ .qr-scanner__error {
176
+ margin: 0;
177
+ font-size: var(--fs-cell);
178
+ color: var(--err);
179
+ }
180
+
181
+ .qr-scanner__switch {
182
+ width: var(--btn-h);
183
+ padding: 0;
184
+ }
185
+ </style>