@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.
@@ -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="checklistTitle"
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="canGenerateChecklist"
107
+ v-if="showCreateList"
91
108
  class="text-none screen-btn-primary"
92
109
  variant="flat"
93
- aria-label="Generate"
110
+ aria-label="Create today's list now"
94
111
  @click="_generateChecklist"
95
112
  :loading="submitting"
96
- :disabled="isScheduleClosed"
97
113
  >
98
- Generate List
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 || 'No Status'"
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 useWorkerTerm from "../composables/useWorkerTerm";
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: "No. of Visits", value: "set" },
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: "Ongoing", value: "ongoing" },
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, acceptAreaChecklist, generateChecklist } =
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("Checklist generated successfully.", "success");
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 || "Failed to generate checklist.", "error");
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>