@7365admin1/layer-common 4.35.1-staging.385 → 4.37.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +16 -4
  2. package/assets/css/primitives.css +0 -43
  3. package/components/AccessCardAddForm.vue +4 -5
  4. package/components/AccessCardAssignToUnitForm.vue +2 -3
  5. package/components/AccessCardPreviewDialog.vue +3 -4
  6. package/components/AppSelect.vue +3 -77
  7. package/components/CameraForm.vue +6 -20
  8. package/components/DashboardMain.vue +60 -66
  9. package/components/Dialog/UpdateMoreAction.vue +1 -11
  10. package/components/DocumentForm.vue +112 -407
  11. package/components/DocumentManagement.vue +12 -213
  12. package/components/EntryPass/QrTemplatePreview.vue +25 -21
  13. package/components/EntryPassInformation.vue +24 -119
  14. package/components/Facility/BookingSetup.vue +0 -63
  15. package/components/HidAccessLogDashboard.vue +17 -74
  16. package/components/HidIntercomManagement.vue +14 -180
  17. package/components/HidQrCodeConfiguration.vue +195 -17
  18. package/components/HidReaderManagement.vue +0 -398
  19. package/components/HidReaderUserRoster.vue +3 -44
  20. package/components/HidUserEnrollment.vue +155 -845
  21. package/components/InvitationClientForm.vue +1 -19
  22. package/components/Layout/NavigationDrawer.vue +1 -4
  23. package/components/NotificationBell.vue +7 -59
  24. package/components/RolePermissionFormCreate.vue +3 -4
  25. package/components/RolePermissionFormPreviewUpdate.vue +5 -12
  26. package/components/RolePermissionMain.vue +0 -1
  27. package/components/ServiceProviderInvitationPrompt.vue +155 -0
  28. package/components/ServiceProviderMain.vue +30 -178
  29. package/components/VisitorForm.vue +0 -12
  30. package/components/VisitorManagement.vue +10 -285
  31. package/components/VisitorSocketPopUp.vue +1 -2
  32. package/composables/useANPRSettings.ts +15 -45
  33. package/composables/useAccessManagement.ts +0 -5
  34. package/composables/useCleaningPermission.ts +0 -2
  35. package/composables/useCommonPermission.ts +0 -32
  36. package/composables/useDocument.ts +0 -7
  37. package/composables/useFacility.ts +0 -11
  38. package/composables/useHidAmico.ts +0 -159
  39. package/composables/useHidNavigation.ts +1 -25
  40. package/composables/usePeople.ts +0 -19
  41. package/composables/usePropertyPermission.ts +0 -19
  42. package/composables/useSecurityPermission.ts +0 -19
  43. package/composables/useServiceProvider.ts +0 -3
  44. package/composables/useServiceProviderInvitation.ts +3 -101
  45. package/composables/useSiteEntryPassSettings.ts +0 -26
  46. package/composables/useWebUsb.ts +11 -14
  47. package/package.json +2 -2
  48. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
  49. package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
  50. package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
  51. package/plugins/API.ts +0 -11
  52. package/types/document.d.ts +0 -14
  53. package/types/facility.d.ts +0 -4
  54. package/types/people.d.ts +1 -28
  55. package/utils/module-routes.ts +1 -8
  56. package/utils/nav-sections.ts +2 -8
  57. package/utils/notification-target.ts +8 -277
  58. package/utils/permission-labels.ts +2 -20
  59. package/components/ClientInvitations.vue +0 -291
  60. package/components/HidAccessPermissions.vue +0 -424
  61. package/components/HidProfileQrCode.vue +0 -332
  62. package/composables/useHidReaderSelection.ts +0 -50
  63. package/composables/useSiteCategory.ts +0 -45
  64. package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +0 -23
  65. package/utils/dashboard-widget-prefs.ts +0 -54
  66. package/utils/hid-permission-assignments.ts +0 -190
  67. package/utils/hid-reader-selection.ts +0 -52
  68. package/utils/occupancy-role.ts +0 -109
  69. package/utils/sp-inviter-row.ts +0 -97
@@ -388,27 +388,9 @@ watch(
388
388
  const roles = ref<any[]>([])
389
389
  const { getRoles } = useRole()
390
390
 
391
- /*
392
- * `type` is the ROLE type, not the service the client subscribes to, and the
393
- * only caller (`ClientMain.vue` "Invite Client") passes no `app` prop -- so
394
- * this sent `type: ""` and the endpoint applied no type filter at all. The
395
- * dropdown therefore listed every org-less role of every type, which includes
396
- * the platform's OWN staff roles (`type: "admin"` -- the two rows the console's
397
- * `/super-admin/role-permissions` screen edits). Offering a Seven365 console
398
- * staff role on a CLIENT invitation is the defect.
399
- *
400
- * A client admin's role is `type: "organization"`, which is what the client
401
- * roles screen (`org/[organization]/roles-permissions.vue`) already lists. This
402
- * narrows to exactly that. It is a narrowing of this one picker only: no role
403
- * changes, no permission changes, and the org-less `organization` roles that
404
- * were already being picked are all still offered.
405
- *
406
- * Keying off the selected service instead would empty the dropdown -- no
407
- * org-less role carries a service type -- so `props.app` is not the filter.
408
- */
409
391
  const { data: roleData, refresh: refreshRoles } = await useLazyAsyncData(
410
392
  "roles",
411
- () => getRoles({ org: org.value?._id, type: "organization", limit: 50 }),
393
+ () => getRoles({ org: org.value?._id, type: props.app, limit: 50 }),
412
394
  { watch: [() => props.app] }
413
395
  )
414
396
 
@@ -129,9 +129,6 @@ 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: () => ({}) },
135
132
  });
136
133
 
137
134
  const emit = defineEmits(["navigate"]);
@@ -199,7 +196,7 @@ const navigationItems = computed(() => {
199
196
  return withoutDeniedRoutes(items, userAppModulesDenied.value);
200
197
  });
201
198
 
202
- const sectionedItems = computed(() => withSections(navigationItems.value, props.sectionLabels as Record<string, string>));
199
+ const sectionedItems = computed(() => withSections(navigationItems.value));
203
200
 
204
201
  /**
205
202
  * The design's brand tile carries the product's own mark. One icon for eleven
@@ -70,10 +70,7 @@
70
70
  :key="item._id"
71
71
  type="button"
72
72
  class="notif-card"
73
- :class="{
74
- 'notif-card--unread': !item.hasRead,
75
- 'notif-card--plain': !targetFor(item),
76
- }"
73
+ :class="{ 'notif-card--unread': !item.hasRead }"
77
74
  @click="openNotification(item)"
78
75
  >
79
76
  <div class="notif-card__top">
@@ -137,7 +134,7 @@ import {
137
134
  timeAgo,
138
135
  type TInboxItem,
139
136
  } from "../utils/notification-inbox";
140
- import { chooseNavigation, notificationTarget } from "../utils/notification-target";
137
+ import { notificationTarget } from "../utils/notification-target";
141
138
 
142
139
  /**
143
140
  * The top-bar notification feed for the web, in every signed-in app.
@@ -171,7 +168,6 @@ const { getAll, getUnreadCount, markAsRead, markAllAsRead } = useNotification();
171
168
  const { currentUser } = useLocalAuth();
172
169
  const route = useRoute();
173
170
  const router = useRouter();
174
- const appConfig = useRuntimeConfig();
175
171
 
176
172
  const open = ref(false);
177
173
  const loading = ref(false);
@@ -307,59 +303,16 @@ function markRead(item: Record<string, any>) {
307
303
  * layer: the ten apps that have no order queue simply do not match it, and the
308
304
  * click there stays what it always was.
309
305
  */
310
- function targetFor(item: Record<string, any>) {
311
- return notificationTarget(item as any, {
312
- // The org app nests under `organization`; the service apps under `org`.
313
- // Staff pages and the org app have neither, so the person's own default
314
- // organisation builds a service-app link from there.
315
- organization: String(
316
- route.params?.organization ?? route.params?.org ?? currentUser.value?.defaultOrg ?? "",
317
- ),
318
- site: String(route.params?.site ?? ""),
319
- });
320
- }
321
-
322
- /** A path this app really has a page for (every app has a 404 fallback, so check the match). */
323
- function hasLocalPage(path: string) {
324
- try {
325
- const resolved = router.resolve(path);
326
- return resolved.matched.length > 0 && !resolved.matched.some((m) => m.path.includes("pathMatch"));
327
- } catch {
328
- return false;
329
- }
330
- }
331
-
332
- /** Every web app's URL, keyed like `TAppKey`, from the layer's runtime config. */
333
- function appUrls() {
334
- const p = appConfig?.public ?? {};
335
- return {
336
- org: String(p.APP_ORG ?? ""),
337
- security: String(p.APP_SECURITY ?? ""),
338
- "property-management": String(p.APP_PROPERTY_MANAGEMENT ?? ""),
339
- hygiene: String(p.APP_HYGIENE ?? ""),
340
- "mechanical-electrical": String(p.APP_MECHANICAL_ELECTRICAL ?? ""),
341
- "pest-control": String(p.APP_PEST_CONTROL ?? ""),
342
- landscaping: String(p.APP_LANDSCAPING ?? ""),
343
- "pool-maintenance": String(p.APP_POOL_MAINTENANCE ?? ""),
344
- };
345
- }
346
-
347
- /** The decision is `chooseNavigation` (unit-tested); this only carries it out. */
348
306
  async function goToSubject(item: Record<string, any>) {
349
- const nav = chooseNavigation(targetFor(item), {
350
- hasRoute: (name) => typeof router.hasRoute === "function" && router.hasRoute(name),
351
- hasPage: hasLocalPage,
352
- appUrls: appUrls(),
307
+ const target = notificationTarget(item as any, {
308
+ organization: String(route.params?.organization ?? ""),
353
309
  });
354
- if (!nav) return;
310
+ if (!target) return;
311
+ if (typeof router.hasRoute !== "function" || !router.hasRoute(target.name)) return;
355
312
 
356
313
  open.value = false;
357
- if (nav.type === "href") {
358
- window.location.assign(nav.href);
359
- return;
360
- }
361
314
  try {
362
- await router.push(nav.to as any);
315
+ await router.push(target);
363
316
  } catch {
364
317
  // A cancelled or duplicate navigation is not worth an error in the top bar.
365
318
  }
@@ -498,11 +451,6 @@ onBeforeUnmount(() => {
498
451
  background: var(--hover);
499
452
  }
500
453
 
501
- /* Nothing to open: still marks read on click, but does not look like a link. */
502
- .notif-card--plain {
503
- cursor: default;
504
- }
505
-
506
454
  .notif-card__top {
507
455
  display: flex;
508
456
  align-items: baseline;
@@ -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), props.type) }}
142
+ {{ formatResourceLabel(String(resourceKey)) }}
143
143
  </span>
144
144
  </v-list-item-title>
145
145
  <template #append>
@@ -279,7 +279,6 @@ const props = defineProps({
279
279
  },
280
280
  });
281
281
 
282
-
283
282
  const emit = defineEmits(["cancel", "success", "success:create-more"]);
284
283
 
285
284
  const validForm = ref(false);
@@ -538,14 +537,14 @@ const moduleSearch = ref<string | null>("");
538
537
  /** `safePermissions`, narrowed to what the search box matches. */
539
538
  const visiblePermissions = computed(() => {
540
539
  const query = (moduleSearch.value ?? "").trim().toLowerCase();
541
- const all = sortResourcesByLabel(safePermissions.value, props.type);
540
+ const all = sortResourcesByLabel(safePermissions.value);
542
541
  if (!query) return all;
543
542
 
544
543
  const matches: Record<string, any> = {};
545
544
  for (const [resource, actions] of Object.entries(all)) {
546
545
  const haystack = [
547
546
  resource,
548
- formatResourceLabel(resource, props.type),
547
+ formatResourceLabel(resource),
549
548
  ...Object.keys(actions ?? {}).flatMap((action) => [
550
549
  action,
551
550
  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), props.type) }}
105
+ {{ formatResourceLabel(String(resourceKey)) }}
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), props.type) }}
194
+ {{ formatResourceLabel(String(resourceKey)) }}
195
195
  </v-list-item-title>
196
196
  <v-list-item-subtitle class="role-perm__row-sub">
197
197
  {{
@@ -311,15 +311,8 @@ 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
- },
320
314
  });
321
315
 
322
-
323
316
  /** A role holding the wildcard has ONE stored entry and reaches everything. */
324
317
  const holdsWildcard = computed(() => (definedModel.value ?? []).includes("*"));
325
318
 
@@ -493,14 +486,14 @@ const moduleSearch = ref<string | null>("");
493
486
 
494
487
  const visiblePermissions = computed(() => {
495
488
  const query = (moduleSearch.value ?? "").trim().toLowerCase();
496
- const all = sortResourcesByLabel(safePermissions.value, props.type);
489
+ const all = sortResourcesByLabel(safePermissions.value);
497
490
  if (!query) return all;
498
491
 
499
492
  const matches: Record<string, any> = {};
500
493
  for (const [resource, actions] of Object.entries(all)) {
501
494
  const haystack = [
502
495
  resource,
503
- formatResourceLabel(resource, props.type),
496
+ formatResourceLabel(resource),
504
497
  ...Object.keys((actions as Record<string, unknown>) ?? {}).flatMap(
505
498
  (action) => [action, formatActionLabel(action)]
506
499
  ),
@@ -516,7 +509,7 @@ const visiblePermissions = computed(() => {
516
509
  // Same A-Z as the tree above it; a second list in a second order reads as a
517
510
  // different kind of thing.
518
511
  const sortedNotOffered = computed(() =>
519
- sortResourcesByLabel(props.notOfferedPermissions as Record<string, any>, props.type)
512
+ sortResourcesByLabel(props.notOfferedPermissions as Record<string, any>)
520
513
  );
521
514
 
522
515
  /*
@@ -120,7 +120,6 @@
120
120
  :can-update-role="props.canUpdateRole"
121
121
  :catalogue-size="permissionCatalogueSize"
122
122
  :module-count="permissionModuleCount"
123
- :type="props.type"
124
123
  />
125
124
  </v-dialog>
126
125
 
@@ -0,0 +1,155 @@
1
+ <template>
2
+ <!-- `AppCard` is the design's card - 14px radius, a 1px `--border` rule and
3
+ the one-pixel shadow on `--card`. The `v-card rounded="lg"
4
+ variant="outlined"` it replaced was Vuetify's own card at its own
5
+ radius and border weight. -->
6
+ <AppCard v-if="invitation" class="sp-invite-prompt">
7
+ <div class="d-flex align-start ga-3 pa-4">
8
+ <v-avatar color="primary" variant="tonal" size="44">
9
+ <v-icon>mdi-office-building-marker-outline</v-icon>
10
+ </v-avatar>
11
+ <div class="flex-grow-1">
12
+ <div class="screen-card-title">
13
+ {{ invitation.orgName }} has invited you to {{ invitation.siteName }}
14
+ </div>
15
+ <div class="screen-hint sp-invite-prompt__sub">
16
+ Approved by Seven365 · {{ expiryLine }}
17
+ </div>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="px-4 sp-invite-prompt__body">
22
+ {{ invitation.orgName }} would like your company to provide services at
23
+ <strong>{{ invitation.siteName }}</strong
24
+ >. If you accept, this site will be added to your list of sites.
25
+ </div>
26
+
27
+ <div class="d-flex align-center ga-2 px-4 py-4">
28
+ <!-- Both stay `v-btn`s: `loading` is the only signal that an accept or a
29
+ decline is in flight, and `AppButton` has no equivalent. They take
30
+ the design's two button classes instead. -->
31
+ <v-btn
32
+ variant="flat"
33
+ class="screen-btn-primary"
34
+ :loading="working === 'accept'"
35
+ :disabled="Boolean(working) || !open"
36
+ @click="accept"
37
+ >
38
+ Accept
39
+ </v-btn>
40
+ <v-btn
41
+ variant="outlined"
42
+ class="screen-btn-ghost"
43
+ :loading="working === 'decline'"
44
+ :disabled="Boolean(working) || !open"
45
+ @click="decline"
46
+ >
47
+ Decline
48
+ </v-btn>
49
+ <v-spacer />
50
+ <!-- Was a raw `v-chip` on Vuetify's fixed `success` green, or no colour
51
+ at all for every other outcome. `StatusChip` maps the status WORD
52
+ through the shared `utils/status.ts` tones, so "complete" and
53
+ "declined" come out the same colours they do everywhere else. -->
54
+ <StatusChip
55
+ v-if="!open"
56
+ :status="invitation.status"
57
+ :label="invitation.statusLabel"
58
+ />
59
+ </div>
60
+ </AppCard>
61
+
62
+ <AppCard v-else-if="error">
63
+ <div class="pa-4 sp-invite-prompt__body">{{ error }}</div>
64
+ </AppCard>
65
+ </template>
66
+
67
+ <script setup lang="ts">
68
+ /**
69
+ * The invitation as the service provider sees it — inside the app they already
70
+ * use, from the session they already have.
71
+ *
72
+ * There is deliberately no sign-up and no one-time code anywhere on this
73
+ * screen: a company that already exists on iService365 never creates a second
74
+ * account. The email they receive only carries them here.
75
+ *
76
+ * An invitation that has not been approved by Seven365 is not readable at all —
77
+ * the server answers "not found" — so this component cannot show one.
78
+ */
79
+ const props = defineProps({
80
+ invitationId: { type: String, required: true },
81
+ });
82
+
83
+ const emit = defineEmits<{ (e: "answered", status: string): void }>();
84
+
85
+ const { getInvitation, declineInvitation } = useServiceProviderInvitation();
86
+ const { addViaInvite } = useCustomerSite();
87
+
88
+ const invitation = ref<any>(null);
89
+ const error = ref("");
90
+ const working = ref("");
91
+
92
+ const open = computed(() => invitation.value?.status === "pending");
93
+
94
+ const expiryLine = computed(() => {
95
+ const expires = invitation.value?.expireAt;
96
+ if (!expires) return "";
97
+ const days = Math.max(
98
+ 0,
99
+ Math.ceil((new Date(expires).getTime() - Date.now()) / 86400000),
100
+ );
101
+ if (!open.value) return "no longer waiting for an answer";
102
+ return days <= 1 ? "expires within a day" : `expires in ${days} days`;
103
+ });
104
+
105
+ async function load() {
106
+ try {
107
+ invitation.value = await getInvitation(props.invitationId);
108
+ } catch (err: any) {
109
+ error.value =
110
+ err?.data?.message || "This invitation is no longer available.";
111
+ }
112
+ }
113
+
114
+ async function accept() {
115
+ working.value = "accept";
116
+ try {
117
+ await addViaInvite(props.invitationId);
118
+ await load();
119
+ emit("answered", "complete");
120
+ } catch (err: any) {
121
+ error.value = err?.data?.message || "Could not accept this invitation.";
122
+ } finally {
123
+ working.value = "";
124
+ }
125
+ }
126
+
127
+ async function decline() {
128
+ working.value = "decline";
129
+ try {
130
+ await declineInvitation(props.invitationId);
131
+ await load();
132
+ emit("answered", "declined");
133
+ } catch (err: any) {
134
+ error.value = err?.data?.message || "Could not decline this invitation.";
135
+ } finally {
136
+ working.value = "";
137
+ }
138
+ }
139
+
140
+ await load();
141
+ </script>
142
+
143
+ <style scoped>
144
+ /* The title was a hand-set 1.05rem/600; `.screen-card-title` is the design's
145
+ 15.5px/800, so only the wrap and the leading are left here. */
146
+ .sp-invite-prompt :deep(.screen-card-title) {
147
+ display: block;
148
+ line-height: 1.35;
149
+ }
150
+
151
+ .sp-invite-prompt__sub,
152
+ .sp-invite-prompt__body {
153
+ font-size: var(--fs-cell);
154
+ }
155
+ </style>