@7365admin1/layer-common 4.97.1-staging.596 → 4.97.1-staging.597

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.
@@ -117,7 +117,7 @@
117
117
  v-if="checklistSet.set !== undefined"
118
118
  class="text-subtitle-2 font-weight-bold px-0"
119
119
  >
120
- Set {{ checklistSet.set }}
120
+ {{ visitLabel(checklistSet.set) }}
121
121
  <StatusChip
122
122
  v-if="checklistSet.isScheduleTask"
123
123
  tone="info"
@@ -262,7 +262,7 @@
262
262
  </template>
263
263
 
264
264
  <script setup lang="ts">
265
- import { dailyStatusLabel, extraCheckLabel, historyUnitLabel } from "../utils/schedule-task";
265
+ import { dailyStatusLabel, extraCheckLabel, historyUnitLabel, visitLabel } from "../utils/schedule-task";
266
266
  const props = defineProps({
267
267
  orgId: { type: String, required: true, default: "" },
268
268
  site: { type: String, required: true, default: "" },
@@ -14,8 +14,10 @@
14
14
  <ol class="sg-steps">
15
15
  <li>
16
16
  Every midnight a new row appears here for the day. It holds every
17
- area, with as many visits as the area's "visits per day", plus
18
- any <strong>Extra Checks</strong> set for that day.
17
+ area, with as many visits as the area's "visits per day". An
18
+ <strong>extra check</strong> from the <strong>Task Calendar</strong>
19
+ ("My extra checks") adds one more visit to its areas at its set
20
+ time that day.
19
21
  </li>
20
22
  <li>
21
23
  Staff open the same list in their mobile app and tick each unit
@@ -451,7 +453,7 @@ async function downloadItem(item: any) {
451
453
 
452
454
  try {
453
455
  downloadingId.value = id;
454
- await downloadChecklistPdf(id);
456
+ await downloadChecklistPdf(id, item?.createdAt);
455
457
  } catch (err) {
456
458
  showMessage(
457
459
  (err as any)?.data?.message ||
@@ -468,6 +470,8 @@ const selectedScheduleStatus = useState<string>(
468
470
  () => ""
469
471
  );
470
472
 
473
+ const selectedScheduleDay = useState<string>("selectedScheduleDay", () => "");
474
+
471
475
  const isItemClosed = (item: any): boolean =>
472
476
  String(item?.status ?? "").toLowerCase() === "closed";
473
477
 
@@ -483,8 +487,9 @@ function onRowClick(data: any) {
483
487
  return;
484
488
  }
485
489
 
486
- // Store status so nested pages know whether the schedule is closed
490
+ // Store status and day so nested pages know whether the list is closed and which day it is
487
491
  selectedScheduleStatus.value = String(item?.status ?? "");
492
+ selectedScheduleDay.value = String(item?.createdAt ?? "");
488
493
 
489
494
  if (props.type === "toilet") {
490
495
  const path = `/${props.orgId}/${props.site}/toilet-checklist/${id}`;
@@ -16,6 +16,13 @@
16
16
  </StatusChip>
17
17
  </v-col>
18
18
  </v-row>
19
+ <p class="checklist-intro" data-test="checklist-intro">
20
+ Each visit lists this area's units. Staff tick them on their phone; here
21
+ you can mark a unit <strong>Done</strong> (&#10003;) or send it back with
22
+ <strong>Needs redo</strong> (&#10007;, with a remark). The last unit of a
23
+ visit needs a photo. A visit marked "Extra check" was added from the Task
24
+ Calendar.
25
+ </p>
19
26
  <TableHygiene
20
27
  :title="scheduleTitle"
21
28
  :headers="headers"
@@ -249,8 +256,8 @@
249
256
  <v-card-title>
250
257
  {{
251
258
  modalData.currentSet !== undefined
252
- ? `Set ${modalData.currentSet} Completed`
253
- : "All Items Checked"
259
+ ? `${visitLabel(modalData.currentSet)} done`
260
+ : "All units ticked"
254
261
  }}
255
262
  </v-card-title>
256
263
 
@@ -285,7 +292,7 @@
285
292
  v-if="!modalData.photoIds.length"
286
293
  class="text-caption text-medium-emphasis mt-1"
287
294
  >
288
- At least one photo is required to complete this task. You can take
295
+ At least one photo is needed to finish this visit. You can take
289
296
  or upload more than one.
290
297
  </p>
291
298
  </v-col>
@@ -382,7 +389,7 @@
382
389
  <v-dialog v-model="showAttachmentDialog" max-width="700" scrollable>
383
390
  <v-card class="screen-modal">
384
391
  <v-card-title class="d-flex align-center">
385
- <span>Set {{ attachmentDialogSetNum }} — Attachments</span>
392
+ <span>{{ visitLabel(attachmentDialogSetNum) }} — photos</span>
386
393
  <v-spacer />
387
394
  <v-btn
388
395
  icon="mdi-close"
@@ -462,6 +469,7 @@
462
469
 
463
470
  <script setup lang="ts">
464
471
  import { readListPage } from "../utils/list-page";
472
+ import { isClosedDay, visitLabel } from "../utils/schedule-task";
465
473
  import { isPhotoRefusal, isSetFinished } from "../utils/checklist-set";
466
474
  import useCleaningSchedules from "../composables/useCleaningSchedules";
467
475
  import useWorkerTerm from "../composables/useWorkerTerm";
@@ -504,8 +512,9 @@ const selectedScheduleStatus = useState<string>(
504
512
  "selectedScheduleStatus",
505
513
  () => ""
506
514
  );
507
- const isScheduleClosed = computed(
508
- () => selectedScheduleStatus.value.toLowerCase() === "closed"
515
+ // Opened from the day list it knows; from a link or a refresh the units tell.
516
+ const isScheduleClosed = computed(() =>
517
+ isClosedDay(selectedScheduleStatus.value, items.value as any[]),
509
518
  );
510
519
 
511
520
  const page = ref<number>(1);
@@ -1075,13 +1084,13 @@ async function _updateUnitChecklist({
1075
1084
  );
1076
1085
  }
1077
1086
 
1078
- showMessage(`Set ${setNumber} done`, "success");
1087
+ showMessage(`${visitLabel(setNumber)} done`, "success");
1079
1088
 
1080
1089
  // Refresh data
1081
1090
  await getUnitCleanerChecklistRefresh();
1082
1091
  } catch (error: any) {
1083
1092
  console.error("Error approving set:", error);
1084
- showMessage(error?.message || "Could not finish this set", "error");
1093
+ showMessage(error?.message || "Could not finish this visit", "error");
1085
1094
  } finally {
1086
1095
  submitting.value = false;
1087
1096
  }
@@ -1089,6 +1098,11 @@ async function _updateUnitChecklist({
1089
1098
  </script>
1090
1099
 
1091
1100
  <style scoped>
1101
+ .checklist-intro {
1102
+ margin: 0 0 12px;
1103
+ font-size: var(--fs-cell);
1104
+ color: var(--text2);
1105
+ }
1092
1106
  /* The idle checklist circle. `--muted` is the design's de-emphasis token and
1093
1107
  clears the 3:1 bar a non-text indicator needs; Vuetify's `grey-lighten-2`
1094
1108
  read 1.32:1. */
@@ -6,10 +6,10 @@
6
6
  :site="site"
7
7
  :service-type="serviceType"
8
8
  :schedule-route="scheduleRoute"
9
- text="These are the areas on this day's checklist. Click an area to see each unit, tick it as done, or send it back to be redone."
9
+ text="These are the areas on this day's checklist. Click an area to see each unit, tick it as done, or send it back to be redone. Visits today counts the area's normal visits plus any extra checks added today."
10
10
  />
11
11
  <TableMain
12
- :title="pageTitle"
12
+ :title="dayTitle"
13
13
  :headers="headers"
14
14
  :items="items"
15
15
  :loading="loading"
@@ -143,7 +143,12 @@
143
143
 
144
144
  <script setup lang="ts">
145
145
  import useCleaningSchedules from "../composables/useCleaningSchedules";
146
- import { dailyChecklistTitle, dailyStatusLabel } from "../utils/schedule-task";
146
+ import {
147
+ dailyChecklistTitle,
148
+ dailyStatusLabel,
149
+ dayLabel,
150
+ isClosedDay,
151
+ } from "../utils/schedule-task";
147
152
  import useUtils from "../composables/useUtils";
148
153
 
149
154
  const props = defineProps({
@@ -177,7 +182,8 @@ const headers = [
177
182
  { title: "Area", value: "name" },
178
183
  { title: "Status", value: "status" },
179
184
  ...(props.showAreaType ? [{ title: "Type", value: "type" }] : []),
180
- { title: "Visits per day", value: "set" },
185
+ // The highest visit number on today's list: normal visits plus any extra checks added.
186
+ { title: "Visits today", value: "set" },
181
187
  ];
182
188
 
183
189
  const { formatDate, back } = useUtils();
@@ -190,9 +196,16 @@ const selectedScheduleStatus = useState<string>(
190
196
  "selectedScheduleStatus",
191
197
  () => ""
192
198
  );
193
- const isScheduleClosed = computed(
194
- () => selectedScheduleStatus.value.toLowerCase() === "closed"
199
+ // Opened from the day list it knows; from a link or a refresh the rows tell.
200
+ const isScheduleClosed = computed(() =>
201
+ isClosedDay(selectedScheduleStatus.value, items.value),
195
202
  );
203
+ const selectedScheduleDay = useState<string>("selectedScheduleDay", () => "");
204
+ // "Daily Checklist – Cleaning – Sat, 24 Oct 2026" (the day only when known).
205
+ const dayTitle = computed(() => {
206
+ const day = dayLabel(selectedScheduleDay.value);
207
+ return day ? `${pageTitle} – ${day}` : pageTitle;
208
+ });
196
209
 
197
210
  const items = ref<Array<Record<string, any>>>([]);
198
211
 
@@ -202,6 +215,7 @@ const statusOptions = [
202
215
  { title: "Not started", value: "open" },
203
216
  { title: "In progress", value: "ongoing" },
204
217
  { title: "Done", value: "completed" },
218
+ { title: "Closed", value: "closed" },
205
219
  ];
206
220
  const areaTypeFilter = ref<TAreaType>("all");
207
221
  const typeOptions = [
@@ -132,8 +132,14 @@ async function check() {
132
132
  }
133
133
 
134
134
  const noUnits: string[] = [];
135
+ // A newer server lists each area's unit count: no lookups at all. Older
136
+ // replies (or cached ones) fall back to one capped lookup per area.
137
+ for (const a of areas) {
138
+ if (typeof a.unitCount === "number" && a.unitCount === 0) noUnits.push(a.name);
139
+ }
140
+ const unknown = areas.filter((a) => typeof a.unitCount !== "number");
135
141
  await Promise.all(
136
- areas.slice(0, MAX_UNIT_CHECKS).map(async (a) => {
142
+ unknown.slice(0, MAX_UNIT_CHECKS).map(async (a) => {
137
143
  try {
138
144
  const full = await getAreaById(a._id);
139
145
  if (!(full?.units || []).length) noUnits.push(a.name);
@@ -83,7 +83,7 @@
83
83
  <span
84
84
  class="text-caption font-weight-bold text-medium-emphasis text-uppercase"
85
85
  >
86
- Set {{ group.set }}
86
+ {{ visitLabel(group.set) }}
87
87
  </span>
88
88
  <StatusChip
89
89
  v-if="group.isScheduleTask"
@@ -206,7 +206,7 @@
206
206
  </template>
207
207
 
208
208
  <script lang="ts" setup>
209
- import { extraCheckLabel } from "../utils/schedule-task";
209
+ import { extraCheckLabel, visitLabel } from "../utils/schedule-task";
210
210
  defineOptions({ inheritAttrs: false });
211
211
 
212
212
  const props = defineProps({
@@ -1,3 +1,5 @@
1
+ import { checklistPdfName } from "../utils/schedule-task";
2
+
1
3
  export default function useCleaningSchedules() {
2
4
  const _cleanerChecklists = ref({
3
5
  _id: "",
@@ -199,7 +201,8 @@ export default function useCleaningSchedules() {
199
201
  window.URL.revokeObjectURL(downloadUrl);
200
202
  }
201
203
 
202
- async function downloadChecklistPdf(scheduleId: string) {
204
+ /** `day`: the day the list is for (the row's createdAt), used to name the file. */
205
+ async function downloadChecklistPdf(scheduleId: string, day?: string | Date) {
203
206
  const res = await useNuxtApp().$api<Blob | MediaSource>(
204
207
  `/api/hygiene-area-checklist/schedule/${scheduleId}/pdf`,
205
208
  {
@@ -210,17 +213,10 @@ export default function useCleaningSchedules() {
210
213
 
211
214
  if (!res) throw new Error("Error downloading checklist PDF");
212
215
 
213
- const date = new Date();
214
- const formattedDate = `${String(date.getDate()).padStart(2, "0")}_${String(
215
- date.getMonth() + 1,
216
- ).padStart(2, "0")}_${date.getFullYear()} ${String(
217
- date.getHours(),
218
- ).padStart(2, "0")}_${String(date.getMinutes()).padStart(2, "0")}`;
219
-
220
216
  const downloadUrl = window.URL.createObjectURL(res);
221
217
  const a = document.createElement("a");
222
218
  a.href = downloadUrl;
223
- a.download = `CHECKLIST_DOWNLOADED_${formattedDate}.pdf`;
219
+ a.download = checklistPdfName(day);
224
220
  a.target = "_blank";
225
221
  a.rel = "noopener noreferrer";
226
222
  document.body.appendChild(a);
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.97.1-staging.596",
5
+ "version": "4.97.1-staging.597",
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.",
@@ -405,3 +405,48 @@ export function historyUnitLabel(status: string | null | undefined): "Done" | "N
405
405
  if (s === "rejected" || s === "needs redo") return "Needs redo";
406
406
  return "Missed";
407
407
  }
408
+
409
+ /** "Visit 2": one round of an area's units ("set" in the data). Same word as "Visits per day". */
410
+ export function visitLabel(set: number | string | null | undefined): string {
411
+ return set === null || set === undefined || set === "" ? "Visit" : `Visit ${set}`;
412
+ }
413
+
414
+ /**
415
+ * Is this day's list closed (view only)? The day list says so when it is
416
+ * opened from there; opened from a link or after a refresh only the rows can
417
+ * tell, and at midnight the server closes every area and unit at once.
418
+ */
419
+ export function isClosedDay(
420
+ dayStatus: string | null | undefined,
421
+ rows: { status?: string | null; units?: { status?: string | null }[] | null }[] | null | undefined = [],
422
+ ): boolean {
423
+ const closed = (s: unknown) => String(s ?? "").trim().toLowerCase() === "closed";
424
+ if (closed(dayStatus)) return true;
425
+ return (rows ?? []).some((r) => closed(r?.status) || (r?.units ?? []).some((u) => closed(u?.status)));
426
+ }
427
+
428
+ /** "Fri 24 Oct 2026" (Singapore), the day a list is for; "" when unknown. */
429
+ export function dayLabel(value: string | Date | null | undefined): string {
430
+ if (!value) return "";
431
+ const d = new Date(value);
432
+ if (Number.isNaN(d.getTime())) return "";
433
+ return d.toLocaleDateString("en-SG", {
434
+ weekday: "short",
435
+ day: "numeric",
436
+ month: "short",
437
+ year: "numeric",
438
+ timeZone: "Asia/Singapore",
439
+ });
440
+ }
441
+
442
+ /**
443
+ * The saved PDF's name: the DAY the list is for (Singapore), so three past
444
+ * days downloaded in a row do not all carry today's time.
445
+ * "Daily-Checklist-2026-10-24.pdf"; without a day, the old download-time name.
446
+ */
447
+ export function checklistPdfName(day: string | Date | null | undefined, now: Date = new Date()): string {
448
+ const d = day ? new Date(day) : null;
449
+ if (d && !Number.isNaN(d.getTime())) return `Daily-Checklist-${sgtParts(d).date}.pdf`;
450
+ const p = (n: number) => String(n).padStart(2, "0");
451
+ return `CHECKLIST_DOWNLOADED_${p(now.getDate())}_${p(now.getMonth() + 1)}_${now.getFullYear()} ${p(now.getHours())}_${p(now.getMinutes())}.pdf`;
452
+ }