@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.
Files changed (31) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/assets/css/screens.css +11 -0
  3. package/components/BulletinBoardForm.vue +112 -0
  4. package/components/BulletinBoardView.vue +2 -0
  5. package/components/BulletinEventRsvp.vue +126 -0
  6. package/components/Nfc/NFCPatrolReportMain.vue +61 -72
  7. package/components/Nfc/NFCPatrolRouteMain.vue +21 -31
  8. package/components/Nfc/NFCRouteScheduleTimes.vue +83 -0
  9. package/components/Nfc/PatrolReport/DailyReportTab.vue +24 -3
  10. package/components/Nfc/PatrolReport/MonthlyReportTab.vue +32 -3
  11. package/components/Nfc/PatrolReport/MonthlyReportTable.vue +29 -1
  12. package/components/Nfc/PatrolReport/PatrolActivityTable.vue +24 -2
  13. package/components/Nfc/PatrolReport/PatrolMonthDialog.vue +195 -0
  14. package/components/Nfc/PatrolReport/PatrolReportTab.vue +11 -2
  15. package/components/Nfc/PatrolReport/PatrolRouteDetailsDialog.vue +214 -0
  16. package/components/Nfc/PatrolReport/ReportLocationHeader.vue +4 -1
  17. package/components/Nfc/PatrolReport/SkipPhotoDetails.vue +137 -0
  18. package/components/Nfc/PatrolReport/SkipPhotoThumb.vue +132 -0
  19. package/components/Nfc/PatrolReport/SummaryReportTable.vue +42 -1
  20. package/components/SiteSettings.vue +4 -0
  21. package/components/VehicleManagement.vue +4 -1
  22. package/composables/useNFCPatrolDailyReport.ts +4 -18
  23. package/composables/useNFCPatrolMonthlyReport.ts +34 -5
  24. package/composables/useNFCPatrolReport.ts +5 -74
  25. package/composables/useReportPdf.ts +220 -0
  26. package/package.json +1 -1
  27. package/types/bulletin-board.d.ts +11 -1
  28. package/types/nfc-patrol-report.ts +14 -0
  29. package/utils/nfc-patrol-report.ts +239 -1
  30. package/utils/nfc-patrol-schedule.ts +73 -0
  31. 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
- <tr v-for="row in rows" :key="row.id">
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 title="Vehicle Mgmt">
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(mapLogToSummaryRow),
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;