@7365admin1/layer-common 4.133.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,14 @@
1
1
  <template>
2
2
  <v-row no-gutters align="center" justify="center">
3
3
  <v-col cols="12">
4
+ <ScheduleGuide
5
+ :org-id="orgId"
6
+ :site="site"
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
+ />
4
10
  <TableMain
5
- :title="'Schedule Tasks'"
11
+ :title="'Extra Visits (Scheduled)'"
6
12
  :items="items"
7
13
  :headers="headers"
8
14
  :loading="loading"
@@ -10,13 +16,23 @@
10
16
  v-model:page="page"
11
17
  :pages="pages"
12
18
  :pageRange="pageRange"
13
- :no-data-text="'No schedule tasks found'"
14
19
  @refresh="getTaskRefresh"
15
20
  :canCreate="canCreateScheduleTask"
16
- createLabel="Create Schedule Task"
21
+ createLabel="Add Extra Visit"
17
22
  @create="openFormDialog"
18
23
  @row-click="onRowClick"
19
24
  >
25
+ <template #no-data>
26
+ <div class="st-empty">
27
+ <strong>No extra visits yet.</strong>
28
+ <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.
32
+ </span>
33
+ </div>
34
+ </template>
35
+
20
36
  <template #actions>
21
37
  <v-row no-gutters align="center" class="w-100">
22
38
  <v-col cols="auto" v-if="canCreateScheduleTask">
@@ -25,7 +41,7 @@
25
41
  variant="flat"
26
42
  @click="openFormDialog"
27
43
  >
28
- Create Schedule Task
44
+ Add Extra Visit
29
45
  </v-btn>
30
46
  </v-col>
31
47
 
@@ -66,13 +82,73 @@
66
82
  <span v-else>N/A</span>
67
83
  </template>
68
84
 
69
- <template #item.status="{ value }">
85
+ <template #item.when="{ item }">
86
+ <span class="st-when">{{ item.whenText }}</span>
87
+ </template>
88
+
89
+ <template #item.nextRun="{ item }">
90
+ {{ item.nextRunText || "-" }}
91
+ </template>
92
+
93
+ <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>
105
+ </template>
106
+
107
+ <template #item.status="{ item }">
70
108
  <StatusChip
71
- class="text-capitalize"
72
- :status="value"
73
- :label="value || 'N/A'"
109
+ :status="item.stateLabel"
110
+ :label="item.stateLabel"
74
111
  />
75
112
  </template>
113
+
114
+ <template #item.actions="{ item }">
115
+ <div class="st-actions">
116
+ <v-btn
117
+ v-if="canUpdateScheduleTask"
118
+ size="small"
119
+ variant="text"
120
+ class="text-none"
121
+ prepend-icon="mdi-pencil-outline"
122
+ @click.stop="onEditRow(item)"
123
+ >
124
+ Edit
125
+ </v-btn>
126
+ <v-btn
127
+ v-if="canUpdateScheduleTask && item.stateKey !== 'ended'"
128
+ size="small"
129
+ variant="text"
130
+ class="text-none"
131
+ :prepend-icon="
132
+ item.stateKey === 'paused' ? 'mdi-play-outline' : 'mdi-pause'
133
+ "
134
+ :loading="pausingId === item._id"
135
+ @click.stop="togglePause(item)"
136
+ >
137
+ {{ item.stateKey === "paused" ? "Resume" : "Pause" }}
138
+ </v-btn>
139
+ <v-btn
140
+ v-if="canDeleteScheduleTask"
141
+ size="small"
142
+ variant="text"
143
+ color="error"
144
+ class="text-none"
145
+ prepend-icon="mdi-delete-outline"
146
+ @click.stop="onDeleteRow(item)"
147
+ >
148
+ Delete
149
+ </v-btn>
150
+ </div>
151
+ </template>
76
152
  </TableMain>
77
153
 
78
154
  <ScheduleTaskForm
@@ -86,14 +162,14 @@
86
162
 
87
163
  <v-dialog v-model="dialogShowMoreActions" width="400" persistent>
88
164
  <HygieneUpdateMoreAction
89
- :title="selectedTask?.title || 'Schedule Task Actions'"
165
+ :title="selectedTask?.title || 'Extra Visit'"
90
166
  :canUpdate="canUpdateScheduleTask"
91
167
  @close="dialogShowMoreActions = false"
92
168
  :canDelete="canDeleteScheduleTask"
93
- :deleteButtonLabel="'Delete Schedule Task'"
169
+ :deleteButtonLabel="'Delete Extra Visit'"
94
170
  @edit="onEditFromMoreAction"
95
171
  @delete="onDeleteFromMoreAction"
96
- :editButtonLabel="'Edit Schedule Task'"
172
+ :editButtonLabel="'Edit Extra Visit'"
97
173
  >
98
174
  <template #content>
99
175
  <v-row no-gutters>
@@ -109,32 +185,28 @@
109
185
 
110
186
  <v-row no-gutters class="mb-2">
111
187
  <v-col cols="5" class="text-subtitle-2 font-weight-bold">
112
- Dates:
188
+ When:
113
189
  </v-col>
114
190
  <v-col cols="7" class="text-subtitle-2">
115
- <div
116
- v-if="selectedTask.dates && selectedTask.dates.length > 0"
117
- class="d-flex flex-wrap gap-1"
118
- >
119
- <v-chip
120
- v-for="date in selectedTask.dates"
121
- :key="date"
122
- size="x-small"
123
- class="mr-1 mb-1"
124
- >
125
- {{ formatDateChip(date) }}
126
- </v-chip>
127
- </div>
128
- <div v-else>N/A</div>
191
+ {{ selectedWhen }}
192
+ </v-col>
193
+ </v-row>
194
+
195
+ <v-row no-gutters class="mb-2">
196
+ <v-col cols="5" class="text-subtitle-2 font-weight-bold">
197
+ Next run:
198
+ </v-col>
199
+ <v-col cols="7" class="text-subtitle-2">
200
+ {{ selectedNext || "-" }}
129
201
  </v-col>
130
202
  </v-row>
131
203
 
132
- <v-row v-if="selectedTask.time" no-gutters class="mb-2">
204
+ <v-row no-gutters class="mb-2">
133
205
  <v-col cols="5" class="text-subtitle-2 font-weight-bold">
134
- Time:
206
+ Last run:
135
207
  </v-col>
136
208
  <v-col cols="7" class="text-subtitle-2">
137
- {{ selectedTask.time || "N/A" }}
209
+ {{ lastRunLabel(selectedTask) }}
138
210
  </v-col>
139
211
  </v-row>
140
212
 
@@ -192,8 +264,8 @@
192
264
  v-if="
193
265
  loadingViewUnits &&
194
266
  !(
195
- area.value ||
196
- area._id in fetchedViewAreaUnits
267
+ (area.value || area._id) in
268
+ fetchedViewAreaUnits
197
269
  )
198
270
  "
199
271
  size="14"
@@ -246,7 +318,8 @@
246
318
  </v-chip>
247
319
  </div>
248
320
  <span v-else class="text-caption text-disabled"
249
- >No units configured</span
321
+ >No units in this area, so it is skipped when
322
+ this visit runs.</span
250
323
  >
251
324
  </v-card-text>
252
325
  </div>
@@ -266,15 +339,14 @@
266
339
  </v-col>
267
340
  </v-row>
268
341
 
269
- <v-row v-if="selectedTask.status" no-gutters class="mb-2">
342
+ <v-row no-gutters class="mb-2">
270
343
  <v-col cols="5" class="text-subtitle-2 font-weight-bold">
271
344
  Status:
272
345
  </v-col>
273
346
  <v-col cols="7" class="text-subtitle-2">
274
347
  <StatusChip
275
- class="text-capitalize"
276
- :status="selectedTask.status"
277
- :label="selectedTask.status || 'N/A'"
348
+ :status="stateOf(selectedTask).label"
349
+ :label="stateOf(selectedTask).label"
278
350
  />
279
351
  </v-col>
280
352
  </v-row>
@@ -307,11 +379,12 @@
307
379
  <v-dialog v-model="dialogDeleteTask" max-width="520">
308
380
  <v-card class="screen-modal">
309
381
  <v-card-title class="text-capitalize">
310
- Delete {{ selectedTask?.title || "Schedule Task" }}
382
+ Delete {{ selectedTask?.title || "Extra Visit" }}
311
383
  </v-card-title>
312
384
 
313
385
  <v-card-text class="screen-modal__body">
314
- Are you sure you want to delete this schedule task?
386
+ This stops all future runs of this extra visit. Visits that already
387
+ happened are kept. Delete it?
315
388
  </v-card-text>
316
389
 
317
390
  <div class="screen-modal__footer">
@@ -348,6 +421,13 @@
348
421
  import useScheduleTask from "../composables/useScheduleTask";
349
422
  import useUtils from "../composables/useUtils";
350
423
  import useAreas from "../composables/useAreas";
424
+ import {
425
+ STATUS_LABELS,
426
+ describeWhen,
427
+ fmtMoment,
428
+ lastRunLabel,
429
+ scheduleNameFor,
430
+ } from "../utils/schedule-task";
351
431
 
352
432
  const props = defineProps({
353
433
  orgId: { type: String, required: true },
@@ -362,8 +442,12 @@ const props = defineProps({
362
442
  });
363
443
 
364
444
  const { formatDate, debounce } = useUtils();
365
- const { getScheduleTasks, getScheduleTaskById, deleteScheduleTask } =
366
- useScheduleTask();
445
+ const {
446
+ getScheduleTasks,
447
+ getScheduleTaskById,
448
+ deleteScheduleTask,
449
+ setScheduleTaskPaused,
450
+ } = useScheduleTask();
367
451
  const { getAreaById } = useAreas();
368
452
 
369
453
  const canViewScheduleTasks = computed(() => props.canViewScheduleTasks);
@@ -374,6 +458,8 @@ const canViewScheduleTaskDetails = computed(
374
458
  () => props.canViewScheduleTaskDetails
375
459
  );
376
460
 
461
+ const scheduleName = scheduleNameFor(props.serviceType).title;
462
+
377
463
  const page = ref(1);
378
464
  const pages = ref(0);
379
465
  const pageRange = ref("-- - -- of --");
@@ -433,41 +519,28 @@ const items = ref<any[]>([]);
433
519
  const headers = [
434
520
  { title: "Title", value: "title" },
435
521
  { title: "Areas", value: "areas" },
436
- { title: "Status", value: "status" },
522
+ { title: "When", value: "when", sortable: false },
523
+ { title: "Next run", value: "nextRun", sortable: false },
524
+ { title: "Last run", value: "lastRun", sortable: false },
525
+ { title: "Status", value: "status", sortable: false },
526
+ { title: "", value: "actions", sortable: false, align: "end" as const },
437
527
  ];
438
528
 
529
+ // Active / Paused / Ended. An older server without `scheduleStatus` shows Active.
530
+ function stateOf(row: Record<string, any>) {
531
+ const key = row?.scheduleStatus || (row?.paused ? "paused" : "active");
532
+ return { key: key as string, label: STATUS_LABELS[key] || "Active" };
533
+ }
534
+ const selectedWhen = computed(() => describeWhen(selectedTask.value as any));
535
+ const selectedNext = computed(() => fmtMoment(selectedTask.value?.nextRun));
536
+ const pausingId = ref("");
537
+
439
538
  const showMessage = (text: string, color: string = "success") => {
440
539
  message.value = text;
441
540
  messageColor.value = color;
442
541
  messageSnackbar.value = true;
443
542
  };
444
543
 
445
- const formatDateChip = (dateStr: string) => {
446
- const parts = dateStr?.split("-");
447
- if (parts?.length === 3) return `${parts[2]}-${parts[1]}-${parts[0]}`;
448
- return dateStr;
449
- };
450
-
451
- const mapShortToDay = (short: string) => {
452
- const mapping: Record<string, string> = {
453
- Mon: "Monday",
454
- Tue: "Tuesday",
455
- Wed: "Wednesday",
456
- Thu: "Thursday",
457
- Fri: "Friday",
458
- Sat: "Saturday",
459
- Sun: "Sunday",
460
- };
461
- return mapping[short] || short;
462
- };
463
-
464
- const formatTaskDays = (dayField: any) => {
465
- if (!dayField) return "N/A";
466
- if (Array.isArray(dayField))
467
- return dayField.map((d) => mapShortToDay(d)).join(", ");
468
- return String(dayField);
469
- };
470
-
471
544
  const {
472
545
  data: getTaskReq,
473
546
  refresh: getTaskRefresh,
@@ -488,7 +561,18 @@ const {
488
561
 
489
562
  watchEffect(() => {
490
563
  if (getTaskReq.value) {
491
- items.value = getTaskReq.value.items || getTaskReq.value || [];
564
+ const rows: any[] = getTaskReq.value.items || getTaskReq.value || [];
565
+ items.value = rows.map((row) => {
566
+ const st = stateOf(row);
567
+ return {
568
+ ...row,
569
+ whenText: describeWhen(row),
570
+ nextRunText: fmtMoment(row.nextRun),
571
+ lastRunText: lastRunLabel(row),
572
+ stateKey: st.key,
573
+ stateLabel: st.label,
574
+ };
575
+ });
492
576
  pages.value = getTaskReq.value.pages || 0;
493
577
  pageRange.value = getTaskReq.value.pageRange || "-- - -- of --";
494
578
  }
@@ -510,7 +594,41 @@ function openFormDialog() {
510
594
  showForm.value = true;
511
595
  }
512
596
 
597
+ function onEditRow(item: Record<string, any>) {
598
+ selectedTask.value = item;
599
+ formMode.value = "edit";
600
+ showForm.value = true;
601
+ }
602
+
603
+ function onDeleteRow(item: Record<string, any>) {
604
+ selectedTask.value = item;
605
+ dialogDeleteTask.value = true;
606
+ }
607
+
608
+ async function togglePause(item: Record<string, any>) {
609
+ const resuming = item.stateKey === "paused";
610
+ try {
611
+ pausingId.value = item._id;
612
+ await setScheduleTaskPaused(item._id, !resuming);
613
+ showMessage(
614
+ resuming
615
+ ? "Resumed. It will run again from its next date."
616
+ : "Paused. It will not run until you resume it.",
617
+ "success",
618
+ );
619
+ await getTaskRefresh();
620
+ } catch (error: any) {
621
+ showMessage(
622
+ error?.data?.message || "Could not change this extra visit.",
623
+ "error",
624
+ );
625
+ } finally {
626
+ pausingId.value = "";
627
+ }
628
+ }
629
+
513
630
  async function onRowClick(data: any) {
631
+ if (!canViewScheduleTaskDetails.value) return;
514
632
  const taskId = data?.item?._id || data?._id || data?.item?.id || data?.id;
515
633
 
516
634
  if (taskId) {
@@ -550,13 +668,36 @@ async function _deleteScheduleTask() {
550
668
 
551
669
  const response = await deleteScheduleTask(id);
552
670
  dialogDeleteTask.value = false;
553
- showMessage("Scheduled task successfully deleted.", "success");
671
+ showMessage("Extra visit deleted. Past visits are kept.", "success");
554
672
 
555
673
  await getTaskRefresh();
556
674
  } catch (error: any) {
557
- showMessage(error?.data?.message, "error");
675
+ showMessage(
676
+ error?.data?.message || "Could not delete this extra visit.",
677
+ "error",
678
+ );
558
679
  } finally {
559
680
  submitting.value = false;
560
681
  }
561
682
  }
562
683
  </script>
684
+
685
+ <style scoped>
686
+ .st-empty {
687
+ display: flex;
688
+ flex-direction: column;
689
+ gap: 4px;
690
+ padding: 24px 16px;
691
+ font-family: var(--font-sans);
692
+ font-size: var(--fs-cell);
693
+ color: var(--text2);
694
+ }
695
+ .st-when {
696
+ white-space: normal;
697
+ }
698
+ .st-actions {
699
+ display: flex;
700
+ justify-content: flex-end;
701
+ flex-wrap: wrap;
702
+ }
703
+ </style>
@@ -92,7 +92,7 @@
92
92
  class="text-none"
93
93
  >
94
94
  <v-icon size="12">mdi-calendar-clock</v-icon>
95
- Schedule Task
95
+ Extra visit
96
96
  </StatusChip>
97
97
  <slot name="group-header-chips" :group="group">
98
98
  <StatusChip
@@ -114,7 +114,7 @@
114
114
  class="text-none"
115
115
  >
116
116
  <v-icon size="12">mdi-progress-clock</v-icon>
117
- Ongoing · {{ group.completedByName }}
117
+ In progress · {{ group.completedByName }}
118
118
  </StatusChip>
119
119
  </slot>
120
120
  </v-col>
@@ -140,6 +140,14 @@ export default function useCleaningSchedules() {
140
140
  );
141
141
  }
142
142
 
143
+ /** Today's day-row, created if missing (safe to repeat). Follow with generateChecklist(site, id). */
144
+ function createTodaysParent(site: string, serviceType: string) {
145
+ return useNuxtApp().$api<{ id: string }>(`/api/hygiene-parent-checklist`, {
146
+ method: "POST",
147
+ body: { site, serviceType },
148
+ });
149
+ }
150
+
143
151
  async function getAreaChecklistHistory({
144
152
  parentChecklistId = "",
145
153
  page = 1,
@@ -231,6 +239,7 @@ export default function useCleaningSchedules() {
231
239
  updateAreaChecklistStatus,
232
240
  uploadAreaChecklistAttachment,
233
241
  generateChecklist,
242
+ createTodaysParent,
234
243
  getAreaChecklistHistory,
235
244
  getAreaChecklistHistoryById,
236
245
  exportAreas,
@@ -73,6 +73,7 @@ export default function useFeedback() {
73
73
  provider = "",
74
74
  service = "",
75
75
  userId = "",
76
+ category = "",
76
77
  }: {
77
78
  page?: number;
78
79
  limit?: number;
@@ -85,6 +86,8 @@ export default function useFeedback() {
85
86
  provider?: string | any;
86
87
  service?: string;
87
88
  userId?: string;
89
+ /** Optional: one category (the feedback subject) or a comma list. Empty = all. */
90
+ category?: string;
88
91
  } = {}) {
89
92
  try {
90
93
  return useNuxtApp().$api<Record<string, any>>("/api/feedbacks2", {
@@ -100,6 +103,7 @@ export default function useFeedback() {
100
103
  status,
101
104
  provider,
102
105
  service,
106
+ ...(category && { category }),
103
107
  ...(currentUser.value?.type != "site" && {
104
108
  userId,
105
109
  }),
@@ -1,3 +1,19 @@
1
+ // Repeat fields (all optional: a payload without them behaves as a one-off on `dates`).
2
+ // "" / null clears an optional field on the server.
3
+ type TScheduleRepeatFields = {
4
+ repeat?: "none" | "daily" | "weekdays" | "weekly" | "monthly";
5
+ daysOfWeek?: number[] | null;
6
+ startDate?: string | null;
7
+ endDate?: string | null;
8
+ };
9
+
10
+ const repeatBody = (p: TScheduleRepeatFields) => ({
11
+ repeat: p.repeat,
12
+ daysOfWeek: p.daysOfWeek,
13
+ startDate: p.startDate,
14
+ endDate: p.endDate,
15
+ });
16
+
1
17
  export default function useScheduleTask() {
2
18
  async function getScheduleTasks({
3
19
  site = "",
@@ -38,7 +54,7 @@ export default function useScheduleTask() {
38
54
  areas: { name: string; value: string }[];
39
55
  site: string;
40
56
  serviceType: string;
41
- }) {
57
+ } & TScheduleRepeatFields) {
42
58
  return useNuxtApp().$api<Record<string, any>>(
43
59
  `/api/hygiene-schedule-tasks/site/${payload.site}`,
44
60
  {
@@ -50,6 +66,7 @@ export default function useScheduleTask() {
50
66
  description: payload.description,
51
67
  areas: payload.areas,
52
68
  serviceType: payload.serviceType,
69
+ ...repeatBody(payload),
53
70
  },
54
71
  },
55
72
  );
@@ -63,7 +80,7 @@ export default function useScheduleTask() {
63
80
  dates: string[];
64
81
  description?: string;
65
82
  areas: { name: string; value: string }[];
66
- },
83
+ } & TScheduleRepeatFields,
67
84
  ) {
68
85
  return useNuxtApp().$api<Record<string, any>>(
69
86
  `/api/hygiene-schedule-tasks/id/${id}`,
@@ -75,11 +92,23 @@ export default function useScheduleTask() {
75
92
  dates: payload.dates,
76
93
  description: payload.description,
77
94
  areas: payload.areas,
95
+ ...repeatBody(payload),
78
96
  },
79
97
  },
80
98
  );
81
99
  }
82
100
 
101
+ /** Pause / resume. The server stops the next runs while paused; nothing is deleted. */
102
+ function setScheduleTaskPaused(id: string, paused: boolean) {
103
+ return useNuxtApp().$api<Record<string, any>>(
104
+ `/api/hygiene-schedule-tasks/id/${id}`,
105
+ {
106
+ method: "PATCH",
107
+ body: { paused },
108
+ },
109
+ );
110
+ }
111
+
83
112
  function deleteScheduleTask(id: string) {
84
113
  return useNuxtApp().$api<Record<string, any>>(
85
114
  `/api/hygiene-schedule-tasks/id/${id}`,
@@ -95,5 +124,6 @@ export default function useScheduleTask() {
95
124
  updateScheduleTask,
96
125
  getScheduleTaskById,
97
126
  deleteScheduleTask,
127
+ setScheduleTaskPaused,
98
128
  };
99
129
  }
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.133.0",
5
+ "version": "4.134.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.",
@@ -53,5 +53,5 @@ declare type TFlattenedUnitItem = TUnitChecklistItem & {
53
53
  set: number;
54
54
  };
55
55
 
56
- declare type TScheduleAreaStatus = "All" | "Open" | "Ongoing" | "Completed";
56
+ declare type TScheduleAreaStatus = "All" | "Open" | "Ongoing" | "Completed" | "Closed";
57
57
  declare type TAreaType = "all" | "common" | "toilet";