@7365admin1/layer-common 4.136.0 → 4.137.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 +10 -4
- 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/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/Layout/Header.vue +2 -2
- package/components/Layout/NavigationDrawer.vue +1 -1
- package/components/Nfc/NFCRouteScheduleTimes.vue +1 -1
- package/components/PlateNumberDisplay.vue +1 -1
- 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/StatusChip.vue +1 -1
- package/components/VisitorsReportPreview.vue +2 -2
- package/package.json +1 -1
- package/utils/breadcrumb.ts +7 -0
- package/utils/nav-sections.ts +2 -0
- package/utils/schedule-task.ts +106 -23
|
@@ -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;
|
|
@@ -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/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.137.0",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|
package/utils/breadcrumb.ts
CHANGED
|
@@ -25,6 +25,13 @@ const ACRONYMS: Record<string, string> = {
|
|
|
25
25
|
const RENAMED: [string, string][] = [
|
|
26
26
|
["Building Mgmt", "Occupancy Mgmt"],
|
|
27
27
|
["Event Mgmt", "Task Calendar"],
|
|
28
|
+
// Schedule screens (owner 21 Oct): the same names the menu and the mobile apps use.
|
|
29
|
+
["Schedule Task", "Extra Visits"],
|
|
30
|
+
...["Cleaning", "Technician", "Pest", "Pool", "Landscape"].flatMap((t): [string, string][] => [
|
|
31
|
+
[`${t} Schedule Area`, "Area"],
|
|
32
|
+
[`${t} Schedule Unit`, "Unit"],
|
|
33
|
+
[`${t} Schedule`, "Daily Checklist"],
|
|
34
|
+
]),
|
|
28
35
|
];
|
|
29
36
|
|
|
30
37
|
export function moduleNameFromRoute(route: {
|
package/utils/nav-sections.ts
CHANGED
|
@@ -50,6 +50,8 @@ export const NAV_SECTIONS: Record<string, string> = {
|
|
|
50
50
|
Area: "SERVICE DELIVERY",
|
|
51
51
|
Unit: "SERVICE DELIVERY",
|
|
52
52
|
"Schedule Task": "SERVICE DELIVERY",
|
|
53
|
+
"Daily Checklist": "SERVICE DELIVERY",
|
|
54
|
+
"Extra Visits": "SERVICE DELIVERY",
|
|
53
55
|
|
|
54
56
|
// EQUIPMENT
|
|
55
57
|
"Equipment Management": "EQUIPMENT",
|
package/utils/schedule-task.ts
CHANGED
|
@@ -176,12 +176,20 @@ export function validateSchedule(f: TScheduleForm, now: Date = new Date()): TSch
|
|
|
176
176
|
return e;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
/** "
|
|
179
|
+
/** "08:00" -> "8:00 am", "14:30" -> "2:30 pm". Anything else is returned as given. */
|
|
180
|
+
export function fmtTime(time: string | null | undefined): string {
|
|
181
|
+
const m = TIME_RE.exec(time || "");
|
|
182
|
+
if (!m) return time || "";
|
|
183
|
+
const h = Number(m[1]);
|
|
184
|
+
return `${h % 12 || 12}:${m[2]} ${h < 12 ? "am" : "pm"}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** "every weekday at 8:00 am from 21 Oct until 31 Dec" - the When, as a sentence fragment. */
|
|
180
188
|
export function describeWhen(
|
|
181
189
|
t: Partial<TRule> & { time?: string },
|
|
182
190
|
nowYear?: string,
|
|
183
191
|
): string {
|
|
184
|
-
const
|
|
192
|
+
const at = t.time ? ` at ${fmtTime(t.time)}` : "";
|
|
185
193
|
const repeat = t.repeat || "none";
|
|
186
194
|
const dates = t.dates || [];
|
|
187
195
|
const range =
|
|
@@ -189,23 +197,23 @@ export function describeWhen(
|
|
|
189
197
|
(t.endDate ? ` until ${fmtDate(t.endDate, nowYear)}` : "");
|
|
190
198
|
switch (repeat) {
|
|
191
199
|
case "daily":
|
|
192
|
-
return
|
|
200
|
+
return `every day${at}${range}`;
|
|
193
201
|
case "weekdays":
|
|
194
|
-
return
|
|
202
|
+
return `every weekday${at}${range}`;
|
|
195
203
|
case "weekly": {
|
|
196
204
|
const names = WEEK_DAYS.filter((d) => (t.daysOfWeek || []).includes(d.value)).map((d) => d.title);
|
|
197
|
-
return
|
|
205
|
+
return `every ${names.join(", ") || "week"}${at}${range}`;
|
|
198
206
|
}
|
|
199
207
|
case "monthly": {
|
|
200
208
|
const day = t.startDate ? Number(t.startDate.slice(8, 10)) : null;
|
|
201
209
|
const on = day === null ? "every month" : day === 31 ? "on the last day of every month" : `on day ${day} of every month`;
|
|
202
|
-
return `${
|
|
210
|
+
return `${on}${at}${range}`;
|
|
203
211
|
}
|
|
204
212
|
default: {
|
|
205
213
|
const sorted = [...dates].sort();
|
|
206
|
-
if (!sorted.length) return
|
|
214
|
+
if (!sorted.length) return `on no date${at}`;
|
|
207
215
|
const shown = sorted.slice(0, 3).map((d) => fmtDate(d, nowYear)).join(", ");
|
|
208
|
-
return
|
|
216
|
+
return `on ${shown}${sorted.length > 3 ? ` +${sorted.length - 3} more` : ""}${at}${
|
|
209
217
|
t.endDate ? ` (until ${fmtDate(t.endDate, nowYear)})` : ""
|
|
210
218
|
}`;
|
|
211
219
|
}
|
|
@@ -226,33 +234,93 @@ export function buildSummary(
|
|
|
226
234
|
return `Adds 1 extra visit to ${areas} (${units}) ${describeWhen(f, year)} — next: ${nextText}.`;
|
|
227
235
|
}
|
|
228
236
|
|
|
229
|
-
|
|
237
|
+
/** The state of an extra visit as a whole. "ended" reads "Finished": nothing is left to run. */
|
|
238
|
+
export const STATUS_LABELS: Record<string, string> = { active: "Active", paused: "Paused", ended: "Finished" };
|
|
239
|
+
|
|
240
|
+
/** What each word means, shown on the page so nobody has to guess. Same words as the mobile apps. */
|
|
241
|
+
export const STATUS_HELP: { word: string; meaning: string }[] = [
|
|
242
|
+
{ word: "Active", meaning: "It will add a visit at its next date and time." },
|
|
243
|
+
{ word: "Paused", meaning: "Stopped for now. Nothing is added until you press Resume." },
|
|
244
|
+
{ word: "Finished", meaning: "Its last date has passed, so nothing more is added. Press \"Extend or repeat\" to keep it going." },
|
|
245
|
+
];
|
|
246
|
+
export const RESULT_HELP: { word: string; meaning: string }[] = [
|
|
247
|
+
{ word: "Not started", meaning: "Nothing is ticked yet." },
|
|
248
|
+
{ word: "In progress", meaning: "Some units are ticked; the rest can still be done today." },
|
|
249
|
+
{ word: "Done", meaning: "Staff ticked every unit of the visit." },
|
|
250
|
+
{ word: "Needs redo", meaning: "A supervisor sent a unit back to be done again." },
|
|
251
|
+
{ word: "Missed", meaning: "The day ended with units not done." },
|
|
252
|
+
];
|
|
230
253
|
|
|
231
|
-
/** "Done 20 Oct" / "Missed 20 Oct" / "
|
|
254
|
+
/** "Done 20 Oct" / "Missed 20 Oct" / "No visit yet". */
|
|
232
255
|
export function lastRunLabel(row: { lastRun?: string | null; lastRunStatus?: string | null }): string {
|
|
233
|
-
if (!row.lastRun) return "
|
|
256
|
+
if (!row.lastRun) return "No visit yet";
|
|
234
257
|
const m = /^(\d{4}-\d{2}-\d{2})/.exec(row.lastRun);
|
|
235
258
|
const day = m ? fmtDate(m[1]) : "";
|
|
236
|
-
const word = { done: "Done", missed: "Missed", pending: "In progress" }[row.lastRunStatus || ""] || "
|
|
259
|
+
const word = { done: "Done", missed: "Missed", pending: "In progress" }[row.lastRunStatus || ""] || "Added";
|
|
237
260
|
return `${word} ${day}`.trim();
|
|
238
261
|
}
|
|
239
262
|
|
|
263
|
+
/** Why an extra visit says Finished, in one sentence. */
|
|
264
|
+
export function finishedReason(row: Partial<TRule>, nowYear?: string): string {
|
|
265
|
+
if (row.repeat && row.repeat !== "none" && row.endDate) {
|
|
266
|
+
return `Its end date (${fmtDate(row.endDate, nowYear)}) has passed.`;
|
|
267
|
+
}
|
|
268
|
+
return "All the dates it was set for have passed.";
|
|
269
|
+
}
|
|
270
|
+
|
|
240
271
|
/**
|
|
241
|
-
* One
|
|
242
|
-
*
|
|
243
|
-
* the trade app serves its daily schedule from.
|
|
272
|
+
* One sentence per list row: "Every Mon, Wed at 2:00 pm in Lobby D — next on Wed 22 Oct".
|
|
273
|
+
* Paused and finished rows say so instead of a next date.
|
|
244
274
|
*/
|
|
245
|
-
export
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
275
|
+
export function describeRow(
|
|
276
|
+
row: Partial<TRule> & {
|
|
277
|
+
areas?: { name?: string }[];
|
|
278
|
+
nextRun?: string | null;
|
|
279
|
+
scheduleStatus?: string | null;
|
|
280
|
+
paused?: boolean;
|
|
281
|
+
},
|
|
282
|
+
nowYear?: string,
|
|
283
|
+
): string {
|
|
284
|
+
const when = describeWhen(row, nowYear);
|
|
285
|
+
const names = (row.areas || []).map((a) => a?.name).filter(Boolean) as string[];
|
|
286
|
+
const where = names.length
|
|
287
|
+
? ` in ${names.slice(0, 3).join(", ")}${names.length > 3 ? ` +${names.length - 3} more` : ""}`
|
|
288
|
+
: "";
|
|
289
|
+
const state = row.scheduleStatus || (row.paused ? "paused" : "active");
|
|
290
|
+
const m = /^(\d{4}-\d{2}-\d{2})/.exec(row.nextRun || "");
|
|
291
|
+
const tail =
|
|
292
|
+
state === "paused"
|
|
293
|
+
? " — paused, nothing is added until you resume it"
|
|
294
|
+
: state === "ended"
|
|
295
|
+
? " — finished"
|
|
296
|
+
: m
|
|
297
|
+
? ` — next on ${DAYS[weekdayOf(m[1])]} ${fmtDate(m[1], nowYear)}`
|
|
298
|
+
: "";
|
|
299
|
+
return `${when.charAt(0).toUpperCase()}${when.slice(1)}${where}${tail}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* One trade word and one page path per trade. Every trade's menu says "Daily
|
|
304
|
+
* Checklist" (owner 21 Oct; the mobile apps use the same name); the page title
|
|
305
|
+
* adds the trade ("Daily Checklist – Cleaning"). The path is the folder the
|
|
306
|
+
* trade app serves its daily checklist from - unchanged, so links and
|
|
307
|
+
* permissions keep working.
|
|
308
|
+
*/
|
|
309
|
+
export const SCHEDULE_NAMES: Record<string, { title: string; trade: string; route: string }> = {
|
|
310
|
+
cleaning_services: { title: "Daily Checklist", trade: "Cleaning", route: "cleaning-schedule" },
|
|
311
|
+
landscaping_services: { title: "Daily Checklist", trade: "Landscape", route: "landscape-schedule" },
|
|
312
|
+
pest_control_services: { title: "Daily Checklist", trade: "Pest", route: "pest-schedule" },
|
|
313
|
+
pool_maintenance_services: { title: "Daily Checklist", trade: "Pool", route: "pool-schedule" },
|
|
314
|
+
mechanical_electrical_services: { title: "Daily Checklist", trade: "Technician", route: "technician-schedule" },
|
|
251
315
|
};
|
|
252
316
|
|
|
253
317
|
export const scheduleNameFor = (serviceType?: string) =>
|
|
254
318
|
SCHEDULE_NAMES[serviceType || ""] || SCHEDULE_NAMES.cleaning_services;
|
|
255
319
|
|
|
320
|
+
/** "Daily Checklist – Cleaning" / "– Technician" / ... */
|
|
321
|
+
export const dailyChecklistTitle = (serviceType?: string) =>
|
|
322
|
+
`Daily Checklist – ${scheduleNameFor(serviceType).trade}`;
|
|
323
|
+
|
|
256
324
|
/** Singapore "today" as YYYY-MM-DD, for "is today's list there yet". */
|
|
257
325
|
export const sgtToday = (now: Date = new Date()) => sgtParts(now).date;
|
|
258
326
|
|
|
@@ -265,13 +333,28 @@ export function hasListForToday(rows: { createdAt?: string | Date }[], now: Date
|
|
|
265
333
|
});
|
|
266
334
|
}
|
|
267
335
|
|
|
268
|
-
/**
|
|
336
|
+
/**
|
|
337
|
+
* The daily checklist's words, the same on web and mobile: Open -> Not started,
|
|
338
|
+
* Ongoing / On-going -> In progress, Completed -> Done. Closed stays Closed (the
|
|
339
|
+
* day ended; anything not ticked counts as Missed). Other words pass through.
|
|
340
|
+
*/
|
|
269
341
|
export function dailyStatusLabel(status: string | null | undefined): string {
|
|
270
342
|
const s = String(status ?? "").trim();
|
|
271
343
|
if (!s) return "No status";
|
|
272
|
-
|
|
344
|
+
if (/^on-?going$/i.test(s)) return "In progress";
|
|
345
|
+
if (/^open$/i.test(s)) return "Not started";
|
|
346
|
+
if (/^completed?$/i.test(s)) return "Done";
|
|
347
|
+
return s;
|
|
273
348
|
}
|
|
274
349
|
|
|
350
|
+
/** What the daily checklist's words mean, shown on the page. */
|
|
351
|
+
export const DAILY_STATUS_HELP: { word: string; meaning: string }[] = [
|
|
352
|
+
{ word: "Not started", meaning: "The day's list is open and nothing is ticked yet." },
|
|
353
|
+
{ word: "In progress", meaning: "Staff have ticked some units; the rest can still be done." },
|
|
354
|
+
{ word: "Done", meaning: "Every unit on the list was ticked." },
|
|
355
|
+
{ word: "Closed", meaning: "The day ended at midnight. The list is view-only, and anything not ticked counts as Missed." },
|
|
356
|
+
];
|
|
357
|
+
|
|
275
358
|
/** A unit's result in the history view: Done, Needs redo (rejected by a supervisor) or Missed (never done). */
|
|
276
359
|
export function historyUnitLabel(status: string | null | undefined): "Done" | "Needs redo" | "Missed" {
|
|
277
360
|
const s = String(status ?? "").trim().toLowerCase();
|