@7365admin1/layer-common 1.11.38 → 1.11.40

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.40
4
+
5
+ ### Patch Changes
6
+
7
+ - 91ae5da: Update Verification Status
8
+
9
+ ## 1.11.39
10
+
11
+ ### Patch Changes
12
+
13
+ - 59c813e: Update Layer-common new version
14
+
3
15
  ## 1.11.38
4
16
 
5
17
  ### Patch Changes
@@ -11,12 +11,21 @@
11
11
  <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-5">
12
12
  <v-form ref="formRef" v-model="validForm" :disabled="disable">
13
13
  <v-row no-gutters class="pt-4">
14
+ <v-col v-if="isCCTV" cols="12">
15
+ <InputLabel class="text-capitalize" title="Camera Name" required />
16
+ <v-text-field
17
+ v-model.trim="camera.name"
18
+ density="comfortable"
19
+ :rules="[requiredRule]"
20
+ placeholder="Enter CCTV camera name"
21
+ />
22
+ </v-col>
14
23
  <v-col cols="12">
15
24
  <InputLabel class="text-capitalize" title="URL" required />
16
25
  <v-text-field
17
26
  v-model.trim="camera.host"
18
27
  density="comfortable"
19
- :rules="[requiredRule, isValidBaseURL]"
28
+ :rules="[requiredRule, hostRule]"
20
29
  placeholder="Enter camera URL"
21
30
  />
22
31
  </v-col>
@@ -32,7 +41,7 @@
32
41
  </v-col>
33
42
 
34
43
  <v-col v-if="isANPR" cols="12">
35
- <InputLabel class="text-capitalize" title="Direction" required />
44
+ <InputLabel class="text-capitalize" title="Type" required />
36
45
  <v-select
37
46
  v-model="camera.direction"
38
47
  :items="anprDirectionOptions"
@@ -50,7 +59,7 @@
50
59
  />
51
60
  </v-col>
52
61
 
53
- <v-col cols="12">
62
+ <v-col v-if="isANPR" cols="12">
54
63
  <InputLabel class="text-capitalize" title="User" required />
55
64
  <v-text-field
56
65
  v-model.trim="camera.username"
@@ -60,7 +69,7 @@
60
69
  />
61
70
  </v-col>
62
71
 
63
- <v-col cols="12">
72
+ <v-col v-if="isANPR" cols="12">
64
73
  <InputLabel class="text-capitalize" title="Password" required />
65
74
  <v-text-field
66
75
  v-model.trim="camera.password"
@@ -70,6 +79,20 @@
70
79
  placeholder="Enter password"
71
80
  />
72
81
  </v-col>
82
+
83
+ <v-col v-if="isANPR" cols="12">
84
+ <InputLabel class="text-capitalize" title="Status" />
85
+ <v-switch
86
+ v-model="camera.status"
87
+ hide-details
88
+ inset
89
+ density="comfortable"
90
+ true-value="active"
91
+ false-value="inactive"
92
+ :color="camera.status === 'active' ? 'success' : 'grey'"
93
+ :label="camera.status === 'active' ? 'Active' : 'Inactive'"
94
+ ></v-switch>
95
+ </v-col>
73
96
  </v-row>
74
97
  </v-form>
75
98
  </v-card-text>
@@ -107,7 +130,7 @@
107
130
  </template>
108
131
 
109
132
  <script setup lang="ts">
110
- import useSiteSettings from '../composables/useSiteSettings';
133
+ import useSiteSettings from "../composables/useSiteSettings";
111
134
 
112
135
  const prop = defineProps({
113
136
  title: {
@@ -139,9 +162,10 @@ const prop = defineProps({
139
162
  password: "",
140
163
  type: "ip",
141
164
  category: "standard",
142
- direction: "none",
165
+ direction: "both",
143
166
  guardPost: null,
144
167
  status: "active",
168
+ name: "",
145
169
  }),
146
170
  },
147
171
  });
@@ -149,6 +173,23 @@ const prop = defineProps({
149
173
  const { requiredRule, isValidBaseURL } = useUtils();
150
174
  const emit = defineEmits(["cancel", "select", "success", "error"]);
151
175
 
176
+ const hostRule = computed(() => {
177
+ return (v: string | any) => {
178
+ if (!v) return true;
179
+
180
+ const isAnpr = prop.type === 'anpr';
181
+
182
+ const anprRegex = /^http:\/\/[a-z0-9.-]+\.[a-z]{2,}(:\d+)?$/i;
183
+ const defaultRegex = /^https:\/\/[a-z0-9.-]+\.[a-z]{2,}\/[a-z0-9._-]+$/i;
184
+
185
+ if (isAnpr) {
186
+ return anprRegex.test(v) || 'Format: http://domain:port';
187
+ }
188
+
189
+ return defaultRegex.test(v) || 'Format: https://domain/path';
190
+ };
191
+ });
192
+
152
193
  const camera = ref<TSiteCamera>({
153
194
  site: "",
154
195
  host: "",
@@ -156,7 +197,8 @@ const camera = ref<TSiteCamera>({
156
197
  password: "",
157
198
  type: "ip",
158
199
  category: "standard",
159
- direction: "none",
200
+ direction: "both",
201
+ name: "",
160
202
  guardPost: null,
161
203
  status: "active",
162
204
  });
@@ -166,11 +208,11 @@ camera.value.site = prop.site;
166
208
  if (prop.mode === "edit") {
167
209
  camera.value = JSON.parse(JSON.stringify(prop.camera));
168
210
  }
169
-
211
+ console.log("prop.camera", prop?.camera);
170
212
  camera.value.site = prop.site;
171
213
  camera.value.type = prop.type;
172
- camera.value.category = prop.type === "anpr" ? "resident" : "standard";
173
- camera.value.direction = prop.type === "anpr" ? "both" : "none";
214
+ camera.value.category = prop.camera?.category && prop.title == "Edit Camera" ? prop.camera?.category : prop.type == "anpr" ? "resident" : "standard";
215
+ camera.value.direction = prop.camera?.direction && prop.title == "Edit Camera" ? prop.camera?.direction : prop.type == "anpr" ? "both" : "none";
174
216
 
175
217
  const validForm = ref(false);
176
218
  const formRef = ref<HTMLFormElement | null>(null);
@@ -178,6 +220,7 @@ const disable = ref(false);
178
220
  const message = ref("");
179
221
 
180
222
  const isANPR = computed(() => prop.type === "anpr");
223
+ const isCCTV = computed(() => prop.type === "ip");
181
224
 
182
225
  const anprCategoryOptions = computed(() => [
183
226
  { title: "Resident", value: "resident" },
@@ -187,8 +230,7 @@ const anprCategoryOptions = computed(() => [
187
230
  const anprDirectionOptions = computed(() => [
188
231
  { title: "Entry", value: "entry" },
189
232
  { title: "Exit", value: "exit" },
190
- { title: "Both", value: "both" },
191
- { title: "None", value: "none" },
233
+ { title: "Both Entry and Exit", value: "both" },
192
234
  ]);
193
235
 
194
236
  function back() {
@@ -210,7 +252,8 @@ const hasChanges = computed(() => {
210
252
  camera.value.password !== prop.camera.password ||
211
253
  camera.value.category !== prop.camera.category ||
212
254
  camera.value.guardPost !== prop.camera.guardPost ||
213
- camera.value.direction !== prop.camera.direction
255
+ camera.value.direction !== prop.camera.direction ||
256
+ camera.value.name !== prop.camera.name
214
257
  );
215
258
  }
216
259
 
@@ -237,6 +280,8 @@ async function submit() {
237
280
  category: camera.value.category,
238
281
  guardPost: camera.value.guardPost,
239
282
  direction: camera.value.direction,
283
+ status: camera.value.status,
284
+ name: camera.value.name,
240
285
  });
241
286
  }
242
287
 
@@ -24,7 +24,7 @@
24
24
 
25
25
  <v-card-text class="pa-0 bg-red">
26
26
  <v-data-table
27
- :headers="prop.headers"
27
+ :headers="computedHeaders"
28
28
  :items="items"
29
29
  fixed-header
30
30
  hide-default-footer
@@ -92,6 +92,12 @@
92
92
  class="pa-5 my-5 px-7"
93
93
  >
94
94
  <v-row no-gutters>
95
+ <v-col v-if="camera.type === 'ip'" cols="12" class="mb-2">
96
+ Camera Name:
97
+ <span class="font-weight-bold">
98
+ {{ camera.name }}
99
+ </span>
100
+ </v-col>
95
101
  <v-col cols="12" class="mb-2">
96
102
  Host:
97
103
  <span class="font-weight-bold">
@@ -99,14 +105,14 @@
99
105
  </span>
100
106
  </v-col>
101
107
 
102
- <v-col cols="12" class="mb-2">
108
+ <v-col v-if="camera.type === 'anpr'" cols="12" class="mb-2">
103
109
  Username:
104
110
  <span class="font-weight-bold">
105
111
  {{ camera.username }}
106
112
  </span>
107
113
  </v-col>
108
114
 
109
- <v-col cols="12" class="mb-2">
115
+ <v-col v-if="camera.type === 'anpr'" cols="12" class="mb-2">
110
116
  Password:
111
117
  <span class="font-weight-bold">
112
118
  {{ camera.password }}
@@ -275,6 +281,15 @@ const prop = defineProps({
275
281
  },
276
282
  });
277
283
 
284
+ const computedHeaders = computed(() => {
285
+ if (prop.type === "ip") {
286
+ const base = [...prop.headers];
287
+ base.splice(0, 0, { text: "Name", value: "name" });
288
+ return base;
289
+ }
290
+ return prop.headers;
291
+ });
292
+
278
293
  const items = ref<Array<TCamera>>([]);
279
294
  const page = ref(1);
280
295
  const pages = ref(0);
@@ -341,6 +356,7 @@ const camera = ref<TSiteCamera>({
341
356
  });
342
357
 
343
358
  function handleRowClick(_: any, data: any) {
359
+ console.log("row clicked", data.item);
344
360
  dialogPreview.value = true;
345
361
  camera.value = JSON.parse(JSON.stringify(data.item));
346
362
  }
@@ -1,9 +1,21 @@
1
1
  <template>
2
2
  <v-row no-gutters>
3
3
  <v-col cols="12">
4
- <TableMain :headers="currentHeaders" :items="items" :loading="loading" :page="page" :pages="pages"
5
- :pageRange="pageRange" :items-per-page="-1" :extension-height="120" :offset="300" show-header
6
- @refresh="handleRefresh" @update:page="handleUpdatePage">
4
+ <TableMain
5
+
6
+ :headers="currentHeaders"
7
+ :items="items"
8
+ :loading="loading"
9
+ :page="page"
10
+ :pages="pages"
11
+ :page-range="pageRange"
12
+ :items-per-page="limit"
13
+ :extension-height="120"
14
+ :offset="300"
15
+ show-header
16
+ @refresh="handleRefresh"
17
+ @update:page="handleUpdatePage"
18
+ >
7
19
  <!-- EXTENSION -->
8
20
  <template #extension>
9
21
  <v-row no-gutters class="w-100 d-flex flex-column ga-2 pt-2">
@@ -144,10 +156,15 @@ async function resolveRoleName(id: string) {
144
156
  roleCache.value[id] = roleName
145
157
  return roleName
146
158
  }
147
-
159
+ const limit = ref(10)
148
160
  /* ================= FETCH ================= */
149
161
  async function fetchActive() {
150
- const res = await getAll({ page: page.value, search: search.value })
162
+ const res = await getAll({
163
+ page: page.value,
164
+ search: search.value,
165
+ limit: limit.value,
166
+ })
167
+
151
168
  const orgs = res?.data?.items || res?.items || []
152
169
 
153
170
  const mapped = await Promise.all(
@@ -160,6 +177,7 @@ async function fetchActive() {
160
177
 
161
178
  try {
162
179
  const siteRes = await getCustomerSitesByOrgId(org._id)
180
+
163
181
  sitesCount =
164
182
  siteRes?.data?.totalItems ||
165
183
  siteRes?.totalItems ||
@@ -169,6 +187,7 @@ async function fetchActive() {
169
187
 
170
188
  const subRes = await getSubscriptionByOrgId(org._id)
171
189
  const subscription = subRes?.data || subRes
190
+
172
191
  if (subscription) {
173
192
  planType = subscription?.paidType || '-'
174
193
  billingCycle = subscription?.billingCycle || '-'
@@ -176,25 +195,33 @@ async function fetchActive() {
176
195
  subscriptionEnd = formatDate(subscription?.nextBillingDate)
177
196
  }
178
197
  } catch (e) {
179
- console.error('Error mapping org:', org._id, e)
198
+ console.error(e)
180
199
  }
181
200
 
182
201
  return {
183
- _id: org._id,
202
+ _id: `${org._id}-${page.value}`,
184
203
  ...org,
185
204
  sites: `${sitesCount} Sites`,
186
205
  planType,
187
206
  billingCycle,
188
207
  subscriptionStart,
189
208
  subscriptionEnd,
190
- status: org?.status || 'Active',
209
+ status: org?.status || 'active',
191
210
  }
192
211
  })
193
212
  )
194
213
 
195
- items.value = mapped
196
- pages.value = res?.data?.totalPages || res?.totalPages || 1
197
- totalItems.value = res?.data?.totalItems || res?.totalItems || mapped.length
214
+ items.value = []
215
+
216
+ await nextTick()
217
+
218
+ items.value = [...mapped]
219
+
220
+ pages.value = res?.pages || 1
221
+
222
+ const match = res?.pageRange?.match(/of\s+(\d+)/i)
223
+
224
+ totalItems.value = match ? Number(match[1]) : mapped.length
198
225
  }
199
226
 
200
227
  async function fetchSuspended() {
@@ -247,9 +274,13 @@ async function fetchSuspended() {
247
274
  })
248
275
  )
249
276
 
250
- items.value = mapped
251
- pages.value = res?.data?.totalPages || res?.totalPages || 1
252
- totalItems.value = res?.data?.totalItems || res?.totalItems || mapped.length
277
+ items.value = [...mapped]
278
+
279
+ pages.value = res?.pages || 1
280
+
281
+ const match = res?.pageRange?.match(/of\s+(\d+)/i)
282
+
283
+ totalItems.value = match ? Number(match[1]) : mapped.length
253
284
  }
254
285
 
255
286
  async function fetchPending() {
@@ -258,60 +289,75 @@ async function fetchPending() {
258
289
  type: 'user-invite',
259
290
  page: page.value,
260
291
  search: search.value,
261
- // app: 'organization',
262
292
  })
263
293
 
264
- items.value = await Promise.all(
294
+ items.value = [
295
+ ...(await Promise.all(
265
296
  (res?.items || []).map(async (i: any) => ({
266
297
  ...i,
267
298
  app:
268
299
  APP_LIST.find(a => a.value === i.metadata?.app)?.title ||
269
300
  i.metadata?.app ||
270
301
  '-',
302
+
271
303
  role: await resolveRoleName(i.metadata?.role),
272
304
  }))
273
- )
305
+ ))
306
+ ]
274
307
 
275
308
  pages.value = res?.pages || 1
276
- totalItems.value = res?.total || items.value.length
309
+
310
+ const match = res?.pageRange?.match(/of\s+(\d+)/i)
311
+
312
+ totalItems.value = match
313
+ ? Number(match[1])
314
+ : items.value.length
277
315
  }
278
316
 
279
317
  async function fetchData() {
280
318
  loading.value = true
319
+
281
320
  try {
282
- if (tab.value === 'suspended') {
321
+ if (tab.value === 'active') {
322
+ await fetchActive()
323
+ }
324
+ else if (tab.value === 'suspended') {
283
325
  items.value = []
284
326
  pages.value = 1
285
327
  totalItems.value = 0
286
- return
287
328
  }
288
- if (tab.value === 'active') await fetchActive()
289
- else await fetchPending()
329
+ else {
330
+ await fetchPending()
331
+ }
290
332
  } finally {
291
333
  loading.value = false
292
334
  }
293
335
  }
294
336
 
295
337
  /* ================= EVENTS ================= */
296
- function onTabChange() {
297
- page.value = 1
298
- fetchData()
299
- }
300
-
301
- function handleUpdatePage(p: number) {
338
+ async function handleUpdatePage(p: number) {
302
339
  page.value = p
303
- fetchData()
340
+ await fetchData()
341
+ }
342
+ async function onTabChange() {
343
+ page.value = 1
344
+ await fetchData()
304
345
  }
305
346
 
306
347
  function handleRefresh() {
307
348
  fetchData()
308
349
  }
309
350
 
351
+ let searchTimeout: any = null
352
+
310
353
  function handleSearch() {
311
- page.value = 1
312
- fetchData()
313
- }
354
+ clearTimeout(searchTimeout)
314
355
 
356
+ searchTimeout = setTimeout(async () => {
357
+ page.value = 1
358
+ await fetchData()
359
+ }, 500)
360
+ }
315
361
  /* ================= DIALOG ================= */
316
362
  function openInviteDialog() {
317
363
  dialog.value = true
@@ -24,7 +24,11 @@
24
24
  </v-row>
25
25
  </v-col>
26
26
 
27
- <v-col v-if="APP === 'organization'" cols="12" class="mt-2">
27
+ <v-col
28
+ v-if="!props.hideApp && props.adminState && APP === 'organization'"
29
+ cols="12"
30
+ class="mt-2"
31
+ >
28
32
  <v-row no-gutters>
29
33
  <InputLabel class="text-capitalize" title="App" required />
30
34
  <v-col cols="12">
@@ -55,7 +59,7 @@
55
59
  </v-row>
56
60
  </v-col>
57
61
 
58
- <v-col v-if="hasSite" cols="12">
62
+ <v-col v-if="hasSite && props.adminState" cols="12">
59
63
  <v-row no-gutters>
60
64
  <InputLabel class="text-capitalize" title="Site" />
61
65
  <v-col cols="12">
@@ -139,6 +143,8 @@ import useUser from '../composables/useUser';
139
143
  import useUtils from '../composables/useUtils';
140
144
 
141
145
  const APP = useRuntimeConfig().public.APP;
146
+
147
+
142
148
  const props = defineProps({
143
149
  title: {
144
150
  type: String,
@@ -177,13 +183,21 @@ const props = defineProps({
177
183
  metadata: { app: "organization", role: "" },
178
184
  }),
179
185
  },
186
+ adminState: {
187
+ type: Boolean,
188
+ default: true
189
+ },
190
+ hideApp: {
191
+ type: Boolean,
192
+ default: false
193
+ }
180
194
  });
181
195
 
182
196
  const emit = defineEmits(["cancel", "success", "success:create-more"]);
183
197
 
184
198
  const validForm = ref(false);
185
199
  const form = ref<HTMLFormElement | null>(null);
186
- const app = computed(() => useRuntimeConfig().public.APP ?? "");
200
+ // const app = computed(() => useRuntimeConfig().public.APP ?? "");
187
201
 
188
202
  const loading = reactive({
189
203
  submittingForm: false,
@@ -200,8 +214,24 @@ const invite = ref<Record<string, any>>({
200
214
  isMemberInvite: false,
201
215
  });
202
216
 
203
- invite.value.app = props.app;
204
- invite.value.org = props.org ?? "";
217
+ watch(
218
+ () => [props.hideApp, props.adminState, props.app, props.org],
219
+ () => {
220
+ invite.value.org = props.org || ""
221
+
222
+ if (props.hideApp) {
223
+ invite.value.app = "property_management_agency"
224
+ return
225
+ }
226
+
227
+ invite.value.app = props.adminState
228
+ ? (props.app || "")
229
+ : "property_management_agency"
230
+ },
231
+ { immediate: true }
232
+ )
233
+ // invite.value.app = props.app;
234
+ // invite.value.org = props.org ?? "";
205
235
 
206
236
  if (props.mode === "edit") {
207
237
  invite.value.email = props.invite.email;
@@ -1,45 +1,64 @@
1
+ <!-- LayoutNavigationDrawer.vue -->
1
2
  <template>
2
- <v-navigation-drawer v-model="drawer" permanent floating class="bg-primary">
3
+ <v-navigation-drawer
4
+ v-model="drawer"
5
+ permanent
6
+ floating
7
+ class="bg-primary"
8
+ >
3
9
  <v-list>
4
10
  <v-list-item>
5
11
  <v-list-item-title class="text-h6 text-white">
6
12
  {{ props.title || APP_NAME }}
7
13
  </v-list-item-title>
8
14
  </v-list-item>
15
+
9
16
  <slot name="action" />
10
- <template
11
- v-for="(navigationItem, navigationIndex) in props.navigationItems"
12
- :key="`${navigationItem.title}-${navigationIndex}`"
17
+
18
+ <!-- NAVIGATION ITEMS -->
19
+ <v-tooltip
20
+ v-for="(item, index) in props.navigationItems"
21
+ :key="`${item.title}-${index}`"
22
+ location="right"
23
+ :disabled="!item.disabled"
13
24
  >
14
- <NavigationItem
15
- v-if="navigationItem.children?.length"
16
- :title="navigationItem.title"
17
- :icon="navigationItem.icon"
18
- :route="navigationItem.route"
19
- :children="navigationItem.children"
20
- />
21
-
22
- <NavigationItem
23
- v-else-if="navigationItem.route"
24
- :title="navigationItem.title"
25
- :icon="navigationItem.icon"
26
- :link="navigationItem.link"
27
- :route="navigationItem.route"
28
- />
29
- </template>
30
-
31
- <slot name="services"> </slot>
25
+ <template #activator="{ props: tooltipProps }">
26
+ <div v-bind="tooltipProps">
27
+ <NavigationItem
28
+ :title="item.title"
29
+ :icon="item.icon"
30
+ :route="item.route"
31
+ :link="item.link"
32
+ :class="{
33
+ 'opacity-50 pointer-events-none': item.disabled
34
+ }"
35
+ @click="() => handleClick(item)"
36
+ />
37
+ </div>
38
+ </template>
39
+
40
+ <span>Please complete onboarding first</span>
41
+ </v-tooltip>
42
+
43
+ <!-- SERVICES SLOT -->
44
+ <slot name="services" />
32
45
  </v-list>
33
46
  </v-navigation-drawer>
34
47
  </template>
35
48
 
36
49
  <script setup lang="ts">
37
50
  const props = defineProps({
38
- navigationItems: { type: Array<TNavigationItem>, required: true },
51
+ navigationItems: { type: Array, required: true },
39
52
  title: { type: String, default: "" }
40
- });
53
+ })
54
+
55
+ const emit = defineEmits(['navigate'])
41
56
 
42
- const { drawer } = useLocal();
57
+ const { drawer } = useLocal()
58
+ const { APP_NAME } = useRuntimeConfig().public
43
59
 
44
- const { APP_NAME } = useRuntimeConfig().public;
60
+ const handleClick = (item: any) => {
61
+ if (item.disabled) return
62
+ emit('navigate', item)
63
+ }
45
64
  </script>
@@ -26,17 +26,11 @@
26
26
  </v-row>
27
27
  </v-col>
28
28
 
29
- <v-col cols="12" class="mt-2">
29
+ <v-col v-if="props.siteState" cols="12" class="mt-2">
30
30
  <v-row no-gutters>
31
31
  <InputLabel title="Site" />
32
32
  <v-col cols="12">
33
- <v-text-field
34
- :model-value="siteName"
35
- density="comfortable"
36
- readonly
37
- disabled
38
- :loading="loadSite"
39
- />
33
+ <v-text-field :model-value="siteName" density="comfortable" readonly disabled :loading="loadSite" />
40
34
  </v-col>
41
35
  </v-row>
42
36
  </v-col>
@@ -53,12 +47,21 @@
53
47
  </v-radio-group>
54
48
  </v-col>
55
49
 
50
+ <v-col cols="12" class="mt-2">
51
+ <v-btn
52
+ size="small"
53
+ variant="tonal"
54
+ class="text-none mb-2"
55
+ @click="() => toggleSelectAll(!isAllSelected)"
56
+ >
57
+ {{ isAllSelected ? 'Unselect All' : 'Select All' }}
58
+ </v-btn>
59
+ </v-col>
60
+
56
61
  <v-col cols="12" class="mt-3">
57
62
  <v-card variant="outlined" border="thin" rounded="lg">
58
63
  <template
59
- v-for="(
60
- actions, resourceKey, resourceIndex
61
- ) in props.permissions"
64
+ v-for="(actions, resourceKey, resourceIndex) in safePermissions"
62
65
  :key="resourceKey"
63
66
  >
64
67
  <v-divider v-if="resourceIndex > 0" />
@@ -94,18 +97,14 @@
94
97
  <!-- Expanded checkboxes when toggled on -->
95
98
  <template v-if="isCategoryEnabled(String(resourceKey))">
96
99
  <template
97
- v-for="(action, actionKey) in actions"
100
+ v-for="([actionKey, action]) in Object.entries(actions || {})"
98
101
  :key="actionKey"
99
102
  >
100
103
  <v-divider />
101
104
  <v-list-item density="compact" :ripple="false">
102
105
  <template #prepend>
103
106
  <v-checkbox-btn
104
- :model-value="
105
- selectedPermissions.includes(
106
- `${String(resourceKey)}:${String(actionKey)}`
107
- )
108
- "
107
+ :model-value="permState.selected.includes(`${String(resourceKey)}:${String(actionKey)}`)"
109
108
  @update:model-value="
110
109
  toggleAction(
111
110
  String(resourceKey),
@@ -204,59 +203,90 @@ const props = defineProps({
204
203
  type: String,
205
204
  default: "app",
206
205
  },
206
+ siteState: {
207
+ type: Boolean,
208
+ default: true,
209
+ },
207
210
  });
208
211
 
209
212
  const emit = defineEmits(["cancel", "success", "success:create-more"]);
210
213
 
211
214
  const validForm = ref(false);
212
215
  const name = ref("");
213
- const selectedPermissions = ref<Array<string>>([]);
214
216
  const platform = ref("web");
215
217
  const disable = ref(false);
216
218
  const message = ref("");
217
219
 
218
- const { requiredRule, requireListRule } = useUtils();
220
+ const { requiredRule } = useUtils();
219
221
  const { createRole } = useRole();
220
222
  const { getSiteById } = useSite();
221
223
 
222
- const enabledCategories = ref<Set<string>>(new Set());
224
+ // Gom selected + enabled vào 1 shallowRef duy nhất
225
+ // để chỉ trigger 1 render cycle khi update cả 2
226
+ const permState = shallowRef({
227
+ selected: [] as string[],
228
+ enabled: [] as string[],
229
+ })
223
230
 
224
- function selectedActionCount(resource: string) {
225
- return selectedPermissions.value.filter((p) => p.startsWith(`${resource}:`))
226
- .length;
231
+ function isCategoryEnabled(resource: string) {
232
+ return permState.value.enabled.includes(resource)
227
233
  }
228
234
 
229
- function isCategoryEnabled(resource: string) {
230
- return enabledCategories.value.has(resource);
235
+ function selectedActionCount(resource: string) {
236
+ return permState.value.selected.filter(p => p.startsWith(`${resource}:`)).length
231
237
  }
232
238
 
233
239
  function toggleCategory(resource: string, value: boolean | null) {
234
- const updated = new Set(enabledCategories.value);
240
+ const { selected, enabled } = permState.value
235
241
  if (value) {
236
- updated.add(resource);
242
+ if (!enabled.includes(resource)) {
243
+ permState.value = { selected, enabled: [...enabled, resource] }
244
+ }
237
245
  } else {
238
- updated.delete(resource);
239
-
240
- selectedPermissions.value = selectedPermissions.value.filter(
241
- (p) => !p.startsWith(`${resource}:`)
242
- );
246
+ permState.value = {
247
+ selected: selected.filter(p => !p.startsWith(`${resource}:`)),
248
+ enabled: enabled.filter(r => r !== resource),
249
+ }
243
250
  }
244
- enabledCategories.value = updated;
245
251
  }
246
252
 
247
253
  function toggleAction(resource: string, action: string, value: boolean | null) {
248
- const key = `${resource}:${action}`;
254
+ if (value !== true && value !== false) return
255
+ const key = `${resource}:${action}`
256
+ const { selected, enabled } = permState.value
249
257
  if (value) {
250
- if (!selectedPermissions.value.includes(key)) {
251
- selectedPermissions.value = [...selectedPermissions.value, key];
258
+ if (!selected.includes(key)) {
259
+ permState.value = { selected: [...selected, key], enabled }
252
260
  }
253
261
  } else {
254
- selectedPermissions.value = selectedPermissions.value.filter(
255
- (p) => p !== key
256
- );
262
+ permState.value = { selected: selected.filter(p => p !== key), enabled }
257
263
  }
258
264
  }
259
265
 
266
+ function toggleSelectAll(value: boolean) {
267
+ if (!value) {
268
+ permState.value = { selected: [], enabled: [] }
269
+ return
270
+ }
271
+
272
+ const perms = safePermissions.value
273
+ if (!perms) return
274
+
275
+ const allKeys: string[] = []
276
+ const categories: string[] = []
277
+
278
+ for (const [resource, actions] of Object.entries(perms)) {
279
+ if (!resource || !actions || typeof actions !== 'object') continue
280
+ categories.push(resource)
281
+ for (const action of Object.keys(actions)) {
282
+ if (action) allKeys.push(`${resource}:${action}`)
283
+ }
284
+ }
285
+
286
+ // 1 assignment duy nhất = 1 render cycle, không crash
287
+ permState.value = { selected: allKeys, enabled: categories }
288
+ }
289
+
260
290
  const siteName = ref("");
261
291
  const loadSite = ref(false);
262
292
 
@@ -286,7 +316,7 @@ async function submit() {
286
316
  const platformValue = platform.value === "web" ? "website" : platform.value;
287
317
  await createRole({
288
318
  name: name.value,
289
- permissions: selectedPermissions.value,
319
+ permissions: permState.value.selected,
290
320
  type: props.type,
291
321
  org: props.org,
292
322
  site: props.siteId,
@@ -301,11 +331,41 @@ async function submit() {
301
331
  }
302
332
 
303
333
  function cancel() {
304
- name.value = "";
305
- selectedPermissions.value = [];
306
- enabledCategories.value = new Set();
307
- platform.value = "web";
308
- message.value = "";
309
- emit("cancel");
334
+ name.value = ""
335
+ permState.value = { selected: [], enabled: [] }
336
+ platform.value = "web"
337
+ message.value = ""
338
+ emit("cancel")
310
339
  }
340
+
341
+ const safePermissions = computed(() => {
342
+ const p = props.permissions || {}
343
+ const cleaned: Record<string, any> = {}
344
+
345
+ for (const [k, v] of Object.entries(p)) {
346
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
347
+ cleaned[k] = v
348
+ } else {
349
+ cleaned[k] = {}
350
+ }
351
+ }
352
+
353
+ return cleaned
354
+ })
355
+
356
+ const isAllSelected = computed(() => {
357
+ const perms = safePermissions.value
358
+ const allKeys: string[] = []
359
+
360
+ Object.entries(perms).forEach(([resource, actions]) => {
361
+ if (!actions) return
362
+ Object.keys(actions).forEach((action) => {
363
+ if (action) allKeys.push(`${resource}:${action}`)
364
+ })
365
+ })
366
+
367
+ return allKeys.length > 0 &&
368
+ allKeys.every(k => permState.value.selected.includes(k))
369
+ })
370
+
311
371
  </script>
@@ -156,6 +156,73 @@
156
156
  </v-col>
157
157
  </v-row>
158
158
 
159
+ <v-row
160
+ v-if="selectedTask.areas && selectedTask.areas.length > 0"
161
+ no-gutters
162
+ class="mb-2"
163
+ >
164
+ <v-col cols="12">
165
+ <div class="text-subtitle-2 font-weight-bold mb-2">Units Under Selected Areas:</div>
166
+ <v-card variant="outlined" rounded>
167
+ <v-list density="compact" nav>
168
+ <template
169
+ v-for="(area, idx) in selectedTask.areas"
170
+ :key="area.value || area._id"
171
+ >
172
+ <v-divider v-if="Number(idx) > 0" />
173
+ <v-list-item
174
+ :title="area.name"
175
+ class="font-weight-medium"
176
+ @click="toggleViewArea(area.value || area._id)"
177
+ >
178
+ <template #prepend>
179
+ <v-icon size="16">
180
+ {{
181
+ openViewAreas.includes(area.value || area._id)
182
+ ? 'mdi-chevron-down'
183
+ : 'mdi-chevron-right'
184
+ }}
185
+ </v-icon>
186
+ </template>
187
+ <template #append>
188
+ <v-progress-circular
189
+ v-if="loadingViewUnits && !(area.value || area._id in fetchedViewAreaUnits)"
190
+ size="14"
191
+ width="2"
192
+ indeterminate
193
+ color="primary"
194
+ />
195
+ <v-chip v-else size="x-small" variant="tonal" color="primary">
196
+ {{ (fetchedViewAreaUnits[area.value || area._id] || []).length }}
197
+ </v-chip>
198
+ </template>
199
+ </v-list-item>
200
+ <v-expand-transition>
201
+ <div v-show="openViewAreas.includes(area.value || area._id)">
202
+ <v-card-text class="px-3 py-2">
203
+ <div
204
+ v-if="(fetchedViewAreaUnits[area.value || area._id] || []).length > 0"
205
+ class="d-flex flex-wrap ga-3"
206
+ >
207
+ <v-chip
208
+ v-for="unit in fetchedViewAreaUnits[area.value || area._id]"
209
+ :key="unit.unit"
210
+ size="small"
211
+ variant="tonal"
212
+ >
213
+ {{ unit.name }}
214
+ </v-chip>
215
+ </div>
216
+ <span v-else class="text-caption text-disabled">No units configured</span>
217
+ </v-card-text>
218
+ </div>
219
+ </v-expand-transition>
220
+ </template>
221
+ </v-list>
222
+ </v-card>
223
+ </v-col>
224
+ </v-row>
225
+
159
226
  <v-row v-if="selectedTask.description" no-gutters class="mb-2">
160
227
  <v-col cols="5" class="text-subtitle-2 font-weight-bold">
161
228
  Description:
@@ -260,6 +327,7 @@
260
327
  <script lang="ts" setup>
261
328
  import useScheduleTask from "../composables/useScheduleTask";
262
329
  import useUtils from "../composables/useUtils";
330
+ import useAreas from "../composables/useAreas";
263
331
 
264
332
  const props = defineProps({
265
333
  orgId: { type: String, required: true },
@@ -276,6 +344,7 @@ const props = defineProps({
276
344
  const { formatDate, debounce } = useUtils();
277
345
  const { getScheduleTasks, getScheduleTaskById, deleteScheduleTask } =
278
346
  useScheduleTask();
347
+ const { getAreaById } = useAreas();
279
348
 
280
349
  const canViewScheduleTasks = computed(() => props.canViewScheduleTasks);
281
350
  const canCreateScheduleTask = computed(() => props.canCreateScheduleTask);
@@ -301,6 +370,42 @@ const message = ref("");
301
370
  const messageSnackbar = ref(false);
302
371
  const messageColor = ref("success");
303
372
 
373
+ const loadingViewUnits = ref(false);
374
+ const fetchedViewAreaUnits = ref<Record<string, { name: string; unit: string }[]>>({});
375
+ const openViewAreas = ref<string[]>([]);
376
+
377
+ const toggleViewArea = (id: string) => {
378
+ const idx = openViewAreas.value.indexOf(id);
379
+ if (idx === -1) openViewAreas.value.push(id);
380
+ else openViewAreas.value.splice(idx, 1);
381
+ };
382
+
383
+ watch(dialogShowMoreActions, async (isOpen) => {
384
+ if (!isOpen) return;
385
+ const areas = (selectedTask.value?.areas || []) as any[];
386
+ if (areas.length === 0) return;
387
+ openViewAreas.value = areas.map((a) => a.value || a._id);
388
+ const toFetch = areas
389
+ .map((a) => a.value || a._id)
390
+ .filter((id) => id && !(id in fetchedViewAreaUnits.value));
391
+ if (toFetch.length === 0) return;
392
+ loadingViewUnits.value = true;
393
+ try {
394
+ await Promise.all(
395
+ toFetch.map(async (id: string) => {
396
+ try {
397
+ const result = await getAreaById(id);
398
+ fetchedViewAreaUnits.value[id] = result?.units || [];
399
+ } catch {
400
+ fetchedViewAreaUnits.value[id] = [];
401
+ }
402
+ })
403
+ );
404
+ } finally {
405
+ loadingViewUnits.value = false;
406
+ }
407
+ });
408
+
304
409
  const items = ref<any[]>([]);
305
410
 
306
411
  const headers = [
@@ -775,8 +775,12 @@ onMounted(() => {
775
775
  vehicle.seasonPassType = prop.vehicleData.seasonPassType || '';
776
776
  vehicle.end = prop.vehicleData.end || '';
777
777
  vehicle.start = prop.vehicleData.start || '';
778
- vehicle.plateNumber = prop.vehicleData.plates?.find((p) => p._id === prop.plateNumberId)?.plateNumber || '';
778
+ vehicle.plateNumber = prop.vehicleData.plateNumber
779
779
  vehicle.peopleId = prop.vehicleData.peopleId || '';
780
+
781
+ if(prop.type === "seasonpass"){
782
+ showSubscriptionDateOptions.value = true;
783
+ }
780
784
  }
781
785
  }, 300);
782
786
  })
@@ -37,13 +37,15 @@
37
37
  </v-dialog>
38
38
 
39
39
  <v-dialog v-model="dialog.createVehicle" v-if="vehicleType" width="450" persistent>
40
- <VehicleForm :type="vehicleType" :mode="mode" :vehicle-data="selectedVehicleObject" :plate-number-id="selectedPlateNumberId" @back="handleBackToSelection"
41
- @done="handleAddVehicleComplete" :org="org" :site="props.site" @close:all="handleCloseAll" />
40
+ <VehicleForm :type="vehicleType" :mode="mode" :vehicle-data="selectedVehicleObject"
41
+ :plate-number-id="selectedPlateNumberId" @back="handleBackToSelection" @done="handleAddVehicleComplete"
42
+ :org="org" :site="props.site" @close:all="handleCloseAll" />
42
43
  </v-dialog>
43
44
 
44
45
  <v-dialog v-model="dialog.updateVehicle" v-if="vehicleType" width="450" persistent>
45
- <VehicleForm :type="vehicleType" :mode="mode" :vehicle-data="selectedPlateNumberObject" :plate-number-id="selectedPlateNumberId" @back="handleBackToSelection"
46
- @done="handleAddVehicleComplete" :org="org" :site="props.site" @close:all="handleCloseAll" @close="handleCloseAll" />
46
+ <VehicleForm :type="vehicleType" :mode="mode" :vehicle-data="selectedPlateNumberObject"
47
+ :plate-number-id="selectedPlateNumberId" @back="handleBackToSelection" @done="handleAddVehicleComplete"
48
+ :org="org" :site="props.site" @close:all="handleCloseAll" @close="handleCloseAll" />
47
49
  </v-dialog>
48
50
 
49
51
  <!-- <v-dialog v-model="dialog.updateVehicle" v-if="vehicleType" width="450" persistent>
@@ -77,7 +79,8 @@
77
79
  </template>
78
80
  <v-list density="compact">
79
81
  <v-list-item v-if="(plateNumberItem as TPlateNumber)?.status == 'active'"
80
- title="Edit Vehicle" prepend-icon="mdi-pencil" @click="handleEditVehicleAction(plateNumberItem as TPlateNumber)" />
82
+ title="Edit Vehicle" prepend-icon="mdi-pencil"
83
+ @click="handleEditVehicleAction(plateNumberItem as TPlateNumber)" />
81
84
  <v-list-item
82
85
  v-if="props.app === 'property_management_agency' && (plateNumberItem as TPlateNumber)?.status == 'pending'"
83
86
  title="Approve" prepend-icon="mdi-check-circle" base-color="success"
@@ -110,7 +113,7 @@
110
113
  <v-col v-else-if="selectedVehicleObject[key]" cols="12">
111
114
  <span class="d-flex ga-3 align-center"><strong>{{ label }}:</strong> {{ formatValues(key,
112
115
  selectedVehicleObject[key])
113
- }}</span>
116
+ }}</span>
114
117
  </v-col>
115
118
  </template>
116
119
  </v-row>
@@ -184,7 +187,7 @@ const plateHeaders = [
184
187
 
185
188
 
186
189
  const { formatCamelCaseToWords, formatDate, debounce } = useUtils();
187
- const { getVehicles, deleteVehicle, formatVehicleStatus, approveVehicle, updateVehicle, getVehicleById } = useVehicle();
190
+ const { getVehicles, deleteVehicle, formatVehicleStatus, approveVehicle, updateVehicle, getSpecificVehicleById } = useVehicle();
188
191
 
189
192
  const items = ref<Array<Record<string, any>>>([]);
190
193
  const page = ref(1);
@@ -322,23 +325,30 @@ async function handleEditVehicleAction(item: TPlateNumber) {
322
325
 
323
326
  try {
324
327
  loadingPlateNumberDetails.value = true;
325
- const res = await getVehicleById(selectedPlateNumberId.value as string)
326
- if(res?.seasonPassType){
328
+ const res = await getSpecificVehicleById(selectedPlateNumberId.value as string)
329
+
330
+ if (res.type === 'blocklist') {
331
+ vehicleType.value = "blocklist";
332
+ } else {
333
+ if (res?.seasonPassType) {
327
334
  vehicleType.value = "seasonpass";
328
335
  } else {
329
- vehicleType.value = item?.type || null;
336
+ vehicleType.value = "whitelist";
330
337
  }
331
- selectedPlateNumberObject.value = res || null;
332
- dialog.showMoreActions = false;
333
- mode.value = "edit";
334
- dialog.updateVehicle = true;
335
-
338
+ }
339
+
340
+
341
+ selectedPlateNumberObject.value = res || null;
342
+ dialog.showMoreActions = false;
343
+ mode.value = "edit";
344
+ dialog.updateVehicle = true;
345
+
336
346
  } catch (error) {
337
347
  console.error("Error fetching vehicle details:", error);
338
348
  } finally {
339
349
  loadingPlateNumberDetails.value = false;
340
- }
341
-
350
+ }
351
+
342
352
  }
343
353
 
344
354
  function handleDeleteVehicleAction(item: TPlateNumber) {
@@ -924,6 +924,11 @@ async function submit() {
924
924
  }
925
925
  }
926
926
 
927
+ // add register status when registering unregistered visitor
928
+ if (prop.mode === 'register'){
929
+ payload.status = "registered"
930
+ }
931
+
927
932
  try {
928
933
  let res;
929
934
  if (prop.mode === "add") {
@@ -141,6 +141,13 @@ export default function useLocalAuth() {
141
141
  });
142
142
  }
143
143
 
144
+ async function updateVerificationStatus(id: string, status: string) {
145
+ return useNuxtApp().$api<Record<string, any>>(`/api/auth/verification/status`, {
146
+ method: "PATCH",
147
+ body: { id, status },
148
+ })
149
+ }
150
+
144
151
  function signUp(email: string, referral: string) {
145
152
  return useNuxtApp().$api<Record<string, any>>("/api/auth/sign-up", {
146
153
  method: "POST",
@@ -159,6 +166,7 @@ export default function useLocalAuth() {
159
166
  resetPassword,
160
167
  currentUser,
161
168
  verify,
169
+ updateVerificationStatus,
162
170
  signUp,
163
171
  };
164
172
  }
@@ -66,6 +66,7 @@ export default function () {
66
66
  | "password"
67
67
  | "username"
68
68
  | "direction"
69
+ | "status"
69
70
  >
70
71
  >
71
72
  ) {
@@ -131,6 +131,18 @@ export default function useVehicle() {
131
131
  return { label, color }
132
132
  }
133
133
 
134
+ // fetch specific plateNumber Vehicle data
135
+
136
+ function getSpecificVehicleById(id: string){
137
+ return useNuxtApp().$api<Record<string, any>>(
138
+ `/api/vehicles/specific/${id}`,
139
+ {
140
+ method: "GET",
141
+ }
142
+ );
143
+ }
144
+
145
+
134
146
  return {
135
147
  getVehicles,
136
148
  addVehicle,
@@ -141,6 +153,7 @@ export default function useVehicle() {
141
153
  getVehicleByNRIC,
142
154
  formatVehicleStatus,
143
155
  approveVehicle,
144
- searchMultipleVehiclesByUnitId
156
+ searchMultipleVehiclesByUnitId,
157
+ getSpecificVehicleById
145
158
  };
146
159
  }
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.38",
5
+ "version": "1.11.40",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
package/types/camera.d.ts CHANGED
@@ -26,6 +26,7 @@ declare type TSiteCamera = {
26
26
  type: "ip" | "anpr";
27
27
  category: "standard" | "resident" | "visitor";
28
28
  direction: "entry" | "exit" | "both" | "none";
29
+ name?: string;
29
30
  guardPost: number | null;
30
31
  status: string;
31
32
  };