@7365admin1/layer-common 4.108.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 +14 -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/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/report-export.ts +120 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
One run of a patrol route, opened from a row of the daily Patrol Route
|
|
3
|
+
Summary: who patrolled it, schedule against actual, and every checkpoint.
|
|
4
|
+
|
|
5
|
+
A skipped checkpoint shows the guard's remarks and, when one was attached,
|
|
6
|
+
the photo taken at the skip (`SkipPhotoThumb`), which opens full size.
|
|
7
|
+
|
|
8
|
+
Status colours follow `PatrolActivityTable`: `success`/`error` are the
|
|
9
|
+
design's `--ok`/`--err` by way of `utils/theme.ts`. A checkpoint still
|
|
10
|
+
`Pending` (a run not finished) stays neutral rather than red.
|
|
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 v-if="row?.detail" class="screen-modal">
|
|
19
|
+
<div class="route-details__head">
|
|
20
|
+
<div>
|
|
21
|
+
<v-card-title class="pa-0">Patrol Route Details</v-card-title>
|
|
22
|
+
<p class="route-details__sub">
|
|
23
|
+
{{ row.detail.routeSummary.routeName || "-" }} ·
|
|
24
|
+
<template v-if="row.date">{{ formatReportDate(row.date) }} · </template>
|
|
25
|
+
{{ row.detail.routeSummary.schedule.start }} - {{ row.detail.routeSummary.schedule.end }}
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
<v-btn
|
|
29
|
+
class="screen-modal__close"
|
|
30
|
+
icon="mdi-close"
|
|
31
|
+
variant="flat"
|
|
32
|
+
aria-label="Close"
|
|
33
|
+
@click="close"
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="screen-modal__body px-6 pb-6">
|
|
38
|
+
<dl class="route-details__facts">
|
|
39
|
+
<div>
|
|
40
|
+
<dt>Security Personnel</dt>
|
|
41
|
+
<dd>{{ row.guardName || "-" }}</dd>
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<dt>Signature</dt>
|
|
45
|
+
<dd>
|
|
46
|
+
<v-img
|
|
47
|
+
v-if="row.guardSignatureFileId"
|
|
48
|
+
:src="getFileUrl(row.guardSignatureFileId)"
|
|
49
|
+
:alt="`${row.guardName || worker.one} signature`"
|
|
50
|
+
class="route-details__signature"
|
|
51
|
+
contain
|
|
52
|
+
/>
|
|
53
|
+
<span v-else>-</span>
|
|
54
|
+
</dd>
|
|
55
|
+
</div>
|
|
56
|
+
<div>
|
|
57
|
+
<dt>Checked</dt>
|
|
58
|
+
<dd>{{ row.checked }}</dd>
|
|
59
|
+
</div>
|
|
60
|
+
<div>
|
|
61
|
+
<dt>Missed</dt>
|
|
62
|
+
<dd>{{ row.missed }}</dd>
|
|
63
|
+
</div>
|
|
64
|
+
<div>
|
|
65
|
+
<dt>Status</dt>
|
|
66
|
+
<dd :class="row.status === 'Completed' ? 'text-success' : 'text-error'">
|
|
67
|
+
{{ row.status }}
|
|
68
|
+
</dd>
|
|
69
|
+
</div>
|
|
70
|
+
</dl>
|
|
71
|
+
|
|
72
|
+
<PatrolRouteSummary :summary="row.detail.routeSummary" />
|
|
73
|
+
|
|
74
|
+
<div class="table-card table-card__shrink">
|
|
75
|
+
<div class="route-details__table-head">
|
|
76
|
+
<span class="screen-card-title">Checkpoints</span>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<v-table>
|
|
80
|
+
<thead>
|
|
81
|
+
<tr>
|
|
82
|
+
<th>Checkpoint</th>
|
|
83
|
+
<th>Start - End Time</th>
|
|
84
|
+
<th>Status</th>
|
|
85
|
+
<th>Remarks</th>
|
|
86
|
+
<th>Skip Photo</th>
|
|
87
|
+
</tr>
|
|
88
|
+
</thead>
|
|
89
|
+
<tbody>
|
|
90
|
+
<tr
|
|
91
|
+
v-for="(activity, index) in row.detail.activities"
|
|
92
|
+
:key="`${activity.id}-${index}`"
|
|
93
|
+
>
|
|
94
|
+
<td>{{ activity.checkpoint }}</td>
|
|
95
|
+
<td class="text-no-wrap">
|
|
96
|
+
<v-icon size="small" class="mr-1">mdi-clock-outline</v-icon>
|
|
97
|
+
{{ activity.startTime }}
|
|
98
|
+
<v-icon size="small" class="mx-2">mdi-arrow-right</v-icon>
|
|
99
|
+
<v-icon size="small" class="mr-1">mdi-clock-outline</v-icon>
|
|
100
|
+
{{ activity.endTime }}
|
|
101
|
+
</td>
|
|
102
|
+
<td>
|
|
103
|
+
<v-chip :color="statusColor(activity.status)" size="small" variant="flat">
|
|
104
|
+
{{ activity.status }}
|
|
105
|
+
<v-icon v-if="activity.status === 'Skipped'" size="small" class="ml-1">
|
|
106
|
+
mdi-flag
|
|
107
|
+
</v-icon>
|
|
108
|
+
</v-chip>
|
|
109
|
+
</td>
|
|
110
|
+
<td class="route-details__remarks">
|
|
111
|
+
<template v-if="activity.status === 'Skipped'">
|
|
112
|
+
{{ activity.remarks || "No remarks provided" }}
|
|
113
|
+
</template>
|
|
114
|
+
<template v-else>-</template>
|
|
115
|
+
</td>
|
|
116
|
+
<td>
|
|
117
|
+
<SkipPhotoThumb :activity="activity" :size="64" />
|
|
118
|
+
</td>
|
|
119
|
+
</tr>
|
|
120
|
+
</tbody>
|
|
121
|
+
</v-table>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div class="screen-modal__footer">
|
|
126
|
+
<v-btn variant="outlined" class="screen-btn-ghost" @click="close">Close</v-btn>
|
|
127
|
+
</div>
|
|
128
|
+
</v-card>
|
|
129
|
+
</v-dialog>
|
|
130
|
+
</template>
|
|
131
|
+
|
|
132
|
+
<script setup lang="ts">
|
|
133
|
+
import useWorkerTerm from "../../../composables/useWorkerTerm";
|
|
134
|
+
import type { NFCPatrolReportSummaryRow } from "../../../types/nfc-patrol-report";
|
|
135
|
+
import { formatReportDate } from "../../../utils/report-export";
|
|
136
|
+
import PatrolRouteSummary from "./PatrolRouteSummary.vue";
|
|
137
|
+
import SkipPhotoThumb from "./SkipPhotoThumb.vue";
|
|
138
|
+
|
|
139
|
+
defineProps<{
|
|
140
|
+
modelValue: boolean;
|
|
141
|
+
row: NFCPatrolReportSummaryRow | null;
|
|
142
|
+
}>();
|
|
143
|
+
|
|
144
|
+
const emit = defineEmits<{
|
|
145
|
+
"update:modelValue": [value: boolean];
|
|
146
|
+
}>();
|
|
147
|
+
|
|
148
|
+
const worker = useWorkerTerm();
|
|
149
|
+
const { getFileUrl } = useFile();
|
|
150
|
+
|
|
151
|
+
function statusColor(status: string) {
|
|
152
|
+
if (status === "Completed") return "success";
|
|
153
|
+
if (status === "Skipped") return "error";
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function close() {
|
|
158
|
+
emit("update:modelValue", false);
|
|
159
|
+
}
|
|
160
|
+
</script>
|
|
161
|
+
|
|
162
|
+
<style scoped>
|
|
163
|
+
.route-details__head {
|
|
164
|
+
display: flex;
|
|
165
|
+
align-items: flex-start;
|
|
166
|
+
justify-content: space-between;
|
|
167
|
+
gap: 12px;
|
|
168
|
+
padding: 20px 24px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.route-details__sub {
|
|
172
|
+
margin: 4px 0 0;
|
|
173
|
+
font-size: var(--fs-cell);
|
|
174
|
+
color: var(--text2);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.route-details__facts {
|
|
178
|
+
display: grid;
|
|
179
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
180
|
+
gap: 16px;
|
|
181
|
+
margin: 0 0 24px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.route-details__facts dt {
|
|
185
|
+
display: block;
|
|
186
|
+
margin-bottom: 4px;
|
|
187
|
+
font-size: var(--fs-section-label);
|
|
188
|
+
font-weight: var(--fw-section-label);
|
|
189
|
+
text-transform: uppercase;
|
|
190
|
+
letter-spacing: 0.04em;
|
|
191
|
+
color: var(--muted);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.route-details__facts dd {
|
|
195
|
+
margin: 0;
|
|
196
|
+
font-weight: var(--fw-cell);
|
|
197
|
+
color: var(--text);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.route-details__signature {
|
|
201
|
+
width: 80px;
|
|
202
|
+
height: 40px;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.route-details__table-head {
|
|
206
|
+
padding: 14px var(--cellpad-x);
|
|
207
|
+
border-bottom: 1px solid var(--border);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.route-details__remarks {
|
|
211
|
+
min-width: 160px;
|
|
212
|
+
white-space: pre-line;
|
|
213
|
+
}
|
|
214
|
+
</style>
|
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
The two type classes are Vuetify's Roboto scale. `--fs-card-title` and
|
|
10
10
|
`--text2` are the design's, and `--text2` measures 9.62:1 on a card where
|
|
11
11
|
`--medium-emphasis` is an opacity applied to the theme ink.
|
|
12
|
+
|
|
13
|
+
`data-pdf-hide` leaves the card out of the PDF, whose page header names the
|
|
14
|
+
site instead (`useReportPdf`).
|
|
12
15
|
-->
|
|
13
16
|
<template>
|
|
14
|
-
<div class="app-card text-center pa-8 mt-2">
|
|
17
|
+
<div class="app-card text-center pa-8 mt-2" data-pdf-hide>
|
|
15
18
|
<v-icon size="64" class="mb-4">mdi-office-building</v-icon>
|
|
16
19
|
<p class="location-header__name">{{ location.name }}</p>
|
|
17
20
|
<p class="location-header__address mb-0">{{ location.address }}</p>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Skipped checkpoints with their photo, time and remarks, drawn in a report's
|
|
3
|
+
PDF directly under the row they belong to: under the checkpoint's own row
|
|
4
|
+
on the Patrol report (`inline`: one photo beside its remarks), under the
|
|
5
|
+
patrol run's row on the Daily report (a grid, one card per checkpoint). A
|
|
6
|
+
photo is never pooled away from the checkpoint and run it was taken at.
|
|
7
|
+
|
|
8
|
+
Plain `<img>` with `crossorigin`, not `v-img`: html2canvas ignores
|
|
9
|
+
`object-fit`, so each photo keeps its own proportions inside its frame (both
|
|
10
|
+
sides `auto` under max caps, never stretched), and it loads at once rather
|
|
11
|
+
than when scrolled into view - the rows it sits in are hidden on screen.
|
|
12
|
+
-->
|
|
13
|
+
<template>
|
|
14
|
+
<div class="skip-details" :class="{ 'skip-details--inline': inline }">
|
|
15
|
+
<figure
|
|
16
|
+
v-for="(activity, index) in activities"
|
|
17
|
+
:key="`${activity.id}-${index}`"
|
|
18
|
+
class="skip-details__item"
|
|
19
|
+
>
|
|
20
|
+
<div class="skip-details__frame">
|
|
21
|
+
<img
|
|
22
|
+
v-if="activity.skippedPhotoFileId"
|
|
23
|
+
:src="getFileUrl(activity.skippedPhotoFileId)"
|
|
24
|
+
:alt="`Skip photo for ${activity.checkpoint}`"
|
|
25
|
+
crossorigin="anonymous"
|
|
26
|
+
/>
|
|
27
|
+
<span v-else class="skip-details__none">No photo attached</span>
|
|
28
|
+
</div>
|
|
29
|
+
<figcaption>
|
|
30
|
+
<span v-if="!inline" class="skip-details__checkpoint">{{ activity.checkpoint }}</span>
|
|
31
|
+
<span class="skip-details__meta">Skipped at {{ activity.endTime }}</span>
|
|
32
|
+
<span class="skip-details__label">Remarks</span>
|
|
33
|
+
<span class="skip-details__remarks">{{ activity.remarks || "No remarks provided" }}</span>
|
|
34
|
+
</figcaption>
|
|
35
|
+
</figure>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import type { NFCPatrolActivity } from "../../../types/nfc-patrol-report";
|
|
41
|
+
|
|
42
|
+
defineProps<{
|
|
43
|
+
activities: NFCPatrolActivity[];
|
|
44
|
+
/** Under the checkpoint's own row: one photo beside its remarks, no name. */
|
|
45
|
+
inline?: boolean;
|
|
46
|
+
}>();
|
|
47
|
+
|
|
48
|
+
const { getFileUrl } = useFile();
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<style scoped>
|
|
52
|
+
.skip-details {
|
|
53
|
+
display: grid;
|
|
54
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
55
|
+
gap: 16px;
|
|
56
|
+
font-family: var(--font-sans);
|
|
57
|
+
font-size: var(--fs-cell);
|
|
58
|
+
font-weight: 500;
|
|
59
|
+
color: var(--text);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.skip-details__item {
|
|
63
|
+
margin: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.skip-details__frame {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
height: 170px;
|
|
71
|
+
border: 1px solid var(--border);
|
|
72
|
+
border-radius: var(--r-inner);
|
|
73
|
+
background: var(--thead);
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Both sides `auto` under the two caps keeps the photo's own proportions -
|
|
78
|
+
html2canvas draws the box it is given. */
|
|
79
|
+
.skip-details__frame img {
|
|
80
|
+
display: block;
|
|
81
|
+
width: auto;
|
|
82
|
+
height: auto;
|
|
83
|
+
max-width: 100%;
|
|
84
|
+
max-height: 100%;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.skip-details__none {
|
|
88
|
+
color: var(--muted);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.skip-details__item figcaption {
|
|
92
|
+
display: flex;
|
|
93
|
+
flex-direction: column;
|
|
94
|
+
gap: 2px;
|
|
95
|
+
margin-top: 8px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.skip-details__checkpoint {
|
|
99
|
+
font-weight: 700;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.skip-details__meta {
|
|
103
|
+
color: var(--text2);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.skip-details__label {
|
|
107
|
+
margin-top: 4px;
|
|
108
|
+
font-size: var(--fs-section-label);
|
|
109
|
+
font-weight: var(--fw-section-label);
|
|
110
|
+
text-transform: uppercase;
|
|
111
|
+
letter-spacing: 0.04em;
|
|
112
|
+
color: var(--muted);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.skip-details__remarks {
|
|
116
|
+
white-space: pre-line;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.skip-details--inline {
|
|
120
|
+
display: block;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.skip-details--inline .skip-details__item {
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: flex-start;
|
|
126
|
+
gap: 16px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.skip-details--inline .skip-details__frame {
|
|
130
|
+
flex: 0 0 280px;
|
|
131
|
+
height: 200px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.skip-details--inline .skip-details__item figcaption {
|
|
135
|
+
margin-top: 0;
|
|
136
|
+
}
|
|
137
|
+
</style>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The photo a guard attached when skipping a checkpoint (`skippedPhotoFileId`
|
|
3
|
+
on the log's checkpoint): a thumbnail that opens the photo full size, with
|
|
4
|
+
the guard's remarks under it. Used by the Patrol Activity table and the
|
|
5
|
+
Patrol Route Details dialog.
|
|
6
|
+
|
|
7
|
+
The thumbnail's click stops at the button: in the Patrol Activity table the
|
|
8
|
+
whole skipped row already opens the remarks dialog.
|
|
9
|
+
|
|
10
|
+
The thumbnail is a cropped background, not a `v-img`: it crops the same in
|
|
11
|
+
html2canvas (which ignores `object-fit`) and loads without being scrolled
|
|
12
|
+
into view. The PDF itself leaves this column out and draws the photo full
|
|
13
|
+
size under its checkpoint (`SkipPhotoDetails`).
|
|
14
|
+
-->
|
|
15
|
+
<template>
|
|
16
|
+
<span class="skip-photo">
|
|
17
|
+
<button
|
|
18
|
+
v-if="activity.skippedPhotoFileId"
|
|
19
|
+
type="button"
|
|
20
|
+
class="skip-photo__thumb"
|
|
21
|
+
:aria-label="`View skip photo for ${activity.checkpoint}`"
|
|
22
|
+
@click.stop="open = true"
|
|
23
|
+
>
|
|
24
|
+
<span
|
|
25
|
+
class="skip-photo__img"
|
|
26
|
+
role="img"
|
|
27
|
+
:aria-label="`Skip photo for ${activity.checkpoint}`"
|
|
28
|
+
:style="{
|
|
29
|
+
width: `${size}px`,
|
|
30
|
+
height: `${size}px`,
|
|
31
|
+
backgroundImage: `url('${getFileUrl(activity.skippedPhotoFileId)}')`,
|
|
32
|
+
}"
|
|
33
|
+
/>
|
|
34
|
+
</button>
|
|
35
|
+
<span v-else-if="activity.status === 'Skipped'" class="screen-hint">No photo</span>
|
|
36
|
+
<span v-else>-</span>
|
|
37
|
+
|
|
38
|
+
<v-dialog v-if="activity.skippedPhotoFileId" v-model="open" max-width="820">
|
|
39
|
+
<v-card class="screen-modal">
|
|
40
|
+
<div class="skip-photo__head">
|
|
41
|
+
<v-card-title class="pa-0">Skip Photo: {{ activity.checkpoint }}</v-card-title>
|
|
42
|
+
<v-btn
|
|
43
|
+
class="screen-modal__close"
|
|
44
|
+
icon="mdi-close"
|
|
45
|
+
variant="flat"
|
|
46
|
+
aria-label="Close"
|
|
47
|
+
@click="open = false"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="screen-modal__body px-6 pb-6">
|
|
52
|
+
<v-img
|
|
53
|
+
:src="getFileUrl(activity.skippedPhotoFileId)"
|
|
54
|
+
:alt="`Skip photo for ${activity.checkpoint}`"
|
|
55
|
+
max-height="70vh"
|
|
56
|
+
contain
|
|
57
|
+
/>
|
|
58
|
+
<p class="skip-photo__remarks">
|
|
59
|
+
<span class="skip-photo__label">Remarks</span>
|
|
60
|
+
{{ activity.remarks || "No remarks provided" }}
|
|
61
|
+
</p>
|
|
62
|
+
<a :href="getFileUrl(activity.skippedPhotoFileId)" target="_blank" rel="noopener">
|
|
63
|
+
Open full size
|
|
64
|
+
</a>
|
|
65
|
+
</div>
|
|
66
|
+
</v-card>
|
|
67
|
+
</v-dialog>
|
|
68
|
+
</span>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script setup lang="ts">
|
|
72
|
+
import type { NFCPatrolActivity } from "../../../types/nfc-patrol-report";
|
|
73
|
+
|
|
74
|
+
withDefaults(
|
|
75
|
+
defineProps<{
|
|
76
|
+
activity: NFCPatrolActivity;
|
|
77
|
+
size?: number;
|
|
78
|
+
}>(),
|
|
79
|
+
{ size: 56 },
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const { getFileUrl } = useFile();
|
|
83
|
+
|
|
84
|
+
const open = ref(false);
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<style scoped>
|
|
88
|
+
.skip-photo__thumb {
|
|
89
|
+
display: block;
|
|
90
|
+
padding: 0;
|
|
91
|
+
border: 1px solid var(--border);
|
|
92
|
+
border-radius: var(--r-inner);
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
background: none;
|
|
95
|
+
cursor: zoom-in;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.skip-photo__img {
|
|
99
|
+
display: block;
|
|
100
|
+
background-size: cover;
|
|
101
|
+
background-position: center;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.skip-photo__thumb:focus-visible {
|
|
105
|
+
outline: 2px solid var(--accent);
|
|
106
|
+
outline-offset: 2px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.skip-photo__head {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: flex-start;
|
|
112
|
+
justify-content: space-between;
|
|
113
|
+
gap: 12px;
|
|
114
|
+
padding: 20px 24px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.skip-photo__remarks {
|
|
118
|
+
margin: 16px 0 8px;
|
|
119
|
+
color: var(--text);
|
|
120
|
+
white-space: pre-line;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.skip-photo__label {
|
|
124
|
+
display: block;
|
|
125
|
+
margin-bottom: 4px;
|
|
126
|
+
font-size: var(--fs-section-label);
|
|
127
|
+
font-weight: var(--fw-section-label);
|
|
128
|
+
text-transform: uppercase;
|
|
129
|
+
letter-spacing: 0.04em;
|
|
130
|
+
color: var(--muted);
|
|
131
|
+
}
|
|
132
|
+
</style>
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
swapping them to `StatusChip` would be a meaning change, not a restyle:
|
|
13
13
|
`utils/status.ts` does not name "Missed", so the shared map would render it
|
|
14
14
|
NEUTRAL and a missed patrol would stop being red. Raised, not decided here.
|
|
15
|
+
|
|
16
|
+
In the PDF each patrol run's skipped checkpoints - photo, time, remarks -
|
|
17
|
+
are drawn in a row of their own right under that run (`SkipPhotoDetails`),
|
|
18
|
+
so a day of many runs keeps every photo with the run it was taken on.
|
|
15
19
|
-->
|
|
16
20
|
<template>
|
|
17
21
|
<div class="table-card table-card__shrink mb-6">
|
|
@@ -33,7 +37,15 @@
|
|
|
33
37
|
</tr>
|
|
34
38
|
</thead>
|
|
35
39
|
<tbody>
|
|
36
|
-
<
|
|
40
|
+
<template v-for="row in rows" :key="row.id">
|
|
41
|
+
<tr
|
|
42
|
+
:class="{ 'summary-table__row--link': row.detail }"
|
|
43
|
+
:data-pdf-keep-with-next="skipped(row).length > 0 || undefined"
|
|
44
|
+
:tabindex="row.detail ? 0 : undefined"
|
|
45
|
+
:title="row.detail ? 'View patrol route details' : undefined"
|
|
46
|
+
@click="row.detail && emit('select', row)"
|
|
47
|
+
@keydown.enter="row.detail && emit('select', row)"
|
|
48
|
+
>
|
|
37
49
|
<td>{{ row.routeName }}</td>
|
|
38
50
|
<td>
|
|
39
51
|
<v-icon size="small" class="mr-1">mdi-clock-outline</v-icon>
|
|
@@ -47,6 +59,7 @@
|
|
|
47
59
|
:alt="`${row.guardName || worker.one} signature`"
|
|
48
60
|
class="guard-signature"
|
|
49
61
|
contain
|
|
62
|
+
eager
|
|
50
63
|
/>
|
|
51
64
|
<span v-else>-</span>
|
|
52
65
|
</td>
|
|
@@ -58,6 +71,12 @@
|
|
|
58
71
|
</span>
|
|
59
72
|
</td>
|
|
60
73
|
</tr>
|
|
74
|
+
<tr v-if="skipped(row).length" data-pdf-only>
|
|
75
|
+
<td colspan="7" class="summary-table__skips">
|
|
76
|
+
<SkipPhotoDetails :activities="skipped(row)" />
|
|
77
|
+
</td>
|
|
78
|
+
</tr>
|
|
79
|
+
</template>
|
|
61
80
|
</tbody>
|
|
62
81
|
</v-table>
|
|
63
82
|
</div>
|
|
@@ -70,6 +89,7 @@ import type {
|
|
|
70
89
|
NFCPatrolReportSummaryRow,
|
|
71
90
|
NFCPatrolReportSummaryStatus,
|
|
72
91
|
} from "../../../types/nfc-patrol-report";
|
|
92
|
+
import SkipPhotoDetails from "./SkipPhotoDetails.vue";
|
|
73
93
|
|
|
74
94
|
const worker = useWorkerTerm();
|
|
75
95
|
defineProps<{
|
|
@@ -77,8 +97,16 @@ defineProps<{
|
|
|
77
97
|
rows: NFCPatrolReportSummaryRow[];
|
|
78
98
|
}>();
|
|
79
99
|
|
|
100
|
+
const emit = defineEmits<{
|
|
101
|
+
select: [row: NFCPatrolReportSummaryRow];
|
|
102
|
+
}>();
|
|
103
|
+
|
|
80
104
|
const { getFileUrl } = useFile();
|
|
81
105
|
|
|
106
|
+
function skipped(row: NFCPatrolReportSummaryRow) {
|
|
107
|
+
return row.detail?.activities.filter((activity) => activity.status === "Skipped") ?? [];
|
|
108
|
+
}
|
|
109
|
+
|
|
82
110
|
function statusClass(status: NFCPatrolReportSummaryStatus) {
|
|
83
111
|
return status === "Completed" ? "text-success" : "text-error";
|
|
84
112
|
}
|
|
@@ -90,6 +118,19 @@ function statusClass(status: NFCPatrolReportSummaryStatus) {
|
|
|
90
118
|
border-bottom: 1px solid var(--border);
|
|
91
119
|
}
|
|
92
120
|
|
|
121
|
+
.summary-table__skips {
|
|
122
|
+
padding-top: 0 !important;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.summary-table__row--link {
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.summary-table__row--link:focus-visible {
|
|
130
|
+
outline: 2px solid var(--accent);
|
|
131
|
+
outline-offset: -2px;
|
|
132
|
+
}
|
|
133
|
+
|
|
93
134
|
.guard-signature {
|
|
94
135
|
width: 80px;
|
|
95
136
|
height: 40px;
|
|
@@ -63,6 +63,10 @@
|
|
|
63
63
|
</v-col>
|
|
64
64
|
<v-col cols="12" lg="6" class="mt-2 pl-lg-2">
|
|
65
65
|
<InputLabel title="Site Documents" class="d-block mb-1" />
|
|
66
|
+
<p class="text-caption text-medium-emphasis mb-2">
|
|
67
|
+
Every resident of this site can open these in the Service365 app (About this site) - for example
|
|
68
|
+
by-laws, house rules or the site plan. Documents for one unit only go on that unit instead.
|
|
69
|
+
</p>
|
|
66
70
|
<InputFileV2 v-model="siteDocuments" :multiple="true" accept="*/*" title="Upload documents"
|
|
67
71
|
:height="104" />
|
|
68
72
|
</v-col>
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
heading. Same handler, same permission gate.
|
|
7
7
|
-->
|
|
8
8
|
<v-col cols="12">
|
|
9
|
-
<PageHeader
|
|
9
|
+
<PageHeader
|
|
10
|
+
title="Vehicle Mgmt"
|
|
11
|
+
subtitle="The register of residents' and visitors' vehicles. Works with or without number-plate (LPR) cameras - cameras only add automatic plate matching at the gate."
|
|
12
|
+
>
|
|
10
13
|
<template #actions>
|
|
11
14
|
<AppButton v-if="qrStickersOn" icon="mdi-qrcode" variant="ghost" @click="openStickerDialog()">
|
|
12
15
|
Print QR stickers
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
} from "../types/nfc-patrol-report";
|
|
6
6
|
import useNFCPatrolLog from "./useNFCPatrolLog";
|
|
7
7
|
import useSite from "./useSite";
|
|
8
|
+
import { mapLogToSummaryRow } from "../utils/nfc-patrol-report";
|
|
8
9
|
|
|
9
10
|
interface NFCPatrolLogListResponse {
|
|
10
11
|
items?: Record<string, any>[];
|
|
@@ -25,23 +26,6 @@ function buildAddress(address?: Record<string, string>) {
|
|
|
25
26
|
return parts.length ? parts.join(", ") : undefined;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
function mapLogToSummaryRow(log: Record<string, any>, index: number): NFCPatrolSummaryRow {
|
|
29
|
-
const checkpoints: Record<string, any>[] = log.checkPoints ?? [];
|
|
30
|
-
const checked = checkpoints.filter((cp) => cp.status === "Completed").length;
|
|
31
|
-
const missed = checkpoints.filter((cp) => cp.status === "Skipped").length;
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
id: log._id,
|
|
35
|
-
routeName: `${index + 1}. ${log.route?.name ?? "-"}`,
|
|
36
|
-
securityCheckSchedule: log.route?.startTime ?? "-",
|
|
37
|
-
guardName: log.sign?.guardName,
|
|
38
|
-
guardSignatureFileId: log.sign?.guardSignatureFileId,
|
|
39
|
-
checked,
|
|
40
|
-
missed,
|
|
41
|
-
status: missed === 0 ? "Completed" : "Incomplete",
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
29
|
export function useNFCPatrolDailyReport(
|
|
46
30
|
filters: NFCPatrolReportFilters,
|
|
47
31
|
site: string,
|
|
@@ -94,7 +78,9 @@ export function useNFCPatrolDailyReport(
|
|
|
94
78
|
address: buildAddress(siteInfo.address),
|
|
95
79
|
},
|
|
96
80
|
title: `Patrol Route Summary: ${routeName}`,
|
|
97
|
-
rows: logs.map(
|
|
81
|
+
rows: logs.map((log, index) =>
|
|
82
|
+
mapLogToSummaryRow(log, index, (siteInfo as { timezone?: string }).timezone),
|
|
83
|
+
),
|
|
98
84
|
};
|
|
99
85
|
} else {
|
|
100
86
|
report.value = null;
|