@7365admin1/layer-common 3.0.31-staging.15 → 3.0.31-staging.17
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/components/Nfc/NFCPatrolReportMain.vue +128 -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/PatrolReportTab.vue +5 -3
- package/components/Nfc/PatrolReport/ReportFilters.vue +50 -18
- package/components/ServiceProviderMain.vue +46 -29
- package/composables/useNFCPatrolDailyReport.ts +1 -4
- package/composables/useNFCPatrolMonthlyReport.ts +62 -30
- package/package.json +1 -1
- package/types/nfc-patrol-report.ts +15 -1
|
@@ -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,72 @@ 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) => `"${cell.textContent?.trim().replace(/"/g, '""') ?? ""}"`)
|
|
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);
|
|
100
190
|
}
|
|
101
191
|
</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>
|
|
@@ -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>
|
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
/>
|
|
17
17
|
</v-col>
|
|
18
18
|
|
|
19
|
-
<v-col
|
|
19
|
+
<v-col
|
|
20
|
+
v-if="showTimeFilter"
|
|
21
|
+
cols="12"
|
|
22
|
+
md="2"
|
|
23
|
+
class="px-md-2 mb-2 mb-md-0"
|
|
24
|
+
>
|
|
20
25
|
<v-select
|
|
21
26
|
:model-value="modelValue.timeRange"
|
|
22
27
|
:items="options.timeRanges"
|
|
@@ -30,7 +35,12 @@
|
|
|
30
35
|
/>
|
|
31
36
|
</v-col>
|
|
32
37
|
|
|
33
|
-
<v-col
|
|
38
|
+
<v-col
|
|
39
|
+
v-if="showDateFilter"
|
|
40
|
+
cols="12"
|
|
41
|
+
md="2"
|
|
42
|
+
class="px-md-2 mb-2 mb-md-0"
|
|
43
|
+
>
|
|
34
44
|
<v-text-field
|
|
35
45
|
:model-value="modelValue.date"
|
|
36
46
|
type="date"
|
|
@@ -46,17 +56,31 @@
|
|
|
46
56
|
<v-spacer />
|
|
47
57
|
|
|
48
58
|
<v-col cols="12" md="2" class="pl-md-2">
|
|
49
|
-
<v-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
<v-menu>
|
|
60
|
+
<template #activator="{ props: menuProps }">
|
|
61
|
+
<v-btn
|
|
62
|
+
v-bind="menuProps"
|
|
63
|
+
variant="outlined"
|
|
64
|
+
block
|
|
65
|
+
size="large"
|
|
66
|
+
rounded="lg"
|
|
67
|
+
class="text-none"
|
|
68
|
+
prepend-icon="mdi-export"
|
|
69
|
+
>
|
|
70
|
+
Export
|
|
71
|
+
</v-btn>
|
|
72
|
+
</template>
|
|
73
|
+
|
|
74
|
+
<v-list density="compact">
|
|
75
|
+
<v-list-item @click="$emit('export', 'pdf')">
|
|
76
|
+
<v-list-item-title>PDF</v-list-item-title>
|
|
77
|
+
</v-list-item>
|
|
78
|
+
|
|
79
|
+
<v-list-item @click="$emit('export', 'csv')">
|
|
80
|
+
<v-list-item-title>CSV</v-list-item-title>
|
|
81
|
+
</v-list-item>
|
|
82
|
+
</v-list>
|
|
83
|
+
</v-menu>
|
|
60
84
|
</v-col>
|
|
61
85
|
</v-row>
|
|
62
86
|
</template>
|
|
@@ -67,14 +91,22 @@ import type {
|
|
|
67
91
|
NFCPatrolReportFilters,
|
|
68
92
|
} from "../../../types/nfc-patrol-report";
|
|
69
93
|
|
|
70
|
-
const props =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
94
|
+
const props = withDefaults(
|
|
95
|
+
defineProps<{
|
|
96
|
+
modelValue: NFCPatrolReportFilters;
|
|
97
|
+
options: NFCPatrolReportFilterOptions;
|
|
98
|
+
showTimeFilter?: boolean;
|
|
99
|
+
showDateFilter?: boolean;
|
|
100
|
+
}>(),
|
|
101
|
+
{
|
|
102
|
+
showTimeFilter: true,
|
|
103
|
+
showDateFilter: true,
|
|
104
|
+
},
|
|
105
|
+
);;
|
|
74
106
|
|
|
75
107
|
const emit = defineEmits<{
|
|
76
108
|
"update:modelValue": [value: NFCPatrolReportFilters];
|
|
77
|
-
export: [];
|
|
109
|
+
export: [type: "pdf" | "csv"];
|
|
78
110
|
}>();
|
|
79
111
|
|
|
80
112
|
function updateFilter(key: keyof NFCPatrolReportFilters, value: unknown) {
|
|
@@ -548,7 +548,7 @@
|
|
|
548
548
|
variant="flat"
|
|
549
549
|
class="text-none sp-invite-submit-btn"
|
|
550
550
|
height="76"
|
|
551
|
-
:disabled="!
|
|
551
|
+
:disabled="!canSubmitInvite"
|
|
552
552
|
:loading="disableServiceProvider"
|
|
553
553
|
@click="submitServiceProviderInvite()"
|
|
554
554
|
>
|
|
@@ -574,6 +574,7 @@ type ListTab = "active" | "pending" | "inactive";
|
|
|
574
574
|
|
|
575
575
|
type TableRow = {
|
|
576
576
|
_id: string;
|
|
577
|
+
userId: string;
|
|
577
578
|
serviceProviderOrgId: string;
|
|
578
579
|
companyName: string;
|
|
579
580
|
email: string;
|
|
@@ -733,11 +734,10 @@ const {
|
|
|
733
734
|
updateStatus: updateServiceProviderStatus,
|
|
734
735
|
invite,
|
|
735
736
|
} = useServiceProvider();
|
|
736
|
-
const {
|
|
737
|
+
const { getAllByUserId } = useMember();
|
|
737
738
|
|
|
738
739
|
const { getVerifications, cancelUserInvitation, getVerificationByEmail } =
|
|
739
740
|
useVerification();
|
|
740
|
-
const { getOrgsByMembership } = useMember();
|
|
741
741
|
|
|
742
742
|
const existingAccount = ref<any>(null);
|
|
743
743
|
const checkingExistingAccount = ref(false);
|
|
@@ -757,11 +757,10 @@ const createInviteDialog = ref(false);
|
|
|
757
757
|
// const loadingInviteRoles = ref(false);
|
|
758
758
|
// const serviceProviderInviteRoles = ref<Array<Record<string, any>>>([]);
|
|
759
759
|
|
|
760
|
-
const { getByUserType } = useMember();
|
|
761
|
-
const { getOrgs } = useOrg();
|
|
762
760
|
|
|
763
761
|
async function checkExistingAccount(email: string) {
|
|
764
762
|
existingAccount.value = null;
|
|
763
|
+
messageServiceProvider.value = "";
|
|
765
764
|
|
|
766
765
|
if (!email.trim() || emailRule(email) !== true) return;
|
|
767
766
|
|
|
@@ -770,30 +769,50 @@ async function checkExistingAccount(email: string) {
|
|
|
770
769
|
try {
|
|
771
770
|
const user = await getUserByEmail(email);
|
|
772
771
|
|
|
773
|
-
|
|
772
|
+
if (!user?._id) return;
|
|
774
773
|
|
|
775
|
-
|
|
776
|
-
user: user._id,
|
|
777
|
-
});
|
|
774
|
+
const members = await getAllByUserId(user._id);
|
|
778
775
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
776
|
+
const appMember = (members.items ?? []).find(
|
|
777
|
+
(member: any) => member.type === serviceProviderInvite.value.app
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
if (!appMember) {
|
|
781
|
+
existingAccount.value = null;
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (
|
|
786
|
+
appMember.org === props.orgId &&
|
|
787
|
+
(appMember.siteId || "") === (props.siteId || "")
|
|
788
|
+
) {
|
|
789
|
+
existingAccount.value = null;
|
|
790
|
+
messageServiceProvider.value =
|
|
791
|
+
"This user is already a member of this service provider.";
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
existingAccount.value = {
|
|
796
|
+
_id: appMember.org,
|
|
797
|
+
text: appMember.orgName,
|
|
798
|
+
org: appMember.org,
|
|
799
|
+
orgName: appMember.orgName,
|
|
800
|
+
siteId: appMember.siteId,
|
|
801
|
+
siteName: appMember.siteName,
|
|
802
|
+
};
|
|
782
803
|
|
|
783
|
-
if (matchedOrg) {
|
|
784
|
-
existingAccount.value = {
|
|
785
|
-
_id: matchedOrg.value,
|
|
786
|
-
text: matchedOrg.text,
|
|
787
|
-
...matchedOrg,
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
804
|
} catch (error) {
|
|
791
805
|
existingAccount.value = null;
|
|
792
806
|
} finally {
|
|
793
807
|
checkingExistingAccount.value = false;
|
|
794
808
|
}
|
|
795
809
|
}
|
|
796
|
-
|
|
810
|
+
const canSubmitInvite = computed(() => {
|
|
811
|
+
return (
|
|
812
|
+
validServiceProvider.value &&
|
|
813
|
+
!messageServiceProvider.value
|
|
814
|
+
);
|
|
815
|
+
});
|
|
797
816
|
watch(
|
|
798
817
|
() => serviceProviderInvite.value.email,
|
|
799
818
|
async (value) => {
|
|
@@ -939,6 +958,7 @@ function formatEmailCell(value: unknown): string {
|
|
|
939
958
|
function mapProviderRows(raw: Record<string, any>[]): TableRow[] {
|
|
940
959
|
return raw.map((row) => ({
|
|
941
960
|
_id: String(row._id ?? ""),
|
|
961
|
+
userId: String(row.user ?? ""),
|
|
942
962
|
serviceProviderOrgId: String(row.serviceProviderOrgId ?? ""),
|
|
943
963
|
companyName: row.name ?? "-",
|
|
944
964
|
email: formatEmailCell(row.email),
|
|
@@ -961,6 +981,7 @@ function inviteTypeLabel(t: string) {
|
|
|
961
981
|
function mapInviteRows(raw: Record<string, any>[]): TableRow[] {
|
|
962
982
|
return raw.map((row) => ({
|
|
963
983
|
_id: String(row._id ?? ""),
|
|
984
|
+
userId: String(row.user ?? ""),
|
|
964
985
|
serviceProviderOrgId: String(row.metadata?.serviceProviderOrgId ?? ""),
|
|
965
986
|
companyName: row.metadata?.name || "-",
|
|
966
987
|
email: formatEmailCell(row.email),
|
|
@@ -1029,16 +1050,12 @@ async function openView(row: TableRow) {
|
|
|
1029
1050
|
|
|
1030
1051
|
loadingMembers.value = true;
|
|
1031
1052
|
try {
|
|
1032
|
-
const data = await
|
|
1033
|
-
|
|
1034
|
-
limit: 1000,
|
|
1035
|
-
org: row.serviceProviderOrgId,
|
|
1036
|
-
status: "active",
|
|
1037
|
-
});
|
|
1053
|
+
const data = await getAllByUserId(row.userId);
|
|
1054
|
+
|
|
1038
1055
|
serviceProviderMembers.value = (data.items ?? []).map((member: any) => ({
|
|
1039
|
-
_id:
|
|
1040
|
-
name: member.name ||
|
|
1041
|
-
role: member.
|
|
1056
|
+
_id: member._id,
|
|
1057
|
+
name: member.name || "-",
|
|
1058
|
+
role: member.role || "-",
|
|
1042
1059
|
}));
|
|
1043
1060
|
} catch (error: any) {
|
|
1044
1061
|
showMessage(errorConverter(error), "error");
|
|
@@ -63,15 +63,12 @@ export function useNFCPatrolDailyReport(
|
|
|
63
63
|
|
|
64
64
|
try {
|
|
65
65
|
const [logsRes, siteInfo] = await Promise.all([
|
|
66
|
-
getPatrolLogs
|
|
66
|
+
getPatrolLogs({
|
|
67
67
|
page: 1,
|
|
68
68
|
limit: 100,
|
|
69
69
|
site,
|
|
70
70
|
date: filters.date,
|
|
71
71
|
routeId: filters.route,
|
|
72
|
-
routeStartTime: filters.timeRange
|
|
73
|
-
? filters.timeRange.split(" - ")[0]
|
|
74
|
-
: undefined,
|
|
75
72
|
}),
|
|
76
73
|
getSiteById(site),
|
|
77
74
|
]);
|
|
@@ -2,6 +2,8 @@ import type {
|
|
|
2
2
|
NFCPatrolReportFilters,
|
|
3
3
|
NFCPatrolSummaryReport,
|
|
4
4
|
NFCPatrolReportSummaryRow,
|
|
5
|
+
NFCPatrolMonthlyReportRow,
|
|
6
|
+
NFCPatrolMonthlyReport,
|
|
5
7
|
} from "../types/nfc-patrol-report";
|
|
6
8
|
import useNFCPatrolLog from "./useNFCPatrolLog";
|
|
7
9
|
import useSite from "./useSite";
|
|
@@ -12,6 +14,55 @@ interface NFCPatrolLogListResponse {
|
|
|
12
14
|
pageRange?: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
const MONTHS = [
|
|
18
|
+
"January",
|
|
19
|
+
"February",
|
|
20
|
+
"March",
|
|
21
|
+
"April",
|
|
22
|
+
"May",
|
|
23
|
+
"June",
|
|
24
|
+
"July",
|
|
25
|
+
"August",
|
|
26
|
+
"September",
|
|
27
|
+
"October",
|
|
28
|
+
"November",
|
|
29
|
+
"December",
|
|
30
|
+
];
|
|
31
|
+
function mapLogsToMonthlyRows(
|
|
32
|
+
logs: Record<string, any>[],
|
|
33
|
+
): NFCPatrolMonthlyReportRow[] {
|
|
34
|
+
const rows = MONTHS.map((month) => ({
|
|
35
|
+
month,
|
|
36
|
+
checked: 0,
|
|
37
|
+
checkedPercentage: 0,
|
|
38
|
+
missed: 0,
|
|
39
|
+
missedPercentage: 0,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
logs.forEach((log) => {
|
|
43
|
+
const monthIndex = new Date(log.date).getMonth();
|
|
44
|
+
|
|
45
|
+
for (const checkpoint of log.checkPoints ?? []) {
|
|
46
|
+
if (checkpoint.status === "Completed") {
|
|
47
|
+
rows[monthIndex].checked++;
|
|
48
|
+
} else if (checkpoint.status === "Skipped") {
|
|
49
|
+
rows[monthIndex].missed++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
rows.forEach((row) => {
|
|
55
|
+
const total = row.checked + row.missed;
|
|
56
|
+
|
|
57
|
+
if (total > 0) {
|
|
58
|
+
row.checkedPercentage = Math.round((row.checked / total) * 100);
|
|
59
|
+
row.missedPercentage = Math.round((row.missed / total) * 100);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return rows;
|
|
64
|
+
}
|
|
65
|
+
|
|
15
66
|
function buildAddress(address?: Record<string, string>) {
|
|
16
67
|
if (!address) return undefined;
|
|
17
68
|
|
|
@@ -27,23 +78,7 @@ function buildAddress(address?: Record<string, string>) {
|
|
|
27
78
|
return parts.length ? parts.join(", ") : undefined;
|
|
28
79
|
}
|
|
29
80
|
|
|
30
|
-
function mapLogToSummaryRow(
|
|
31
|
-
log: Record<string, any>,
|
|
32
|
-
index: number,
|
|
33
|
-
): NFCPatrolReportSummaryRow {
|
|
34
|
-
const checkpoints: Record<string, any>[] = log.checkPoints ?? [];
|
|
35
|
-
const checked = checkpoints.filter((cp) => cp.status === "Completed").length;
|
|
36
|
-
const missed = checkpoints.filter((cp) => cp.status === "Skipped").length;
|
|
37
81
|
|
|
38
|
-
return {
|
|
39
|
-
id: log._id,
|
|
40
|
-
routeName: `${index + 1}. ${log.route?.name ?? "-"}`,
|
|
41
|
-
securityCheckSchedule: log.route?.startTime ?? "-",
|
|
42
|
-
checked,
|
|
43
|
-
missed,
|
|
44
|
-
status: missed === 0 ? "Completed" : "Incomplete",
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
82
|
|
|
48
83
|
export function useNFCPatrolMonthlyReport(
|
|
49
84
|
filters: NFCPatrolReportFilters,
|
|
@@ -52,12 +87,12 @@ export function useNFCPatrolMonthlyReport(
|
|
|
52
87
|
const { getAll: getPatrolLogs } = useNFCPatrolLog();
|
|
53
88
|
const { getSiteById } = useSite();
|
|
54
89
|
|
|
55
|
-
const report = ref<
|
|
90
|
+
const report = ref<NFCPatrolMonthlyReport | null>(null);
|
|
56
91
|
const loading = ref(false);
|
|
57
92
|
const notFound = ref(false);
|
|
58
93
|
|
|
59
94
|
async function fetchReport() {
|
|
60
|
-
if (!site || !filters.
|
|
95
|
+
if (!site || !filters.route) {
|
|
61
96
|
report.value = null;
|
|
62
97
|
notFound.value = false;
|
|
63
98
|
return;
|
|
@@ -65,19 +100,16 @@ export function useNFCPatrolMonthlyReport(
|
|
|
65
100
|
|
|
66
101
|
loading.value = true;
|
|
67
102
|
notFound.value = false;
|
|
68
|
-
|
|
103
|
+
const currentYear = new Date().getFullYear().toString();
|
|
69
104
|
try {
|
|
70
105
|
const [logsRes, siteInfo] = await Promise.all([
|
|
71
|
-
getPatrolLogs
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
? filters.timeRange.split(" - ")[0]
|
|
79
|
-
: undefined,
|
|
80
|
-
type: "month",
|
|
106
|
+
getPatrolLogs({
|
|
107
|
+
page: 1,
|
|
108
|
+
limit: 100,
|
|
109
|
+
site,
|
|
110
|
+
routeId: filters.route,
|
|
111
|
+
date: currentYear,
|
|
112
|
+
type: "month",
|
|
81
113
|
}),
|
|
82
114
|
getSiteById(site),
|
|
83
115
|
]);
|
|
@@ -93,7 +125,7 @@ export function useNFCPatrolMonthlyReport(
|
|
|
93
125
|
address: buildAddress(siteInfo.address),
|
|
94
126
|
},
|
|
95
127
|
title: `Patrol Route Summary: ${routeName}`,
|
|
96
|
-
rows: logs
|
|
128
|
+
rows: mapLogsToMonthlyRows(logs),
|
|
97
129
|
};
|
|
98
130
|
} else {
|
|
99
131
|
report.value = null;
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ export interface NFCPatrolReportFilterOptions {
|
|
|
26
26
|
|
|
27
27
|
export interface NFCPatrolReportLocation {
|
|
28
28
|
name?: string;
|
|
29
|
-
address?: string;
|
|
29
|
+
address?: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export interface NFCPatrolRouteSummary {
|
|
@@ -77,3 +77,17 @@ export interface NFCPatrolSummaryReport {
|
|
|
77
77
|
title: string;
|
|
78
78
|
rows: NFCPatrolReportSummaryRow[];
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
export interface NFCPatrolMonthlyReportRow {
|
|
82
|
+
month: string;
|
|
83
|
+
checked: number;
|
|
84
|
+
checkedPercentage: number;
|
|
85
|
+
missed: number;
|
|
86
|
+
missedPercentage: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface NFCPatrolMonthlyReport {
|
|
90
|
+
location: NFCPatrolReportLocation;
|
|
91
|
+
title: string;
|
|
92
|
+
rows: NFCPatrolMonthlyReportRow[];
|
|
93
|
+
}
|