@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,6 +1,11 @@
1
1
  <template>
2
2
  <v-row no-gutters>
3
- <TableMain :headers="headers" :items="items" :loading="loading" :page="page" :pages="pages" :pageRange="pageRange"
3
+ <!--
4
+ The screen had no heading at all - the page name lived only in the
5
+ navigation rail. `TableMain` draws the design's 22px/800 title row with
6
+ the create button on that same line, so the title is all this needs.
7
+ -->
8
+ <TableMain title="Overnight Parking" :headers="headers" :items="items" :loading="loading" :page="page" :pages="pages" :pageRange="pageRange"
4
9
  :createLabel="createLabel" :show-header="true" :extension-height="extensionHeight" :offset="offset"
5
10
  v-model:search="searchInput" @refresh="emits('refresh')" @create="emits('create')" @row-click="handleRowClick"
6
11
  @update:page="handleUpdatePage">
@@ -37,25 +42,44 @@
37
42
  {{ formatDateDDMMYYYYLocal(value) }}
38
43
  </template>
39
44
 
45
+ <!--
46
+ The column had no slot, so an OBJECT `invitedBy` printed its raw JSON
47
+ into the cell - `{ "_id": "u-1", "name": "Unit A-12-01" }`. Never seen,
48
+ because the table had never drawn a row. `types/overnight-parking.d.ts`
49
+ declares `{ _id, name }`; every OTHER collection in `core` stores
50
+ `invitedBy` as a plain id/name string, and the overnight parking model
51
+ has no such field at all - so both shapes are read rather than betting
52
+ on one, and an absent value says N/A instead of nothing.
53
+ -->
54
+ <template #item.invitedBy="{ value }">
55
+ {{ (typeof value === "object" ? value?.name : value) || "N/A" }}
56
+ </template>
57
+
58
+ <!--
59
+ The same four words, the same four meanings - approved/expired/
60
+ rejected/pending map to ok/neutral/err/warn in `utils/status.ts`,
61
+ which is the exact pairing `formatStatus` already made. The shared
62
+ chip's tones are the measured ones, so the label reads at AA in both
63
+ themes; a `flat` `v-chip` painted a Vuetify colour does not.
64
+ -->
40
65
  <template #item.status="{ value }">
41
- <v-chip class="text-capitalize" :color="formatStatus(value).color" density="compact" variant="flat" pill>
42
- {{ formatStatus(value).label }}
43
- </v-chip>
66
+ <StatusChip :status="value" :label="formatStatus(value).label" />
44
67
  </template>
45
68
 
46
69
  <template #item.action="{ item }">
47
70
  <v-menu v-if="item?.status === 'pending'" activator="parent" transition="scale-transition">
48
71
  <template #activator="{ props }">
49
- <v-btn variant="flat" density="compact" icon="mdi-dots-vertical" v-bind="props"></v-btn>
72
+ <v-btn variant="flat" density="compact" class="screen-kebab" icon="mdi-dots-vertical"
73
+ v-bind="props"></v-btn>
50
74
  </template>
51
75
  <v-list>
52
76
  <v-list-item @click="openActionDialog(item, 'approved')">
53
77
  <v-list-item-title>
54
- <v-icon class="text-success mr-2 text-subtitle-1">mdi-check</v-icon>Approve</v-list-item-title>
78
+ <v-icon class="text-success mr-2" size="16">mdi-check</v-icon>Approve</v-list-item-title>
55
79
  </v-list-item>
56
80
  <v-list-item @click="openActionDialog(item, 'rejected')">
57
81
  <v-list-item-title>
58
- <v-icon class="text-error mr-2 text-subtitle-1">mdi-close</v-icon>Reject</v-list-item-title>
82
+ <v-icon class="text-error mr-2" size="16">mdi-close</v-icon>Reject</v-list-item-title>
59
83
  </v-list-item>
60
84
  </v-list>
61
85
  </v-menu>
@@ -84,7 +108,7 @@
84
108
 
85
109
  <v-dialog v-model="actionDialog.open" max-width="520" persistent>
86
110
  <v-card>
87
- <v-card-title class="text-subtitle-1 font-weight-bold pt-4 px-4">
111
+ <v-card-title class="screen-card-title pt-4 px-4">
88
112
  {{ actionDialog.action === 'approved' ? 'Approve Request' : 'Reject Request' }}
89
113
  </v-card-title>
90
114
  <v-card-text class="px-4 pb-2">
@@ -96,14 +120,24 @@
96
120
  </v-card-text>
97
121
  <v-card-actions class="px-4 pb-4">
98
122
  <v-spacer />
99
- <v-btn variant="text" :disabled="actionLoading" @click="closeActionDialog">Cancel</v-btn>
100
- <v-btn color="primary" variant="flat" :loading="actionLoading" :disabled="!remarks.trim()"
101
- @click="confirmAction">
123
+ <v-btn variant="outlined" class="screen-btn-ghost" :disabled="actionLoading"
124
+ @click="closeActionDialog">Cancel</v-btn>
125
+ <v-btn variant="flat" class="screen-btn-primary" :loading="actionLoading"
126
+ :disabled="!remarks.trim()" @click="confirmAction">
102
127
  Confirm
103
128
  </v-btn>
104
129
  </v-card-actions>
105
130
  </v-card>
106
131
  </v-dialog>
132
+
133
+ <!--
134
+ Approve/Reject had NO failure path at all: `confirmAction` was a
135
+ try/finally with no catch, so a 400/403/network failure left the dialog
136
+ open, the spinner off and not one word on screen - the guard cannot tell
137
+ a saved approval from a refused one. The shared `Snackbar` is what every
138
+ other screen in the layer uses to say so.
139
+ -->
140
+ <Snackbar v-model="messageSnackbar" :text="snackbarMessage" :color="messageColor" />
107
141
  </v-row>
108
142
  </template>
109
143
 
@@ -215,15 +249,18 @@ function formatStatus(status: TOvernightParkingRequest["status"]) {
215
249
  }
216
250
  }
217
251
 
218
- const { data: overnightParkingRequests, pending: isPendingOvernightParkingRequests, refresh: refreshOvernightParkingRequests } = await useLazyAsyncData<TOvernightParkingRequest[]>(() => getOvernightParkingRequests({ site: props.site, status: statusFilter.value }), { immediate: true })
252
+ // `page` was read by the pager and never sent to the API, so page 2 re-fetched
253
+ // page 1 - with the API's `limit` of 10, every request past the tenth was
254
+ // unreachable. It is part of the query now.
255
+ const { data: overnightParkingRequests, pending: isPendingOvernightParkingRequests, refresh: refreshOvernightParkingRequests } = await useLazyAsyncData<TOvernightParkingRequest[]>(() => getOvernightParkingRequests({ site: props.site, status: statusFilter.value, page: page.value }), { immediate: true })
219
256
 
220
257
 
221
258
  watch(overnightParkingRequests, (data: any) => {
222
- // items.value = Array.isArray(data?.items)
223
- // ? data.items
224
- // : Array.isArray(data)
225
- // ? data
226
- // : []
259
+ items.value = Array.isArray(data?.items)
260
+ ? data.items
261
+ : Array.isArray(data)
262
+ ? data
263
+ : []
227
264
  pages.value = data?.pages ?? 0;
228
265
  pageRange.value = data?.pageRange ?? "-- - -- of --"
229
266
  }, { immediate: true })
@@ -247,6 +284,8 @@ function handleRowClick(data: { item?: TOvernightParkingRequest }) {
247
284
  }
248
285
 
249
286
  function handleUpdatePage(value: number) {
287
+ page.value = value;
288
+ refreshOvernightParkingRequests();
250
289
  emits("update:page", value);
251
290
  }
252
291
 
@@ -262,9 +301,21 @@ function closeActionDialog() {
262
301
  actionDialog.open = false
263
302
  }
264
303
 
304
+ const snackbarMessage = ref("")
305
+ const messageColor = ref("")
306
+ const messageSnackbar = ref(false)
307
+
308
+ function showMessage(msg: string, color: string) {
309
+ snackbarMessage.value = msg
310
+ messageColor.value = color
311
+ messageSnackbar.value = true
312
+ }
313
+
265
314
  async function confirmAction() {
266
315
  if (!actionDialog.requestId || !remarks.value.trim()) return
267
316
 
317
+ const verb = actionDialog.action === 'approved' ? 'approve' : 'reject'
318
+
268
319
  try {
269
320
  actionLoading.value = true
270
321
  await updateOvernightParkingRequest(actionDialog.requestId, {
@@ -272,8 +323,14 @@ async function confirmAction() {
272
323
  remarks: remarks.value.trim(),
273
324
  })
274
325
  actionDialog.open = false
326
+ // One refresh. The second, unawaited call fired the same GET a second time
327
+ // and could land BEFORE the first, putting the pre-update rows back.
275
328
  await refreshOvernightParkingRequests()
276
- refreshOvernightParkingRequests()
329
+ showMessage(`Request ${actionDialog.action}.`, "success")
330
+ } catch (error: any) {
331
+ // The dialog stays OPEN on failure, with the remarks still typed, so the
332
+ // action can be retried rather than silently lost.
333
+ showMessage(error?.data?.message || `Failed to ${verb} this request. Please try again.`, "error")
277
334
  } finally {
278
335
  actionLoading.value = false
279
336
  }
@@ -281,6 +338,9 @@ async function confirmAction() {
281
338
 
282
339
  watch([statusFilter], ([newStatus], [oldStatus]) => {
283
340
  if (newStatus === oldStatus) return
341
+ // A new filter is a new result set - staying on page 3 of the old one asks
342
+ // the API for a page that usually does not exist, and the table draws empty.
343
+ page.value = 1
284
344
  refreshOvernightParkingRequests()
285
345
  }, { immediate: false })
286
346
  </script>
@@ -0,0 +1,43 @@
1
+ <!--
2
+ THE PAGE TITLE ROW.
3
+
4
+ The design opens EVERY screen with this row: the page's name at 22px/800,
5
+ and the screen's primary action on the right of the same line. The product
6
+ has never had it - the page name lived only in the browser tab and in the
7
+ navigation rail - which is the single most visible difference between the
8
+ staging site and the handoff.
9
+
10
+ It is a label and a slot. It fetches nothing, decides nothing and navigates
11
+ nowhere; the action passed into it is the screen's own button.
12
+ -->
13
+ <template>
14
+ <div class="app-page-head">
15
+ <!-- A caller with actions but no page name (the shared table card, when the
16
+ screen has not given it a title) gets just the action group, at the
17
+ left, rather than an empty heading holding a line of blank space. -->
18
+ <div v-if="title || $slots.title || subtitle || $slots.subtitle">
19
+ <h1 class="app-page-head__title">
20
+ <slot name="title">{{ title }}</slot>
21
+ </h1>
22
+
23
+ <!-- The design's second line: a location, a count, a date range. Only
24
+ drawn when a screen supplies one. -->
25
+ <div v-if="subtitle || $slots.subtitle" class="app-page-head__sub">
26
+ <v-icon v-if="subtitleIcon" :icon="subtitleIcon" size="15" />
27
+ <slot name="subtitle">{{ subtitle }}</slot>
28
+ </div>
29
+ </div>
30
+
31
+ <div v-if="$slots.actions" class="app-page-head__actions">
32
+ <slot name="actions" />
33
+ </div>
34
+ </div>
35
+ </template>
36
+
37
+ <script setup lang="ts">
38
+ defineProps({
39
+ title: { type: String, default: "" },
40
+ subtitle: { type: String, default: "" },
41
+ subtitleIcon: { type: String, default: "" },
42
+ });
43
+ </script>
@@ -1,21 +1,20 @@
1
1
  <template>
2
2
  <v-row no-gutters class="w-100">
3
- <v-card class="w-100">
3
+ <v-card flat class="w-100 screen-card">
4
4
  <v-card-text>
5
5
  <v-btn
6
6
  block
7
- color="primary-button"
8
7
  :height="40"
9
8
  text="Scan QR Code"
10
- class="text-capitalize"
11
- disabled
9
+ class="screen-btn-primary"
12
10
  prepend-icon="mdi-qrcode"
11
+ @click="handleScanQRCode"
13
12
  />
14
13
  <v-autocomplete
15
14
  v-model="selectedPass"
16
15
  v-model:search="passInput"
17
16
  :hide-no-data="false"
18
- class="mt-3"
17
+ class="mt-3 filter-field"
19
18
  :items="passItems"
20
19
  :rules="props.passRules"
21
20
  item-title="prefixAndName"
@@ -23,7 +22,6 @@
23
22
  label="Pass"
24
23
  variant="outlined"
25
24
  hide-details
26
- density="compact"
27
25
  persistent-hint
28
26
  small-chips
29
27
  :clearable="props.clearable"
@@ -45,7 +43,7 @@
45
43
  v-model="selectedKeys"
46
44
  v-model:search="keyInput"
47
45
  :hide-no-data="false"
48
- class="mt-3"
46
+ class="mt-3 filter-field"
49
47
  :items="keyItems"
50
48
  :rules="props.passRules"
51
49
  item-title="prefixAndName"
@@ -54,7 +52,6 @@
54
52
  multiple
55
53
  variant="outlined"
56
54
  hide-details
57
- density="compact"
58
55
  persistent-hint
59
56
  small-chips
60
57
  closable-chips
@@ -78,16 +75,21 @@
78
75
  <template v-if="selectedType">
79
76
  <v-number-input
80
77
  v-model="count"
78
+ class="filter-field"
81
79
  variant="outlined"
82
80
  :min="1"
83
81
  :precision="0"
84
- density="compact"
85
82
  :rules="countRules"
86
83
  />
87
84
  </template>
88
85
  </v-card-text>
89
86
  </v-card>
90
87
  </v-row>
88
+ <visitor-pass-key-q-r-scanner
89
+ :dialog="dialog.scanQRCode"
90
+ v-model:scannedValue="scannedValue"
91
+ @close-dialog="dialog.scanQRCode = false"
92
+ />
91
93
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
92
94
  </template>
93
95
 
@@ -95,46 +97,36 @@
95
97
  import type { PropType } from "vue";
96
98
  import type { ValidationRule } from "vuetify/lib/types.mjs";
97
99
  import usePassKey from "../composables/usePassKey";
98
- import useKey from "../composables/useKey";
99
- import { string } from "zod/v4";
100
+ import { errorConverter } from "../utils/data";
101
+ import VisitorPassKeyQRScanner from "./VisitorPassKeyQRScanner.vue";
100
102
 
101
- const props = defineProps({
102
- passRules: {
103
- type: Array as PropType<ValidationRule[]>,
104
- default: [],
105
- },
106
- countRules: {
107
- type: Array as PropType<ValidationRule[]>,
108
- default: [],
109
- },
110
- site: {
111
- type: String,
112
- required: true,
113
- },
114
- type: {
115
- type: String as PropType<TVisitorType>,
116
- required: true,
117
- },
118
- contractorType: {
119
- type: String,
120
- default: "",
121
- },
122
- passKeys: {
123
- type: Array,
124
- default: [],
125
- },
126
- hideKeys: {
127
- type: Boolean,
128
- default: false,
129
- },
130
- clearable: {
131
- type: Boolean,
132
- default: false,
133
- },
103
+ type PassInformationProps = {
104
+ passRules?: ValidationRule[];
105
+ countRules?: ValidationRule[];
106
+ site: string;
107
+ type: TVisitorType;
108
+ contractorType?: string;
109
+ passKeys?: unknown[];
110
+ hideKeys?: boolean;
111
+ clearable?: boolean;
112
+ pass?: TPassKeyPayload[];
113
+ keys?: TPassKeyPayload[];
114
+ };
115
+
116
+ const props = withDefaults(defineProps<PassInformationProps>(), {
117
+ passRules: [],
118
+ countRules: [],
119
+ contractorType: "",
120
+ passKeys: [],
121
+ hideKeys: false,
122
+ clearable: false,
123
+ pass: [],
124
+ keys: [],
134
125
  });
135
126
 
136
- const pass = defineModel<TPassKeyPayload[]>("pass", { default: [] });
137
- const keys = defineModel<TPassKeyPayload[]>("keys", { default: [] });
127
+ const emit = defineEmits(["update:pass", "update:keys"]);
128
+ const pass = ref<TPassKeyPayload[]>(props.pass);
129
+ const keys = ref<TPassKeyPayload[]>(props.keys);
138
130
  const selectedPass = ref<string>("");
139
131
  const selectedKeys = ref<string[]>([]);
140
132
  const passInput = ref("");
@@ -144,6 +136,11 @@ const keyItems = ref<any[]>([]);
144
136
  const selectedType = ref<"qr-pass" | "nfc-card">();
145
137
  const count = ref(1);
146
138
 
139
+ const dialog = reactive({
140
+ scanQRCode: false,
141
+ });
142
+ const scannedValue = ref("");
143
+
147
144
  const { getPassKeysByPageSearch } = usePassKey();
148
145
 
149
146
  const typeItems = [
@@ -179,6 +176,23 @@ function showMessage(msg: string, color: string) {
179
176
  messageSnackbar.value = true;
180
177
  }
181
178
 
179
+ function getItemLabel(item: any) {
180
+ return (item?.prefixAndName ?? item?.raw?.prefixAndName ?? "")
181
+ .toString()
182
+ .trim();
183
+ }
184
+
185
+ function handleScanQRCode() {
186
+ scannedValue.value = "";
187
+ dialog.scanQRCode = true;
188
+ if (passItems.value.length === 0) {
189
+ fetchPasses();
190
+ }
191
+ if (!props.hideKeys && keyItems.value.length === 0) {
192
+ fetchKeys();
193
+ }
194
+ }
195
+
182
196
  const isPassListLoading = ref(false);
183
197
 
184
198
  async function fetchPasses() {
@@ -207,7 +221,7 @@ const isKeyListLoading = ref(false);
207
221
  async function fetchKeys() {
208
222
  isKeyListLoading.value = true;
209
223
  try {
210
- const keys: any = await getPassKeysByPageSearch({
224
+ const result: any = await getPassKeysByPageSearch({
211
225
  page: 1,
212
226
  sites: [props.site],
213
227
  limit: 10000,
@@ -223,7 +237,7 @@ async function fetchKeys() {
223
237
  return false;
224
238
  });
225
239
 
226
- const filteredArray = keys.items.filter((item: any) => {
240
+ const filteredArray = result.items.filter((item: any) => {
227
241
  if (Array.isArray(props?.passKeys) && props.passKeys.length) {
228
242
  return !props.passKeys.includes(item._id);
229
243
  }
@@ -231,18 +245,60 @@ async function fetchKeys() {
231
245
  });
232
246
  keyItems.value = [...previouslySelectedKeys, ...filteredArray];
233
247
  } catch (error) {
234
- showMessage(errorConverter(error as string), "error");
248
+ showMessage(errorConverter(error), "error");
235
249
  } finally {
236
250
  isKeyListLoading.value = false;
237
251
  }
238
252
  }
239
253
 
240
254
  watch(selectedPass, (newVal) => {
241
- pass.value = [{ keyId: newVal }];
255
+ const updated = newVal ? [{ keyId: newVal }] : [];
256
+ pass.value = updated;
257
+ emit("update:pass", updated);
242
258
  });
243
259
 
244
260
  watch(selectedKeys, (newVal) => {
245
- keys.value = newVal.map((key) => ({ keyId: key }));
261
+ const updated = newVal.map((key) => ({ keyId: key }));
262
+ keys.value = updated;
263
+ emit("update:keys", updated);
264
+ });
265
+
266
+ watch(scannedValue, async (newValue) => {
267
+ if (!newValue) return;
268
+
269
+ const scanned = String(newValue).trim();
270
+ if (!scanned) return;
271
+
272
+ if (passItems.value.length === 0) {
273
+ await fetchPasses();
274
+ }
275
+ if (!props.hideKeys && keyItems.value.length === 0) {
276
+ await fetchKeys();
277
+ }
278
+
279
+ const matchedPass = passItems.value.find((item) => {
280
+ return getItemLabel(item).toLowerCase() === scanned.toLowerCase();
281
+ });
282
+
283
+ if (matchedPass && matchedPass._id) {
284
+ selectedPass.value = matchedPass._id;
285
+ return;
286
+ }
287
+
288
+ if (!props.hideKeys) {
289
+ const matchedKey = keyItems.value.find((item) => {
290
+ return getItemLabel(item).toLowerCase() === scanned.toLowerCase();
291
+ });
292
+
293
+ if (matchedKey && matchedKey._id) {
294
+ if (!selectedKeys.value.includes(matchedKey._id)) {
295
+ selectedKeys.value = [...selectedKeys.value, matchedKey._id];
296
+ }
297
+ return;
298
+ }
299
+ }
300
+
301
+ showMessage(`Scanned pass or key "${scanned}" not found`, "error");
246
302
  });
247
303
 
248
304
  //prevent negative value;
@@ -254,10 +310,10 @@ watch(count, (newCount) => {
254
310
 
255
311
  onMounted(() => {
256
312
  if (pass.value.length > 0) {
257
- selectedPass.value = pass.value[0].keyId;
313
+ selectedPass.value = pass.value[0]?.keyId ?? "";
258
314
  }
259
315
  if (keys.value.length > 0) {
260
- selectedKeys.value = keys.value.map((k) => k.keyId);
316
+ selectedKeys.value = keys.value.map((k) => k.keyId ?? "");
261
317
  }
262
318
  });
263
319
  </script>
@@ -1,40 +1,54 @@
1
1
  <template>
2
- <v-card width="100%" :disabled="loading || processingPassword" :loading="loading || processingPassword">
3
- <v-toolbar density="compact">
4
- <v-row no-gutters class="pa-3 text-h6 font-weight-bold">
5
- <span>{{ promptTitle }}</span>
6
- </v-row>
7
- </v-toolbar>
8
- <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-5 px-7 text-center">
2
+ <!--
3
+ A dialog card, so it wears `.screen-modal` (16px corner, the modal shadow,
4
+ the `--card` surface). The two `v-toolbar`s it used to have were grey
5
+ slabs standing in for a title row and a footer - a `v-toolbar` keeps that
6
+ grey on the dark theme, so this dialog dropped a light slab onto a dark
7
+ card at both ends. A `v-card-title` and a `v-card-actions` are the same
8
+ two rows without the slab, and they follow the theme.
9
+ -->
10
+ <v-card class="screen-modal" width="100%" :disabled="loading || processingPassword" :loading="loading || processingPassword">
11
+ <v-card-title>{{ promptTitle }}</v-card-title>
12
+
13
+ <v-card-text class="screen-modal__body pa-5 my-5 px-7 text-center">
9
14
  <v-row no-gutters class="w-100">
10
15
  <v-col cols="12">
11
- <v-icon icon="mdi-lock-outline" :color="'gray'" class="text-h2" />
16
+ <!-- Was `:color="'gray'"`, a fixed Vuetify grey that is the
17
+ same grey on both themes. `--muted` is the design's
18
+ secondary ink and follows the theme. -->
19
+ <v-icon icon="mdi-lock-outline" class="text-h2 screen-lock-icon" />
12
20
  </v-col>
13
21
  <v-col cols="12" v-if="message" class="mt-2">
14
22
  {{ message }}
15
23
  </v-col>
16
24
  <v-col cols="12" class="mt-5 text-start">
17
- <v-text-field v-model="passwordInput" label="Password" type="password" density="compact" :error-messages="errorMessage"></v-text-field>
25
+ <!-- Still a `v-text-field`, not an `AppField`: it carries
26
+ the `:error-messages` surface that `handleConfirm`
27
+ writes the server's reply into, which `AppField` has
28
+ no equivalent for. `.filter-field` gives it the
29
+ design's 40px / 10px / 13.5px shape without taking
30
+ that away. -->
31
+ <v-text-field v-model="passwordInput" class="filter-field" label="Password" type="password" density="compact" :error-messages="errorMessage"></v-text-field>
18
32
  </v-col>
19
33
  </v-row>
20
34
  </v-card-text>
21
35
 
22
- <v-toolbar class="pa-0" density="compact">
23
- <v-row no-gutters>
24
- <v-col cols="6" class="pa-0">
25
- <v-btn block variant="text" class="text-none" size="large" tile @click="emit('cancel')" height="48">
26
- Cancel
27
- </v-btn>
28
- </v-col>
36
+ <!-- The design's footer: two equal-width buttons, ghost then primary,
37
+ on a bordered strip. The confirming button was a hardcoded
38
+ `color="red"` fill; the handoff draws no destructive button, so it
39
+ takes the primary shape like every other modal's confirm, and the
40
+ destructive-variant gap is raised for a design decision rather than
41
+ invented here. -->
42
+ <div class="screen-modal__footer">
43
+ <v-btn class="screen-btn-ghost" variant="outlined" @click="emit('cancel')">
44
+ Cancel
45
+ </v-btn>
29
46
 
30
- <v-col cols="6" class="pa-0">
31
- <v-btn block tile variant="flat" class="text-none" size="large" height="48" color="red" :disabled="!passwordInput"
32
- @click="handleConfirm">
33
- Confirm
34
- </v-btn>
35
- </v-col>
36
- </v-row>
37
- </v-toolbar>
47
+ <v-btn class="screen-btn-primary" variant="flat" :disabled="!passwordInput"
48
+ @click="handleConfirm">
49
+ Confirm
50
+ </v-btn>
51
+ </div>
38
52
  </v-card>
39
53
  </template>
40
54
 
@@ -75,7 +89,7 @@ async function handleConfirm() {
75
89
  const userId = currentUser.value?._id;
76
90
  if (!userId) {
77
91
  throw new Error('User not found. Please login again.');
78
- }
92
+ }
79
93
  const res = await confirmPassword({ userId, password: passwordInput.value });
80
94
  if (res) {
81
95
  emit('confirm');
@@ -92,4 +106,8 @@ async function handleConfirm() {
92
106
 
93
107
  </script>
94
108
 
95
- <style scoped></style>
109
+ <style scoped>
110
+ .screen-lock-icon {
111
+ color: var(--muted);
112
+ }
113
+ </style>