@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.
- package/assets/css/primitives.css +1113 -0
- package/assets/css/screens.css +1097 -0
- package/assets/css/shell.css +258 -0
- package/assets/css/tokens.css +236 -0
- package/components/AcceptDialog.vue +17 -18
- package/components/AccessCard/AvailableStats.vue +24 -16
- package/components/AccessCardAddForm.vue +42 -42
- package/components/AccessCardAssignToUnitForm.vue +13 -21
- package/components/AccessCardDeleteDialog.vue +41 -39
- package/components/AccessCardDetailsDialog.vue +125 -79
- package/components/AccessCardPreviewDialog.vue +58 -84
- package/components/AccessCardQrTagging.vue +60 -63
- package/components/AccessCardReplaceForm.vue +27 -23
- package/components/AccessManagement.vue +86 -85
- package/components/AddEqupmentForm.vue +18 -22
- package/components/AddPassKeyToVisitor.vue +95 -73
- package/components/AppButton.vue +62 -0
- package/components/AppCard.vue +20 -0
- package/components/AppDateField.vue +49 -0
- package/components/AppField.vue +76 -0
- package/components/AppKpiTile.vue +65 -0
- package/components/AppSegmented.vue +38 -0
- package/components/AppSelect.vue +247 -0
- package/components/AreaChecklistHistoryLogs.vue +51 -44
- package/components/AreaChecklistHistoryMain.vue +14 -7
- package/components/AreaFormDialog.vue +71 -59
- package/components/AreaMain.vue +73 -107
- package/components/AttachmentsViewer.vue +84 -22
- package/components/AttendanceCheckInOutDialog.vue +85 -88
- package/components/AttendanceDetailsDialog.vue +57 -63
- package/components/AttendanceMain.vue +4 -3
- package/components/AttendanceMapSearchDialog.vue +42 -44
- package/components/AttendanceSettingsDialog.vue +79 -63
- package/components/Avatar/Main.vue +54 -2
- package/components/BillingMain.vue +26 -30
- package/components/BtnUploadFile.vue +53 -11
- package/components/BuildingForm.vue +34 -27
- package/components/BuildingManagement/buildings.vue +39 -39
- package/components/BuildingManagement/units.vue +26 -42
- package/components/BuildingUnitFormAdd.vue +20 -23
- package/components/BuildingUnitFormEdit.vue +8 -4
- package/components/BulletinBoardForm.vue +19 -24
- package/components/BulletinBoardManagement.vue +49 -57
- package/components/BulletinBoardView.vue +9 -6
- package/components/BulletinExpirationChip.vue +13 -4
- package/components/Button/Close.vue +2 -2
- package/components/CameraForm.vue +21 -22
- package/components/CameraMain.vue +50 -49
- package/components/Card/DeleteConfirmation.vue +24 -22
- package/components/Card/MemberInfoSummary.vue +28 -3
- package/components/Card/Toggle.vue +15 -3
- package/components/CardToolbar.vue +37 -0
- package/components/Carousel.vue +56 -36
- package/components/Chat/Bubbles.vue +43 -14
- package/components/Chat/Information.vue +49 -32
- package/components/Chat/ListCard.vue +38 -9
- package/components/Chat/Message.vue +41 -15
- package/components/Chat/Navigation.vue +27 -15
- package/components/Chat/SkeletonLoader.vue +9 -2
- package/components/ChecklistItemRow.vue +25 -1
- package/components/CleaningScheduleMain.vue +44 -49
- package/components/ClientDetailForm.vue +81 -56
- package/components/ClientMain.vue +92 -82
- package/components/ConfirmDialog.vue +14 -8
- package/components/Container/Standard.vue +9 -7
- package/components/CreateSubsciptionPlan.vue +148 -89
- package/components/DashboardEmptyState.vue +48 -3
- package/components/DashboardMain.vue +924 -676
- package/components/DashboardPanel.vue +84 -21
- package/components/DeliveryCompany.vue +34 -20
- package/components/Dialog/DeleteConfirmation.vue +20 -21
- package/components/Dialog/ReplaceAutofillPrompt.vue +19 -26
- package/components/Dialog/ReusablePrompt.vue +30 -29
- package/components/Dialog/UpdateMoreAction.vue +62 -59
- package/components/DocumentForm.vue +14 -21
- package/components/DocumentManagement.vue +162 -108
- package/components/DocumentViewer.vue +29 -12
- package/components/DrawImage.vue +29 -21
- package/components/EntryPass/QrTemplatePreview.vue +27 -16
- package/components/EntryPassInformation.vue +40 -10
- package/components/EntryPassMain.vue +49 -27
- package/components/EquipmentItemMain.vue +84 -97
- package/components/EquipmentManagementMain.vue +81 -103
- package/components/Facility/BookingSetup.vue +43 -52
- package/components/Feedback/Form.vue +26 -12
- package/components/FeedbackMain.vue +35 -49
- package/components/FileInput.vue +75 -58
- package/components/FileInputWithList.vue +59 -12
- package/components/FormDialog.vue +60 -21
- package/components/HidAccessLogDashboard.vue +512 -490
- package/components/HidCameraCaptureDialog.vue +216 -0
- package/components/HidEnabledGate.vue +67 -9
- package/components/HidIntercomManagement.vue +1322 -653
- package/components/HidQrCodeConfiguration.vue +1030 -258
- package/components/HidReaderForm.vue +142 -59
- package/components/HidReaderManagement.vue +211 -323
- package/components/HidServiceSettingsPanel.vue +44 -13
- package/components/HidUserEnrollment.vue +1185 -412
- package/components/HygieneUpdateMoreAction.vue +44 -44
- package/components/IncidentReport/Authorities.vue +30 -18
- package/components/IncidentReport/IncidentInformation.vue +10 -10
- package/components/IncidentReport/IncidentInformationDownload.vue +3 -4
- package/components/IncidentReport/affectedEntities.vue +41 -20
- package/components/Input/Date.vue +3 -0
- package/components/Input/DatePicker.vue +1 -2
- package/components/Input/DateTimePicker.vue +1 -1
- package/components/Input/File.vue +55 -10
- package/components/Input/FileV2.vue +39 -6
- package/components/Input/InputPhoneNumberV2.vue +2 -1
- package/components/Input/ListGroupSelection.vue +56 -8
- package/components/Input/NRICNumber.vue +1 -0
- package/components/Input/NewDate.vue +1 -0
- package/components/Input/Number.vue +1 -0
- package/components/Input/Password.vue +1 -0
- package/components/Input/PhoneNumber.vue +21 -1
- package/components/Input/VehicleNumber.vue +1 -0
- package/components/InputLabel.vue +2 -2
- package/components/InvitationClientForm.vue +21 -19
- package/components/InvitationForm.vue +15 -21
- package/components/InvitationMain.vue +46 -61
- package/components/Layout/Header.vue +263 -36
- package/components/Layout/NavigationDrawer.vue +300 -30
- package/components/ListItem.vue +33 -3
- package/components/LocalPagination.vue +27 -3
- package/components/ManageChecklistMain.vue +136 -86
- package/components/MemberInformation.vue +32 -16
- package/components/MemberMain.vue +113 -93
- package/components/MyAttendanceMain.vue +3 -5
- package/components/NavigationItem.vue +83 -3
- package/components/Nfc/NFCPatrolReportMain.vue +47 -33
- package/components/Nfc/NFCPatrolRouteForm.vue +18 -20
- package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
- package/components/Nfc/NFCPatrolSettings.vue +14 -4
- package/components/Nfc/NFCTagForm.vue +22 -8
- package/components/Nfc/NFCTagMain.vue +41 -43
- package/components/Nfc/PatrolReport/DailyReportTab.vue +8 -1
- package/components/Nfc/PatrolReport/MonthlyReportTab.vue +8 -1
- package/components/Nfc/PatrolReport/MonthlyReportTable.vue +5 -3
- package/components/Nfc/PatrolReport/PatrolActivityTable.vue +26 -6
- package/components/Nfc/PatrolReport/PatrolReportTab.vue +8 -0
- package/components/Nfc/PatrolReport/PatrolRouteSummary.vue +41 -9
- package/components/Nfc/PatrolReport/RemarksDialog.vue +39 -9
- package/components/Nfc/PatrolReport/ReportEmptyState.vue +29 -9
- package/components/Nfc/PatrolReport/ReportFilters.vue +19 -10
- package/components/Nfc/PatrolReport/ReportLocationHeader.vue +32 -6
- package/components/Nfc/PatrolReport/SummaryReportTable.vue +28 -6
- package/components/NotificationSettings.vue +373 -0
- package/components/NumberSettingField.vue +9 -2
- package/components/OnlineFormConfigurationForm.vue +725 -237
- package/components/OnlineFormFill.vue +213 -106
- package/components/OnlineFormRenderPreview.vue +25 -30
- package/components/OnlineFormsConfiguration.vue +113 -109
- package/components/OrgViewDialog.vue +34 -16
- package/components/OvernightParkingAvailability.vue +49 -26
- package/components/OvernightParkingManagement.vue +78 -18
- package/components/PageHeader.vue +43 -0
- package/components/PassInformation.vue +109 -53
- package/components/PasswordConfirmation.vue +44 -26
- package/components/PeopleForm.vue +41 -20
- package/components/PhotoUpload.vue +110 -48
- package/components/PlaceholderComponent.vue +35 -19
- package/components/PlanActionsMenu.vue +12 -2
- package/components/PlanStatusBadge.vue +7 -8
- package/components/PlateNumberDisplay.vue +9 -6
- package/components/QrTemplate/PrintDialog.vue +66 -43
- package/components/RolePermissionFormCreate.vue +104 -79
- package/components/RolePermissionFormPreviewUpdate.vue +111 -109
- package/components/RolePermissionMain.vue +183 -81
- package/components/ScanVisitorQRCode.vue +99 -35
- package/components/ScheduleAreaMain.vue +34 -59
- package/components/ScheduleTaskForm.vue +61 -42
- package/components/ScheduleTaskMain.vue +56 -69
- package/components/SearchVehicleNumberUser.vue +39 -37
- package/components/ServiceProviderFormCreate.vue +29 -20
- package/components/ServiceProviderInvitationPrompt.vue +155 -0
- package/components/ServiceProviderMain.vue +601 -432
- package/components/Signature.vue +40 -34
- package/components/SignaturePad.vue +5 -29
- package/components/SiteSettings.vue +41 -11
- package/components/SiteSettingsWorkOrder.vue +61 -30
- package/components/SlideCardGroup.vue +35 -11
- package/components/Snackbar.vue +36 -3
- package/components/SpecificAttr.vue +18 -8
- package/components/StatusChip.vue +156 -0
- package/components/StockCard.vue +17 -12
- package/components/SubscriptionPlanTable.vue +207 -140
- package/components/SwitchContext.vue +201 -9
- package/components/TableHygiene.vue +36 -46
- package/components/TableList.vue +10 -10
- package/components/TableListSecondary.vue +105 -54
- package/components/TableMain.vue +251 -78
- package/components/TableV3.vue +61 -70
- package/components/TiptapEditor.vue +33 -40
- package/components/TiptapEditorQr.vue +5 -2
- package/components/Tooltip/Info.vue +69 -24
- package/components/UnitMain.vue +40 -60
- package/components/UnitPersonCard.vue +42 -13
- package/components/VehicleAddSelection.vue +48 -32
- package/components/VehicleForm.vue +112 -129
- package/components/VehicleManagement.vue +26 -12
- package/components/VehicleUpdateMoreAction.vue +59 -46
- package/components/VisitorDataFromScannedQRCodeForm.vue +90 -65
- package/components/VisitorForm.vue +774 -210
- package/components/VisitorFormSelection.vue +40 -37
- package/components/VisitorManagement.vue +239 -82
- package/components/VisitorPassKeyQRScanner.vue +65 -18
- package/components/VisitorSearchResultSummary.vue +60 -16
- package/components/VisitorSocketPopUp.vue +22 -14
- package/components/VisitorsReportPreview.vue +44 -29
- package/components/WorkOrder/Create.vue +26 -17
- package/components/WorkOrder/ListView.vue +38 -7
- package/components/WorkOrder/Main.vue +62 -59
- package/composables/useFeedback.ts +3 -3
- package/composables/useHidAmico.ts +269 -0
- package/composables/useHidNavigation.ts +2 -9
- package/composables/useMember.ts +3 -3
- package/composables/useNFCPatrolDailyReport.ts +13 -0
- package/composables/useNFCPatrolMonthlyReport.ts +22 -56
- package/composables/useNFCPatrolReport.ts +13 -0
- package/composables/useNotificationPreference.ts +87 -0
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useRole.ts +22 -0
- package/composables/useServiceProvider.ts +3 -3
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +336 -0
- package/composables/useSubscriptionPlan.ts +62 -0
- package/composables/useThemePreference.ts +63 -0
- package/composables/useVisitor.ts +6 -1
- package/composables/useWebUsb.ts +127 -37
- package/composables/useWorkOrder.ts +17 -3
- package/error.vue +30 -6
- package/nuxt.config.ts +36 -0
- package/package.json +25 -4
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +2 -2
- package/pages/[org]/[site]/access-mgmt/{identity-mapping → hid-cards}/index.vue +2 -2
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/pages/notification-settings.vue +19 -0
- package/pages/payment-method-linked.vue +34 -10
- package/pages/require-customer.vue +48 -10
- package/pages/require-organization-membership.vue +32 -8
- package/pages/unauthorized.vue +29 -4
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +69 -2
- package/types/nfc-patrol-report.ts +3 -2
- package/types/site.d.ts +71 -0
- package/types/subscription.ts +18 -11
- package/utils/breadcrumb.test.ts +108 -0
- package/utils/breadcrumb.ts +82 -0
- package/utils/dashboard.test.ts +118 -0
- package/utils/dashboard.ts +53 -0
- package/utils/data.test.ts +95 -0
- package/utils/data.ts +40 -2
- package/utils/hid-discover.test.ts +102 -0
- package/utils/list-page.test.ts +60 -0
- package/utils/list-page.ts +38 -0
- package/utils/materialSymbols.ts +363 -0
- package/utils/nav-sections.test.ts +190 -0
- package/utils/nav-sections.ts +151 -0
- package/utils/nfc-patrol-report.test.ts +130 -0
- package/utils/nfc-patrol-report.ts +101 -0
- package/utils/role.test.ts +69 -0
- package/utils/role.ts +10 -0
- package/utils/route-name.test.ts +85 -0
- package/utils/route-name.ts +24 -0
- package/utils/status.test.ts +110 -0
- package/utils/status.ts +135 -0
- package/utils/table-card.test.ts +84 -0
- package/utils/theme-aa-ledger.ts +148 -0
- package/utils/theme.test.ts +485 -7
- package/utils/theme.ts +295 -110
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.editorconfig +0 -12
- package/.github/workflows/main.yml +0 -17
- package/.github/workflows/publish-staging.yml +0 -47
- package/.github/workflows/publish.yml +0 -39
- package/PUBLISHING.md +0 -269
- package/components/HidIdentityMapping.vue +0 -975
- package/components/ScheduleTaskAreaFormDialog.vue +0 -144
- package/components/ScheduleTaskAreaUpdateMoreAction.vue +0 -109
- package/components/ScheduleTastTicketMain.vue +0 -182
- package/components/TableWithButton.vue +0 -94
- package/test/visitor-socket.test.mjs +0 -36
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-row no-gutters>
|
|
3
|
-
|
|
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
|
-
<
|
|
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"
|
|
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
|
|
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
|
|
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="
|
|
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="
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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="
|
|
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
|
|
99
|
-
import
|
|
100
|
+
import { errorConverter } from "../utils/data";
|
|
101
|
+
import VisitorPassKeyQRScanner from "./VisitorPassKeyQRScanner.vue";
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
passRules
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
137
|
-
const
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
109
|
+
<style scoped>
|
|
110
|
+
.screen-lock-icon {
|
|
111
|
+
color: var(--muted);
|
|
112
|
+
}
|
|
113
|
+
</style>
|