@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.
- package/CHANGELOG.md +16 -4
- package/assets/css/primitives.css +54 -11
- package/assets/css/screens.css +3 -3
- package/assets/css/tokens.css +17 -12
- package/components/AnprCameraBadge.vue +1 -1
- package/components/CameraWall.vue +7 -7
- package/components/CameraWallTile.vue +8 -8
- package/components/Chat/ListCard.vue +1 -1
- package/components/CleaningScheduleMain.vue +56 -20
- package/components/ClientMain.vue +1 -1
- package/components/ClientSubscriptionForm.vue +1 -1
- package/components/DashboardMain.vue +13 -13
- package/components/HidAccessLogDashboard.vue +3 -3
- package/components/HidIntercomManagement.vue +14 -14
- package/components/HidQrCodeConfiguration.vue +3 -3
- package/components/HidReaderManagement.vue +1 -1
- package/components/InventoryPhotoThumbs.vue +1 -1
- package/components/Layout/Header.vue +2 -2
- package/components/Layout/NavigationDrawer.vue +1 -1
- package/components/Nfc/NFCRouteScheduleTimes.vue +1 -1
- package/components/PlateNumberDisplay.vue +1 -1
- package/components/RolePermissionFormCreate.vue +6 -2
- package/components/RolePermissionFormPreviewUpdate.vue +20 -4
- package/components/ScheduleAreaMain.vue +6 -9
- package/components/ScheduleGuide.vue +101 -0
- package/components/ScheduleTaskForm.vue +88 -7
- package/components/ScheduleTaskMain.vue +163 -56
- package/components/StatusChip.vue +1 -1
- package/components/VisitorsReportPreview.vue +2 -2
- package/composables/useCleaningPermission.ts +3 -0
- package/composables/useCommonPermission.ts +12 -0
- package/composables/useLandscapingPermission.ts +2 -0
- package/composables/useMechanicalElectricalPermission.ts +3 -0
- package/composables/usePestControlPermission.ts +2 -0
- package/composables/usePoolMaintenancePermission.ts +2 -0
- package/constants/permissions.ts +3 -1
- package/package.json +1 -1
- package/utils/breadcrumb.ts +7 -0
- package/utils/nav-sections.ts +2 -0
- package/utils/org-modules.ts +3 -0
- package/utils/permission-labels.ts +6 -0
- package/utils/schedule-task.ts +106 -23
- package/utils/security-catalogue.ts +1 -1
- package/utils/trade-catalogue.ts +43 -0
package/utils/schedule-task.ts
CHANGED
|
@@ -176,12 +176,20 @@ export function validateSchedule(f: TScheduleForm, now: Date = new Date()): TSch
|
|
|
176
176
|
return e;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
/** "
|
|
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
|
|
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
|
|
200
|
+
return `every day${at}${range}`;
|
|
193
201
|
case "weekdays":
|
|
194
|
-
return
|
|
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
|
|
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 `${
|
|
210
|
+
return `${on}${at}${range}`;
|
|
203
211
|
}
|
|
204
212
|
default: {
|
|
205
213
|
const sorted = [...dates].sort();
|
|
206
|
-
if (!sorted.length) return
|
|
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
|
|
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
|
-
|
|
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" / "
|
|
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 "
|
|
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 || ""] || "
|
|
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
|
|
242
|
-
*
|
|
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
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|