@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,975 +0,0 @@
1
- <template>
2
- <section class="hid-identity-mapping">
3
- <div class="toolbar">
4
- <v-select
5
- v-if="readers.length > 1"
6
- v-model="selectedReaderId"
7
- :items="readerOptions"
8
- item-title="title"
9
- item-value="value"
10
- variant="outlined"
11
- density="compact"
12
- hide-details
13
- class="reader-input"
14
- @update:model-value="loadIdentities"
15
- />
16
-
17
- <v-btn
18
- rounded="xl"
19
- class="text-none refresh-button"
20
- variant="flat"
21
- prepend-icon="mdi-refresh"
22
- :loading="loading"
23
- @click="loadIdentities"
24
- >
25
- Refresh
26
- </v-btn>
27
-
28
- <div class="toolbar-spacer" />
29
-
30
- <v-text-field
31
- v-model="search"
32
- placeholder="Search HID user or mapped user"
33
- prepend-inner-icon="mdi-magnify"
34
- variant="outlined"
35
- density="compact"
36
- hide-details
37
- class="search-input"
38
- />
39
-
40
- <v-select
41
- v-model="type"
42
- :items="typeOptions"
43
- item-title="title"
44
- item-value="value"
45
- variant="outlined"
46
- density="compact"
47
- hide-details
48
- class="type-input"
49
- @update:model-value="loadIdentities"
50
- />
51
- </div>
52
-
53
- <v-card flat border class="table-card">
54
- <v-tabs v-model="activeTab" class="mapping-tabs" density="compact">
55
- <v-tab value="all" class="text-none">All</v-tab>
56
- <v-tab value="mapped" class="text-none">Mapped</v-tab>
57
- <v-tab value="unmapped" class="text-none">Unmapped</v-tab>
58
- <v-tab value="review" class="text-none">Needs Review</v-tab>
59
- </v-tabs>
60
-
61
- <v-table>
62
- <thead>
63
- <tr>
64
- <th>HID User</th>
65
- <th>HID User ID</th>
66
- <th>Registration No.</th>
67
- <th>Access Method</th>
68
- <th>Mapped To</th>
69
- <th>Unit / Company</th>
70
- <th>Status</th>
71
- <th class="text-right"></th>
72
- </tr>
73
- </thead>
74
- <tbody>
75
- <tr v-for="identity in filteredIdentities" :key="identity._id">
76
- <td>{{ getIdentityName(identity) }}</td>
77
- <td>{{ identity.hidUserId || "N/A" }}</td>
78
- <td>{{ identity.registration || "N/A" }}</td>
79
- <td>{{ getAccessMethod(identity) }}</td>
80
- <td>{{ getMappedLabel(identity) }}</td>
81
- <td>{{ getUnitLabel(identity) }}</td>
82
- <td>
83
- <v-chip
84
- size="small"
85
- variant="flat"
86
- :class="isMapped(identity) ? 'mapped-chip' : 'unmapped-chip'"
87
- >
88
- {{ isMapped(identity) ? "Mapped" : "Unmapped" }}
89
- </v-chip>
90
- </td>
91
- <td class="text-right">
92
- <v-menu>
93
- <template #activator="{ props: menuProps }">
94
- <v-btn v-bind="menuProps" icon="mdi-dots-vertical" variant="text" density="comfortable" />
95
- </template>
96
- <v-list density="compact" min-width="160">
97
- <v-list-item @click="openMapDialog(identity)">
98
- <v-list-item-title>{{ isMapped(identity) ? "Edit Mapping" : "Map Identity" }}</v-list-item-title>
99
- </v-list-item>
100
- <v-list-item :disabled="!isMapped(identity)" @click="unmapIdentity(identity)">
101
- <v-list-item-title>Unmap</v-list-item-title>
102
- </v-list-item>
103
- </v-list>
104
- </v-menu>
105
- </td>
106
- </tr>
107
- </tbody>
108
- </v-table>
109
-
110
- <div v-if="!filteredIdentities.length" class="empty-state">
111
- No HID identities found.
112
- </div>
113
- </v-card>
114
-
115
- <v-dialog v-model="mapDialog" max-width="520" persistent>
116
- <v-card class="mapping-dialog" rounded="lg">
117
- <v-card-title class="dialog-title">
118
- {{ selectedIdentity && isMapped(selectedIdentity) ? "Edit Mapping" : "Map Identity" }}
119
- </v-card-title>
120
-
121
- <v-card-text class="dialog-body">
122
- <div class="identity-summary">
123
- <div>
124
- <span>HID User</span>
125
- <strong>{{ selectedIdentity ? getIdentityName(selectedIdentity) : "N/A" }}</strong>
126
- </div>
127
- <div>
128
- <span>HID User ID</span>
129
- <strong>{{ selectedIdentity?.hidUserId || "N/A" }}</strong>
130
- </div>
131
- </div>
132
-
133
- <div class="field-label">User Type <span>*</span></div>
134
- <v-select
135
- v-model="mappingForm.type"
136
- :items="mappingTypeOptions"
137
- item-title="title"
138
- item-value="value"
139
- density="compact"
140
- variant="outlined"
141
- hide-details="auto"
142
- class="mb-3"
143
- @update:model-value="onMappingTypeChange"
144
- />
145
-
146
- <div class="field-label">Search User <span>*</span></div>
147
- <v-autocomplete
148
- v-model="mappingForm.recordId"
149
- v-model:search="recordSearch"
150
- :items="recordOptions"
151
- item-title="title"
152
- item-value="value"
153
- density="compact"
154
- variant="outlined"
155
- hide-details="auto"
156
- placeholder="Search name, unit, company or email"
157
- :loading="recordLoading"
158
- no-filter
159
- class="mb-3"
160
- @update:search="debouncedSearchRecords"
161
- @update:model-value="onRecordSelect"
162
- />
163
-
164
- <div class="field-label">Unit / Company</div>
165
- <v-text-field
166
- v-model="mappingForm.unitLabel"
167
- density="compact"
168
- variant="outlined"
169
- hide-details="auto"
170
- placeholder="Auto filled when possible"
171
- class="mb-3"
172
- />
173
-
174
- <div class="field-label">Notes</div>
175
- <v-textarea
176
- v-model="mappingForm.notes"
177
- density="compact"
178
- variant="outlined"
179
- hide-details="auto"
180
- rows="3"
181
- placeholder="Optional notes"
182
- />
183
- </v-card-text>
184
-
185
- <v-card-actions class="dialog-actions pa-0">
186
- <v-btn class="text-none action-cancel" variant="flat" height="48" @click="mapDialog = false">
187
- Cancel
188
- </v-btn>
189
- <v-btn
190
- class="text-none action-submit"
191
- variant="flat"
192
- height="48"
193
- :loading="saving"
194
- :disabled="!mappingForm.recordId"
195
- @click="saveMapping"
196
- >
197
- Save
198
- </v-btn>
199
- </v-card-actions>
200
- </v-card>
201
- </v-dialog>
202
-
203
- <v-snackbar v-model="snackbar.show" :color="snackbar.color" timeout="2600">
204
- {{ snackbar.message }}
205
- </v-snackbar>
206
- </section>
207
- </template>
208
-
209
- <script setup lang="ts">
210
- const props = defineProps({
211
- site: {
212
- type: String,
213
- required: true,
214
- },
215
- org: {
216
- type: String,
217
- default: "",
218
- },
219
- });
220
-
221
- type MappingType = "resident" | "visitor" | "staff";
222
-
223
- const { getReaders, getIdentities, runObjectOperation, createIdentity, updateIdentity } = useHidAmico();
224
- const { getAll: getMembers } = useMember();
225
- const { getVisitors } = useVisitor();
226
- const { getUsers } = useUser();
227
-
228
- const readers = ref<Record<string, any>[]>([]);
229
- const identities = ref<Record<string, any>[]>([]);
230
- const selectedReaderId = ref("");
231
- const activeTab = ref("all");
232
- const search = ref("");
233
- const type = ref("all");
234
- const loading = ref(false);
235
- const mapDialog = ref(false);
236
- const selectedIdentity = ref<Record<string, any>>();
237
- const recordSearch = ref("");
238
- const recordOptions = ref<Array<{ title: string; value: string; raw: Record<string, any> }>>([]);
239
- const recordLoading = ref(false);
240
- const saving = ref(false);
241
- let searchTimer: ReturnType<typeof setTimeout> | undefined;
242
-
243
- const snackbar = reactive({
244
- show: false,
245
- message: "",
246
- color: "success",
247
- });
248
-
249
- const mappingForm = reactive({
250
- type: "resident" as MappingType,
251
- recordId: "",
252
- unitLabel: "",
253
- notes: "",
254
- record: undefined as Record<string, any> | undefined,
255
- });
256
-
257
- const typeOptions = [
258
- { title: "All Types", value: "all" },
259
- { title: "Resident", value: "resident" },
260
- { title: "Visitor", value: "visitor" },
261
- { title: "Administrator", value: "staff" },
262
- ];
263
-
264
- const mappingTypeOptions = [
265
- { title: "Resident", value: "resident" },
266
- { title: "Visitor", value: "visitor" },
267
- { title: "Administrator", value: "staff" },
268
- ];
269
-
270
- const readerOptions = computed(() =>
271
- readers.value.map((reader) => ({
272
- title: reader.name || reader.deviceId || reader._id,
273
- value: reader._id,
274
- })),
275
- );
276
-
277
- const filteredIdentities = computed(() => {
278
- const query = search.value.trim().toLowerCase();
279
- return identities.value.filter((identity) => {
280
- const mapped = isMapped(identity);
281
- const matchesTab =
282
- activeTab.value === "all" ||
283
- (activeTab.value === "mapped" && mapped) ||
284
- (activeTab.value === "unmapped" && !mapped) ||
285
- (activeTab.value === "review" && !mapped);
286
- const matchesSearch = !query || [
287
- getIdentityName(identity),
288
- identity.hidUserId,
289
- identity.registration,
290
- identity.cardNo,
291
- getMappedLabel(identity),
292
- getUnitLabel(identity),
293
- getAccessMethod(identity),
294
- ].some((value) => String(value ?? "").toLowerCase().includes(query));
295
-
296
- return matchesTab && matchesSearch;
297
- });
298
- });
299
-
300
- watch(
301
- () => props.site,
302
- () => init(),
303
- { immediate: true },
304
- );
305
-
306
- async function init() {
307
- await loadReaders();
308
- await loadIdentities();
309
- }
310
-
311
- async function loadReaders() {
312
- if (!props.site) return;
313
- const response = await getReaders({ site: props.site, page: 1, limit: 100 });
314
- readers.value = normalizeItems(response, "readers");
315
- if (!selectedReaderId.value && readers.value.length) {
316
- selectedReaderId.value = readers.value[0]._id;
317
- }
318
- }
319
-
320
- async function loadIdentities() {
321
- if (!selectedReaderId.value) {
322
- identities.value = [];
323
- return;
324
- }
325
-
326
- loading.value = true;
327
- try {
328
- const [identityResponse, readerResponse] = await Promise.all([
329
- getIdentities(selectedReaderId.value, {
330
- page: 1,
331
- limit: 500,
332
- status: "active",
333
- }),
334
- runObjectOperation(selectedReaderId.value, {
335
- operation: "load",
336
- object: "users",
337
- limit: 500,
338
- offset: 0,
339
- }),
340
- ]);
341
- const readerUsers = normalizeHidUsers(readerResponse);
342
- let localIdentities = normalizeItems(identityResponse, "identities");
343
- const localUserIds = new Set(
344
- localIdentities
345
- .map((identity: Record<string, any>) => toHidNumericId(identity.hidUserId))
346
- .filter((hidUserId): hidUserId is number => Boolean(hidUserId)),
347
- );
348
- const deviceOnlyUsers = readerUsers.filter((user) => !localUserIds.has(user.hidUserId));
349
-
350
- if (deviceOnlyUsers.length) {
351
- await Promise.all(deviceOnlyUsers.map(async (user) => {
352
- try {
353
- await createIdentity(selectedReaderId.value, {
354
- site: props.site,
355
- hidUserId: String(user.hidUserId),
356
- registration: user.registration,
357
- cardNo: user.cardNo,
358
- type: "resident",
359
- status: "active",
360
- metadata: {
361
- name: user.name,
362
- hidImported: true,
363
- importedAt: new Date().toISOString(),
364
- },
365
- });
366
- } catch {
367
- // A parallel sync may have already created the identity.
368
- }
369
- }));
370
-
371
- const refreshedResponse = await getIdentities(selectedReaderId.value, {
372
- page: 1,
373
- limit: 500,
374
- status: "active",
375
- });
376
- localIdentities = normalizeItems(refreshedResponse, "identities");
377
- }
378
-
379
- const identityByUserId = new Map<number, Record<string, any>>();
380
- localIdentities.forEach((identity: Record<string, any>) => {
381
- const hidUserId = toHidNumericId(identity.hidUserId);
382
- if (hidUserId) identityByUserId.set(hidUserId, identity);
383
- });
384
-
385
- identities.value = readerUsers
386
- .map((user) => {
387
- const identity = identityByUserId.get(user.hidUserId);
388
- if (!identity) return undefined;
389
- return {
390
- ...identity,
391
- hidUserId: String(user.hidUserId),
392
- registration: user.registration || identity.registration,
393
- cardNo: user.cardNo || identity.cardNo,
394
- metadata: {
395
- ...identity.metadata,
396
- name: identity.metadata?.name || user.name,
397
- hidReaderSource: true,
398
- },
399
- };
400
- })
401
- .filter(Boolean)
402
- .filter((identity: Record<string, any>) => type.value === "all" || identity.type === type.value);
403
- } finally {
404
- loading.value = false;
405
- }
406
- }
407
-
408
- function normalizeHidUsers(response: Record<string, any>) {
409
- const data = response?.data || response || {};
410
- const users = data?.users || data?.data?.users || data?.result?.users || [];
411
- return (Array.isArray(users) ? users : [])
412
- .map((user: Record<string, any>) => ({
413
- hidUserId: toHidNumericId(user.id),
414
- registration: String(user.registration || ""),
415
- cardNo: String(user.card_value || user.cardNo || ""),
416
- name: String(user.name || ""),
417
- }))
418
- .filter((user) => Boolean(user.hidUserId));
419
- }
420
-
421
- function toHidNumericId(value: unknown) {
422
- const raw = String(value ?? "").trim();
423
- if (!raw) return undefined;
424
- const numeric = Number(raw);
425
- if (Number.isInteger(numeric) && numeric > 0) return numeric;
426
- const match = raw.match(/(\d+)$/);
427
- const parsed = match ? Number(match[1]) : NaN;
428
- return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
429
- }
430
-
431
- function normalizeItems(response: any, key: string) {
432
- const candidates = [
433
- response?.items,
434
- response?.[key],
435
- response?.data?.items,
436
- response?.data?.[key],
437
- response?.data?.data?.items,
438
- response?.data?.data?.[key],
439
- response?.data?.results,
440
- response?.results,
441
- response?.data?.data,
442
- response?.data,
443
- ];
444
-
445
- return candidates.find((items) => Array.isArray(items)) ?? [];
446
- }
447
-
448
- function isMapped(identity: Record<string, any>) {
449
- return Boolean(identity.person || identity.user || identity.member || identity.visitor || identity.metadata?.mappedRecordId);
450
- }
451
-
452
- function getIdentityName(identity: Record<string, any>) {
453
- return identity.metadata?.name || identity.name || identity.metadata?.hidName || "N/A";
454
- }
455
-
456
- function getMappedLabel(identity: Record<string, any>) {
457
- return identity.metadata?.mappedName || identity.metadata?.personName || identity.metadata?.userName || "Unmapped";
458
- }
459
-
460
- function getUnitLabel(identity: Record<string, any>) {
461
- return identity.metadata?.unitLabel || identity.metadata?.company || [identity.metadata?.block, identity.metadata?.level, identity.metadata?.unit].filter(Boolean).join("/") || "N/A";
462
- }
463
-
464
- function getAccessMethod(identity: Record<string, any>) {
465
- if (identity.cardNo) return "Card";
466
- if (identity.metadata?.qrCode || identity.metadata?.qrcode) return "QR Code";
467
- if (identity.metadata?.panicPin || identity.metadata?.pin) return "PIN";
468
- if (identity.metadata?.pinPassword) return "ID and Password";
469
- if (identity.metadata?.photo || identity.metadata?.facialData) return "Facial";
470
- return "N/A";
471
- }
472
-
473
- function openMapDialog(identity: Record<string, any>) {
474
- selectedIdentity.value = identity;
475
- mappingForm.type = normalizeIdentityType(identity.type);
476
- mappingForm.recordId = identity.metadata?.mappedRecordId || String(identity.member || identity.user || identity.person || identity.visitor || "");
477
- mappingForm.unitLabel = getUnitLabel(identity) === "N/A" ? "" : getUnitLabel(identity);
478
- mappingForm.notes = identity.metadata?.mappingNotes || "";
479
- mappingForm.record = undefined;
480
- recordSearch.value = "";
481
- recordOptions.value = [];
482
- mapDialog.value = true;
483
- searchRecords();
484
- }
485
-
486
- function normalizeIdentityType(value: string): MappingType {
487
- if (value === "visitor") return "visitor";
488
- if (value === "staff") return "staff";
489
- return "resident";
490
- }
491
-
492
- function onMappingTypeChange() {
493
- mappingForm.recordId = "";
494
- mappingForm.record = undefined;
495
- recordOptions.value = [];
496
- searchRecords();
497
- }
498
-
499
- function debouncedSearchRecords() {
500
- if (searchTimer) clearTimeout(searchTimer);
501
- searchTimer = setTimeout(() => searchRecords(), 300);
502
- }
503
-
504
- async function searchRecords() {
505
- recordLoading.value = true;
506
- try {
507
- if (mappingForm.type === "visitor") {
508
- const response = await getVisitors({
509
- site: props.site,
510
- search: recordSearch.value,
511
- page: 1,
512
- limit: 20,
513
- order: "desc",
514
- });
515
- recordOptions.value = normalizeItems(response, "items").map((record: Record<string, any>) =>
516
- toRecordOption(record, "visitor"),
517
- );
518
- return;
519
- }
520
-
521
- let records: Record<string, any>[] = [];
522
-
523
- if (mappingForm.type === "staff") {
524
- const response = await getMembers({
525
- page: 1,
526
- limit: 20,
527
- search: recordSearch.value,
528
- org: props.org,
529
- type: "property_management_agency",
530
- status: "active",
531
- });
532
- records = normalizeItems(response, "members");
533
- }
534
-
535
- if (!records.length) {
536
- const usersResponse = await getUsers({
537
- status: "active",
538
- search: recordSearch.value,
539
- page: 1,
540
- });
541
- records = normalizeItems(usersResponse, "users").map((user: Record<string, any>) => ({
542
- ...user,
543
- __mappingSource: "user",
544
- }));
545
- }
546
-
547
- recordOptions.value = records.map((record: Record<string, any>) =>
548
- toRecordOption(record, record.__mappingSource || "member"),
549
- );
550
- } catch {
551
- recordOptions.value = [];
552
- } finally {
553
- recordLoading.value = false;
554
- }
555
- }
556
-
557
- function toRecordOption(record: Record<string, any>, source = "member") {
558
- const id = getRecordId(record, source);
559
- const name = getRecordName(record);
560
- const unit = getRecordUnit(record);
561
- return {
562
- title: unit && unit !== "N/A" ? `${name} - ${unit}` : name,
563
- value: id,
564
- raw: {
565
- ...record,
566
- __mappingSource: source,
567
- },
568
- };
569
- }
570
-
571
- function getRecordId(record: Record<string, any>, source = "member") {
572
- if (source === "user") {
573
- return String(record._id || record.id || record.user?._id || record.user?.id || record.user || "");
574
- }
575
-
576
- return String(
577
- record._id ||
578
- record.id ||
579
- record.member?._id ||
580
- record.member?.id ||
581
- record.member ||
582
- record.memberId ||
583
- record.user?._id ||
584
- record.user?.id ||
585
- record.user ||
586
- "",
587
- );
588
- }
589
-
590
- function getRecordName(record: Record<string, any>) {
591
- return (
592
- record.name ||
593
- record.userName ||
594
- record.user?.name ||
595
- record.memberName ||
596
- record.fullName ||
597
- record.firstName ||
598
- record.email ||
599
- record.user?.email ||
600
- "Unnamed"
601
- );
602
- }
603
-
604
- function getRecordUnit(record: Record<string, any>) {
605
- return (
606
- record.unitName ||
607
- record.unitLabel ||
608
- record.unit?.name ||
609
- record.unitNo ||
610
- record.company ||
611
- record.customerSiteName ||
612
- record.siteName ||
613
- record.roleName ||
614
- record.orgName ||
615
- "N/A"
616
- );
617
- }
618
-
619
- function onRecordSelect(value: string) {
620
- const option = recordOptions.value.find((item) => item.value === value);
621
- mappingForm.record = option?.raw;
622
- if (option?.raw) {
623
- mappingForm.unitLabel = getRecordUnit(option.raw) === "N/A" ? "" : getRecordUnit(option.raw);
624
- }
625
- }
626
-
627
- async function saveMapping() {
628
- if (!selectedIdentity.value || !mappingForm.recordId) return;
629
-
630
- saving.value = true;
631
- try {
632
- const record = mappingForm.record || recordOptions.value.find((item) => item.value === mappingForm.recordId)?.raw || {};
633
- const payload: Record<string, any> = {
634
- type: mappingForm.type,
635
- metadata: {
636
- ...(selectedIdentity.value.metadata || {}),
637
- mappedRecordId: mappingForm.recordId,
638
- mappedName: getRecordName(record),
639
- unitLabel: mappingForm.unitLabel,
640
- mappingNotes: mappingForm.notes,
641
- mappedAt: new Date().toISOString(),
642
- },
643
- };
644
-
645
- if (mappingForm.type === "visitor") {
646
- payload.visitor = mappingForm.recordId;
647
- payload.member = "";
648
- payload.user = "";
649
- payload.person = "";
650
- } else if (mappingForm.type === "staff") {
651
- payload.user = getRecordUserId(record);
652
- payload.member = getRecordMemberId(record);
653
- payload.visitor = "";
654
- payload.person = "";
655
- } else {
656
- payload.member = getRecordMemberId(record) || mappingForm.recordId;
657
- payload.user = getRecordUserId(record);
658
- payload.visitor = "";
659
- payload.person = "";
660
- }
661
-
662
- await updateIdentity(selectedIdentity.value._id, payload);
663
- showMessage("Identity mapped successfully.", "success");
664
- mapDialog.value = false;
665
- await loadIdentities();
666
- } catch (error: any) {
667
- showMessage(error?.data?.message || error?.message || "Unable to save mapping.", "error");
668
- } finally {
669
- saving.value = false;
670
- }
671
- }
672
-
673
- async function unmapIdentity(identity: Record<string, any>) {
674
- try {
675
- await updateIdentity(identity._id, {
676
- member: "",
677
- user: "",
678
- person: "",
679
- visitor: "",
680
- metadata: {
681
- ...(identity.metadata || {}),
682
- mappedRecordId: "",
683
- mappedName: "",
684
- mappingNotes: "",
685
- unmappedAt: new Date().toISOString(),
686
- },
687
- });
688
- showMessage("Identity unmapped successfully.", "success");
689
- await loadIdentities();
690
- } catch (error: any) {
691
- showMessage(error?.data?.message || error?.message || "Unable to unmap identity.", "error");
692
- }
693
- }
694
-
695
- function getRecordUserId(record: Record<string, any>) {
696
- if (record.__mappingSource === "user") {
697
- return String(record._id || record.id || "");
698
- }
699
-
700
- const user = record.user;
701
- if (typeof user === "object" && user) {
702
- return String(user._id || user.id || "");
703
- }
704
-
705
- return String(user || "");
706
- }
707
-
708
- function getRecordMemberId(record: Record<string, any>) {
709
- if (record.__mappingSource === "user") return "";
710
-
711
- const member = record.member;
712
- if (typeof member === "object" && member) {
713
- return String(member._id || member.id || "");
714
- }
715
-
716
- return String(record._id || record.id || record.memberId || member || "");
717
- }
718
-
719
- function showMessage(message: string, color: string) {
720
- snackbar.message = message;
721
- snackbar.color = color;
722
- snackbar.show = true;
723
- }
724
- </script>
725
-
726
- <style scoped>
727
- .hid-identity-mapping {
728
- padding: 24px 0;
729
- width: 100%;
730
- min-width: 0;
731
- }
732
-
733
- .toolbar {
734
- display: flex;
735
- flex-wrap: wrap;
736
- gap: 12px;
737
- align-items: center;
738
- margin-bottom: 16px;
739
- }
740
-
741
- .toolbar-spacer {
742
- flex: 1;
743
- }
744
-
745
- .reader-input,
746
- .search-input {
747
- max-width: 280px;
748
- }
749
-
750
- .type-input {
751
- max-width: 170px;
752
- }
753
-
754
- .refresh-button {
755
- min-width: 130px;
756
- background: #e4e4e4;
757
- color: #0a2638;
758
- }
759
-
760
- .table-card {
761
- min-height: 430px;
762
- overflow-x: auto;
763
- overflow-y: hidden;
764
- }
765
-
766
- .table-card :deep(table) {
767
- table-layout: fixed;
768
- width: 100%;
769
- min-width: 1040px;
770
- }
771
-
772
- .table-card :deep(th),
773
- .table-card :deep(td) {
774
- white-space: nowrap;
775
- }
776
-
777
- .table-card :deep(th:nth-child(1)),
778
- .table-card :deep(td:nth-child(1)) {
779
- width: 16%;
780
- }
781
-
782
- .table-card :deep(th:nth-child(2)),
783
- .table-card :deep(td:nth-child(2)) {
784
- width: 12%;
785
- }
786
-
787
- .table-card :deep(th:nth-child(3)),
788
- .table-card :deep(td:nth-child(3)) {
789
- width: 14%;
790
- }
791
-
792
- .table-card :deep(th:nth-child(4)),
793
- .table-card :deep(td:nth-child(4)) {
794
- width: 14%;
795
- }
796
-
797
- .table-card :deep(th:nth-child(5)),
798
- .table-card :deep(td:nth-child(5)) {
799
- width: 16%;
800
- }
801
-
802
- .table-card :deep(th:nth-child(6)),
803
- .table-card :deep(td:nth-child(6)) {
804
- width: 14%;
805
- }
806
-
807
- .table-card :deep(th:nth-child(7)),
808
- .table-card :deep(td:nth-child(7)) {
809
- width: 10%;
810
- }
811
-
812
- .table-card :deep(th:nth-child(8)),
813
- .table-card :deep(td:nth-child(8)) {
814
- width: 4%;
815
- }
816
-
817
- .mapping-tabs {
818
- border-bottom: 1px solid #eeeeee;
819
- }
820
-
821
- .mapping-tabs :deep(.v-slide-group__content) {
822
- display: grid;
823
- grid-template-columns: repeat(4, minmax(0, 1fr));
824
- width: 100%;
825
- }
826
-
827
- .mapping-tabs :deep(.v-tab) {
828
- flex: none;
829
- min-width: 0;
830
- width: 100%;
831
- max-width: none;
832
- color: #0a2638;
833
- font-size: 12px;
834
- font-weight: 600;
835
- }
836
-
837
- .mapping-tabs :deep(.v-tab--selected) {
838
- font-weight: 600;
839
- }
840
-
841
- .mapped-chip,
842
- .unmapped-chip {
843
- min-width: 96px;
844
- justify-content: center;
845
- font-weight: 600;
846
- }
847
-
848
- .mapped-chip {
849
- background: #dff7e7 !important;
850
- color: #1b7f3a !important;
851
- }
852
-
853
- .unmapped-chip {
854
- background: #fff0cc !important;
855
- color: #9a6700 !important;
856
- }
857
-
858
- .empty-state {
859
- min-height: 280px;
860
- display: grid;
861
- place-items: center;
862
- color: #687382;
863
- font-size: 13px;
864
- }
865
-
866
- .mapping-dialog {
867
- overflow: hidden;
868
- }
869
-
870
- .dialog-title {
871
- font-size: 14px;
872
- font-weight: 700;
873
- padding: 12px 16px;
874
- border-bottom: 1px solid #ececec;
875
- }
876
-
877
- .dialog-body {
878
- padding: 18px 16px 20px;
879
- }
880
-
881
- .identity-summary {
882
- display: grid;
883
- grid-template-columns: 1fr 1fr;
884
- gap: 12px;
885
- padding: 12px;
886
- margin-bottom: 16px;
887
- background: #f5f7f9;
888
- border: 1px solid #e2e6ea;
889
- }
890
-
891
- .identity-summary span {
892
- display: block;
893
- margin-bottom: 4px;
894
- color: #687382;
895
- font-size: 11px;
896
- }
897
-
898
- .identity-summary strong {
899
- color: #0a2638;
900
- font-size: 13px;
901
- }
902
-
903
- .field-label {
904
- color: #4f5a66;
905
- font-size: 12px;
906
- font-weight: 600;
907
- margin-bottom: 6px;
908
- }
909
-
910
- .field-label span {
911
- color: #e53935;
912
- }
913
-
914
- .dialog-actions {
915
- display: grid;
916
- grid-template-columns: 1fr 1fr;
917
- }
918
-
919
- .action-cancel {
920
- background: #ffffff;
921
- color: #111827;
922
- border-radius: 0;
923
- }
924
-
925
- .action-submit {
926
- background: #062d42;
927
- color: #ffffff;
928
- border-radius: 0;
929
- }
930
-
931
- @media (max-width: 960px) {
932
- .toolbar {
933
- align-items: stretch;
934
- }
935
-
936
- .toolbar-spacer {
937
- display: none;
938
- }
939
-
940
- .reader-input,
941
- .search-input,
942
- .type-input,
943
- .refresh-button {
944
- max-width: none;
945
- width: 100%;
946
- }
947
-
948
- .table-card {
949
- min-height: 360px;
950
- }
951
- }
952
-
953
- @media (max-width: 600px) {
954
- .hid-identity-mapping {
955
- padding: 12px 0;
956
- }
957
-
958
- .mapping-tabs :deep(.v-slide-group__content) {
959
- display: flex;
960
- }
961
-
962
- .mapping-tabs :deep(.v-tab) {
963
- min-width: 120px;
964
- }
965
-
966
- .identity-summary {
967
- grid-template-columns: 1fr;
968
- }
969
-
970
- :global(.v-dialog > .v-overlay__content) {
971
- max-width: calc(100vw - 24px) !important;
972
- margin: 12px !important;
973
- }
974
- }
975
- </style>