@7365admin1/layer-common 4.90.2-staging.490 → 4.90.2
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 -57
- package/assets/css/screens.css +4 -8
- package/components/AccessCardDetailsDialog.vue +6 -59
- package/components/AccessCardPreviewDialog.vue +53 -371
- package/components/AppSelect.vue +4 -143
- package/components/BuildingUnitFormEdit.vue +0 -6
- package/components/Dialog/UpdateMoreAction.vue +1 -11
- package/components/DocumentForm.vue +112 -407
- package/components/DocumentManagement.vue +12 -213
- package/components/Facility/BookingSetup.vue +0 -63
- package/components/HidAccessLogDashboard.vue +17 -74
- package/components/HidIntercomManagement.vue +14 -183
- package/components/HidQrCodeConfiguration.vue +195 -17
- package/components/HidReaderManagement.vue +0 -398
- package/components/HidReaderUserRoster.vue +3 -44
- package/components/HidUserEnrollment.vue +292 -2255
- package/components/InvitationClientForm.vue +1 -19
- package/components/InvitationForm.vue +4 -31
- package/components/InvitationMain.vue +22 -124
- package/components/MemberInformation.vue +4 -4
- package/components/PassInformation.vue +2 -2
- package/components/SiteSettings.vue +0 -48
- package/components/TableMain.vue +7 -22
- package/components/VehicleManagement.vue +5 -80
- package/components/VisitorForm.vue +19 -65
- package/components/VisitorManagement.vue +5 -267
- package/composables/useAccessManagement.ts +1 -17
- package/composables/useDocument.ts +0 -7
- package/composables/useFacility.ts +0 -11
- package/composables/useHidAmico.ts +0 -165
- package/composables/useHidNavigation.ts +1 -25
- package/composables/useMember.ts +0 -12
- package/composables/useNFCPatrolReportFilters.ts +3 -21
- package/composables/useNFCPatrolRoute.ts +1 -8
- package/composables/usePeople.ts +0 -19
- package/composables/useServiceProvider.ts +2 -45
- package/composables/useSettingsPermission.ts +0 -14
- package/composables/useUser.ts +2 -6
- package/composables/useVehicle.ts +1 -40
- package/composables/useVerification.ts +0 -3
- package/middleware/01.auth.ts +7 -3
- package/package.json +1 -1
- 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/types/document.d.ts +0 -14
- package/types/facility.d.ts +0 -4
- package/types/member.d.ts +0 -4
- package/types/people.d.ts +1 -28
- package/types/service-provider.d.ts +0 -5
- package/types/site.d.ts +1 -7
- package/components/BuildingUnitManagement.vue +0 -298
- package/components/HidAccessPermissions.vue +0 -424
- package/components/HidProfileQrCode.vue +0 -332
- package/components/VehicleQrStickerDialog.vue +0 -249
- 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/hid-enrolment-subject.ts +0 -344
- package/utils/hid-permission-assignments.ts +0 -198
- package/utils/hid-reader-selection.ts +0 -52
- package/utils/invite-group.ts +0 -61
- package/utils/invite-sites.ts +0 -25
- package/utils/occupancy-role.ts +0 -109
- package/utils/service-type.ts +0 -30
- package/utils/vehicle-qr-sticker.ts +0 -184
|
@@ -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;
|
|
@@ -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;
|
|
@@ -376,7 +376,6 @@ const visitorRecords = ref<Record<string, HidAccessRecord>>({});
|
|
|
376
376
|
const logs = ref<HidAccessRecord[]>([]);
|
|
377
377
|
const liveAccessLogs = ref<HidAccessRecord[]>([]);
|
|
378
378
|
const administratorUserIds = ref<Set<number>>(new Set());
|
|
379
|
-
const { selectHidReader, resolveForSite } = useHidReaderSelection();
|
|
380
379
|
const selectedReaderId = ref("");
|
|
381
380
|
const activeTab = ref<AccessTab>("all");
|
|
382
381
|
const search = ref("");
|
|
@@ -402,10 +401,7 @@ const exportForm = reactive({
|
|
|
402
401
|
});
|
|
403
402
|
|
|
404
403
|
const statusOptions = ["Authorized", "Not Authorized", "Unknown"];
|
|
405
|
-
const accessMethodFilterOptions = ["Facial", "QR Code", "ID and Password", "PIN", "Card"
|
|
406
|
-
|
|
407
|
-
/** The persisted event the resident app records when it opens a door on a call. */
|
|
408
|
-
const INTERCOM_DOOR_OPEN_EVENT = "intercom_door_open";
|
|
404
|
+
const accessMethodFilterOptions = ["Facial", "QR Code", "ID and Password", "PIN", "Card"];
|
|
409
405
|
const accessMethodOptions = ["All", ...accessMethodFilterOptions];
|
|
410
406
|
|
|
411
407
|
const readerOptions = computed(() =>
|
|
@@ -593,11 +589,9 @@ async function loadReaders() {
|
|
|
593
589
|
if (!props.site) return;
|
|
594
590
|
const response = await getReaders({ site: props.site, page: 1, limit: 100 });
|
|
595
591
|
readers.value = response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
an id the site does not own. */
|
|
600
|
-
selectedReaderId.value = resolveForSite(props.site, readers.value);
|
|
592
|
+
if (!selectedReaderId.value && readers.value.length) {
|
|
593
|
+
selectedReaderId.value = readers.value[0]._id;
|
|
594
|
+
}
|
|
601
595
|
}
|
|
602
596
|
|
|
603
597
|
async function loadDashboard() {
|
|
@@ -612,7 +606,7 @@ async function loadDashboard() {
|
|
|
612
606
|
resetDashboardData();
|
|
613
607
|
try {
|
|
614
608
|
const isVisitorTab = activeTab.value === "visitor";
|
|
615
|
-
const [logResponse,
|
|
609
|
+
const [logResponse, hidAccessLogResponse, visitorResponse, readerUserResponse] = await Promise.all([
|
|
616
610
|
loadDashboardSource(
|
|
617
611
|
getLogs(selectedReaderId.value, {
|
|
618
612
|
page: page.value,
|
|
@@ -621,20 +615,6 @@ async function loadDashboard() {
|
|
|
621
615
|
null,
|
|
622
616
|
"persisted access logs",
|
|
623
617
|
),
|
|
624
|
-
// Door opens from the resident app during an intercom call. The reader
|
|
625
|
-
// never logs a DTMF door open, so these exist only as persisted events
|
|
626
|
-
// and are fetched by type: the general page above is shared with every
|
|
627
|
-
// other event the reader emits and is dropped whenever the reader has
|
|
628
|
-
// live logs, which it always has.
|
|
629
|
-
loadDashboardSource(
|
|
630
|
-
getLogs(selectedReaderId.value, {
|
|
631
|
-
page: page.value,
|
|
632
|
-
limit,
|
|
633
|
-
type: INTERCOM_DOOR_OPEN_EVENT,
|
|
634
|
-
}),
|
|
635
|
-
null,
|
|
636
|
-
"intercom door-open logs",
|
|
637
|
-
),
|
|
638
618
|
loadDashboardSource(
|
|
639
619
|
getReaderAccessLogs(selectedReaderId.value, {
|
|
640
620
|
page: page.value,
|
|
@@ -701,14 +681,9 @@ async function loadDashboard() {
|
|
|
701
681
|
// identity metadata here: it can contain a stale photo from another user.
|
|
702
682
|
allIdentities.value = readerUsers;
|
|
703
683
|
identities.value = readerUsers;
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
? []
|
|
708
|
-
: logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? []),
|
|
709
|
-
// Always kept: the reader has no live counterpart for these.
|
|
710
|
-
...intercomItems,
|
|
711
|
-
];
|
|
684
|
+
logs.value = liveItems.length
|
|
685
|
+
? []
|
|
686
|
+
: logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? [];
|
|
712
687
|
liveAccessLogs.value = liveItems;
|
|
713
688
|
serverPages.value = Number(hidAccessLogResponse?.pages ?? hidAccessLogResponse?.data?.pages ?? 1);
|
|
714
689
|
serverPageRange.value = String(
|
|
@@ -844,8 +819,6 @@ function getAccessMethod(identity: HidAccessRecord, log?: HidAccessRecord) {
|
|
|
844
819
|
id_password: "ID and Password",
|
|
845
820
|
pin: "PIN",
|
|
846
821
|
card: "Card",
|
|
847
|
-
// Set by the API on intercom_door_open events; no reader field implies it.
|
|
848
|
-
intercom: "Intercom",
|
|
849
822
|
};
|
|
850
823
|
const normalizedMethod = String(payload.normalizedMethod || "");
|
|
851
824
|
if (normalizedMethodLabels[normalizedMethod]) return normalizedMethodLabels[normalizedMethod];
|
|
@@ -860,36 +833,11 @@ function getAccessMethod(identity: HidAccessRecord, log?: HidAccessRecord) {
|
|
|
860
833
|
return identity.metadata?.facialData || identity.metadata?.imageTimestamp ? "Facial" : "N/A";
|
|
861
834
|
}
|
|
862
835
|
|
|
863
|
-
/**
|
|
864
|
-
* One shape for the whole Access Date and Time column: `15:18:43 23/9/2026`.
|
|
865
|
-
*
|
|
866
|
-
* Reader rows arrive as a number and went through `formatHidEpochDate`; an
|
|
867
|
-
* intercom door open never touches the reader, so the API writes it as an ISO
|
|
868
|
-
* string and this branch took `toLocaleString` instead - which put
|
|
869
|
-
* "9/23/2026, 2:15:29 PM" in the same column, one row below "15:18:43
|
|
870
|
-
* 23/9/2026". Both branches now build the same string.
|
|
871
|
-
*
|
|
872
|
-
* Local parts here, UTC parts there, on purpose: the reader encodes its own
|
|
873
|
-
* wall clock as if it were UTC, while an ISO string is a real instant. Reading
|
|
874
|
-
* each the way it was written is what makes both columns show the time that was
|
|
875
|
-
* on the clock at the door.
|
|
876
|
-
*/
|
|
877
836
|
function formatDate(value?: string | number) {
|
|
878
837
|
if (!value) return "N/A";
|
|
879
838
|
if (typeof value === "number") return formatHidEpochDate(value);
|
|
880
|
-
const date = new Date(value);
|
|
881
|
-
|
|
882
|
-
return formatClock(
|
|
883
|
-
[date.getHours(), date.getMinutes(), date.getSeconds()],
|
|
884
|
-
date.getDate(),
|
|
885
|
-
date.getMonth() + 1,
|
|
886
|
-
date.getFullYear(),
|
|
887
|
-
);
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
function formatClock(time: number[], day: number, month: number, year: number) {
|
|
891
|
-
const clock = time.map((part) => String(part).padStart(2, "0")).join(":");
|
|
892
|
-
return `${clock} ${day}/${month}/${year}`;
|
|
839
|
+
const date = typeof value === "number" ? new Date(value * 1000) : new Date(value);
|
|
840
|
+
return Number.isNaN(date.getTime()) ? "N/A" : date.toLocaleString();
|
|
893
841
|
}
|
|
894
842
|
|
|
895
843
|
function parseDate(value?: string | number) {
|
|
@@ -919,12 +867,13 @@ function formatHidEpochDate(value: number) {
|
|
|
919
867
|
const date = new Date(value * 1000);
|
|
920
868
|
if (Number.isNaN(date.getTime())) return "N/A";
|
|
921
869
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
date.
|
|
925
|
-
date.
|
|
926
|
-
|
|
927
|
-
|
|
870
|
+
const time = [
|
|
871
|
+
date.getUTCHours(),
|
|
872
|
+
date.getUTCMinutes(),
|
|
873
|
+
date.getUTCSeconds(),
|
|
874
|
+
].map((part) => String(part).padStart(2, "0")).join(":");
|
|
875
|
+
|
|
876
|
+
return `${time} ${date.getUTCDate()}/${date.getUTCMonth() + 1}/${date.getUTCFullYear()}`;
|
|
928
877
|
}
|
|
929
878
|
|
|
930
879
|
function openHistoryDialog(row: AccessRow) {
|
|
@@ -1599,12 +1548,6 @@ function escapeHtml(value: unknown) {
|
|
|
1599
1548
|
.replace(/"/g, """)
|
|
1600
1549
|
.replace(/'/g, "'");
|
|
1601
1550
|
}
|
|
1602
|
-
|
|
1603
|
-
/* Publish every change, including the seed above: a screen that had to fall
|
|
1604
|
-
back to the first reader should leave the others agreeing with it. */
|
|
1605
|
-
watch(selectedReaderId, (value) => {
|
|
1606
|
-
if (value) selectHidReader(props.site, value);
|
|
1607
|
-
});
|
|
1608
1551
|
</script>
|
|
1609
1552
|
|
|
1610
1553
|
<style scoped>
|