@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
@@ -6,6 +6,8 @@ type HidReaderPayload = {
6
6
  username?: string;
7
7
  password?: string;
8
8
  deviceId?: string;
9
+ portalId?: number;
10
+ portalName?: string;
9
11
  monitorPath?: string;
10
12
  enabled?: boolean;
11
13
  status?: string;
@@ -28,6 +30,83 @@ type HidIdentityPayload = {
28
30
  metadata?: Record<string, any>;
29
31
  };
30
32
 
33
+ type HidVisitorQrData = {
34
+ visitorId: string;
35
+ readerId: string;
36
+ hidUserId: number;
37
+ qrValue: string;
38
+ qrFormat: "0";
39
+ portalId: number;
40
+ portalName: string;
41
+ readerName: string;
42
+ issuedAt: string;
43
+ expiresAt: string;
44
+ };
45
+
46
+ type HidVisitorQrResponse = HidVisitorQrData | { data: HidVisitorQrData };
47
+
48
+ type HidPermissionListResponse = {
49
+ items: THidPermissionCandidate[];
50
+ page: number;
51
+ pages: number;
52
+ pageRange: string;
53
+ limit: number;
54
+ };
55
+
56
+ type HidFacialEnrollmentResult = {
57
+ user_id?: number;
58
+ success?: boolean;
59
+ scores?: Record<string, unknown>;
60
+ errors?: Array<{ code?: number; message?: string }>;
61
+ };
62
+
63
+ export type HidPhysicalCardType = "pacs" | "csn";
64
+
65
+ export type HidPhysicalCard = {
66
+ id: string;
67
+ value: string;
68
+ userId: number;
69
+ cardType: HidPhysicalCardType | "unknown";
70
+ facilityCode?: number;
71
+ cardNumber?: number;
72
+ source?: "manual" | "reader" | "device";
73
+ enrolledAt?: string;
74
+ };
75
+
76
+ type HidPhysicalCardInput =
77
+ | { cardType: "pacs"; facilityCode: number; cardNumber: number }
78
+ | { cardType: "csn"; cardValue: string };
79
+
80
+ export type HidSipAccountData = {
81
+ orgId: string;
82
+ site: string;
83
+ userId: string;
84
+ extension: string;
85
+ username: string;
86
+ password: string;
87
+ sipAddress: string;
88
+ websocketUrl: string;
89
+ status: "active";
90
+ provisionedAt: string;
91
+ };
92
+
93
+ /**
94
+ * True only for the specific validation error an API that predates core #1878
95
+ * returns when the discover body carries `site`. Deliberately narrow: after
96
+ * #1878 `site` is required, so this message can no longer be produced and the
97
+ * fallback below can never hide a real authorization refusal.
98
+ */
99
+ export function isUnknownSiteFieldError(error: unknown): boolean {
100
+ const record = typeof error === "object" && error !== null
101
+ ? error as Record<string, unknown>
102
+ : {};
103
+ const data = typeof record.data === "object" && record.data !== null
104
+ ? record.data as Record<string, unknown>
105
+ : {};
106
+ const message = String(data.message || record.message || "");
107
+ return message.includes("\"site\"") && message.includes("not allowed");
108
+ }
109
+
31
110
  export default function useHidAmico() {
32
111
  const basePath = "/api/access-management/hid";
33
112
 
@@ -78,6 +157,16 @@ export default function useHidAmico() {
78
157
  });
79
158
  }
80
159
 
160
+ function issueVisitorQr(
161
+ readerId: string,
162
+ payload: { visitorId: string; validityMinutes: number },
163
+ ) {
164
+ return useNuxtApp().$api<HidVisitorQrResponse>(`${basePath}/readers/${readerId}/visitor-qr`, {
165
+ method: "POST",
166
+ body: payload,
167
+ });
168
+ }
169
+
81
170
  function getReaderConfiguration(readerId: string, payload: Record<string, string[]> = {}) {
82
171
  return useNuxtApp().$api<Record<string, any>>(`${basePath}/readers/${readerId}/configuration`, {
83
172
  method: "POST",
@@ -112,6 +201,127 @@ export default function useHidAmico() {
112
201
  });
113
202
  }
114
203
 
204
+ function setUserImage(
205
+ readerId: string,
206
+ hidUserId: string | number,
207
+ image: Blob,
208
+ options: { timestamp?: number; match?: boolean } = {},
209
+ ) {
210
+ return useNuxtApp().$api<{ data: HidFacialEnrollmentResult }>(
211
+ `${basePath}/readers/${readerId}/users/${hidUserId}/image`,
212
+ {
213
+ method: "PUT",
214
+ query: {
215
+ ...(options.timestamp ? { timestamp: options.timestamp } : {}),
216
+ match: options.match ?? true,
217
+ },
218
+ headers: { "Content-Type": "application/octet-stream" },
219
+ body: image,
220
+ },
221
+ );
222
+ }
223
+
224
+ function deleteUserImage(readerId: string, hidUserId: string | number) {
225
+ return useNuxtApp().$api<Record<string, unknown>>(
226
+ `${basePath}/readers/${readerId}/users/${hidUserId}/image`,
227
+ { method: "DELETE" },
228
+ );
229
+ }
230
+
231
+ function getUserCards(readerId: string, hidUserId: string | number) {
232
+ return useNuxtApp().$api<{
233
+ data: { readerId: string; hidUserId: number; cards: HidPhysicalCard[] };
234
+ }>(`${basePath}/readers/${readerId}/users/${hidUserId}/cards`, {
235
+ method: "GET",
236
+ });
237
+ }
238
+
239
+ function assignUserCard(
240
+ readerId: string,
241
+ hidUserId: string | number,
242
+ payload: HidPhysicalCardInput,
243
+ ) {
244
+ return useNuxtApp().$api<{ data: { card: HidPhysicalCard; verified: boolean } }>(
245
+ `${basePath}/readers/${readerId}/users/${hidUserId}/cards`,
246
+ { method: "POST", body: payload },
247
+ );
248
+ }
249
+
250
+ function enrollUserCard(
251
+ readerId: string,
252
+ hidUserId: string | number,
253
+ cardType: HidPhysicalCardType,
254
+ timeoutSeconds = 30,
255
+ ) {
256
+ return useNuxtApp().$api<{ data: { card: HidPhysicalCard; verified: boolean } }>(
257
+ `${basePath}/readers/${readerId}/users/${hidUserId}/cards/enroll`,
258
+ { method: "POST", body: { cardType, timeoutSeconds } },
259
+ );
260
+ }
261
+
262
+ function cancelUserCardEnrollment(readerId: string) {
263
+ return useNuxtApp().$api<{ data: { cancelled: boolean } }>(
264
+ `${basePath}/readers/${readerId}/cards/enrollment/cancel`,
265
+ { method: "POST" },
266
+ );
267
+ }
268
+
269
+ function deleteUserCard(
270
+ readerId: string,
271
+ hidUserId: string | number,
272
+ cardId: string | number,
273
+ ) {
274
+ return useNuxtApp().$api<{ data: { deleted: boolean } }>(
275
+ `${basePath}/readers/${readerId}/users/${hidUserId}/cards/${cardId}`,
276
+ { method: "DELETE" },
277
+ );
278
+ }
279
+
280
+ async function discoverReader(
281
+ payload: Pick<HidReaderPayload, "baseUrl" | "username" | "password" | "site">,
282
+ ) {
283
+ const post = (body: Record<string, unknown>) =>
284
+ useNuxtApp().$api<Record<string, unknown>>(`${basePath}/readers/discover`, {
285
+ method: "POST",
286
+ body,
287
+ });
288
+
289
+ try {
290
+ return await post(payload as Record<string, unknown>);
291
+ } catch (error: unknown) {
292
+ // The discover endpoint only started accepting (and requiring) `site` in
293
+ // core #1878. An API that predates it validates the body with Joi's
294
+ // default allowUnknown:false and answers `"site" is not allowed`, so the
295
+ // web layer would break the moment it shipped ahead of the API. Retry once
296
+ // without `site` so both API versions work.
297
+ // ponytail: remove this fallback once core #1878 is deployed everywhere.
298
+ if (!isUnknownSiteFieldError(error)) throw error;
299
+ const { site: _site, ...legacy } = payload;
300
+ return await post(legacy as Record<string, unknown>);
301
+ }
302
+ }
303
+
304
+ function getUserPinStatus(readerId: string, hidUserId: string | number) {
305
+ return useNuxtApp().$api<{ data: { pinEnrolled: boolean } }>(
306
+ `${basePath}/readers/${readerId}/users/${hidUserId}/pin`,
307
+ { method: "GET" },
308
+ );
309
+ }
310
+
311
+ function setUserPin(readerId: string, hidUserId: string | number, pin: string) {
312
+ return useNuxtApp().$api<{ data: { pinEnrolled: boolean } }>(
313
+ `${basePath}/readers/${readerId}/users/${hidUserId}/pin`,
314
+ { method: "PUT", body: { pin } },
315
+ );
316
+ }
317
+
318
+ function deleteUserPin(readerId: string, hidUserId: string | number) {
319
+ return useNuxtApp().$api<{ data: { pinEnrolled: boolean } }>(
320
+ `${basePath}/readers/${readerId}/users/${hidUserId}/pin`,
321
+ { method: "DELETE" },
322
+ );
323
+ }
324
+
115
325
  function createIdentity(readerId: string, payload: HidIdentityPayload) {
116
326
  return useNuxtApp().$api<Record<string, any>>(`${basePath}/readers/${readerId}/identities`, {
117
327
  method: "POST",
@@ -151,18 +361,73 @@ export default function useHidAmico() {
151
361
  });
152
362
  }
153
363
 
364
+ function getSitePermissions(siteId: string, orgId: string, readerId: string) {
365
+ return useNuxtApp().$api<{ data: THidSitePermissions }>(
366
+ `${basePath}/sites/${siteId}/permissions`,
367
+ { method: "GET", query: { orgId, readerId } },
368
+ );
369
+ }
370
+
371
+ function ensureSipAccount(siteId: string, orgId: string) {
372
+ return useNuxtApp().$api<{ data: HidSipAccountData }>(
373
+ `${basePath}/sites/${siteId}/intercom/sip-account`,
374
+ { method: "POST", body: { orgId } },
375
+ );
376
+ }
377
+
378
+ function getPermissionCandidates(
379
+ siteId: string,
380
+ query: {
381
+ orgId: string;
382
+ readerId: string;
383
+ category: THidPermissionCategory | "intercom";
384
+ search?: string;
385
+ page?: number;
386
+ limit?: number;
387
+ },
388
+ ) {
389
+ return useNuxtApp().$api<HidPermissionListResponse>(
390
+ `${basePath}/sites/${siteId}/permission-candidates`,
391
+ { method: "GET", query },
392
+ );
393
+ }
394
+
395
+ function updateSitePermissions(
396
+ siteId: string,
397
+ orgId: string,
398
+ readerId: string,
399
+ assignments: Array<Pick<THidPermissionAssignment, "subjectId" | "category" | "intercom">>,
400
+ ) {
401
+ return useNuxtApp().$api<{ data: THidSitePermissions; message: string }>(
402
+ `${basePath}/sites/${siteId}/permissions`,
403
+ { method: "PUT", body: { orgId, readerId, assignments } },
404
+ );
405
+ }
406
+
154
407
  return {
155
408
  getReaders,
409
+ discoverReader,
156
410
  createReader,
157
411
  updateReader,
158
412
  deleteReader,
159
413
  testReader,
160
414
  syncReader,
161
415
  runObjectOperation,
416
+ issueVisitorQr,
162
417
  getReaderConfiguration,
163
418
  setReaderConfiguration,
164
419
  getLogs,
165
420
  getUserImage,
421
+ setUserImage,
422
+ deleteUserImage,
423
+ getUserCards,
424
+ assignUserCard,
425
+ enrollUserCard,
426
+ cancelUserCardEnrollment,
427
+ deleteUserCard,
428
+ getUserPinStatus,
429
+ setUserPin,
430
+ deleteUserPin,
166
431
  getIdentities,
167
432
  createIdentity,
168
433
  updateIdentity,
@@ -170,5 +435,9 @@ export default function useHidAmico() {
170
435
  getIntercomStatus,
171
436
  makeIntercomCall,
172
437
  finalizeIntercomCall,
438
+ ensureSipAccount,
439
+ getSitePermissions,
440
+ getPermissionCandidates,
441
+ updateSitePermissions,
173
442
  };
174
443
  }
@@ -28,9 +28,9 @@ export default function useHidNavigation() {
28
28
  },
29
29
  },
30
30
  {
31
- title: "Administrator",
31
+ title: "HID Cards",
32
32
  route: {
33
- name: "org-site-access-mgmt-administrator",
33
+ name: "org-site-access-mgmt-hid-cards",
34
34
  params: { org, site },
35
35
  },
36
36
  },
@@ -41,13 +41,6 @@ export default function useHidNavigation() {
41
41
  params: { org, site },
42
42
  },
43
43
  },
44
- {
45
- title: "Identity Mapping",
46
- route: {
47
- name: "org-site-access-mgmt-identity-mapping",
48
- params: { org, site },
49
- },
50
- },
51
44
  {
52
45
  title: "Access Logs",
53
46
  route: {
@@ -1,8 +1,8 @@
1
1
  export default function useMember() {
2
2
  const members = useState<Array<TMember>>("members", () => []);
3
- const page = useState("page", () => 1);
4
- const pages = useState("pages", () => 0);
5
- const pageRange = useState("pageRange", () => "-- - -- of --");
3
+ const page = useState("memberPage", () => 1);
4
+ const pages = useState("memberPages", () => 0);
5
+ const pageRange = useState("memberPageRange", () => "-- - -- of --");
6
6
 
7
7
  const member = useState<TMember>("member", () => ({
8
8
  _id: "",
@@ -52,16 +52,24 @@ export function useNFCPatrolDailyReport(
52
52
  const report = ref<NFCPatrolSummaryReport | null>(null);
53
53
  const loading = ref(false);
54
54
  const notFound = ref(false);
55
+ /**
56
+ * A request that FAILED is not a route with no patrols. Without this the
57
+ * screen answered a 500 with "No patrol log found for this route", which is
58
+ * a statement about the data the server never made.
59
+ */
60
+ const failed = ref(false);
55
61
 
56
62
  async function fetchReport() {
57
63
  if (!site || !filters.date || !filters.route) {
58
64
  report.value = null;
59
65
  notFound.value = false;
66
+ failed.value = false;
60
67
  return;
61
68
  }
62
69
 
63
70
  loading.value = true;
64
71
  notFound.value = false;
72
+ failed.value = false;
65
73
 
66
74
  try {
67
75
  const [logsRes, siteInfo] = await Promise.all([
@@ -92,6 +100,10 @@ export function useNFCPatrolDailyReport(
92
100
  report.value = null;
93
101
  notFound.value = true;
94
102
  }
103
+ } catch {
104
+ report.value = null;
105
+ notFound.value = false;
106
+ failed.value = true;
95
107
  } finally {
96
108
  loading.value = false;
97
109
  }
@@ -101,6 +113,7 @@ export function useNFCPatrolDailyReport(
101
113
  report,
102
114
  loading,
103
115
  notFound,
116
+ failed,
104
117
  fetchReport,
105
118
  };
106
119
  }
@@ -2,11 +2,11 @@ import type {
2
2
  NFCPatrolReportFilters,
3
3
  NFCPatrolSummaryReport,
4
4
  NFCPatrolReportSummaryRow,
5
- NFCPatrolMonthlyReportRow,
6
5
  NFCPatrolMonthlyReport,
7
6
  } from "../types/nfc-patrol-report";
8
7
  import useNFCPatrolLog from "./useNFCPatrolLog";
9
8
  import useSite from "./useSite";
9
+ import { collectAllPages, mapLogsToMonthlyRows } from "../utils/nfc-patrol-report";
10
10
 
11
11
  interface NFCPatrolLogListResponse {
12
12
  items?: Record<string, any>[];
@@ -14,55 +14,6 @@ interface NFCPatrolLogListResponse {
14
14
  pageRange?: string;
15
15
  }
16
16
 
17
- const MONTHS = [
18
- "January",
19
- "February",
20
- "March",
21
- "April",
22
- "May",
23
- "June",
24
- "July",
25
- "August",
26
- "September",
27
- "October",
28
- "November",
29
- "December",
30
- ];
31
- function mapLogsToMonthlyRows(
32
- logs: Record<string, any>[],
33
- ): NFCPatrolMonthlyReportRow[] {
34
- const rows = MONTHS.map((month) => ({
35
- month,
36
- checked: 0,
37
- checkedPercentage: 0,
38
- missed: 0,
39
- missedPercentage: 0,
40
- }));
41
-
42
- logs.forEach((log) => {
43
- const monthIndex = new Date(log.date).getMonth();
44
-
45
- for (const checkpoint of log.checkPoints ?? []) {
46
- if (checkpoint.status === "Completed") {
47
- rows[monthIndex].checked++;
48
- } else if (checkpoint.status === "Skipped") {
49
- rows[monthIndex].missed++;
50
- }
51
- }
52
- });
53
-
54
- rows.forEach((row) => {
55
- const total = row.checked + row.missed;
56
-
57
- if (total > 0) {
58
- row.checkedPercentage = Math.round((row.checked / total) * 100);
59
- row.missedPercentage = Math.round((row.missed / total) * 100);
60
- }
61
- });
62
-
63
- return rows;
64
- }
65
-
66
17
  function buildAddress(address?: Record<string, string>) {
67
18
  if (!address) return undefined;
68
19
 
@@ -90,32 +41,42 @@ export function useNFCPatrolMonthlyReport(
90
41
  const report = ref<NFCPatrolMonthlyReport | null>(null);
91
42
  const loading = ref(false);
92
43
  const notFound = ref(false);
44
+ /**
45
+ * A request that FAILED is not a route with no patrols. Without this the
46
+ * screen answered a 500 with "No patrol log found for this route", which is
47
+ * a statement about the data the server never made.
48
+ */
49
+ const failed = ref(false);
93
50
 
94
51
  async function fetchReport() {
95
52
  if (!site || !filters.route) {
96
53
  report.value = null;
97
54
  notFound.value = false;
55
+ failed.value = false;
98
56
  return;
99
57
  }
100
58
 
101
59
  loading.value = true;
102
60
  notFound.value = false;
61
+ failed.value = false;
103
62
  const currentYear = new Date().getFullYear().toString();
104
63
  try {
105
- const [logsRes, siteInfo] = await Promise.all([
106
- getPatrolLogs({
107
- page: 1,
64
+ // The whole year, not page one of it. `collectAllPages` reads the page
65
+ // count out of the first reply and walks the rest.
66
+ const [logs, siteInfo] = await Promise.all([
67
+ collectAllPages((page) =>
68
+ getPatrolLogs({
69
+ page,
108
70
  limit: 100,
109
71
  site,
110
72
  routeId: filters.route,
111
73
  date: currentYear,
112
74
  type: "month",
113
- }),
75
+ }),
76
+ ),
114
77
  getSiteById(site),
115
78
  ]);
116
79
 
117
- const logs = logsRes.items ?? [];
118
-
119
80
  if (logs.length) {
120
81
  const routeName = logs[0]?.route?.name ?? "-";
121
82
 
@@ -131,6 +92,10 @@ export function useNFCPatrolMonthlyReport(
131
92
  report.value = null;
132
93
  notFound.value = true;
133
94
  }
95
+ } catch {
96
+ report.value = null;
97
+ notFound.value = false;
98
+ failed.value = true;
134
99
  } finally {
135
100
  loading.value = false;
136
101
  }
@@ -140,6 +105,7 @@ export function useNFCPatrolMonthlyReport(
140
105
  report,
141
106
  loading,
142
107
  notFound,
108
+ failed,
143
109
  fetchReport,
144
110
  };
145
111
  }
@@ -91,6 +91,12 @@ export function useNFCPatrolReport(
91
91
  const report = ref<NFCPatrolReport | null>(null);
92
92
  const loading = ref(false);
93
93
  const notFound = ref(false);
94
+ /**
95
+ * A request that FAILED is not a route with no patrols. Without this the
96
+ * screen answered a 500 with "No patrol log found for this route", which is
97
+ * a statement about the data the server never made.
98
+ */
99
+ const failed = ref(false);
94
100
  const selectedActivityRemarks = ref("");
95
101
  const dialogRemarks = ref(false);
96
102
 
@@ -98,11 +104,13 @@ export function useNFCPatrolReport(
98
104
  if (!site || !filters.date || !filters.route || !filters.timeRange) {
99
105
  report.value = null;
100
106
  notFound.value = false;
107
+ failed.value = false;
101
108
  return;
102
109
  }
103
110
 
104
111
  loading.value = true;
105
112
  notFound.value = false;
113
+ failed.value = false;
106
114
 
107
115
  try {
108
116
  const [logsRes, siteInfo] = await Promise.all([
@@ -128,6 +136,10 @@ export function useNFCPatrolReport(
128
136
  report.value = null;
129
137
  notFound.value = true;
130
138
  }
139
+ } catch {
140
+ report.value = null;
141
+ notFound.value = false;
142
+ failed.value = true;
131
143
  } finally {
132
144
  loading.value = false;
133
145
  }
@@ -165,6 +177,7 @@ export function useNFCPatrolReport(
165
177
  report,
166
178
  loading,
167
179
  notFound,
180
+ failed,
168
181
  selectedActivityRemarks,
169
182
  dialogRemarks,
170
183
  fetchReport,
@@ -0,0 +1,87 @@
1
+ /**
2
+ * A person's own notification settings.
3
+ *
4
+ * The server decides everything that matters here: which categories this person
5
+ * is offered, which channels each one can actually reach them on, and what the
6
+ * words are. Nothing on this side maintains a second list — a screen that made
7
+ * up its own module names is how the previous version came to show three
8
+ * switches that governed nothing.
9
+ *
10
+ * There is no user id in either call. Whose settings these are comes from the
11
+ * session, so a person can only ever see and change their own.
12
+ */
13
+
14
+ export type TNotificationChannelSetting = {
15
+ key: string;
16
+ label: string;
17
+ /** false = this category cannot reach them this way; drawn, dimmed, explained */
18
+ supported: boolean;
19
+ /** the plain-English reason, present only when unsupported */
20
+ note?: string;
21
+ enabled: boolean;
22
+ };
23
+
24
+ export type TNotificationCategorySetting = {
25
+ key: string;
26
+ label: string;
27
+ description: string;
28
+ safety: boolean;
29
+ channels: TNotificationChannelSetting[];
30
+ };
31
+
32
+ export type TNotificationPreferences = {
33
+ channels: Array<{ key: string; label: string }>;
34
+ categories: TNotificationCategorySetting[];
35
+ };
36
+
37
+ const EMPTY: TNotificationPreferences = { channels: [], categories: [] };
38
+
39
+ export default function useNotificationPreference() {
40
+ function getPreferences() {
41
+ return useNuxtApp().$api<{ data: TNotificationPreferences }>(
42
+ "/api/notification-preferences",
43
+ { method: "GET" },
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Save the whole screen at once.
49
+ *
50
+ * What is sent is the list of switches that are OFF, which is also how the
51
+ * server stores it: somebody who has changed nothing has no stored row at
52
+ * all, and behaves exactly as they did before this screen existed.
53
+ */
54
+ function savePreferences(preferences: TNotificationPreferences) {
55
+ const off = preferences.categories.flatMap((category) =>
56
+ category.channels
57
+ .filter((channel) => channel.supported && !channel.enabled)
58
+ .map((channel) => ({ category: category.key, channel: channel.key })),
59
+ );
60
+
61
+ return useNuxtApp().$api<{ data: TNotificationPreferences }>(
62
+ "/api/notification-preferences",
63
+ { method: "PUT", body: { off } },
64
+ );
65
+ }
66
+
67
+ return { getPreferences, savePreferences, EMPTY_PREFERENCES: EMPTY };
68
+ }
69
+
70
+ /**
71
+ * The sentence shown when somebody switches a safety or security alert off.
72
+ *
73
+ * It says what will stop arriving and how to undo it, and it does not argue —
74
+ * the owner's decision is that these can be switched off, so the job here is to
75
+ * make sure nobody does it by accident, not to talk them out of it.
76
+ */
77
+ export function safetyOffConfirmation(categoryLabel: string, channelLabel: string) {
78
+ return {
79
+ title: `Turn off ${categoryLabel}?`,
80
+ body:
81
+ `You will stop getting ${categoryLabel.toLowerCase()} alerts under "${channelLabel}". ` +
82
+ `These are safety and security alerts, so they are on for everyone by default. ` +
83
+ `You can turn them back on here at any time.`,
84
+ confirm: "Turn it off",
85
+ cancel: "Keep it on",
86
+ };
87
+ }
@@ -20,6 +20,8 @@ export type FormBlock =
20
20
  | { type: "checkbox-group"; title?: string; single?: boolean; items: CheckboxItemDef[] }
21
21
  | { type: "paragraph"; text: string }
22
22
  | { type: "section-header"; text: string }
23
+ | { type: "ordered-list"; title?: string; items: string[] }
24
+ | { type: "unordered-list"; title?: string; items: string[] }
23
25
  | { type: "divider" }
24
26
  | { type: "signature-row"; label: string; key: string }
25
27
  | { type: "acknowledgement"; text: string }