@7365admin1/layer-common 1.11.26 → 1.11.28

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.11.28
4
+
5
+ ### Patch Changes
6
+
7
+ - 21ced98: Update Site Settings
8
+
9
+ ## 1.11.27
10
+
11
+ ### Patch Changes
12
+
13
+ - 2797c03: updates and fixes
14
+
3
15
  ## 1.11.26
4
16
 
5
17
  ### Patch Changes
@@ -435,8 +435,6 @@ const prop = defineProps({
435
435
  });
436
436
 
437
437
  const { add: _addCard, updateById: _updateCardById } = useCard();
438
- const { getAll: _getBuildings } = useBuilding();
439
- const { getAllUnits: _getUnits } = useBuildingUnit();
440
438
  const {
441
439
  getDoorAccessLevels: _getDoorAccessLevels,
442
440
  getLiftAccessLevels: _getLiftAccessLevels,
@@ -444,6 +442,7 @@ const {
444
442
  addPhysicalCard: _addPhysicalCard,
445
443
  addNonPhysicalCard: _addNonPhysicalCard,
446
444
  bulkPhysicalAccessCard: _bulkPhysicalAccessCard,
445
+ getBlockLevelUnitList: _getBlockLevelUnitList,
447
446
  } = useAccessManagement();
448
447
  const { getBySiteId: _getEntryPassSettings } = useSiteEntryPassSettings();
449
448
  const { getFileUrl } = useFile();
@@ -570,7 +569,7 @@ const liftAccessLevelItems = ref<{ name: string; no: string }[]>([]);
570
569
  const buildingItems = ref<{ name: string; value: string }[]>([]);
571
570
  const levelItems = ref<{ name: string; value: string }[]>([]);
572
571
  const unitItems = ref<{ name: string; value: string }[]>([]);
573
- const buildingsData = ref<Record<string, any>[]>([]);
572
+ const blockLevelUnitData = ref<any[]>([]);
574
573
  const encryptedAcmUrl = ref("");
575
574
 
576
575
  const route = useRoute();
@@ -644,28 +643,24 @@ function updateEndDate(startDate: string | null) {
644
643
  }
645
644
  }
646
645
 
647
- // Watch for Assign toggle to fetch buildings
646
+ // Watch for Assign toggle to fetch block/level/unit data
648
647
  watch(() => card.value.showAssign, async (newVal) => {
649
648
  if (newVal) {
650
649
  try {
651
- const response = await _getBuildings({
652
- page: 1,
653
- status: "active",
654
- site: siteId,
655
- });
656
- buildingsData.value = response.items || [];
657
- buildingItems.value = buildingsData.value.map((building: any) => ({
658
- name: building.block,
659
- value: building._id,
650
+ const response = await _getBlockLevelUnitList(siteId);
651
+ blockLevelUnitData.value = (response as any)?.data || [];
652
+ buildingItems.value = blockLevelUnitData.value.map((b: any) => ({
653
+ name: b.block,
654
+ value: b._id,
660
655
  }));
661
656
  } catch (error) {
662
- console.error("Failed to fetch buildings:", error);
657
+ console.error("Failed to fetch block/level/unit list:", error);
663
658
  }
664
659
  } else {
665
- // Reset selections when toggled off
666
660
  card.value.assignBuilding = "";
667
661
  card.value.assignLevel = "";
668
662
  card.value.assignUnit = "";
663
+ blockLevelUnitData.value = [];
669
664
  buildingItems.value = [];
670
665
  levelItems.value = [];
671
666
  unitItems.value = [];
@@ -673,51 +668,35 @@ watch(() => card.value.showAssign, async (newVal) => {
673
668
  });
674
669
 
675
670
  // Watch for building selection to populate levels
676
- watch(() => card.value.assignBuilding, async (newVal) => {
671
+ watch(() => card.value.assignBuilding, (newVal) => {
672
+ card.value.assignLevel = "";
673
+ card.value.assignUnit = "";
674
+ unitItems.value = [];
675
+
677
676
  if (newVal) {
678
- const selectedBuilding = buildingsData.value.find((b: any) => b._id === newVal);
679
- if (selectedBuilding && selectedBuilding.levels) {
680
- levelItems.value = selectedBuilding.levels.map((level: string) => ({
681
- name: level,
682
- value: level,
683
- }));
684
- }
685
- // Reset level and unit when building changes
686
- card.value.assignLevel = "";
687
- card.value.assignUnit = "";
688
- unitItems.value = [];
677
+ const selectedBlock = blockLevelUnitData.value.find((b: any) => b._id === newVal);
678
+ levelItems.value = selectedBlock?.levels?.map((l: any) => ({
679
+ name: l.level,
680
+ value: l._id,
681
+ })) || [];
689
682
  } else {
690
683
  levelItems.value = [];
691
- card.value.assignLevel = "";
692
- card.value.assignUnit = "";
693
- unitItems.value = [];
694
684
  }
695
685
  });
696
686
 
697
- // Watch for level selection to fetch units
698
- watch(() => card.value.assignLevel, async (newVal) => {
687
+ // Watch for level selection to populate units
688
+ watch(() => card.value.assignLevel, (newVal) => {
689
+ card.value.assignUnit = "";
690
+
699
691
  if (newVal && card.value.assignBuilding) {
700
- try {
701
- const response = await _getUnits({
702
- page: 1,
703
- status: "active",
704
- site: siteId,
705
- building: card.value.assignBuilding,
706
- });
707
- const units = response.items || [];
708
- // Filter units by selected level
709
- const filteredUnits = units.filter((unit: any) => unit.level === newVal);
710
- unitItems.value = filteredUnits.map((unit: any) => ({
711
- name: unit.name,
712
- value: unit._id,
713
- }));
714
- } catch (error) {
715
- console.error("Failed to fetch units:", error);
716
- }
717
- card.value.assignUnit = "";
692
+ const selectedBlock = blockLevelUnitData.value.find((b: any) => b._id === card.value.assignBuilding);
693
+ const selectedLevel = selectedBlock?.levels?.find((l: any) => l._id === newVal);
694
+ unitItems.value = selectedLevel?.units?.map((u: any) => ({
695
+ name: u.name,
696
+ value: u._id,
697
+ })) || [];
718
698
  } else {
719
699
  unitItems.value = [];
720
- card.value.assignUnit = "";
721
700
  }
722
701
  });
723
702
 
@@ -68,7 +68,6 @@
68
68
  persistent-placeholder
69
69
  :rules="[requiredArrayRule]"
70
70
  :disabled="!form.level"
71
- :loading="unitLoading"
72
71
  multiple
73
72
  chips
74
73
  closable-chips
@@ -209,9 +208,8 @@ const {
209
208
  getLiftAccessLevels: _getLiftAccessLevels,
210
209
  assignAccessCard: _assignAccessCard,
211
210
  getAvailableAccessCards: _getAvailableAccessCards,
211
+ getBlockLevelUnitList: _getBlockLevelUnitList,
212
212
  } = useAccessManagement();
213
- const { getAll: _getBuildings } = useBuilding();
214
- const { getAllUnits: _getUnits } = useBuildingUnit();
215
213
  const { requiredRule } = useUtils();
216
214
 
217
215
  const route = useRoute();
@@ -221,7 +219,6 @@ const validForm = ref(false);
221
219
  const loading = ref(false);
222
220
  const levelsLoading = ref(false);
223
221
  const buildingLoading = ref(false);
224
- const unitLoading = ref(false);
225
222
  const availableLoading = ref(false);
226
223
  const availableCount = ref<number | null>(null);
227
224
 
@@ -245,7 +242,7 @@ const NO_SELECTION = { name: "No Selection", no: null };
245
242
  const accessLevelItems = ref<{ name: string; no: string | null }[]>([]);
246
243
  const liftAccessLevelItems = ref<{ name: string; no: string | null }[]>([]);
247
244
  const buildingItems = ref<{ name: string; value: string }[]>([]);
248
- const buildingsData = ref<Record<string, any>[]>([]);
245
+ const blockLevelUnitData = ref<any[]>([]);
249
246
  const levelItems = ref<{ name: string; value: string }[]>([]);
250
247
  const unitItems = ref<{ name: string; value: string }[]>([]);
251
248
 
@@ -303,15 +300,15 @@ onMounted(async () => {
303
300
  buildingLoading.value = true;
304
301
  levelsLoading.value = true;
305
302
 
306
- const [buildingsResult, acmResult] = await Promise.allSettled([
307
- _getBuildings({ page: 1, status: "active", site: siteId.value }),
303
+ const [blockLevelResult, acmResult] = await Promise.allSettled([
304
+ _getBlockLevelUnitList(siteId.value),
308
305
  $fetch<{ encrypted: string }>("/api/encrypt-acm-url"),
309
306
  ]);
310
307
 
311
- // Buildings
312
- if (buildingsResult.status === "fulfilled") {
313
- buildingsData.value = buildingsResult.value.items || [];
314
- buildingItems.value = buildingsData.value.map((b: any) => ({
308
+ // Blocks / Levels / Units
309
+ if (blockLevelResult.status === "fulfilled") {
310
+ blockLevelUnitData.value = (blockLevelResult.value as any)?.data || [];
311
+ buildingItems.value = blockLevelUnitData.value.map((b: any) => ({
315
312
  name: b.block,
316
313
  value: b._id,
317
314
  }));
@@ -345,20 +342,18 @@ onMounted(async () => {
345
342
 
346
343
  watch(
347
344
  () => form.value.building,
348
- async (newVal) => {
345
+ (newVal) => {
349
346
  form.value.level = null;
350
347
  form.value.units = [];
351
348
  levelItems.value = [];
352
349
  unitItems.value = [];
353
350
 
354
351
  if (newVal) {
355
- const selectedBuilding = buildingsData.value.find(
356
- (b: any) => b._id === newVal
357
- );
358
- if (selectedBuilding?.levels) {
359
- levelItems.value = selectedBuilding.levels.map((level: string) => ({
360
- name: level,
361
- value: level,
352
+ const selectedBlock = blockLevelUnitData.value.find((b: any) => b._id === newVal);
353
+ if (selectedBlock?.levels) {
354
+ levelItems.value = selectedBlock.levels.map((l: any) => ({
355
+ name: l.level,
356
+ value: l._id,
362
357
  }));
363
358
  }
364
359
  }
@@ -367,29 +362,18 @@ watch(
367
362
 
368
363
  watch(
369
364
  () => form.value.level,
370
- async (newVal) => {
365
+ (newVal) => {
371
366
  form.value.units = [];
372
367
  unitItems.value = [];
373
368
 
374
369
  if (newVal && form.value.building) {
375
- unitLoading.value = true;
376
- try {
377
- const response = await _getUnits({
378
- page: 1,
379
- status: "active",
380
- site: siteId.value,
381
- building: form.value.building,
382
- });
383
- const units = response.items || [];
384
- const filtered = units.filter((u: any) => u.level === newVal);
385
- unitItems.value = filtered.map((u: any) => ({
370
+ const selectedBlock = blockLevelUnitData.value.find((b: any) => b._id === form.value.building);
371
+ const selectedLevel = selectedBlock?.levels?.find((l: any) => l._id === newVal);
372
+ if (selectedLevel?.units) {
373
+ unitItems.value = selectedLevel.units.map((u: any) => ({
386
374
  name: u.name,
387
375
  value: u._id,
388
376
  }));
389
- } catch {
390
- console.error("Failed to fetch units");
391
- } finally {
392
- unitLoading.value = false;
393
377
  }
394
378
  }
395
379
  }
@@ -152,7 +152,7 @@ const props = defineProps({
152
152
  default: () => [
153
153
  {
154
154
  title: "Block",
155
- value: "block.name",
155
+ value: "block.block",
156
156
  },
157
157
  {
158
158
  title: "Level",
@@ -21,7 +21,7 @@ const prop = defineProps({
21
21
  },
22
22
  placeholder: {
23
23
  type: String,
24
- default: 'MM/DD/YYYY'
24
+ default: 'DD/MM/YYYY'
25
25
  }
26
26
  })
27
27
 
@@ -47,12 +47,12 @@ function validate() {
47
47
  function convertToReadableFormat(dateStr: string): string {
48
48
  if (!dateStr) return ""
49
49
  const dateObj = new Date(dateStr + "T00:00:00")
50
- const options: Intl.DateTimeFormatOptions = {
51
- year: 'numeric',
52
- month: '2-digit',
53
- day: '2-digit'
54
- }
55
- return dateObj.toLocaleDateString('en-US', options)
50
+ if (Number.isNaN(dateObj.getTime())) return ""
51
+
52
+ const day = String(dateObj.getDate()).padStart(2, "0")
53
+ const month = String(dateObj.getMonth() + 1).padStart(2, "0")
54
+ const year = String(dateObj.getFullYear())
55
+ return `${day}/${month}/${year}`
56
56
  }
57
57
 
58
58
  function handleInitialDate() {
@@ -131,6 +131,13 @@
131
131
  </template>
132
132
 
133
133
  <script setup lang="ts">
134
+ import useCustomerSite from '../composables/useCustomerSite';
135
+ import useLocal from '../composables/useLocal';
136
+ import { useLocalSetup } from '../composables/useLocalSetup';
137
+ import useRole from '../composables/useRole';
138
+ import useUser from '../composables/useUser';
139
+ import useUtils from '../composables/useUtils';
140
+
134
141
  const APP = useRuntimeConfig().public.APP;
135
142
  const props = defineProps({
136
143
  title: {
@@ -190,6 +197,7 @@ const invite = ref<Record<string, any>>({
190
197
  org: "",
191
198
  site: "",
192
199
  siteName: "",
200
+ isMemberInvite: false,
193
201
  });
194
202
 
195
203
  invite.value.app = props.app;
@@ -298,9 +306,14 @@ const roles = ref<Array<Record<string, any>>>([]);
298
306
  const { getRoles } = useRole();
299
307
 
300
308
  const { data: RolesData, refresh: refreshRoles } = await useLazyAsyncData(
301
- "get-roles-by-type",
302
- () => getRoles({ org: props.org, type: app.value, limit: 50 }),
303
- { watch: [app] }
309
+ "get-roles-by-type-" + props.org + "-" + props.app,
310
+ () =>
311
+ getRoles({
312
+ org: props.org,
313
+ type: props.app,
314
+ limit: 50,
315
+ }),
316
+ { watch: [() => props.app] }
304
317
  );
305
318
 
306
319
  watchEffect(() => {
@@ -309,10 +322,10 @@ watchEffect(() => {
309
322
  }
310
323
  });
311
324
 
312
- function handleUpdateApp(value: string) {
325
+ async function handleUpdateApp(value: string) {
313
326
  invite.value.role = "";
314
327
  invite.value.site = "";
315
- refreshRoles();
328
+ await refreshRoles()
316
329
  }
317
330
 
318
331
  const createMore = ref(false);
@@ -91,6 +91,9 @@
91
91
  </template>
92
92
 
93
93
  <script setup lang="ts">
94
+ import useRole from '../composables/useRole';
95
+ import useUtils from '../composables/useUtils';
96
+
94
97
  const props = defineProps({
95
98
  title: {
96
99
  type: String,
@@ -111,6 +111,9 @@
111
111
  </template>
112
112
 
113
113
  <script setup lang="ts">
114
+ import useRole from '../composables/useRole';
115
+ import useUtils from '../composables/useUtils';
116
+
114
117
  const definedModel = defineModel<Array<string>>({
115
118
  default: () => [],
116
119
  });
@@ -327,6 +327,8 @@
327
327
  </template>
328
328
 
329
329
  <script lang="ts" setup>
330
+ import useServiceProvider from '../composables/useServiceProvider';
331
+
330
332
  const props = defineProps({
331
333
  orgId: {
332
334
  type: String,
@@ -418,7 +420,7 @@ const serviceProvider = ref({
418
420
  const {
419
421
  getAll: getAllServiceProvider,
420
422
  add: addServiceProvider,
421
- invite: inviteServiceProvider,
423
+ createServiceProviderInvite
422
424
  } = useServiceProvider();
423
425
 
424
426
  const { getSiteById } = useSite();
@@ -536,12 +538,15 @@ async function submitServiceProviderInvite() {
536
538
  disableServiceProvider.value = true;
537
539
  messageServiceProvider.value = "";
538
540
  try {
539
- await inviteServiceProvider({
540
- email: serviceProvider.value.email,
541
- orgId: props.orgId,
542
- siteId: props.siteId,
543
- siteName: site.value?.name ?? "",
544
- });
541
+ const payload = {
542
+ email: serviceProvider.value.email?.trim(),
543
+ orgId: props.orgId,
544
+ siteId: props.siteId,
545
+ siteName: site.value?.name || props.siteName,
546
+ };
547
+
548
+
549
+ await createServiceProviderInvite(payload);
545
550
  if (createMoreServiceProvider.value) {
546
551
  serviceProvider.value.email = "";
547
552
  } else {
@@ -5,47 +5,57 @@
5
5
  <v-expansion-panel color="primary">
6
6
  <v-expansion-panel-title>
7
7
  <v-icon class="mr-2">mdi-cog</v-icon>
8
- Site Settings
8
+ Site Information
9
9
  </v-expansion-panel-title>
10
10
 
11
11
  <v-expansion-panel-text>
12
- <v-row no-gutters class="d-flex justify-center">
13
- <v-col cols="12">
14
- <v-row no-gutters>
15
- <v-col cols="12">
16
- <span class="text-h4 font-weight-bold"> Site Settings </span>
12
+ <v-row no-gutters class="d-flex justify-center py-10">
13
+ <v-form class="w-100">
14
+ <v-row>
15
+ <!-- Cover Photo + Preview -->
16
+ <v-col cols="12" lg="5" class="pt-0">
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" />
19
+ </v-col>
20
+ <v-col cols="12" lg="7" class="pl-lg-4 pt-4 pt-lg-0">
21
+ <InputLabel title="Preview" class="d-block mb-2" />
22
+ <v-img v-if="coverPhotoUrl" :src="coverPhotoUrl" aspect-ratio="16/9" cover rounded="lg"
23
+ class="w-100 border" />
24
+ <div v-else
25
+ class="w-100 d-flex align-center justify-center rounded-lg bg-grey-lighten-4 border border-dashed"
26
+ style="aspect-ratio: 16/9">
27
+ <span class="text-caption text-medium-emphasis">No cover photo uploaded</span>
28
+ </div>
17
29
  </v-col>
18
30
 
19
- <v-col cols="12">
20
- <v-row no-gutters>
21
- <v-col cols="12" lg="4" class="mt-2">
22
- <NumberSettingField
23
- v-model="blocks"
24
- title="No. of blocks"
25
- type="blocks"
26
- :read-only="!canManageSiteSettings"
27
- :existing-block-number="existingBlockNumber"
28
- :site-id="siteId"
29
- :disabled="existingBlockNumber === blocks"
30
- @success="refreshSiteData"
31
- />
32
- </v-col>
33
- </v-row>
31
+ <!-- Description + Site Documents -->
32
+ <v-col cols="12" lg="6" class="mt-2">
33
+ <InputLabel title="Description" class="d-block mb-1" />
34
+ <v-textarea v-model="description" variant="outlined" density="compact" rows="4" hide-details no-resize
35
+ style="height: 119px" />
36
+ </v-col>
37
+ <v-col cols="12" lg="6" class="mt-2 pl-lg-2">
38
+ <InputLabel title="Site Documents" class="d-block mb-1" />
39
+ <InputFileV2 v-model="siteDocuments" :multiple="true" accept="*/*" title="Upload documents"
40
+ :height="104" />
41
+ </v-col>
42
+
43
+ <!-- Submit -->
44
+ <v-col cols="12" class="pt-0 d-flex justify-end">
45
+ <v-btn color="primary-button" class="text-none" size="large" variant="flat" text="Save"
46
+ :loading="savingSiteInfo" @click="saveSiteInfo" />
47
+ </v-col>
48
+
49
+ <!-- Divider -->
50
+ <v-col cols="12" class="my-2">
51
+ <v-divider />
34
52
  </v-col>
35
53
 
36
54
  <v-col cols="12">
37
55
  <v-row no-gutters>
38
56
  <v-col cols="12" lg="4" class="mt-2">
39
- <NumberSettingField
40
- v-model="guardPosts"
41
- title="No. of guard posts"
42
- type="guard_posts"
43
- :read-only="!canManageSiteSettings"
44
- :existing-guard-posts-number="existingGuardPostNumber"
45
- :site-id="siteId"
46
- :disabled="existingGuardPostNumber === guardPosts"
47
- @success="refreshSiteData"
48
- />
57
+ <NumberSettingField v-model="blocks" title="No. of blocks" type="blocks" :site-id="siteId"
58
+ :disabled="existingBlockNumber === blocks" @success="refreshSiteData" />
49
59
  </v-col>
50
60
  </v-row>
51
61
  </v-col>
@@ -53,135 +63,76 @@
53
63
  <v-col cols="12">
54
64
  <v-row no-gutters>
55
65
  <v-col cols="12" lg="4" class="mt-2">
56
- <v-row no-gutters>
57
- <v-form v-model="gracePeriodValid">
58
- <v-row>
59
- <v-col cols="6">
60
- <InputLabel
61
- class="text-capitalize font-weight-bold"
62
- title="Grace Period"
63
- required
64
- />
65
- <v-text-field
66
- v-model="gracePeriod"
67
- type="number"
68
- density="comfortable"
69
- :readonly="!canManageSiteSettings"
70
- :rules="[requiredRule]"
71
- />
72
- </v-col>
73
-
74
- <v-col cols="6">
75
- <v-btn
76
- v-if="canManageSiteSettings"
77
- color="primary"
78
- class="text-none mt-6"
79
- size="large"
80
- variant="flat"
81
- :disabled="
82
- !gracePeriodValid ||
83
- existingGracePeriodNumber === gracePeriod
84
- "
85
- :loading="gracePeriodLoading"
86
- text="Save"
87
- @click="handleSaveGracePeriod"
88
- />
89
- </v-col>
90
- </v-row>
91
- </v-form>
92
- </v-row>
66
+ <NumberSettingField v-model="guardPosts" title="No. of guard posts" type="guard_posts"
67
+ :site-id="siteId" :disabled="existingGuardPostNumber === guardPosts"
68
+ @success="refreshSiteData" />
93
69
  </v-col>
94
70
  </v-row>
95
71
  </v-col>
96
72
  </v-row>
97
- </v-col>
73
+ </v-form>
98
74
  </v-row>
99
75
  </v-expansion-panel-text>
100
76
  </v-expansion-panel>
101
77
 
102
78
  <!-- ANPR CAMERA -->
103
- <v-expansion-panel color="primary">
79
+ <v-expansion-panel v-if="showAnprCamera" color="primary">
104
80
  <v-expansion-panel-title>
105
81
  <v-icon class="mr-2">mdi-camera</v-icon>
106
82
  ANPR Camera
107
83
  </v-expansion-panel-title>
108
84
 
109
85
  <v-expansion-panel-text>
110
- <CameraMain
111
- :site="siteId"
112
- :read-only="!canManageSiteSettings"
113
- :guard-posts="siteData?.metadata?.guardPosts"
114
- />
86
+ <CameraMain :site="siteId" :read-only="!canManageSiteSettings"
87
+ :guard-posts="siteData?.metadata?.guardPosts" />
115
88
  </v-expansion-panel-text>
116
89
  </v-expansion-panel>
117
90
 
118
91
  <!-- CCTV CAMERA -->
119
- <v-expansion-panel color="primary">
92
+ <v-expansion-panel v-if="showCctvCamera" color="primary">
120
93
  <v-expansion-panel-title>
121
94
  <v-icon class="mr-2">mdi-cctv</v-icon>
122
95
  CCTV Camera
123
96
  </v-expansion-panel-title>
124
97
 
125
98
  <v-expansion-panel-text>
126
- <CameraMain
127
- :site="siteId"
128
- type="ip"
129
- :read-only="!canManageSiteSettings"
130
- />
99
+ <CameraMain :site="siteId" type="ip" :read-only="!canManageSiteSettings" />
131
100
  </v-expansion-panel-text>
132
101
  </v-expansion-panel>
133
102
 
134
- <v-expansion-panel color="primary">
103
+ <v-expansion-panel v-if="showWorkOrderSettings" color="primary" class="">
135
104
  <v-expansion-panel-title>
136
105
  <v-icon class="mr-2">mdi-format-list-numbered</v-icon>
137
106
  Work Order Settings
138
107
  </v-expansion-panel-title>
139
108
 
140
109
  <v-expansion-panel-text>
141
- <v-text-field
142
- v-model="prefix"
143
- label="Prefix"
144
- :disabled="isLoading || !canManageSiteSettings"
145
- @input="handleInput"
146
- />
147
-
148
- <v-select
149
- v-model="noOfDigits"
150
- :items="[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
151
- label="No. of Digits"
152
- :disabled="isLoading || !canManageSiteSettings"
153
- />
110
+ <v-text-field v-model="prefix" label="Prefix" :disabled="isLoading || !canManageSiteSettings"
111
+ @input="handleInput" />
112
+
113
+ <v-select v-model="noOfDigits" :items="[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" label="No. of Digits"
114
+ :disabled="isLoading || !canManageSiteSettings" />
154
115
 
155
116
  <v-text-field v-model="orderNumberPreview" label="Preview" readonly />
156
117
 
157
- <v-btn
158
- v-if="canManageSiteSettings"
159
- class="mt-2"
160
- :loading="isLoading"
161
- @click="save"
162
- >
118
+ <v-btn v-if="canManageSiteSettings" class="mt-2" :loading="isLoading" @click="save">
163
119
  Save
164
120
  </v-btn>
165
121
  </v-expansion-panel-text>
166
122
  </v-expansion-panel>
167
123
 
168
- <v-expansion-panel color="primary">
124
+ <v-expansion-panel v-if="showDeliveryCompanies" color="primary">
169
125
  <v-expansion-panel-title>
170
126
  <v-icon class="mr-2">mdi-truck</v-icon>
171
127
  Delivery Companies
172
128
  </v-expansion-panel-title>
173
129
  <v-expansion-panel-text class="">
174
- <DeliveryCompany
175
- :site="siteId"
176
- v-model:initial="deliveryCompanies"
177
- @refresh-site="refreshSiteData"
178
- @update:companiesValue="handleUpdateCompanies"
179
- :read-only="!canManageSiteSettings"
180
- />
130
+ <DeliveryCompany :site="siteId" v-model:initial="deliveryCompanies" @refresh-site="refreshSiteData"
131
+ @update:companiesValue="handleUpdateCompanies" :read-only="!canManageSiteSettings" />
181
132
  </v-expansion-panel-text>
182
133
  </v-expansion-panel>
183
134
 
184
- <v-expansion-panel color="primary">
135
+ <v-expansion-panel v-if="showResidentVisitors" color="primary">
185
136
  <v-expansion-panel-title>
186
137
  <v-icon class="mr-2">mdi-car</v-icon>
187
138
  Resident Visitors
@@ -190,6 +141,8 @@
190
141
  <OvernightParkingAvailability :site="siteId" />
191
142
  </v-expansion-panel-text>
192
143
  </v-expansion-panel>
144
+
145
+ <slot name="panels" :can-manage="canManageSiteSettings" :site-id="siteId" />
193
146
  </v-expansion-panels>
194
147
 
195
148
  <Snackbar v-model="toast.show" :text="toast.message" :color="toast.color" />
@@ -197,14 +150,25 @@
197
150
  </template>
198
151
 
199
152
  <script setup lang="ts">
153
+ import useFile from '../composables/useFile';
154
+ import useSiteSettings from '../composables/useSiteSettings';
155
+ import useUtils from '../composables/useUtils';
156
+ import useWorkOrder from '../composables/useWorkOrder';
157
+
200
158
  const props = defineProps({
201
159
  siteId: { type: String, required: true },
202
160
  canManageSiteSettings: { type: Boolean, default: false },
161
+ showAnprCamera: { type: Boolean, default: false },
162
+ showCctvCamera: { type: Boolean, default: false },
163
+ showWorkOrderSettings: { type: Boolean, default: false },
164
+ showDeliveryCompanies: { type: Boolean, default: false },
165
+ showResidentVisitors: { type: Boolean, default: false },
203
166
  });
204
167
 
205
- const openPanels = ref([0]); // default open first
206
168
 
207
- const { getSiteById, updateSitebyId } = useSiteSettings();
169
+ const openPanels = ref([]); // default open first
170
+
171
+ const { getSiteById, updateSitebyId, updateSiteInformation } = useSiteSettings();
208
172
  const { requiredRule } = useUtils();
209
173
  const { getFileUrl } = useFile();
210
174
  const { getWorkOrderSettings, createWorkOrderSettings } = useWorkOrder();
@@ -228,12 +192,30 @@ const prefix = ref("");
228
192
  const noOfDigits = ref(1);
229
193
  const isLoading = ref(false);
230
194
 
195
+ const coverPhoto = ref<string[]>([]);
196
+ const description = ref<string>("");
197
+ const siteDocuments = ref<string[]>([]);
198
+ const siteDocumentsNames = ref<Record<string, string>>({});
199
+ const savingSiteInfo = ref<boolean>(false);
200
+
201
+
202
+ const coverPhotoUrl = computed(() =>
203
+ coverPhoto.value.length > 0 ? getFileUrl(coverPhoto.value[0]) : "",
204
+ );
205
+
231
206
  const toast = reactive({
232
207
  show: false,
233
208
  message: "",
234
209
  color: "",
235
210
  });
236
211
 
212
+ function showSnackbar(text: string, color = "success") {
213
+ toast.message = text;
214
+ toast.color = color;
215
+ toast.show = true;
216
+ }
217
+
218
+
237
219
  const { data: siteData, refresh: refreshSiteData } = await useLazyAsyncData(
238
220
  `site-${props.siteId}`,
239
221
  () => getSiteById(props.siteId)
@@ -242,24 +224,35 @@ const { data: siteData, refresh: refreshSiteData } = await useLazyAsyncData(
242
224
  watch(
243
225
  siteData,
244
226
  (val: TSite) => {
245
- if (!val) return;
246
-
247
- blocks.value = val.metadata?.block || 0;
248
- guardPosts.value = val.metadata?.guardPosts || 0;
249
- gracePeriod.value = val.metadata?.gracePeriod || 0;
250
-
251
- existingBlockNumber.value = blocks.value;
252
- existingGuardPostNumber.value = guardPosts.value;
253
- existingGracePeriodNumber.value = gracePeriod.value;
254
- deliveryCompanies.value = Array.isArray(val.deliveryCompanyList)
255
- ? [...val.deliveryCompanyList]
256
- : [];
257
-
258
- uploadedSiteLogo.value = val.metadata?.incidentLogo || "";
227
+ const siteDataValue = val as any;
228
+ if (siteDataValue) {
229
+ blocks.value = siteDataValue.metadata?.block || 0;
230
+ existingBlockNumber.value = blocks.value;
231
+
232
+ guardPosts.value = siteDataValue.metadata?.guardPosts || 0;
233
+ existingGuardPostNumber.value = guardPosts.value;
234
+ deliveryCompanies.value = Array.isArray(
235
+ siteDataValue?.deliveryCompanyList,
236
+ )
237
+ ? [...siteDataValue.deliveryCompanyList]
238
+ : [];
239
+
240
+ const info = siteDataValue.siteInformation;
241
+ if (info) {
242
+ coverPhoto.value = info.bgImage ? [info.bgImage] : [];
243
+ description.value = info.description ?? "";
244
+ const docs: { id: string; name: string }[] = info.docs ?? [];
245
+ siteDocuments.value = docs.map((d) => d.id);
246
+ docs.forEach((d) => {
247
+ siteDocumentsNames.value[d.id] = d.name;
248
+ });
249
+ }
250
+ }
259
251
  },
260
252
  { immediate: true }
261
253
  );
262
254
 
255
+
263
256
  async function handleSaveGracePeriod() {
264
257
  gracePeriodLoading.value = true;
265
258
  try {
@@ -276,6 +269,29 @@ async function handleSaveGracePeriod() {
276
269
  }
277
270
  }
278
271
 
272
+ async function saveSiteInfo() {
273
+ savingSiteInfo.value = true;
274
+ try {
275
+ await updateSiteInformation(props.siteId, {
276
+ bgImage: coverPhoto.value[0] ?? "",
277
+ description: description.value,
278
+ docs: siteDocuments.value.map((id) => ({
279
+ id,
280
+ name: siteDocumentsNames.value[id] ?? "",
281
+ })),
282
+ });
283
+ showSnackbar("Site information updated successfully.");
284
+ } catch (err: any) {
285
+ const msg =
286
+ err?.response?._data?.message ??
287
+ err?.message ??
288
+ "Failed to update site information.";
289
+ showSnackbar(msg, "error");
290
+ } finally {
291
+ savingSiteInfo.value = false;
292
+ }
293
+ }
294
+
279
295
  async function handleLogo(action: string) {
280
296
  try {
281
297
  await updateSitebyId(props.siteId, {
@@ -303,8 +319,8 @@ const { data: workOrderSetting } = await useLazyAsyncData(
303
319
 
304
320
  watchEffect(() => {
305
321
  if (workOrderSetting.value) {
306
- prefix.value = workOrderSetting.value.prefix;
307
- noOfDigits.value = workOrderSetting.value.noOfDigits;
322
+ prefix.value = workOrderSetting.value?.prefix;
323
+ noOfDigits.value = workOrderSetting.value?.noOfDigits;
308
324
  }
309
325
  });
310
326
 
@@ -366,23 +366,25 @@
366
366
  />
367
367
  <v-menu v-else-if="activeTab === 'overnight-parking'">
368
368
  <template #activator="{ props }">
369
- <v-avatar class="rounded-xl border-md">
370
- <v-icon v-bind="props" icon="mdi-dots-vertical" />
369
+ <v-avatar v-bind="props" class="rounded-xl border-md">
370
+ <v-icon icon="mdi-dots-vertical" />
371
371
  </v-avatar>
372
372
  </template>
373
373
 
374
374
  <v-card>
375
375
  <v-list-item
376
+ v-if="
377
+ !overNightParkingListActions[0].disabled ||
378
+ !['approved', 'rejected'].includes(
379
+ item?.overnightParking?.status
380
+ )
381
+ "
376
382
  @click.stop="
377
383
  openApproveRejectOverNightParkingRequestDialog(
378
384
  item,
379
385
  overNightParkingListActions[0].status
380
386
  )
381
387
  "
382
- :disabled="
383
- item?.overnightParking?.status == 'approved' ||
384
- !overNightParkingListActions[0].disabled
385
- "
386
388
  >
387
389
  <template #title>
388
390
  <span class="text-caption">
@@ -392,14 +394,18 @@
392
394
  </v-list-item>
393
395
  <v-divider />
394
396
  <v-list-item
395
- v-if="item?.overnightParking?.status !== 'approved'"
397
+ v-if="
398
+ !overNightParkingListActions[1].disabled ||
399
+ !['approved', 'rejected'].includes(
400
+ item?.overnightParking?.status
401
+ )
402
+ "
396
403
  @click="
397
404
  openApproveRejectOverNightParkingRequestDialog(
398
405
  item,
399
406
  overNightParkingListActions[1].status
400
407
  )
401
408
  "
402
- :disabled="!overNightParkingListActions[1].disabled"
403
409
  >
404
410
  <template #title>
405
411
  <span class="text-caption">
@@ -407,6 +413,22 @@
407
413
  </span>
408
414
  </template>
409
415
  </v-list-item>
416
+ <v-list-item
417
+ v-if="item?.overnightParking?.remarks"
418
+ @click="
419
+ openApproveRejectOverNightParkingRequestDialog(
420
+ item,
421
+ overNightParkingListActions[2].status,
422
+ item?.overnightParking?.remarks
423
+ )
424
+ "
425
+ >
426
+ <template #title>
427
+ <span class="text-caption">
428
+ {{ overNightParkingListActions[2].text }}
429
+ </span>
430
+ </template>
431
+ </v-list-item>
410
432
  <v-divider />
411
433
  </v-card>
412
434
  </v-menu>
@@ -831,7 +853,7 @@
831
853
  />
832
854
  </v-row>
833
855
  </v-toolbar>
834
- <v-card-text class="px-4 pb-2">
856
+ <v-card-text class="px-4 pb-6">
835
857
  <v-row no-gutters justify="center" align-content="center">
836
858
  <v-col
837
859
  v-if="
@@ -853,16 +875,31 @@
853
875
  outlined
854
876
  rows="3"
855
877
  clearable
878
+ :hide-details="
879
+ approveRejectOvernightParkingRequestDialog.status ===
880
+ 'remarks'
881
+ "
882
+ :readonly="
883
+ approveRejectOvernightParkingRequestDialog.status ===
884
+ 'remarks'
885
+ "
856
886
  />
857
887
  </v-col>
858
888
  </v-row>
859
889
  </v-card-text>
860
- <v-toolbar class="pa-0" density="compact">
890
+ <v-toolbar
891
+ v-if="approveRejectOvernightParkingRequestDialog.status !== 'remarks'"
892
+ class="pa-0"
893
+ density="compact"
894
+ >
861
895
  <v-row no-gutters>
862
896
  <v-col cols="6">
863
897
  <v-btn
864
898
  text="No"
865
- variant="text"
899
+ color="grey-lighten-3"
900
+ variant="flat"
901
+ height="48"
902
+ tile
866
903
  block
867
904
  :disabled="isApprovingRejectingOvernightParkingRequest"
868
905
  @click="closeApproveRejectOvernightParkingDialog"
@@ -874,7 +911,7 @@
874
911
  color="success"
875
912
  variant="flat"
876
913
  height="48"
877
- rounded="0"
914
+ tile
878
915
  block
879
916
  :disabled="
880
917
  !overNightParkingRequestApproveRejectRemarks ||
@@ -1077,6 +1114,10 @@ const overNightParkingListActions = [
1077
1114
  status: "rejected",
1078
1115
  disabled: props.canUpdateVisitor,
1079
1116
  },
1117
+ {
1118
+ text: "View Remarks",
1119
+ status: "remarks",
1120
+ },
1080
1121
  ];
1081
1122
  const overNightParkingRequestApproveRejectRemarks = ref("");
1082
1123
  const approveRejectOvernightParkingRequestDialog = ref({
@@ -1087,7 +1128,8 @@ const selectedOvernightParkingRequest = ref<Record<string, any>>({});
1087
1128
 
1088
1129
  function openApproveRejectOverNightParkingRequestDialog(
1089
1130
  visitor: Record<string, any>,
1090
- status: string
1131
+ status: string,
1132
+ remarks?: string
1091
1133
  ) {
1092
1134
  selectedOvernightParkingRequest.value = visitor;
1093
1135
  approveRejectOvernightParkingRequestDialog.value = {
@@ -1095,6 +1137,7 @@ function openApproveRejectOverNightParkingRequestDialog(
1095
1137
  status,
1096
1138
  };
1097
1139
  dialog.approveRejectOvernightParkingRequestDialog = true;
1140
+ overNightParkingRequestApproveRejectRemarks.value = remarks ?? "";
1098
1141
  }
1099
1142
 
1100
1143
  const isApprovingRejectingOvernightParkingRequest = ref(false);
@@ -279,6 +279,16 @@ export default function useAccessManagement() {
279
279
  );
280
280
  }
281
281
 
282
+ function getBlockLevelUnitList(siteId: string) {
283
+ return useNuxtApp().$api<{ message: string; data: any[] }>(
284
+ `/api/access-management/block-level-unit-list`,
285
+ {
286
+ method: "GET",
287
+ query: { site: siteId },
288
+ }
289
+ );
290
+ }
291
+
282
292
  function signQr(payload: { cardId: string; purpose: string }) {
283
293
  return useNuxtApp().$api<{ message: string; data: string }>(
284
294
  `/api/access-management/sign-qr`,
@@ -310,5 +320,6 @@ export default function useAccessManagement() {
310
320
  generateQrVms,
311
321
  createVisitorPass,
312
322
  signQr,
323
+ getBlockLevelUnitList,
313
324
  };
314
325
  }
@@ -69,9 +69,9 @@ function generateTimeSlotsFromStart(
69
69
  return slots;
70
70
  }
71
71
 
72
- // 11/24/2025, 09:05 format
72
+ // 24/11/2025, 09:05 format
73
73
  function toLocalTimeNumeric(utcString: string) {
74
- return new Date(utcString).toLocaleString("en-US", {
74
+ return new Date(utcString).toLocaleString("en-GB", {
75
75
  year: "numeric",
76
76
  month: "2-digit",
77
77
  day: "2-digit",
@@ -137,4 +137,4 @@ return {
137
137
 
138
138
  }
139
139
 
140
- }
140
+ }
@@ -116,7 +116,7 @@ export default function useOrg() {
116
116
  value: Partial<Pick<TOrg, "name" | "email" | "nature" | "contact">>
117
117
  ) {
118
118
  return useNuxtApp()
119
- .$api<Record<string, any>>("/api/organizations/onboarding-org", {
119
+ .$api<Record<string, any>>("/api/organizations", {
120
120
  method: "POST",
121
121
  body: value,
122
122
  })
@@ -126,7 +126,7 @@ export default function useOrg() {
126
126
  ) as TOrg;
127
127
  });
128
128
  }
129
-
129
+
130
130
  return {
131
131
  org,
132
132
  getOrgs,
@@ -1,3 +1,5 @@
1
+ import useMember from "./useMember";
2
+
1
3
  export default function useRole() {
2
4
  function createRole(
3
5
  { name, permissions, type, org } = {} as {
@@ -128,21 +128,29 @@ export default function useServiceProvider() {
128
128
 
129
129
  async function createServiceProviderInvite({
130
130
  email,
131
+ orgId,
131
132
  siteId,
132
- serviceProviderOrgId,
133
+ siteName,
133
134
  }: {
134
135
  email: string;
136
+ orgId: string;
135
137
  siteId: string;
136
- serviceProviderOrgId: string;
138
+ siteName: string;
137
139
  }) {
138
140
  try {
139
- await useNuxtApp().$api<Record<string, any>>(
141
+ const res = await useNuxtApp().$api<Record<string, any>>(
140
142
  "/api/auth/invite/service-provider",
141
143
  {
142
144
  method: "POST",
143
- body: { email, siteId, serviceProviderOrgId },
145
+ body: {
146
+ email,
147
+ orgId,
148
+ siteId,
149
+ siteName,
150
+ },
144
151
  }
145
152
  );
153
+ console.log(">>> response:", res);
146
154
  } catch (err) {
147
155
  console.error("Error creating service provider invite:", err);
148
156
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "1.11.26",
5
+ "version": "1.11.28",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
@@ -35,4 +35,4 @@ declare type TCustomerSite = {
35
35
  }
36
36
  };
37
37
 
38
- declare type TSiteCategory = "residential" | "commercial" | "industrial" | "institutional" | "mixed_development" | "infrastructure" | "hospitality"
38
+ declare type TSiteCategory = "residential" | "commercial" | "industrial" | "institutional" | "mixed_development" | "infrastructure" | "hospitality"