@7365admin1/layer-common 4.97.1-staging.581 → 4.97.1-staging.583
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/assets/css/primitives.css +54 -11
- package/assets/css/screens.css +3 -3
- package/assets/css/tokens.css +17 -12
- package/components/AnprCameraBadge.vue +1 -1
- package/components/AreaMain.vue +11 -5
- package/components/BillingMain.vue +12 -53
- package/components/BulletinBoardManagement.vue +9 -13
- package/components/CameraWall.vue +7 -7
- package/components/CameraWallTile.vue +8 -8
- package/components/Chat/ListCard.vue +1 -1
- package/components/CleaningScheduleMain.vue +56 -20
- package/components/ClientMain.vue +1 -1
- package/components/ClientSubscriptionForm.vue +1 -1
- package/components/DashboardMain.vue +13 -13
- package/components/HidAccessLogDashboard.vue +3 -3
- package/components/HidIntercomManagement.vue +14 -14
- package/components/HidQrCodeConfiguration.vue +3 -3
- package/components/HidReaderManagement.vue +1 -1
- package/components/InventoryPhotoThumbs.vue +1 -1
- package/components/InvitationMain.vue +8 -0
- package/components/Layout/Header.vue +2 -2
- package/components/Layout/NavigationDrawer.vue +1 -1
- package/components/MemberMain.vue +8 -0
- package/components/Nfc/NFCRouteScheduleTimes.vue +1 -1
- package/components/PlateNumberDisplay.vue +1 -1
- package/components/RolePermissionMain.vue +13 -0
- package/components/ScheduleAreaMain.vue +6 -9
- package/components/ScheduleGuide.vue +101 -0
- package/components/ScheduleTaskForm.vue +88 -7
- package/components/ScheduleTaskMain.vue +163 -56
- package/components/SiteSettings.vue +5 -5
- package/components/StatusChip.vue +1 -1
- package/components/VisitorsReportPreview.vue +2 -2
- package/composables/useKey.ts +20 -16
- package/package.json +1 -1
- package/pages/unauthorized.vue +4 -3
- package/utils/breadcrumb.ts +7 -0
- package/utils/nav-sections.ts +2 -0
- package/utils/schedule-task.ts +106 -23
|
@@ -59,8 +59,10 @@
|
|
|
59
59
|
variant="outlined"
|
|
60
60
|
density="comfortable"
|
|
61
61
|
class="filter-field mb-4"
|
|
62
|
-
placeholder="For example:
|
|
62
|
+
placeholder="For example: Lunch toilet check"
|
|
63
63
|
:error-messages="fieldError('title')"
|
|
64
|
+
hint="Staff see this name on their phone, on the extra round."
|
|
65
|
+
persistent-hint
|
|
64
66
|
hide-details="auto"
|
|
65
67
|
/>
|
|
66
68
|
<InputLabel for="description" title="Instructions" />
|
|
@@ -72,6 +74,8 @@
|
|
|
72
74
|
class="filter-field"
|
|
73
75
|
rows="3"
|
|
74
76
|
placeholder="Optional. Anything staff should know for this visit."
|
|
77
|
+
hint="Optional. For example: refill soap and tissue as well."
|
|
78
|
+
persistent-hint
|
|
75
79
|
hide-details="auto"
|
|
76
80
|
/>
|
|
77
81
|
</div>
|
|
@@ -81,8 +85,9 @@
|
|
|
81
85
|
<v-stepper-window-item :value="2">
|
|
82
86
|
<div class="st-step">
|
|
83
87
|
<p class="st-step__help">
|
|
84
|
-
Pick the areas that get the extra visit. Every unit in
|
|
85
|
-
area
|
|
88
|
+
Pick the areas that get the extra visit. Every unit in a
|
|
89
|
+
picked area gets one more tick box on the Daily Checklist
|
|
90
|
+
for this visit.
|
|
86
91
|
</p>
|
|
87
92
|
<InputLabel for="selected-areas" title="Areas" required />
|
|
88
93
|
<v-select
|
|
@@ -165,7 +170,8 @@
|
|
|
165
170
|
<div class="st-step">
|
|
166
171
|
<p class="st-step__help">
|
|
167
172
|
Choose the time, and whether it happens once or repeats.
|
|
168
|
-
Times are Singapore time.
|
|
173
|
+
Times are Singapore time. The extra round appears on staff's
|
|
174
|
+
checklist at this time.
|
|
169
175
|
</p>
|
|
170
176
|
<v-row dense>
|
|
171
177
|
<v-col cols="12" sm="6">
|
|
@@ -193,6 +199,8 @@
|
|
|
193
199
|
variant="outlined"
|
|
194
200
|
density="comfortable"
|
|
195
201
|
class="filter-field"
|
|
202
|
+
:hint="REPEAT_HINTS[repeat]"
|
|
203
|
+
persistent-hint
|
|
196
204
|
hide-details="auto"
|
|
197
205
|
/>
|
|
198
206
|
</v-col>
|
|
@@ -200,7 +208,7 @@
|
|
|
200
208
|
|
|
201
209
|
<!-- Once: pick the day(s) -->
|
|
202
210
|
<div v-if="repeat === 'none'" class="mt-4">
|
|
203
|
-
<InputLabel title="Day" required />
|
|
211
|
+
<InputLabel title="Day(s)" required />
|
|
204
212
|
<div class="st-panel">
|
|
205
213
|
<v-date-picker
|
|
206
214
|
v-model="pickerDates"
|
|
@@ -213,7 +221,8 @@
|
|
|
213
221
|
class="ma-0"
|
|
214
222
|
/>
|
|
215
223
|
<div class="st-step__help mt-2">
|
|
216
|
-
{{ selectedDates.length }} day(s) selected.
|
|
224
|
+
{{ selectedDates.length }} day(s) selected. Click a day
|
|
225
|
+
again to remove it.
|
|
217
226
|
</div>
|
|
218
227
|
</div>
|
|
219
228
|
<div v-if="fieldError('dates').length" class="st-error">
|
|
@@ -304,6 +313,32 @@
|
|
|
304
313
|
<div class="st-summary__label">This will:</div>
|
|
305
314
|
<div class="st-summary__text">{{ summary }}</div>
|
|
306
315
|
</div>
|
|
316
|
+
|
|
317
|
+
<!-- What staff will see: the same card shape as the mobile checklist. -->
|
|
318
|
+
<div class="st-summary__label mt-4">
|
|
319
|
+
What staff will see on their phone:
|
|
320
|
+
</div>
|
|
321
|
+
<div class="st-preview">
|
|
322
|
+
<div class="st-preview__head">
|
|
323
|
+
<v-icon size="18" class="mr-1">mdi-clock-outline</v-icon>
|
|
324
|
+
{{ previewTime }} · Extra visit
|
|
325
|
+
</div>
|
|
326
|
+
<div class="st-preview__title">
|
|
327
|
+
{{ subject.trim() || "(your title)" }}
|
|
328
|
+
</div>
|
|
329
|
+
<div v-if="description.trim()" class="st-preview__desc">
|
|
330
|
+
{{ description.trim() }}
|
|
331
|
+
</div>
|
|
332
|
+
<div
|
|
333
|
+
v-for="area in selectedAreas"
|
|
334
|
+
:key="areaKey(area)"
|
|
335
|
+
class="st-preview__row"
|
|
336
|
+
>
|
|
337
|
+
<v-icon size="18" class="mr-1">mdi-checkbox-blank-outline</v-icon>
|
|
338
|
+
{{ area.name }} -
|
|
339
|
+
{{ unitCountOf(area) }} unit{{ unitCountOf(area) === 1 ? "" : "s" }} to tick
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
307
342
|
<div v-if="fieldError('never').length" class="st-error mt-2">
|
|
308
343
|
{{ fieldError("never")[0] }}
|
|
309
344
|
</div>
|
|
@@ -372,6 +407,7 @@ import {
|
|
|
372
407
|
REPEAT_OPTIONS,
|
|
373
408
|
WEEK_DAYS,
|
|
374
409
|
buildSummary,
|
|
410
|
+
fmtTime,
|
|
375
411
|
sgtToday,
|
|
376
412
|
validateSchedule,
|
|
377
413
|
type TRepeat,
|
|
@@ -395,6 +431,11 @@ const props = defineProps({
|
|
|
395
431
|
type: String,
|
|
396
432
|
default: "",
|
|
397
433
|
},
|
|
434
|
+
/** The step the dialog opens on (3 = When, used by "Extend or repeat"). */
|
|
435
|
+
startStep: {
|
|
436
|
+
type: Number,
|
|
437
|
+
default: 1,
|
|
438
|
+
},
|
|
398
439
|
});
|
|
399
440
|
|
|
400
441
|
const show = defineModel("show", { type: Boolean, default: false });
|
|
@@ -476,6 +517,15 @@ const formState = computed(() => ({
|
|
|
476
517
|
const errors = computed<TScheduleErrors>(() => validateSchedule(formState.value));
|
|
477
518
|
const summary = computed(() => buildSummary(formState.value, totalUnits.value));
|
|
478
519
|
|
|
520
|
+
const REPEAT_HINTS: Record<TRepeat, string> = {
|
|
521
|
+
none: "Only on the day(s) you pick below.",
|
|
522
|
+
daily: "Every day, weekends included.",
|
|
523
|
+
weekdays: "Monday to Friday.",
|
|
524
|
+
weekly: "On the weekdays you pick below.",
|
|
525
|
+
monthly: "Once a month, on the start date's day.",
|
|
526
|
+
};
|
|
527
|
+
const previewTime = computed(() => fmtTime(scheduleTime.value) || "--");
|
|
528
|
+
|
|
479
529
|
const STEP_FIELDS: Record<number, (keyof TScheduleErrors)[]> = {
|
|
480
530
|
1: ["title"],
|
|
481
531
|
2: ["areas"],
|
|
@@ -694,6 +744,7 @@ const close = () => {
|
|
|
694
744
|
watch(
|
|
695
745
|
[show, () => props.mode, () => props.taskData],
|
|
696
746
|
async ([isOpen, mode]) => {
|
|
747
|
+
if (isOpen) step.value = props.startStep || 1;
|
|
697
748
|
if (isOpen && mode === "edit") {
|
|
698
749
|
await loadTaskData();
|
|
699
750
|
} else if (!isOpen) {
|
|
@@ -710,7 +761,7 @@ watch(
|
|
|
710
761
|
}
|
|
711
762
|
.st-step__help {
|
|
712
763
|
margin: 0 0 12px;
|
|
713
|
-
font-size: var(--fs-
|
|
764
|
+
font-size: var(--fs-cell);
|
|
714
765
|
font-weight: 600;
|
|
715
766
|
color: var(--text2);
|
|
716
767
|
}
|
|
@@ -748,6 +799,36 @@ watch(
|
|
|
748
799
|
font-size: 15px;
|
|
749
800
|
font-weight: 600;
|
|
750
801
|
}
|
|
802
|
+
.st-preview {
|
|
803
|
+
margin-top: 6px;
|
|
804
|
+
padding: 14px 16px;
|
|
805
|
+
border: 1px solid var(--border);
|
|
806
|
+
border-left: 4px solid rgb(var(--v-theme-primary));
|
|
807
|
+
border-radius: var(--r-inner);
|
|
808
|
+
background: var(--card);
|
|
809
|
+
font-size: var(--fs-cell);
|
|
810
|
+
}
|
|
811
|
+
.st-preview__head {
|
|
812
|
+
display: flex;
|
|
813
|
+
align-items: center;
|
|
814
|
+
font-size: var(--fs-chip-lg);
|
|
815
|
+
font-weight: 700;
|
|
816
|
+
color: var(--text2);
|
|
817
|
+
}
|
|
818
|
+
.st-preview__title {
|
|
819
|
+
margin: 4px 0;
|
|
820
|
+
font-size: var(--fs-card-title);
|
|
821
|
+
font-weight: 800;
|
|
822
|
+
}
|
|
823
|
+
.st-preview__desc {
|
|
824
|
+
margin-bottom: 6px;
|
|
825
|
+
color: var(--text2);
|
|
826
|
+
}
|
|
827
|
+
.st-preview__row {
|
|
828
|
+
display: flex;
|
|
829
|
+
align-items: center;
|
|
830
|
+
padding: 4px 0;
|
|
831
|
+
}
|
|
751
832
|
.st-stepper,
|
|
752
833
|
.st-stepper__header {
|
|
753
834
|
background: transparent;
|
|
@@ -5,10 +5,52 @@
|
|
|
5
5
|
:org-id="orgId"
|
|
6
6
|
:site="site"
|
|
7
7
|
:service-type="serviceType"
|
|
8
|
-
text="An extra visit adds one more round to
|
|
9
|
-
|
|
8
|
+
text="An extra visit adds one more round to the Daily Checklist at a time you choose - once, or on a repeat."
|
|
9
|
+
how-title="How extra visits work"
|
|
10
|
+
how-key="extra-visits"
|
|
11
|
+
>
|
|
12
|
+
<ol class="sg-steps">
|
|
13
|
+
<li>
|
|
14
|
+
Every night the app makes the <strong>Daily Checklist</strong>
|
|
15
|
+
for every area on this site. You do not need to do anything for
|
|
16
|
+
that.
|
|
17
|
+
</li>
|
|
18
|
+
<li>
|
|
19
|
+
An <strong>extra visit</strong> adds one more round to that
|
|
20
|
+
checklist, in the areas you pick, at the time you set - once, or
|
|
21
|
+
repeating (every day, weekdays, chosen days, or monthly).
|
|
22
|
+
</li>
|
|
23
|
+
<li>
|
|
24
|
+
At that time, staff see the extra round in their mobile app's
|
|
25
|
+
daily checklist (with a push notification) and tick each unit as
|
|
26
|
+
they finish it.
|
|
27
|
+
</li>
|
|
28
|
+
<li>
|
|
29
|
+
Here and in the Daily Checklist you see how it went:
|
|
30
|
+
<strong>Done</strong>, <strong>In progress</strong>,
|
|
31
|
+
<strong>Needs redo</strong> or <strong>Missed</strong>.
|
|
32
|
+
</li>
|
|
33
|
+
</ol>
|
|
34
|
+
<div class="sg-example">
|
|
35
|
+
<strong>Example:</strong> the lobby toilets get busy at lunch. Add
|
|
36
|
+
an extra visit called "Lunch toilet check", pick the area "Lobby
|
|
37
|
+
Toilets", set 2:00 pm, every weekday. From the next weekday, staff
|
|
38
|
+
get one more toilet round at 2:00 pm, on top of their normal ones.
|
|
39
|
+
</div>
|
|
40
|
+
<div class="sg-h">What the words mean</div>
|
|
41
|
+
<dl class="sg-legend">
|
|
42
|
+
<template v-for="h in STATUS_HELP" :key="h.word">
|
|
43
|
+
<dt><StatusChip :status="h.word" :label="h.word" /></dt>
|
|
44
|
+
<dd>{{ h.meaning }}</dd>
|
|
45
|
+
</template>
|
|
46
|
+
<template v-for="h in RESULT_HELP" :key="'r' + h.word">
|
|
47
|
+
<dt><StatusChip :status="h.word" :label="h.word" /></dt>
|
|
48
|
+
<dd>Last visit: {{ h.meaning }}</dd>
|
|
49
|
+
</template>
|
|
50
|
+
</dl>
|
|
51
|
+
</ScheduleGuide>
|
|
10
52
|
<TableMain
|
|
11
|
-
:title="'Extra Visits
|
|
53
|
+
:title="'Extra Visits'"
|
|
12
54
|
:items="items"
|
|
13
55
|
:headers="headers"
|
|
14
56
|
:loading="loading"
|
|
@@ -26,9 +68,14 @@
|
|
|
26
68
|
<div class="st-empty">
|
|
27
69
|
<strong>No extra visits yet.</strong>
|
|
28
70
|
<span>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
71
|
+
Your Daily Checklist already covers the normal rounds. Add an
|
|
72
|
+
extra visit when an area needs one more round at a set time -
|
|
73
|
+
for example "Lunch toilet check, Lobby Toilets, 2:00 pm every
|
|
74
|
+
weekday".
|
|
75
|
+
</span>
|
|
76
|
+
<span v-if="canCreateScheduleTask">
|
|
77
|
+
Press <strong>Add Extra Visit</strong> above to start. Your
|
|
78
|
+
Areas and Units must be set up first.
|
|
32
79
|
</span>
|
|
33
80
|
</div>
|
|
34
81
|
</template>
|
|
@@ -61,27 +108,6 @@
|
|
|
61
108
|
</v-row>
|
|
62
109
|
</template>
|
|
63
110
|
|
|
64
|
-
<template #item.areas="{ value }">
|
|
65
|
-
<div v-if="value && value.length > 0">
|
|
66
|
-
<StatusChip
|
|
67
|
-
v-for="(area, idx) in value.slice(0, 2)"
|
|
68
|
-
:key="idx"
|
|
69
|
-
class="mr-1 mb-1"
|
|
70
|
-
:dot="false"
|
|
71
|
-
tone="neutral"
|
|
72
|
-
:label="area.name"
|
|
73
|
-
/>
|
|
74
|
-
<StatusChip
|
|
75
|
-
v-if="value.length > 2"
|
|
76
|
-
class="mb-1"
|
|
77
|
-
:dot="false"
|
|
78
|
-
tone="neutral"
|
|
79
|
-
:label="`+${value.length - 2} more`"
|
|
80
|
-
/>
|
|
81
|
-
</div>
|
|
82
|
-
<span v-else>N/A</span>
|
|
83
|
-
</template>
|
|
84
|
-
|
|
85
111
|
<template #item.when="{ item }">
|
|
86
112
|
<span class="st-when">{{ item.whenText }}</span>
|
|
87
113
|
</template>
|
|
@@ -91,24 +117,34 @@
|
|
|
91
117
|
</template>
|
|
92
118
|
|
|
93
119
|
<template #item.lastRun="{ item }">
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
v-else
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
120
|
+
<div class="st-cell">
|
|
121
|
+
<StatusChip
|
|
122
|
+
v-if="item.lastRun && item.lastRunWord"
|
|
123
|
+
:status="item.lastRunWord"
|
|
124
|
+
:label="item.lastRunText"
|
|
125
|
+
/>
|
|
126
|
+
<span v-else>{{ item.lastRunText }}</span>
|
|
127
|
+
<a
|
|
128
|
+
v-if="item.lastRun"
|
|
129
|
+
href="#"
|
|
130
|
+
class="st-link"
|
|
131
|
+
@click.prevent.stop="openChecklist(item)"
|
|
132
|
+
>
|
|
133
|
+
See that day's checklist
|
|
134
|
+
</a>
|
|
135
|
+
</div>
|
|
105
136
|
</template>
|
|
106
137
|
|
|
107
138
|
<template #item.status="{ item }">
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
139
|
+
<div class="st-cell">
|
|
140
|
+
<StatusChip
|
|
141
|
+
:status="item.stateLabel"
|
|
142
|
+
:label="item.stateLabel"
|
|
143
|
+
/>
|
|
144
|
+
<span v-if="item.stateKey === 'ended'" class="st-note">
|
|
145
|
+
{{ item.finishedText }}
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
112
148
|
</template>
|
|
113
149
|
|
|
114
150
|
<template #item.actions="{ item }">
|
|
@@ -123,6 +159,17 @@
|
|
|
123
159
|
>
|
|
124
160
|
Edit
|
|
125
161
|
</v-btn>
|
|
162
|
+
<v-btn
|
|
163
|
+
v-if="canUpdateScheduleTask && item.stateKey === 'ended'"
|
|
164
|
+
size="small"
|
|
165
|
+
variant="tonal"
|
|
166
|
+
color="primary"
|
|
167
|
+
class="text-none"
|
|
168
|
+
prepend-icon="mdi-calendar-refresh"
|
|
169
|
+
@click.stop="onExtendRow(item)"
|
|
170
|
+
>
|
|
171
|
+
Extend or repeat
|
|
172
|
+
</v-btn>
|
|
126
173
|
<v-btn
|
|
127
174
|
v-if="canUpdateScheduleTask && item.stateKey !== 'ended'"
|
|
128
175
|
size="small"
|
|
@@ -155,6 +202,7 @@
|
|
|
155
202
|
v-model:show="showForm"
|
|
156
203
|
:mode="formMode"
|
|
157
204
|
:taskData="selectedTask"
|
|
205
|
+
:start-step="formStartStep"
|
|
158
206
|
:site="props.site"
|
|
159
207
|
:serviceType="props.serviceType"
|
|
160
208
|
@refresh="getTaskRefresh"
|
|
@@ -194,7 +242,7 @@
|
|
|
194
242
|
|
|
195
243
|
<v-row no-gutters class="mb-2">
|
|
196
244
|
<v-col cols="5" class="text-subtitle-2 font-weight-bold">
|
|
197
|
-
Next
|
|
245
|
+
Next visit:
|
|
198
246
|
</v-col>
|
|
199
247
|
<v-col cols="7" class="text-subtitle-2">
|
|
200
248
|
{{ selectedNext || "-" }}
|
|
@@ -203,10 +251,18 @@
|
|
|
203
251
|
|
|
204
252
|
<v-row no-gutters class="mb-2">
|
|
205
253
|
<v-col cols="5" class="text-subtitle-2 font-weight-bold">
|
|
206
|
-
Last
|
|
254
|
+
Last visit:
|
|
207
255
|
</v-col>
|
|
208
256
|
<v-col cols="7" class="text-subtitle-2">
|
|
209
257
|
{{ lastRunLabel(selectedTask) }}
|
|
258
|
+
<a
|
|
259
|
+
v-if="selectedTask.lastRun"
|
|
260
|
+
href="#"
|
|
261
|
+
class="st-link d-block"
|
|
262
|
+
@click.prevent="openChecklist(selectedTask)"
|
|
263
|
+
>
|
|
264
|
+
See that day's checklist
|
|
265
|
+
</a>
|
|
210
266
|
</v-col>
|
|
211
267
|
</v-row>
|
|
212
268
|
|
|
@@ -319,7 +375,7 @@
|
|
|
319
375
|
</div>
|
|
320
376
|
<span v-else class="text-caption text-disabled"
|
|
321
377
|
>No units in this area, so it is skipped when
|
|
322
|
-
this visit
|
|
378
|
+
this visit is added.</span
|
|
323
379
|
>
|
|
324
380
|
</v-card-text>
|
|
325
381
|
</div>
|
|
@@ -348,6 +404,12 @@
|
|
|
348
404
|
:status="stateOf(selectedTask).label"
|
|
349
405
|
:label="stateOf(selectedTask).label"
|
|
350
406
|
/>
|
|
407
|
+
<div
|
|
408
|
+
v-if="stateOf(selectedTask).key === 'ended'"
|
|
409
|
+
class="st-note mt-1"
|
|
410
|
+
>
|
|
411
|
+
{{ finishedReason(selectedTask as any) }}
|
|
412
|
+
</div>
|
|
351
413
|
</v-col>
|
|
352
414
|
</v-row>
|
|
353
415
|
|
|
@@ -383,8 +445,8 @@
|
|
|
383
445
|
</v-card-title>
|
|
384
446
|
|
|
385
447
|
<v-card-text class="screen-modal__body">
|
|
386
|
-
This stops all future
|
|
387
|
-
happened
|
|
448
|
+
This stops all future extra visits from it. Visits that already
|
|
449
|
+
happened stay on their Daily Checklists. Delete it?
|
|
388
450
|
</v-card-text>
|
|
389
451
|
|
|
390
452
|
<div class="screen-modal__footer">
|
|
@@ -422,8 +484,12 @@ import useScheduleTask from "../composables/useScheduleTask";
|
|
|
422
484
|
import useUtils from "../composables/useUtils";
|
|
423
485
|
import useAreas from "../composables/useAreas";
|
|
424
486
|
import {
|
|
487
|
+
RESULT_HELP,
|
|
488
|
+
STATUS_HELP,
|
|
425
489
|
STATUS_LABELS,
|
|
490
|
+
describeRow,
|
|
426
491
|
describeWhen,
|
|
492
|
+
finishedReason,
|
|
427
493
|
fmtMoment,
|
|
428
494
|
lastRunLabel,
|
|
429
495
|
scheduleNameFor,
|
|
@@ -458,7 +524,16 @@ const canViewScheduleTaskDetails = computed(
|
|
|
458
524
|
() => props.canViewScheduleTaskDetails
|
|
459
525
|
);
|
|
460
526
|
|
|
461
|
-
const
|
|
527
|
+
const checklistRoute = scheduleNameFor(props.serviceType).route;
|
|
528
|
+
|
|
529
|
+
/** Opens the Daily Checklist filtered to the day this extra visit was last added. */
|
|
530
|
+
function openChecklist(row: Record<string, any>) {
|
|
531
|
+
const day = String(row?.lastRun || "").slice(0, 10);
|
|
532
|
+
navigateTo({
|
|
533
|
+
path: `/${props.orgId}/${props.site}/${checklistRoute}`,
|
|
534
|
+
query: /^\d{4}-\d{2}-\d{2}$/.test(day) ? { date: day } : {},
|
|
535
|
+
});
|
|
536
|
+
}
|
|
462
537
|
|
|
463
538
|
const page = ref(1);
|
|
464
539
|
const pages = ref(0);
|
|
@@ -468,6 +543,7 @@ const searchInput = ref("");
|
|
|
468
543
|
const showForm = ref(false);
|
|
469
544
|
const selectedTask = ref<Record<string, any>>({});
|
|
470
545
|
const formMode = ref<"add" | "edit">("add");
|
|
546
|
+
const formStartStep = ref(1);
|
|
471
547
|
const submitting = ref(false);
|
|
472
548
|
const dialogShowMoreActions = ref(false);
|
|
473
549
|
const dialogDeleteTask = ref(false);
|
|
@@ -517,11 +593,10 @@ watch(dialogShowMoreActions, async (isOpen) => {
|
|
|
517
593
|
const items = ref<any[]>([]);
|
|
518
594
|
|
|
519
595
|
const headers = [
|
|
520
|
-
{ title: "
|
|
521
|
-
{ title: "
|
|
522
|
-
{ title: "
|
|
523
|
-
{ title: "
|
|
524
|
-
{ title: "Last run", value: "lastRun", sortable: false },
|
|
596
|
+
{ title: "Extra visit", value: "title" },
|
|
597
|
+
{ title: "What will happen", value: "when", sortable: false },
|
|
598
|
+
{ title: "Next visit", value: "nextRun", sortable: false },
|
|
599
|
+
{ title: "Last visit", value: "lastRun", sortable: false },
|
|
525
600
|
{ title: "Status", value: "status", sortable: false },
|
|
526
601
|
{ title: "", value: "actions", sortable: false, align: "end" as const },
|
|
527
602
|
];
|
|
@@ -566,7 +641,9 @@ watchEffect(() => {
|
|
|
566
641
|
const st = stateOf(row);
|
|
567
642
|
return {
|
|
568
643
|
...row,
|
|
569
|
-
whenText:
|
|
644
|
+
whenText: describeRow(row),
|
|
645
|
+
finishedText: finishedReason(row),
|
|
646
|
+
lastRunWord: ({ done: "Done", missed: "Missed", pending: "In progress" } as Record<string, string>)[row.lastRunStatus] || "",
|
|
570
647
|
nextRunText: fmtMoment(row.nextRun),
|
|
571
648
|
lastRunText: lastRunLabel(row),
|
|
572
649
|
stateKey: st.key,
|
|
@@ -590,6 +667,7 @@ watch(
|
|
|
590
667
|
|
|
591
668
|
function openFormDialog() {
|
|
592
669
|
formMode.value = "add";
|
|
670
|
+
formStartStep.value = 1;
|
|
593
671
|
selectedTask.value = {};
|
|
594
672
|
showForm.value = true;
|
|
595
673
|
}
|
|
@@ -597,6 +675,15 @@ function openFormDialog() {
|
|
|
597
675
|
function onEditRow(item: Record<string, any>) {
|
|
598
676
|
selectedTask.value = item;
|
|
599
677
|
formMode.value = "edit";
|
|
678
|
+
formStartStep.value = 1;
|
|
679
|
+
showForm.value = true;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// A finished extra visit: open its "When" step so a new end date or repeat is one change away.
|
|
683
|
+
function onExtendRow(item: Record<string, any>) {
|
|
684
|
+
selectedTask.value = item;
|
|
685
|
+
formMode.value = "edit";
|
|
686
|
+
formStartStep.value = 3;
|
|
600
687
|
showForm.value = true;
|
|
601
688
|
}
|
|
602
689
|
|
|
@@ -612,8 +699,8 @@ async function togglePause(item: Record<string, any>) {
|
|
|
612
699
|
await setScheduleTaskPaused(item._id, !resuming);
|
|
613
700
|
showMessage(
|
|
614
701
|
resuming
|
|
615
|
-
? "Resumed.
|
|
616
|
-
: "Paused.
|
|
702
|
+
? "Resumed. Extra visits are added again from its next date."
|
|
703
|
+
: "Paused. No extra visits are added until you resume it.",
|
|
617
704
|
"success",
|
|
618
705
|
);
|
|
619
706
|
await getTaskRefresh();
|
|
@@ -649,6 +736,7 @@ async function onRowClick(data: any) {
|
|
|
649
736
|
const onEditFromMoreAction = async () => {
|
|
650
737
|
dialogShowMoreActions.value = false;
|
|
651
738
|
formMode.value = "edit";
|
|
739
|
+
formStartStep.value = stateOf(selectedTask.value).key === "ended" ? 3 : 1;
|
|
652
740
|
showForm.value = true;
|
|
653
741
|
};
|
|
654
742
|
|
|
@@ -693,8 +781,27 @@ async function _deleteScheduleTask() {
|
|
|
693
781
|
color: var(--text2);
|
|
694
782
|
}
|
|
695
783
|
.st-when {
|
|
784
|
+
display: inline-block;
|
|
785
|
+
min-width: 220px;
|
|
696
786
|
white-space: normal;
|
|
697
787
|
}
|
|
788
|
+
.st-cell {
|
|
789
|
+
display: flex;
|
|
790
|
+
flex-direction: column;
|
|
791
|
+
align-items: flex-start;
|
|
792
|
+
gap: 4px;
|
|
793
|
+
}
|
|
794
|
+
.st-note {
|
|
795
|
+
font-size: var(--fs-chip-lg);
|
|
796
|
+
color: var(--text2);
|
|
797
|
+
white-space: normal;
|
|
798
|
+
}
|
|
799
|
+
.st-link {
|
|
800
|
+
font-size: var(--fs-chip-lg);
|
|
801
|
+
font-weight: 600;
|
|
802
|
+
color: rgb(var(--v-theme-primary));
|
|
803
|
+
text-decoration: underline;
|
|
804
|
+
}
|
|
698
805
|
.st-actions {
|
|
699
806
|
display: flex;
|
|
700
807
|
justify-content: flex-end;
|
|
@@ -544,11 +544,6 @@ const saveANPRSettings = async () => {
|
|
|
544
544
|
try {
|
|
545
545
|
isSaving.value = true;
|
|
546
546
|
|
|
547
|
-
// Replace this with your actual API endpoint / store action
|
|
548
|
-
// await $fetch(`/api/sites/${siteId.value}/camera-settings`, {
|
|
549
|
-
// method: 'POST',
|
|
550
|
-
// body: cameraSettings.value
|
|
551
|
-
// })
|
|
552
547
|
const data = {
|
|
553
548
|
site: props.siteId,
|
|
554
549
|
ANPRSwitches: ANPRSwitches.value,
|
|
@@ -559,6 +554,11 @@ const saveANPRSettings = async () => {
|
|
|
559
554
|
show("Saved successfully", "success");
|
|
560
555
|
} catch (error) {
|
|
561
556
|
console.error("Failed to save settings:", error);
|
|
557
|
+
show(
|
|
558
|
+
(error as any)?.data?.message ||
|
|
559
|
+
"Could not save the camera (ANPR) settings. Please try again.",
|
|
560
|
+
"error"
|
|
561
|
+
);
|
|
562
562
|
} finally {
|
|
563
563
|
isSaving.value = false;
|
|
564
564
|
}
|
|
@@ -104,7 +104,7 @@ const toneClass = computed(() =>
|
|
|
104
104
|
font-size: var(--fs-chip);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/* The role tag: `padding:3px 10px;border-radius:999px;font-size:
|
|
107
|
+
/* The role tag: `padding:3px 10px;border-radius:999px;font-size:12.5px`. */
|
|
108
108
|
.status-chip--pill {
|
|
109
109
|
padding: 3px 10px;
|
|
110
110
|
font-size: var(--fs-chip);
|
|
@@ -397,12 +397,12 @@ onMounted(() => {
|
|
|
397
397
|
|
|
398
398
|
:deep(.v-data-table thead tr th) {
|
|
399
399
|
padding: 6px !important;
|
|
400
|
-
font-size:
|
|
400
|
+
font-size: 12px !important;
|
|
401
401
|
}
|
|
402
402
|
|
|
403
403
|
:deep(.v-data-table tbody tr td) {
|
|
404
404
|
padding: 4px !important;
|
|
405
|
-
font-size:
|
|
405
|
+
font-size: 12px !important;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
:deep(.v-img) {
|
package/composables/useKey.ts
CHANGED
|
@@ -132,21 +132,25 @@ export default function useKey() {
|
|
|
132
132
|
keyLog.value = data || new MKeyLog();
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
// Await each request so a failed save reaches the caller's catch instead of showing "success".
|
|
136
|
+
async function addKeyLog(_keyLog: TKeyLog) {
|
|
136
137
|
isAdding.value = true;
|
|
137
138
|
delete _keyLog._id;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
try {
|
|
140
|
+
const data = await useNuxtApp().$api<{
|
|
141
|
+
message: string | undefined;
|
|
142
|
+
}>(keyLogApiUrl, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
body: JSON.stringify(_keyLog),
|
|
145
|
+
});
|
|
146
|
+
return data?.message;
|
|
147
|
+
} finally {
|
|
148
|
+
isAdding.value = false;
|
|
149
|
+
}
|
|
146
150
|
}
|
|
147
151
|
|
|
148
|
-
function getKeyById(id: string) {
|
|
149
|
-
const data = useNuxtApp().$api<TKeyLog | undefined>(
|
|
152
|
+
async function getKeyById(id: string) {
|
|
153
|
+
const data = await useNuxtApp().$api<TKeyLog | undefined>(
|
|
150
154
|
`${keyLogApiUrl}/${id}`,
|
|
151
155
|
{
|
|
152
156
|
method: "GET",
|
|
@@ -158,22 +162,22 @@ export default function useKey() {
|
|
|
158
162
|
return data;
|
|
159
163
|
}
|
|
160
164
|
|
|
161
|
-
function updateKeyById(id: string, payload: TKeyLog) {
|
|
165
|
+
async function updateKeyById(id: string, payload: TKeyLog) {
|
|
162
166
|
payload.codes =
|
|
163
167
|
payload.codes instanceof Array && payload.codes.length > 0
|
|
164
168
|
? payload.codes
|
|
165
169
|
: [];
|
|
166
|
-
const data = useNuxtApp().$api<{
|
|
170
|
+
const data = await useNuxtApp().$api<{
|
|
167
171
|
message: { acknowledged: boolean };
|
|
168
172
|
}>(`${keyLogApiUrl}/${id}`, {
|
|
169
173
|
method: "PUT",
|
|
170
174
|
body: payload,
|
|
171
175
|
});
|
|
172
|
-
return data
|
|
176
|
+
return data?.message;
|
|
173
177
|
}
|
|
174
178
|
|
|
175
|
-
function deleteKeyById(id: string) {
|
|
176
|
-
const data = useNuxtApp().$api<{ message: TKeyLog }>(
|
|
179
|
+
async function deleteKeyById(id: string) {
|
|
180
|
+
const data = await useNuxtApp().$api<{ message: TKeyLog }>(
|
|
177
181
|
`${keyLogApiUrl}/${id}`,
|
|
178
182
|
{
|
|
179
183
|
method: "DELETE",
|