@7365admin1/layer-common 4.136.0 → 4.138.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 (40) hide show
  1. package/CHANGELOG.md +16 -4
  2. package/assets/css/primitives.css +54 -11
  3. package/assets/css/screens.css +3 -3
  4. package/assets/css/tokens.css +17 -12
  5. package/components/AnprCameraBadge.vue +1 -1
  6. package/components/AreaMain.vue +11 -5
  7. package/components/BillingMain.vue +12 -53
  8. package/components/BulletinBoardManagement.vue +9 -13
  9. package/components/CameraWall.vue +7 -7
  10. package/components/CameraWallTile.vue +8 -8
  11. package/components/Chat/ListCard.vue +1 -1
  12. package/components/CleaningScheduleMain.vue +56 -20
  13. package/components/ClientMain.vue +1 -1
  14. package/components/ClientSubscriptionForm.vue +1 -1
  15. package/components/DashboardMain.vue +13 -13
  16. package/components/HidAccessLogDashboard.vue +3 -3
  17. package/components/HidIntercomManagement.vue +14 -14
  18. package/components/HidQrCodeConfiguration.vue +3 -3
  19. package/components/HidReaderManagement.vue +1 -1
  20. package/components/InventoryPhotoThumbs.vue +1 -1
  21. package/components/InvitationMain.vue +8 -0
  22. package/components/Layout/Header.vue +2 -2
  23. package/components/Layout/NavigationDrawer.vue +1 -1
  24. package/components/MemberMain.vue +8 -0
  25. package/components/Nfc/NFCRouteScheduleTimes.vue +1 -1
  26. package/components/PlateNumberDisplay.vue +1 -1
  27. package/components/RolePermissionMain.vue +13 -0
  28. package/components/ScheduleAreaMain.vue +6 -9
  29. package/components/ScheduleGuide.vue +101 -0
  30. package/components/ScheduleTaskForm.vue +88 -7
  31. package/components/ScheduleTaskMain.vue +163 -56
  32. package/components/SiteSettings.vue +5 -5
  33. package/components/StatusChip.vue +1 -1
  34. package/components/VisitorsReportPreview.vue +2 -2
  35. package/composables/useKey.ts +20 -16
  36. package/package.json +1 -1
  37. package/pages/unauthorized.vue +4 -3
  38. package/utils/breadcrumb.ts +7 -0
  39. package/utils/nav-sections.ts +2 -0
  40. package/utils/schedule-task.ts +106 -23
@@ -9,6 +9,29 @@
9
9
  <template>
10
10
  <div class="schedule-guide">
11
11
  <p v-if="text" class="schedule-guide__text">{{ text }}</p>
12
+ <!--
13
+ "How it works": the page's own short manual (owner 21 Oct - a supervisor
14
+ who was never trained must understand the screen from the screen). Open
15
+ by default until the viewer closes it; the choice is remembered on this
16
+ browser only.
17
+ -->
18
+ <v-expansion-panels
19
+ v-if="$slots.default && howTitle"
20
+ v-model="howPanel"
21
+ class="schedule-guide__how"
22
+ variant="accordion"
23
+ flat
24
+ >
25
+ <v-expansion-panel value="how">
26
+ <v-expansion-panel-title class="schedule-guide__how-title">
27
+ <v-icon size="20" class="mr-2">mdi-help-circle-outline</v-icon>
28
+ {{ howTitle }}
29
+ </v-expansion-panel-title>
30
+ <v-expansion-panel-text class="schedule-guide__how-body">
31
+ <slot />
32
+ </v-expansion-panel-text>
33
+ </v-expansion-panel>
34
+ </v-expansion-panels>
12
35
  <v-alert
13
36
  v-if="problems.length"
14
37
  type="warning"
@@ -44,6 +67,27 @@ const props = defineProps({
44
67
  serviceType: { type: String, default: "" },
45
68
  scheduleRoute: { type: String, default: "" },
46
69
  text: { type: String, default: "" },
70
+ /** Heading of the "How it works" panel; the panel shows only with this and a default slot. */
71
+ howTitle: { type: String, default: "" },
72
+ /** Remembers open/closed per panel on this browser. */
73
+ howKey: { type: String, default: "" },
74
+ });
75
+
76
+ const HOW_STORE = () => `schedule-guide-how:${props.howKey || props.howTitle}`;
77
+ const howPanel = ref<string | undefined>("how");
78
+ onMounted(() => {
79
+ try {
80
+ if (localStorage.getItem(HOW_STORE()) === "closed") howPanel.value = undefined;
81
+ } catch {
82
+ // storage blocked: stay open
83
+ }
84
+ });
85
+ watch(howPanel, (v) => {
86
+ try {
87
+ localStorage.setItem(HOW_STORE(), v ? "open" : "closed");
88
+ } catch {
89
+ // storage blocked: nothing to remember
90
+ }
47
91
  });
48
92
 
49
93
  const { getAreas, getAreaById } = useAreas();
@@ -125,8 +169,65 @@ watch(() => [props.site, props.serviceType], check);
125
169
  font-size: var(--fs-cell);
126
170
  color: var(--text2);
127
171
  }
172
+ .schedule-guide__how {
173
+ margin-bottom: 12px;
174
+ border: 1px solid var(--border);
175
+ border-radius: var(--r-inner);
176
+ overflow: hidden;
177
+ }
178
+ .schedule-guide__how-title {
179
+ font-family: var(--font-sans);
180
+ font-size: var(--fs-card-title);
181
+ font-weight: 700;
182
+ }
183
+ .schedule-guide__how-body {
184
+ font-family: var(--font-sans);
185
+ font-size: var(--fs-cell);
186
+ line-height: 1.55;
187
+ }
128
188
  .schedule-guide__list {
129
189
  margin: 0;
130
190
  padding-left: 18px;
131
191
  }
132
192
  </style>
193
+
194
+ <!-- Shared by the "How it works" content the schedule screens put in the slot. -->
195
+ <style>
196
+ .sg-steps {
197
+ margin: 0 0 12px;
198
+ padding-left: 22px;
199
+ }
200
+ .sg-steps li {
201
+ margin-bottom: 6px;
202
+ }
203
+ .sg-example {
204
+ margin: 0 0 12px;
205
+ padding: 12px 14px;
206
+ border-left: 3px solid rgb(var(--v-theme-primary));
207
+ border-radius: var(--r-inner-sm);
208
+ background: var(--hover);
209
+ }
210
+ .sg-legend {
211
+ display: grid;
212
+ grid-template-columns: max-content 1fr;
213
+ gap: 8px 12px;
214
+ align-items: center;
215
+ margin: 0;
216
+ }
217
+ .sg-legend dd {
218
+ margin: 0;
219
+ }
220
+ .sg-h {
221
+ margin: 4px 0 8px;
222
+ font-weight: 700;
223
+ }
224
+ @media (max-width: 599.98px) {
225
+ .sg-legend {
226
+ grid-template-columns: 1fr;
227
+ gap: 2px 0;
228
+ }
229
+ .sg-legend dd {
230
+ margin-bottom: 8px;
231
+ }
232
+ }
233
+ </style>
@@ -59,8 +59,10 @@
59
59
  variant="outlined"
60
60
  density="comfortable"
61
61
  class="filter-field mb-4"
62
- placeholder="For example: Midday toilet round"
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 an
85
- area is added to the checklist.
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-chip-lg);
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 your daily schedule at the time you choose, once or on a repeat. Staff on this site get a push notification when it is added."
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 (Scheduled)'"
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
- An extra visit adds one more round to your daily
30
- {{ scheduleName }} at a time you choose. Set up your Areas and
31
- Units first, then add one.
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
- <StatusChip
95
- v-if="item.lastRun && item.lastRunStatus === 'done'"
96
- status="Done"
97
- :label="item.lastRunText"
98
- />
99
- <StatusChip
100
- v-else-if="item.lastRun && item.lastRunStatus === 'missed'"
101
- status="Missed"
102
- :label="item.lastRunText"
103
- />
104
- <span v-else>{{ item.lastRunText }}</span>
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
- <StatusChip
109
- :status="item.stateLabel"
110
- :label="item.stateLabel"
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 run:
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 run:
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 runs.</span
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 runs of this extra visit. Visits that already
387
- happened are kept. Delete it?
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 scheduleName = scheduleNameFor(props.serviceType).title;
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: "Title", value: "title" },
521
- { title: "Areas", value: "areas" },
522
- { title: "When", value: "when", sortable: false },
523
- { title: "Next run", value: "nextRun", sortable: false },
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: describeWhen(row),
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. It will run again from its next date."
616
- : "Paused. It will not run until you resume it.",
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;