@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
|
@@ -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-
|
|
171
|
-
color="
|
|
212
|
+
icon="mdi-clock-time-eight-outline"
|
|
213
|
+
color="success"
|
|
172
214
|
size="20"
|
|
173
215
|
/>
|
|
174
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
</
|
|
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
|
|
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
|
-
|
|
1003
|
-
const
|
|
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:
|
|
1257
|
-
|
|
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
|
-
|
|
1744
|
-
|
|
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
|
@@ -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>
|