@7365admin1/layer-common 3.1.2 → 3.1.3-staging.39
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/.github/workflows/publish-staging.yml +11 -0
- package/components/AddEqupmentForm.vue +52 -1
- package/components/CameraMain.vue +5 -6
- package/components/DashboardMain.vue +231 -87
- package/components/EquipmentManagementMain.vue +56 -0
- package/components/InvitationMain.vue +4 -3
- package/components/ManageChecklistMain.vue +44 -38
- package/components/Nfc/NFCPatrolReportMain.vue +146 -38
- package/components/Nfc/PatrolReport/DailyReportTab.vue +10 -6
- package/components/Nfc/PatrolReport/MonthlyReportTab.vue +19 -9
- package/components/Nfc/PatrolReport/MonthlyReportTable.vue +69 -0
- package/components/Nfc/PatrolReport/PatrolActivityTable.vue +20 -0
- package/components/Nfc/PatrolReport/PatrolReportTab.vue +5 -3
- package/components/Nfc/PatrolReport/ReportDatePicker.vue +61 -0
- package/components/Nfc/PatrolReport/ReportFilters.vue +52 -20
- package/components/Nfc/PatrolReport/SummaryReportTable.vue +22 -0
- package/components/PhotoUpload.vue +19 -4
- package/components/ServiceProviderMain.vue +59 -25
- package/components/VisitorForm.vue +78 -18
- package/components/VisitorManagement.vue +12 -16
- package/composables/useCleaningSchedulePermission.ts +16 -51
- package/composables/useEquipment.ts +2 -0
- package/composables/useLocalAuth.ts +1 -1
- package/composables/useNFCPatrolDailyReport.ts +3 -4
- package/composables/useNFCPatrolMonthlyReport.ts +62 -30
- package/composables/useNFCPatrolReport.ts +2 -0
- package/composables/useServiceProvider.ts +21 -0
- package/package.json +1 -1
- package/types/customer.d.ts +2 -1
- package/types/equipment.d.ts +5 -1
- package/types/nfc-patrol-report.ts +19 -1
|
@@ -221,7 +221,7 @@ const headers = computed(() => {
|
|
|
221
221
|
title: "Status",
|
|
222
222
|
value: "status",
|
|
223
223
|
},
|
|
224
|
-
|
|
224
|
+
|
|
225
225
|
];
|
|
226
226
|
|
|
227
227
|
if (props.status === "pending" && props.cancelInvitation) {
|
|
@@ -270,10 +270,11 @@ const {
|
|
|
270
270
|
page: page.value,
|
|
271
271
|
status: props.status,
|
|
272
272
|
search: headerSearch.value,
|
|
273
|
-
type: "user-invite,member-invite",
|
|
273
|
+
// type: "user-invite,member-invite",
|
|
274
|
+
type: "member-invite",
|
|
274
275
|
app: props.app,
|
|
275
276
|
orgId: props.org,
|
|
276
|
-
siteId: props.siteId,
|
|
277
|
+
// siteId: props.siteId,
|
|
277
278
|
}),
|
|
278
279
|
{
|
|
279
280
|
watch: [() => props.status]
|
|
@@ -97,7 +97,10 @@
|
|
|
97
97
|
Rejected
|
|
98
98
|
</v-chip>
|
|
99
99
|
</div>
|
|
100
|
-
<div
|
|
100
|
+
<div
|
|
101
|
+
v-if="item.remarks"
|
|
102
|
+
class="text-body-2 text-medium-emphasis"
|
|
103
|
+
>
|
|
101
104
|
{{ item.remarks }}
|
|
102
105
|
</div>
|
|
103
106
|
<div
|
|
@@ -225,11 +228,7 @@
|
|
|
225
228
|
<v-card-text class="pa-4">
|
|
226
229
|
<v-row>
|
|
227
230
|
<v-col cols="12">
|
|
228
|
-
<InputLabel
|
|
229
|
-
title="Remarks (Optional)"
|
|
230
|
-
:required="false"
|
|
231
|
-
class="mb-2"
|
|
232
|
-
/>
|
|
231
|
+
<InputLabel title="Remarks" :required="false" class="mb-2" />
|
|
233
232
|
<v-textarea
|
|
234
233
|
v-model="modalData.remark"
|
|
235
234
|
placeholder="Enter your remarks here"
|
|
@@ -243,7 +242,7 @@
|
|
|
243
242
|
<v-col cols="12">
|
|
244
243
|
<v-divider class="mb-4" />
|
|
245
244
|
<PhotoUpload
|
|
246
|
-
title="Take/Upload
|
|
245
|
+
title="Take/Upload Photos"
|
|
247
246
|
:required="false"
|
|
248
247
|
v-model="modalData.photos"
|
|
249
248
|
v-model:photo-ids="modalData.photoIds"
|
|
@@ -305,31 +304,38 @@
|
|
|
305
304
|
|
|
306
305
|
<v-divider />
|
|
307
306
|
|
|
308
|
-
<v-
|
|
309
|
-
<v-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
class="
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
307
|
+
<v-toolbar class="pa-0" density="compact">
|
|
308
|
+
<v-row no-gutters>
|
|
309
|
+
<v-col cols="6" class="pa-0">
|
|
310
|
+
<v-btn
|
|
311
|
+
block
|
|
312
|
+
variant="text"
|
|
313
|
+
class="text-none"
|
|
314
|
+
size="large"
|
|
315
|
+
height="48"
|
|
316
|
+
:disabled="rejectModal.submitting"
|
|
317
|
+
@click="closeRejectDialog"
|
|
318
|
+
>
|
|
319
|
+
Cancel
|
|
320
|
+
</v-btn>
|
|
321
|
+
</v-col>
|
|
322
|
+
|
|
323
|
+
<v-col cols="6" class="pa-0">
|
|
324
|
+
<v-btn
|
|
325
|
+
block
|
|
326
|
+
variant="flat"
|
|
327
|
+
color="black"
|
|
328
|
+
class="text-none font-weight-bold rounded-0"
|
|
329
|
+
height="48"
|
|
330
|
+
:loading="rejectModal.submitting"
|
|
331
|
+
:disabled="!isRejectModalValid || rejectModal.submitting"
|
|
332
|
+
@click="submitRejectDialog"
|
|
333
|
+
>
|
|
334
|
+
Confirm
|
|
335
|
+
</v-btn>
|
|
336
|
+
</v-col>
|
|
337
|
+
</v-row>
|
|
338
|
+
</v-toolbar>
|
|
333
339
|
</v-card>
|
|
334
340
|
</v-dialog>
|
|
335
341
|
|
|
@@ -435,12 +441,12 @@ const { back } = useUtils();
|
|
|
435
441
|
const route = useRoute();
|
|
436
442
|
const scheduleTitle = computed(() => {
|
|
437
443
|
const path = route.path;
|
|
438
|
-
if (path.includes(
|
|
439
|
-
if (path.includes(
|
|
440
|
-
if (path.includes(
|
|
441
|
-
if (path.includes(
|
|
442
|
-
if (path.includes(
|
|
443
|
-
return
|
|
444
|
+
if (path.includes("cleaning-schedule")) return "Cleaning Checklist";
|
|
445
|
+
if (path.includes("technician-schedule")) return "Technician Checklist";
|
|
446
|
+
if (path.includes("landscape-schedule")) return "Landscape Checklist";
|
|
447
|
+
if (path.includes("pool-schedule")) return "Pool Checklist";
|
|
448
|
+
if (path.includes("pest-schedule")) return "Pest Checklist";
|
|
449
|
+
return "Cleaner Checklist";
|
|
444
450
|
});
|
|
445
451
|
|
|
446
452
|
const canAddRemarks = computed(() => props.canAddRemarks);
|
|
@@ -14,44 +14,63 @@
|
|
|
14
14
|
</v-col>
|
|
15
15
|
</v-row>
|
|
16
16
|
|
|
17
|
-
<v-tabs
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
<v-tabs
|
|
18
|
+
v-model="activeTab"
|
|
19
|
+
class="mb-6"
|
|
20
|
+
bg-color="transparent"
|
|
21
|
+
grow
|
|
22
|
+
>
|
|
23
|
+
<v-tab value="patrol" class="text-none border-thin">
|
|
24
|
+
Patrol Report
|
|
25
|
+
</v-tab>
|
|
26
|
+
|
|
27
|
+
<v-tab value="daily" class="text-none border-thin">
|
|
28
|
+
Daily Report
|
|
29
|
+
</v-tab>
|
|
30
|
+
|
|
31
|
+
<v-tab value="monthly" class="text-none border-thin">
|
|
32
|
+
Monthly Report
|
|
33
|
+
</v-tab>
|
|
21
34
|
</v-tabs>
|
|
22
35
|
|
|
23
36
|
<v-window v-model="activeTab" class="pt-4">
|
|
24
37
|
<v-window-item value="patrol">
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
<div v-if="activeTab === 'patrol'">
|
|
39
|
+
<PatrolReportTab
|
|
40
|
+
:active="true"
|
|
41
|
+
:site="site"
|
|
42
|
+
:filters="filters"
|
|
43
|
+
:options="options"
|
|
44
|
+
@update:filters="updateFilters"
|
|
45
|
+
@export="exportReport"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
33
48
|
</v-window-item>
|
|
34
49
|
|
|
35
50
|
<v-window-item value="daily">
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
<div v-if="activeTab === 'daily'">
|
|
52
|
+
<DailyReportTab
|
|
53
|
+
:active="true"
|
|
54
|
+
:site="site"
|
|
55
|
+
:filters="filters"
|
|
56
|
+
:options="options"
|
|
57
|
+
@update:filters="updateFilters"
|
|
58
|
+
@export="exportReport"
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
44
61
|
</v-window-item>
|
|
45
62
|
|
|
46
63
|
<v-window-item value="monthly">
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
<div v-if="activeTab === 'monthly'">
|
|
65
|
+
<MonthlyReportTab
|
|
66
|
+
:active="true"
|
|
67
|
+
:site="site"
|
|
68
|
+
:filters="filters"
|
|
69
|
+
:options="options"
|
|
70
|
+
@update:filters="updateFilters"
|
|
71
|
+
@export="exportReport"
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
55
74
|
</v-window-item>
|
|
56
75
|
</v-window>
|
|
57
76
|
</v-col>
|
|
@@ -59,15 +78,20 @@
|
|
|
59
78
|
</template>
|
|
60
79
|
|
|
61
80
|
<script setup lang="ts">
|
|
81
|
+
import html2canvas from "html2canvas";
|
|
82
|
+
import jsPDF from "jspdf";
|
|
83
|
+
|
|
62
84
|
import type {
|
|
63
85
|
NFCPatrolReportFilters,
|
|
64
86
|
NFCPatrolReportTab,
|
|
65
87
|
} from "../../types/nfc-patrol-report";
|
|
66
|
-
|
|
88
|
+
|
|
67
89
|
import { useNFCPatrolReportFilters } from "../../composables/useNFCPatrolReportFilters";
|
|
90
|
+
|
|
91
|
+
import PatrolReportTab from "./PatrolReport/PatrolReportTab.vue";
|
|
68
92
|
import DailyReportTab from "./PatrolReport/DailyReportTab.vue";
|
|
69
93
|
import MonthlyReportTab from "./PatrolReport/MonthlyReportTab.vue";
|
|
70
|
-
|
|
94
|
+
|
|
71
95
|
|
|
72
96
|
const props = defineProps<{
|
|
73
97
|
site: string;
|
|
@@ -75,11 +99,16 @@ const props = defineProps<{
|
|
|
75
99
|
}>();
|
|
76
100
|
|
|
77
101
|
const router = useRouter();
|
|
102
|
+
|
|
103
|
+
|
|
78
104
|
const activeTab = ref<NFCPatrolReportTab>("patrol");
|
|
79
|
-
const { filters, options, fetchOptions } = useNFCPatrolReportFilters(props.site);
|
|
80
|
-
const { exportReport: exportActiveReport } = useNFCPatrolReportExport();
|
|
81
105
|
|
|
82
|
-
|
|
106
|
+
const { filters, options, fetchOptions } =
|
|
107
|
+
useNFCPatrolReportFilters(props.site);
|
|
108
|
+
|
|
109
|
+
onMounted(() => {
|
|
110
|
+
fetchOptions();
|
|
111
|
+
});
|
|
83
112
|
|
|
84
113
|
function goBack() {
|
|
85
114
|
router.back();
|
|
@@ -91,11 +120,90 @@ function updateFilters(value: NFCPatrolReportFilters) {
|
|
|
91
120
|
filters.date = value.date;
|
|
92
121
|
}
|
|
93
122
|
|
|
94
|
-
async function exportReport() {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
123
|
+
async function exportReport(type: "pdf" | "csv") {
|
|
124
|
+
const report = document.querySelector(
|
|
125
|
+
".report-export",
|
|
126
|
+
) as HTMLElement | null;
|
|
127
|
+
|
|
128
|
+
if (!report) return;
|
|
129
|
+
|
|
130
|
+
if (type === "pdf") {
|
|
131
|
+
const canvas = await html2canvas(report, {
|
|
132
|
+
scale: 2,
|
|
133
|
+
useCORS: true,
|
|
134
|
+
backgroundColor: "#fff",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const pdf = new jsPDF("p", "mm", "a4");
|
|
138
|
+
|
|
139
|
+
const pdfWidth = pdf.internal.pageSize.getWidth();
|
|
140
|
+
const pdfHeight = pdf.internal.pageSize.getHeight();
|
|
141
|
+
|
|
142
|
+
const imgWidth = pdfWidth;
|
|
143
|
+
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
|
144
|
+
|
|
145
|
+
const imgData = canvas.toDataURL("image/png");
|
|
146
|
+
|
|
147
|
+
let heightLeft = imgHeight;
|
|
148
|
+
let position = 0;
|
|
149
|
+
|
|
150
|
+
pdf.addImage(imgData, "PNG", 0, position, imgWidth, imgHeight);
|
|
151
|
+
|
|
152
|
+
heightLeft -= pdfHeight;
|
|
153
|
+
|
|
154
|
+
while (heightLeft > 0) {
|
|
155
|
+
position -= pdfHeight;
|
|
156
|
+
|
|
157
|
+
pdf.addPage();
|
|
158
|
+
pdf.addImage(imgData, "PNG", 0, position, imgWidth, imgHeight);
|
|
159
|
+
|
|
160
|
+
heightLeft -= pdfHeight;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
pdf.save(`${activeTab.value}-report.pdf`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const table = report.querySelector("table");
|
|
168
|
+
|
|
169
|
+
if (!table) return;
|
|
170
|
+
|
|
171
|
+
const csv = Array.from(table.querySelectorAll("tr"))
|
|
172
|
+
.map((row) =>
|
|
173
|
+
Array.from(row.querySelectorAll("th,td"))
|
|
174
|
+
.map((cell) => escapeCsvCell(getCsvCellValue(cell)))
|
|
175
|
+
.join(","),
|
|
176
|
+
)
|
|
177
|
+
.join("\n");
|
|
178
|
+
|
|
179
|
+
const blob = new Blob([csv], {
|
|
180
|
+
type: "text/csv;charset=utf-8;",
|
|
99
181
|
});
|
|
182
|
+
|
|
183
|
+
const link = document.createElement("a");
|
|
184
|
+
|
|
185
|
+
link.href = URL.createObjectURL(blob);
|
|
186
|
+
link.download = `${activeTab.value}-report.csv`;
|
|
187
|
+
link.click();
|
|
188
|
+
|
|
189
|
+
URL.revokeObjectURL(link.href);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getCsvCellValue(cell: Element) {
|
|
193
|
+
const image = cell.querySelector("img");
|
|
194
|
+
|
|
195
|
+
if (image) {
|
|
196
|
+
const imageUrl = image.currentSrc || image.src;
|
|
197
|
+
|
|
198
|
+
if (imageUrl) {
|
|
199
|
+
return imageUrl;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return cell.textContent?.trim() ?? "";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function escapeCsvCell(value: string) {
|
|
207
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
100
208
|
}
|
|
101
209
|
</script>
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
<ReportFilters
|
|
4
4
|
:model-value="filters"
|
|
5
5
|
:options="options"
|
|
6
|
+
:show-time-filter="false"
|
|
6
7
|
@update:model-value="$emit('update:filters', $event)"
|
|
7
|
-
@export="$emit('export')"
|
|
8
|
+
@export="(type) => $emit('export', type)"
|
|
8
9
|
/>
|
|
9
|
-
|
|
10
|
+
<div class="report-export">
|
|
10
11
|
<v-divider :thickness="1" class="border-opacity-100" />
|
|
11
12
|
|
|
12
13
|
<template v-if="report">
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
<SummaryReportTable :title="report.title" :rows="report.rows" />
|
|
16
17
|
</template>
|
|
17
18
|
|
|
19
|
+
|
|
18
20
|
<ReportEmptyState
|
|
19
21
|
v-else-if="!loading && notFound"
|
|
20
22
|
title="No Daily Report"
|
|
@@ -22,6 +24,7 @@
|
|
|
22
24
|
/>
|
|
23
25
|
|
|
24
26
|
<ReportEmptyState v-else-if="!loading" />
|
|
27
|
+
</div>
|
|
25
28
|
</div>
|
|
26
29
|
</template>
|
|
27
30
|
|
|
@@ -38,23 +41,23 @@ import SummaryReportTable from "./SummaryReportTable.vue";
|
|
|
38
41
|
|
|
39
42
|
const props = defineProps<{
|
|
40
43
|
active: boolean;
|
|
41
|
-
site: string;
|
|
44
|
+
site: string;
|
|
42
45
|
filters: NFCPatrolReportFilters;
|
|
43
46
|
options: NFCPatrolReportFilterOptions;
|
|
44
47
|
}>();
|
|
45
48
|
|
|
46
49
|
defineEmits<{
|
|
47
50
|
"update:filters": [value: NFCPatrolReportFilters];
|
|
48
|
-
export: [];
|
|
51
|
+
export: [type: "pdf" | "csv"];
|
|
49
52
|
}>();
|
|
50
53
|
|
|
51
54
|
const { report, loading, notFound, fetchReport } = useNFCPatrolDailyReport(
|
|
52
55
|
props.filters,
|
|
53
|
-
props.site,
|
|
56
|
+
props.site,
|
|
54
57
|
);
|
|
55
58
|
|
|
56
59
|
watch(
|
|
57
|
-
() => [props.filters.route, props.filters.
|
|
60
|
+
() => [props.filters.route, props.filters.date, props.active],
|
|
58
61
|
() => {
|
|
59
62
|
if (props.active) {
|
|
60
63
|
void fetchReport();
|
|
@@ -62,4 +65,5 @@ watch(
|
|
|
62
65
|
},
|
|
63
66
|
{ immediate: true },
|
|
64
67
|
);
|
|
68
|
+
|
|
65
69
|
</script>
|
|
@@ -3,23 +3,30 @@
|
|
|
3
3
|
<ReportFilters
|
|
4
4
|
:model-value="filters"
|
|
5
5
|
:options="options"
|
|
6
|
+
:show-time-filter="false"
|
|
7
|
+
:show-date-filter="false"
|
|
6
8
|
@update:model-value="$emit('update:filters', $event)"
|
|
7
|
-
@export="$emit('export')"
|
|
9
|
+
@export="(type) => $emit('export', type)"
|
|
8
10
|
/>
|
|
9
|
-
|
|
11
|
+
<div class="report-export">
|
|
10
12
|
<v-divider :thickness="1" class="border-opacity-100" />
|
|
11
13
|
|
|
12
14
|
<template v-if="report">
|
|
13
15
|
<ReportLocationHeader :location="report.location" />
|
|
14
16
|
<v-divider :thickness="1" class="border-opacity-100 mb-6" />
|
|
15
|
-
|
|
17
|
+
|
|
18
|
+
<MonthlyReportTable
|
|
19
|
+
:title="report.title"
|
|
20
|
+
:rows="report.rows"
|
|
21
|
+
/>
|
|
16
22
|
</template>
|
|
17
23
|
|
|
18
24
|
<ReportEmptyState
|
|
19
25
|
v-else-if="!loading"
|
|
20
|
-
title="No Monthly Report
|
|
21
|
-
message="
|
|
26
|
+
title="No Monthly Report"
|
|
27
|
+
message="No patrol log found for this route"
|
|
22
28
|
/>
|
|
29
|
+
</div>
|
|
23
30
|
</div>
|
|
24
31
|
</template>
|
|
25
32
|
|
|
@@ -28,11 +35,13 @@ import type {
|
|
|
28
35
|
NFCPatrolReportFilterOptions,
|
|
29
36
|
NFCPatrolReportFilters,
|
|
30
37
|
} from "../../../types/nfc-patrol-report";
|
|
38
|
+
|
|
31
39
|
import { useNFCPatrolMonthlyReport } from "../../../composables/useNFCPatrolMonthlyReport";
|
|
40
|
+
|
|
32
41
|
import ReportEmptyState from "./ReportEmptyState.vue";
|
|
33
42
|
import ReportFilters from "./ReportFilters.vue";
|
|
34
43
|
import ReportLocationHeader from "./ReportLocationHeader.vue";
|
|
35
|
-
import
|
|
44
|
+
import MonthlyReportTable from "./MonthlyReportTable.vue";
|
|
36
45
|
|
|
37
46
|
const props = defineProps<{
|
|
38
47
|
active: boolean;
|
|
@@ -43,14 +52,14 @@ const props = defineProps<{
|
|
|
43
52
|
|
|
44
53
|
defineEmits<{
|
|
45
54
|
"update:filters": [value: NFCPatrolReportFilters];
|
|
46
|
-
export: [];
|
|
55
|
+
export: [type: "pdf" | "csv"];
|
|
47
56
|
}>();
|
|
48
57
|
|
|
49
|
-
const { report, loading, fetchReport } =
|
|
58
|
+
const { report, loading, notFound, fetchReport } =
|
|
50
59
|
useNFCPatrolMonthlyReport(props.filters, props.site);
|
|
51
60
|
|
|
52
61
|
watch(
|
|
53
|
-
() => [props.filters.route, props.
|
|
62
|
+
() => [props.filters.route, props.active],
|
|
54
63
|
() => {
|
|
55
64
|
if (props.active) {
|
|
56
65
|
void fetchReport();
|
|
@@ -58,4 +67,5 @@ watch(
|
|
|
58
67
|
},
|
|
59
68
|
{ immediate: true },
|
|
60
69
|
);
|
|
70
|
+
|
|
61
71
|
</script>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mt-6">
|
|
3
|
+
<div class="text-h6 font-weight-bold mb-4">
|
|
4
|
+
{{ title }}
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<v-table class="border rounded-lg">
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th class="text-left font-weight-bold">Month</th>
|
|
11
|
+
<th class="text-center font-weight-bold">Checked</th>
|
|
12
|
+
<th class="text-center font-weight-bold">% Checked</th>
|
|
13
|
+
<th class="text-center font-weight-bold">Missed</th>
|
|
14
|
+
<th class="text-center font-weight-bold">% Missed</th>
|
|
15
|
+
</tr>
|
|
16
|
+
</thead>
|
|
17
|
+
|
|
18
|
+
<tbody>
|
|
19
|
+
<tr
|
|
20
|
+
v-for="row in rows"
|
|
21
|
+
:key="row.month"
|
|
22
|
+
>
|
|
23
|
+
<td>{{ row.month }}</td>
|
|
24
|
+
|
|
25
|
+
<td class="text-center">
|
|
26
|
+
{{ row.checked }}
|
|
27
|
+
</td>
|
|
28
|
+
|
|
29
|
+
<td class="text-center">
|
|
30
|
+
{{ row.checkedPercentage }}%
|
|
31
|
+
</td>
|
|
32
|
+
|
|
33
|
+
<td class="text-center">
|
|
34
|
+
{{ row.missed }}
|
|
35
|
+
</td>
|
|
36
|
+
|
|
37
|
+
<td class="text-center">
|
|
38
|
+
{{ row.missedPercentage }}%
|
|
39
|
+
</td>
|
|
40
|
+
</tr>
|
|
41
|
+
</tbody>
|
|
42
|
+
</v-table>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script setup lang="ts">
|
|
47
|
+
import type { NFCPatrolMonthlyReportRow } from "../../../types/nfc-patrol-report";
|
|
48
|
+
|
|
49
|
+
defineProps<{
|
|
50
|
+
title: string;
|
|
51
|
+
rows: NFCPatrolMonthlyReportRow[];
|
|
52
|
+
}>();
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<style scoped>
|
|
56
|
+
.v-table {
|
|
57
|
+
border: 1px solid rgb(var(--v-theme-outline));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
thead th {
|
|
61
|
+
background: transparent;
|
|
62
|
+
white-space: nowrap;
|
|
63
|
+
font-weight: 600;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
tbody td {
|
|
67
|
+
height: 56px;
|
|
68
|
+
}
|
|
69
|
+
</style>
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
<thead>
|
|
8
8
|
<tr>
|
|
9
9
|
<th>Checkpoints</th>
|
|
10
|
+
<th>Security Personnel</th>
|
|
11
|
+
<th>Signature</th>
|
|
10
12
|
<th>Start - End Time</th>
|
|
11
13
|
<th>
|
|
12
14
|
Travel time<br />
|
|
@@ -23,6 +25,17 @@
|
|
|
23
25
|
@click="handleActivityClick(activity)"
|
|
24
26
|
>
|
|
25
27
|
<td>{{ activity.checkpoint }}</td>
|
|
28
|
+
<td>{{ activity.guardName || "-" }}</td>
|
|
29
|
+
<td>
|
|
30
|
+
<v-img
|
|
31
|
+
v-if="activity.guardSignatureFileId"
|
|
32
|
+
:src="getFileUrl(activity.guardSignatureFileId)"
|
|
33
|
+
:alt="`${activity.guardName || 'Guard'} signature`"
|
|
34
|
+
class="guard-signature"
|
|
35
|
+
contain
|
|
36
|
+
/>
|
|
37
|
+
<span v-else>-</span>
|
|
38
|
+
</td>
|
|
26
39
|
<td>
|
|
27
40
|
<v-icon size="small" class="mr-1">mdi-clock-outline</v-icon>
|
|
28
41
|
{{ activity.startTime }}
|
|
@@ -59,6 +72,8 @@ defineProps<{
|
|
|
59
72
|
activities: NFCPatrolActivity[];
|
|
60
73
|
}>();
|
|
61
74
|
|
|
75
|
+
const { getFileUrl } = useFile();
|
|
76
|
+
|
|
62
77
|
const emit = defineEmits<{
|
|
63
78
|
remarks: [activity: NFCPatrolActivity];
|
|
64
79
|
}>();
|
|
@@ -78,4 +93,9 @@ function handleActivityClick(activity: NFCPatrolActivity) {
|
|
|
78
93
|
.cursor-pointer {
|
|
79
94
|
cursor: pointer;
|
|
80
95
|
}
|
|
96
|
+
|
|
97
|
+
.guard-signature {
|
|
98
|
+
width: 80px;
|
|
99
|
+
height: 40px;
|
|
100
|
+
}
|
|
81
101
|
</style>
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
:model-value="filters"
|
|
5
5
|
:options="options"
|
|
6
6
|
@update:model-value="$emit('update:filters', $event)"
|
|
7
|
-
@export="$emit('export')"
|
|
7
|
+
@export="(type) => $emit('export', type)"
|
|
8
8
|
/>
|
|
9
|
-
|
|
9
|
+
<div class="report-export">
|
|
10
10
|
<v-divider :thickness="1" class="border-opacity-100" />
|
|
11
11
|
|
|
12
12
|
<template v-if="report">
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
<ReportEmptyState v-else-if="!loading" />
|
|
31
31
|
|
|
32
32
|
<RemarksDialog v-model="dialogRemarks" :remarks="selectedActivityRemarks" />
|
|
33
|
+
</div>
|
|
33
34
|
</div>
|
|
34
35
|
</template>
|
|
35
36
|
|
|
@@ -55,7 +56,7 @@ const props = defineProps<{
|
|
|
55
56
|
|
|
56
57
|
defineEmits<{
|
|
57
58
|
"update:filters": [value: NFCPatrolReportFilters];
|
|
58
|
-
export: [];
|
|
59
|
+
export: [type: "pdf" | "csv"];
|
|
59
60
|
}>();
|
|
60
61
|
|
|
61
62
|
const {
|
|
@@ -77,4 +78,5 @@ watch(
|
|
|
77
78
|
},
|
|
78
79
|
{ immediate: true },
|
|
79
80
|
);
|
|
81
|
+
|
|
80
82
|
</script>
|