@7365admin1/layer-common 4.82.1-staging.476 → 4.83.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 +10 -4
- package/assets/css/primitives.css +0 -57
- package/assets/css/screens.css +4 -8
- package/components/AccessCardDetailsDialog.vue +4 -57
- package/components/AccessCardPreviewDialog.vue +50 -291
- package/components/AppSelect.vue +4 -143
- package/components/AttendanceDetailsDialog.vue +1 -48
- package/components/AttendanceMain.vue +10 -104
- package/components/AttendanceSettingsDialog.vue +1 -53
- package/components/BuildingUnitFormEdit.vue +0 -6
- package/components/BulletinBoardView.vue +1 -1
- package/components/Dialog/UpdateMoreAction.vue +1 -11
- package/components/DocumentForm.vue +112 -407
- package/components/DocumentManagement.vue +12 -213
- package/components/Facility/BookingSetup.vue +0 -63
- package/components/HidAccessLogDashboard.vue +17 -74
- package/components/HidIntercomManagement.vue +14 -183
- package/components/HidQrCodeConfiguration.vue +195 -17
- package/components/HidReaderManagement.vue +0 -398
- package/components/HidReaderUserRoster.vue +3 -44
- package/components/HidUserEnrollment.vue +250 -1573
- package/components/InventoryMain.vue +2 -11
- package/components/InvitationClientForm.vue +1 -19
- package/components/MemberMain.vue +1 -2
- package/components/MyAttendanceMain.vue +2 -60
- package/components/NotificationBell.vue +2 -14
- package/components/RolePermissionMain.vue +2 -4
- package/components/TableMain.vue +7 -22
- package/components/VisitorForm.vue +0 -12
- package/components/VisitorManagement.vue +5 -267
- package/composables/useAttendance.ts +0 -53
- package/composables/useAttendancePermission.ts +0 -25
- package/composables/useCleaningPermission.ts +0 -5
- package/composables/useDocument.ts +0 -7
- package/composables/useFacility.ts +0 -11
- package/composables/useHidAmico.ts +0 -165
- package/composables/useHidNavigation.ts +1 -25
- package/composables/useLandscapingPermission.ts +0 -5
- package/composables/useMember.ts +0 -12
- package/composables/usePeople.ts +0 -19
- package/composables/usePestControlPermission.ts +0 -5
- package/composables/usePoolMaintenancePermission.ts +0 -5
- package/composables/useSecurityPermission.ts +0 -5
- package/composables/useServiceProvider.ts +2 -40
- package/constants/permissions.ts +0 -1
- package/nuxt.config.ts +0 -18
- package/package.json +2 -4
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
- package/plugins/API.ts +0 -16
- package/types/attendance.d.ts +1 -13
- package/types/document.d.ts +0 -14
- package/types/facility.d.ts +0 -4
- package/types/member.d.ts +0 -4
- package/types/people.d.ts +1 -28
- package/types/service-provider.d.ts +0 -5
- package/utils/notification-target.ts +4 -43
- package/utils/role.ts +2 -15
- package/components/AttendanceProxyDialog.vue +0 -391
- package/components/BuildingUnitManagement.vue +0 -298
- package/components/HidAccessPermissions.vue +0 -424
- package/components/HidProfileQrCode.vue +0 -332
- package/composables/useHidReaderSelection.ts +0 -50
- package/composables/useSiteActions.ts +0 -29
- package/composables/useSiteCategory.ts +0 -45
- package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +0 -23
- package/server/plugins/no-powered-by.ts +0 -7
- package/utils/access-suspended.ts +0 -18
- package/utils/attendance-proxy.ts +0 -110
- package/utils/hid-enrolment-subject.ts +0 -144
- package/utils/hid-permission-assignments.ts +0 -190
- package/utils/hid-reader-selection.ts +0 -52
- package/utils/occupancy-role.ts +0 -109
- package/utils/sanitize-html.ts +0 -16
- package/utils/security-headers.ts +0 -52
- package/utils/service-type.ts +0 -30
- package/utils/site-actions.ts +0 -18
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
:org="orgId"
|
|
6
6
|
message="Enable HID as a service for this site before managing physical HID cards."
|
|
7
7
|
>
|
|
8
|
-
<HidUserEnrollment :site="siteId"
|
|
8
|
+
<HidUserEnrollment :site="siteId" card-management />
|
|
9
9
|
</HidEnabledGate>
|
|
10
10
|
</v-container>
|
|
11
11
|
</template>
|
package/plugins/API.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { isPermissionRefusal } from "../utils/permission-refusal";
|
|
2
|
-
import { accessSuspendedMessage, SIGNIN_NOTICE_COOKIE } from "../utils/access-suspended";
|
|
3
2
|
|
|
4
3
|
export default defineNuxtPlugin((nuxtApp) => {
|
|
5
4
|
const { cookieConfig } = useRuntimeConfig().public;
|
|
6
|
-
let signingOut = false;
|
|
7
5
|
|
|
8
6
|
const api = $fetch.create({
|
|
9
7
|
baseURL: "/",
|
|
@@ -34,20 +32,6 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|
|
34
32
|
// 401 + "You do not have permission ..." (vehicle writes and every other
|
|
35
33
|
// `requireSiteReach` gate). See utils/permission-refusal.ts.
|
|
36
34
|
onResponseError({ response }) {
|
|
37
|
-
// Every membership suspended (2026-10-01): the server already ended the
|
|
38
|
-
// session. Sign out here and let the sign-in page say why, once.
|
|
39
|
-
const suspended = accessSuspendedMessage((response as any)?._data);
|
|
40
|
-
if (suspended && import.meta.client && !signingOut) {
|
|
41
|
-
signingOut = true;
|
|
42
|
-
void nuxtApp.runWithContext(() => {
|
|
43
|
-
useCookie(SIGNIN_NOTICE_COOKIE, { ...cookieConfig, maxAge: 300 }).value = suspended;
|
|
44
|
-
for (const name of ["sid", "user", "organization"]) useCookie(name, cookieConfig).value = null;
|
|
45
|
-
const { APP_MAIN, APP_NAME } = useRuntimeConfig().public as Record<string, string>;
|
|
46
|
-
if (String(APP_NAME ?? "").toLowerCase() === "main" || !APP_MAIN) return navigateTo("/sign-in");
|
|
47
|
-
window.location.href = `${APP_MAIN}/sign-in`;
|
|
48
|
-
});
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
35
|
if (!isPermissionRefusal(response?.status, (response as any)?._data)) return;
|
|
52
36
|
if (!import.meta.client) return;
|
|
53
37
|
const refresh = (nuxtApp as any).$refreshAppRole;
|
package/types/attendance.d.ts
CHANGED
|
@@ -11,11 +11,9 @@ declare type TAttendance = {
|
|
|
11
11
|
declare type TAttendanceCheckIn = {
|
|
12
12
|
checkIn: {
|
|
13
13
|
timestamp: string;
|
|
14
|
-
location
|
|
14
|
+
location: TAttendanceLocation;
|
|
15
15
|
img?: string;
|
|
16
|
-
privatePhoto?: string;
|
|
17
16
|
};
|
|
18
|
-
proxy?: TAttendanceProxy;
|
|
19
17
|
};
|
|
20
18
|
|
|
21
19
|
declare type TAttendanceCheckOut = {
|
|
@@ -37,14 +35,4 @@ declare type TAttendanceSettings = {
|
|
|
37
35
|
location?: TAttendanceLocation;
|
|
38
36
|
isGeofencingEnabled?: boolean;
|
|
39
37
|
mile?: number;
|
|
40
|
-
/** Site device mode: one shared phone may check staff in/out (default OFF). */
|
|
41
|
-
siteDeviceMode?: boolean;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/** Who a Site device mode check-in is for. Never the full NRIC. */
|
|
45
|
-
declare type TAttendanceProxy = {
|
|
46
|
-
name: string;
|
|
47
|
-
nricLast4: string;
|
|
48
|
-
nricPrefix?: string;
|
|
49
|
-
post?: string;
|
|
50
38
|
};
|
package/types/document.d.ts
CHANGED
|
@@ -3,18 +3,4 @@ declare type TDocument = {
|
|
|
3
3
|
name: string;
|
|
4
4
|
attachment: string;
|
|
5
5
|
type: string;
|
|
6
|
-
/**
|
|
7
|
-
* Set at upload or renewal, absent when the document never expires.
|
|
8
|
-
*
|
|
9
|
-
* An ISO instant on the way out of the API and a `YYYY-MM-DD` string on the
|
|
10
|
-
* way back in, which is what `InputDatePicker` binds to - hence the loose
|
|
11
|
-
* type rather than `string`.
|
|
12
|
-
*/
|
|
13
|
-
expiryDate?: string | Date | null;
|
|
14
|
-
remarks?: string;
|
|
15
|
-
size?: number;
|
|
16
|
-
parentId?: string | null;
|
|
17
|
-
createdAt?: string | Date;
|
|
18
|
-
updatedAt?: string | Date;
|
|
19
|
-
status?: string;
|
|
20
6
|
};
|
package/types/facility.d.ts
CHANGED
|
@@ -73,10 +73,6 @@ declare type TFacility = {
|
|
|
73
73
|
hourInterval?: number;
|
|
74
74
|
isBookingFeeEnabled: boolean;
|
|
75
75
|
isGuestEnabled: boolean;
|
|
76
|
-
/** Residents may complete this facility's checklist themselves. */
|
|
77
|
-
isResidentChecklistEnabled?: boolean;
|
|
78
|
-
/** Security may complete it. Mutually exclusive with the above. */
|
|
79
|
-
isSecurityChecklistEnabled?: boolean;
|
|
80
76
|
bookingAmount: number | null;
|
|
81
77
|
peakBookingFeeAmount?: number | null;
|
|
82
78
|
bookingFeeGuestAmount?: number | null;
|
package/types/member.d.ts
CHANGED
|
@@ -10,10 +10,6 @@ declare type TMember = {
|
|
|
10
10
|
customerOrgId?: string;
|
|
11
11
|
customerSiteId?: string;
|
|
12
12
|
status?: string;
|
|
13
|
-
email?: string;
|
|
14
|
-
/** The building unit this member manages (a unit's Management tab). */
|
|
15
|
-
unit?: string | null;
|
|
16
|
-
unitName?: string;
|
|
17
13
|
createdAt?: string;
|
|
18
14
|
updatedAt?: string;
|
|
19
15
|
deletedAt?: string;
|
package/types/people.d.ts
CHANGED
|
@@ -22,38 +22,11 @@ declare type TPeople = {
|
|
|
22
22
|
email?: string;
|
|
23
23
|
files?: { name: string, id: string }[];
|
|
24
24
|
isOwner?: boolean;
|
|
25
|
-
/**
|
|
26
|
-
* Owner / occupant / person-in-charge, as the BUSINESS people form asks it.
|
|
27
|
-
* Absent on everyone else - `isOwner` is still the flag that decides unit
|
|
28
|
-
* ownership, and the server derives it from this when it is sent. See
|
|
29
|
-
* `OccupancyRoles` in core's `person.model.ts`.
|
|
30
|
-
*/
|
|
31
|
-
occupancyRole?: TOccupancyRole;
|
|
32
|
-
// Link to this person's `users` row. Empty for the person types that carry
|
|
33
|
-
// no login, and empty for a resident whose account was never written.
|
|
34
|
-
user?: string;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
declare type TOccupancyRole = "owner" | "occupant" | "person-in-charge";
|
|
38
|
-
|
|
39
|
-
/** One standing and everything the product says about it. See `utils/occupancy-role.ts`. */
|
|
40
|
-
declare type TOccupancyRoleOption = {
|
|
41
|
-
value: TOccupancyRole;
|
|
42
|
-
/** The label a stored role is printed with. */
|
|
43
|
-
title: string;
|
|
44
|
-
/** The row menu's wording. */
|
|
45
|
-
action: string;
|
|
46
|
-
/** The confirmation's heading and its sentence. */
|
|
47
|
-
prompt: string;
|
|
48
|
-
description: string;
|
|
49
25
|
};
|
|
50
26
|
|
|
51
27
|
declare type TPlateNumber = { plateNumber: string, recNo?: string, _id?: string, status: TVehicleStatus, type: TVehicleType, anprCameras?: TVehicleAnprCamera[] };
|
|
52
28
|
|
|
53
29
|
|
|
54
|
-
|
|
55
|
-
// before it is sent, because absence is what the server reads as "never
|
|
56
|
-
// asked". See `PeopleFormMgmt.vue`.
|
|
57
|
-
declare type TPeoplePayload = Pick<TGuest, "name" | "block" | "level" | "unit" | "unitName" | "contact" | "plateNumber" | "nric" | "contact" | "remarks" | "org" | "site" | "start" | "end" | "type" | "isOwner"> & { status?: string; user?: string; occupancyRole?: TOccupancyRole | "" }
|
|
30
|
+
declare type TPeoplePayload = Pick<TGuest, "name" | "block" | "level" | "unit" | "unitName" | "contact" | "plateNumber" | "nric" | "contact" | "remarks" | "org" | "site" | "start" | "end" | "type" | "isOwner"> & { status?: string }
|
|
58
31
|
|
|
59
32
|
declare type TPeopleType = "guest" | "resident" | "tenant"
|
|
@@ -7,11 +7,6 @@ declare type TServiceProvider = {
|
|
|
7
7
|
serviceProviderOrgId: string;
|
|
8
8
|
type: string;
|
|
9
9
|
nature: string;
|
|
10
|
-
status?: string;
|
|
11
|
-
email?: string;
|
|
12
|
-
/** The building unit this provider serves (a unit's Management tab). */
|
|
13
|
-
unit?: string | null;
|
|
14
|
-
unitName?: string;
|
|
15
10
|
};
|
|
16
11
|
|
|
17
12
|
declare type TServiceProviderName = {
|
|
@@ -45,7 +45,7 @@ export type TTargetRow = {
|
|
|
45
45
|
* (core `withNotificationKind`); `site` stands in for `siteId` on rows kept
|
|
46
46
|
* unscoped on purpose. Older rows have neither and are read from `module`.
|
|
47
47
|
*/
|
|
48
|
-
params?: { id?: string; org?: string; kind?: string; site?: string
|
|
48
|
+
params?: { id?: string; org?: string; kind?: string; site?: string } | null;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
/** What the surrounding app knows that the stored row does not. */
|
|
@@ -127,26 +127,9 @@ export function notificationKind(item: TTargetRow | null | undefined): string {
|
|
|
127
127
|
return "";
|
|
128
128
|
}
|
|
129
129
|
if (module === "other") {
|
|
130
|
-
|
|
131
|
-
// Inventory alerts sent before core had an inventory module were stored as "other".
|
|
132
|
-
return /request waiting|request (was )?(approved|rejected)|low (on )?stock|stock handed to you|hand-over (confirmed|declined)/i.test(title)
|
|
133
|
-
? "inventory"
|
|
134
|
-
: "";
|
|
130
|
+
return /(accepted|declined) your invitation$/i.test(title) ? "serviceProviderInviteAnswer" : "";
|
|
135
131
|
}
|
|
136
|
-
|
|
137
|
-
return module === "work-order" ? "workOrder" : module;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Which inventory tab an alert is about. Alerts carry the store id but no tab,
|
|
142
|
-
* so the tab follows the alert's fixed title (API-core inventory-notify.service)
|
|
143
|
-
* - the same rule the mobile apps use. Values are the web tabs' own names.
|
|
144
|
-
*/
|
|
145
|
-
export function inventoryTabForAlert(title: string): "requests" | "stock" | "mine" | "" {
|
|
146
|
-
if (/stock handed to you|hand-over (confirmed|declined)/i.test(title)) return "mine";
|
|
147
|
-
if (/request waiting|request (was )?(approved|rejected)/i.test(title)) return "requests";
|
|
148
|
-
if (/low (on )?stock/i.test(title)) return "stock";
|
|
149
|
-
return "";
|
|
132
|
+
return module;
|
|
150
133
|
}
|
|
151
134
|
|
|
152
135
|
/**
|
|
@@ -199,8 +182,6 @@ const SITE_PAGES: Record<string, TSitePage[]> = {
|
|
|
199
182
|
{ apps: ["security"], page: () => "virtual-patrol/cctv" },
|
|
200
183
|
{ apps: ["property-management"], page: () => "cctv" },
|
|
201
184
|
],
|
|
202
|
-
// A robot at the site raised an alarm (core safety alert, category robotAlert).
|
|
203
|
-
robotAlert: [{ apps: ["security", "property-management", "hygiene"], page: () => "robot-management/alerts" }],
|
|
204
185
|
// hygiene has its Service Providers page too since 2026-09-28
|
|
205
186
|
serviceProviderInviteAnswer: [{ apps: SERVICE_APPS, page: () => "service-providers" }],
|
|
206
187
|
};
|
|
@@ -233,12 +214,7 @@ function sitePageTarget(
|
|
|
233
214
|
const list = apps === "any" ? [home, ...SERVICE_APPS.filter((a) => a !== home)] : apps;
|
|
234
215
|
for (const app of list) candidates.push({ app, path: base + page(id) });
|
|
235
216
|
}
|
|
236
|
-
|
|
237
|
-
// Open the alert's own store on the tab it is about (InventoryMain reads ?storeId=&tab=).
|
|
238
|
-
const storeId = String(item.params?.storeId ?? item.params?.id ?? "").trim();
|
|
239
|
-
const tab = inventoryTabForAlert(String(item.title ?? ""));
|
|
240
|
-
const query: Record<string, string> = { ...(storeId ? { storeId } : {}), ...(tab ? { tab } : {}) };
|
|
241
|
-
return Object.keys(query).length ? { candidates, query } : { candidates };
|
|
217
|
+
return { candidates };
|
|
242
218
|
}
|
|
243
219
|
|
|
244
220
|
/** Seven365 staff pages, in the organisation app. */
|
|
@@ -318,21 +294,6 @@ export function notificationTarget(
|
|
|
318
294
|
};
|
|
319
295
|
}
|
|
320
296
|
|
|
321
|
-
/** Kinds that only the iService365 phone apps can open (residents' own screens). */
|
|
322
|
-
const PHONE_ONLY_KINDS = new Set(["prelovedMarketplace", "marketplace-order"]);
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* What the bell says when a click has nowhere to go, so a click is never a
|
|
326
|
-
* silent nothing. A link grants nothing either way: the page opened still
|
|
327
|
-
* checks the person may see it.
|
|
328
|
-
*/
|
|
329
|
-
export function noTargetMessage(item: TTargetRow | null | undefined): string {
|
|
330
|
-
if (PHONE_ONLY_KINDS.has(notificationKind(item))) {
|
|
331
|
-
return "This notification opens in the iService365 app on your phone.";
|
|
332
|
-
}
|
|
333
|
-
return "There is no page to open for this notification here. It has been marked as read.";
|
|
334
|
-
}
|
|
335
|
-
|
|
336
297
|
/**
|
|
337
298
|
* The full URL for a target whose page lives in another web app, or "" when
|
|
338
299
|
* there is none to build (no path, or that app's URL is not configured).
|
package/utils/role.ts
CHANGED
|
@@ -9,19 +9,6 @@
|
|
|
9
9
|
export const showSiteField = (siteState: boolean, type: string): boolean =>
|
|
10
10
|
siteState && type !== "admin";
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* A role's name with its site, "Admin · St. Regis Residences" (owner
|
|
14
|
-
* 2026-10-01: role names are unique per SITE, so two sites may both have an
|
|
15
|
-
* "Admin" - two independent roles - and every list that can show both must
|
|
16
|
-
* say which is which). Organisation-level roles (no site) show the name alone.
|
|
17
|
-
* `siteName` comes from the role list (`GET /api/roles`, core 2026-10-01).
|
|
18
|
-
*/
|
|
19
|
-
export function roleLabel(role: { name?: unknown; siteName?: unknown; site?: unknown } | null | undefined): string {
|
|
20
|
-
const name = typeof role?.name === "string" ? role.name : "";
|
|
21
|
-
const site = typeof role?.siteName === "string" ? role.siteName.trim() : "";
|
|
22
|
-
return role?.site && site ? `${name} · ${site}` : name;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
12
|
/**
|
|
26
13
|
* The Assign Role picker's list, with the member's CURRENT role always in it.
|
|
27
14
|
*
|
|
@@ -63,13 +50,13 @@ export function memberRoleChip(
|
|
|
63
50
|
const limited = limitedSites(item);
|
|
64
51
|
if (orgLevel && limited.length) {
|
|
65
52
|
return {
|
|
66
|
-
text: limited.map((l) => `${l.roleName || "No role"}
|
|
53
|
+
text: limited.map((l) => `${l.roleName || "No role"} (${l.siteName || "a site"})`).join(", "),
|
|
67
54
|
tone: "info",
|
|
68
55
|
};
|
|
69
56
|
}
|
|
70
57
|
if (orgLevel && item?.roleState === "other-site") {
|
|
71
58
|
const site = typeof item?.roleSiteName === "string" && item.roleSiteName ? item.roleSiteName : "";
|
|
72
|
-
return { text: site ? `${name || "Role"}
|
|
59
|
+
return { text: site ? `${name || "Role"} (${site})` : name || "Role", tone: "info" };
|
|
73
60
|
}
|
|
74
61
|
switch (item?.roleState) {
|
|
75
62
|
case "missing":
|