@7365admin1/layer-common 3.0.7 → 3.0.9

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/components/CameraMain.vue +17 -1
  3. package/components/DashboardMain.vue +1854 -848
  4. package/components/EntryPassInformation.vue +60 -0
  5. package/components/HidAccessLogDashboard.vue +1691 -0
  6. package/components/HidEnabledGate.vue +51 -0
  7. package/components/HidIdentityMapping.vue +975 -0
  8. package/components/HidQrCodeConfiguration.vue +618 -0
  9. package/components/HidReaderForm.vue +246 -0
  10. package/components/HidReaderManagement.vue +1233 -0
  11. package/components/HidServiceSettingsPanel.vue +150 -0
  12. package/components/HidUserEnrollment.vue +1274 -0
  13. package/components/Layout/NavigationDrawer.vue +31 -1
  14. package/components/MemberInformation.vue +23 -15
  15. package/components/NavigationItem.vue +11 -4
  16. package/components/OnlineFormFill.vue +34 -0
  17. package/components/PassInformation.vue +73 -34
  18. package/components/SiteSettings.vue +60 -116
  19. package/components/VisitorForm.vue +815 -290
  20. package/components/VisitorFormSelection.vue +12 -2
  21. package/components/VisitorManagement.vue +652 -205
  22. package/composables/useCommonPermission.ts +59 -0
  23. package/composables/useDashboard.ts +46 -1
  24. package/composables/useHidAmico.ts +152 -0
  25. package/composables/useHidNavigation.ts +106 -0
  26. package/composables/useMember.ts +7 -1
  27. package/composables/useOptionalServices.ts +118 -0
  28. package/composables/useSettingsPermission.ts +138 -0
  29. package/composables/useSiteSettings.ts +1 -1
  30. package/composables/useUser.ts +1 -2
  31. package/nuxt.config.ts +1 -1
  32. package/package.json +3 -2
  33. package/pages/[org]/[site]/access-mgmt/access-logs/index.vue +21 -0
  34. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +21 -0
  35. package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +21 -0
  36. package/pages/[org]/[site]/access-mgmt/hid-users/index.vue +21 -0
  37. package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +21 -0
  38. package/types/local.d.ts +2 -1
  39. package/types/visitor.d.ts +4 -2
@@ -12,7 +12,7 @@
12
12
 
13
13
  <!-- NAVIGATION ITEMS -->
14
14
  <v-tooltip
15
- v-for="(item, index) in props.navigationItems"
15
+ v-for="(item, index) in navigationItems"
16
16
  :key="`${item.title}-${index}`"
17
17
  location="right"
18
18
  :disabled="!item.disabled"
@@ -52,6 +52,36 @@ const emit = defineEmits(["navigate"]);
52
52
 
53
53
  const { drawer } = useLocal();
54
54
  const { APP_NAME } = useRuntimeConfig().public;
55
+ const route = useRoute();
56
+ const orgId = computed(() => String(route.params.org ?? ""));
57
+ const siteId = computed(() => String(route.params.site ?? ""));
58
+ const { useHidFaceReaderMenu } = useHidNavigation();
59
+ const { menuItem: hidFaceReaderMenu } = useHidFaceReaderMenu({
60
+ org: orgId,
61
+ site: siteId,
62
+ cacheKey: "layout-navigation-hid-site",
63
+ });
64
+
65
+ const navigationItems = computed(() => {
66
+ const items = [...(props.navigationItems as TNavigationItem[])];
67
+
68
+ if (
69
+ hidFaceReaderMenu.value &&
70
+ !items.some((item) => item.title === hidFaceReaderMenu.value?.title)
71
+ ) {
72
+ const insertAfterIndex = items.findIndex(
73
+ (item) => item.title === "Pass & Key Mgmt",
74
+ );
75
+
76
+ if (insertAfterIndex >= 0) {
77
+ items.splice(insertAfterIndex + 1, 0, hidFaceReaderMenu.value);
78
+ } else {
79
+ items.push(hidFaceReaderMenu.value);
80
+ }
81
+ }
82
+
83
+ return items;
84
+ });
55
85
 
56
86
  const handleClick = (item: any) => {
57
87
  if (item.disabled) return;
@@ -22,7 +22,7 @@
22
22
  <v-autocomplete v-model="selectedPass" v-model:search="passInput" :hide-no-data="false"
23
23
  class="mt-3" :items="passItemsFilteredFinal" item-title="prefixAndName" item-value="_id"
24
24
  label="Pass (optional)" variant="outlined" density="compact" :error-messages="errorScanPassMessage"
25
- persistent-hint small-chips>
25
+ persistent-hint small-chips @click="fetchPasses">
26
26
  <template v-slot:no-data>
27
27
  <v-list-item density="compact">
28
28
  <v-list-item-title v-if="passInput">
@@ -160,20 +160,28 @@ const passTypesComputed = computed(() => {
160
160
  }
161
161
  })
162
162
 
163
- const { data: passesData, refresh: refreshPassesData, pending: fetchPassesPending } = await useLazyAsyncData('get-pass-keys', () => {
164
- return getPassKeysByPageSearch({
165
- search: passInput.value,
166
- page: 1,
167
- limit: 500,
168
- passTypes: passTypesComputed.value,
169
- sites: [props.site],
170
- statuses: ["Available"]
171
- })
172
- })
173
-
174
- watch(passesData, (data: any) => {
175
- passItems.value = Array.isArray(data?.items) ? data.items : []
176
- })
163
+ const isPassListLoading = ref(false);
164
+
165
+ async function fetchPasses() {
166
+ // load pass list
167
+ passItems.value = [];
168
+ isPassListLoading.value = true;
169
+ try {
170
+ const result = await getPassKeysByPageSearch({
171
+ page: 1,
172
+ sites: [props.site],
173
+ limit: 10000,
174
+ passTypes: passTypesComputed.value,
175
+ statuses: ["Available"],
176
+ sortBy: { column: "prefixAndName", order: "1" },
177
+ });
178
+ passItems.value = result?.items;
179
+ } catch (error) {
180
+ showMessage(errorConverter(error as string), "error");
181
+ } finally {
182
+ isPassListLoading.value = false;
183
+ }
184
+ }
177
185
 
178
186
  const passItemsFilteredFinal = computed(() => {
179
187
  return passItems.value.filter((item: TPassKey) => {
@@ -22,9 +22,9 @@
22
22
  </v-list-group>
23
23
 
24
24
  <v-list-item
25
- v-else-if="props.route && props.route.name"
25
+ v-else-if="routeTo"
26
26
  :prepend-icon="icon"
27
- :to="props.route"
27
+ :to="routeTo"
28
28
  class="text-subtitle-2"
29
29
  >
30
30
  {{ title }}
@@ -76,8 +76,15 @@ const props = defineProps({
76
76
  });
77
77
 
78
78
  function onParentClick() {
79
- if (props.route && props.route.name) {
80
- navigateTo(props.route);
79
+ if (routeTo.value) {
80
+ navigateTo(routeTo.value);
81
81
  }
82
82
  }
83
+
84
+ const routeTo = computed(() => {
85
+ if (!props.route) return null;
86
+ if (props.route.name) return props.route;
87
+ if (props.route.path) return props.route;
88
+ return null;
89
+ });
83
90
  </script>
@@ -27,6 +27,24 @@
27
27
  size="x-small"
28
28
  class="text-capitalize"
29
29
  >{{ submissionStatus }}</v-chip>
30
+ <template v-if="requiresPayment">
31
+ <v-chip
32
+ v-if="initialManagementValues?.paymentStatus"
33
+ color="green"
34
+ variant="flat"
35
+ size="x-small"
36
+ >Payment {{ initialManagementValues.paymentStatus }}</v-chip>
37
+ <v-chip
38
+ v-else
39
+ color="orange"
40
+ variant="flat"
41
+ size="x-small"
42
+ >Payment Not Paid</v-chip>
43
+ <span
44
+ v-if="initialManagementValues?.paymentOrderId"
45
+ class="text-caption text-medium-emphasis"
46
+ >Order: {{ initialManagementValues.paymentOrderId }}</span>
47
+ </template>
30
48
  </div>
31
49
 
32
50
  <!-- Scrollable body -->
@@ -404,6 +422,7 @@ const props = defineProps({
404
422
  submissionUnit: { type: String, default: "" },
405
423
  approvalMode: { type: Boolean, default: false },
406
424
  initialManagementValues: { type: Object as PropType<Record<string, string>>, default: () => ({}) },
425
+ formType: { type: String, default: "" },
407
426
  /** Optional custom submit handler. Receives the collected form values.
408
427
  * If omitted the component submits to /api/online-forms automatically. */
409
428
  submitFn: {
@@ -505,6 +524,21 @@ const submissionStatusColor = computed(() => {
505
524
  }
506
525
  });
507
526
 
527
+ const PAYMENT_REQUIRED_FORM_TYPES = new Set([
528
+ "Application for Electronic Access Card",
529
+ "Application for Booking of BBQ Pit",
530
+ "Application for Moving In/Out & Delivery",
531
+ "Application for Main Entrance RFID Transponder",
532
+ ]);
533
+
534
+ const requiresPayment = computed(() => {
535
+ if (!PAYMENT_REQUIRED_FORM_TYPES.has(props.formType)) return false;
536
+ if (props.formType === "Application for Main Entrance RFID Transponder") {
537
+ return !!(props.initialValues.reasonLost || props.initialValues.reasonDamaged);
538
+ }
539
+ return true;
540
+ });
541
+
508
542
  // Today's date formatted DD MMM YYYY
509
543
  const todayDate = computed(() => {
510
544
  const d = new Date();
@@ -27,6 +27,8 @@
27
27
  persistent-hint
28
28
  small-chips
29
29
  :clearable="props.clearable"
30
+ @click="fetchPasses"
31
+ :loading="isPassListLoading"
30
32
  >
31
33
  <template v-slot:chip="{ props, item }">
32
34
  <v-chip
@@ -56,6 +58,8 @@
56
58
  persistent-hint
57
59
  small-chips
58
60
  :clearable="props.clearable"
61
+ @click="fetchKeys"
62
+ :loading="isKeyListLoading"
59
63
  >
60
64
  <template v-slot:chip="{ props, item }">
61
65
  <v-chip
@@ -83,6 +87,7 @@
83
87
  </v-card-text>
84
88
  </v-card>
85
89
  </v-row>
90
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
86
91
  </template>
87
92
 
88
93
  <script setup lang="ts">
@@ -90,6 +95,7 @@ import type { PropType } from "vue";
90
95
  import type { ValidationRule } from "vuetify/lib/types.mjs";
91
96
  import usePassKey from "../composables/usePassKey";
92
97
  import useKey from "../composables/useKey";
98
+ import { string } from "zod/v4";
93
99
 
94
100
  const props = defineProps({
95
101
  passRules: {
@@ -112,6 +118,10 @@ const props = defineProps({
112
118
  type: String,
113
119
  default: "",
114
120
  },
121
+ passKeys: {
122
+ type: Array,
123
+ default: [],
124
+ },
115
125
  hideKeys: {
116
126
  type: Boolean,
117
127
  default: false,
@@ -158,43 +168,72 @@ const passTypesComputed = computed(() => {
158
168
  }
159
169
  });
160
170
 
161
- const {
162
- data: passesData,
163
- refresh: refreshPassesData,
164
- pending: fetchPassesPending,
165
- } = await useLazyAsyncData("get-pass-keys", () => {
166
- return getPassKeysByPageSearch({
167
- search: passInput.value,
168
- page: 1,
169
- limit: 500,
170
- passTypes: passTypesComputed.value,
171
- sites: [props.site],
172
- statuses: ["Available"],
173
- });
174
- });
171
+ const message = ref("");
172
+ const messageColor = ref("");
173
+ const messageSnackbar = ref(false);
175
174
 
176
- watch(passesData, (data: any) => {
177
- passItems.value = data?.items || [];
178
- });
175
+ function showMessage(msg: string, color: string) {
176
+ message.value = msg;
177
+ messageColor.value = color;
178
+ messageSnackbar.value = true;
179
+ }
179
180
 
180
- const {
181
- data: keysData,
182
- refresh: refreshKeysData,
183
- pending: fetchKeysPending,
184
- } = await useLazyAsyncData("get-keys", () => {
185
- return getPassKeysByPageSearch({
186
- search: keyInput.value,
187
- statuses: ["Available"],
188
- passTypes: ["pass-key"],
189
- page: 1,
190
- limit: 500,
191
- sites: [props.site],
192
- });
193
- });
181
+ const isPassListLoading = ref(false);
194
182
 
195
- watch(keysData, (data: any) => {
196
- keyItems.value = data?.items || [];
197
- });
183
+ async function fetchPasses() {
184
+ // load pass list
185
+ passItems.value = [];
186
+ isPassListLoading.value = true;
187
+ try {
188
+ const result = await getPassKeysByPageSearch({
189
+ page: 1,
190
+ sites: [props.site],
191
+ limit: 10000,
192
+ passTypes: passTypesComputed.value,
193
+ statuses: ["Available"],
194
+ sortBy: { column: "prefixAndName", order: "1" },
195
+ });
196
+ passItems.value = result?.items;
197
+ } catch (error) {
198
+ showMessage(errorConverter(error as string), "error");
199
+ } finally {
200
+ isPassListLoading.value = false;
201
+ }
202
+ }
203
+
204
+ const isKeyListLoading = ref(false);
205
+
206
+ async function fetchKeys() {
207
+ keyItems.value = [];
208
+ isKeyListLoading.value = true;
209
+ try {
210
+ const keys: any = await getPassKeysByPageSearch({
211
+ page: 1,
212
+ sites: [props.site],
213
+ limit: 10000,
214
+ passTypes: ["pass-key"],
215
+ statuses: ["Available"],
216
+ sortBy: { column: "prefixAndName", order: "1" },
217
+ });
218
+ const selectedKeys = keyItems.value.filter((item: any) => {
219
+ if (Array.isArray(props?.passKeys) && props.passKeys.length) {
220
+ return props.passKeys.includes(item._id);
221
+ }
222
+ return false;
223
+ });
224
+ const filteredArray = keys.items.filter((item: any) => {
225
+ if (Array.isArray(props?.passKeys) && props.passKeys.length) {
226
+ return !props.passKeys.includes(item._id);
227
+ }
228
+ return true;
229
+ });
230
+ keyItems.value = [...selectedKeys, ...filteredArray];
231
+ } catch (error) {
232
+ showMessage(errorConverter(error as string), "error");
233
+ } finally {
234
+ isKeyListLoading.value = false;
235
+ }
236
+ }
198
237
 
199
238
  watch(selectedPass, (newVal) => {
200
239
  pass.value = [{ keyId: newVal }];
@@ -2,7 +2,7 @@
2
2
  <v-row no-gutters>
3
3
  <v-expansion-panels multiple v-model="openPanels">
4
4
  <!-- SITE SETTINGS -->
5
- <v-expansion-panel color="primary">
5
+ <v-expansion-panel v-if="canManageSiteInformation" color="primary">
6
6
  <v-expansion-panel-title>
7
7
  <v-icon class="mr-2">mdi-cog</v-icon>
8
8
  Site Information
@@ -14,73 +14,36 @@
14
14
  <v-row>
15
15
  <!-- Cover Photo + Preview -->
16
16
  <v-col cols="12" lg="5" class="pt-0">
17
- <InputLabel
18
- title="Cover Photo (displayed in resident app)"
19
- class="d-block mb-2"
20
- />
21
- <InputFileV2
22
- v-model="coverPhoto"
23
- accept="image/*"
24
- title="Upload cover photo"
25
- :height="120"
26
- />
17
+ <InputLabel title="Cover Photo (displayed in resident app)" class="d-block mb-2" />
18
+ <InputFileV2 v-model="coverPhoto" accept="image/*" title="Upload cover photo" :height="120" />
27
19
  </v-col>
28
20
  <v-col cols="12" lg="7" class="pl-lg-4 pt-4 pt-lg-0">
29
21
  <InputLabel title="Preview" class="d-block mb-2" />
30
- <v-img
31
- v-if="coverPhotoUrl"
32
- :src="coverPhotoUrl"
33
- aspect-ratio="16/9"
34
- cover
35
- rounded="lg"
36
- class="w-100 border"
37
- />
38
- <div
39
- v-else
22
+ <v-img v-if="coverPhotoUrl" :src="coverPhotoUrl" aspect-ratio="16/9" cover rounded="lg"
23
+ class="w-100 border" />
24
+ <div v-else
40
25
  class="w-100 d-flex align-center justify-center rounded-lg bg-grey-lighten-4 border border-dashed"
41
- style="aspect-ratio: 16/9"
42
- >
43
- <span class="text-caption text-medium-emphasis"
44
- >No cover photo uploaded</span
45
- >
26
+ style="aspect-ratio: 16/9">
27
+ <span class="text-caption text-medium-emphasis">No cover photo uploaded</span>
46
28
  </div>
47
29
  </v-col>
48
30
 
49
31
  <!-- Description + Site Documents -->
50
32
  <v-col cols="12" lg="6" class="mt-2">
51
33
  <InputLabel title="Description" class="d-block mb-1" />
52
- <v-textarea
53
- v-model="description"
54
- variant="outlined"
55
- density="compact"
56
- rows="4"
57
- hide-details
58
- no-resize
59
- style="height: 119px"
60
- />
34
+ <v-textarea v-model="description" variant="outlined" density="compact" rows="4" hide-details no-resize
35
+ style="height: 119px" />
61
36
  </v-col>
62
37
  <v-col cols="12" lg="6" class="mt-2 pl-lg-2">
63
38
  <InputLabel title="Site Documents" class="d-block mb-1" />
64
- <InputFileV2
65
- v-model="siteDocuments"
66
- :multiple="true"
67
- accept="*/*"
68
- title="Upload documents"
69
- :height="104"
70
- />
39
+ <InputFileV2 v-model="siteDocuments" :multiple="true" accept="*/*" title="Upload documents"
40
+ :height="104" />
71
41
  </v-col>
72
42
 
73
43
  <!-- Submit -->
74
44
  <v-col cols="12" class="pt-0 d-flex justify-end">
75
- <v-btn
76
- color="primary-button"
77
- class="text-none"
78
- size="large"
79
- variant="flat"
80
- text="Save"
81
- :loading="savingSiteInfo"
82
- @click="saveSiteInfo"
83
- />
45
+ <v-btn color="primary-button" class="text-none" size="large" variant="flat" text="Save"
46
+ :loading="savingSiteInfo" @click="saveSiteInfo" />
84
47
  </v-col>
85
48
 
86
49
  <!-- Divider -->
@@ -91,14 +54,8 @@
91
54
  <v-col cols="12">
92
55
  <v-row no-gutters>
93
56
  <v-col cols="12" lg="4" class="mt-2">
94
- <NumberSettingField
95
- v-model="blocks"
96
- title="No. of blocks"
97
- type="blocks"
98
- :site-id="siteId"
99
- :disabled="existingBlockNumber === blocks"
100
- @success="refreshSiteData"
101
- />
57
+ <NumberSettingField v-model="blocks" title="No. of blocks" type="blocks" :site-id="siteId"
58
+ :disabled="existingBlockNumber === blocks" @success="refreshSiteData" />
102
59
  </v-col>
103
60
  </v-row>
104
61
  </v-col>
@@ -106,14 +63,9 @@
106
63
  <v-col cols="12">
107
64
  <v-row no-gutters>
108
65
  <v-col cols="12" lg="4" class="mt-2">
109
- <NumberSettingField
110
- v-model="guardPosts"
111
- title="No. of guard posts"
112
- type="guard_posts"
113
- :site-id="siteId"
114
- :disabled="existingGuardPostNumber === guardPosts"
115
- @success="refreshSiteData"
116
- />
66
+ <NumberSettingField v-model="guardPosts" title="No. of guard posts" type="guard_posts"
67
+ :site-id="siteId" :disabled="existingGuardPostNumber === guardPosts"
68
+ @success="refreshSiteData" />
117
69
  </v-col>
118
70
  </v-row>
119
71
  </v-col>
@@ -124,7 +76,7 @@
124
76
  </v-expansion-panel>
125
77
 
126
78
  <!-- ANPR CAMERA -->
127
- <v-expansion-panel v-if="showAnprCamera" color="primary">
79
+ <v-expansion-panel v-if="showAnprCamera && canManageANPRCamera" color="primary">
128
80
  <v-expansion-panel-title>
129
81
  <v-icon class="mr-2">mdi-camera</v-icon>
130
82
  ANPR Camera
@@ -134,45 +86,24 @@
134
86
  <v-container fluid class="px-0">
135
87
  <v-row dense>
136
88
  <v-col cols="6">
137
- <v-switch
138
- v-model="ANPRSwitches.enableUnregistered"
139
- :disabled="!canManageSiteSettings"
140
- label="Enable Unregistered Vehicles"
141
- color="primary"
142
- hide-details
143
- ></v-switch>
89
+ <v-switch v-model="ANPRSwitches.enableUnregistered" :disabled="!canManageSiteSettings"
90
+ label="Enable Unregistered Vehicles" color="primary" hide-details></v-switch>
144
91
  </v-col>
145
92
 
146
93
  <v-col cols="6">
147
- <v-switch
148
- v-model="ANPRSwitches.openBarrierPickUpDropOff"
149
- :disabled="!canManageSiteSettings"
150
- label="Open Barrier for Pick-up and Drop-off"
151
- color="primary"
152
- hide-details
153
- ></v-switch>
94
+ <v-switch v-model="ANPRSwitches.openBarrierPickUpDropOff" :disabled="!canManageSiteSettings"
95
+ label="Open Barrier for Pick-up and Drop-off" color="primary" hide-details></v-switch>
154
96
  </v-col>
155
97
 
156
98
  <v-col cols="6">
157
- <v-switch
158
- v-model="ANPRSwitches.vehicleSnapshot"
159
- :disabled="!canManageSiteSettings"
160
- label="Capture Vehicle Snapshot"
161
- color="primary"
162
- hide-details
163
- ></v-switch>
99
+ <v-switch v-model="ANPRSwitches.vehicleSnapshot" :disabled="!canManageSiteSettings"
100
+ label="Capture Vehicle Snapshot" color="primary" hide-details></v-switch>
164
101
  </v-col>
165
102
  </v-row>
166
103
 
167
104
  <v-row v-if="canManageSiteSettings">
168
105
  <v-col cols="auto">
169
- <v-btn
170
- variant="flat"
171
- color="primary"
172
- class="text-none"
173
- :loading="isSaving"
174
- @click="saveANPRSettings"
175
- >
106
+ <v-btn variant="flat" color="primary" class="text-none" :loading="isSaving" @click="saveANPRSettings">
176
107
  Save
177
108
  </v-btn>
178
109
  </v-col>
@@ -181,47 +112,35 @@
181
112
 
182
113
  <v-divider class="my-4"></v-divider>
183
114
 
184
- <CameraMain
185
- :site="siteId"
186
- :read-only="!canManageSiteSettings"
187
- :guard-posts="siteData?.metadata?.guardPosts"
188
- />
115
+ <CameraMain :site="siteId" :read-only="!canManageSiteSettings"
116
+ :guard-posts="siteData?.metadata?.guardPosts" />
189
117
  </v-expansion-panel-text>
190
118
  </v-expansion-panel>
191
119
 
192
120
  <!-- CCTV CAMERA -->
193
- <v-expansion-panel v-if="showCctvCamera" color="primary">
121
+ <v-expansion-panel v-if="showCctvCamera && canManageCCTVCamera" color="primary">
194
122
  <v-expansion-panel-title>
195
123
  <v-icon class="mr-2">mdi-cctv</v-icon>
196
124
  CCTV Camera
197
125
  </v-expansion-panel-title>
198
126
 
199
127
  <v-expansion-panel-text>
200
- <CameraMain
201
- :site="siteId"
202
- type="ip"
203
- :read-only="!canManageSiteSettings"
204
- />
128
+ <CameraMain :site="siteId" type="ip" :read-only="!canManageSiteSettings" />
205
129
  </v-expansion-panel-text>
206
130
  </v-expansion-panel>
207
131
 
208
- <v-expansion-panel v-if="showDeliveryCompanies" color="primary">
132
+ <v-expansion-panel v-if="showDeliveryCompanies && canManageDeliveryCompanies" color="primary">
209
133
  <v-expansion-panel-title>
210
134
  <v-icon class="mr-2">mdi-truck</v-icon>
211
135
  Delivery Companies
212
136
  </v-expansion-panel-title>
213
137
  <v-expansion-panel-text class="">
214
- <DeliveryCompany
215
- :site="siteId"
216
- v-model:initial="deliveryCompanies"
217
- @refresh-site="refreshSiteData"
218
- @update:companiesValue="handleUpdateCompanies"
219
- :read-only="!canManageSiteSettings"
220
- />
138
+ <DeliveryCompany :site="siteId" v-model:initial="deliveryCompanies" @refresh-site="refreshSiteData"
139
+ @update:companiesValue="handleUpdateCompanies" :read-only="!canManageSiteSettings" />
221
140
  </v-expansion-panel-text>
222
141
  </v-expansion-panel>
223
142
 
224
- <v-expansion-panel v-if="showResidentVisitors" color="primary">
143
+ <v-expansion-panel v-if="showResidentVisitors && canManageResidentVisitors" color="primary">
225
144
  <v-expansion-panel-title>
226
145
  <v-icon class="mr-2">mdi-car</v-icon>
227
146
  Resident Visitors
@@ -231,6 +150,23 @@
231
150
  </v-expansion-panel-text>
232
151
  </v-expansion-panel>
233
152
 
153
+ <v-expansion-panel color="primary">
154
+ <v-expansion-panel-title>
155
+ <v-icon class="mr-2">mdi-face-recognition</v-icon>
156
+ HID Face Reader
157
+ </v-expansion-panel-title>
158
+ <v-expansion-panel-text>
159
+ <HidServiceSettingsPanel
160
+ :site="siteId"
161
+ :org="orgId"
162
+ :site-data="siteData"
163
+ @refresh="refreshSiteData"
164
+ />
165
+ <v-divider class="my-4" />
166
+ <HidQrCodeConfiguration :site="siteId" />
167
+ </v-expansion-panel-text>
168
+ </v-expansion-panel>
169
+
234
170
  <slot
235
171
  name="panels"
236
172
  :can-manage="canManageSiteSettings"
@@ -250,6 +186,7 @@ import useANPRSettings from "../composables/useANPRSettings";
250
186
 
251
187
  const props = defineProps({
252
188
  siteId: { type: String, required: true },
189
+ orgId: { type: String, default: "" },
253
190
  createdBy: { type: String },
254
191
  canManageSiteSettings: { type: Boolean, default: false },
255
192
  showAnprCamera: { type: Boolean, default: false },
@@ -259,6 +196,12 @@ const props = defineProps({
259
196
  });
260
197
 
261
198
  const openPanels = ref([]); // default open first
199
+ const { canManageSiteInformation,
200
+ canManageANPRCamera,
201
+ canManageCCTVCamera,
202
+ canManageDeliveryCompanies,
203
+ canManageResidentVisitors,
204
+ } = useSettingsPermission();
262
205
 
263
206
  const { getSiteById, updateSitebyId, updateSiteInformation } =
264
207
  useSiteSettings();
@@ -306,6 +249,7 @@ const { data: siteData, refresh: refreshSiteData } = await useLazyAsyncData(
306
249
  `site-${props.siteId}`,
307
250
  () => getSiteById(props.siteId)
308
251
  );
252
+ const orgId = computed(() => props.orgId || String((siteData.value as any)?.orgId ?? ""));
309
253
 
310
254
  watch(
311
255
  siteData,