@7365admin1/layer-common 4.97.1-staging.578 → 4.97.1-staging.580

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.
@@ -49,7 +49,7 @@
49
49
  <StatusChip
50
50
  class="text-capitalize"
51
51
  :status="status"
52
- :label="status || 'N/A'"
52
+ :label="status === 'N/A' ? status : dailyStatusLabel(status)"
53
53
  />
54
54
  </v-col>
55
55
  </v-row>
@@ -125,7 +125,7 @@
125
125
  pill
126
126
  class="ml-2"
127
127
  >
128
- Schedule Task
128
+ Extra visit
129
129
  </StatusChip>
130
130
  </v-list-subheader>
131
131
 
@@ -165,8 +165,8 @@
165
165
  green as on every other screen. -->
166
166
  <StatusChip
167
167
  class="text-capitalize"
168
- :status="unit.status || 'Open'"
169
- :label="unit.status || 'Open'"
168
+ :status="historyUnitLabel(unit.status)"
169
+ :label="historyUnitLabel(unit.status)"
170
170
  />
171
171
  </v-col>
172
172
  </v-row>
@@ -262,6 +262,7 @@
262
262
  </template>
263
263
 
264
264
  <script setup lang="ts">
265
+ import { dailyStatusLabel, historyUnitLabel } from "../utils/schedule-task";
265
266
  const props = defineProps({
266
267
  orgId: { type: String, required: true, default: "" },
267
268
  site: { type: String, required: true, default: "" },
@@ -1,9 +1,17 @@
1
1
  <template>
2
2
  <v-row no-gutters align="center" justify="center">
3
3
  <v-col cols="12" lg="12">
4
+ <ScheduleGuide
5
+ v-if="canViewSchedules"
6
+ :org-id="orgId"
7
+ :site="site"
8
+ :service-type="serviceType"
9
+ :schedule-route="scheduleRoute"
10
+ text="A new checklist is created for every area automatically each midnight. Staff tick off each unit on the mobile app."
11
+ />
4
12
  <TableMain
5
13
  v-if="canViewSchedules"
6
- :title="checklistTitle"
14
+ :title="pageTitle"
7
15
  :items="items"
8
16
  :headers="headers"
9
17
  :loading="loading"
@@ -15,6 +23,28 @@
15
23
  @refresh="onRefresh"
16
24
  @row-click="onRowClick"
17
25
  >
26
+ <template #no-data>
27
+ <div class="cs-empty">
28
+ <strong>No daily lists yet.</strong>
29
+ <span>
30
+ A list is created automatically every midnight, once your Areas
31
+ and Units are set up.
32
+ </span>
33
+ </div>
34
+ </template>
35
+
36
+ <template #actions>
37
+ <v-btn
38
+ v-if="showCreateToday"
39
+ class="text-none screen-btn-primary"
40
+ variant="flat"
41
+ :loading="creatingToday"
42
+ @click="createTodaysList"
43
+ >
44
+ Create today's list now
45
+ </v-btn>
46
+ </template>
47
+
18
48
  <template #extension>
19
49
  <v-row no-gutters class="w-100 d-flex flex-column">
20
50
  <div class="filter-bar">
@@ -73,7 +103,7 @@
73
103
  <StatusChip
74
104
  class="text-capitalize"
75
105
  :status="value"
76
- :label="value || 'No Status'"
106
+ :label="dailyStatusLabel(value)"
77
107
  />
78
108
  </template>
79
109
  <template #item.completedAt="{ value }">
@@ -110,16 +140,22 @@
110
140
  </div>
111
141
  </AppCard>
112
142
  </div>
143
+ <Snackbar
144
+ v-model="messageSnackbar"
145
+ :text="message"
146
+ :color="messageColor"
147
+ />
113
148
  </v-col>
114
149
  </v-row>
115
150
  </template>
116
151
 
117
152
  <script lang="ts" setup>
118
153
  import useCleaningSchedules from "../composables/useCleaningSchedules";
119
- import useWorkerTerm from "../composables/useWorkerTerm";
120
- import { workerTitle } from "../utils/worker-terms";
121
- // "Cleaner Checklist" in Hygiene, "Gardener Checklist" in Landscape, ...
122
- const checklistTitle = workerTitle(`${useWorkerTerm().one} checklist`);
154
+ import {
155
+ dailyStatusLabel,
156
+ hasListForToday,
157
+ scheduleNameFor,
158
+ } from "../utils/schedule-task";
123
159
  import useUtils from "../composables/useUtils";
124
160
 
125
161
  const props = defineProps({
@@ -132,16 +168,24 @@ const props = defineProps({
132
168
  canViewScheduleDetails: { type: Boolean, default: false },
133
169
  canDownloadSchedule: { type: Boolean, default: false },
134
170
  canManageScheduleTasks: { type: Boolean, default: false },
171
+ canGenerateChecklist: { type: Boolean, default: false },
135
172
  });
136
173
 
174
+ // "Daily Schedule - Cleaning" / "- Landscape" / "- Pest" / "- Pool" / "- Technician"
175
+ const pageTitle = `Daily Schedule – ${scheduleNameFor(props.serviceType).title.replace(
176
+ / Schedule$/,
177
+ "",
178
+ )}`;
179
+
137
180
  const startDate = ref("");
138
181
  const endDate = ref("");
139
182
  const status = ref<TScheduleAreaStatus>("all");
140
183
  const statusOptions = [
141
184
  { title: "All", value: "all" },
142
185
  { title: "Open", value: "open" },
143
- { title: "Ongoing", value: "ongoing" },
186
+ { title: "In progress", value: "ongoing" },
144
187
  { title: "Completed", value: "completed" },
188
+ { title: "Closed", value: "closed" },
145
189
  ] as const;
146
190
 
147
191
  const items = ref<Array<Record<string, any>>>([]);
@@ -159,8 +203,18 @@ const headers = [
159
203
  const remainingTime = ref({} as Record<string, string>);
160
204
  const remainingSeconds = ref({} as Record<string, number>);
161
205
  const downloadingId = ref<string | null>(null);
206
+ const creatingToday = ref(false);
162
207
 
163
- const { getCleaningSchedules, downloadChecklistPdf } = useCleaningSchedules();
208
+ const message = ref("");
209
+ const messageSnackbar = ref(false);
210
+ const messageColor = ref("error");
211
+
212
+ const {
213
+ getCleaningSchedules,
214
+ downloadChecklistPdf,
215
+ createTodaysParent,
216
+ generateChecklist,
217
+ } = useCleaningSchedules();
164
218
  const { formatDate } = useUtils();
165
219
 
166
220
  /**
@@ -280,6 +334,36 @@ const updateRemainingTime = () => {
280
334
  });
281
335
  };
282
336
 
337
+ // Only on the unfiltered first page, where today's row would be if it exists.
338
+ const showCreateToday = computed(
339
+ () =>
340
+ props.canGenerateChecklist &&
341
+ !loading.value &&
342
+ !!getCleanerChecklistReq.value &&
343
+ page.value === 1 &&
344
+ !startDate.value &&
345
+ !endDate.value &&
346
+ status.value === "all" &&
347
+ !hasListForToday(items.value),
348
+ );
349
+
350
+ async function createTodaysList() {
351
+ try {
352
+ creatingToday.value = true;
353
+ const parent = await createTodaysParent(props.site, props.serviceType);
354
+ await generateChecklist(props.site, parent.id);
355
+ showMessage("Today's list is ready.", "success");
356
+ await getCleanerChecklistRefresh();
357
+ } catch (error: any) {
358
+ showMessage(
359
+ error?.data?.message || error?.message || "Could not create today's list.",
360
+ "error",
361
+ );
362
+ } finally {
363
+ creatingToday.value = false;
364
+ }
365
+ }
366
+
283
367
  watchEffect(() => {
284
368
  if (getCleanerChecklistReq.value) {
285
369
  items.value = getCleanerChecklistReq.value.items;
@@ -333,7 +417,11 @@ async function downloadItem(item: any) {
333
417
  downloadingId.value = id;
334
418
  await downloadChecklistPdf(id);
335
419
  } catch (err) {
336
- console.error("Failed to download checklist PDF", err);
420
+ showMessage(
421
+ (err as any)?.data?.message ||
422
+ "Could not download the PDF. Please try again.",
423
+ "error",
424
+ );
337
425
  } finally {
338
426
  downloadingId.value = null;
339
427
  }
@@ -375,15 +463,22 @@ function onRowClick(data: any) {
375
463
  }
376
464
 
377
465
  function showMessage(msg: string, color: string = "error") {
378
- try {
379
- console.warn(msg);
380
- } catch (e) {
381
- alert(msg);
382
- }
466
+ message.value = msg;
467
+ messageColor.value = color;
468
+ messageSnackbar.value = true;
383
469
  }
384
470
  </script>
385
471
 
386
472
  <style scoped>
473
+ .cs-empty {
474
+ display: flex;
475
+ flex-direction: column;
476
+ gap: 4px;
477
+ padding: 24px 16px;
478
+ font-family: var(--font-sans);
479
+ font-size: var(--fs-cell);
480
+ color: var(--text2);
481
+ }
387
482
  .cs-no-permission {
388
483
  font-family: var(--font-sans);
389
484
  font-size: var(--fs-cell);
@@ -109,9 +109,9 @@
109
109
  <div class="d-flex align-center ga-2 mb-2">
110
110
  <v-icon color="error" size="18">mdi-close-circle</v-icon>
111
111
  <span class="text-caption font-weight-bold">
112
- Rejection details
112
+ Needs redo
113
113
  </span>
114
- <StatusChip status="Rejected" :dot="false" />
114
+ <StatusChip status="Needs redo" :dot="false" />
115
115
  </div>
116
116
  <div
117
117
  v-if="item.remarks"
@@ -146,7 +146,7 @@
146
146
  </v-row>
147
147
  </template>
148
148
 
149
- <!-- Approve / Reject action buttons -->
149
+ <!-- Done / Needs redo action buttons (labels only; the API still says approve / reject) -->
150
150
  <template #item-append="{ item, group }">
151
151
  <v-row
152
152
  v-if="!isScheduleClosed && canManageScheduleTasks"
@@ -165,6 +165,8 @@
165
165
  color="error"
166
166
  :loading="!!loadingActions[getKey(item, group.set)]"
167
167
  :disabled="!!loadingActions[getKey(item, group.set)]"
168
+ aria-label="Needs redo"
169
+ title="Needs redo"
168
170
  @click.stop="handleItemActionClick(item, group.set, 'reject')"
169
171
  />
170
172
  </v-col>
@@ -180,6 +182,8 @@
180
182
  color="success"
181
183
  :loading="!!loadingActions[getKey(item, group.set)]"
182
184
  :disabled="!!loadingActions[getKey(item, group.set)]"
185
+ aria-label="Done"
186
+ title="Done"
183
187
  @click.stop="handleItemActionClick(item, group.set, 'approve')"
184
188
  />
185
189
  </v-col>
@@ -190,7 +194,7 @@
190
194
  <v-row no-gutters align="center" class="ga-2">
191
195
  <v-col v-if="isGroupAnyApproved(group)" cols="auto">
192
196
  <StatusChip
193
- :status="isGroupComplete(group) ? 'Completed' : 'Ongoing'"
197
+ :status="isGroupComplete(group) ? 'Completed' : 'In progress'"
194
198
  :tone="isGroupComplete(group) ? 'ok' : 'warn'"
195
199
  :dot="false"
196
200
  class="text-none"
@@ -202,7 +206,7 @@
202
206
  : "mdi-progress-clock"
203
207
  }}
204
208
  </v-icon>
205
- {{ isGroupComplete(group) ? "Completed" : "Ongoing" }}
209
+ {{ isGroupComplete(group) ? "Completed" : "In progress" }}
206
210
  <template v-if="getGroupCompletedByName(group)">
207
211
  · {{ getGroupCompletedByName(group) }}
208
212
  </template>
@@ -316,7 +320,7 @@
316
320
  -->
317
321
  <v-dialog v-model="showRejectModal" max-width="600" persistent>
318
322
  <v-card class="screen-modal">
319
- <v-card-title>Reject Checklist Item</v-card-title>
323
+ <v-card-title>Needs redo</v-card-title>
320
324
 
321
325
  <v-divider />
322
326
 
@@ -901,14 +905,14 @@ async function submitRejectDialog(): Promise<void> {
901
905
  rejectModal.remarks.trim(),
902
906
  rejectModal.photoIds
903
907
  );
904
- showMessage(response?.message || "Unit rejected successfully", "success");
908
+ showMessage(response?.message || "Marked as needs redo", "success");
905
909
  showRejectModal.value = false;
906
910
  resetRejectModal();
907
911
  await getUnitCleanerChecklistRefresh();
908
912
  } catch (error: any) {
909
913
  console.error("Error rejecting unit checklist:", error);
910
914
  showMessage(
911
- error?.data?.message || error?.message || "Failed to reject checklist",
915
+ error?.data?.message || error?.message || "Could not mark this unit as needs redo",
912
916
  "error"
913
917
  );
914
918
  } finally {
@@ -985,7 +989,7 @@ async function _callUpdateUnitChecklist(
985
989
  (s) => s.unit === item.unit && s.set === set
986
990
  );
987
991
  if (isApproved && action === "approve") {
988
- showMessage("Unit already approved", "info");
992
+ showMessage("This unit is already done", "info");
989
993
  return;
990
994
  }
991
995
 
@@ -1000,7 +1004,7 @@ async function _callUpdateUnitChecklist(
1000
1004
  );
1001
1005
  showMessage(
1002
1006
  response?.message ||
1003
- `Unit ${action === "approve" ? "approved" : "rejected"} successfully`,
1007
+ `Unit marked as ${action === "approve" ? "done" : "needs redo"}`,
1004
1008
  "success"
1005
1009
  );
1006
1010
  await getUnitCleanerChecklistRefresh();
@@ -1071,13 +1075,13 @@ async function _updateUnitChecklist({
1071
1075
  );
1072
1076
  }
1073
1077
 
1074
- showMessage(`Set ${setNumber} approved successfully`, "success");
1078
+ showMessage(`Set ${setNumber} done`, "success");
1075
1079
 
1076
1080
  // Refresh data
1077
1081
  await getUnitCleanerChecklistRefresh();
1078
1082
  } catch (error: any) {
1079
1083
  console.error("Error approving set:", error);
1080
- showMessage(error?.message || "Failed to approve set", "error");
1084
+ showMessage(error?.message || "Could not finish this set", "error");
1081
1085
  } finally {
1082
1086
  submitting.value = false;
1083
1087
  }
@@ -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>