@7365admin1/layer-common 1.11.28 → 1.11.30

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.30
4
+
5
+ ### Patch Changes
6
+
7
+ - 26fbfd1: Fix issue with double attrib
8
+
9
+ ## 1.11.29
10
+
11
+ ### Patch Changes
12
+
13
+ - 1fa1e46: Update layer-common
14
+
3
15
  ## 1.11.28
4
16
 
5
17
  ### Patch Changes
@@ -9,16 +9,58 @@
9
9
  :extension-height="110"
10
10
  :offset="300"
11
11
  :pageRange="pageRange"
12
- :canCreate="canAddVisitor"
13
- :canScanVisitorQRCode="canScanVisitorQRCode"
14
12
  @refresh="getVisitorRefresh"
15
13
  @update:page="handleUpdatePage"
16
- createLabel="Add Visitor"
17
14
  show-header
18
15
  @row-click="handleRowClick"
19
- @create="handleAddNew"
20
- @scan="dialog.scanVisitorQRCode = true"
21
16
  >
17
+ <template #actions>
18
+ <v-row no-gutters>
19
+ <v-btn
20
+ v-if="canAddVisitor"
21
+ class="text-none"
22
+ rounded="pill"
23
+ variant="tonal"
24
+ size="large"
25
+ @click="handleAddNew"
26
+ text="Add Visitor"
27
+ />
28
+ <v-btn
29
+ v-if="canScanVisitorQRCode"
30
+ text="Scan QR Code"
31
+ class="text-none ml-2"
32
+ rounded="pill"
33
+ variant="tonal"
34
+ size="large"
35
+ @click="dialog.scanVisitorQRCode = true"
36
+ />
37
+ <v-menu>
38
+ <template #activator="{ props }">
39
+ <v-btn
40
+ v-bind="props"
41
+ text="Download Report"
42
+ class="text-none ml-2"
43
+ prepend-icon="mdi-download"
44
+ rounded="pill"
45
+ variant="tonal"
46
+ size="large"
47
+ />
48
+ </template>
49
+ <v-list>
50
+ <v-list-item
51
+ prepend-icon="mdi-file-pdf"
52
+ title="Download PDF Report"
53
+ @click="handleDownloadPDFReport"
54
+ />
55
+ <v-list-item
56
+ prepend-icon="mdi-file-csv"
57
+ title="Download CSV Report"
58
+ @click="handleDownloadCSVReport"
59
+ />
60
+ </v-list>
61
+ </v-menu>
62
+ </v-row>
63
+ </template>
22
64
  <template #extension>
23
65
  <v-row no-gutters class="w-100 d-flex flex-column">
24
66
  <v-tabs
@@ -167,21 +209,34 @@
167
209
  <v-row no-gutters class="d-flex flex-column py-2">
168
210
  <span class="d-flex align-center ga-2">
169
211
  <v-icon
170
- icon="mdi-clock-time-four-outline"
171
- color="green"
212
+ icon="mdi-clock-time-eight-outline"
213
+ color="success"
172
214
  size="20"
173
215
  />
174
- <span class="text-capitalize">{{
175
- UTCToLocalTIme(item.checkIn) || "-"
176
- }}</span>
177
- <span>
178
- <v-icon
179
- v-if="item?.snapshotEntryImage"
180
- size="17"
181
- icon="mdi-image"
182
- @click.stop="handleViewImage(item.snapshotEntryImage)"
216
+ <template v-if="!item.checkIn && canUpdateVisitor">
217
+ <v-btn
218
+ size="x-small"
219
+ class="text-capitalize"
220
+ color="success"
221
+ text="Checkin"
222
+ :disabled="isVisitorDataFromScannedQRCodeCheckingInOut"
223
+ @click.stop="handleCheckin(item)"
224
+ :loading="isVisitorDataFromScannedQRCodeCheckingInOut"
183
225
  />
184
- </span>
226
+ </template>
227
+ <template v-else>
228
+ <span class="text-capitalize">{{
229
+ UTCToLocalTIme(item.checkIn)
230
+ }}</span>
231
+ <span>
232
+ <v-icon
233
+ v-if="item?.snapshotEntryImage"
234
+ size="17"
235
+ icon="mdi-image"
236
+ @click.stop="handleViewImage(item.snapshotEntryImage)"
237
+ />
238
+ </span>
239
+ </template>
185
240
  </span>
186
241
 
187
242
  <span
@@ -218,7 +273,7 @@
218
273
  <v-icon icon="mdi-clock-time-eight-outline" color="red" size="20" />
219
274
  <template v-if="item.checkOut">
220
275
  <span class="text-capitalize">{{
221
- UTCToLocalTIme(item.checkOut) || "-"
276
+ UTCToLocalTIme(item.checkOut)
222
277
  }}</span>
223
278
  <span>
224
279
  <v-icon
@@ -236,6 +291,7 @@
236
291
  />
237
292
  </span>
238
293
  </template>
294
+ <template v-else> <span class="mt-1"> N/A </span> </template>
239
295
  </span>
240
296
 
241
297
  <div
@@ -323,23 +379,6 @@
323
379
  <v-icon icon="mdi-clock-time-four-outline" size="20" />
324
380
  {{ item.arrivalTime ?? "N/A" }}
325
381
  </span>
326
-
327
- <span v-if="!item.checkIn && canUpdateVisitor">
328
- <span class="d-flex align-center ga-2 cursor-pointer">
329
- <v-icon
330
- icon="mdi-clock-time-eight-outline"
331
- color="success"
332
- size="20"
333
- />
334
- <v-btn
335
- size="x-small"
336
- class="text-capitalize"
337
- color="success"
338
- text="Checkin"
339
- @click.stop="handleCheckin(item)"
340
- />
341
- </span>
342
- </span>
343
382
  </v-row>
344
383
  </template>
345
384
 
@@ -863,7 +902,9 @@
863
902
  class="text-h6 text-center"
864
903
  >
865
904
  {{
866
- `Are you sure you want to ${approveRejectOvernightParkingRequestDialog.text} the visitor's overnight parking request?`
905
+ `Are you sure you want to ${approveRejectOvernightParkingRequestDialog.text} the visitor's overnight
906
+ parking
907
+ request?`
867
908
  }}
868
909
  </v-col>
869
910
 
@@ -937,6 +978,8 @@ definePageMeta({
937
978
  middleware: ["01-auth", "02-org"],
938
979
  });
939
980
 
981
+ const { downloadPDF } = usePDFDownload();
982
+
940
983
  const props = defineProps({
941
984
  canAddVisitor: {
942
985
  type: Boolean,
@@ -999,8 +1042,14 @@ const selectedVisitorId = ref<string | null>(""); // selected visitor for viewin
999
1042
 
1000
1043
  //filter states
1001
1044
  const searchInput = ref("");
1002
- const dateFrom = ref("");
1003
- const dateTo = ref("");
1045
+
1046
+ const today = new Date();
1047
+ const oneMonthAgo = new Date();
1048
+ oneMonthAgo.setMonth(today.getMonth() - 1);
1049
+
1050
+ const dateTo = ref(normalizeDateOnly(today.toISOString()));
1051
+ const dateFrom = ref(normalizeDateOnly(oneMonthAgo.toISOString()));
1052
+
1004
1053
  const filterTypes = ref<TVisitorType[]>([]);
1005
1054
  const displayNotCheckedOut = ref<boolean>(false);
1006
1055
  const mode = ref<"add" | "edit" | "register">("add");
@@ -1253,8 +1302,12 @@ const {
1253
1302
  page: page.value,
1254
1303
  site: siteId,
1255
1304
  search: searchInput.value,
1256
- dateTo: moment(dateTo.value).endOf("day").toISOString(),
1257
- dateFrom: moment(dateFrom.value).startOf("day").toISOString(),
1305
+ dateTo: dateTo.value
1306
+ ? moment(dateTo.value).endOf("day").toISOString()
1307
+ : undefined,
1308
+ dateFrom: dateFrom.value
1309
+ ? moment(dateFrom.value).startOf("day").toISOString()
1310
+ : undefined,
1258
1311
  type:
1259
1312
  filterTypes.value.length === 0 && activeTab.value === "registered"
1260
1313
  ? "contractor,delivery,walk-in,pick-up,drop-off,guest"
@@ -1678,6 +1731,158 @@ async function proceedCheckout() {
1678
1731
  }
1679
1732
  }
1680
1733
 
1734
+ function buildReportParams(): any {
1735
+ const params: any = {
1736
+ site: siteId,
1737
+ search: searchInput.value,
1738
+ dateTo: dateTo.value
1739
+ ? moment(dateTo.value).endOf("day").toISOString()
1740
+ : undefined,
1741
+ dateFrom: dateFrom.value
1742
+ ? moment(dateFrom.value).startOf("day").toISOString()
1743
+ : undefined,
1744
+ type:
1745
+ filterTypes.value.length === 0 && activeTab.value === "registered"
1746
+ ? "contractor,delivery,walk-in,pick-up,drop-off,guest"
1747
+ : filterTypes.value.filter(Boolean).join(","),
1748
+ checkedOut: displayNotCheckedOut.value ? false : undefined,
1749
+ limit: 100,
1750
+ };
1751
+
1752
+ if (activeTab.value === "guests") {
1753
+ params.type = "guest";
1754
+ params.status = "approved";
1755
+ } else if (activeTab.value === "resident-transactions") {
1756
+ params.status = "registered";
1757
+ params.type = "resident,tenant";
1758
+ } else if (activeTab.value === "overnight-parking") {
1759
+ params.tab = "Overnight Parking";
1760
+ } else {
1761
+ params.status = activeTab.value;
1762
+ }
1763
+
1764
+ return params;
1765
+ }
1766
+
1767
+ async function handleDownloadPDFReport() {
1768
+ try {
1769
+ showMessage("Generating PDF report...", "info");
1770
+
1771
+ // Build URL with query parameters
1772
+ const queryParams = new URLSearchParams();
1773
+ if (searchInput.value) queryParams.append("search", searchInput.value);
1774
+ if (dateFrom.value) queryParams.append("dateFrom", dateFrom.value);
1775
+ if (dateTo.value) queryParams.append("dateTo", dateTo.value);
1776
+ if (filterTypes.value.length > 0)
1777
+ queryParams.append("type", filterTypes.value.join(","));
1778
+ if (activeTab.value) queryParams.append("tab", activeTab.value);
1779
+ if (displayNotCheckedOut.value) queryParams.append("checkedOut", "true");
1780
+
1781
+ const queryString = queryParams.toString();
1782
+ const url = `/${route.params.org}/${siteId}/visitor-management/preview${
1783
+ queryString ? "?" + queryString : ""
1784
+ }`;
1785
+ const title = `Visitor-Report-${siteId}-${
1786
+ new Date().toISOString().split("T")[0]
1787
+ }`;
1788
+
1789
+ await downloadPDF({ url, title, orientation: "P", appKey: "SECURITY" });
1790
+ showMessage("PDF report downloaded successfully!", "success");
1791
+ } catch (error) {
1792
+ showMessage("Error downloading PDF report", "error");
1793
+ }
1794
+ }
1795
+
1796
+ async function handleDownloadCSVReport() {
1797
+ try {
1798
+ showMessage("Generating CSV report...", "info");
1799
+
1800
+ const params = buildReportParams();
1801
+ const reportData = await getVisitors(params);
1802
+ const csvData = generateCSVData(reportData.items ?? []);
1803
+ downloadCSVFile(csvData);
1804
+ showMessage("CSV report downloaded successfully!", "success");
1805
+ } catch (error) {
1806
+ showMessage("Error downloading CSV report", "error");
1807
+ }
1808
+ }
1809
+
1810
+ function generateCSVData(visitors: any[]): string {
1811
+ const headers = [
1812
+ "NAME",
1813
+ "TYPE",
1814
+ "COMPANY",
1815
+ "BLOCK",
1816
+ "LEVEL",
1817
+ "UNIT",
1818
+ "CONTACT",
1819
+ "VEHICLE NO.",
1820
+ "CHECK-IN",
1821
+ "CHECK-OUT",
1822
+ "CHECK-IN IMAGE",
1823
+ "CHECK-OUT IMAGE",
1824
+ ];
1825
+
1826
+ const formatExportTypes = (type: string) => {
1827
+ if (!type || type === "null") return "";
1828
+ if (type === "guest") return "Drive In";
1829
+ const typeParts = type.split("-");
1830
+ return typeParts.map((part) => formatCamelCaseToWords(part)).join("-");
1831
+ };
1832
+
1833
+ const rows = visitors.map((visitor) => [
1834
+ visitor.name || "",
1835
+ formatExportTypes(formatType(visitor)) || "",
1836
+ visitor.company || "",
1837
+ visitor.block || "",
1838
+ visitor.level || "",
1839
+ visitor.unitName || "",
1840
+ visitor.contact || "",
1841
+ visitor.plateNumber || "",
1842
+ formatDate(visitor.checkIn) || "",
1843
+ formatDate(visitor.checkOut) || "",
1844
+ visitor.snapshotEntryImage
1845
+ ? getFileUrlAnpr(`${siteId}/${visitor.snapshotEntryImage}`)
1846
+ : "",
1847
+ visitor.snapshotExitImage
1848
+ ? getFileUrlAnpr(`${siteId}/${visitor.snapshotExitImage}`)
1849
+ : "",
1850
+ ]);
1851
+
1852
+ const csvContent = [
1853
+ headers.map((h) => `"${h}"`).join(","),
1854
+ ...rows.map((row) =>
1855
+ row
1856
+ .map((cell) => {
1857
+ const cellStr = String(cell || "");
1858
+ return `"${cellStr.replace(/"/g, '""')}"`;
1859
+ })
1860
+ .join(",")
1861
+ ),
1862
+ ].join("\n");
1863
+
1864
+ return csvContent;
1865
+ }
1866
+
1867
+ function downloadCSVFile(csvContent: string) {
1868
+ const now = new Date();
1869
+ const dateStr = now.toISOString().split("T")[0];
1870
+ const timeStr = now.toTimeString().split(" ")[0].replace(/:/g, "_");
1871
+ const filename = `VISITOR_MANAGEMENT_CSV_REPORT_${activeTab.value}_${dateStr}_${timeStr}.csv`;
1872
+
1873
+ const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
1874
+ const link = document.createElement("a");
1875
+ const url = URL.createObjectURL(blob);
1876
+
1877
+ link.setAttribute("href", url);
1878
+ link.setAttribute("download", filename);
1879
+ link.style.visibility = "hidden";
1880
+
1881
+ document.body.appendChild(link);
1882
+ link.click();
1883
+ document.body.removeChild(link);
1884
+ }
1885
+
1681
1886
  const updateRouteQuery = debounce(
1682
1887
  // (search: string, from: string, to: string, types: string[], status, checkedOut) => {
1683
1888
  ({
@@ -1740,8 +1945,12 @@ watch([searchInput, dateFrom, dateTo, filterTypes, activeTab], () => {
1740
1945
  onMounted(() => {
1741
1946
  activeTab.value = (route.query.tab as string) || "unregistered";
1742
1947
  searchInput.value = (route.query.search as string) || "";
1743
- dateFrom.value = normalizeDateOnly((route.query.dateFrom as string) || "");
1744
- dateTo.value = normalizeDateOnly((route.query.dateTo as string) || "");
1948
+ if (route.query.dateFrom) {
1949
+ dateFrom.value = normalizeDateOnly(route.query.dateFrom as string);
1950
+ }
1951
+ if (route.query.dateTo) {
1952
+ dateTo.value = normalizeDateOnly(route.query.dateTo as string);
1953
+ }
1745
1954
  filterTypes.value = ((route.query.type as string)?.split(",") || []).filter(
1746
1955
  Boolean
1747
1956
  ) as TVisitorType[];
@@ -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
+ 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>
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.28",
5
+ "version": "1.11.30",
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>