@7365admin1/layer-common 4.132.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.
@@ -391,7 +391,12 @@ export const PERMISSIONS = {
391
391
  ],
392
392
 
393
393
  // ---- security ----
394
+ // Security catalogue v2 (2026-10-20). `catalogue:security-v2` is the marker the
395
+ // role editor stamps on save; it grants nothing (see utils/security-catalogue.ts).
396
+ "offline-data": ["see-offline-data"],
397
+ catalogue: ["security-v2"],
394
398
  "visitor-mgmt": [
399
+ "invite-visitor",
395
400
  "see-all-visitor",
396
401
  "add-visitor",
397
402
  "update-visitor",
@@ -399,6 +404,8 @@ export const PERMISSIONS = {
399
404
  "delete-visitor",
400
405
  ],
401
406
  "vehicle-mgmt": [
407
+ "see-blocklist",
408
+ "manage-blocklist",
402
409
  "see-all-vehicles",
403
410
  "add-vehicle",
404
411
  "update-vehicle",
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.132.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";
@@ -54,6 +54,7 @@ export const ALWAYS_ALLOWED_MODULES: readonly string[] = Object.freeze([
54
54
  "roles",
55
55
  "roles-and-permissions",
56
56
  "site-settings",
57
+ "offline-data",
57
58
  ]);
58
59
 
59
60
  /**
@@ -246,6 +247,21 @@ export function ownerGrant(
246
247
  return chosen ? (permissions as unknown[]).map(String) : null;
247
248
  }
248
249
 
250
+ /**
251
+ * Legacy grant keys that also carry a newer key split out of them. Mirrors
252
+ * core `PERMISSION_COVERS` (security catalogue v2 entries only).
253
+ */
254
+ const GRANT_COVERS: Readonly<Record<string, readonly string[]>> = {
255
+ "vehicle-mgmt:see-all-vehicles": ["vehicle-mgmt:see-blocklist"],
256
+ "vehicle-mgmt:update-vehicle": ["vehicle-mgmt:manage-blocklist"],
257
+ "visitor-mgmt:add-visitor": ["visitor-mgmt:invite-visitor"],
258
+ "visitorManagement:add-visitor": ["visitor-mgmt:invite-visitor"],
259
+ "visitor:create": ["visitor-mgmt:invite-visitor"],
260
+ };
261
+
262
+ const grantHolds = (grant: readonly string[], key: string) =>
263
+ grant.includes(key) || grant.some((g) => GRANT_COVERS[g]?.includes(key));
264
+
249
265
  /**
250
266
  * Narrow a `{ resource: { action } }` catalogue to that grant. Running the
251
267
  * organisation (`ALWAYS_ALLOWED_MODULES`) is never narrowed, matching the
@@ -266,7 +282,7 @@ export function catalogueForOwnerGrant<T extends Record<string, unknown>>(
266
282
  if (!actions || typeof actions !== "object") continue;
267
283
  const kept = Object.fromEntries(
268
284
  Object.entries(actions as Record<string, unknown>).filter(([action]) =>
269
- grant.includes(`${resource}:${action}`),
285
+ grantHolds(grant, `${resource}:${action}`),
270
286
  ),
271
287
  );
272
288
  if (Object.keys(kept).length) narrowed[resource] = kept;
@@ -0,0 +1,281 @@
1
+ /**
2
+ * EXTRA VISITS (SCHEDULE TASKS) - the rules and the wording, in one place.
3
+ *
4
+ * Mirrors the server (module-hygiene `schedule-task-rules.util.ts`) so the form
5
+ * can say "this will never run" BEFORE the user saves, and the list and the
6
+ * summary can speak plain English. The server stays the authority; every
7
+ * message it sends is still shown. All times are Singapore time (UTC+8).
8
+ *
9
+ * Pure: no Vue, no imports, so it is unit-tested with `node --test`.
10
+ */
11
+
12
+ export type TRepeat = "none" | "daily" | "weekdays" | "weekly" | "monthly";
13
+
14
+ export const REPEAT_OPTIONS: { title: string; value: TRepeat }[] = [
15
+ { title: "Once", value: "none" },
16
+ { title: "Every day", value: "daily" },
17
+ { title: "Weekdays (Mon to Fri)", value: "weekdays" },
18
+ { title: "Weekly", value: "weekly" },
19
+ { title: "Monthly", value: "monthly" },
20
+ ];
21
+
22
+ /** Sunday = 0, as the server stores it. Shown Monday first. */
23
+ export const WEEK_DAYS: { title: string; value: number }[] = [
24
+ { title: "Mon", value: 1 },
25
+ { title: "Tue", value: 2 },
26
+ { title: "Wed", value: 3 },
27
+ { title: "Thu", value: 4 },
28
+ { title: "Fri", value: 5 },
29
+ { title: "Sat", value: 6 },
30
+ { title: "Sun", value: 0 },
31
+ ];
32
+
33
+ export type TScheduleForm = {
34
+ title: string;
35
+ time: string; // HH:mm
36
+ repeat: TRepeat;
37
+ dates: string[]; // YYYY-MM-DD, used when repeat is "none"
38
+ daysOfWeek: number[];
39
+ startDate: string; // YYYY-MM-DD, when repeating
40
+ endDate: string; // YYYY-MM-DD or ""
41
+ areaCount: number;
42
+ };
43
+
44
+ export type TScheduleErrors = Partial<
45
+ Record<"title" | "areas" | "time" | "dates" | "daysOfWeek" | "startDate" | "endDate" | "never", string>
46
+ >;
47
+
48
+ const SGT_MS = 8 * 60 * 60 * 1000;
49
+ const TIME_RE = /^([0-1]\d|2[0-3]):([0-5]\d)$/;
50
+ const pad = (n: number) => String(n).padStart(2, "0");
51
+ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
52
+ const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
53
+
54
+ /** Singapore calendar date, minutes since midnight, weekday (0 = Sunday). */
55
+ export function sgtParts(now: Date = new Date()) {
56
+ const s = new Date(now.getTime() + SGT_MS);
57
+ return {
58
+ date: `${s.getUTCFullYear()}-${pad(s.getUTCMonth() + 1)}-${pad(s.getUTCDate())}`,
59
+ minutes: s.getUTCHours() * 60 + s.getUTCMinutes(),
60
+ weekday: s.getUTCDay(),
61
+ };
62
+ }
63
+
64
+ const toMinutes = (time: string) => {
65
+ const [h, m] = time.split(":").map(Number);
66
+ return h * 60 + m;
67
+ };
68
+ const utc = (date: string) => {
69
+ const [y, m, d] = date.split("-").map(Number);
70
+ return new Date(Date.UTC(y, m - 1, d));
71
+ };
72
+ const weekdayOf = (date: string) => utc(date).getUTCDay();
73
+ const daysInMonth = (date: string) => {
74
+ const [y, m] = date.split("-").map(Number);
75
+ return new Date(Date.UTC(y, m, 0)).getUTCDate();
76
+ };
77
+ const addDays = (date: string, n: number) => {
78
+ const t = utc(date);
79
+ t.setUTCDate(t.getUTCDate() + n);
80
+ return `${t.getUTCFullYear()}-${pad(t.getUTCMonth() + 1)}-${pad(t.getUTCDate())}`;
81
+ };
82
+
83
+ /** "21 Oct" (the year is added only when it is not the current one). */
84
+ export function fmtDate(date: string, nowYear = sgtParts().date.slice(0, 4)): string {
85
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date || "")) return date || "";
86
+ const [y, m, d] = date.split("-");
87
+ return `${Number(d)} ${MONTHS[Number(m) - 1]}${y === nowYear ? "" : ` ${y}`}`;
88
+ }
89
+
90
+ /** "Tue 21 Oct, 08:00" from a server ISO string ("2026-10-21T08:00:00+08:00") - read as written, no time zone maths. */
91
+ export function fmtMoment(iso: string | null | undefined, nowYear?: string): string {
92
+ const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(iso || "");
93
+ if (!m) return "";
94
+ return `${DAYS[weekdayOf(m[1])]} ${fmtDate(m[1], nowYear)}, ${m[2]}`;
95
+ }
96
+
97
+ type TRule = Pick<TScheduleForm, "repeat" | "dates" | "daysOfWeek" | "startDate" | "endDate" | "time">;
98
+
99
+ const isRepeating = (r: Pick<TRule, "repeat">) => !!r.repeat && r.repeat !== "none";
100
+
101
+ function occursOn(r: TRule, date: string): boolean {
102
+ if (r.endDate && date > r.endDate) return false;
103
+ if (!isRepeating(r)) return r.dates.includes(date);
104
+ if (r.startDate && date < r.startDate) return false;
105
+ const wd = weekdayOf(date);
106
+ switch (r.repeat) {
107
+ case "daily":
108
+ return true;
109
+ case "weekdays":
110
+ return wd >= 1 && wd <= 5;
111
+ case "weekly":
112
+ return r.daysOfWeek.includes(wd);
113
+ case "monthly": {
114
+ const wanted = r.startDate ? Number(r.startDate.slice(8, 10)) : 1;
115
+ return Number(date.slice(8, 10)) === Math.min(wanted, daysInMonth(date));
116
+ }
117
+ default:
118
+ return false;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * The next day this would run if saved right now ("YYYY-MM-DD"), or null when it never would.
124
+ * A day whose time has already passed does not count: a task saved after its time waits for its next day.
125
+ */
126
+ export function nextRunDate(r: TRule, now: Date = new Date()): string | null {
127
+ if (r.time && !TIME_RE.test(r.time)) return null;
128
+ const { date: today, minutes } = sgtParts(now);
129
+ const due = r.time ? toMinutes(r.time) : 0;
130
+ if (occursOn(r, today) && minutes < due) return today;
131
+ if (!isRepeating(r)) {
132
+ const later = r.dates.filter((d) => d > today && (!r.endDate || d <= r.endDate)).sort();
133
+ return later[0] ?? null;
134
+ }
135
+ const from = r.startDate && r.startDate > today ? r.startDate : today;
136
+ // 400 days covers the longest gap (a monthly rule on the 31st).
137
+ for (let i = from === today ? 1 : 0; i <= 400; i++) {
138
+ const d = addDays(from, i);
139
+ if (r.endDate && d > r.endDate) return null;
140
+ if (occursOn(r, d)) return d;
141
+ }
142
+ return null;
143
+ }
144
+
145
+ /** Plain-English problems with the form, mirroring the server's checks. Empty object = fine. */
146
+ export function validateSchedule(f: TScheduleForm, now: Date = new Date()): TScheduleErrors {
147
+ const e: TScheduleErrors = {};
148
+ const { date: today, minutes } = sgtParts(now);
149
+ const repeating = isRepeating(f);
150
+
151
+ if (!f.title.trim()) e.title = "Please enter a title.";
152
+ if (f.areaCount < 1) e.areas = "Pick at least one area.";
153
+ if (!TIME_RE.test(f.time || "")) e.time = "Please choose a time (24-hour, like 08:30).";
154
+
155
+ if (repeating) {
156
+ if (!f.startDate) e.startDate = "Please choose a start date.";
157
+ if (f.repeat === "weekly" && !f.daysOfWeek.length) e.daysOfWeek = "Pick at least one weekday.";
158
+ } else if (!f.dates.length) {
159
+ e.dates = "Pick at least one date.";
160
+ }
161
+
162
+ const start = repeating ? f.startDate : [...f.dates].sort()[0];
163
+ if (f.endDate && start && start > f.endDate) e.endDate = "The end date must be on or after the start date.";
164
+ else if (f.endDate && f.endDate < today) e.endDate = "The end date has already passed.";
165
+
166
+ if (!repeating && f.dates.length && !e.time) {
167
+ if (f.dates.every((d) => d < today)) e.dates = "All the dates you picked have already passed. Pick today or a later date.";
168
+ else if (f.dates.every((d) => d <= today) && f.dates.includes(today) && toMinutes(f.time) <= minutes) {
169
+ e.dates = "That time has already passed today. Pick a later time or another date.";
170
+ }
171
+ }
172
+
173
+ if (!Object.keys(e).length && !nextRunDate(f, now)) {
174
+ e.never = "This schedule would never run. Check the dates, end date and time.";
175
+ }
176
+ return e;
177
+ }
178
+
179
+ /** "at 08:00 every weekday from 21 Oct until 31 Dec" - the When, as a sentence fragment. */
180
+ export function describeWhen(
181
+ t: Partial<TRule> & { time?: string },
182
+ nowYear?: string,
183
+ ): string {
184
+ const time = t.time ? `at ${t.time} ` : "";
185
+ const repeat = t.repeat || "none";
186
+ const dates = t.dates || [];
187
+ const range =
188
+ (t.startDate ? ` from ${fmtDate(t.startDate, nowYear)}` : "") +
189
+ (t.endDate ? ` until ${fmtDate(t.endDate, nowYear)}` : "");
190
+ switch (repeat) {
191
+ case "daily":
192
+ return `${time}every day${range}`;
193
+ case "weekdays":
194
+ return `${time}every weekday${range}`;
195
+ case "weekly": {
196
+ const names = WEEK_DAYS.filter((d) => (t.daysOfWeek || []).includes(d.value)).map((d) => d.title);
197
+ return `${time}every ${names.join(", ") || "week"}${range}`;
198
+ }
199
+ case "monthly": {
200
+ const day = t.startDate ? Number(t.startDate.slice(8, 10)) : null;
201
+ 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}`;
203
+ }
204
+ default: {
205
+ const sorted = [...dates].sort();
206
+ if (!sorted.length) return `${time}on no date`.trim();
207
+ const shown = sorted.slice(0, 3).map((d) => fmtDate(d, nowYear)).join(", ");
208
+ return `${time}on ${shown}${sorted.length > 3 ? ` +${sorted.length - 3} more` : ""}${
209
+ t.endDate ? ` (until ${fmtDate(t.endDate, nowYear)})` : ""
210
+ }`;
211
+ }
212
+ }
213
+ }
214
+
215
+ /** The sentence shown before saving. */
216
+ export function buildSummary(
217
+ f: TScheduleForm,
218
+ unitCount: number,
219
+ now: Date = new Date(),
220
+ ): string {
221
+ const year = sgtParts(now).date.slice(0, 4);
222
+ const areas = `${f.areaCount} area${f.areaCount === 1 ? "" : "s"}`;
223
+ const units = `${unitCount} unit${unitCount === 1 ? "" : "s"}`;
224
+ const next = nextRunDate(f, now);
225
+ const nextText = next ? `${DAYS[weekdayOf(next)]} ${fmtDate(next, year)}` : "never";
226
+ return `Adds 1 extra visit to ${areas} (${units}) ${describeWhen(f, year)} — next: ${nextText}.`;
227
+ }
228
+
229
+ export const STATUS_LABELS: Record<string, string> = { active: "Active", paused: "Paused", ended: "Ended" };
230
+
231
+ /** "Done 20 Oct" / "Missed 20 Oct" / "Not run yet". */
232
+ export function lastRunLabel(row: { lastRun?: string | null; lastRunStatus?: string | null }): string {
233
+ if (!row.lastRun) return "Not run yet";
234
+ const m = /^(\d{4}-\d{2}-\d{2})/.exec(row.lastRun);
235
+ const day = m ? fmtDate(m[1]) : "";
236
+ const word = { done: "Done", missed: "Missed", pending: "In progress" }[row.lastRunStatus || ""] || "Ran";
237
+ return `${word} ${day}`.trim();
238
+ }
239
+
240
+ /**
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.
244
+ */
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" },
251
+ };
252
+
253
+ export const scheduleNameFor = (serviceType?: string) =>
254
+ SCHEDULE_NAMES[serviceType || ""] || SCHEDULE_NAMES.cleaning_services;
255
+
256
+ /** Singapore "today" as YYYY-MM-DD, for "is today's list there yet". */
257
+ export const sgtToday = (now: Date = new Date()) => sgtParts(now).date;
258
+
259
+ /** True when any row in the list was created today (Singapore time). */
260
+ export function hasListForToday(rows: { createdAt?: string | Date }[], now: Date = new Date()): boolean {
261
+ const today = sgtToday(now);
262
+ return rows.some((r) => {
263
+ const t = r.createdAt ? new Date(r.createdAt) : null;
264
+ return !!t && !Number.isNaN(t.getTime()) && sgtParts(t).date === today;
265
+ });
266
+ }
267
+
268
+ /** One spelling for the in-flight state ("Ongoing" / "On-going" -> "In progress"); other words pass through. */
269
+ export function dailyStatusLabel(status: string | null | undefined): string {
270
+ const s = String(status ?? "").trim();
271
+ if (!s) return "No status";
272
+ return /^on-?going$/i.test(s) ? "In progress" : s;
273
+ }
274
+
275
+ /** A unit's result in the history view: Done, Needs redo (rejected by a supervisor) or Missed (never done). */
276
+ export function historyUnitLabel(status: string | null | undefined): "Done" | "Needs redo" | "Missed" {
277
+ const s = String(status ?? "").trim().toLowerCase();
278
+ if (s === "completed" || s === "done" || s === "approved") return "Done";
279
+ if (s === "rejected" || s === "needs redo") return "Needs redo";
280
+ return "Missed";
281
+ }
@@ -0,0 +1,59 @@
1
+ import { holdsPermission } from "./permission-spellings.ts";
2
+
3
+ /**
4
+ * Security catalogue v2 marker. The role editor adds it on save (security roles
5
+ * only). It grants nothing -- it only says "this role was saved with the new
6
+ * boxes, so check them exactly". A role without it keeps today's behaviour.
7
+ * Mirrors core/API-core; keep the three in step.
8
+ */
9
+ export const SECURITY_CATALOGUE_MARKER = "catalogue:security-v2";
10
+
11
+ /** new key -> legacy keys that stand in for it on an UNMARKED role. null = open to all. */
12
+ export const SECURITY_V2_FALLBACKS: Readonly<Record<string, readonly string[] | null>> = {
13
+ "vehicle-mgmt:see-blocklist": ["vehicle-mgmt:see-all-vehicles"],
14
+ "vehicle-mgmt:manage-blocklist": ["vehicle-mgmt:update-vehicle"],
15
+ "visitor-mgmt:invite-visitor": [
16
+ "visitor-mgmt:add-visitor",
17
+ "visitorManagement:add-visitor",
18
+ "visitor:create",
19
+ ],
20
+ "offline-data:see-offline-data": null,
21
+ };
22
+
23
+ export const isSecurityV2Marked = (perms?: readonly string[] | null) =>
24
+ !!perms?.includes(SECURITY_CATALOGUE_MARKER);
25
+
26
+ /** Real keys = anything that is not the marker or the wildcard. */
27
+ export const countRealPermissions = (perms?: readonly string[] | null) =>
28
+ (perms ?? []).filter((p) => p !== SECURITY_CATALOGUE_MARKER).length;
29
+
30
+ /**
31
+ * Effective grant for a v2 key. Empty list / "*" = allowed (same as
32
+ * usePermission). Unmarked role -> legacy fallback; marked role -> exact key.
33
+ */
34
+ export function effectiveSecurityGrant(
35
+ perms: readonly string[] | undefined | null,
36
+ key: string,
37
+ ): boolean {
38
+ if (!perms?.length || perms.includes("*")) return true;
39
+ if (isSecurityV2Marked(perms)) return holdsPermission(perms, key);
40
+ const legacy = SECURITY_V2_FALLBACKS[key];
41
+ if (legacy === undefined) return holdsPermission(perms, key);
42
+ if (legacy === null) return true;
43
+ return legacy.some((k) => holdsPermission(perms, k));
44
+ }
45
+
46
+ /** Marker to append on save: only when the list has a real key and no "*". */
47
+ export function withSecurityMarker(perms: readonly string[]): string[] {
48
+ const keys = perms.filter((p) => p !== SECURITY_CATALOGUE_MARKER);
49
+ if (!keys.length || keys.includes("*")) return keys;
50
+ return [...keys, SECURITY_CATALOGUE_MARKER];
51
+ }
52
+
53
+ /** New keys to pre-tick when opening an UNMARKED role in the editor. */
54
+ export function securityV2PreTicks(perms: readonly string[]): string[] {
55
+ if (!perms.length || perms.includes("*") || isSecurityV2Marked(perms)) return [];
56
+ return Object.keys(SECURITY_V2_FALLBACKS).filter(
57
+ (k) => !perms.includes(k) && effectiveSecurityGrant(perms, k),
58
+ );
59
+ }
package/utils/status.ts CHANGED
@@ -73,6 +73,12 @@ const TONES: Record<string, TStatusTone> = {
73
73
 
74
74
  complete: "ok",
75
75
 
76
+ // Extra visits (schedule tasks): the last run's result and the task's state.
77
+ done: "ok",
78
+ missed: "err",
79
+ paused: "warn",
80
+ "needs redo": "warn",
81
+
76
82
  inactive: "neutral", // was `grey`
77
83
  "in active": "neutral", // the same word, printed "In-Active"
78
84
  declined: "neutral", // was Vuetify's `default`
@@ -27,6 +27,8 @@ export type VehicleActionPermissions = {
27
27
  canUpdateVehicle?: boolean;
28
28
  canDeleteVehicle?: boolean;
29
29
  canApproveVehicle?: boolean;
30
+ /** Block / Unblock. Omitted = allowed (today's behaviour). */
31
+ canManageBlocklist?: boolean;
30
32
  };
31
33
 
32
34
  export type VehicleAction = {
@@ -81,6 +83,12 @@ export function vehiclePlateActions(
81
83
  const isRestorable = RESTORABLE.has(status);
82
84
 
83
85
  const canUpdate = !!permissions.canUpdateVehicle;
86
+ // Server accepts manage-blocklist OR update-vehicle. When the caller passes the
87
+ // flag it decides alone; absent, block follows update (today's rule).
88
+ const canBlock =
89
+ permissions.canManageBlocklist === undefined
90
+ ? canUpdate
91
+ : !!permissions.canManageBlocklist;
84
92
  const actions: VehicleAction[] = [];
85
93
 
86
94
  if (canUpdate && (isAllowed || isBlocked)) {
@@ -118,7 +126,7 @@ export function vehiclePlateActions(
118
126
  // same thing. A deleted whitelist plate keeps Block - that turns it into a
119
127
  // blocked plate, which Restore (back to allowed) does not.
120
128
  const restoresToBlocked = isRestorable && type === "blocklist";
121
- if (canUpdate && (isAllowed || isUnblocked || (isRestorable && !restoresToBlocked))) {
129
+ if (canBlock && (isAllowed || isUnblocked || (isRestorable && !restoresToBlocked))) {
122
130
  actions.push({
123
131
  key: "block",
124
132
  title: "Block",
@@ -127,7 +135,7 @@ export function vehiclePlateActions(
127
135
  });
128
136
  }
129
137
 
130
- if (canUpdate && isBlocked) {
138
+ if (canBlock && isBlocked) {
131
139
  actions.push({
132
140
  key: "unblock",
133
141
  title: "Unblock",
@@ -1,85 +0,0 @@
1
- export default function useScheduleTaskArea() {
2
- async function getAreas({
3
- site = "",
4
- search = "",
5
- page = 1,
6
- limit = 10,
7
- } = {}) {
8
- return useNuxtApp().$api<Record<string, any>>(
9
- "/api/hygiene-schedule-task-area",
10
- {
11
- method: "GET",
12
- query: {
13
- site,
14
- search,
15
- page,
16
- limit,
17
- },
18
- }
19
- );
20
- }
21
-
22
- function createArea(name: string, site: string) {
23
- return useNuxtApp().$api<Record<string, any>>(
24
- "/api/hygiene-schedule-task-area",
25
- {
26
- method: "POST",
27
- body: {
28
- name,
29
- site,
30
- },
31
- }
32
- );
33
- }
34
-
35
- function updateArea(id: string, name: string) {
36
- return useNuxtApp().$api<Record<string, any>>(
37
- `/api/hygiene-schedule-task-area/id/${id}`,
38
- {
39
- method: "PATCH",
40
- body: { name },
41
- }
42
- );
43
- }
44
-
45
- function deleteArea(id: string) {
46
- return useNuxtApp().$api<Record<string, any>>(
47
- `/api/hygiene-schedule-task-area/id/${id}`,
48
- {
49
- method: "DELETE",
50
- }
51
- );
52
- }
53
-
54
- function getAreaById(id: string) {
55
- return useNuxtApp().$api<Record<string, any>>(
56
- `/api/hygiene-schedule-task-area/id/${id}`,
57
- {
58
- method: "GET",
59
- }
60
- );
61
- }
62
-
63
- function uploadAreas(file: File, site: string) {
64
- const formData = new FormData();
65
- formData.append("file", file);
66
- formData.append("site", site);
67
-
68
- return useNuxtApp().$api<Record<string, any>>(
69
- "/api/hygiene-schedule-task-area/upload",
70
- {
71
- method: "POST",
72
- body: formData,
73
- }
74
- );
75
- }
76
-
77
- return {
78
- getAreas,
79
- createArea,
80
- updateArea,
81
- deleteArea,
82
- getAreaById,
83
- uploadAreas,
84
- };
85
- }