@7365admin1/layer-common 4.35.1-staging.384 → 4.36.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 (76) hide show
  1. package/CHANGELOG.md +10 -4
  2. package/assets/css/primitives.css +0 -43
  3. package/assets/css/robot-hero.css +2 -2
  4. package/components/AccessCardAddForm.vue +4 -5
  5. package/components/AccessCardAssignToUnitForm.vue +2 -3
  6. package/components/AccessCardPreviewDialog.vue +3 -4
  7. package/components/AppSelect.vue +3 -77
  8. package/components/CameraForm.vue +6 -20
  9. package/components/DashboardMain.vue +60 -66
  10. package/components/Dialog/UpdateMoreAction.vue +1 -11
  11. package/components/DocumentForm.vue +112 -407
  12. package/components/DocumentManagement.vue +12 -213
  13. package/components/EntryPass/QrTemplatePreview.vue +25 -21
  14. package/components/EntryPassInformation.vue +24 -119
  15. package/components/Facility/BookingSetup.vue +0 -63
  16. package/components/HidAccessLogDashboard.vue +17 -74
  17. package/components/HidIntercomManagement.vue +14 -180
  18. package/components/HidQrCodeConfiguration.vue +195 -17
  19. package/components/HidReaderManagement.vue +0 -398
  20. package/components/HidReaderUserRoster.vue +3 -44
  21. package/components/HidUserEnrollment.vue +155 -845
  22. package/components/InvitationClientForm.vue +1 -19
  23. package/components/Layout/NavigationDrawer.vue +1 -4
  24. package/components/NotificationBell.vue +7 -59
  25. package/components/Robot/BuildingMap.vue +9 -16
  26. package/components/Robot/LiftQueue.vue +4 -9
  27. package/components/Robot/MapEditor.vue +8 -1
  28. package/components/RolePermissionFormCreate.vue +3 -4
  29. package/components/RolePermissionFormPreviewUpdate.vue +5 -12
  30. package/components/RolePermissionMain.vue +0 -1
  31. package/components/ServiceProviderInvitationPrompt.vue +155 -0
  32. package/components/ServiceProviderMain.vue +30 -178
  33. package/components/VisitorForm.vue +0 -12
  34. package/components/VisitorManagement.vue +10 -285
  35. package/components/VisitorSocketPopUp.vue +1 -2
  36. package/composables/useANPRSettings.ts +15 -45
  37. package/composables/useAccessManagement.ts +0 -5
  38. package/composables/useCleaningPermission.ts +0 -2
  39. package/composables/useCommonPermission.ts +0 -32
  40. package/composables/useDocument.ts +0 -7
  41. package/composables/useFacility.ts +0 -11
  42. package/composables/useHidAmico.ts +0 -159
  43. package/composables/useHidNavigation.ts +1 -25
  44. package/composables/usePeople.ts +0 -19
  45. package/composables/usePropertyPermission.ts +0 -19
  46. package/composables/useRobotBuilding.ts +3 -22
  47. package/composables/useRobots.ts +0 -13
  48. package/composables/useSecurityPermission.ts +0 -19
  49. package/composables/useServiceProvider.ts +0 -3
  50. package/composables/useServiceProviderInvitation.ts +3 -101
  51. package/composables/useSiteEntryPassSettings.ts +0 -26
  52. package/composables/useWebUsb.ts +11 -14
  53. package/package.json +2 -2
  54. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
  55. package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
  56. package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
  57. package/plugins/API.ts +0 -11
  58. package/types/document.d.ts +0 -14
  59. package/types/facility.d.ts +0 -4
  60. package/types/people.d.ts +1 -28
  61. package/utils/module-routes.ts +1 -8
  62. package/utils/nav-sections.ts +2 -8
  63. package/utils/notification-target.ts +8 -277
  64. package/utils/permission-labels.ts +2 -20
  65. package/components/ClientInvitations.vue +0 -291
  66. package/components/HidAccessPermissions.vue +0 -424
  67. package/components/HidProfileQrCode.vue +0 -332
  68. package/composables/useHidReaderSelection.ts +0 -50
  69. package/composables/useSiteCategory.ts +0 -45
  70. package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +0 -23
  71. package/utils/dashboard-widget-prefs.ts +0 -54
  72. package/utils/hid-permission-assignments.ts +0 -190
  73. package/utils/hid-reader-selection.ts +0 -52
  74. package/utils/occupancy-role.ts +0 -109
  75. package/utils/robot-fleet-core.ts +0 -57
  76. package/utils/sp-inviter-row.ts +0 -86
@@ -1,291 +0,0 @@
1
- <template>
2
- <div>
3
- <PageHeader
4
- title="Client invitations"
5
- subtitle="Other organisations asking your company to provide services at their sites."
6
- />
7
-
8
- <AppCard class="table-card">
9
- <CardToolbar>
10
- <template #tabs>
11
- <button
12
- v-for="t in tabs"
13
- :key="t.value"
14
- type="button"
15
- role="tab"
16
- class="app-tab"
17
- :class="{ 'app-tab--active': tab === t.value }"
18
- :aria-selected="tab === t.value"
19
- @click="tab = t.value"
20
- >
21
- {{ t.label }}<span v-if="counts[t.value]"> ({{ counts[t.value] }})</span>
22
- </button>
23
- </template>
24
- </CardToolbar>
25
-
26
- <div class="pa-4 d-flex flex-column ga-3">
27
- <div v-if="loading" class="screen-hint">Loading…</div>
28
- <div v-else-if="loadError" class="text-error" role="alert">{{ loadError }}</div>
29
- <div v-else-if="!items.length" class="screen-hint">{{ emptyLine }}</div>
30
-
31
- <AppCard
32
- v-for="item in items"
33
- :key="item._id"
34
- class="client-invite"
35
- :class="{ 'client-invite--linked': item._id === linked }"
36
- >
37
- <div class="d-flex align-start ga-3 pa-4">
38
- <v-avatar color="primary" variant="tonal" size="44">
39
- <v-icon>mdi-office-building-marker-outline</v-icon>
40
- </v-avatar>
41
- <div class="flex-grow-1">
42
- <div class="screen-card-title">
43
- {{ item.orgName }} has invited your company to {{ item.siteName }}
44
- </div>
45
- <div class="screen-hint client-invite__meta">
46
- <span v-if="item.service">Services: {{ serviceLabel(item.service) }} · </span>
47
- <span v-if="item.invitedByName">Invited by {{ item.invitedByName }} · </span>
48
- <span>Sent {{ day(item.createdAt) }}</span>
49
- </div>
50
- </div>
51
- <StatusChip :status="item.status" :label="item.statusLabel" />
52
- </div>
53
-
54
- <div class="px-4 pb-4 client-invite__body">
55
- {{ stateLine(item) }}
56
- </div>
57
-
58
- <!-- Buttons only while the invitation is waiting for an answer. -->
59
- <div v-if="item.status === 'pending'" class="d-flex align-center ga-2 px-4 pb-4">
60
- <v-btn
61
- variant="flat"
62
- class="screen-btn-primary"
63
- :loading="working[item._id] === 'accept'"
64
- :disabled="Boolean(working[item._id])"
65
- @click="answer(item, 'accept')"
66
- >
67
- Accept
68
- </v-btn>
69
- <v-btn
70
- variant="outlined"
71
- class="screen-btn-ghost"
72
- :loading="working[item._id] === 'decline'"
73
- :disabled="Boolean(working[item._id])"
74
- @click="answer(item, 'decline')"
75
- >
76
- Decline
77
- </v-btn>
78
- </div>
79
-
80
- <div v-else-if="item.status === 'complete'" class="px-4 pb-4">
81
- <v-btn
82
- v-if="item.needsSiteLink"
83
- variant="flat"
84
- class="screen-btn-primary"
85
- :loading="working[item._id] === 'link'"
86
- :disabled="Boolean(working[item._id])"
87
- @click="addToSites(item)"
88
- >
89
- Add {{ item.siteName }} to your Sites
90
- </v-btn>
91
- <NuxtLink v-else-if="item.onThisOrg" :to="`/org/${org}/sites`">Open Sites</NuxtLink>
92
- <NuxtLink
93
- v-else-if="item.providerOrgId && item.providerOrgId !== org"
94
- :to="`/org/${item.providerOrgId}/sites`"
95
- >
96
- Open {{ item.providerOrgName || "that company" }}'s Sites
97
- </NuxtLink>
98
- </div>
99
-
100
- <div v-if="errors[item._id]" class="px-4 pb-4 text-error client-invite__body" role="alert">
101
- {{ errors[item._id] }}
102
- </div>
103
- </AppCard>
104
- </div>
105
- </AppCard>
106
- </div>
107
- </template>
108
-
109
- <script setup lang="ts">
110
- /**
111
- * "Client invitations" (owner request 2026-09-24): where a service-provider
112
- * company answers invitations from its clients, in four tabs, with one plain
113
- * sentence per card saying what happened. Accept / Decline appear only while
114
- * an invitation is waiting - an answered one has no buttons at all.
115
- *
116
- * Accepting adds the site to the company ON SCREEN (`org`), which is what makes
117
- * it appear on this company's Sites list and in the service app's site
118
- * switcher. The server refuses a company the person is not a member of.
119
- */
120
- import type { TReceivedServiceProviderInvitation } from "../composables/useServiceProviderInvitation";
121
-
122
- const props = defineProps({
123
- /** The organisation on screen - the company that accepts. */
124
- org: { type: String, required: true },
125
- });
126
-
127
- type Tab = "pending" | "accepted" | "declined" | "expired";
128
-
129
- const tabs: Array<{ value: Tab; label: string }> = [
130
- { value: "pending", label: "Pending" },
131
- { value: "accepted", label: "Accepted" },
132
- { value: "declined", label: "Declined" },
133
- { value: "expired", label: "Expired" },
134
- ];
135
-
136
- const route = useRoute();
137
- const {
138
- getReceivedInvitations,
139
- getInvitation,
140
- acceptInvitation,
141
- declineInvitation,
142
- linkInvitationSite,
143
- } = useServiceProviderInvitation();
144
-
145
- const tab = ref<Tab>("pending");
146
- const items = ref<TReceivedServiceProviderInvitation[]>([]);
147
- const counts = ref<Record<string, number>>({});
148
- const loading = ref(false);
149
- const loadError = ref("");
150
- const working = reactive<Record<string, string>>({});
151
- const errors = reactive<Record<string, string>>({});
152
-
153
- /** The invitation the bell linked to (`?invite=<id>`), outlined. */
154
- const linked = computed(() => {
155
- const value = String(route.query?.invite ?? "").trim();
156
- return /^[a-f0-9]{24}$/i.test(value) ? value : "";
157
- });
158
-
159
- const emptyLine = computed(
160
- () =>
161
- ({
162
- pending: "Nothing is waiting for your answer.",
163
- accepted: "You have not accepted any invitations yet.",
164
- declined: "You have not declined any invitations.",
165
- expired: "No expired or withdrawn invitations.",
166
- })[tab.value],
167
- );
168
-
169
- function day(value?: string | null) {
170
- if (!value) return "";
171
- const d = new Date(value);
172
- return Number.isNaN(d.getTime())
173
- ? ""
174
- : d.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
175
- }
176
-
177
- /** " on 24 Sep 2026", or nothing when the date is unknown. */
178
- function on(value?: string | null) {
179
- const text = day(value);
180
- return text ? ` on ${text}` : "";
181
- }
182
-
183
- function serviceLabel(value: string) {
184
- return value.replace(/[-_]/g, " ").replace(/^\w/, (c) => c.toUpperCase());
185
- }
186
-
187
- /** One sentence: where the invitation stands and what, if anything, to do. */
188
- function stateLine(item: TReceivedServiceProviderInvitation) {
189
- switch (item.status) {
190
- case "pending":
191
- return `Waiting for you. Accepting adds ${item.siteName} to this company's Sites list${
192
- item.expireAt ? ` — please answer by ${day(item.expireAt)}` : ""
193
- }.`;
194
- case "complete":
195
- if (item.onThisOrg) {
196
- return `Accepted${on(item.answeredAt)} — ${item.siteName} is on your Sites list as a site you serve for ${item.orgName}.`;
197
- }
198
- if (item.needsSiteLink) {
199
- return "This invitation was accepted, but the site wasn't added to your Sites. Add it now.";
200
- }
201
- if (!item.providerOrgId || item.providerOrgId === props.org) {
202
- return `Accepted${on(item.answeredAt)}, but ${item.siteName} is not on this company's Sites list. Please contact Seven365 support so we can add it.`;
203
- }
204
- return `Accepted${on(item.answeredAt)} for ${
205
- item.providerOrgName || "another of your companies"
206
- } — the site is on that company's Sites list, not this one's.`;
207
- case "declined":
208
- return `Declined${on(item.answeredAt)}. ${item.orgName} has been told.`;
209
- case "cancelled":
210
- return `${item.orgName} withdrew this invitation${on(item.answeredAt)}.`;
211
- default:
212
- return `This invitation expired${on(item.expireAt)} without an answer. Ask ${item.orgName} to send a new one if you still want this site.`;
213
- }
214
- }
215
-
216
- async function load() {
217
- loading.value = true;
218
- loadError.value = "";
219
- try {
220
- const res = await getReceivedInvitations({ org: props.org, status: tab.value });
221
- items.value = Array.isArray(res?.items) ? res.items : [];
222
- counts.value = res?.counts ?? {};
223
- } catch (err: any) {
224
- items.value = [];
225
- loadError.value = err?.data?.message || "Could not load your client invitations. Please try again.";
226
- } finally {
227
- loading.value = false;
228
- }
229
- }
230
-
231
- async function answer(item: TReceivedServiceProviderInvitation, action: "accept" | "decline") {
232
- errors[item._id] = "";
233
- working[item._id] = action;
234
- try {
235
- if (action === "accept") await acceptInvitation(item._id, props.org);
236
- else await declineInvitation(item._id);
237
- // Follow the invitation to the tab it now belongs to (the tab watcher
238
- // reloads the list).
239
- tab.value = action === "accept" ? "accepted" : "declined";
240
- } catch (err: any) {
241
- errors[item._id] =
242
- err?.data?.message ||
243
- (action === "accept" ? "Could not accept this invitation." : "Could not decline this invitation.");
244
- } finally {
245
- working[item._id] = "";
246
- }
247
- }
248
-
249
- /** Accepted but never added to the Sites list: add it now, then show it. */
250
- async function addToSites(item: TReceivedServiceProviderInvitation) {
251
- errors[item._id] = "";
252
- working[item._id] = "link";
253
- try {
254
- await linkInvitationSite(item._id, props.org);
255
- await load();
256
- } catch (err: any) {
257
- errors[item._id] = err?.data?.message || "Could not add this site to your Sites.";
258
- } finally {
259
- working[item._id] = "";
260
- }
261
- }
262
-
263
- // Opened from the bell: start on the tab the linked invitation is in.
264
- if (linked.value) {
265
- try {
266
- const one: any = await getInvitation(linked.value, props.org);
267
- if (one?.tab && tabs.some((t) => t.value === one.tab)) tab.value = one.tab;
268
- } catch {
269
- // Not ours or gone: the page still lists the rest.
270
- }
271
- }
272
- await load();
273
- watch(tab, load);
274
- </script>
275
-
276
- <style scoped>
277
- .client-invite--linked {
278
- outline: 2px solid rgb(var(--v-theme-primary));
279
- outline-offset: 2px;
280
- }
281
-
282
- .client-invite :deep(.screen-card-title) {
283
- display: block;
284
- line-height: 1.35;
285
- }
286
-
287
- .client-invite__meta,
288
- .client-invite__body {
289
- font-size: var(--fs-cell);
290
- }
291
- </style>