@7365admin1/layer-common 3.2.1 → 3.2.2-staging.101
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/.changeset/camera-wall-gated-endpoint.md +47 -0
- package/.changeset/camera-wall-plain-english.md +60 -0
- package/.changeset/camera-wall-selection-and-guidance.md +65 -0
- package/.changeset/dark-primary-contrast.md +39 -0
- package/.changeset/dashboard-dark-theme-contrast.md +15 -0
- package/.changeset/notification-preferences.md +17 -0
- package/.changeset/pm-sidebar-grouping-and-list-scaffold.md +27 -0
- package/.changeset/service-provider-invitation-actions.md +28 -0
- package/assets/css/primitives.css +680 -0
- package/assets/css/screens.css +541 -0
- package/assets/css/shell.css +258 -0
- package/assets/css/tokens.css +220 -0
- package/components/AccessCardQrTagging.vue +1 -1
- package/components/AccessManagement.vue +53 -72
- package/components/AppButton.vue +52 -0
- package/components/AppCard.vue +20 -0
- package/components/AppDateField.vue +35 -0
- package/components/AppField.vue +51 -0
- package/components/AppKpiTile.vue +65 -0
- package/components/AppSegmented.vue +38 -0
- package/components/AppSelect.vue +203 -0
- package/components/AreaChecklistHistoryLogs.vue +51 -44
- package/components/AreaChecklistHistoryMain.vue +6 -2
- package/components/AreaMain.vue +9 -17
- package/components/AttendanceMain.vue +4 -3
- package/components/Avatar/Main.vue +54 -2
- package/components/BillingMain.vue +26 -30
- package/components/BuildingManagement/buildings.vue +27 -38
- package/components/BuildingManagement/units.vue +26 -42
- package/components/BulletinBoardManagement.vue +23 -10
- package/components/CameraWall.vue +1034 -0
- package/components/CameraWallTile.vue +818 -0
- package/components/CardToolbar.vue +37 -0
- package/components/CleaningScheduleMain.vue +29 -46
- package/components/ClientMain.vue +90 -78
- package/components/DashboardEmptyState.vue +11 -1
- package/components/DashboardMain.vue +349 -275
- package/components/DashboardPanel.vue +1 -1
- package/components/DocumentManagement.vue +129 -93
- package/components/EntryPassInformation.vue +4 -4
- package/components/EntryPassMain.vue +49 -27
- package/components/EquipmentItemMain.vue +4 -6
- package/components/EquipmentManagementMain.vue +9 -7
- package/components/FeedbackMain.vue +42 -56
- package/components/FormDialog.vue +60 -21
- package/components/HidAccessLogDashboard.vue +119 -44
- package/components/HidCameraCaptureDialog.vue +199 -0
- package/components/HidIntercomManagement.vue +825 -202
- package/components/HidQrCodeConfiguration.vue +927 -93
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +454 -106
- package/components/InvitationMain.vue +46 -61
- package/components/Layout/Header.vue +289 -37
- package/components/Layout/NavigationDrawer.vue +340 -30
- package/components/ManageChecklistMain.vue +17 -17
- package/components/MemberMain.vue +79 -60
- package/components/MyAttendanceMain.vue +3 -5
- package/components/NavigationItem.vue +9 -0
- package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
- package/components/NotificationSettings.vue +361 -0
- package/components/OnlineFormConfigurationForm.vue +620 -224
- package/components/OnlineFormFill.vue +36 -8
- package/components/OnlineFormsConfiguration.vue +7 -3
- package/components/PageHeader.vue +43 -0
- package/components/PassInformation.vue +104 -45
- package/components/QrTemplate/CreditCardLandscape.vue +19 -12
- package/components/QrTemplate/PrintDialog.vue +209 -196
- package/components/RolePermissionMain.vue +184 -91
- package/components/ScanVisitorQRCode.vue +35 -7
- package/components/ScheduleAreaMain.vue +15 -49
- package/components/ScheduleTaskMain.vue +23 -26
- package/components/ScheduleTastTicketMain.vue +18 -43
- package/components/ServiceProviderInvitationPrompt.vue +150 -0
- package/components/ServiceProviderMain.vue +346 -157
- package/components/SiteSettings.vue +25 -10
- package/components/SiteSettingsWorkOrder.vue +61 -30
- package/components/SpecificAttr.vue +10 -2
- package/components/StatusChip.vue +93 -0
- package/components/StockCard.vue +17 -12
- package/components/SwitchContext.vue +33 -1
- package/components/TableHygiene.vue +29 -46
- package/components/TableList.vue +1 -1
- package/components/TableListSecondary.vue +1 -1
- package/components/TableMain.vue +161 -77
- package/components/TableV3.vue +61 -66
- package/components/UnitMain.vue +9 -17
- package/components/VehicleManagement.vue +20 -9
- package/components/VisitorForm.vue +294 -78
- package/components/VisitorManagement.vue +221 -71
- package/components/VisitorSocketPopUp.vue +56 -2
- package/components/WorkOrder/Main.vue +42 -51
- package/composables/useComment.ts +3 -3
- package/composables/useHidAmico.ts +151 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useLocalAuth.ts +9 -36
- package/composables/useNotificationPreference.ts +87 -0
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useOrg.ts +1 -1
- package/composables/useRole.ts +22 -0
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +336 -0
- package/composables/useSiteSettings.ts +50 -0
- package/composables/useThemePreference.ts +63 -0
- package/composables/useVisitor.ts +6 -1
- package/composables/useVisitorSocket.ts +26 -0
- package/composables/useWebUsb.ts +127 -37
- package/nuxt.config.ts +31 -0
- package/package.json +4 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/pages/notification-settings.vue +19 -0
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +68 -9
- package/test/visitor-socket.test.mjs +36 -0
- package/types/org.d.ts +2 -0
- package/types/site.d.ts +71 -0
- package/utils/camera-wall.test.ts +571 -0
- package/utils/camera-wall.ts +926 -0
- package/utils/nav-sections.test.ts +190 -0
- package/utils/nav-sections.ts +151 -0
- package/utils/status.test.ts +77 -0
- package/utils/status.ts +90 -0
- package/utils/theme.test.ts +534 -0
- package/utils/theme.ts +294 -0
- package/components/HidIdentityMapping.vue +0 -975
- package/middleware/member.ts +0 -4
- package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
|
@@ -1,50 +1,28 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-row no-gutters>
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@row-click="handleRowClick"
|
|
16
|
-
>
|
|
17
|
-
<template #actions>
|
|
18
|
-
<v-row no-gutters>
|
|
19
|
-
<v-btn
|
|
20
|
-
v-if="canAddVisitor"
|
|
21
|
-
class="text-none"
|
|
22
|
-
rounded="pill"
|
|
23
|
-
variant="tonal"
|
|
24
|
-
size="large"
|
|
25
|
-
@click="handleAddNew"
|
|
26
|
-
text="Add Visitor"
|
|
27
|
-
/>
|
|
28
|
-
<v-btn
|
|
3
|
+
<!--
|
|
4
|
+
THE DESIGN'S PAGE TITLE ROW (Phase 9).
|
|
5
|
+
|
|
6
|
+
Drawn here rather than through `TableMain`'s `title` prop because the
|
|
7
|
+
design puts the screen's actions on the title line, and `TableMain`
|
|
8
|
+
renders its `actions` slot as a separate row under the heading. Same
|
|
9
|
+
three buttons, same handlers, same permission gates - moved, not changed.
|
|
10
|
+
-->
|
|
11
|
+
<v-col cols="12">
|
|
12
|
+
<PageHeader title="Visitor Mgmt">
|
|
13
|
+
<template #actions>
|
|
14
|
+
<AppButton
|
|
29
15
|
v-if="canScanVisitorQRCode"
|
|
30
|
-
|
|
31
|
-
class="text-none ml-2"
|
|
32
|
-
rounded="pill"
|
|
33
|
-
variant="tonal"
|
|
34
|
-
size="large"
|
|
16
|
+
variant="ghost"
|
|
35
17
|
@click="dialog.scanVisitorQRCode = true"
|
|
36
|
-
|
|
18
|
+
>
|
|
19
|
+
Scan QR Code
|
|
20
|
+
</AppButton>
|
|
37
21
|
<v-menu>
|
|
38
22
|
<template #activator="{ props }">
|
|
39
|
-
<v-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class="text-none ml-2"
|
|
43
|
-
prepend-icon="mdi-download"
|
|
44
|
-
rounded="pill"
|
|
45
|
-
variant="tonal"
|
|
46
|
-
size="large"
|
|
47
|
-
/>
|
|
23
|
+
<AppButton v-bind="props" variant="ghost" icon="mdi-download">
|
|
24
|
+
Download Report
|
|
25
|
+
</AppButton>
|
|
48
26
|
</template>
|
|
49
27
|
<v-list>
|
|
50
28
|
<v-list-item
|
|
@@ -59,8 +37,27 @@
|
|
|
59
37
|
/>
|
|
60
38
|
</v-list>
|
|
61
39
|
</v-menu>
|
|
62
|
-
|
|
63
|
-
|
|
40
|
+
<AppButton v-if="canAddVisitor" icon="mdi-plus" @click="handleAddNew">
|
|
41
|
+
Add Visitor
|
|
42
|
+
</AppButton>
|
|
43
|
+
</template>
|
|
44
|
+
</PageHeader>
|
|
45
|
+
</v-col>
|
|
46
|
+
|
|
47
|
+
<TableMain
|
|
48
|
+
:headers="headers"
|
|
49
|
+
:items="items"
|
|
50
|
+
:loading="getVisitorPending"
|
|
51
|
+
:page="page"
|
|
52
|
+
:pages="pages"
|
|
53
|
+
:extension-height="110"
|
|
54
|
+
:offset="300"
|
|
55
|
+
:pageRange="pageRange"
|
|
56
|
+
@refresh="getVisitorRefresh"
|
|
57
|
+
@update:page="handleUpdatePage"
|
|
58
|
+
show-header
|
|
59
|
+
@row-click="handleRowClick"
|
|
60
|
+
>
|
|
64
61
|
<template #extension>
|
|
65
62
|
<v-row no-gutters class="w-100 d-flex flex-column">
|
|
66
63
|
<v-tabs
|
|
@@ -109,32 +106,18 @@
|
|
|
109
106
|
hide-details
|
|
110
107
|
/>
|
|
111
108
|
<InputDatePicker v-model="dateTo" density="compact" hide-details />
|
|
112
|
-
<
|
|
109
|
+
<AppSelect
|
|
113
110
|
v-if="activeTab == 'registered'"
|
|
114
111
|
v-model="filterTypes"
|
|
115
112
|
label="Filter by types"
|
|
116
113
|
item-title="label"
|
|
117
114
|
item-value="value"
|
|
118
115
|
:items="visitorSelection"
|
|
119
|
-
|
|
116
|
+
class="visitor-type-filter"
|
|
117
|
+
compact
|
|
120
118
|
clearable
|
|
121
119
|
multiple
|
|
122
|
-
|
|
123
|
-
hide-details
|
|
124
|
-
>
|
|
125
|
-
<template v-slot:selection="{ item, index }">
|
|
126
|
-
<div class="d-flex align-center text-caption text-nowrap">
|
|
127
|
-
<v-chip
|
|
128
|
-
v-if="index === 0"
|
|
129
|
-
color="error"
|
|
130
|
-
:text="filterTypeSelectionLabel()"
|
|
131
|
-
size="x-small"
|
|
132
|
-
variant="tonal"
|
|
133
|
-
class="ml-2"
|
|
134
|
-
/>
|
|
135
|
-
</div>
|
|
136
|
-
</template>
|
|
137
|
-
</v-select>
|
|
120
|
+
/>
|
|
138
121
|
</v-card>
|
|
139
122
|
</v-row>
|
|
140
123
|
</template>
|
|
@@ -303,6 +286,18 @@
|
|
|
303
286
|
>
|
|
304
287
|
</div>
|
|
305
288
|
|
|
289
|
+
<v-btn
|
|
290
|
+
v-if="hidQrEnabled"
|
|
291
|
+
icon="mdi-eye-outline"
|
|
292
|
+
size="x-small"
|
|
293
|
+
variant="text"
|
|
294
|
+
color="primary"
|
|
295
|
+
title="View HID QR code"
|
|
296
|
+
aria-label="View HID QR code"
|
|
297
|
+
class="align-self-start mt-1"
|
|
298
|
+
@click.stop="openVisitorHidQr(item)"
|
|
299
|
+
/>
|
|
300
|
+
|
|
306
301
|
<v-menu :close-on-content-click="true">
|
|
307
302
|
<template v-slot:activator="{ props: menuProps }">
|
|
308
303
|
<div
|
|
@@ -588,7 +583,7 @@
|
|
|
588
583
|
:org="orgId"
|
|
589
584
|
:site="siteId"
|
|
590
585
|
:visitor-data="selectedVisitorDataObject"
|
|
591
|
-
:current-user="currentUser
|
|
586
|
+
:current-user="currentUser?._id"
|
|
592
587
|
:type="activeVisitorFormType"
|
|
593
588
|
:enable-unregistered="enableUnregistered"
|
|
594
589
|
@back="handleClickBack"
|
|
@@ -1177,12 +1172,52 @@
|
|
|
1177
1172
|
</v-card>
|
|
1178
1173
|
</v-dialog>
|
|
1179
1174
|
|
|
1175
|
+
<v-dialog v-model="hidQrDialog" max-width="440" persistent>
|
|
1176
|
+
<v-card>
|
|
1177
|
+
<v-toolbar color="transparent" density="comfortable" class="px-2">
|
|
1178
|
+
<v-toolbar-title class="text-subtitle-1 font-weight-bold">Visitor QR Code</v-toolbar-title>
|
|
1179
|
+
<v-btn icon="mdi-close" variant="text" title="Close" @click="hidQrDialog = false" />
|
|
1180
|
+
</v-toolbar>
|
|
1181
|
+
<v-card-text class="px-8 pb-6 text-center">
|
|
1182
|
+
<v-progress-circular v-if="hidQrLoading" indeterminate color="primary" class="my-10" />
|
|
1183
|
+
<template v-else-if="hidQrImageUrl">
|
|
1184
|
+
<div class="text-h6 font-weight-bold">{{ hidQrVisitor?.name || "Visitor Access Pass" }}</div>
|
|
1185
|
+
<div class="visitor-hid-qr my-5">
|
|
1186
|
+
<v-img :src="hidQrImageUrl" width="280" height="280" alt="Visitor HID QR code" />
|
|
1187
|
+
</div>
|
|
1188
|
+
<div class="text-caption text-medium-emphasis">
|
|
1189
|
+
Valid until {{ formatHidQrExpiry(hidQrExpiresAt) }}
|
|
1190
|
+
</div>
|
|
1191
|
+
<v-chip class="mt-2" size="small" :color="hidQrExpired ? 'error' : 'success'" variant="tonal">
|
|
1192
|
+
{{ hidQrExpired ? "Expired" : `Expires in ${formatHidQrCountdown(hidQrRemainingSeconds)}` }}
|
|
1193
|
+
</v-chip>
|
|
1194
|
+
</template>
|
|
1195
|
+
<v-alert v-if="hidQrError" type="warning" variant="tonal" density="compact" class="mt-4 text-left">
|
|
1196
|
+
{{ hidQrError }}
|
|
1197
|
+
</v-alert>
|
|
1198
|
+
<v-btn
|
|
1199
|
+
v-if="!hidQrLoading && (!hidQrImageUrl || hidQrExpired)"
|
|
1200
|
+
block
|
|
1201
|
+
color="primary-button"
|
|
1202
|
+
variant="flat"
|
|
1203
|
+
class="mt-4 text-none"
|
|
1204
|
+
prepend-icon="mdi-refresh"
|
|
1205
|
+
:loading="hidQrGenerating"
|
|
1206
|
+
@click="generateVisitorHidQr"
|
|
1207
|
+
>
|
|
1208
|
+
{{ hidQrExpired ? "Generate QR again" : "Generate HID QR code" }}
|
|
1209
|
+
</v-btn>
|
|
1210
|
+
</v-card-text>
|
|
1211
|
+
</v-card>
|
|
1212
|
+
</v-dialog>
|
|
1213
|
+
|
|
1180
1214
|
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
1181
1215
|
</v-row>
|
|
1182
1216
|
</template>
|
|
1183
1217
|
|
|
1184
1218
|
<script setup lang="ts">
|
|
1185
1219
|
import moment from "moment-timezone";
|
|
1220
|
+
import QRCode from "qrcode";
|
|
1186
1221
|
|
|
1187
1222
|
definePageMeta({
|
|
1188
1223
|
middleware: ["01-auth", "02-org"],
|
|
@@ -1244,6 +1279,8 @@ const {
|
|
|
1244
1279
|
} = useUtils();
|
|
1245
1280
|
const { formatLocation } = useSecurityUtils();
|
|
1246
1281
|
const { getFileUrlAnpr } = useFile();
|
|
1282
|
+
const { getSiteById } = useSite();
|
|
1283
|
+
const { getIdentities, issueVisitorQr } = useHidAmico();
|
|
1247
1284
|
// const { status: visitorStatus, search } = useRoute().query as { status: string, search: string};
|
|
1248
1285
|
|
|
1249
1286
|
const routeName = route.name;
|
|
@@ -1255,6 +1292,106 @@ const pageRange = ref("-- - -- of --");
|
|
|
1255
1292
|
const activeTab = ref("registered");
|
|
1256
1293
|
const activeVisitorFormType = ref<TVisitorType | null>(null);
|
|
1257
1294
|
const selectedVisitorId = ref<string | null>(""); // selected visitor for viewing/actions
|
|
1295
|
+
const hidQrDialog = ref(false);
|
|
1296
|
+
const hidQrLoading = ref(false);
|
|
1297
|
+
const hidQrGenerating = ref(false);
|
|
1298
|
+
const hidQrError = ref("");
|
|
1299
|
+
const hidQrImageUrl = ref("");
|
|
1300
|
+
const hidQrExpiresAt = ref("");
|
|
1301
|
+
const hidQrNow = ref(Date.now());
|
|
1302
|
+
const hidQrVisitor = ref<Record<string, any> | null>(null);
|
|
1303
|
+
const hidQrConfig = ref({ enabled: false, readerId: "", validityMinutes: 60 });
|
|
1304
|
+
let hidQrTimer: ReturnType<typeof setInterval> | null = null;
|
|
1305
|
+
const hidQrEnabled = computed(() => hidQrConfig.value.enabled && Boolean(hidQrConfig.value.readerId));
|
|
1306
|
+
const hidQrRemainingSeconds = computed(() => {
|
|
1307
|
+
const expiry = new Date(hidQrExpiresAt.value).getTime();
|
|
1308
|
+
return Number.isFinite(expiry) ? Math.max(0, Math.ceil((expiry - hidQrNow.value) / 1000)) : 0;
|
|
1309
|
+
});
|
|
1310
|
+
const hidQrExpired = computed(() => Boolean(hidQrExpiresAt.value) && hidQrRemainingSeconds.value === 0);
|
|
1311
|
+
|
|
1312
|
+
async function loadHidQrConfig() {
|
|
1313
|
+
try {
|
|
1314
|
+
const response: any = await getSiteById(siteId);
|
|
1315
|
+
const site = response?.data ?? response;
|
|
1316
|
+
const config = site?.metadata?.hidQrCodePass ?? {};
|
|
1317
|
+
hidQrConfig.value = {
|
|
1318
|
+
enabled: Boolean(config.enabled && config.onlineMode !== false),
|
|
1319
|
+
readerId: String(config.readerId || ""),
|
|
1320
|
+
validityMinutes: Number(config.validityMinutes) > 0 ? Number(config.validityMinutes) : 60,
|
|
1321
|
+
};
|
|
1322
|
+
} catch {
|
|
1323
|
+
hidQrConfig.value = { enabled: false, readerId: "", validityMinutes: 60 };
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function extractIdentityItems(response: any) {
|
|
1328
|
+
return response?.items ?? response?.data?.items ?? [];
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
async function openVisitorHidQr(visitor: Record<string, any>) {
|
|
1332
|
+
hidQrVisitor.value = visitor;
|
|
1333
|
+
hidQrDialog.value = true;
|
|
1334
|
+
hidQrLoading.value = true;
|
|
1335
|
+
hidQrError.value = "";
|
|
1336
|
+
hidQrImageUrl.value = "";
|
|
1337
|
+
hidQrExpiresAt.value = "";
|
|
1338
|
+
try {
|
|
1339
|
+
const response = await getIdentities(hidQrConfig.value.readerId, { type: "visitor", limit: 100 });
|
|
1340
|
+
const identity = extractIdentityItems(response).find(
|
|
1341
|
+
(item: any) => String(item.visitor || item.metadata?.visitor || "") === String(visitor._id),
|
|
1342
|
+
);
|
|
1343
|
+
const qrValue = String(identity?.metadata?.qrValue || identity?.cardNo || "");
|
|
1344
|
+
hidQrExpiresAt.value = String(identity?.metadata?.expiresAt || "");
|
|
1345
|
+
if (!qrValue || !hidQrExpiresAt.value) {
|
|
1346
|
+
hidQrError.value = "This visitor does not have an HID QR code yet.";
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
hidQrImageUrl.value = await QRCode.toDataURL(qrValue, { width: 360, margin: 2, errorCorrectionLevel: "M" });
|
|
1350
|
+
hidQrNow.value = Date.now();
|
|
1351
|
+
} catch (error: unknown) {
|
|
1352
|
+
hidQrError.value = errorConverter(error) || "Unable to load the visitor HID QR code.";
|
|
1353
|
+
} finally {
|
|
1354
|
+
hidQrLoading.value = false;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
async function generateVisitorHidQr() {
|
|
1359
|
+
if (!hidQrVisitor.value?._id || !hidQrConfig.value.readerId) return;
|
|
1360
|
+
hidQrGenerating.value = true;
|
|
1361
|
+
hidQrError.value = "";
|
|
1362
|
+
try {
|
|
1363
|
+
const response = await issueVisitorQr(hidQrConfig.value.readerId, {
|
|
1364
|
+
visitorId: String(hidQrVisitor.value._id),
|
|
1365
|
+
validityMinutes: hidQrConfig.value.validityMinutes,
|
|
1366
|
+
});
|
|
1367
|
+
const credential = "data" in response ? response.data : response;
|
|
1368
|
+
hidQrImageUrl.value = await QRCode.toDataURL(credential.qrValue, { width: 360, margin: 2, errorCorrectionLevel: "M" });
|
|
1369
|
+
hidQrExpiresAt.value = credential.expiresAt;
|
|
1370
|
+
hidQrNow.value = Date.now();
|
|
1371
|
+
} catch (error: unknown) {
|
|
1372
|
+
hidQrError.value = errorConverter(error) || "Unable to generate the visitor HID QR code.";
|
|
1373
|
+
} finally {
|
|
1374
|
+
hidQrGenerating.value = false;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function formatHidQrExpiry(value: string) {
|
|
1379
|
+
const date = new Date(value);
|
|
1380
|
+
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function formatHidQrCountdown(totalSeconds: number) {
|
|
1384
|
+
const safe = Math.max(0, totalSeconds);
|
|
1385
|
+
const hours = Math.floor(safe / 3600);
|
|
1386
|
+
const minutes = Math.floor((safe % 3600) / 60);
|
|
1387
|
+
const seconds = safe % 60;
|
|
1388
|
+
return [hours, minutes, seconds].map((part) => String(part).padStart(2, "0")).join(":");
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
watch(hidQrDialog, (open) => {
|
|
1392
|
+
if (hidQrTimer) clearInterval(hidQrTimer);
|
|
1393
|
+
hidQrTimer = open ? setInterval(() => { hidQrNow.value = Date.now(); }, 1000) : null;
|
|
1394
|
+
});
|
|
1258
1395
|
|
|
1259
1396
|
//filter states
|
|
1260
1397
|
const searchInput = ref("");
|
|
@@ -1505,11 +1642,6 @@ const formattedFields = {
|
|
|
1505
1642
|
attachments: "Attachments",
|
|
1506
1643
|
} as const;
|
|
1507
1644
|
|
|
1508
|
-
function filterTypeSelectionLabel() {
|
|
1509
|
-
const length = filterTypes.value.length;
|
|
1510
|
-
return `${length} selected ${length === 1 ? "type" : "types"}` as string;
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
1645
|
const passStatusOptions = [
|
|
1514
1646
|
{ label: "Returned", value: "Returned" },
|
|
1515
1647
|
{ label: "Not Returned", value: "In Use" },
|
|
@@ -1601,7 +1733,6 @@ const {
|
|
|
1601
1733
|
watch(getVisitorReq, (newData: any) => {
|
|
1602
1734
|
if (newData) {
|
|
1603
1735
|
items.value = newData.items ?? [];
|
|
1604
|
-
// items.value = [{ _id: "testid", name: "John Doe", type: "Contractor", company: "ABC Corp", location: "Block A, Level 1, Unit 101", contact: "12345678", plateNumber: "SGX1234A", checkIn: new Date().toISOString(), checkOut: null, status: "registered", visitorPass: [{ _id: "1", keyId: "pass1", prefixAndName: "VIP Pass" }, { _id: "2", keyId: "pass2", prefixAndName: "VIP Pass 2" }], passKeys: [{ _id: "1", keyId: "key2", prefixAndName: "Master Key" }] }];
|
|
1605
1736
|
pages.value = newData.pages ?? 0;
|
|
1606
1737
|
pageRange.value = newData?.pageRange ?? "-- - -- of --";
|
|
1607
1738
|
}
|
|
@@ -1708,7 +1839,7 @@ async function handleVisitorDataFromScannedQRCodeCheckInOut(
|
|
|
1708
1839
|
site: visitorData.site ?? siteId,
|
|
1709
1840
|
},
|
|
1710
1841
|
update: {
|
|
1711
|
-
updatedBy: currentUser.value
|
|
1842
|
+
updatedBy: currentUser.value?._id,
|
|
1712
1843
|
visitorPass: visitorPass.find(
|
|
1713
1844
|
(i: Record<string, any>) => i.keyId === null
|
|
1714
1845
|
)
|
|
@@ -2347,7 +2478,14 @@ const syncVisitorFiltersFromRoute = () => {
|
|
|
2347
2478
|
}
|
|
2348
2479
|
};
|
|
2349
2480
|
|
|
2350
|
-
onMounted(
|
|
2481
|
+
onMounted(() => {
|
|
2482
|
+
syncVisitorFiltersFromRoute();
|
|
2483
|
+
void loadHidQrConfig();
|
|
2484
|
+
});
|
|
2485
|
+
|
|
2486
|
+
onBeforeUnmount(() => {
|
|
2487
|
+
if (hidQrTimer) clearInterval(hidQrTimer);
|
|
2488
|
+
});
|
|
2351
2489
|
|
|
2352
2490
|
watch(() => route.query, syncVisitorFiltersFromRoute, { deep: true });
|
|
2353
2491
|
|
|
@@ -2398,4 +2536,16 @@ watchEffect(async () => {
|
|
|
2398
2536
|
});
|
|
2399
2537
|
</script>
|
|
2400
2538
|
|
|
2401
|
-
<style scoped
|
|
2539
|
+
<style scoped>
|
|
2540
|
+
/* The width the type filter carried as `max-width="200"` on the v-select. */
|
|
2541
|
+
.visitor-type-filter {
|
|
2542
|
+
max-width: 200px;
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
.visitor-hid-qr {
|
|
2546
|
+
display: inline-flex;
|
|
2547
|
+
padding: 12px;
|
|
2548
|
+
border: 1px solid #dce3e8;
|
|
2549
|
+
background: #fff;
|
|
2550
|
+
}
|
|
2551
|
+
</style>
|
|
@@ -66,6 +66,29 @@
|
|
|
66
66
|
</template>
|
|
67
67
|
</v-snackbar>
|
|
68
68
|
|
|
69
|
+
<!--
|
|
70
|
+
Camera health is its own alert, deliberately separate from the plate
|
|
71
|
+
snackbar above: it is not a visitor event, it must not be silenced by the
|
|
72
|
+
unregistered-visitor switch, and it must not be hidden on the unregistered
|
|
73
|
+
tab. Several failing cameras collapse into one line rather than one
|
|
74
|
+
snackbar each.
|
|
75
|
+
-->
|
|
76
|
+
<v-snackbar
|
|
77
|
+
v-if="canViewVisitor"
|
|
78
|
+
v-model="cameraAlert.modal"
|
|
79
|
+
:color="cameraFaults.size ? 'warning' : 'success'"
|
|
80
|
+
:timeout="10000"
|
|
81
|
+
location="top right"
|
|
82
|
+
class="rounded-xl"
|
|
83
|
+
multi-line
|
|
84
|
+
:z-index="100000000"
|
|
85
|
+
>
|
|
86
|
+
<span class="text-1-4rem">{{ cameraAlertText }}</span>
|
|
87
|
+
<template #actions>
|
|
88
|
+
<v-btn icon="mdi-close" variant="text" @click="cameraAlert.modal = false" />
|
|
89
|
+
</template>
|
|
90
|
+
</v-snackbar>
|
|
91
|
+
|
|
69
92
|
<v-dialog v-model="permanentMessageDialog.modal" max-width="480" persistent>
|
|
70
93
|
<v-card>
|
|
71
94
|
<v-card-title class="text-h6">Notice</v-card-title>
|
|
@@ -132,6 +155,36 @@ const permanentMessageDialog = reactive({
|
|
|
132
155
|
text: "",
|
|
133
156
|
});
|
|
134
157
|
|
|
158
|
+
// Outstanding camera faults for this site, keyed by camera id so a repeat for
|
|
159
|
+
// the same camera replaces its line instead of adding another.
|
|
160
|
+
const cameraFaults = reactive(new Map<string, string>());
|
|
161
|
+
const cameraRecovery = ref("");
|
|
162
|
+
const cameraAlert = reactive({ modal: false });
|
|
163
|
+
|
|
164
|
+
const cameraAlertText = computed(
|
|
165
|
+
() => cameraAlertMessage([...cameraFaults.values()]) || cameraRecovery.value
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const handleCameraHealth = (data: TVisitorSocketData) => {
|
|
169
|
+
// Older API-core builds send the fault text with no `event`, so a bare
|
|
170
|
+
// message is a fault.
|
|
171
|
+
if (data?.event === "camera-recovered") {
|
|
172
|
+
// Without a camera id there is no way to tell which fault cleared.
|
|
173
|
+
if (data?.camera) cameraFaults.delete(data.camera);
|
|
174
|
+
else cameraFaults.clear();
|
|
175
|
+
// Alerts are now infrequent, so the operator is told it came back —
|
|
176
|
+
// otherwise the only signal a camera recovered is silence.
|
|
177
|
+
cameraRecovery.value = cameraFaults.size ? "" : data?.message || "";
|
|
178
|
+
cameraAlert.modal = Boolean(cameraAlertText.value);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!data?.message) return;
|
|
183
|
+
cameraRecovery.value = "";
|
|
184
|
+
cameraFaults.set(data?.camera || data.message, data.message);
|
|
185
|
+
cameraAlert.modal = true;
|
|
186
|
+
};
|
|
187
|
+
|
|
135
188
|
watch(
|
|
136
189
|
() => props.siteId,
|
|
137
190
|
async (siteId) => {
|
|
@@ -233,8 +286,9 @@ const connectSocket = () => {
|
|
|
233
286
|
}
|
|
234
287
|
|
|
235
288
|
|
|
236
|
-
|
|
237
|
-
|
|
289
|
+
// Camera health, not a visitor event: its own alert, its own gate.
|
|
290
|
+
if (data?.message || data?.event) {
|
|
291
|
+
handleCameraHealth(data);
|
|
238
292
|
}
|
|
239
293
|
|
|
240
294
|
if (data?.messagePermanent) {
|
|
@@ -1,58 +1,47 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
>
|
|
2
|
+
<div>
|
|
3
|
+
<!--
|
|
4
|
+
The design's page scaffold. The screen had no heading, its Create button
|
|
5
|
+
sat top-left and the search floated top-right outside the card, with
|
|
6
|
+
refresh and the pager stacked on their own bar above the table. Same
|
|
7
|
+
button, same dialog, same permission, same debounced search, same pager.
|
|
8
|
+
-->
|
|
9
|
+
<PageHeader title="Work Orders">
|
|
10
|
+
<template v-if="canCreateWorkOrder" #actions>
|
|
11
|
+
<AppButton @click="(showCreateDialog = true), (isEditMode = false)">
|
|
13
12
|
Create Work Order
|
|
14
|
-
</
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
variant="outlined"
|
|
20
|
-
density="comfortable"
|
|
21
|
-
clearable
|
|
22
|
-
hide-details
|
|
23
|
-
class="ml-2"
|
|
24
|
-
style="max-width: 250px"
|
|
25
|
-
/>
|
|
26
|
-
</v-row>
|
|
27
|
-
</v-col>
|
|
13
|
+
</AppButton>
|
|
14
|
+
</template>
|
|
15
|
+
</PageHeader>
|
|
16
|
+
|
|
17
|
+
<v-row no-gutters>
|
|
28
18
|
<v-col cols="12">
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<v-btn fab icon density="comfortable" @click="updatePage(1)">
|
|
39
|
-
<v-icon>mdi-refresh</v-icon>
|
|
40
|
-
</v-btn>
|
|
19
|
+
<AppCard class="table-card">
|
|
20
|
+
<CardToolbar :count="pageRange">
|
|
21
|
+
<template #left>
|
|
22
|
+
<AppButton
|
|
23
|
+
variant="icon"
|
|
24
|
+
icon="mdi-refresh"
|
|
25
|
+
aria-label="Refresh"
|
|
26
|
+
@click="updatePage(1)"
|
|
27
|
+
/>
|
|
41
28
|
</template>
|
|
42
29
|
|
|
43
|
-
<template #
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
30
|
+
<template #right>
|
|
31
|
+
<AppField
|
|
32
|
+
v-model="searchText"
|
|
33
|
+
search
|
|
34
|
+
compact
|
|
35
|
+
placeholder="Search work order..."
|
|
36
|
+
klass="app-toolbar__search"
|
|
37
|
+
/>
|
|
38
|
+
<local-pagination
|
|
39
|
+
v-model="page"
|
|
40
|
+
:length="pages"
|
|
41
|
+
@update:value="updatePage"
|
|
42
|
+
/>
|
|
54
43
|
</template>
|
|
55
|
-
</
|
|
44
|
+
</CardToolbar>
|
|
56
45
|
|
|
57
46
|
<v-data-table
|
|
58
47
|
:headers="headers"
|
|
@@ -62,6 +51,7 @@
|
|
|
62
51
|
fixed-header
|
|
63
52
|
hide-default-footer
|
|
64
53
|
:hide-default-header="false"
|
|
54
|
+
:loading="loading"
|
|
65
55
|
@click:row="tableRowClickHandler"
|
|
66
56
|
style="max-height: calc(100vh - (200px))"
|
|
67
57
|
>
|
|
@@ -69,9 +59,10 @@
|
|
|
69
59
|
{{ formatDateDDMMYYYYLocal(item.createdAt) }}
|
|
70
60
|
</template>
|
|
71
61
|
</v-data-table>
|
|
72
|
-
</
|
|
62
|
+
</AppCard>
|
|
73
63
|
</v-col>
|
|
74
|
-
|
|
64
|
+
</v-row>
|
|
65
|
+
</div>
|
|
75
66
|
|
|
76
67
|
<WorkOrderCreate
|
|
77
68
|
v-model="showCreateDialog"
|
|
@@ -89,13 +89,13 @@ export default function useComment() {
|
|
|
89
89
|
|
|
90
90
|
comments.value = _comments.items
|
|
91
91
|
.map((comment: any) => {
|
|
92
|
-
if (comment.createdBy === currentUser.value
|
|
92
|
+
if (comment.createdBy === currentUser.value?._id) {
|
|
93
93
|
comment.justify = "end";
|
|
94
94
|
} else {
|
|
95
95
|
comment.justify = "start";
|
|
96
96
|
if (
|
|
97
97
|
((Array.isArray(comment.seenBy) &&
|
|
98
|
-
!comment.seenBy.includes(currentUser.value
|
|
98
|
+
!comment.seenBy.includes(currentUser.value?._id)) ||
|
|
99
99
|
!comment?.seenBy) &&
|
|
100
100
|
comment?._id
|
|
101
101
|
) {
|
|
@@ -111,7 +111,7 @@ export default function useComment() {
|
|
|
111
111
|
);
|
|
112
112
|
|
|
113
113
|
if (Array.isArray(updateSeenIds) && updateSeenIds.length > 0) {
|
|
114
|
-
const seenBy = await updateSeenBy(updateSeenIds, currentUser.value
|
|
114
|
+
const seenBy = await updateSeenBy(updateSeenIds, currentUser.value?._id);
|
|
115
115
|
}
|
|
116
116
|
} catch (error) {
|
|
117
117
|
console.log("error :", error);
|