@7365admin1/layer-common 1.11.27 → 1.11.29

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.
@@ -0,0 +1,400 @@
1
+ <template>
2
+ <v-card height="100%" width="100%" flat class="pa-2 pa-sm-5">
3
+ <v-row no-gutters>
4
+ <v-col cols="12">
5
+ <v-row no-gutters class="d-flex ga-2 justify-space-between align-start mb-3 mb-sm-5 flex-wrap">
6
+
7
+ <div class="d-flex flex-column" style="min-width: 0;">
8
+ <span class="text-subtitle-1 font-weight-bold" style="word-break: break-word;">Visitor Management - Visitor ({{ items.length }} visitors)</span>
9
+ <span style="font-size: clamp(11px, 1.1vw, 13px); word-break: break-word;">Date Range: {{ formatDateDDMMYYYY(dateFrom) }} - {{ formatDateDDMMYYYY(dateTo) ?? "" }}</span>
10
+ <span class="text-subtitle-1" style="word-break: break-word;">Site Name: {{ activeSiteName }}</span>
11
+ </div>
12
+
13
+ </v-row>
14
+ <div style="overflow-x: auto; max-width: 100%;">
15
+ <v-data-table :headers="headers" :items="items" show-header :items-per-page="100" hide-default-footer class="text-black"
16
+ class="text-black" no-data-text="No entries to display">
17
+
18
+ <template v-slot:item.typeCompany="{ item }">
19
+ <span class="d-flex align-center ga-2" style="flex-wrap: wrap;">
20
+ <v-icon icon="mdi-user" size="15" />
21
+ <span v-if="item.type === 'contractor'" class="text-capitalize" style="word-break: break-word;">{{
22
+ formatCamelCaseToWords(item.contractorType as string)
23
+ }}</span>
24
+ <span v-else class="text-capitalize" style="word-break: break-word;">{{ formatType(item) }}</span>
25
+ </span>
26
+ <span class="d-flex align-center ga-2" style="flex-wrap: wrap;">
27
+ <v-icon icon="mdi-domain" size="15" />
28
+ <span class="text-capitalize" style="word-break: break-word;">{{ item?.company || "N/A" }}</span>
29
+ </template>
30
+
31
+ <template v-slot:item.location="{ item }">
32
+ <span class="d-flex align-center ga-2" style="flex-wrap: wrap;">
33
+ <v-icon icon="mdi-storefront-outline" size="15" />
34
+ <span class="text-capitalize" style="word-break: break-word;">{{
35
+ formatLocation({
36
+ block: item.block as number,
37
+ level: item.level as string,
38
+ unit: item.unitName as string,
39
+ })
40
+ }}</span>
41
+ </span>
42
+ </template>
43
+
44
+ <template v-slot:item.contact-vehicleNumber="{ item }">
45
+ <span class="d-flex align-center ga-2" style="flex-wrap: wrap;">
46
+ <v-icon icon="mdi-phone" size="15" />
47
+ <span class="text-capitalize" style="word-break: break-word;">{{ item?.contact || "N/A" }}</span>
48
+ </span>
49
+ <span class="d-flex align-center ga-2" style="flex-wrap: wrap;">
50
+ <v-icon icon="mdi-car-back" size="15" />
51
+ <span class="text-capitalize" style="word-break: break-word;">{{ item?.plateNumber || "N/A" }}</span>
52
+ </span>
53
+ </template>
54
+
55
+ <template v-slot:item.checkInOut="{ item }">
56
+ <span class="d-flex align-center ga-2" style="flex-wrap: wrap;">
57
+ <v-icon icon="mdi-login" size="15" />
58
+ <span class="text-capitalize" style="word-break: break-word;">{{ formatDate(item.checkIn as string) }}</span>
59
+ </span>
60
+ <span class="d-flex align-center ga-2" style="flex-wrap: wrap;">
61
+ <v-icon icon="mdi-logout" size="15" />
62
+ <span class="text-capitalize" style="word-break: break-word;">{{ formatDate(item.checkOut as string) }}</span>
63
+ </span>
64
+ </template>
65
+
66
+ <template v-slot:item.checkInOutImage="{ item }">
67
+ <div class="d-flex flex-column ga-2">
68
+ <div v-if="item?.snapshotEntryImage" class="d-flex flex-column">
69
+ <span class="text-caption font-weight-bold text-blue-darken-1">Check-In</span>
70
+ <v-img :src="getFileUrlAnpr(`${siteId}/${item.snapshotEntryImage}`)"
71
+ style="max-width: clamp(60px, 10vw, 100px); max-height: clamp(60px, 10vw, 100px);"
72
+ class="border" />
73
+ </div>
74
+ <div v-if="item?.snapshotExitImage" class="d-flex flex-column">
75
+ <span class="text-caption font-weight-bold text-red-darken-1">Check-Out</span>
76
+ <v-img :src="getFileUrlAnpr(`${siteId}/${item.snapshotExitImage}`)"
77
+ style="max-width: clamp(60px, 10vw, 100px); max-height: clamp(60px, 10vw, 100px);"
78
+ class="border" />
79
+ </div>
80
+ <span v-if="!item?.snapshotEntryImage && !item?.snapshotExitImage"
81
+ class="text-caption text-disabled">
82
+ No images
83
+ </span>
84
+ </div>
85
+ </template>
86
+
87
+
88
+ </v-data-table>
89
+ </div>
90
+ </v-col>
91
+
92
+ </v-row>
93
+ </v-card>
94
+ </template>
95
+
96
+ <script setup lang="ts">
97
+ import moment from 'moment-timezone';
98
+ import useLocalAuth from '../composables/useLocalAuth';
99
+ import useVisitor from '../composables/useVisitor';
100
+ import useUtils from '../composables/useUtils';
101
+ import useSite from '../composables/useSite';
102
+
103
+
104
+ definePageMeta({
105
+ middleware: ["01-auth", "02-org"],
106
+ });
107
+
108
+
109
+ const props = defineProps({
110
+ siteId: {
111
+ type: String,
112
+ required: true,
113
+ },
114
+ })
115
+
116
+ const { getVisitors } = useVisitor();
117
+ const { formatCamelCaseToWords } = useUtils();
118
+ const { currentUser } = useLocalAuth();
119
+ const { getSiteById } = useSite(); const { getFileUrlAnpr } = useFile(); const route = useRoute();
120
+ const { org: orgId, site: siteId } = route.params as {
121
+ org: string;
122
+ site: string;
123
+ reportId: string;
124
+ };
125
+
126
+ const activeSiteName = computed(() => {
127
+ const site = sites.value.find(s => s._id === props.siteId)
128
+ return site ? site.name : ''
129
+ })
130
+
131
+
132
+ const headers: Record<string, any>[] = [
133
+ { title: "Name", value: "name", align: "start" },
134
+ { title: "Type/Company", value: "typeCompany", align: "start" },
135
+ { title: "Location", value: "location", align: "start" },
136
+ { title: "Contact/Vehicle No", value: "contact-vehicleNumber", align: "start" },
137
+ { title: "Check In/Out", value: "checkInOut", align: "center" },
138
+ { title: "Check In/Out Image", value: "checkInOutImage", align: "start" },
139
+ ]
140
+
141
+
142
+
143
+
144
+ const searchInput = ref("");
145
+ const dateFrom = ref<string | null>(null);
146
+ const dateTo = ref<string | null>(null);
147
+ const filterTypes = ref<string[]>([]);
148
+ const activeTab = ref("registered");
149
+ const displayNotCheckedOut = ref(false);
150
+
151
+ const sites = ref<Array<Record<string, any>>>([]);
152
+
153
+ const items = ref<TVisitor[]>([])
154
+
155
+ const formatType = (item: any) =>
156
+ (item.deliveryType ? item.deliveryType + "-" : "") + item.type;
157
+
158
+
159
+ const { data: getSitesReq } = await useLazyAsyncData(
160
+ "get-customer-site-" + props.siteId,
161
+ () =>
162
+ getSiteById(props.siteId),
163
+ {
164
+ watch: [() => props.siteId],
165
+ }
166
+ );
167
+
168
+ watch(getSitesReq, (newData: any) => {
169
+ if (newData) {
170
+ sites.value = newData ? [newData] : []
171
+ }
172
+ })
173
+
174
+ const formatLocation = ({ block, level, unit }: { block: number, level: string, unit: string | TBuildingUnit }) => {
175
+ const parts = [];
176
+
177
+ if (block) parts.push(`Block ${block}`);
178
+ if (level) parts.push(level);
179
+ if (unit) parts.push(unit);
180
+
181
+ return parts.join(" / ");
182
+ };
183
+
184
+ function formatDate(dateString: string) {
185
+ if (!dateString) return "";
186
+
187
+ const date = new Date(dateString);
188
+ return date.toLocaleDateString("en-US", {
189
+ year: "numeric",
190
+ month: "short",
191
+ day: "numeric",
192
+ hour: "2-digit",
193
+ minute: "2-digit",
194
+ });
195
+ }
196
+
197
+
198
+ const {
199
+ data: getVisitorReq,
200
+ refresh: getVisitorRefresh,
201
+ pending: getVisitorPending,
202
+ } = await useLazyAsyncData(
203
+ "get-all-visitors",
204
+ async () => {
205
+ const params: any = {
206
+ page: 1,
207
+ site: siteId,
208
+ limit: 100,
209
+ search: searchInput.value,
210
+ dateTo: dateTo.value ? moment(dateTo.value).endOf("day").toISOString() : undefined,
211
+ dateFrom: dateFrom.value ? moment(dateFrom.value).startOf("day").toISOString() : undefined,
212
+ type:
213
+ filterTypes.value.length === 0 && activeTab.value === "registered"
214
+ ? "contractor,delivery,walk-in,pick-up,drop-off,guest"
215
+ : filterTypes.value.filter(Boolean).join(","),
216
+ checkedOut: displayNotCheckedOut.value ? false : undefined,
217
+ };
218
+
219
+ if (activeTab.value === "guests") {
220
+ params.type = "guest";
221
+ params.status = "approved";
222
+ } else if (activeTab.value === "resident-transactions") {
223
+ params.status = "registered";
224
+ params.type = "resident,tenant";
225
+ } else if (activeTab.value === "overnight-parking") {
226
+ params.tab = "Overnight Parking";
227
+ } else {
228
+ params.status = activeTab.value;
229
+ }
230
+
231
+ return await getVisitors(params);
232
+ },
233
+ {
234
+ watch: [
235
+ searchInput,
236
+ dateFrom,
237
+ dateTo,
238
+ filterTypes,
239
+ activeTab,
240
+ displayNotCheckedOut,
241
+ ],
242
+ immediate: true,
243
+ }
244
+ );
245
+
246
+ watch(getVisitorReq, (newData: any) => {
247
+ if (newData) {
248
+ items.value = newData.items || []
249
+ }
250
+ })
251
+
252
+
253
+ function normalizeDateOnly(value: string) {
254
+ if (!value) return "";
255
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
256
+
257
+ const parsedDate = new Date(value);
258
+ if (Number.isNaN(parsedDate.getTime())) return "";
259
+
260
+ const year = parsedDate.getFullYear();
261
+ const month = String(parsedDate.getMonth() + 1).padStart(2, "0");
262
+ const day = String(parsedDate.getDate()).padStart(2, "0");
263
+ return `${year}-${month}-${day}`;
264
+ }
265
+
266
+
267
+ function formatDateDDMMYYYY(date: Date | string | null) {
268
+ if (!date) return "";
269
+ const parsedDate = typeof date === "string" ? new Date(date) : date;
270
+ const day = String(parsedDate?.getDate()).padStart(2, '0')
271
+ const month = String(parsedDate?.getMonth() + 1).padStart(2, '0')
272
+ const year = parsedDate?.getFullYear()
273
+
274
+ return `${day}/${month}/${year}`
275
+ }
276
+
277
+
278
+ onMounted(() => {
279
+ activeTab.value = (route.query.tab as string) || "unregistered";
280
+ searchInput.value = (route.query.search as string) || "";
281
+ dateFrom.value = normalizeDateOnly((route.query.dateFrom as string) || "");
282
+ dateTo.value = normalizeDateOnly((route.query.dateTo as string) || "");
283
+ filterTypes.value = ((route.query.type as string)?.split(",") || []).filter(
284
+ Boolean
285
+ ) as TVisitorType[];
286
+ displayNotCheckedOut.value =
287
+ (route.query.checkedOut as string) == "true" || false;
288
+ });
289
+
290
+
291
+ </script>
292
+
293
+ <style scoped>
294
+ * {
295
+ box-sizing: border-box;
296
+ }
297
+
298
+ :deep(.v-card) {
299
+ overflow-x: auto;
300
+ }
301
+
302
+ :deep(.v-data-table-header__content) {
303
+ background-color: #1976d2 !important;
304
+ color: white !important;
305
+ font-weight: bold;
306
+ }
307
+
308
+ :deep(.v-data-table thead tr th) {
309
+ background-color: #1976d2 !important;
310
+ color: white !important;
311
+ border-right: 1px solid rgba(255, 255, 255, 0.2) !important;
312
+ font-size: clamp(11px, 1.2vw, 14px) !important;
313
+ padding: clamp(8px, 1vw, 12px) !important;
314
+ word-break: break-word;
315
+ }
316
+
317
+ :deep(.v-data-table thead tr th:last-child) {
318
+ border-right: none !important;
319
+ }
320
+
321
+ :deep(.v-data-table tbody tr td) {
322
+ border-right: 1px solid #e0e0e0 !important;
323
+ font-size: clamp(10px, 1.1vw, 13px) !important;
324
+ padding: clamp(6px, 0.8vw, 10px) !important;
325
+ word-break: break-word;
326
+ word-wrap: break-word;
327
+ overflow-wrap: break-word;
328
+ }
329
+
330
+ :deep(.v-data-table tbody tr td:last-child) {
331
+ border-right: none !important;
332
+ }
333
+
334
+ :deep(.v-data-table tbody tr:last-child td) {
335
+ border-bottom: 1px solid #e0e0e0 !important;
336
+ }
337
+
338
+ :deep(.v-img) {
339
+ max-width: clamp(60px, 10vw, 100px) !important;
340
+ max-height: clamp(60px, 10vw, 100px) !important;
341
+ }
342
+
343
+ :deep(.text-caption) {
344
+ font-size: clamp(9px, 1vw, 12px) !important;
345
+ }
346
+
347
+ :deep(.text-subtitle-1) {
348
+ font-size: clamp(14px, 2vw, 18px) !important;
349
+ }
350
+
351
+ :deep(.text-h5) {
352
+ font-size: clamp(16px, 2.5vw, 24px) !important;
353
+ }
354
+
355
+ :deep(.v-icon) {
356
+ width: clamp(12px, 1.5vw, 20px) !important;
357
+ height: clamp(12px, 1.5vw, 20px) !important;
358
+ }
359
+
360
+ :deep(.gap-2) {
361
+ gap: clamp(4px, 0.5vw, 8px) !important;
362
+ }
363
+
364
+ /* Mobile optimization */
365
+ @media (max-width: 768px) {
366
+ :deep(.v-data-table) {
367
+ min-width: 100%;
368
+ }
369
+
370
+ :deep(.v-data-table thead tr th) {
371
+ padding: 6px !important;
372
+ font-size: 11px !important;
373
+ }
374
+
375
+ :deep(.v-data-table tbody tr td) {
376
+ padding: 4px !important;
377
+ font-size: 10px !important;
378
+ }
379
+
380
+ :deep(.v-img) {
381
+ max-width: 60px !important;
382
+ max-height: 60px !important;
383
+ }
384
+ }
385
+
386
+ /* Tablet optimization */
387
+ @media (max-width: 1024px) {
388
+ :deep(.v-data-table) {
389
+ font-size: 12px;
390
+ }
391
+
392
+ :deep(.v-data-table thead tr th) {
393
+ padding: 8px !important;
394
+ }
395
+
396
+ :deep(.v-data-table tbody tr td) {
397
+ padding: 6px !important;
398
+ }
399
+ }
400
+ </style>
@@ -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
  }
@@ -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.27",
5
+ "version": "1.11.29",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <v-row no-gutters>
3
+ <VisitorsReportPreview :site-id="siteId" />
4
+ </v-row>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ definePageMeta({
9
+ layout: "plain",
10
+ })
11
+ const route = useRoute();
12
+ const { org: orgId, site: siteId } = route.params as {
13
+ org: string;
14
+ site: string;
15
+ };
16
+
17
+ </script>
18
+
19
+ <style scoped>
20
+
21
+ </style>
@@ -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"