@7365admin1/layer-common 4.35.1-staging.385 → 4.36.0
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/CHANGELOG.md +10 -4
- package/assets/css/primitives.css +0 -43
- package/assets/css/robot-hero.css +2 -2
- package/components/AccessCardAddForm.vue +4 -5
- package/components/AccessCardAssignToUnitForm.vue +2 -3
- package/components/AccessCardPreviewDialog.vue +3 -4
- package/components/AppSelect.vue +3 -77
- package/components/CameraForm.vue +6 -20
- package/components/DashboardMain.vue +60 -66
- package/components/Dialog/UpdateMoreAction.vue +1 -11
- package/components/DocumentForm.vue +112 -407
- package/components/DocumentManagement.vue +12 -213
- package/components/EntryPass/QrTemplatePreview.vue +25 -21
- package/components/EntryPassInformation.vue +24 -119
- package/components/Facility/BookingSetup.vue +0 -63
- package/components/HidAccessLogDashboard.vue +17 -74
- package/components/HidIntercomManagement.vue +14 -180
- package/components/HidQrCodeConfiguration.vue +195 -17
- package/components/HidReaderManagement.vue +0 -398
- package/components/HidReaderUserRoster.vue +3 -44
- package/components/HidUserEnrollment.vue +155 -845
- package/components/InvitationClientForm.vue +1 -19
- package/components/Layout/NavigationDrawer.vue +1 -4
- package/components/NotificationBell.vue +7 -59
- package/components/Robot/BuildingMap.vue +9 -16
- package/components/Robot/LiftQueue.vue +4 -9
- package/components/Robot/MapEditor.vue +8 -1
- package/components/RolePermissionFormCreate.vue +3 -4
- package/components/RolePermissionFormPreviewUpdate.vue +5 -12
- package/components/RolePermissionMain.vue +0 -1
- package/components/ServiceProviderInvitationPrompt.vue +155 -0
- package/components/ServiceProviderMain.vue +30 -178
- package/components/VisitorForm.vue +0 -12
- package/components/VisitorManagement.vue +10 -285
- package/components/VisitorSocketPopUp.vue +1 -2
- package/composables/useANPRSettings.ts +15 -45
- package/composables/useAccessManagement.ts +0 -5
- package/composables/useCleaningPermission.ts +0 -2
- package/composables/useCommonPermission.ts +0 -32
- package/composables/useDocument.ts +0 -7
- package/composables/useFacility.ts +0 -11
- package/composables/useHidAmico.ts +0 -159
- package/composables/useHidNavigation.ts +1 -25
- package/composables/usePeople.ts +0 -19
- package/composables/usePropertyPermission.ts +0 -19
- package/composables/useRobotBuilding.ts +3 -22
- package/composables/useRobots.ts +0 -13
- package/composables/useSecurityPermission.ts +0 -19
- package/composables/useServiceProvider.ts +0 -3
- package/composables/useServiceProviderInvitation.ts +3 -101
- package/composables/useSiteEntryPassSettings.ts +0 -26
- package/composables/useWebUsb.ts +11 -14
- package/package.json +2 -2
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
- package/plugins/API.ts +0 -11
- package/types/document.d.ts +0 -14
- package/types/facility.d.ts +0 -4
- package/types/people.d.ts +1 -28
- package/utils/module-routes.ts +1 -8
- package/utils/nav-sections.ts +2 -8
- package/utils/notification-target.ts +8 -277
- package/utils/permission-labels.ts +2 -20
- package/components/ClientInvitations.vue +0 -291
- package/components/HidAccessPermissions.vue +0 -424
- package/components/HidProfileQrCode.vue +0 -332
- package/composables/useHidReaderSelection.ts +0 -50
- package/composables/useSiteCategory.ts +0 -45
- package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +0 -23
- package/utils/dashboard-widget-prefs.ts +0 -54
- package/utils/hid-permission-assignments.ts +0 -190
- package/utils/hid-reader-selection.ts +0 -52
- package/utils/occupancy-role.ts +0 -109
- package/utils/robot-fleet-core.ts +0 -57
- package/utils/sp-inviter-row.ts +0 -97
|
@@ -57,24 +57,7 @@
|
|
|
57
57
|
</div>
|
|
58
58
|
</v-col>
|
|
59
59
|
|
|
60
|
-
<v-col cols="12">
|
|
61
|
-
<v-tabs
|
|
62
|
-
v-model="activeTab"
|
|
63
|
-
class="doc-tabs"
|
|
64
|
-
density="compact"
|
|
65
|
-
:hide-slider="false"
|
|
66
|
-
>
|
|
67
|
-
<v-tab value="active" class="text-none">Documents</v-tab>
|
|
68
|
-
<v-tab value="expired" class="text-none">Document (Expired)</v-tab>
|
|
69
|
-
</v-tabs>
|
|
70
|
-
</v-col>
|
|
71
|
-
|
|
72
|
-
<!--
|
|
73
|
-
Hidden on the Expired tab: that tab is a site-wide worklist rather than a
|
|
74
|
-
place in the folder tree, so a breadcrumb there would name a folder whose
|
|
75
|
-
contents are not what is on screen.
|
|
76
|
-
-->
|
|
77
|
-
<v-col cols="12" class="mb-2" v-if="isInsideFolder && !isExpiredTab">
|
|
60
|
+
<v-col cols="12" class="mb-2" v-if="isInsideFolder">
|
|
78
61
|
<div class="folder-path-bar d-flex align-center ga-2 px-3 py-2">
|
|
79
62
|
<v-btn
|
|
80
63
|
icon="mdi-arrow-left"
|
|
@@ -166,79 +149,19 @@
|
|
|
166
149
|
<span>{{ item.date_modified || "N/A" }}</span>
|
|
167
150
|
</template>
|
|
168
151
|
|
|
169
|
-
<template #item.expiry_date="{ item }">
|
|
170
|
-
<span>{{ item.expiry_date || "N/A" }}</span>
|
|
171
|
-
</template>
|
|
172
|
-
|
|
173
152
|
<template #item.remarks="{ item }">
|
|
174
153
|
<span>{{ item.remarks || "N/A" }}</span>
|
|
175
154
|
</template>
|
|
176
155
|
|
|
177
156
|
<template #item.action="{ item }">
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
v-if="isExpiredTab"
|
|
187
|
-
variant="flat"
|
|
188
|
-
class="table-download-btn text-none"
|
|
189
|
-
:disabled="!canUpdateDocument"
|
|
190
|
-
@click.stop="openUpdateDialog(item._raw)"
|
|
191
|
-
>
|
|
192
|
-
Update
|
|
193
|
-
</v-btn>
|
|
194
|
-
|
|
195
|
-
<v-btn
|
|
196
|
-
v-else
|
|
197
|
-
variant="flat"
|
|
198
|
-
class="table-download-btn text-none"
|
|
199
|
-
@click.stop="downloadDocument(item._raw)"
|
|
200
|
-
>
|
|
201
|
-
Download
|
|
202
|
-
</v-btn>
|
|
203
|
-
|
|
204
|
-
<v-menu
|
|
205
|
-
v-if="canUpdateDocument || canDeleteDocument"
|
|
206
|
-
v-model="documentActionMenu[item._raw._id]"
|
|
207
|
-
location="bottom end"
|
|
208
|
-
offset="6"
|
|
209
|
-
>
|
|
210
|
-
<template #activator="{ props }">
|
|
211
|
-
<v-btn
|
|
212
|
-
v-bind="props"
|
|
213
|
-
icon="mdi-dots-vertical"
|
|
214
|
-
variant="text"
|
|
215
|
-
size="small"
|
|
216
|
-
density="comfortable"
|
|
217
|
-
class="folder-action-btn"
|
|
218
|
-
@click.stop
|
|
219
|
-
/>
|
|
220
|
-
</template>
|
|
221
|
-
|
|
222
|
-
<v-list class="folder-action-list pa-0">
|
|
223
|
-
<v-list-item
|
|
224
|
-
v-if="canUpdateDocument"
|
|
225
|
-
class="folder-action-item"
|
|
226
|
-
@click.stop="openDocumentEdit(item._raw)"
|
|
227
|
-
>
|
|
228
|
-
<v-list-item-title class="text-body-2">Edit</v-list-item-title>
|
|
229
|
-
</v-list-item>
|
|
230
|
-
<v-list-item
|
|
231
|
-
v-if="canDeleteDocument"
|
|
232
|
-
class="folder-action-item"
|
|
233
|
-
@click.stop="openDocumentDelete(item._raw)"
|
|
234
|
-
>
|
|
235
|
-
<v-list-item-title class="text-body-2">
|
|
236
|
-
Delete
|
|
237
|
-
</v-list-item-title>
|
|
238
|
-
</v-list-item>
|
|
239
|
-
</v-list>
|
|
240
|
-
</v-menu>
|
|
241
|
-
</template>
|
|
157
|
+
<v-btn
|
|
158
|
+
v-if="!isFolderItem(item._raw)"
|
|
159
|
+
variant="flat"
|
|
160
|
+
class="table-download-btn text-none"
|
|
161
|
+
@click.stop="downloadDocument(item._raw)"
|
|
162
|
+
>
|
|
163
|
+
Download
|
|
164
|
+
</v-btn>
|
|
242
165
|
|
|
243
166
|
<v-menu
|
|
244
167
|
v-else
|
|
@@ -277,11 +200,8 @@
|
|
|
277
200
|
</TableV3>
|
|
278
201
|
</v-col>
|
|
279
202
|
|
|
280
|
-
<!--
|
|
281
|
-
|
|
282
|
-
table, one row per file, rather than a stacked pair of fields.
|
|
283
|
-
-->
|
|
284
|
-
<v-dialog v-model="createDialog" max-width="1100" persistent>
|
|
203
|
+
<!-- Create Dialog -->
|
|
204
|
+
<v-dialog v-model="createDialog" width="450" persistent>
|
|
285
205
|
<DocumentForm
|
|
286
206
|
v-if="createDialog"
|
|
287
207
|
:parent-id="activeParentId"
|
|
@@ -290,17 +210,6 @@
|
|
|
290
210
|
/>
|
|
291
211
|
</v-dialog>
|
|
292
212
|
|
|
293
|
-
<!-- Update Dialog: renew an expired document by replacing its file. -->
|
|
294
|
-
<v-dialog v-model="updateDialog" max-width="1100" persistent>
|
|
295
|
-
<DocumentForm
|
|
296
|
-
v-if="updateDialog"
|
|
297
|
-
mode="update"
|
|
298
|
-
:document="selectedDocument"
|
|
299
|
-
@cancel="updateDialog = false"
|
|
300
|
-
@success="successRenew()"
|
|
301
|
-
/>
|
|
302
|
-
</v-dialog>
|
|
303
|
-
|
|
304
213
|
<!-- Create Folder Dialog -->
|
|
305
214
|
<v-dialog v-model="createFolderDialog" width="450" persistent>
|
|
306
215
|
<v-card width="100%">
|
|
@@ -575,15 +484,6 @@
|
|
|
575
484
|
</v-row>
|
|
576
485
|
</v-col>
|
|
577
486
|
|
|
578
|
-
<v-col cols="12" class="mb-2">
|
|
579
|
-
<v-row no-gutters>
|
|
580
|
-
<v-col cols="6" class="text-body-1">Expiry Date</v-col>
|
|
581
|
-
<v-col cols="6" class="text-right text-body-1">{{
|
|
582
|
-
previewDetails.expiryDate
|
|
583
|
-
}}</v-col>
|
|
584
|
-
</v-row>
|
|
585
|
-
</v-col>
|
|
586
|
-
|
|
587
487
|
<v-col cols="12">
|
|
588
488
|
<v-row no-gutters>
|
|
589
489
|
<v-col cols="6" class="text-body-1">Remarks</v-col>
|
|
@@ -734,10 +634,6 @@ const props = defineProps({
|
|
|
734
634
|
title: "Date Modified",
|
|
735
635
|
value: "date_modified",
|
|
736
636
|
},
|
|
737
|
-
{
|
|
738
|
-
title: "Expiry Date",
|
|
739
|
-
value: "expiry_date",
|
|
740
|
-
},
|
|
741
637
|
{
|
|
742
638
|
title: "Remarks",
|
|
743
639
|
value: "remarks",
|
|
@@ -837,13 +733,6 @@ const currentFolderPath = computed(() => {
|
|
|
837
733
|
const fileTypes = ref(["all", "pdf", "csv", "doc"]);
|
|
838
734
|
const selectedFileType = ref("all");
|
|
839
735
|
|
|
840
|
-
/**
|
|
841
|
-
* Which half of the list is on screen. The value is sent straight to the API as
|
|
842
|
-
* its `expiry` filter, so the tab and the query cannot drift apart.
|
|
843
|
-
*/
|
|
844
|
-
const activeTab = ref<"active" | "expired">("active");
|
|
845
|
-
const isExpiredTab = computed(() => activeTab.value === "expired");
|
|
846
|
-
|
|
847
736
|
const {
|
|
848
737
|
data: getDocumentReq,
|
|
849
738
|
refresh: getDocuments,
|
|
@@ -859,20 +748,12 @@ const {
|
|
|
859
748
|
parentId: activeParentId.value,
|
|
860
749
|
type:
|
|
861
750
|
selectedFileType.value === "all" ? undefined : selectedFileType.value,
|
|
862
|
-
expiry: activeTab.value,
|
|
863
751
|
}),
|
|
864
752
|
{
|
|
865
|
-
watch: [page, headerSearch, activeParentId, selectedFileType
|
|
753
|
+
watch: [page, headerSearch, activeParentId, selectedFileType],
|
|
866
754
|
}
|
|
867
755
|
);
|
|
868
756
|
|
|
869
|
-
// Page 3 of the Documents tab is rarely page 3 of a much shorter Expired list,
|
|
870
|
-
// so a tab change that kept the page number would land on an empty table that
|
|
871
|
-
// looks like "nothing has expired".
|
|
872
|
-
watch(activeTab, () => {
|
|
873
|
-
page.value = 1;
|
|
874
|
-
});
|
|
875
|
-
|
|
876
757
|
const loading = computed(() => getAllReqStatus.value === "pending");
|
|
877
758
|
|
|
878
759
|
// The page totals are the server's count of the WHOLE list, read through the
|
|
@@ -907,28 +788,6 @@ function formatDate(value?: string | null) {
|
|
|
907
788
|
});
|
|
908
789
|
}
|
|
909
790
|
|
|
910
|
-
/**
|
|
911
|
-
* Expiry is a day, not a moment: the picker collects `YYYY-MM-DD` and the
|
|
912
|
-
* design renders `13/12/2026`. Reusing `formatDate` would append a meaningless
|
|
913
|
-
* 00:00 to every row.
|
|
914
|
-
*/
|
|
915
|
-
function formatDateOnly(value?: string | null) {
|
|
916
|
-
if (!value) {
|
|
917
|
-
return "N/A";
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
const dateValue = new Date(value);
|
|
921
|
-
if (Number.isNaN(dateValue.getTime())) {
|
|
922
|
-
return "N/A";
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
return dateValue.toLocaleDateString("en-SG", {
|
|
926
|
-
year: "numeric",
|
|
927
|
-
month: "2-digit",
|
|
928
|
-
day: "2-digit",
|
|
929
|
-
});
|
|
930
|
-
}
|
|
931
|
-
|
|
932
791
|
function formatBytes(bytes = 0) {
|
|
933
792
|
const b = Number(bytes) || 0;
|
|
934
793
|
if (b === 0) return "0 B";
|
|
@@ -964,20 +823,13 @@ const tableItems = computed(() => {
|
|
|
964
823
|
: formatBytes(Number(doc.file_size ?? doc.size ?? 0)),
|
|
965
824
|
date_created: formatDate(doc.created_at || doc.createdAt),
|
|
966
825
|
date_modified: formatDate(doc.updated_at || doc.updatedAt),
|
|
967
|
-
// Folders never expire, so they read N/A rather than borrowing a date.
|
|
968
|
-
expiry_date:
|
|
969
|
-
doc.type === "folder"
|
|
970
|
-
? "N/A"
|
|
971
|
-
: formatDateOnly(doc.expiry_date || doc.expiryDate),
|
|
972
826
|
remarks: doc.remarks || "N/A",
|
|
973
827
|
}));
|
|
974
828
|
});
|
|
975
829
|
|
|
976
830
|
const createDialog = ref(false);
|
|
977
831
|
const editDialog = ref(false);
|
|
978
|
-
const updateDialog = ref(false);
|
|
979
832
|
const previewDialog = ref(false);
|
|
980
|
-
const documentActionMenu = ref<Record<string, boolean>>({});
|
|
981
833
|
|
|
982
834
|
// Create folder dialog state
|
|
983
835
|
const createFolderDialog = ref(false);
|
|
@@ -1028,10 +880,6 @@ const previewDetails = computed(() => {
|
|
|
1028
880
|
fileSize,
|
|
1029
881
|
dateCreated: formatDate(document?.created_at || document?.createdAt),
|
|
1030
882
|
dateModified: formatDate(document?.updated_at || document?.updatedAt),
|
|
1031
|
-
expiryDate:
|
|
1032
|
-
document?.type === "folder"
|
|
1033
|
-
? "N/A"
|
|
1034
|
-
: formatDateOnly(document?.expiry_date || document?.expiryDate),
|
|
1035
883
|
remarks: document?.remarks || "N/A",
|
|
1036
884
|
};
|
|
1037
885
|
});
|
|
@@ -1303,29 +1151,6 @@ function openDeleteDialog() {
|
|
|
1303
1151
|
message.value = "";
|
|
1304
1152
|
}
|
|
1305
1153
|
|
|
1306
|
-
/*
|
|
1307
|
-
* The three row-kebab entry points. Each names its document first: the dialogs
|
|
1308
|
-
* below all read `selectedDocument`, which until now was only ever set by
|
|
1309
|
-
* opening the detail modal, so firing one straight from a row without setting
|
|
1310
|
-
* it would have acted on whichever document was inspected last.
|
|
1311
|
-
*/
|
|
1312
|
-
function openDocumentEdit(document: TDocument) {
|
|
1313
|
-
selectedDocument.value = document;
|
|
1314
|
-
documentActionMenu.value = {};
|
|
1315
|
-
editDialog.value = true;
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
function openDocumentDelete(document: TDocument) {
|
|
1319
|
-
selectedDocument.value = document;
|
|
1320
|
-
documentActionMenu.value = {};
|
|
1321
|
-
openDeleteDialog();
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
function openUpdateDialog(document: TDocument) {
|
|
1325
|
-
selectedDocument.value = document;
|
|
1326
|
-
updateDialog.value = true;
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
1154
|
function openEditDialog() {
|
|
1330
1155
|
editDialog.value = true;
|
|
1331
1156
|
}
|
|
@@ -1354,19 +1179,6 @@ function successUpdate() {
|
|
|
1354
1179
|
showMessage("Document updated successfully!", "success");
|
|
1355
1180
|
}
|
|
1356
1181
|
|
|
1357
|
-
/**
|
|
1358
|
-
* A renewal usually moves the document off the tab it was renewed from, so the
|
|
1359
|
-
* refresh is what makes it disappear from the Expired list. A renewal that did
|
|
1360
|
-
* not move the expiry date forward stays put, which is the intended behaviour
|
|
1361
|
-
* rather than a failed save - hence the neutral wording.
|
|
1362
|
-
*/
|
|
1363
|
-
function successRenew() {
|
|
1364
|
-
updateDialog.value = false;
|
|
1365
|
-
previewDialog.value = false;
|
|
1366
|
-
getDocuments();
|
|
1367
|
-
showMessage("Document updated successfully!", "success");
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
1182
|
function selectFileType(type: string) {
|
|
1371
1183
|
selectedFileType.value = type;
|
|
1372
1184
|
filterMenu.value = false;
|
|
@@ -1391,19 +1203,6 @@ function selectFileType(type: string) {
|
|
|
1391
1203
|
color: var(--text);
|
|
1392
1204
|
}
|
|
1393
1205
|
|
|
1394
|
-
/* The tab strip sits on the table card, so it takes the card's rule rather
|
|
1395
|
-
than Vuetify's default underline, and the cell type rather than the button
|
|
1396
|
-
scale - the same treatment the toolbar above it already gets. */
|
|
1397
|
-
.doc-tabs {
|
|
1398
|
-
border-bottom: 1px solid var(--border);
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
.doc-tabs :deep(.v-tab) {
|
|
1402
|
-
font-size: var(--fs-cell);
|
|
1403
|
-
letter-spacing: normal;
|
|
1404
|
-
min-width: 0;
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
1206
|
.table-download-btn {
|
|
1408
1207
|
min-width: 106px;
|
|
1409
1208
|
height: 30px;
|
|
@@ -32,18 +32,28 @@
|
|
|
32
32
|
</v-col>
|
|
33
33
|
|
|
34
34
|
<v-col cols="12" class="mt-5">
|
|
35
|
-
<!--
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
v-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/>
|
|
35
|
+
<!-- UPPER QR -->
|
|
36
|
+
<v-row no-gutters>
|
|
37
|
+
<v-col cols="6">
|
|
38
|
+
<img v-if="qrSmall" :src="qrSmall" width="80" height="80" />
|
|
39
|
+
</v-col>
|
|
40
|
+
<v-col cols="6">
|
|
41
|
+
<img v-if="qrSmall" :src="qrSmall" width="80" height="80" />
|
|
42
|
+
</v-col>
|
|
43
|
+
</v-row>
|
|
44
|
+
|
|
45
|
+
<!-- CENTER QR -->
|
|
46
|
+
<img v-if="qrLarge" :src="qrLarge" width="130" height="130" class="my-2" />
|
|
47
|
+
|
|
48
|
+
<!-- LOWER QR -->
|
|
49
|
+
<v-row no-gutters>
|
|
50
|
+
<v-col cols="6">
|
|
51
|
+
<img v-if="qrSmall" :src="qrSmall" width="80" height="80" />
|
|
52
|
+
</v-col>
|
|
53
|
+
<v-col cols="6">
|
|
54
|
+
<img v-if="qrSmall" :src="qrSmall" width="80" height="80" />
|
|
55
|
+
</v-col>
|
|
56
|
+
</v-row>
|
|
47
57
|
</v-col>
|
|
48
58
|
|
|
49
59
|
<v-col cols="12" class="mt-5">
|
|
@@ -80,11 +90,12 @@ defineProps({
|
|
|
80
90
|
|
|
81
91
|
const emit = defineEmits(["update:modelValue"]);
|
|
82
92
|
|
|
93
|
+
const qrSmall = ref<string>("");
|
|
83
94
|
const qrLarge = ref<string>("");
|
|
84
95
|
|
|
85
96
|
onMounted(async () => {
|
|
86
|
-
|
|
87
|
-
qrLarge.value = await QRCode.toDataURL("123123", { width:
|
|
97
|
+
qrSmall.value = await QRCode.toDataURL("123123", { width: 80, margin: 1 });
|
|
98
|
+
qrLarge.value = await QRCode.toDataURL("123123", { width: 130, margin: 1 });
|
|
88
99
|
});
|
|
89
100
|
|
|
90
101
|
const now = new Date();
|
|
@@ -101,11 +112,4 @@ const timeStr = now.toLocaleTimeString();
|
|
|
101
112
|
font-weight: var(--fw-card-title);
|
|
102
113
|
color: var(--text);
|
|
103
114
|
}
|
|
104
|
-
|
|
105
|
-
/* The dialog is 550px wide but narrows on small screens, so the QR is capped
|
|
106
|
-
at its own width rather than a fixed 260px that would overflow the card. */
|
|
107
|
-
.qr-preview__code {
|
|
108
|
-
max-width: 100%;
|
|
109
|
-
height: auto;
|
|
110
|
-
}
|
|
111
115
|
</style>
|
|
@@ -99,18 +99,10 @@
|
|
|
99
99
|
<template v-if="passType === 'NFC'">
|
|
100
100
|
<div class="mt-3">
|
|
101
101
|
<InputLabel title="Select Cards" />
|
|
102
|
-
|
|
103
|
-
operator scroll. Typing now goes to the endpoint as `search`
|
|
104
|
-
(debounced, page 1), and reaching the end of the list fetches
|
|
105
|
-
the next page. Vuetify's own client-side match on `cardNo` is
|
|
106
|
-
left on as well: API-core does not read `search` yet, so until
|
|
107
|
-
it does that is what makes typing narrow the list. -->
|
|
108
|
-
<v-autocomplete
|
|
102
|
+
<v-select
|
|
109
103
|
v-model="selectedCards"
|
|
110
|
-
v-model:search="cardSearch"
|
|
111
104
|
:items="filteredCardItems"
|
|
112
105
|
:loading="cardsLoading"
|
|
113
|
-
:hide-no-data="false"
|
|
114
106
|
item-title="cardNo"
|
|
115
107
|
item-value="_id"
|
|
116
108
|
density="comfortable"
|
|
@@ -118,30 +110,10 @@
|
|
|
118
110
|
chips
|
|
119
111
|
closable-chips
|
|
120
112
|
hide-details
|
|
121
|
-
placeholder="
|
|
113
|
+
placeholder="Select cards..."
|
|
122
114
|
return-object
|
|
123
|
-
@update:model-value="cardSearch = ''"
|
|
124
115
|
>
|
|
125
|
-
|
|
126
|
-
<!-- Sentinel for infinite scroll. Keyed on the page so it
|
|
127
|
-
re-mounts after each append: an IntersectionObserver only
|
|
128
|
-
reports changes, so a sentinel that stays in view after a
|
|
129
|
-
short page would otherwise never fire again. -->
|
|
130
|
-
<div
|
|
131
|
-
v-if="cardHasMore"
|
|
132
|
-
:key="cardPage"
|
|
133
|
-
v-intersect="onCardListEndReached"
|
|
134
|
-
class="d-flex justify-center py-2"
|
|
135
|
-
>
|
|
136
|
-
<v-progress-circular
|
|
137
|
-
v-if="cardsLoadingMore"
|
|
138
|
-
indeterminate
|
|
139
|
-
size="20"
|
|
140
|
-
width="2"
|
|
141
|
-
/>
|
|
142
|
-
</div>
|
|
143
|
-
</template>
|
|
144
|
-
</v-autocomplete>
|
|
116
|
+
</v-select>
|
|
145
117
|
</div>
|
|
146
118
|
|
|
147
119
|
<div class="mt-3">
|
|
@@ -350,7 +322,6 @@ const props = defineProps({
|
|
|
350
322
|
const emit = defineEmits(["update:modelValue", "update:quantity", "update:cards", "update:availableQrCount"]);
|
|
351
323
|
|
|
352
324
|
const { getAvailableContractorCards, generateQrVms } = useAccessManagement();
|
|
353
|
-
const { debounce } = useUtils();
|
|
354
325
|
|
|
355
326
|
const nfcEnabled = computed(() => props.settings?.data?.settings?.nfcPass ?? false);
|
|
356
327
|
const hidQrCodeEnabled = computed(() => {
|
|
@@ -396,53 +367,21 @@ const filteredCardItems = computed(() =>
|
|
|
396
367
|
: cardItems.value
|
|
397
368
|
);
|
|
398
369
|
const cardsLoading = ref(false);
|
|
399
|
-
const cardsLoadingMore = ref(false);
|
|
400
|
-
// Typed text in the card picker. Reset after each pick so the next search
|
|
401
|
-
// starts from the full list instead of staying narrowed to the last term.
|
|
402
|
-
const cardSearch = ref("");
|
|
403
|
-
const CARD_PAGE_SIZE = 100;
|
|
404
|
-
const cardPage = ref(1);
|
|
405
|
-
const cardHasMore = ref(false);
|
|
406
|
-
// Bumped on every request. A response whose number is no longer current is
|
|
407
|
-
// dropped, so a slow page-1 search cannot overwrite a newer one and a
|
|
408
|
-
// load-more cannot append onto a list that has since been replaced.
|
|
409
|
-
let cardRequestSeq = 0;
|
|
410
370
|
const generating = ref(false);
|
|
411
371
|
const availableCount = ref<number | null>(null);
|
|
412
372
|
|
|
413
|
-
|
|
414
|
-
// carry the search term and only NFC reacts to typing. The QR path keeps
|
|
415
|
-
// its page-1, no-search fetch and its top-up generation untouched.
|
|
416
|
-
const cardFetchType = computed<"NFC" | "QRCODE" | null>(() =>
|
|
417
|
-
passType.value === "NFC" ? "NFC" : passType.value === "QR" ? "QRCODE" : null,
|
|
418
|
-
);
|
|
419
|
-
|
|
420
|
-
async function fetchCards(
|
|
421
|
-
type: "NFC" | "QRCODE" = "NFC",
|
|
422
|
-
opts: { page?: number; append?: boolean } = {},
|
|
423
|
-
) {
|
|
373
|
+
async function fetchCards(type: "NFC" | "QRCODE" = "NFC") {
|
|
424
374
|
if (!props.siteId || !props.unitId) return;
|
|
425
|
-
|
|
426
|
-
const append = opts.append === true;
|
|
427
|
-
const seq = ++cardRequestSeq;
|
|
428
|
-
const query = {
|
|
429
|
-
type,
|
|
430
|
-
siteId: props.siteId,
|
|
431
|
-
unitId: props.unitId,
|
|
432
|
-
page,
|
|
433
|
-
limit: CARD_PAGE_SIZE,
|
|
434
|
-
// Vuetify writes null into the search model when it clears the field.
|
|
435
|
-
search: type === "NFC" ? cardSearch.value ?? "" : undefined,
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
if (append) cardsLoadingMore.value = true;
|
|
439
|
-
else cardsLoading.value = true;
|
|
375
|
+
cardsLoading.value = true;
|
|
440
376
|
if (type === "QRCODE") emit("update:availableQrCount", null);
|
|
441
377
|
try {
|
|
442
|
-
const res = await getAvailableContractorCards(
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
378
|
+
const res = await getAvailableContractorCards({
|
|
379
|
+
type,
|
|
380
|
+
siteId: props.siteId,
|
|
381
|
+
unitId: props.unitId,
|
|
382
|
+
});
|
|
383
|
+
cardItems.value = res?.data?.[0]?.items ?? [];
|
|
384
|
+
availableCount.value = (res?.data?.[2] as any)?.count ?? cardItems.value.length;
|
|
446
385
|
|
|
447
386
|
if (type === "QRCODE" && props.settings?.data?._id && availableCount.value !== null && availableCount.value > 0 && availableCount.value < 50) {
|
|
448
387
|
cardsLoading.value = false;
|
|
@@ -453,64 +392,30 @@ async function fetchCards(
|
|
|
453
392
|
quantity: 50,
|
|
454
393
|
});
|
|
455
394
|
// Re-fetch to get the updated count after generation
|
|
456
|
-
const refreshed = await getAvailableContractorCards(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
395
|
+
const refreshed = await getAvailableContractorCards({
|
|
396
|
+
type,
|
|
397
|
+
siteId: props.siteId,
|
|
398
|
+
unitId: props.unitId,
|
|
399
|
+
});
|
|
400
|
+
cardItems.value = refreshed?.data?.[0]?.items ?? [];
|
|
401
|
+
availableCount.value = (refreshed?.data?.[2] as any)?.count ?? cardItems.value.length;
|
|
460
402
|
generating.value = false;
|
|
461
403
|
}
|
|
462
404
|
|
|
463
|
-
cardItems.value = append ? [...cardItems.value, ...items] : items;
|
|
464
|
-
cardPage.value = page;
|
|
465
|
-
// A short page is the last one. An exact multiple of the page size costs
|
|
466
|
-
// one extra, empty fetch, which then turns this off.
|
|
467
|
-
cardHasMore.value = items.length === CARD_PAGE_SIZE;
|
|
468
|
-
|
|
469
405
|
if (type === "QRCODE") emit("update:availableQrCount", availableCount.value);
|
|
470
406
|
} catch (err: any) {
|
|
471
|
-
if (seq !== cardRequestSeq) return;
|
|
472
407
|
const msg = err?.data?.message ?? err?.message ?? String(err);
|
|
473
408
|
showSnackbar(`Entry pass service error: ${msg}`);
|
|
409
|
+
availableCount.value = null;
|
|
410
|
+
cardItems.value = [];
|
|
474
411
|
generating.value = false;
|
|
475
|
-
|
|
476
|
-
// clears the list as before.
|
|
477
|
-
if (!append) {
|
|
478
|
-
availableCount.value = null;
|
|
479
|
-
cardItems.value = [];
|
|
480
|
-
cardHasMore.value = false;
|
|
481
|
-
if (type === "QRCODE") emit("update:availableQrCount", null);
|
|
482
|
-
}
|
|
412
|
+
if (type === "QRCODE") emit("update:availableQrCount", null);
|
|
483
413
|
} finally {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (seq === cardRequestSeq) {
|
|
487
|
-
cardsLoading.value = false;
|
|
488
|
-
cardsLoadingMore.value = false;
|
|
489
|
-
generating.value = false;
|
|
490
|
-
}
|
|
414
|
+
cardsLoading.value = false;
|
|
415
|
+
generating.value = false;
|
|
491
416
|
}
|
|
492
417
|
}
|
|
493
418
|
|
|
494
|
-
function onCardListEndReached(isIntersecting: boolean) {
|
|
495
|
-
if (!isIntersecting || !cardHasMore.value) return;
|
|
496
|
-
if (cardsLoading.value || cardsLoadingMore.value) return;
|
|
497
|
-
const type = cardFetchType.value;
|
|
498
|
-
if (!type) return;
|
|
499
|
-
fetchCards(type, { page: cardPage.value + 1, append: true });
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const searchCards = debounce(() => {
|
|
503
|
-
if (cardFetchType.value !== "NFC") return;
|
|
504
|
-
fetchCards("NFC");
|
|
505
|
-
}, 500);
|
|
506
|
-
|
|
507
|
-
// Vuetify hands back null when it clears the field, and re-sets "" on blur;
|
|
508
|
-
// only a real change of the trimmed term should cost a request.
|
|
509
|
-
watch(cardSearch, (val, old) => {
|
|
510
|
-
if ((val ?? "").trim() === (old ?? "").trim()) return;
|
|
511
|
-
searchCards();
|
|
512
|
-
});
|
|
513
|
-
|
|
514
419
|
function addCard(card: any) {
|
|
515
420
|
const already = selectedCards.value.some((c) => c._id === card._id);
|
|
516
421
|
if (!already) selectedCards.value = [...selectedCards.value, card];
|
|
@@ -482,55 +482,6 @@
|
|
|
482
482
|
</draggable>
|
|
483
483
|
</section>
|
|
484
484
|
|
|
485
|
-
<!-- RESIDENT CHECKLISTS -->
|
|
486
|
-
<v-col cols="12" class="mt-4 border-t">
|
|
487
|
-
<v-row align="center" no-gutters class="pt-5">
|
|
488
|
-
<v-col cols="9" class="d-flex align-center text-body-2">
|
|
489
|
-
<v-row no-gutters>
|
|
490
|
-
<v-col cols="12" class="header-style">
|
|
491
|
-
Resident Checklists
|
|
492
|
-
</v-col>
|
|
493
|
-
<v-col cols="12" class="text-body-2 text-medium-emphasis">
|
|
494
|
-
Allow residents to view and complete assigned checklists. Each
|
|
495
|
-
checklist requires a photo upload before submission.
|
|
496
|
-
</v-col>
|
|
497
|
-
</v-row>
|
|
498
|
-
</v-col>
|
|
499
|
-
<v-col cols="3" class="d-flex align-center justify-end">
|
|
500
|
-
<v-switch
|
|
501
|
-
v-model="facility.isResidentChecklistEnabled"
|
|
502
|
-
class="d-flex align-center"
|
|
503
|
-
color="accent"
|
|
504
|
-
hide-details
|
|
505
|
-
@update:model-value="onToggleResidentChecklist"
|
|
506
|
-
/>
|
|
507
|
-
</v-col>
|
|
508
|
-
</v-row>
|
|
509
|
-
|
|
510
|
-
<!-- SECURITY CHECKLISTS -->
|
|
511
|
-
<v-row align="center" no-gutters class="border-t pt-5 mt-5">
|
|
512
|
-
<v-col cols="9" class="d-flex align-center text-body-2">
|
|
513
|
-
<v-row no-gutters>
|
|
514
|
-
<v-col cols="12" class="header-style">
|
|
515
|
-
Security Checklists
|
|
516
|
-
</v-col>
|
|
517
|
-
<v-col cols="12" class="text-body-2 text-medium-emphasis">
|
|
518
|
-
Allow Security to view and complete assigned checklists
|
|
519
|
-
</v-col>
|
|
520
|
-
</v-row>
|
|
521
|
-
</v-col>
|
|
522
|
-
<v-col cols="3" class="d-flex align-center justify-end">
|
|
523
|
-
<v-switch
|
|
524
|
-
v-model="facility.isSecurityChecklistEnabled"
|
|
525
|
-
class="d-flex align-center"
|
|
526
|
-
color="accent"
|
|
527
|
-
hide-details
|
|
528
|
-
@update:model-value="onToggleSecurityChecklist"
|
|
529
|
-
/>
|
|
530
|
-
</v-col>
|
|
531
|
-
</v-row>
|
|
532
|
-
</v-col>
|
|
533
|
-
|
|
534
485
|
<v-col cols="12" class="mt-10">
|
|
535
486
|
<p class="header-style font-weight-bold mb-5 mt-md-0 ml-md-4">
|
|
536
487
|
Other Settings
|
|
@@ -1069,20 +1020,6 @@ watchEffect(() => {
|
|
|
1069
1020
|
}
|
|
1070
1021
|
});
|
|
1071
1022
|
|
|
1072
|
-
/**
|
|
1073
|
-
* One checklist, one owner, and always one. A facility is completed either by
|
|
1074
|
-
* its residents or by Security, so the two switches are each other's inverse:
|
|
1075
|
-
* turning either off turns the other on. There is no state where a facility
|
|
1076
|
-
* has no checklist owner.
|
|
1077
|
-
*/
|
|
1078
|
-
function onToggleResidentChecklist(enabled: boolean | null) {
|
|
1079
|
-
facility.value.isSecurityChecklistEnabled = !enabled;
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
function onToggleSecurityChecklist(enabled: boolean | null) {
|
|
1083
|
-
facility.value.isResidentChecklistEnabled = !enabled;
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
1023
|
function onToggleBookingUnitLimit(val: any) {
|
|
1087
1024
|
if (val) {
|
|
1088
1025
|
isBookingUnitLimitEnabled.value = true;
|