@7365admin1/layer-common 4.136.0 → 4.138.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/AreaMain.vue +11 -5
- package/components/BillingMain.vue +12 -53
- package/components/BulletinBoardManagement.vue +9 -13
- 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/InvitationMain.vue +8 -0
- package/components/Layout/Header.vue +2 -2
- package/components/Layout/NavigationDrawer.vue +1 -1
- package/components/MemberMain.vue +8 -0
- package/components/Nfc/NFCRouteScheduleTimes.vue +1 -1
- package/components/PlateNumberDisplay.vue +1 -1
- package/components/RolePermissionMain.vue +13 -0
- 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/SiteSettings.vue +5 -5
- package/components/StatusChip.vue +1 -1
- package/components/VisitorsReportPreview.vue +2 -2
- package/composables/useKey.ts +20 -16
- package/package.json +1 -1
- package/pages/unauthorized.vue +4 -3
- package/utils/breadcrumb.ts +7 -0
- package/utils/nav-sections.ts +2 -0
- package/utils/schedule-task.ts +106 -23
|
@@ -541,11 +541,6 @@ const saveANPRSettings = async () => {
|
|
|
541
541
|
try {
|
|
542
542
|
isSaving.value = true;
|
|
543
543
|
|
|
544
|
-
// Replace this with your actual API endpoint / store action
|
|
545
|
-
// await $fetch(`/api/sites/${siteId.value}/camera-settings`, {
|
|
546
|
-
// method: 'POST',
|
|
547
|
-
// body: cameraSettings.value
|
|
548
|
-
// })
|
|
549
544
|
const data = {
|
|
550
545
|
site: props.siteId,
|
|
551
546
|
ANPRSwitches: ANPRSwitches.value,
|
|
@@ -556,6 +551,11 @@ const saveANPRSettings = async () => {
|
|
|
556
551
|
show("Saved successfully", "success");
|
|
557
552
|
} catch (error) {
|
|
558
553
|
console.error("Failed to save settings:", error);
|
|
554
|
+
show(
|
|
555
|
+
(error as any)?.data?.message ||
|
|
556
|
+
"Could not save the camera (ANPR) settings. Please try again.",
|
|
557
|
+
"error"
|
|
558
|
+
);
|
|
559
559
|
} finally {
|
|
560
560
|
isSaving.value = false;
|
|
561
561
|
}
|
|
@@ -104,7 +104,7 @@ const toneClass = computed(() =>
|
|
|
104
104
|
font-size: var(--fs-chip);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/* The role tag: `padding:3px 10px;border-radius:999px;font-size:
|
|
107
|
+
/* The role tag: `padding:3px 10px;border-radius:999px;font-size:12.5px`. */
|
|
108
108
|
.status-chip--pill {
|
|
109
109
|
padding: 3px 10px;
|
|
110
110
|
font-size: var(--fs-chip);
|
|
@@ -397,12 +397,12 @@ onMounted(() => {
|
|
|
397
397
|
|
|
398
398
|
:deep(.v-data-table thead tr th) {
|
|
399
399
|
padding: 6px !important;
|
|
400
|
-
font-size:
|
|
400
|
+
font-size: 12px !important;
|
|
401
401
|
}
|
|
402
402
|
|
|
403
403
|
:deep(.v-data-table tbody tr td) {
|
|
404
404
|
padding: 4px !important;
|
|
405
|
-
font-size:
|
|
405
|
+
font-size: 12px !important;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
:deep(.v-img) {
|
package/composables/useKey.ts
CHANGED
|
@@ -132,21 +132,25 @@ export default function useKey() {
|
|
|
132
132
|
keyLog.value = data || new MKeyLog();
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
// Await each request so a failed save reaches the caller's catch instead of showing "success".
|
|
136
|
+
async function addKeyLog(_keyLog: TKeyLog) {
|
|
136
137
|
isAdding.value = true;
|
|
137
138
|
delete _keyLog._id;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
try {
|
|
140
|
+
const data = await useNuxtApp().$api<{
|
|
141
|
+
message: string | undefined;
|
|
142
|
+
}>(keyLogApiUrl, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
body: JSON.stringify(_keyLog),
|
|
145
|
+
});
|
|
146
|
+
return data?.message;
|
|
147
|
+
} finally {
|
|
148
|
+
isAdding.value = false;
|
|
149
|
+
}
|
|
146
150
|
}
|
|
147
151
|
|
|
148
|
-
function getKeyById(id: string) {
|
|
149
|
-
const data = useNuxtApp().$api<TKeyLog | undefined>(
|
|
152
|
+
async function getKeyById(id: string) {
|
|
153
|
+
const data = await useNuxtApp().$api<TKeyLog | undefined>(
|
|
150
154
|
`${keyLogApiUrl}/${id}`,
|
|
151
155
|
{
|
|
152
156
|
method: "GET",
|
|
@@ -158,22 +162,22 @@ export default function useKey() {
|
|
|
158
162
|
return data;
|
|
159
163
|
}
|
|
160
164
|
|
|
161
|
-
function updateKeyById(id: string, payload: TKeyLog) {
|
|
165
|
+
async function updateKeyById(id: string, payload: TKeyLog) {
|
|
162
166
|
payload.codes =
|
|
163
167
|
payload.codes instanceof Array && payload.codes.length > 0
|
|
164
168
|
? payload.codes
|
|
165
169
|
: [];
|
|
166
|
-
const data = useNuxtApp().$api<{
|
|
170
|
+
const data = await useNuxtApp().$api<{
|
|
167
171
|
message: { acknowledged: boolean };
|
|
168
172
|
}>(`${keyLogApiUrl}/${id}`, {
|
|
169
173
|
method: "PUT",
|
|
170
174
|
body: payload,
|
|
171
175
|
});
|
|
172
|
-
return data
|
|
176
|
+
return data?.message;
|
|
173
177
|
}
|
|
174
178
|
|
|
175
|
-
function deleteKeyById(id: string) {
|
|
176
|
-
const data = useNuxtApp().$api<{ message: TKeyLog }>(
|
|
179
|
+
async function deleteKeyById(id: string) {
|
|
180
|
+
const data = await useNuxtApp().$api<{ message: TKeyLog }>(
|
|
177
181
|
`${keyLogApiUrl}/${id}`,
|
|
178
182
|
{
|
|
179
183
|
method: "DELETE",
|
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.
|
|
5
|
+
"version": "4.138.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.",
|
package/pages/unauthorized.vue
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
The "
|
|
2
|
+
The "no permission for this page" gate (wording only; non-members use
|
|
3
|
+
require-organization-membership).
|
|
3
4
|
|
|
4
5
|
Was Vuetify's chrome: `text-subtitle-1` for the sentence, `text-subtitle-2`
|
|
5
6
|
on the buttons, and `variant="tonal" rounded="xl"` - a 24px-cornered pill in
|
|
@@ -12,8 +13,8 @@
|
|
|
12
13
|
<template>
|
|
13
14
|
<v-row no-gutters class="fill-height" align="center" justify="center">
|
|
14
15
|
<v-col cols="12" lg="6" md="6" sm="6" class="text-center gate-page__message">
|
|
15
|
-
You
|
|
16
|
-
|
|
16
|
+
You don't have permission to open this page. If you need it for your
|
|
17
|
+
work, ask your administrator to give your role access.
|
|
17
18
|
<v-row no-gutters justify="center" class="mt-2">
|
|
18
19
|
<v-btn
|
|
19
20
|
class="screen-btn-primary"
|
package/utils/breadcrumb.ts
CHANGED
|
@@ -25,6 +25,13 @@ const ACRONYMS: Record<string, string> = {
|
|
|
25
25
|
const RENAMED: [string, string][] = [
|
|
26
26
|
["Building Mgmt", "Occupancy Mgmt"],
|
|
27
27
|
["Event Mgmt", "Task Calendar"],
|
|
28
|
+
// Schedule screens (owner 21 Oct): the same names the menu and the mobile apps use.
|
|
29
|
+
["Schedule Task", "Extra Visits"],
|
|
30
|
+
...["Cleaning", "Technician", "Pest", "Pool", "Landscape"].flatMap((t): [string, string][] => [
|
|
31
|
+
[`${t} Schedule Area`, "Area"],
|
|
32
|
+
[`${t} Schedule Unit`, "Unit"],
|
|
33
|
+
[`${t} Schedule`, "Daily Checklist"],
|
|
34
|
+
]),
|
|
28
35
|
];
|
|
29
36
|
|
|
30
37
|
export function moduleNameFromRoute(route: {
|
package/utils/nav-sections.ts
CHANGED
|
@@ -50,6 +50,8 @@ export const NAV_SECTIONS: Record<string, string> = {
|
|
|
50
50
|
Area: "SERVICE DELIVERY",
|
|
51
51
|
Unit: "SERVICE DELIVERY",
|
|
52
52
|
"Schedule Task": "SERVICE DELIVERY",
|
|
53
|
+
"Daily Checklist": "SERVICE DELIVERY",
|
|
54
|
+
"Extra Visits": "SERVICE DELIVERY",
|
|
53
55
|
|
|
54
56
|
// EQUIPMENT
|
|
55
57
|
"Equipment Management": "EQUIPMENT",
|
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();
|