@7365admin1/layer-common 4.41.0 → 4.42.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.42.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5472953: Roles and permissions (owner approval 2026-09-25): Robots offered in the Property Management and Cleaning role editors; the PM editor's camera-wall row reads "CCTV" (label only, `virtual-patrol:*` strings unchanged); PM's CCTV page follows the client's module list (`org-site-cctv` -> `virtual-patrol`, fails open); `NavigationDrawer` takes `sectionLabels` so an app can rename a sidebar heading.
8
+
3
9
  ## 4.41.0
4
10
 
5
11
  ### Minor Changes
@@ -1471,11 +1477,11 @@
1471
1477
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
1472
1478
  own rule from two endpoints the console already calls, unprojected:
1473
1479
 
1474
- GET /api/members/user/:user/app/admin the Seven365 staff membership
1475
- GET /api/roles/id/:role that membership's role document
1480
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
1481
+ GET /api/roles/id/:role that membership's role document
1476
1482
 
1477
- owner = member.type === "admin" && role.type === "admin" && role.default === true
1478
- staff = member.type === "admin" && role.type === "admin"
1483
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
1484
+ staff = member.type === "admin" && role.type === "admin"
1479
1485
 
1480
1486
  `role.default` is the marker because it is the only property of a platform
1481
1487
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -129,6 +129,9 @@ import { withoutDeniedRoutes } from "../../utils/module-routes";
129
129
  const props = defineProps({
130
130
  navigationItems: { type: Array, required: true },
131
131
  title: { type: String, default: "" },
132
+ // An app's own name for a section heading (`withSections`), e.g.
133
+ // `{ "SECURITY OPERATIONS": "ESTATE SECURITY" }` in Property Management.
134
+ sectionLabels: { type: Object, default: () => ({}) },
132
135
  });
133
136
 
134
137
  const emit = defineEmits(["navigate"]);
@@ -196,7 +199,7 @@ const navigationItems = computed(() => {
196
199
  return withoutDeniedRoutes(items, userAppModulesDenied.value);
197
200
  });
198
201
 
199
- const sectionedItems = computed(() => withSections(navigationItems.value));
202
+ const sectionedItems = computed(() => withSections(navigationItems.value, props.sectionLabels as Record<string, string>));
200
203
 
201
204
  /**
202
205
  * The design's brand tile carries the product's own mark. One icon for eleven
@@ -139,7 +139,7 @@
139
139
  </template>
140
140
  <v-list-item-title>
141
141
  <span class="text-capitalize role-create__row-title">
142
- {{ formatResourceLabel(String(resourceKey)) }}
142
+ {{ formatResourceLabel(String(resourceKey), props.type) }}
143
143
  </span>
144
144
  </v-list-item-title>
145
145
  <template #append>
@@ -279,6 +279,7 @@ const props = defineProps({
279
279
  },
280
280
  });
281
281
 
282
+
282
283
  const emit = defineEmits(["cancel", "success", "success:create-more"]);
283
284
 
284
285
  const validForm = ref(false);
@@ -537,14 +538,14 @@ const moduleSearch = ref<string | null>("");
537
538
  /** `safePermissions`, narrowed to what the search box matches. */
538
539
  const visiblePermissions = computed(() => {
539
540
  const query = (moduleSearch.value ?? "").trim().toLowerCase();
540
- const all = sortResourcesByLabel(safePermissions.value);
541
+ const all = sortResourcesByLabel(safePermissions.value, props.type);
541
542
  if (!query) return all;
542
543
 
543
544
  const matches: Record<string, any> = {};
544
545
  for (const [resource, actions] of Object.entries(all)) {
545
546
  const haystack = [
546
547
  resource,
547
- formatResourceLabel(resource),
548
+ formatResourceLabel(resource, props.type),
548
549
  ...Object.keys(actions ?? {}).flatMap((action) => [
549
550
  action,
550
551
  formatActionLabel(action),
@@ -102,7 +102,7 @@
102
102
  />
103
103
  </template>
104
104
  <v-list-item-title class="text-capitalize role-perm__row-title">
105
- {{ formatResourceLabel(String(resourceKey)) }}
105
+ {{ formatResourceLabel(String(resourceKey), props.type) }}
106
106
  </v-list-item-title>
107
107
  <template #append>
108
108
  <div class="role-perm__cat-actions">
@@ -191,7 +191,7 @@
191
191
  <v-divider v-if="resourceIndex > 0" />
192
192
  <v-list-item density="compact" :ripple="false">
193
193
  <v-list-item-title class="text-capitalize role-perm__row-title">
194
- {{ formatResourceLabel(String(resourceKey)) }}
194
+ {{ formatResourceLabel(String(resourceKey), props.type) }}
195
195
  </v-list-item-title>
196
196
  <v-list-item-subtitle class="role-perm__row-sub">
197
197
  {{
@@ -311,8 +311,15 @@ const props = defineProps({
311
311
  type: Number,
312
312
  default: 0,
313
313
  },
314
+ // The role type the editor was opened for; only changes a module's NAME
315
+ // where one app calls a key something else (`RESOURCE_LABELS_BY_TYPE`).
316
+ type: {
317
+ type: String,
318
+ default: "",
319
+ },
314
320
  });
315
321
 
322
+
316
323
  /** A role holding the wildcard has ONE stored entry and reaches everything. */
317
324
  const holdsWildcard = computed(() => (definedModel.value ?? []).includes("*"));
318
325
 
@@ -486,14 +493,14 @@ const moduleSearch = ref<string | null>("");
486
493
 
487
494
  const visiblePermissions = computed(() => {
488
495
  const query = (moduleSearch.value ?? "").trim().toLowerCase();
489
- const all = sortResourcesByLabel(safePermissions.value);
496
+ const all = sortResourcesByLabel(safePermissions.value, props.type);
490
497
  if (!query) return all;
491
498
 
492
499
  const matches: Record<string, any> = {};
493
500
  for (const [resource, actions] of Object.entries(all)) {
494
501
  const haystack = [
495
502
  resource,
496
- formatResourceLabel(resource),
503
+ formatResourceLabel(resource, props.type),
497
504
  ...Object.keys((actions as Record<string, unknown>) ?? {}).flatMap(
498
505
  (action) => [action, formatActionLabel(action)]
499
506
  ),
@@ -509,7 +516,7 @@ const visiblePermissions = computed(() => {
509
516
  // Same A-Z as the tree above it; a second list in a second order reads as a
510
517
  // different kind of thing.
511
518
  const sortedNotOffered = computed(() =>
512
- sortResourcesByLabel(props.notOfferedPermissions as Record<string, any>)
519
+ sortResourcesByLabel(props.notOfferedPermissions as Record<string, any>, props.type)
513
520
  );
514
521
 
515
522
  /*
@@ -120,6 +120,7 @@
120
120
  :can-update-role="props.canUpdateRole"
121
121
  :catalogue-size="permissionCatalogueSize"
122
122
  :module-count="permissionModuleCount"
123
+ :type="props.type"
123
124
  />
124
125
  </v-dialog>
125
126
 
@@ -13,6 +13,7 @@ export default function useCleaningPermission() {
13
13
  bulletinBoardPermissions,
14
14
 
15
15
  inventoryPermissions,
16
+ robotPermissions,
16
17
  } = useCommonPermissions();
17
18
  const permissions: TPermissions = {
18
19
  inventory: inventoryPermissions,
@@ -291,6 +292,7 @@ export default function useCleaningPermission() {
291
292
  "Allows the user to view detailed information for item checkouts.",
292
293
  },
293
294
  },
295
+ "robot-mgmt": robotPermissions,
294
296
  };
295
297
 
296
298
  return {
@@ -331,7 +331,39 @@ export function useCommonPermissions() {
331
331
  },
332
332
  };
333
333
 
334
+ /*
335
+ * ROBOTS - owner approval 2026-09-25, item 1: the Property Management and
336
+ * Cleaning role editors offer Robots, so a narrower role can be given robot
337
+ * access. Same five strings, spelling and wording as `useSecurityPermission`
338
+ * and exactly what core `robot-permission.util.ts ROBOT_PERMISSIONS` enforces.
339
+ * Catalogue-only: offering an entry grants nobody anything until it is ticked.
340
+ */
341
+ const robotPermissions = {
342
+ "add-robot": {
343
+ check: true,
344
+ description: "Allows the user to add a new robot to the system.",
345
+ },
346
+ "see-all-robots": {
347
+ check: true,
348
+ description: "Allows the user to view the list of all robots.",
349
+ },
350
+ "see-robot-details": {
351
+ check: true,
352
+ description: "Allows the user to view the details of a specific robot.",
353
+ },
354
+ "update-robot": {
355
+ check: true,
356
+ description: "Allows the user to update robot details.",
357
+ },
358
+ "delete-robot": {
359
+ check: true,
360
+ description:
361
+ "Allows the user to remove a robot from the system permanently.",
362
+ },
363
+ };
364
+
334
365
  return {
366
+ robotPermissions,
335
367
  inventoryPermissions,
336
368
  invitationPermissions,
337
369
  memberPermissions,
@@ -42,6 +42,7 @@ export default function usePropertyPermission() {
42
42
  siteSettingsPermissions,
43
43
 
44
44
  inventoryPermissions,
45
+ robotPermissions,
45
46
  } = useCommonPermissions();
46
47
 
47
48
 
@@ -618,6 +619,7 @@ export default function usePropertyPermission() {
618
619
  "Allows the user to view the list of all resident transactions on mobile. Checked by the iMA365 mobile app.",
619
620
  },
620
621
  },
622
+ "robot-mgmt": robotPermissions,
621
623
  "site-settings": siteSettingsPermissions
622
624
  };
623
625
 
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.41.0",
5
+ "version": "4.42.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.",
@@ -12,7 +12,7 @@
12
12
  * (checked against every app's own `useLocalPermission` nav gate, 2026-09-11).
13
13
  * Shared or ambiguous pages stay out: calendar, keys (gated on `site-settings`),
14
14
  * access-mgmt (`site-settings:manage-entry-pass` also opens it), equipment and
15
- * inventory, the property CCTV wall, service-providers and support tickets.
15
+ * inventory, service-providers and support tickets.
16
16
  *
17
17
  * NEVER MAPPED: the dashboard, settings, members, invitations and roles. A client
18
18
  * must always be able to run its own people and roles; the server never denies
@@ -40,6 +40,13 @@ export const MODULE_ROUTES: Readonly<Record<string, string>> = Object.freeze({
40
40
  // Includes its CCTV page: security's drawer shows it to whoever can see
41
41
  // Virtual Patrol, so it follows the same module.
42
42
  "org-site-virtual-patrol": "virtual-patrol",
43
+ // Property Management's camera wall (owner approval 2026-09-25, item 3). Its
44
+ // menu, page and the server's wall endpoint all read the four legacy
45
+ // `virtual-patrol:` strings (`useCameraViewPermission`, core
46
+ // `CAMERA_VIEW_PERMISSIONS`), and the PM role editor now calls that row
47
+ // "CCTV". So it follows the same module as Security's own CCTV page above.
48
+ // Fails open like every entry: a client with no module list keeps it.
49
+ "org-site-cctv": "virtual-patrol",
43
50
  "org-site-robot-management": "robot-mgmt",
44
51
  "org-site-attendance": "attendance-mgmt",
45
52
  "org-site-my-attendance": "my-attendance",
@@ -125,7 +125,13 @@ export const SECTION_ORDER: string[] = [
125
125
  * objects; only the order changes.
126
126
  */
127
127
  export function withSections<T extends { title?: string }>(
128
- items: T[]
128
+ items: T[],
129
+ /**
130
+ * An application's own name for a heading, e.g. Property Management prints
131
+ * SECURITY OPERATIONS as ESTATE SECURITY (owner approval 2026-09-25, item 6).
132
+ * Changes the printed label only; grouping and order are unchanged.
133
+ */
134
+ labels: Readonly<Record<string, string>> = {}
129
135
  ): Array<TSectionedNavItem<T>> {
130
136
  // An unmapped title inherits the section of the item above it.
131
137
  let current = "";
@@ -144,7 +150,7 @@ export function withSections<T extends { title?: string }>(
144
150
 
145
151
  let printed = "";
146
152
  return grouped.map(({ item, section }) => {
147
- const label = section === printed ? "" : section;
153
+ const label = section === printed ? "" : labels[section] ?? section;
148
154
  printed = section;
149
155
  return { item, section: label };
150
156
  });
@@ -55,14 +55,31 @@ export const RESOURCE_LABELS: Record<string, string> = {
55
55
  roles: "Roles and permissions",
56
56
  };
57
57
 
58
+ /**
59
+ * Names that differ BY APPLICATION, keyed by the role `type` the editor is
60
+ * opened with. Only where one key means a different screen in one app.
61
+ *
62
+ * Owner approval 2026-09-25, item 2: in the Property Management editor the
63
+ * `virtual-patrol` row is the CCTV camera wall (the four legacy strings
64
+ * `useCameraViewPermission` and core `CAMERA_VIEW_PERMISSIONS` read), not the
65
+ * Security app's Virtual Patrol. Label only: the key and every stored string
66
+ * stay `virtual-patrol:*`, so every role that holds them keeps working.
67
+ */
68
+ export const RESOURCE_LABELS_BY_TYPE: Record<string, Record<string, string>> = {
69
+ property_management_agency: { "virtual-patrol": "CCTV" },
70
+ real_estate_developer: { "virtual-patrol": "CCTV" },
71
+ };
72
+
58
73
  /**
59
74
  * `work_orders` -> `Work orders`.
60
75
  *
61
76
  * Sentence case, not Title Case: a list of 47 Title Cased phrases is harder to
62
77
  * scan, not easier.
63
78
  */
64
- export function formatResourceLabel(key: string): string {
79
+ export function formatResourceLabel(key: string, type?: string): string {
65
80
  if (!key) return "";
81
+ const byType = type ? RESOURCE_LABELS_BY_TYPE[type]?.[key] : undefined;
82
+ if (byType) return byType;
66
83
  if (RESOURCE_LABELS[key]) return RESOURCE_LABELS[key];
67
84
 
68
85
  const words = key
@@ -115,11 +132,12 @@ export function formatActionLabel(action: string): string {
115
132
  */
116
133
  export function sortResourcesByLabel<T>(
117
134
  tree: Record<string, T> | null | undefined,
135
+ type?: string,
118
136
  ): Record<string, T> {
119
137
  if (!tree || typeof tree !== "object") return {};
120
138
  const sorted: Record<string, T> = {};
121
139
  for (const key of Object.keys(tree).sort((a, b) =>
122
- formatResourceLabel(a).localeCompare(formatResourceLabel(b), undefined, {
140
+ formatResourceLabel(a, type).localeCompare(formatResourceLabel(b, type), undefined, {
123
141
  sensitivity: "base",
124
142
  numeric: true,
125
143
  }),