@7365admin1/layer-common 4.132.0 → 4.134.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 +17 -4
- package/components/AreaChecklistHistoryLogs.vue +5 -4
- package/components/CleaningScheduleMain.vue +109 -15
- package/components/FeedbackMain.vue +17 -1
- package/components/ManageChecklistMain.vue +16 -12
- package/components/RolePermissionFormCreate.vue +5 -1
- package/components/RolePermissionFormPreviewUpdate.vue +34 -4
- package/components/RolePermissionMain.vue +3 -1
- package/components/ScheduleAreaMain.vue +56 -71
- package/components/ScheduleGuide.vue +132 -0
- package/components/ScheduleTaskForm.vue +520 -353
- package/components/ScheduleTaskMain.vue +211 -70
- package/components/TableHygiene.vue +2 -2
- package/components/VehicleAddSelection.vue +5 -1
- package/components/VehicleManagement.vue +10 -4
- package/composables/useCleaningSchedules.ts +9 -0
- package/composables/useFeedback.ts +4 -0
- package/composables/useScheduleTask.ts +32 -2
- package/composables/useSecurityPermission.ts +19 -0
- package/constants/permissions.ts +7 -0
- package/package.json +1 -1
- package/types/cleaner-schedule.d.ts +1 -1
- package/utils/org-modules.ts +17 -1
- package/utils/schedule-task.ts +281 -0
- package/utils/security-catalogue.ts +59 -0
- package/utils/status.ts +6 -0
- package/utils/vehicle-actions.ts +10 -2
- package/composables/useScheduleTaskArea.ts +0 -85
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-row no-gutters align="center" justify="center">
|
|
3
3
|
<v-col cols="12" lg="12">
|
|
4
|
+
<ScheduleGuide
|
|
5
|
+
:org-id="orgId"
|
|
6
|
+
:site="site"
|
|
7
|
+
:service-type="serviceType"
|
|
8
|
+
:schedule-route="scheduleRoute"
|
|
9
|
+
text="These are the areas on this day's checklist. Open an area to see its units and visits."
|
|
10
|
+
/>
|
|
4
11
|
<TableMain
|
|
5
|
-
:title="
|
|
12
|
+
:title="pageTitle"
|
|
6
13
|
:headers="headers"
|
|
7
14
|
:items="items"
|
|
8
15
|
:loading="loading"
|
|
@@ -14,6 +21,16 @@
|
|
|
14
21
|
@row-click="onRowClick"
|
|
15
22
|
@refresh="getScheduleAreasRefresh"
|
|
16
23
|
>
|
|
24
|
+
<template #no-data>
|
|
25
|
+
<div class="sa-empty">
|
|
26
|
+
<strong>No areas on this list yet.</strong>
|
|
27
|
+
<span>
|
|
28
|
+
Areas appear here once they are set up with Units and the list is
|
|
29
|
+
created. Use "Create today's list now" if the midnight list is
|
|
30
|
+
missing.
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
17
34
|
<template #item.type="{ item }">
|
|
18
35
|
<span class="text-capitalize">{{ item.type }}</span>
|
|
19
36
|
</template>
|
|
@@ -87,53 +104,18 @@
|
|
|
87
104
|
History
|
|
88
105
|
</v-btn>
|
|
89
106
|
<v-btn
|
|
90
|
-
v-if="
|
|
107
|
+
v-if="showCreateList"
|
|
91
108
|
class="text-none screen-btn-primary"
|
|
92
109
|
variant="flat"
|
|
93
|
-
aria-label="
|
|
110
|
+
aria-label="Create today's list now"
|
|
94
111
|
@click="_generateChecklist"
|
|
95
112
|
:loading="submitting"
|
|
96
|
-
:disabled="isScheduleClosed"
|
|
97
113
|
>
|
|
98
|
-
|
|
114
|
+
Create today's list now
|
|
99
115
|
</v-btn>
|
|
100
116
|
</v-col>
|
|
101
117
|
</v-row>
|
|
102
118
|
</template>
|
|
103
|
-
<template #item.acceptedBy="{ item }">
|
|
104
|
-
<div
|
|
105
|
-
v-if="
|
|
106
|
-
item.acceptedByName ||
|
|
107
|
-
(item.status && String(item.status).toLowerCase() === 'accepted')
|
|
108
|
-
"
|
|
109
|
-
>
|
|
110
|
-
<!--
|
|
111
|
-
`color`/`variant` on a plain `<span>` do nothing - Vuetify only
|
|
112
|
-
reads them on its own components, so this drew as bare cell text
|
|
113
|
-
while reading as a tonal green chip. It is the accepted chip the
|
|
114
|
-
sibling branches already use.
|
|
115
|
-
-->
|
|
116
|
-
<StatusChip
|
|
117
|
-
status="Accepted"
|
|
118
|
-
class="text-capitalize"
|
|
119
|
-
:label="item.acceptedByName || item.acceptedBy || 'Accepted'"
|
|
120
|
-
/>
|
|
121
|
-
</div>
|
|
122
|
-
<div v-else-if="isScheduleClosed">
|
|
123
|
-
<StatusChip status="Closed" class="text-capitalize" />
|
|
124
|
-
</div>
|
|
125
|
-
<div v-else>
|
|
126
|
-
<v-btn
|
|
127
|
-
v-if="canManageScheduleTasks"
|
|
128
|
-
variant="outlined"
|
|
129
|
-
class="text-capitalize screen-btn-ghost"
|
|
130
|
-
@click.stop="() => _acceptAreaChecklist(item._id)"
|
|
131
|
-
>
|
|
132
|
-
Accept
|
|
133
|
-
</v-btn>
|
|
134
|
-
</div>
|
|
135
|
-
</template>
|
|
136
|
-
|
|
137
119
|
<template #item.createdAt="{ value }">
|
|
138
120
|
{{
|
|
139
121
|
formatDate
|
|
@@ -148,7 +130,7 @@
|
|
|
148
130
|
<StatusChip
|
|
149
131
|
class="text-capitalize"
|
|
150
132
|
:status="value"
|
|
151
|
-
:label="value
|
|
133
|
+
:label="dailyStatusLabel(value)"
|
|
152
134
|
/>
|
|
153
135
|
</template>
|
|
154
136
|
</TableMain></v-col
|
|
@@ -160,10 +142,7 @@
|
|
|
160
142
|
|
|
161
143
|
<script setup lang="ts">
|
|
162
144
|
import useCleaningSchedules from "../composables/useCleaningSchedules";
|
|
163
|
-
import
|
|
164
|
-
import { workerTitle } from "../utils/worker-terms";
|
|
165
|
-
// "Cleaner Checklist" in Hygiene, "Gardener Checklist" in Landscape, ...
|
|
166
|
-
const checklistTitle = workerTitle(`${useWorkerTerm().one} checklist`);
|
|
145
|
+
import { dailyStatusLabel, scheduleNameFor } from "../utils/schedule-task";
|
|
167
146
|
import useUtils from "../composables/useUtils";
|
|
168
147
|
|
|
169
148
|
const props = defineProps({
|
|
@@ -178,6 +157,12 @@ const props = defineProps({
|
|
|
178
157
|
canManageScheduleTasks: { type: Boolean, default: false },
|
|
179
158
|
});
|
|
180
159
|
|
|
160
|
+
// "Daily Schedule - Cleaning" / "- Landscape" / ... (same title as the day list)
|
|
161
|
+
const pageTitle = `Daily Schedule – ${scheduleNameFor(props.serviceType).title.replace(
|
|
162
|
+
/ Schedule$/,
|
|
163
|
+
"",
|
|
164
|
+
)}`;
|
|
165
|
+
|
|
181
166
|
const submitting = ref(false);
|
|
182
167
|
const message = ref("");
|
|
183
168
|
const messageSnackbar = ref(false);
|
|
@@ -191,7 +176,7 @@ const headers = [
|
|
|
191
176
|
{ title: "Area", value: "name" },
|
|
192
177
|
{ title: "Status", value: "status" },
|
|
193
178
|
{ title: "Type", value: "type" },
|
|
194
|
-
{ title: "
|
|
179
|
+
{ title: "Visits per day", value: "set" },
|
|
195
180
|
];
|
|
196
181
|
|
|
197
182
|
const { formatDate, back } = useUtils();
|
|
@@ -214,7 +199,7 @@ const statusFilter = ref<TScheduleAreaStatus>("all");
|
|
|
214
199
|
const statusOptions = [
|
|
215
200
|
{ title: "All", value: "all" },
|
|
216
201
|
{ title: "Open", value: "open" },
|
|
217
|
-
{ title: "
|
|
202
|
+
{ title: "In progress", value: "ongoing" },
|
|
218
203
|
{ title: "Completed", value: "completed" },
|
|
219
204
|
];
|
|
220
205
|
const areaTypeFilter = ref<TAreaType>("all");
|
|
@@ -224,8 +209,7 @@ const typeOptions = [
|
|
|
224
209
|
{ title: "Toilet", value: "toilet" },
|
|
225
210
|
];
|
|
226
211
|
|
|
227
|
-
const { getScheduleAreas,
|
|
228
|
-
useCleaningSchedules();
|
|
212
|
+
const { getScheduleAreas, generateChecklist } = useCleaningSchedules();
|
|
229
213
|
|
|
230
214
|
const {
|
|
231
215
|
data: getScheduleAreasReq,
|
|
@@ -262,6 +246,16 @@ watchEffect(() => {
|
|
|
262
246
|
}
|
|
263
247
|
});
|
|
264
248
|
|
|
249
|
+
// Only when the day has no areas at all: that is the missed-midnight case.
|
|
250
|
+
const showCreateList = computed(
|
|
251
|
+
() =>
|
|
252
|
+
canGenerateChecklist.value &&
|
|
253
|
+
!isScheduleClosed.value &&
|
|
254
|
+
!loading.value &&
|
|
255
|
+
!!getScheduleAreasReq.value &&
|
|
256
|
+
items.value.length === 0,
|
|
257
|
+
);
|
|
258
|
+
|
|
265
259
|
function showMessage(msg: string, color: string = "error") {
|
|
266
260
|
message.value = msg;
|
|
267
261
|
messageColor.value = color;
|
|
@@ -288,38 +282,17 @@ function openHistory() {
|
|
|
288
282
|
navigateTo(path);
|
|
289
283
|
}
|
|
290
284
|
|
|
291
|
-
async function _acceptAreaChecklist(id?: string) {
|
|
292
|
-
if (!id) {
|
|
293
|
-
showMessage("No checklist id provided to accept.", "error");
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
submitting.value = true;
|
|
299
|
-
await acceptAreaChecklist(id);
|
|
300
|
-
showMessage("Area checklist accepted successfully.", "success");
|
|
301
|
-
|
|
302
|
-
if (typeof getScheduleAreasRefresh === "function") {
|
|
303
|
-
getScheduleAreasRefresh();
|
|
304
|
-
}
|
|
305
|
-
} catch (error: any) {
|
|
306
|
-
showMessage(error?.message || "Failed to accept area checklist.", "error");
|
|
307
|
-
} finally {
|
|
308
|
-
submitting.value = false;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
285
|
async function _generateChecklist() {
|
|
313
286
|
try {
|
|
314
287
|
submitting.value = true;
|
|
315
288
|
await generateChecklist(props.site, props.scheduleAreaId);
|
|
316
|
-
showMessage("
|
|
289
|
+
showMessage("Today's list is ready.", "success");
|
|
317
290
|
|
|
318
291
|
if (typeof getScheduleAreasRefresh === "function") {
|
|
319
292
|
getScheduleAreasRefresh();
|
|
320
293
|
}
|
|
321
294
|
} catch (error: any) {
|
|
322
|
-
showMessage(error?.message || "
|
|
295
|
+
showMessage(error?.data?.message || error?.message || "Could not create today's list.", "error");
|
|
323
296
|
} finally {
|
|
324
297
|
submitting.value = false;
|
|
325
298
|
}
|
|
@@ -354,3 +327,15 @@ onUnmounted(() => {
|
|
|
354
327
|
} catch (_) {}
|
|
355
328
|
});
|
|
356
329
|
</script>
|
|
330
|
+
|
|
331
|
+
<style scoped>
|
|
332
|
+
.sa-empty {
|
|
333
|
+
display: flex;
|
|
334
|
+
flex-direction: column;
|
|
335
|
+
gap: 4px;
|
|
336
|
+
padding: 24px 16px;
|
|
337
|
+
font-family: var(--font-sans);
|
|
338
|
+
font-size: var(--fs-cell);
|
|
339
|
+
color: var(--text2);
|
|
340
|
+
}
|
|
341
|
+
</style>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
THE ONE-LINE EXPLANATION + "BEFORE YOU START" BANNER, shared by every schedule
|
|
3
|
+
screen (Cleaning / Landscape / Pest / Pool / Technician). The intro says what
|
|
4
|
+
the screen is for; the banner appears only when the site is not set up yet
|
|
5
|
+
(no Areas, an Area without "visits per day", or an Area without Units), and
|
|
6
|
+
links straight to where that is fixed. It never blocks the screen and stays
|
|
7
|
+
silent if the check itself fails.
|
|
8
|
+
-->
|
|
9
|
+
<template>
|
|
10
|
+
<div class="schedule-guide">
|
|
11
|
+
<p v-if="text" class="schedule-guide__text">{{ text }}</p>
|
|
12
|
+
<v-alert
|
|
13
|
+
v-if="problems.length"
|
|
14
|
+
type="warning"
|
|
15
|
+
variant="tonal"
|
|
16
|
+
density="comfortable"
|
|
17
|
+
class="schedule-guide__banner"
|
|
18
|
+
title="Before you start"
|
|
19
|
+
>
|
|
20
|
+
<ul class="schedule-guide__list">
|
|
21
|
+
<li v-for="p in problems" :key="p">{{ p }}</li>
|
|
22
|
+
</ul>
|
|
23
|
+
<template #append>
|
|
24
|
+
<v-btn
|
|
25
|
+
:to="areaLink"
|
|
26
|
+
variant="outlined"
|
|
27
|
+
size="small"
|
|
28
|
+
class="text-none"
|
|
29
|
+
>
|
|
30
|
+
Set up Areas and Units
|
|
31
|
+
</v-btn>
|
|
32
|
+
</template>
|
|
33
|
+
</v-alert>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup lang="ts">
|
|
38
|
+
import useAreas from "../composables/useAreas";
|
|
39
|
+
import { scheduleNameFor } from "../utils/schedule-task";
|
|
40
|
+
|
|
41
|
+
const props = defineProps({
|
|
42
|
+
orgId: { type: String, required: true },
|
|
43
|
+
site: { type: String, required: true },
|
|
44
|
+
serviceType: { type: String, default: "" },
|
|
45
|
+
scheduleRoute: { type: String, default: "" },
|
|
46
|
+
text: { type: String, default: "" },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const { getAreas, getAreaById } = useAreas();
|
|
50
|
+
const problems = ref<string[]>([]);
|
|
51
|
+
|
|
52
|
+
const areaLink = computed(
|
|
53
|
+
() =>
|
|
54
|
+
`/${props.orgId}/${props.site}/${
|
|
55
|
+
props.scheduleRoute || scheduleNameFor(props.serviceType).route
|
|
56
|
+
}/area`,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// How many areas get a units check: one request each, so capped.
|
|
60
|
+
const MAX_UNIT_CHECKS = 30;
|
|
61
|
+
const plural = (n: number, one: string, many: string) => (n === 1 ? one : many);
|
|
62
|
+
|
|
63
|
+
async function check() {
|
|
64
|
+
try {
|
|
65
|
+
const res = await getAreas({
|
|
66
|
+
site: props.site,
|
|
67
|
+
limit: 200,
|
|
68
|
+
serviceType: props.serviceType,
|
|
69
|
+
});
|
|
70
|
+
const areas: any[] = res?.items || [];
|
|
71
|
+
const found: string[] = [];
|
|
72
|
+
|
|
73
|
+
if (!areas.length) {
|
|
74
|
+
problems.value = [
|
|
75
|
+
"There are no Areas yet. Add your Areas, then the Units in each one.",
|
|
76
|
+
];
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const noVisits = areas.filter((a) => !(Number(a.set) >= 1));
|
|
81
|
+
if (noVisits.length) {
|
|
82
|
+
found.push(
|
|
83
|
+
`${noVisits.length} ${plural(noVisits.length, "area has", "areas have")} no visits per day set: ${noVisits
|
|
84
|
+
.slice(0, 3)
|
|
85
|
+
.map((a) => a.name)
|
|
86
|
+
.join(", ")}${noVisits.length > 3 ? "..." : ""}.`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const noUnits: string[] = [];
|
|
91
|
+
await Promise.all(
|
|
92
|
+
areas.slice(0, MAX_UNIT_CHECKS).map(async (a) => {
|
|
93
|
+
try {
|
|
94
|
+
const full = await getAreaById(a._id);
|
|
95
|
+
if (!(full?.units || []).length) noUnits.push(a.name);
|
|
96
|
+
} catch {
|
|
97
|
+
// a failed lookup is not evidence of a missing unit
|
|
98
|
+
}
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
if (noUnits.length) {
|
|
102
|
+
found.push(
|
|
103
|
+
`${noUnits.length} ${plural(noUnits.length, "area has", "areas have")} no Units, so ${plural(noUnits.length, "it is", "they are")} skipped: ${noUnits
|
|
104
|
+
.slice(0, 3)
|
|
105
|
+
.join(", ")}${noUnits.length > 3 ? "..." : ""}.`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
problems.value = found;
|
|
109
|
+
} catch {
|
|
110
|
+
problems.value = [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
onMounted(check);
|
|
115
|
+
watch(() => [props.site, props.serviceType], check);
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<style scoped>
|
|
119
|
+
.schedule-guide {
|
|
120
|
+
margin-bottom: 12px;
|
|
121
|
+
}
|
|
122
|
+
.schedule-guide__text {
|
|
123
|
+
margin: 0 0 12px;
|
|
124
|
+
font-family: var(--font-sans);
|
|
125
|
+
font-size: var(--fs-cell);
|
|
126
|
+
color: var(--text2);
|
|
127
|
+
}
|
|
128
|
+
.schedule-guide__list {
|
|
129
|
+
margin: 0;
|
|
130
|
+
padding-left: 18px;
|
|
131
|
+
}
|
|
132
|
+
</style>
|