@7365admin1/layer-common 4.107.0 → 4.109.0
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 +20 -0
- package/assets/css/screens.css +11 -0
- package/components/BulletinBoardForm.vue +112 -0
- package/components/BulletinBoardView.vue +2 -0
- package/components/BulletinEventRsvp.vue +126 -0
- package/components/Nfc/NFCPatrolReportMain.vue +61 -72
- package/components/Nfc/NFCPatrolRouteMain.vue +21 -31
- package/components/Nfc/NFCRouteScheduleTimes.vue +83 -0
- package/components/Nfc/PatrolReport/DailyReportTab.vue +24 -3
- package/components/Nfc/PatrolReport/MonthlyReportTab.vue +32 -3
- package/components/Nfc/PatrolReport/MonthlyReportTable.vue +29 -1
- package/components/Nfc/PatrolReport/PatrolActivityTable.vue +24 -2
- package/components/Nfc/PatrolReport/PatrolMonthDialog.vue +195 -0
- package/components/Nfc/PatrolReport/PatrolReportTab.vue +11 -2
- package/components/Nfc/PatrolReport/PatrolRouteDetailsDialog.vue +214 -0
- package/components/Nfc/PatrolReport/ReportLocationHeader.vue +4 -1
- package/components/Nfc/PatrolReport/SkipPhotoDetails.vue +137 -0
- package/components/Nfc/PatrolReport/SkipPhotoThumb.vue +132 -0
- package/components/Nfc/PatrolReport/SummaryReportTable.vue +42 -1
- package/components/OnlineFormFill.vue +11 -0
- package/components/SiteSettings.vue +4 -0
- package/components/VehicleManagement.vue +4 -1
- package/composables/useNFCPatrolDailyReport.ts +4 -18
- package/composables/useNFCPatrolMonthlyReport.ts +34 -5
- package/composables/useNFCPatrolReport.ts +5 -74
- package/composables/useReportPdf.ts +220 -0
- package/package.json +1 -1
- package/types/bulletin-board.d.ts +11 -1
- package/types/nfc-patrol-report.ts +14 -0
- package/utils/nfc-patrol-report.ts +239 -1
- package/utils/nfc-patrol-schedule.ts +73 -0
- package/utils/online-form-submission.ts +48 -0
- package/utils/report-export.ts +120 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
:options="options"
|
|
6
6
|
:show-time-filter="false"
|
|
7
7
|
@update:model-value="$emit('update:filters', $event)"
|
|
8
|
-
@export="(type) => $emit('export', type)"
|
|
8
|
+
@export="(type) => $emit('export', type, csvSections())"
|
|
9
9
|
/>
|
|
10
10
|
<div class="report-export">
|
|
11
11
|
<v-divider :thickness="1" class="border-opacity-100" />
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<template v-if="report">
|
|
14
14
|
<ReportLocationHeader :location="report.location" />
|
|
15
15
|
<v-divider :thickness="1" class="border-opacity-100 mb-6" />
|
|
16
|
-
<SummaryReportTable :title="report.title" :rows="report.rows" />
|
|
16
|
+
<SummaryReportTable :title="report.title" :rows="report.rows" @select="openDetails" />
|
|
17
17
|
</template>
|
|
18
18
|
|
|
19
19
|
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
/>
|
|
32
32
|
|
|
33
33
|
<ReportEmptyState v-else-if="!loading" />
|
|
34
|
+
|
|
35
|
+
<PatrolRouteDetailsDialog v-model="detailsOpen" :row="selectedRow" />
|
|
34
36
|
</div>
|
|
35
37
|
</div>
|
|
36
38
|
</template>
|
|
@@ -39,8 +41,12 @@
|
|
|
39
41
|
import type {
|
|
40
42
|
NFCPatrolReportFilterOptions,
|
|
41
43
|
NFCPatrolReportFilters,
|
|
44
|
+
NFCPatrolReportSummaryRow,
|
|
42
45
|
} from "../../../types/nfc-patrol-report";
|
|
43
46
|
import { useNFCPatrolDailyReport } from "../../../composables/useNFCPatrolDailyReport";
|
|
47
|
+
import { dailyReportCsv } from "../../../utils/nfc-patrol-report";
|
|
48
|
+
import type { CsvSection } from "../../../utils/report-export";
|
|
49
|
+
import PatrolRouteDetailsDialog from "./PatrolRouteDetailsDialog.vue";
|
|
44
50
|
import ReportEmptyState from "./ReportEmptyState.vue";
|
|
45
51
|
import ReportFilters from "./ReportFilters.vue";
|
|
46
52
|
import ReportLocationHeader from "./ReportLocationHeader.vue";
|
|
@@ -55,7 +61,8 @@ const props = defineProps<{
|
|
|
55
61
|
|
|
56
62
|
defineEmits<{
|
|
57
63
|
"update:filters": [value: NFCPatrolReportFilters];
|
|
58
|
-
|
|
64
|
+
/** `csv` is the report's tables, built from its data rather than read off the screen. */
|
|
65
|
+
export: [type: "pdf" | "csv", csv: CsvSection[]];
|
|
59
66
|
}>();
|
|
60
67
|
|
|
61
68
|
const { report, loading, notFound, failed, fetchReport } = useNFCPatrolDailyReport(
|
|
@@ -63,6 +70,20 @@ const { report, loading, notFound, failed, fetchReport } = useNFCPatrolDailyRepo
|
|
|
63
70
|
props.site,
|
|
64
71
|
);
|
|
65
72
|
|
|
73
|
+
const { getFileUrl } = useFile();
|
|
74
|
+
|
|
75
|
+
function csvSections() {
|
|
76
|
+
return report.value ? dailyReportCsv(report.value, getFileUrl) : [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const detailsOpen = ref(false);
|
|
80
|
+
const selectedRow = ref<NFCPatrolReportSummaryRow | null>(null);
|
|
81
|
+
|
|
82
|
+
function openDetails(row: NFCPatrolReportSummaryRow) {
|
|
83
|
+
selectedRow.value = row;
|
|
84
|
+
detailsOpen.value = true;
|
|
85
|
+
}
|
|
86
|
+
|
|
66
87
|
watch(
|
|
67
88
|
() => [props.filters.route, props.filters.date, props.active],
|
|
68
89
|
() => {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
:show-time-filter="false"
|
|
7
7
|
:show-date-filter="false"
|
|
8
8
|
@update:model-value="$emit('update:filters', $event)"
|
|
9
|
-
@export="(type) => $emit('export', type)"
|
|
9
|
+
@export="(type) => $emit('export', type, csvSections())"
|
|
10
10
|
/>
|
|
11
11
|
<div class="report-export">
|
|
12
12
|
<v-divider :thickness="1" class="border-opacity-100" />
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
<MonthlyReportTable
|
|
19
19
|
:title="report.title"
|
|
20
20
|
:rows="report.rows"
|
|
21
|
+
@select="openMonth"
|
|
21
22
|
/>
|
|
22
23
|
</template>
|
|
23
24
|
|
|
@@ -33,6 +34,13 @@
|
|
|
33
34
|
title="No Monthly Report"
|
|
34
35
|
message="No patrol log found for this route"
|
|
35
36
|
/>
|
|
37
|
+
|
|
38
|
+
<PatrolMonthDialog
|
|
39
|
+
v-model="monthOpen"
|
|
40
|
+
:title="monthTitle"
|
|
41
|
+
:route-name="monthRows[0]?.detail?.routeSummary.routeName ?? ''"
|
|
42
|
+
:rows="monthRows"
|
|
43
|
+
/>
|
|
36
44
|
</div>
|
|
37
45
|
</div>
|
|
38
46
|
</template>
|
|
@@ -41,9 +49,13 @@
|
|
|
41
49
|
import type {
|
|
42
50
|
NFCPatrolReportFilterOptions,
|
|
43
51
|
NFCPatrolReportFilters,
|
|
52
|
+
NFCPatrolReportSummaryRow,
|
|
44
53
|
} from "../../../types/nfc-patrol-report";
|
|
45
54
|
|
|
46
55
|
import { useNFCPatrolMonthlyReport } from "../../../composables/useNFCPatrolMonthlyReport";
|
|
56
|
+
import { monthlyReportCsv } from "../../../utils/nfc-patrol-report";
|
|
57
|
+
import PatrolMonthDialog from "./PatrolMonthDialog.vue";
|
|
58
|
+
import type { CsvSection } from "../../../utils/report-export";
|
|
47
59
|
|
|
48
60
|
import ReportEmptyState from "./ReportEmptyState.vue";
|
|
49
61
|
import ReportFilters from "./ReportFilters.vue";
|
|
@@ -59,12 +71,29 @@ const props = defineProps<{
|
|
|
59
71
|
|
|
60
72
|
defineEmits<{
|
|
61
73
|
"update:filters": [value: NFCPatrolReportFilters];
|
|
62
|
-
|
|
74
|
+
/** `csv` is the report's tables, built from its data rather than read off the screen. */
|
|
75
|
+
export: [type: "pdf" | "csv", csv: CsvSection[]];
|
|
63
76
|
}>();
|
|
64
77
|
|
|
65
|
-
const { report, loading, notFound, failed, fetchReport } =
|
|
78
|
+
const { report, loading, notFound, failed, fetchReport, monthRuns } =
|
|
66
79
|
useNFCPatrolMonthlyReport(props.filters, props.site);
|
|
67
80
|
|
|
81
|
+
// A month's row opens the list of that month's patrols.
|
|
82
|
+
const monthOpen = ref(false);
|
|
83
|
+
const monthTitle = ref("");
|
|
84
|
+
const monthRows = shallowRef<NFCPatrolReportSummaryRow[]>([]);
|
|
85
|
+
|
|
86
|
+
function openMonth(monthIndex: number) {
|
|
87
|
+
// The yearly report always covers the current year (useNFCPatrolMonthlyReport).
|
|
88
|
+
monthTitle.value = `${report.value?.rows[monthIndex]?.month ?? ""} ${new Date().getFullYear()}`;
|
|
89
|
+
monthRows.value = monthRuns(monthIndex);
|
|
90
|
+
monthOpen.value = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function csvSections() {
|
|
94
|
+
return report.value ? monthlyReportCsv(report.value) : [];
|
|
95
|
+
}
|
|
96
|
+
|
|
68
97
|
watch(
|
|
69
98
|
() => [props.filters.route, props.active],
|
|
70
99
|
() => {
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The year, month by month. A month that had patrols opens its list of patrol
|
|
3
|
+
runs (`PatrolMonthDialog`), and each run there opens its checkpoints and
|
|
4
|
+
skip photos.
|
|
5
|
+
-->
|
|
1
6
|
<template>
|
|
2
7
|
<div class="mt-6">
|
|
3
8
|
<div class="screen-card-title mb-4">
|
|
@@ -17,8 +22,13 @@
|
|
|
17
22
|
|
|
18
23
|
<tbody>
|
|
19
24
|
<tr
|
|
20
|
-
v-for="row in rows"
|
|
25
|
+
v-for="(row, index) in rows"
|
|
21
26
|
:key="row.month"
|
|
27
|
+
:class="{ 'monthly-table__row--link': row.runs }"
|
|
28
|
+
:tabindex="row.runs ? 0 : undefined"
|
|
29
|
+
:title="row.runs ? `View the patrols of ${row.month}` : undefined"
|
|
30
|
+
@click="row.runs && emit('select', index)"
|
|
31
|
+
@keydown.enter="row.runs && emit('select', index)"
|
|
22
32
|
>
|
|
23
33
|
<td>{{ row.month }}</td>
|
|
24
34
|
|
|
@@ -52,6 +62,11 @@ defineProps<{
|
|
|
52
62
|
title: string;
|
|
53
63
|
rows: NFCPatrolMonthlyReportRow[];
|
|
54
64
|
}>();
|
|
65
|
+
|
|
66
|
+
const emit = defineEmits<{
|
|
67
|
+
/** The month (0-11) whose patrols to list. */
|
|
68
|
+
select: [monthIndex: number];
|
|
69
|
+
}>();
|
|
55
70
|
</script>
|
|
56
71
|
|
|
57
72
|
<style scoped>
|
|
@@ -68,4 +83,17 @@ thead th {
|
|
|
68
83
|
tbody td {
|
|
69
84
|
height: 56px;
|
|
70
85
|
}
|
|
86
|
+
|
|
87
|
+
.monthly-table__row--link {
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.monthly-table__row--link:hover > td {
|
|
92
|
+
background: var(--hover);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.monthly-table__row--link:focus-visible {
|
|
96
|
+
outline: 2px solid var(--accent);
|
|
97
|
+
outline-offset: -2px;
|
|
98
|
+
}
|
|
71
99
|
</style>
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
tokens by way of `utils/theme.ts`, and `utils/status.ts` does not name
|
|
11
11
|
"Skipped", so routing this through `StatusChip` would turn a red skipped
|
|
12
12
|
checkpoint neutral. That is a design decision, not a conversion.
|
|
13
|
+
|
|
14
|
+
A skipped checkpoint's photo shows in its own column (`SkipPhotoThumb`); the
|
|
15
|
+
thumbnail opens the photo, the rest of the row still opens the remarks. The
|
|
16
|
+
PDF drops that column and draws the photo with its remarks in a row of its
|
|
17
|
+
own right under the checkpoint (`SkipPhotoDetails`), kept on the same page.
|
|
13
18
|
-->
|
|
14
19
|
<template>
|
|
15
20
|
<div class="table-card table-card__shrink">
|
|
@@ -26,13 +31,14 @@
|
|
|
26
31
|
<th>Signature</th>
|
|
27
32
|
<th>Start - End Time</th>
|
|
28
33
|
<th>Status</th>
|
|
34
|
+
<th data-pdf-hide>Skip Photo</th>
|
|
29
35
|
</tr>
|
|
30
36
|
</thead>
|
|
31
37
|
<tbody>
|
|
38
|
+
<template v-for="(activity, index) in activities" :key="`${activity.id}-${index}`">
|
|
32
39
|
<tr
|
|
33
|
-
v-for="activity in activities"
|
|
34
|
-
:key="activity.id"
|
|
35
40
|
:class="{ 'cursor-pointer': activity.status === 'Skipped' }"
|
|
41
|
+
:data-pdf-keep-with-next="activity.status === 'Skipped' || undefined"
|
|
36
42
|
@click="handleActivityClick(activity)"
|
|
37
43
|
>
|
|
38
44
|
<td>{{ activity.checkpoint }}</td>
|
|
@@ -44,6 +50,7 @@
|
|
|
44
50
|
:alt="`${activity.guardName || worker.one} signature`"
|
|
45
51
|
class="guard-signature"
|
|
46
52
|
contain
|
|
53
|
+
eager
|
|
47
54
|
/>
|
|
48
55
|
<span v-else>-</span>
|
|
49
56
|
</td>
|
|
@@ -62,7 +69,16 @@
|
|
|
62
69
|
</v-icon>
|
|
63
70
|
</v-chip>
|
|
64
71
|
</td>
|
|
72
|
+
<td data-pdf-hide>
|
|
73
|
+
<SkipPhotoThumb :activity="activity" />
|
|
74
|
+
</td>
|
|
75
|
+
</tr>
|
|
76
|
+
<tr v-if="activity.status === 'Skipped'" data-pdf-only>
|
|
77
|
+
<td colspan="5" class="activity-table__skip">
|
|
78
|
+
<SkipPhotoDetails :activities="[activity]" inline />
|
|
79
|
+
</td>
|
|
65
80
|
</tr>
|
|
81
|
+
</template>
|
|
66
82
|
</tbody>
|
|
67
83
|
</v-table>
|
|
68
84
|
</div>
|
|
@@ -75,6 +91,8 @@ import type {
|
|
|
75
91
|
NFCPatrolActivity,
|
|
76
92
|
NFCPatrolActivityStatus,
|
|
77
93
|
} from "../../../types/nfc-patrol-report";
|
|
94
|
+
import SkipPhotoDetails from "./SkipPhotoDetails.vue";
|
|
95
|
+
import SkipPhotoThumb from "./SkipPhotoThumb.vue";
|
|
78
96
|
|
|
79
97
|
const worker = useWorkerTerm();
|
|
80
98
|
defineProps<{
|
|
@@ -104,6 +122,10 @@ function handleActivityClick(activity: NFCPatrolActivity) {
|
|
|
104
122
|
border-bottom: 1px solid var(--border);
|
|
105
123
|
}
|
|
106
124
|
|
|
125
|
+
.activity-table__skip {
|
|
126
|
+
padding-top: 0 !important;
|
|
127
|
+
}
|
|
128
|
+
|
|
107
129
|
.cursor-pointer {
|
|
108
130
|
cursor: pointer;
|
|
109
131
|
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Every patrol run of one month of the yearly report, opened from its month's
|
|
3
|
+
row: by day, then by scheduled start. A run opens the same Patrol Route
|
|
4
|
+
Details dialog as the daily summary - checkpoints, remarks, skip photos.
|
|
5
|
+
|
|
6
|
+
The month's totals sit in the title row so the list can be checked against
|
|
7
|
+
the yearly table it came from.
|
|
8
|
+
|
|
9
|
+
Ten runs a page, paged the way the app's tables are ("1-10 of 124" and the
|
|
10
|
+
two arrows). The month is already loaded, so paging asks the server nothing.
|
|
11
|
+
-->
|
|
12
|
+
<template>
|
|
13
|
+
<v-dialog
|
|
14
|
+
:model-value="modelValue"
|
|
15
|
+
max-width="960"
|
|
16
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
17
|
+
>
|
|
18
|
+
<v-card class="screen-modal">
|
|
19
|
+
<div class="month-dialog__head">
|
|
20
|
+
<div>
|
|
21
|
+
<v-card-title class="pa-0">{{ title }}</v-card-title>
|
|
22
|
+
<p class="month-dialog__sub">
|
|
23
|
+
{{ routeName }} · {{ rows.length }} {{ rows.length === 1 ? "patrol" : "patrols" }} ·
|
|
24
|
+
{{ checked }} checked · {{ missed }} missed
|
|
25
|
+
</p>
|
|
26
|
+
</div>
|
|
27
|
+
<v-btn
|
|
28
|
+
class="screen-modal__close"
|
|
29
|
+
icon="mdi-close"
|
|
30
|
+
variant="flat"
|
|
31
|
+
aria-label="Close"
|
|
32
|
+
@click="close"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="screen-modal__body px-6 pb-6">
|
|
37
|
+
<div class="table-card table-card__shrink">
|
|
38
|
+
<div class="month-dialog__pager">
|
|
39
|
+
<span class="table-card__range">{{ pageRange }}</span>
|
|
40
|
+
<v-btn
|
|
41
|
+
icon="mdi-chevron-left"
|
|
42
|
+
variant="text"
|
|
43
|
+
density="comfortable"
|
|
44
|
+
aria-label="Previous page"
|
|
45
|
+
:disabled="page <= 1"
|
|
46
|
+
@click="page--"
|
|
47
|
+
/>
|
|
48
|
+
<v-btn
|
|
49
|
+
icon="mdi-chevron-right"
|
|
50
|
+
variant="text"
|
|
51
|
+
density="comfortable"
|
|
52
|
+
aria-label="Next page"
|
|
53
|
+
:disabled="page >= pages"
|
|
54
|
+
@click="page++"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
<v-table>
|
|
58
|
+
<thead>
|
|
59
|
+
<tr>
|
|
60
|
+
<th>Date</th>
|
|
61
|
+
<th>Security Check Schedule</th>
|
|
62
|
+
<th>Security Personnel</th>
|
|
63
|
+
<th>Checked</th>
|
|
64
|
+
<th>Missed</th>
|
|
65
|
+
<th>Status</th>
|
|
66
|
+
</tr>
|
|
67
|
+
</thead>
|
|
68
|
+
<tbody>
|
|
69
|
+
<tr
|
|
70
|
+
v-for="row in pageRows"
|
|
71
|
+
:key="row.id"
|
|
72
|
+
class="month-dialog__row"
|
|
73
|
+
tabindex="0"
|
|
74
|
+
title="View patrol route details"
|
|
75
|
+
@click="open(row)"
|
|
76
|
+
@keydown.enter="open(row)"
|
|
77
|
+
>
|
|
78
|
+
<td class="text-no-wrap">{{ formatReportDate(row.date ?? "") }}</td>
|
|
79
|
+
<td>
|
|
80
|
+
<v-icon size="small" class="mr-1">mdi-clock-outline</v-icon>
|
|
81
|
+
{{ row.securityCheckSchedule }}
|
|
82
|
+
</td>
|
|
83
|
+
<td>{{ row.guardName || "-" }}</td>
|
|
84
|
+
<td>{{ row.checked }}</td>
|
|
85
|
+
<td>{{ row.missed }}</td>
|
|
86
|
+
<td>
|
|
87
|
+
<span :class="row.status === 'Completed' ? 'text-success' : 'text-error'">
|
|
88
|
+
{{ row.status }}
|
|
89
|
+
</span>
|
|
90
|
+
</td>
|
|
91
|
+
</tr>
|
|
92
|
+
</tbody>
|
|
93
|
+
</v-table>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div class="screen-modal__footer">
|
|
98
|
+
<v-btn variant="outlined" class="screen-btn-ghost" @click="close">Close</v-btn>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<PatrolRouteDetailsDialog v-model="detailsOpen" :row="selected" />
|
|
102
|
+
</v-card>
|
|
103
|
+
</v-dialog>
|
|
104
|
+
</template>
|
|
105
|
+
|
|
106
|
+
<script setup lang="ts">
|
|
107
|
+
import type { NFCPatrolReportSummaryRow } from "../../../types/nfc-patrol-report";
|
|
108
|
+
import { formatReportDate } from "../../../utils/report-export";
|
|
109
|
+
import PatrolRouteDetailsDialog from "./PatrolRouteDetailsDialog.vue";
|
|
110
|
+
|
|
111
|
+
const props = defineProps<{
|
|
112
|
+
modelValue: boolean;
|
|
113
|
+
/** "October 2026" */
|
|
114
|
+
title: string;
|
|
115
|
+
routeName: string;
|
|
116
|
+
rows: NFCPatrolReportSummaryRow[];
|
|
117
|
+
}>();
|
|
118
|
+
|
|
119
|
+
const emit = defineEmits<{
|
|
120
|
+
"update:modelValue": [value: boolean];
|
|
121
|
+
}>();
|
|
122
|
+
|
|
123
|
+
const PAGE_SIZE = 10;
|
|
124
|
+
const page = ref(1);
|
|
125
|
+
const pages = computed(() => Math.max(1, Math.ceil(props.rows.length / PAGE_SIZE)));
|
|
126
|
+
const pageRows = computed(() => props.rows.slice((page.value - 1) * PAGE_SIZE, page.value * PAGE_SIZE));
|
|
127
|
+
const pageRange = computed(() => {
|
|
128
|
+
const total = props.rows.length;
|
|
129
|
+
if (!total) return "0 of 0";
|
|
130
|
+
const from = (page.value - 1) * PAGE_SIZE + 1;
|
|
131
|
+
return `${from}-${Math.min(total, page.value * PAGE_SIZE)} of ${total}`;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Another month opens on its first page.
|
|
135
|
+
watch(
|
|
136
|
+
() => props.rows,
|
|
137
|
+
() => {
|
|
138
|
+
page.value = 1;
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const checked = computed(() => props.rows.reduce((sum, row) => sum + row.checked, 0));
|
|
143
|
+
const missed = computed(() => props.rows.reduce((sum, row) => sum + row.missed, 0));
|
|
144
|
+
|
|
145
|
+
const detailsOpen = ref(false);
|
|
146
|
+
const selected = ref<NFCPatrolReportSummaryRow | null>(null);
|
|
147
|
+
|
|
148
|
+
function open(row: NFCPatrolReportSummaryRow) {
|
|
149
|
+
selected.value = row;
|
|
150
|
+
detailsOpen.value = true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function close() {
|
|
154
|
+
detailsOpen.value = false;
|
|
155
|
+
emit("update:modelValue", false);
|
|
156
|
+
}
|
|
157
|
+
</script>
|
|
158
|
+
|
|
159
|
+
<style scoped>
|
|
160
|
+
.month-dialog__head {
|
|
161
|
+
display: flex;
|
|
162
|
+
align-items: flex-start;
|
|
163
|
+
justify-content: space-between;
|
|
164
|
+
gap: 12px;
|
|
165
|
+
padding: 20px 24px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.month-dialog__sub {
|
|
169
|
+
margin: 4px 0 0;
|
|
170
|
+
font-size: var(--fs-cell);
|
|
171
|
+
color: var(--text2);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.month-dialog__pager {
|
|
175
|
+
display: flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
justify-content: flex-end;
|
|
178
|
+
gap: 2px;
|
|
179
|
+
padding: 6px 12px;
|
|
180
|
+
border-bottom: 1px solid var(--border);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.month-dialog__pager .table-card__range {
|
|
184
|
+
margin-right: 6px;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.month-dialog__row {
|
|
188
|
+
cursor: pointer;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.month-dialog__row:focus-visible {
|
|
192
|
+
outline: 2px solid var(--accent);
|
|
193
|
+
outline-offset: -2px;
|
|
194
|
+
}
|
|
195
|
+
</style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
:model-value="filters"
|
|
5
5
|
:options="options"
|
|
6
6
|
@update:model-value="$emit('update:filters', $event)"
|
|
7
|
-
@export="(type) => $emit('export', type)"
|
|
7
|
+
@export="(type) => $emit('export', type, csvSections())"
|
|
8
8
|
/>
|
|
9
9
|
<div class="report-export">
|
|
10
10
|
<v-divider :thickness="1" class="border-opacity-100" />
|
|
@@ -47,6 +47,8 @@ import type {
|
|
|
47
47
|
NFCPatrolReportFilters,
|
|
48
48
|
} from "../../../types/nfc-patrol-report";
|
|
49
49
|
import { useNFCPatrolReport } from "../../../composables/useNFCPatrolReport";
|
|
50
|
+
import { patrolReportCsv } from "../../../utils/nfc-patrol-report";
|
|
51
|
+
import type { CsvSection } from "../../../utils/report-export";
|
|
50
52
|
import PatrolActivityTable from "./PatrolActivityTable.vue";
|
|
51
53
|
import PatrolRouteSummary from "./PatrolRouteSummary.vue";
|
|
52
54
|
import RemarksDialog from "./RemarksDialog.vue";
|
|
@@ -63,7 +65,8 @@ const props = defineProps<{
|
|
|
63
65
|
|
|
64
66
|
defineEmits<{
|
|
65
67
|
"update:filters": [value: NFCPatrolReportFilters];
|
|
66
|
-
|
|
68
|
+
/** `csv` is the report's tables, built from its data rather than read off the screen. */
|
|
69
|
+
export: [type: "pdf" | "csv", csv: CsvSection[]];
|
|
67
70
|
}>();
|
|
68
71
|
|
|
69
72
|
const {
|
|
@@ -77,6 +80,12 @@ const {
|
|
|
77
80
|
openRemarksDialog,
|
|
78
81
|
} = useNFCPatrolReport(props.filters, props.site);
|
|
79
82
|
|
|
83
|
+
const { getFileUrl } = useFile();
|
|
84
|
+
|
|
85
|
+
function csvSections() {
|
|
86
|
+
return report.value ? patrolReportCsv(report.value, getFileUrl) : [];
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
watch(
|
|
81
90
|
() => [props.filters.route, props.filters.timeRange, props.filters.date, props.active],
|
|
82
91
|
() => {
|