@7365admin1/layer-common 3.2.2-staging.106 → 3.2.2-staging.108
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/components/CreateSubsciptionPlan.vue +82 -57
- package/components/EntryPassInformation.vue +29 -1
- package/components/HidAccessLogDashboard.vue +42 -29
- package/components/HidIntercomManagement.vue +43 -9
- package/components/HidQrCodeConfiguration.vue +1 -2
- package/components/HidReaderForm.vue +102 -11
- package/components/HidReaderManagement.vue +1 -0
- package/components/HidUserEnrollment.vue +21 -2
- package/components/MemberInformation.vue +9 -0
- package/components/SubscriptionPlanTable.vue +168 -104
- package/components/VisitorForm.vue +30 -1
- package/composables/useHidAmico.ts +13 -0
- package/composables/useSubscriptionPlan.ts +62 -0
- package/package.json +1 -1
- package/types/subscription.ts +18 -11
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<v-card class="pa-2">
|
|
2
|
+
<v-card class="pa-2 pb-0">
|
|
3
3
|
<!-- Header -->
|
|
4
|
-
<v-card-title class="
|
|
4
|
+
<v-card-title class="pb-2">
|
|
5
5
|
<span class="text-h6 font-weight-bold">
|
|
6
6
|
{{ isEditing ? "Edit Plan" : "Create New Plan" }}
|
|
7
7
|
</span>
|
|
8
|
-
|
|
9
|
-
<v-btn
|
|
10
|
-
icon="mdi-close"
|
|
11
|
-
variant="text"
|
|
12
|
-
density="comfortable"
|
|
13
|
-
aria-label="Close"
|
|
14
|
-
@click="$emit('cancel')"
|
|
15
|
-
/>
|
|
16
8
|
</v-card-title>
|
|
17
9
|
|
|
18
|
-
<v-divider />
|
|
19
|
-
|
|
20
10
|
<!-- Form Content -->
|
|
21
11
|
<v-form ref="formRef" class="my-4" @submit.prevent="submit">
|
|
22
12
|
<v-card-text class="pa-2 max-h-70vh overflow-y-auto">
|
|
@@ -68,8 +58,6 @@
|
|
|
68
58
|
</template>
|
|
69
59
|
</v-text-field>
|
|
70
60
|
|
|
71
|
-
<v-divider class="my-4" />
|
|
72
|
-
|
|
73
61
|
<!-- Pricing Section -->
|
|
74
62
|
<div class="text-subtitle-2 font-weight-bold mb-3 text-grey-darken-3">
|
|
75
63
|
Pricing
|
|
@@ -116,14 +104,14 @@
|
|
|
116
104
|
selected-class="bg-grey-darken-3 text-white"
|
|
117
105
|
>
|
|
118
106
|
<v-chip
|
|
119
|
-
v-for="app in
|
|
120
|
-
:key="app.
|
|
121
|
-
:value="app.
|
|
107
|
+
v-for="app in APP_LIST"
|
|
108
|
+
:key="app.value"
|
|
109
|
+
:value="app.value"
|
|
122
110
|
filter
|
|
123
111
|
variant="outlined"
|
|
124
112
|
size="small"
|
|
125
113
|
>
|
|
126
|
-
{{ app.
|
|
114
|
+
{{ app.title }}
|
|
127
115
|
</v-chip>
|
|
128
116
|
</v-chip-group>
|
|
129
117
|
</div>
|
|
@@ -155,26 +143,37 @@
|
|
|
155
143
|
<v-card variant="outlined" class="pa-1 mb-3">
|
|
156
144
|
<v-list density="compact" class="py-0">
|
|
157
145
|
<v-list-item
|
|
158
|
-
v-for="app in
|
|
159
|
-
:key="app.
|
|
146
|
+
v-for="app in APP_LIST"
|
|
147
|
+
:key="app.value"
|
|
160
148
|
class="px-2"
|
|
161
149
|
>
|
|
162
150
|
<template #prepend>
|
|
163
151
|
<v-checkbox-btn
|
|
164
|
-
:model-value="form.customApps.includes(app.
|
|
152
|
+
:model-value="form.customApps.includes(app.value)"
|
|
165
153
|
density="compact"
|
|
166
|
-
@update:model-value="toggleApp(form.customApps, app.
|
|
154
|
+
@update:model-value="toggleApp(form.customApps, app.value)"
|
|
167
155
|
/>
|
|
168
156
|
</template>
|
|
169
157
|
|
|
170
158
|
<v-list-item-title class="text-body-2">
|
|
171
|
-
{{ app.
|
|
159
|
+
{{ app.title }}
|
|
172
160
|
</v-list-item-title>
|
|
173
161
|
|
|
174
162
|
<template #append>
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
163
|
+
<div class="d-flex align-center" style="gap: 4px">
|
|
164
|
+
<span class="text-caption text-grey">$</span>
|
|
165
|
+
<v-text-field
|
|
166
|
+
v-model.number="form.customAppPrices[app.value]"
|
|
167
|
+
type="number"
|
|
168
|
+
min="0"
|
|
169
|
+
density="compact"
|
|
170
|
+
variant="outlined"
|
|
171
|
+
hide-details
|
|
172
|
+
style="width: 84px"
|
|
173
|
+
:disabled="!form.customApps.includes(app.value)"
|
|
174
|
+
/>
|
|
175
|
+
<span class="text-caption text-grey">/{{ form.billingCycle === 'annually' ? 'yr' : 'mo' }}</span>
|
|
176
|
+
</div>
|
|
178
177
|
</template>
|
|
179
178
|
</v-list-item>
|
|
180
179
|
</v-list>
|
|
@@ -211,28 +210,21 @@
|
|
|
211
210
|
</v-alert>
|
|
212
211
|
</v-card-text>
|
|
213
212
|
|
|
214
|
-
<v-divider />
|
|
215
|
-
|
|
216
213
|
<!-- Footer Buttons -->
|
|
217
|
-
<v-
|
|
218
|
-
<v-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
class="flex-1 text-none"
|
|
232
|
-
>
|
|
233
|
-
{{ isEditing ? "Save Changes" : "Create Plan" }}
|
|
234
|
-
</v-btn>
|
|
235
|
-
</v-card-actions>
|
|
214
|
+
<v-toolbar density="compact">
|
|
215
|
+
<v-row no-gutters>
|
|
216
|
+
<v-col cols="6">
|
|
217
|
+
<v-btn tile block variant="text" class="text-none" size="48" @click="$emit('cancel')">
|
|
218
|
+
Cancel
|
|
219
|
+
</v-btn>
|
|
220
|
+
</v-col>
|
|
221
|
+
<v-col cols="6">
|
|
222
|
+
<v-btn tile block type="submit" variant="flat" color="primary" class="text-none" size="48">
|
|
223
|
+
{{ isEditing ? "Save Changes" : "Create Plan" }}
|
|
224
|
+
</v-btn>
|
|
225
|
+
</v-col>
|
|
226
|
+
</v-row>
|
|
227
|
+
</v-toolbar>
|
|
236
228
|
</v-form>
|
|
237
229
|
</v-card>
|
|
238
230
|
</template>
|
|
@@ -241,7 +233,9 @@
|
|
|
241
233
|
import { reactive, computed, watch } from "vue";
|
|
242
234
|
|
|
243
235
|
import {
|
|
244
|
-
|
|
236
|
+
APP_LIST,
|
|
237
|
+
STATIC_APP_MONTHLY_PRICE,
|
|
238
|
+
STATIC_APP_YEARLY_PRICE,
|
|
245
239
|
type PlanFormState,
|
|
246
240
|
type SubscriptionPlan,
|
|
247
241
|
} from "../types/subscription";
|
|
@@ -284,6 +278,14 @@ const form = reactive<PlanFormState>({
|
|
|
284
278
|
props.initialPlan?.planType === "custom_apps"
|
|
285
279
|
? [...(props.initialPlan?.applicationIds ?? [])]
|
|
286
280
|
: [],
|
|
281
|
+
customAppPrices: Object.fromEntries(
|
|
282
|
+
APP_LIST.map((app) => [
|
|
283
|
+
app.value,
|
|
284
|
+
props.initialPlan?.billingCycle === "annually"
|
|
285
|
+
? STATIC_APP_YEARLY_PRICE
|
|
286
|
+
: STATIC_APP_MONTHLY_PRICE,
|
|
287
|
+
])
|
|
288
|
+
),
|
|
287
289
|
});
|
|
288
290
|
|
|
289
291
|
const errors = reactive({
|
|
@@ -304,6 +306,29 @@ watch(
|
|
|
304
306
|
}
|
|
305
307
|
);
|
|
306
308
|
|
|
309
|
+
function staticPriceForCycle(cycle: typeof form.billingCycle) {
|
|
310
|
+
return cycle === "annually" ? STATIC_APP_YEARLY_PRICE : STATIC_APP_MONTHLY_PRICE;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/*
|
|
314
|
+
* Keep custom app prices in sync with the billing cycle, but only for
|
|
315
|
+
* apps still sitting at the previous cycle's default (leave manual edits alone)
|
|
316
|
+
*/
|
|
317
|
+
watch(
|
|
318
|
+
() => form.billingCycle,
|
|
319
|
+
(newCycle, oldCycle) => {
|
|
320
|
+
const oldDefault = staticPriceForCycle(oldCycle);
|
|
321
|
+
const newDefault = staticPriceForCycle(newCycle);
|
|
322
|
+
if (oldDefault === newDefault) return;
|
|
323
|
+
|
|
324
|
+
for (const app of APP_LIST) {
|
|
325
|
+
if (form.customAppPrices[app.value] === oldDefault) {
|
|
326
|
+
form.customAppPrices[app.value] = newDefault;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
);
|
|
331
|
+
|
|
307
332
|
/*
|
|
308
333
|
* Toggle application selection helper
|
|
309
334
|
*/
|
|
@@ -320,15 +345,10 @@ function toggleApp(list: string[], id: string) {
|
|
|
320
345
|
* Custom app total price calculation
|
|
321
346
|
*/
|
|
322
347
|
const customTotalPrice = computed(() => {
|
|
323
|
-
return form.customApps.reduce(
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
return (
|
|
328
|
-
total +
|
|
329
|
-
(form.billingCycle === "annually" ? app.yearlyPrice : app.monthlyPrice)
|
|
330
|
-
);
|
|
331
|
-
}, 0);
|
|
348
|
+
return form.customApps.reduce(
|
|
349
|
+
(total, value) => total + (form.customAppPrices[value] ?? 0),
|
|
350
|
+
0
|
|
351
|
+
);
|
|
332
352
|
});
|
|
333
353
|
|
|
334
354
|
/*
|
|
@@ -340,6 +360,11 @@ function validate(): boolean {
|
|
|
340
360
|
|
|
341
361
|
if (form.planType !== "free_bundle" && !form.billingCycle) {
|
|
342
362
|
errors.pricing = "Please select a billing cycle.";
|
|
363
|
+
} else if (
|
|
364
|
+
(form.planType === "free_bundle" || form.planType === "paid_bundle") &&
|
|
365
|
+
form.bundleApps.length === 0
|
|
366
|
+
) {
|
|
367
|
+
errors.pricing = "Select at least one application.";
|
|
343
368
|
} else if (
|
|
344
369
|
form.planType === "paid_bundle" &&
|
|
345
370
|
(!form.bundlePrice || form.bundlePrice <= 0)
|
|
@@ -166,8 +166,28 @@
|
|
|
166
166
|
</template>
|
|
167
167
|
|
|
168
168
|
<template v-if="passType === 'HID_QR'">
|
|
169
|
+
<v-card variant="tonal" color="primary" class="mt-3 pa-3" rounded="lg">
|
|
170
|
+
<div class="d-flex align-center ga-3">
|
|
171
|
+
<v-icon icon="mdi-door-open" size="28" />
|
|
172
|
+
<div>
|
|
173
|
+
<div class="text-caption text-medium-emphasis">Access Reader</div>
|
|
174
|
+
<div class="text-subtitle-2 font-weight-bold">{{ hidReaderName || "Reader not configured" }}</div>
|
|
175
|
+
<div class="text-caption mt-1">Access Portal: {{ hidPortalName || "Portal not configured" }}</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</v-card>
|
|
179
|
+
<v-alert
|
|
180
|
+
v-if="!hidPortalName"
|
|
181
|
+
type="warning"
|
|
182
|
+
variant="tonal"
|
|
183
|
+
density="compact"
|
|
184
|
+
class="mt-3"
|
|
185
|
+
icon="mdi-alert"
|
|
186
|
+
>
|
|
187
|
+
Reconnect this HID reader and select a portal before generating a visitor QR code.
|
|
188
|
+
</v-alert>
|
|
169
189
|
<v-alert
|
|
170
|
-
v-if="!hidQrPrinterConfigured"
|
|
190
|
+
v-else-if="!hidQrPrinterConfigured"
|
|
171
191
|
type="info"
|
|
172
192
|
variant="tonal"
|
|
173
193
|
density="compact"
|
|
@@ -283,6 +303,14 @@ const props = defineProps({
|
|
|
283
303
|
type: Boolean,
|
|
284
304
|
default: false,
|
|
285
305
|
},
|
|
306
|
+
hidReaderName: {
|
|
307
|
+
type: String,
|
|
308
|
+
default: "",
|
|
309
|
+
},
|
|
310
|
+
hidPortalName: {
|
|
311
|
+
type: String,
|
|
312
|
+
default: "",
|
|
313
|
+
},
|
|
286
314
|
excludedCardIds: {
|
|
287
315
|
type: Array as PropType<string[]>,
|
|
288
316
|
default: () => [],
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
</v-btn>
|
|
13
13
|
|
|
14
14
|
<v-select
|
|
15
|
-
v-if="readers.length
|
|
15
|
+
v-if="readers.length"
|
|
16
16
|
v-model="selectedReaderId"
|
|
17
17
|
:items="readerOptions"
|
|
18
18
|
item-title="title"
|
|
@@ -412,7 +412,7 @@ const accessMethodOptions = ["All", "Facial", "QR Code", "ID and Password", "PIN
|
|
|
412
412
|
|
|
413
413
|
const readerOptions = computed(() =>
|
|
414
414
|
readers.value.map((reader) => ({
|
|
415
|
-
title: reader.name || reader.deviceId || reader._id
|
|
415
|
+
title: `${reader.name || reader.deviceId || reader._id} — ${reader.portalName || "Portal not configured"}`,
|
|
416
416
|
value: reader._id,
|
|
417
417
|
})),
|
|
418
418
|
);
|
|
@@ -582,7 +582,6 @@ async function loadDashboard() {
|
|
|
582
582
|
loading.value = true;
|
|
583
583
|
resetDashboardData();
|
|
584
584
|
try {
|
|
585
|
-
const isAdministratorTab = activeTab.value === "administrator";
|
|
586
585
|
const isVisitorTab = activeTab.value === "visitor";
|
|
587
586
|
const [identityResponse, logResponse, hidAccessLogs, configurationResponse, roleResponse, visitorResponse, readerUserResponse] = await Promise.all([
|
|
588
587
|
loadDashboardSource(
|
|
@@ -603,9 +602,7 @@ async function loadDashboard() {
|
|
|
603
602
|
),
|
|
604
603
|
loadDashboardSource(loadLiveAccessLogs(selectedReaderId.value), [], "live access logs"),
|
|
605
604
|
loadDashboardSource(loadReaderConfiguration(selectedReaderId.value), {}, "reader configuration"),
|
|
606
|
-
|
|
607
|
-
? loadDashboardSource(loadAdministratorRoles(selectedReaderId.value), null, "administrator roles")
|
|
608
|
-
: Promise.resolve(null),
|
|
605
|
+
loadDashboardSource(loadAdministratorRoles(selectedReaderId.value), null, "administrator roles"),
|
|
609
606
|
isVisitorTab && props.org
|
|
610
607
|
? loadDashboardSource(
|
|
611
608
|
getVisitors({
|
|
@@ -632,14 +629,9 @@ async function loadDashboard() {
|
|
|
632
629
|
);
|
|
633
630
|
administratorUserIds.value = getAdministratorRoleUserIds(roleResponse);
|
|
634
631
|
allIdentities.value = mergedIdentities;
|
|
635
|
-
identities.value =
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
|
|
639
|
-
})
|
|
640
|
-
: activeTab.value === "visitor"
|
|
641
|
-
? mergedIdentities.filter(isVisitorIdentity)
|
|
642
|
-
: mergedIdentities.filter((identity: Record<string, any>) => !isVisitorIdentity(identity));
|
|
632
|
+
identities.value = mergedIdentities.filter(
|
|
633
|
+
(identity: Record<string, any>) => classifyIdentity(identity) === activeTab.value,
|
|
634
|
+
);
|
|
643
635
|
logs.value = logResponse?.items ?? logResponse?.data?.items ?? logResponse?.data?.logs ?? [];
|
|
644
636
|
liveAccessLogs.value = hidAccessLogs;
|
|
645
637
|
readerConfiguration.value = normalizeReaderConfiguration(configurationResponse);
|
|
@@ -781,8 +773,10 @@ function normalizeHidUsers(response: Record<string, any>) {
|
|
|
781
773
|
registration: user.registration,
|
|
782
774
|
cardNo: user.card_value || user.cardNo || "",
|
|
783
775
|
name: user.name,
|
|
776
|
+
hidUserTypeId: user.user_type_id,
|
|
784
777
|
metadata: {
|
|
785
778
|
name: user.name,
|
|
779
|
+
hidUserTypeId: user.user_type_id,
|
|
786
780
|
imageTimestamp: user.image_timestamp ? String(user.image_timestamp) : "",
|
|
787
781
|
hidReaderSource: true,
|
|
788
782
|
},
|
|
@@ -835,21 +829,22 @@ function resetDashboardData() {
|
|
|
835
829
|
|
|
836
830
|
function isVisibleForActiveTab(row: AccessRow) {
|
|
837
831
|
const identity = findIdentityByKey(row.identityKey);
|
|
838
|
-
|
|
839
|
-
if (activeTab.value === "visitor") {
|
|
840
|
-
return isVisitor;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
if (activeTab.value === "administrator") {
|
|
844
|
-
const hidUserId = toHidNumericId(identity?.hidUserId ?? row.identityKey.split("|")[0]);
|
|
845
|
-
return Boolean(hidUserId && administratorUserIds.value.has(hidUserId));
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
return !isVisitor;
|
|
832
|
+
return classifyIdentity(identity, row) === activeTab.value;
|
|
849
833
|
}
|
|
850
834
|
|
|
851
835
|
function isVisitorIdentity(identity: Record<string, unknown> | undefined) {
|
|
852
|
-
return Boolean(identity && (
|
|
836
|
+
return Boolean(identity && (
|
|
837
|
+
identity.type === "visitor"
|
|
838
|
+
|| identity.visitor
|
|
839
|
+
|| Number(identity.hidUserTypeId ?? (identity.metadata as Record<string, unknown> | undefined)?.hidUserTypeId) === 1
|
|
840
|
+
));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function classifyIdentity(identity?: Record<string, any>, row?: AccessRow): AccessTab {
|
|
844
|
+
const hidUserId = toHidNumericId(identity?.hidUserId ?? row?.identityKey.split("|")[0]);
|
|
845
|
+
if (hidUserId && administratorUserIds.value.has(hidUserId)) return "administrator";
|
|
846
|
+
if (row?.identityType === "visitor" || row?.visitorId || isVisitorIdentity(identity)) return "visitor";
|
|
847
|
+
return "resident";
|
|
853
848
|
}
|
|
854
849
|
|
|
855
850
|
function loadAdministratorRoles(readerId: string) {
|
|
@@ -1200,7 +1195,18 @@ function getRawAccessLogs(event: Record<string, any>) {
|
|
|
1200
1195
|
payload.access_logs ||
|
|
1201
1196
|
payload.accessLogs;
|
|
1202
1197
|
|
|
1203
|
-
|
|
1198
|
+
if (Array.isArray(accessLogs)) return accessLogs;
|
|
1199
|
+
|
|
1200
|
+
const objectChanges = payload.object_changes || result.object_changes || [];
|
|
1201
|
+
return Array.isArray(objectChanges)
|
|
1202
|
+
? objectChanges
|
|
1203
|
+
.filter((change: Record<string, any>) => (
|
|
1204
|
+
change?.object === "access_logs"
|
|
1205
|
+
&& change?.type !== "deleted"
|
|
1206
|
+
&& change?.values
|
|
1207
|
+
))
|
|
1208
|
+
.map((change: Record<string, any>) => change.values)
|
|
1209
|
+
: [];
|
|
1204
1210
|
}
|
|
1205
1211
|
|
|
1206
1212
|
function mapAccessLogToRow(rawLog: Record<string, any>, event: Record<string, any>, index: string): AccessRow {
|
|
@@ -1212,7 +1218,9 @@ function mapAccessLogToRow(rawLog: Record<string, any>, event: Record<string, an
|
|
|
1212
1218
|
return {
|
|
1213
1219
|
key: `${event._id || event.id || "log"}-${rawLog.id || rawLog.time || index}`,
|
|
1214
1220
|
identityKey,
|
|
1215
|
-
identityType:
|
|
1221
|
+
identityType: identity
|
|
1222
|
+
? classifyIdentity(identity)
|
|
1223
|
+
: String(eventIdentity.type || "").toLowerCase() || undefined,
|
|
1216
1224
|
visitorId: String(identity?.visitor || eventIdentity.visitor || "") || undefined,
|
|
1217
1225
|
name: identity ? getName(identity) : getRawLogName(rawLog, event),
|
|
1218
1226
|
facialData: identity ? getFacialData(identity) : "N/A",
|
|
@@ -1276,7 +1284,12 @@ function findIdentityForAccessLog(rawLog: Record<string, any>, event: Record<str
|
|
|
1276
1284
|
const eventIdentityId = event.payload?.identity?._id;
|
|
1277
1285
|
if (eventIdentityId) {
|
|
1278
1286
|
const byId = allIdentities.value.find((identity) => String(identity._id) === String(eventIdentityId));
|
|
1279
|
-
|
|
1287
|
+
const rawUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId;
|
|
1288
|
+
if (byId && (
|
|
1289
|
+
rawUserId === undefined
|
|
1290
|
+
|| rawUserId === null
|
|
1291
|
+
|| String(byId.hidUserId) === String(rawUserId)
|
|
1292
|
+
)) return byId;
|
|
1280
1293
|
}
|
|
1281
1294
|
|
|
1282
1295
|
const hidUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? event.payload?.identity?.hidUserId ?? event.payload?.identityLookup?.hidUserId;
|
|
@@ -24,6 +24,19 @@
|
|
|
24
24
|
{{ webPhone.callState.value === 'incoming' ? 'Incoming Call' : 'Web Call' }}
|
|
25
25
|
</v-btn>
|
|
26
26
|
|
|
27
|
+
<v-select
|
|
28
|
+
v-if="readers.length"
|
|
29
|
+
v-model="selectedReaderId"
|
|
30
|
+
:items="readerOptions"
|
|
31
|
+
item-title="title"
|
|
32
|
+
item-value="value"
|
|
33
|
+
variant="outlined"
|
|
34
|
+
density="compact"
|
|
35
|
+
hide-details
|
|
36
|
+
class="reader-input"
|
|
37
|
+
@update:model-value="page = 1"
|
|
38
|
+
/>
|
|
39
|
+
|
|
27
40
|
<div class="toolbar-spacer" />
|
|
28
41
|
|
|
29
42
|
<v-text-field
|
|
@@ -702,6 +715,7 @@ const {
|
|
|
702
715
|
|
|
703
716
|
const activeTab = ref<IntercomTab>("intercom");
|
|
704
717
|
const readers = ref<Record<string, any>[]>([]);
|
|
718
|
+
const selectedReaderId = ref("");
|
|
705
719
|
const intercomRows = ref<IntercomRow[]>([]);
|
|
706
720
|
const contacts = ref<ContactRow[]>([]);
|
|
707
721
|
const loading = ref(false);
|
|
@@ -821,6 +835,11 @@ const contactForm = reactive({
|
|
|
821
835
|
serverAddress: "",
|
|
822
836
|
});
|
|
823
837
|
|
|
838
|
+
const readerOptions = computed(() => readers.value.map((reader) => ({
|
|
839
|
+
title: `${reader.name || reader.deviceId || reader._id} — ${reader.portalName || "Portal not configured"}`,
|
|
840
|
+
value: reader._id,
|
|
841
|
+
})));
|
|
842
|
+
|
|
824
843
|
const visibleRows = computed(() => activeTab.value === "intercom" ? filteredIntercomRows.value : filteredContacts.value);
|
|
825
844
|
const pages = computed(() => Math.max(1, Math.ceil(visibleRows.value.length / limit)));
|
|
826
845
|
const pageRange = computed(() => {
|
|
@@ -834,6 +853,7 @@ const pageRange = computed(() => {
|
|
|
834
853
|
const filteredIntercomRows = computed(() => {
|
|
835
854
|
const query = search.value.trim().toLowerCase();
|
|
836
855
|
return intercomRows.value.filter((row) => {
|
|
856
|
+
const matchesReader = !selectedReaderId.value || row.reader._id === selectedReaderId.value;
|
|
837
857
|
const matchesSearch = !query || [
|
|
838
858
|
row.name,
|
|
839
859
|
row.location,
|
|
@@ -843,18 +863,22 @@ const filteredIntercomRows = computed(() => {
|
|
|
843
863
|
row.status,
|
|
844
864
|
].some((value) => String(value || "").toLowerCase().includes(query));
|
|
845
865
|
const matchesStatus = statusFilter.value === "Status" || row.status === statusFilter.value;
|
|
846
|
-
return matchesSearch && matchesStatus;
|
|
866
|
+
return matchesReader && matchesSearch && matchesStatus;
|
|
847
867
|
});
|
|
848
868
|
});
|
|
849
869
|
|
|
850
870
|
const filteredContacts = computed(() => {
|
|
851
871
|
const query = search.value.trim().toLowerCase();
|
|
852
|
-
return contacts.value.filter((contact) =>
|
|
853
|
-
contact.
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
872
|
+
return contacts.value.filter((contact) => {
|
|
873
|
+
const matchesReader = !selectedReaderId.value || contact.readerId === selectedReaderId.value;
|
|
874
|
+
const matchesSearch = !query || [
|
|
875
|
+
contact.name,
|
|
876
|
+
contact.location,
|
|
877
|
+
contact.serverAddress,
|
|
878
|
+
contact.dialCode,
|
|
879
|
+
].some((value) => String(value || "").toLowerCase().includes(query));
|
|
880
|
+
return matchesReader && matchesSearch;
|
|
881
|
+
});
|
|
858
882
|
});
|
|
859
883
|
|
|
860
884
|
const paginatedIntercomRows = computed(() => paginate(filteredIntercomRows.value));
|
|
@@ -977,6 +1001,9 @@ async function reload() {
|
|
|
977
1001
|
try {
|
|
978
1002
|
const response = await getReaders({ site: props.site, page: 1, limit: 100 });
|
|
979
1003
|
readers.value = response?.items ?? response?.data?.items ?? response?.data?.readers ?? [];
|
|
1004
|
+
if (!readers.value.some((reader) => reader._id === selectedReaderId.value)) {
|
|
1005
|
+
selectedReaderId.value = readers.value[0]?._id || "";
|
|
1006
|
+
}
|
|
980
1007
|
await loadIntercomRows();
|
|
981
1008
|
await loadContacts();
|
|
982
1009
|
} catch (error: any) {
|
|
@@ -1328,9 +1355,10 @@ function toggleWebMute() {
|
|
|
1328
1355
|
function openContactDialog(contact?: ContactRow) {
|
|
1329
1356
|
selectedContact.value = contact || null;
|
|
1330
1357
|
contactForm.id = contact?.id;
|
|
1331
|
-
contactForm.readerId = contact?.readerId || readers.value[0]?._id || "";
|
|
1358
|
+
contactForm.readerId = contact?.readerId || selectedReaderId.value || readers.value[0]?._id || "";
|
|
1332
1359
|
contactForm.name = contact?.name === "N/A" ? "" : contact?.name || "";
|
|
1333
|
-
|
|
1360
|
+
const selectedReader = readers.value.find((reader) => reader._id === contactForm.readerId);
|
|
1361
|
+
contactForm.location = contact?.location === "N/A" ? "" : contact?.location || selectedReader?.location || "";
|
|
1334
1362
|
contactForm.serverAddress = contact?.serverAddress === "N/A" ? "" : contact?.serverAddress || "";
|
|
1335
1363
|
contactDialog.value = true;
|
|
1336
1364
|
}
|
|
@@ -1695,6 +1723,11 @@ export default {
|
|
|
1695
1723
|
max-width: 220px;
|
|
1696
1724
|
}
|
|
1697
1725
|
|
|
1726
|
+
.reader-input {
|
|
1727
|
+
width: 300px;
|
|
1728
|
+
max-width: 36vw;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1698
1731
|
.status-input {
|
|
1699
1732
|
max-width: 160px;
|
|
1700
1733
|
}
|
|
@@ -2673,6 +2706,7 @@ export default {
|
|
|
2673
2706
|
}
|
|
2674
2707
|
|
|
2675
2708
|
.top-placeholder,
|
|
2709
|
+
.reader-input,
|
|
2676
2710
|
.search-input,
|
|
2677
2711
|
.status-input {
|
|
2678
2712
|
width: 100%;
|
|
@@ -501,7 +501,6 @@ const identificationMethodOptions: Array<{
|
|
|
501
501
|
{ key: "qrCode", label: "QR Code" },
|
|
502
502
|
{ key: "idPassword", label: "ID Password" },
|
|
503
503
|
{ key: "pin", label: "PIN" },
|
|
504
|
-
{ key: "bluetooth", label: "Bluetooth" },
|
|
505
504
|
];
|
|
506
505
|
const accessPermissionOptions: Array<{
|
|
507
506
|
category: THidPermissionCategory;
|
|
@@ -590,7 +589,7 @@ async function loadConfig() {
|
|
|
590
589
|
qrCode: configuredMethods?.qrCode ?? Boolean(config.enabled),
|
|
591
590
|
idPassword: configuredMethods?.idPassword ?? true,
|
|
592
591
|
pin: configuredMethods?.pin ?? false,
|
|
593
|
-
bluetooth:
|
|
592
|
+
bluetooth: false,
|
|
594
593
|
},
|
|
595
594
|
printer: {
|
|
596
595
|
vendorId: String(config.printer?.vendorId || ""),
|