@7365admin1/layer-common 4.135.0 → 4.137.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +16 -4
  2. package/assets/css/primitives.css +54 -11
  3. package/assets/css/screens.css +3 -3
  4. package/assets/css/tokens.css +17 -12
  5. package/components/AnprCameraBadge.vue +1 -1
  6. package/components/CameraWall.vue +7 -7
  7. package/components/CameraWallTile.vue +8 -8
  8. package/components/Chat/ListCard.vue +1 -1
  9. package/components/CleaningScheduleMain.vue +56 -20
  10. package/components/ClientMain.vue +1 -1
  11. package/components/ClientSubscriptionForm.vue +1 -1
  12. package/components/DashboardMain.vue +13 -13
  13. package/components/HidAccessLogDashboard.vue +3 -3
  14. package/components/HidIntercomManagement.vue +14 -14
  15. package/components/HidQrCodeConfiguration.vue +3 -3
  16. package/components/HidReaderManagement.vue +1 -1
  17. package/components/InventoryPhotoThumbs.vue +1 -1
  18. package/components/Layout/Header.vue +2 -2
  19. package/components/Layout/NavigationDrawer.vue +1 -1
  20. package/components/Nfc/NFCRouteScheduleTimes.vue +1 -1
  21. package/components/PlateNumberDisplay.vue +1 -1
  22. package/components/RolePermissionFormCreate.vue +6 -2
  23. package/components/RolePermissionFormPreviewUpdate.vue +20 -4
  24. package/components/ScheduleAreaMain.vue +6 -9
  25. package/components/ScheduleGuide.vue +101 -0
  26. package/components/ScheduleTaskForm.vue +88 -7
  27. package/components/ScheduleTaskMain.vue +163 -56
  28. package/components/StatusChip.vue +1 -1
  29. package/components/VisitorsReportPreview.vue +2 -2
  30. package/composables/useCleaningPermission.ts +3 -0
  31. package/composables/useCommonPermission.ts +12 -0
  32. package/composables/useLandscapingPermission.ts +2 -0
  33. package/composables/useMechanicalElectricalPermission.ts +3 -0
  34. package/composables/usePestControlPermission.ts +2 -0
  35. package/composables/usePoolMaintenancePermission.ts +2 -0
  36. package/constants/permissions.ts +3 -1
  37. package/package.json +1 -1
  38. package/utils/breadcrumb.ts +7 -0
  39. package/utils/nav-sections.ts +2 -0
  40. package/utils/org-modules.ts +3 -0
  41. package/utils/permission-labels.ts +6 -0
  42. package/utils/schedule-task.ts +106 -23
  43. package/utils/security-catalogue.ts +1 -1
  44. package/utils/trade-catalogue.ts +43 -0
@@ -176,12 +176,20 @@ export function validateSchedule(f: TScheduleForm, now: Date = new Date()): TSch
176
176
  return e;
177
177
  }
178
178
 
179
- /** "at 08:00 every weekday from 21 Oct until 31 Dec" - the When, as a sentence fragment. */
179
+ /** "08:00" -> "8:00 am", "14:30" -> "2:30 pm". Anything else is returned as given. */
180
+ export function fmtTime(time: string | null | undefined): string {
181
+ const m = TIME_RE.exec(time || "");
182
+ if (!m) return time || "";
183
+ const h = Number(m[1]);
184
+ return `${h % 12 || 12}:${m[2]} ${h < 12 ? "am" : "pm"}`;
185
+ }
186
+
187
+ /** "every weekday at 8:00 am from 21 Oct until 31 Dec" - the When, as a sentence fragment. */
180
188
  export function describeWhen(
181
189
  t: Partial<TRule> & { time?: string },
182
190
  nowYear?: string,
183
191
  ): string {
184
- const time = t.time ? `at ${t.time} ` : "";
192
+ const at = t.time ? ` at ${fmtTime(t.time)}` : "";
185
193
  const repeat = t.repeat || "none";
186
194
  const dates = t.dates || [];
187
195
  const range =
@@ -189,23 +197,23 @@ export function describeWhen(
189
197
  (t.endDate ? ` until ${fmtDate(t.endDate, nowYear)}` : "");
190
198
  switch (repeat) {
191
199
  case "daily":
192
- return `${time}every day${range}`;
200
+ return `every day${at}${range}`;
193
201
  case "weekdays":
194
- return `${time}every weekday${range}`;
202
+ return `every weekday${at}${range}`;
195
203
  case "weekly": {
196
204
  const names = WEEK_DAYS.filter((d) => (t.daysOfWeek || []).includes(d.value)).map((d) => d.title);
197
- return `${time}every ${names.join(", ") || "week"}${range}`;
205
+ return `every ${names.join(", ") || "week"}${at}${range}`;
198
206
  }
199
207
  case "monthly": {
200
208
  const day = t.startDate ? Number(t.startDate.slice(8, 10)) : null;
201
209
  const on = day === null ? "every month" : day === 31 ? "on the last day of every month" : `on day ${day} of every month`;
202
- return `${time}${on}${range}`;
210
+ return `${on}${at}${range}`;
203
211
  }
204
212
  default: {
205
213
  const sorted = [...dates].sort();
206
- if (!sorted.length) return `${time}on no date`.trim();
214
+ if (!sorted.length) return `on no date${at}`;
207
215
  const shown = sorted.slice(0, 3).map((d) => fmtDate(d, nowYear)).join(", ");
208
- return `${time}on ${shown}${sorted.length > 3 ? ` +${sorted.length - 3} more` : ""}${
216
+ return `on ${shown}${sorted.length > 3 ? ` +${sorted.length - 3} more` : ""}${at}${
209
217
  t.endDate ? ` (until ${fmtDate(t.endDate, nowYear)})` : ""
210
218
  }`;
211
219
  }
@@ -226,33 +234,93 @@ export function buildSummary(
226
234
  return `Adds 1 extra visit to ${areas} (${units}) ${describeWhen(f, year)} — next: ${nextText}.`;
227
235
  }
228
236
 
229
- export const STATUS_LABELS: Record<string, string> = { active: "Active", paused: "Paused", ended: "Ended" };
237
+ /** The state of an extra visit as a whole. "ended" reads "Finished": nothing is left to run. */
238
+ export const STATUS_LABELS: Record<string, string> = { active: "Active", paused: "Paused", ended: "Finished" };
239
+
240
+ /** What each word means, shown on the page so nobody has to guess. Same words as the mobile apps. */
241
+ export const STATUS_HELP: { word: string; meaning: string }[] = [
242
+ { word: "Active", meaning: "It will add a visit at its next date and time." },
243
+ { word: "Paused", meaning: "Stopped for now. Nothing is added until you press Resume." },
244
+ { word: "Finished", meaning: "Its last date has passed, so nothing more is added. Press \"Extend or repeat\" to keep it going." },
245
+ ];
246
+ export const RESULT_HELP: { word: string; meaning: string }[] = [
247
+ { word: "Not started", meaning: "Nothing is ticked yet." },
248
+ { word: "In progress", meaning: "Some units are ticked; the rest can still be done today." },
249
+ { word: "Done", meaning: "Staff ticked every unit of the visit." },
250
+ { word: "Needs redo", meaning: "A supervisor sent a unit back to be done again." },
251
+ { word: "Missed", meaning: "The day ended with units not done." },
252
+ ];
230
253
 
231
- /** "Done 20 Oct" / "Missed 20 Oct" / "Not run yet". */
254
+ /** "Done 20 Oct" / "Missed 20 Oct" / "No visit yet". */
232
255
  export function lastRunLabel(row: { lastRun?: string | null; lastRunStatus?: string | null }): string {
233
- if (!row.lastRun) return "Not run yet";
256
+ if (!row.lastRun) return "No visit yet";
234
257
  const m = /^(\d{4}-\d{2}-\d{2})/.exec(row.lastRun);
235
258
  const day = m ? fmtDate(m[1]) : "";
236
- const word = { done: "Done", missed: "Missed", pending: "In progress" }[row.lastRunStatus || ""] || "Ran";
259
+ const word = { done: "Done", missed: "Missed", pending: "In progress" }[row.lastRunStatus || ""] || "Added";
237
260
  return `${word} ${day}`.trim();
238
261
  }
239
262
 
263
+ /** Why an extra visit says Finished, in one sentence. */
264
+ export function finishedReason(row: Partial<TRule>, nowYear?: string): string {
265
+ if (row.repeat && row.repeat !== "none" && row.endDate) {
266
+ return `Its end date (${fmtDate(row.endDate, nowYear)}) has passed.`;
267
+ }
268
+ return "All the dates it was set for have passed.";
269
+ }
270
+
240
271
  /**
241
- * One name and one page path per trade. The names are the web menu names (M&E
242
- * is "Technician Schedule" - the mobile app mirrors it); the path is the folder
243
- * the trade app serves its daily schedule from.
272
+ * One sentence per list row: "Every Mon, Wed at 2:00 pm in Lobby D — next on Wed 22 Oct".
273
+ * Paused and finished rows say so instead of a next date.
244
274
  */
245
- export const SCHEDULE_NAMES: Record<string, { title: string; route: string }> = {
246
- cleaning_services: { title: "Cleaning Schedule", route: "cleaning-schedule" },
247
- landscaping_services: { title: "Landscape Schedule", route: "landscape-schedule" },
248
- pest_control_services: { title: "Pest Schedule", route: "pest-schedule" },
249
- pool_maintenance_services: { title: "Pool Schedule", route: "pool-schedule" },
250
- mechanical_electrical_services: { title: "Technician Schedule", route: "technician-schedule" },
275
+ export function describeRow(
276
+ row: Partial<TRule> & {
277
+ areas?: { name?: string }[];
278
+ nextRun?: string | null;
279
+ scheduleStatus?: string | null;
280
+ paused?: boolean;
281
+ },
282
+ nowYear?: string,
283
+ ): string {
284
+ const when = describeWhen(row, nowYear);
285
+ const names = (row.areas || []).map((a) => a?.name).filter(Boolean) as string[];
286
+ const where = names.length
287
+ ? ` in ${names.slice(0, 3).join(", ")}${names.length > 3 ? ` +${names.length - 3} more` : ""}`
288
+ : "";
289
+ const state = row.scheduleStatus || (row.paused ? "paused" : "active");
290
+ const m = /^(\d{4}-\d{2}-\d{2})/.exec(row.nextRun || "");
291
+ const tail =
292
+ state === "paused"
293
+ ? " — paused, nothing is added until you resume it"
294
+ : state === "ended"
295
+ ? " — finished"
296
+ : m
297
+ ? ` — next on ${DAYS[weekdayOf(m[1])]} ${fmtDate(m[1], nowYear)}`
298
+ : "";
299
+ return `${when.charAt(0).toUpperCase()}${when.slice(1)}${where}${tail}`;
300
+ }
301
+
302
+ /**
303
+ * One trade word and one page path per trade. Every trade's menu says "Daily
304
+ * Checklist" (owner 21 Oct; the mobile apps use the same name); the page title
305
+ * adds the trade ("Daily Checklist – Cleaning"). The path is the folder the
306
+ * trade app serves its daily checklist from - unchanged, so links and
307
+ * permissions keep working.
308
+ */
309
+ export const SCHEDULE_NAMES: Record<string, { title: string; trade: string; route: string }> = {
310
+ cleaning_services: { title: "Daily Checklist", trade: "Cleaning", route: "cleaning-schedule" },
311
+ landscaping_services: { title: "Daily Checklist", trade: "Landscape", route: "landscape-schedule" },
312
+ pest_control_services: { title: "Daily Checklist", trade: "Pest", route: "pest-schedule" },
313
+ pool_maintenance_services: { title: "Daily Checklist", trade: "Pool", route: "pool-schedule" },
314
+ mechanical_electrical_services: { title: "Daily Checklist", trade: "Technician", route: "technician-schedule" },
251
315
  };
252
316
 
253
317
  export const scheduleNameFor = (serviceType?: string) =>
254
318
  SCHEDULE_NAMES[serviceType || ""] || SCHEDULE_NAMES.cleaning_services;
255
319
 
320
+ /** "Daily Checklist – Cleaning" / "– Technician" / ... */
321
+ export const dailyChecklistTitle = (serviceType?: string) =>
322
+ `Daily Checklist – ${scheduleNameFor(serviceType).trade}`;
323
+
256
324
  /** Singapore "today" as YYYY-MM-DD, for "is today's list there yet". */
257
325
  export const sgtToday = (now: Date = new Date()) => sgtParts(now).date;
258
326
 
@@ -265,13 +333,28 @@ export function hasListForToday(rows: { createdAt?: string | Date }[], now: Date
265
333
  });
266
334
  }
267
335
 
268
- /** One spelling for the in-flight state ("Ongoing" / "On-going" -> "In progress"); other words pass through. */
336
+ /**
337
+ * The daily checklist's words, the same on web and mobile: Open -> Not started,
338
+ * Ongoing / On-going -> In progress, Completed -> Done. Closed stays Closed (the
339
+ * day ended; anything not ticked counts as Missed). Other words pass through.
340
+ */
269
341
  export function dailyStatusLabel(status: string | null | undefined): string {
270
342
  const s = String(status ?? "").trim();
271
343
  if (!s) return "No status";
272
- return /^on-?going$/i.test(s) ? "In progress" : s;
344
+ if (/^on-?going$/i.test(s)) return "In progress";
345
+ if (/^open$/i.test(s)) return "Not started";
346
+ if (/^completed?$/i.test(s)) return "Done";
347
+ return s;
273
348
  }
274
349
 
350
+ /** What the daily checklist's words mean, shown on the page. */
351
+ export const DAILY_STATUS_HELP: { word: string; meaning: string }[] = [
352
+ { word: "Not started", meaning: "The day's list is open and nothing is ticked yet." },
353
+ { word: "In progress", meaning: "Staff have ticked some units; the rest can still be done." },
354
+ { word: "Done", meaning: "Every unit on the list was ticked." },
355
+ { word: "Closed", meaning: "The day ended at midnight. The list is view-only, and anything not ticked counts as Missed." },
356
+ ];
357
+
275
358
  /** A unit's result in the history view: Done, Needs redo (rejected by a supervisor) or Missed (never done). */
276
359
  export function historyUnitLabel(status: string | null | undefined): "Done" | "Needs redo" | "Missed" {
277
360
  const s = String(status ?? "").trim().toLowerCase();
@@ -25,7 +25,7 @@ export const isSecurityV2Marked = (perms?: readonly string[] | null) =>
25
25
 
26
26
  /** Real keys = anything that is not the marker or the wildcard. */
27
27
  export const countRealPermissions = (perms?: readonly string[] | null) =>
28
- (perms ?? []).filter((p) => p !== SECURITY_CATALOGUE_MARKER).length;
28
+ (perms ?? []).filter((p) => !p.startsWith("catalogue:")).length; // every catalogue marker (security-v2, trade-v2)
29
29
 
30
30
  /**
31
31
  * Effective grant for a v2 key. Empty list / "*" = allowed (same as
@@ -0,0 +1,43 @@
1
+ import { holdsPermission } from "./permission-spellings.ts";
2
+
3
+ /**
4
+ * Trade catalogue v2 (hygiene, M&E, pest control, pool, landscape), owner
5
+ * 2026-10-21: "Task Calendar -> See calendar" per role, and "keep access until
6
+ * changed". The role editor stamps the marker when it saves a trade role. A role
7
+ * without it was never saved with the box and keeps the calendar (it was open
8
+ * to every role). The marker grants nothing. Mirrors core
9
+ * `trade-catalogue.util.ts`; keep the two in step.
10
+ */
11
+ export const TRADE_CATALOGUE_MARKER = "catalogue:trade-v2";
12
+ export const TASK_CALENDAR_KEY = "event-mgmt:see-task-calendar";
13
+
14
+ export const isTradeV2Marked = (perms?: readonly string[] | null) =>
15
+ !!perms?.includes(TRADE_CATALOGUE_MARKER);
16
+
17
+ /** Is this editor catalogue a trade one (it offers the See calendar box)? */
18
+ export const isTradeCatalogue = (catalogue?: Record<string, any> | null) =>
19
+ !!catalogue?.["event-mgmt"]?.["see-task-calendar"];
20
+
21
+ /**
22
+ * May this role open the Task Calendar? No role = no. Empty list / "*" = yes
23
+ * (empty means all). Unmarked = yes (keeps today's access). Marked = the box.
24
+ */
25
+ export function canSeeTaskCalendar(perms: readonly string[] | null | undefined): boolean {
26
+ if (!perms) return false;
27
+ if (!perms.length || perms.includes("*")) return true;
28
+ if (!isTradeV2Marked(perms)) return true;
29
+ return holdsPermission(perms, TASK_CALENDAR_KEY);
30
+ }
31
+
32
+ /** Marker to append on save: only when the list has a real key and no "*". */
33
+ export function withTradeMarker(perms: readonly string[]): string[] {
34
+ const keys = perms.filter((p) => p !== TRADE_CATALOGUE_MARKER);
35
+ if (!keys.some((p) => !p.startsWith("catalogue:")) || keys.includes("*")) return keys;
36
+ return [...keys, TRADE_CATALOGUE_MARKER];
37
+ }
38
+
39
+ /** Box to pre-tick when opening an UNMARKED trade role in the editor. */
40
+ export function tradeV2PreTicks(perms: readonly string[]): string[] {
41
+ if (!perms.length || perms.includes("*") || isTradeV2Marked(perms)) return [];
42
+ return perms.includes(TASK_CALENDAR_KEY) ? [] : [TASK_CALENDAR_KEY];
43
+ }