@7365admin1/layer-common 4.97.1-staging.500 → 4.97.1-staging.501
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.
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
<template v-else>
|
|
27
27
|
<div class="hid-access-permissions__intro">
|
|
28
28
|
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
29
|
-
|
|
30
|
-
somebody off to close the door to them without removing
|
|
31
|
-
or PIN.
|
|
29
|
+
Everybody enrolled on this reader, by type. Enrolling already grants
|
|
30
|
+
access — switch somebody off to close the door to them without removing
|
|
31
|
+
their face, card or PIN.
|
|
32
32
|
</p>
|
|
33
33
|
</div>
|
|
34
34
|
|
|
@@ -45,12 +45,36 @@
|
|
|
45
45
|
@refresh="loadEnrolled"
|
|
46
46
|
@update:page="goToPage"
|
|
47
47
|
>
|
|
48
|
+
<!-- WHICH OF THE READER'S PEOPLE TO SHOW.
|
|
49
|
+
The reader holds residents, members and contractors; each tab is
|
|
50
|
+
one `type`, filtered and paged by the server. The strip is
|
|
51
|
+
`TableMain`'s own slot and the shared `.app-tab` markup, the same
|
|
52
|
+
as every other tabbed table here - not a `v-tabs`, whose selected
|
|
53
|
+
state is drawn from Vuetify's `on-surface` rather than the
|
|
54
|
+
design's accent underline.
|
|
55
|
+
The count beside each label is the whole reader's, from the
|
|
56
|
+
draft, so it follows an unsaved tick. -->
|
|
57
|
+
<template #tabs>
|
|
58
|
+
<button
|
|
59
|
+
v-for="tab in TABS"
|
|
60
|
+
:key="tab.value"
|
|
61
|
+
type="button"
|
|
62
|
+
class="app-tab"
|
|
63
|
+
:class="{ 'app-tab--active': activeTab === tab.value }"
|
|
64
|
+
:aria-pressed="activeTab === tab.value"
|
|
65
|
+
@click="selectTab(tab.value)"
|
|
66
|
+
>
|
|
67
|
+
{{ tab.label }}
|
|
68
|
+
<span class="hid-access-permissions__tab-count">{{ tabCount(tab) }}</span>
|
|
69
|
+
</button>
|
|
70
|
+
</template>
|
|
71
|
+
|
|
48
72
|
<template #extension>
|
|
49
73
|
<div class="app-filter-row">
|
|
50
74
|
<AppField
|
|
51
75
|
v-model="search"
|
|
52
76
|
search
|
|
53
|
-
placeholder="
|
|
77
|
+
:placeholder="searchPlaceholder"
|
|
54
78
|
@keyup.enter="searchFromFirstPage"
|
|
55
79
|
/>
|
|
56
80
|
</div>
|
|
@@ -121,6 +145,7 @@ import {
|
|
|
121
145
|
readCandidatePage,
|
|
122
146
|
seedAssignments,
|
|
123
147
|
setAssignment,
|
|
148
|
+
subjectOfIdentity,
|
|
124
149
|
toAssignmentPayload,
|
|
125
150
|
type HidAssignmentState,
|
|
126
151
|
} from "../utils/hid-permission-assignments";
|
|
@@ -156,11 +181,52 @@ const emit = defineEmits<{
|
|
|
156
181
|
|
|
157
182
|
const { getSitePermissions, getIdentities, updateSitePermissions } = useHidAmico();
|
|
158
183
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
184
|
+
/**
|
|
185
|
+
* WHO EACH TAB LISTS, AND WHAT A GRANT MADE FROM IT IS FILED UNDER.
|
|
186
|
+
*
|
|
187
|
+
* `type` is the identity's own, filtered by the SERVER (`listIdentities` does an
|
|
188
|
+
* exact match on it), and it is what the enrolment writes and what
|
|
189
|
+
* `permissionIdentityType` re-confirms on every reconcile. `category` is the
|
|
190
|
+
* permission category a NEW grant from this tab belongs to.
|
|
191
|
+
*
|
|
192
|
+
* The two are a pair rather than one derived from the other, because the tab is
|
|
193
|
+
* the only thing on this screen that knows which of them the operator means: a
|
|
194
|
+
* contractor and a property-management member are both a `member` link, and
|
|
195
|
+
* guessing between them from the record is exactly what went wrong before.
|
|
196
|
+
*/
|
|
197
|
+
const TABS = [
|
|
198
|
+
{ value: "resident", label: "Resident", noun: "Resident", type: "resident", category: "resident" },
|
|
199
|
+
{ value: "member", label: "Member", noun: "Member", type: "staff", category: "property_management" },
|
|
200
|
+
{
|
|
201
|
+
value: "provider",
|
|
202
|
+
label: "Service provider",
|
|
203
|
+
noun: "Person",
|
|
204
|
+
type: "contractor",
|
|
205
|
+
category: "service_provider_member",
|
|
206
|
+
},
|
|
207
|
+
] as const satisfies ReadonlyArray<{
|
|
208
|
+
value: string;
|
|
209
|
+
label: string;
|
|
210
|
+
noun: string;
|
|
211
|
+
type: string;
|
|
212
|
+
category: THidPermissionCategory;
|
|
213
|
+
}>;
|
|
214
|
+
|
|
215
|
+
const activeTab = ref<(typeof TABS)[number]["value"]>("resident");
|
|
216
|
+
const currentTab = computed(
|
|
217
|
+
() => TABS.find((tab) => tab.value === activeTab.value) ?? TABS[0],
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
/** Granted counts per category, for the tab strip. One call, all three. */
|
|
221
|
+
const counts = ref<Record<string, number>>({});
|
|
222
|
+
const tabCount = (tab: (typeof TABS)[number]) => counts.value[tab.category] ?? 0;
|
|
223
|
+
|
|
224
|
+
/* Name over unit in one cell rather than a "Unit" column — see `subtitleOf`.
|
|
225
|
+
The first column follows the tab; "Door access" is the same action for all. */
|
|
226
|
+
const headers = computed(() => [
|
|
227
|
+
{ title: currentTab.value.noun, value: "name", sortable: false },
|
|
162
228
|
{ title: "Door access", value: "selected", sortable: false, width: 190 },
|
|
163
|
-
];
|
|
229
|
+
]);
|
|
164
230
|
|
|
165
231
|
const search = ref("");
|
|
166
232
|
const page = ref(1);
|
|
@@ -187,6 +253,13 @@ const readerLabel = computed(() => {
|
|
|
187
253
|
return readerLocation.value ? `${name} · ${readerLocation.value}` : name;
|
|
188
254
|
});
|
|
189
255
|
|
|
256
|
+
/* "unit" only means something for a resident. */
|
|
257
|
+
const searchPlaceholder = computed(() =>
|
|
258
|
+
activeTab.value === "resident"
|
|
259
|
+
? "Search name, unit or registration"
|
|
260
|
+
: "Search name or registration",
|
|
261
|
+
);
|
|
262
|
+
|
|
190
263
|
const assignedTotal = computed(() => draft.value.size);
|
|
191
264
|
const dirty = computed(() => hasChanges(savedState.value, draft.value));
|
|
192
265
|
/* The draft is the truth once the dialog is open, so `selected` is recomputed
|
|
@@ -199,6 +272,7 @@ watch(
|
|
|
199
272
|
if (!isOpen || !readerId.value) return;
|
|
200
273
|
search.value = "";
|
|
201
274
|
page.value = 1;
|
|
275
|
+
activeTab.value = "resident";
|
|
202
276
|
void loadAll();
|
|
203
277
|
},
|
|
204
278
|
{ immediate: true },
|
|
@@ -221,6 +295,13 @@ async function loadAssignments() {
|
|
|
221
295
|
const assignments = response?.data?.assignments ?? [];
|
|
222
296
|
savedState.value = seedAssignments(assignments);
|
|
223
297
|
draft.value = seedAssignments(assignments);
|
|
298
|
+
/*
|
|
299
|
+
* Counted from the assignments themselves rather than the server's `counts`
|
|
300
|
+
* block, so the strip follows the DRAFT and a tick updates the number it
|
|
301
|
+
* sits beside. `countByCategory` is the same helper the candidate screens
|
|
302
|
+
* use.
|
|
303
|
+
*/
|
|
304
|
+
counts.value = countByCategory(draft.value);
|
|
224
305
|
} catch (error) {
|
|
225
306
|
showToast(getErrorMessage(error, "Unable to load this reader's permissions."), "error");
|
|
226
307
|
}
|
|
@@ -230,27 +311,6 @@ function toRecord(value: unknown): Record<string, unknown> {
|
|
|
230
311
|
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
|
231
312
|
}
|
|
232
313
|
|
|
233
|
-
/**
|
|
234
|
-
* The subject an identity names, in the order `createIdentity` validates them.
|
|
235
|
-
*
|
|
236
|
-
* A visitor returns null and is left out of the list: visitor access comes from
|
|
237
|
-
* `user_access_rules`, not from a group, so there is nothing here to switch
|
|
238
|
-
* off. An identity linked to nobody — a device user the reader holds that was
|
|
239
|
-
* never tied to a person — is dropped for the same reason.
|
|
240
|
-
*/
|
|
241
|
-
function subjectOf(identity: Record<string, unknown>) {
|
|
242
|
-
if (identity.person) {
|
|
243
|
-
return { subjectId: String(identity.person), category: "resident" as const };
|
|
244
|
-
}
|
|
245
|
-
if (identity.member) {
|
|
246
|
-
return { subjectId: String(identity.member), category: "property_management" as const };
|
|
247
|
-
}
|
|
248
|
-
if (identity.serviceProvider) {
|
|
249
|
-
return { subjectId: String(identity.serviceProvider), category: "service_provider" as const };
|
|
250
|
-
}
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
314
|
/**
|
|
255
315
|
* Why a row cannot be switched, in words the reader of the screen can act on.
|
|
256
316
|
*
|
|
@@ -294,10 +354,10 @@ async function loadEnrolled() {
|
|
|
294
354
|
page: page.value,
|
|
295
355
|
limit: 10,
|
|
296
356
|
search: search.value.trim(),
|
|
297
|
-
//
|
|
298
|
-
// count comes back from the same query, so narrowing the list on
|
|
299
|
-
// would print a total that never matched the rows under it.
|
|
300
|
-
type:
|
|
357
|
+
// THIS TAB's people, and filtered by the SERVER rather than here. The
|
|
358
|
+
// pager's count comes back from the same query, so narrowing the list on
|
|
359
|
+
// this side would print a total that never matched the rows under it.
|
|
360
|
+
type: currentTab.value.type,
|
|
301
361
|
});
|
|
302
362
|
const result = readCandidatePage(response);
|
|
303
363
|
/*
|
|
@@ -309,11 +369,13 @@ async function loadEnrolled() {
|
|
|
309
369
|
*/
|
|
310
370
|
enrolled.value = result.items.map((row) => {
|
|
311
371
|
const identity = toRecord(row);
|
|
312
|
-
const subject =
|
|
372
|
+
const subject = subjectOfIdentity(identity);
|
|
313
373
|
const metadata = toRecord(identity.metadata);
|
|
314
374
|
return {
|
|
315
375
|
subjectId: subject?.subjectId ?? "",
|
|
316
|
-
|
|
376
|
+
// The record's own category where it has one; otherwise the tab's,
|
|
377
|
+
// which is the only thing that knows what the operator means.
|
|
378
|
+
category: subject?.category ?? currentTab.value.category,
|
|
317
379
|
// The row's own key. `subjectId` is empty for the rows below, and two
|
|
318
380
|
// empty keys would collapse into one row in the table.
|
|
319
381
|
rowKey: String(identity._id ?? identity.hidUserId ?? Math.random()),
|
|
@@ -342,6 +404,24 @@ async function loadEnrolled() {
|
|
|
342
404
|
}
|
|
343
405
|
}
|
|
344
406
|
|
|
407
|
+
/**
|
|
408
|
+
* Switching tabs refetches, because the filter is the SERVER's.
|
|
409
|
+
*
|
|
410
|
+
* The draft is untouched: it is the whole reader's, and the save sends all of it
|
|
411
|
+
* whatever tab is in front of you. Clearing it here - or building the payload
|
|
412
|
+
* from the loaded rows - is what would revoke everybody on the other two tabs.
|
|
413
|
+
*
|
|
414
|
+
* Search is cleared with the tab. It is a server-side search within one type, so
|
|
415
|
+
* carrying it across would land on an empty list and look like the tab was empty.
|
|
416
|
+
*/
|
|
417
|
+
function selectTab(value: (typeof TABS)[number]["value"]) {
|
|
418
|
+
if (activeTab.value === value) return;
|
|
419
|
+
activeTab.value = value;
|
|
420
|
+
search.value = "";
|
|
421
|
+
page.value = 1;
|
|
422
|
+
void loadEnrolled();
|
|
423
|
+
}
|
|
424
|
+
|
|
345
425
|
function goToPage(value: number) {
|
|
346
426
|
page.value = Number(value) || 1;
|
|
347
427
|
void loadEnrolled();
|
|
@@ -352,15 +432,34 @@ function searchFromFirstPage() {
|
|
|
352
432
|
void loadEnrolled();
|
|
353
433
|
}
|
|
354
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Grant or revoke one person.
|
|
437
|
+
*
|
|
438
|
+
* The category comes from the row where the server gave it one, and otherwise
|
|
439
|
+
* from the TAB - never from re-reading the identity's links on this side. A
|
|
440
|
+
* contractor and a property-management member are both a `member` link, and the
|
|
441
|
+
* stale local copy of that reading is what used to file contractors as staff: the
|
|
442
|
+
* reconcile then derived `staff` from that category and rewrote the person's
|
|
443
|
+
* type, moving them into the Member tab.
|
|
444
|
+
*
|
|
445
|
+
* An ALREADY GRANTED person keeps the category their assignment holds, so merely
|
|
446
|
+
* visiting a tab and toggling somebody off and on cannot re-file them.
|
|
447
|
+
*/
|
|
355
448
|
function toggle(item: THidPermissionCandidate, selected: boolean) {
|
|
356
|
-
|
|
449
|
+
const subjectId = String(item.subjectId ?? "");
|
|
450
|
+
if (!subjectId) return;
|
|
451
|
+
const category = savedState.value.get(subjectId)?.category
|
|
452
|
+
?? item.category
|
|
453
|
+
?? currentTab.value.category;
|
|
357
454
|
draft.value = setAssignment(
|
|
358
455
|
draft.value,
|
|
359
456
|
savedState.value,
|
|
360
|
-
|
|
361
|
-
|
|
457
|
+
subjectId,
|
|
458
|
+
category,
|
|
362
459
|
selected,
|
|
363
460
|
);
|
|
461
|
+
// The strip sits beside the rows, so it follows the tick rather than the save.
|
|
462
|
+
counts.value = countByCategory(draft.value);
|
|
364
463
|
}
|
|
365
464
|
|
|
366
465
|
async function save() {
|
|
@@ -401,6 +500,15 @@ function getErrorMessage(error: unknown, fallback: string) {
|
|
|
401
500
|
</script>
|
|
402
501
|
|
|
403
502
|
<style scoped lang="scss">
|
|
503
|
+
/* The granted count beside a tab label. Quieter than the label and set in
|
|
504
|
+
tabular figures so the three tabs do not shift width as numbers change. */
|
|
505
|
+
.hid-access-permissions__tab-count {
|
|
506
|
+
margin-left: 6px;
|
|
507
|
+
color: var(--muted);
|
|
508
|
+
font-weight: 600;
|
|
509
|
+
font-variant-numeric: tabular-nums;
|
|
510
|
+
}
|
|
511
|
+
|
|
404
512
|
.hid-access-permissions {
|
|
405
513
|
&__title { padding-block: 16px; }
|
|
406
514
|
|
|
@@ -969,6 +969,7 @@ import {
|
|
|
969
969
|
memberCandidateOf,
|
|
970
970
|
memberHasAccount,
|
|
971
971
|
HID_ALL_SERVICES,
|
|
972
|
+
enrolmentSubjectOfCategory,
|
|
972
973
|
permissionCategoryOf,
|
|
973
974
|
providerAccountsKnown,
|
|
974
975
|
providerCandidateOf,
|
|
@@ -993,6 +994,7 @@ import {
|
|
|
993
994
|
import {
|
|
994
995
|
seedAssignments,
|
|
995
996
|
setAssignment,
|
|
997
|
+
subjectOfIdentity,
|
|
996
998
|
toAssignmentPayload,
|
|
997
999
|
type HidAssignmentState,
|
|
998
1000
|
} from "../utils/hid-permission-assignments";
|
|
@@ -2570,22 +2572,20 @@ async function openEdit(user: HidUser) {
|
|
|
2570
2572
|
form.photoPreview = "";
|
|
2571
2573
|
facialEnrolled.value = hasFacialEnrollment(user);
|
|
2572
2574
|
/*
|
|
2573
|
-
*
|
|
2574
|
-
*
|
|
2575
|
-
* service provider's own employee are stored
|
|
2576
|
-
*
|
|
2577
|
-
*
|
|
2575
|
+
* ONE reading of the links, shared with the Access Permissions screen.
|
|
2576
|
+
* `subjectOfIdentity` knows that a `member` link is how BOTH a
|
|
2577
|
+
* property-management member and a service provider's own employee are stored,
|
|
2578
|
+
* and uses the identity's `type` to tell them apart - without which a
|
|
2579
|
+
* contractor's edit would be headed "Member".
|
|
2580
|
+
*
|
|
2581
|
+
* This was the same ternary written out inline. The other copy of it went
|
|
2582
|
+
* stale, so neither is written out any more.
|
|
2578
2583
|
*/
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
? "service_provider_staff"
|
|
2585
|
-
: "property_management";
|
|
2586
|
-
form.subjectId = String(
|
|
2587
|
-
user.person || user.member || user.serviceProvider || ""
|
|
2588
|
-
);
|
|
2584
|
+
const editedSubject = subjectOfIdentity(user as Record<string, unknown>);
|
|
2585
|
+
form.subjectCategory = enrolmentSubjectOfCategory(
|
|
2586
|
+
editedSubject?.category
|
|
2587
|
+
) as TEnrolmentSubjectValue;
|
|
2588
|
+
form.subjectId = editedSubject?.subjectId ?? "";
|
|
2589
2589
|
// The resident this user already holds is not a conflict with itself. A real
|
|
2590
2590
|
// one only appears if the operator reassigns the row to somebody else.
|
|
2591
2591
|
residentConflict.value = null;
|
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.97.1-staging.
|
|
5
|
+
"version": "4.97.1-staging.501",
|
|
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.",
|
|
@@ -240,6 +240,28 @@ export function permissionCategoryOf(category: unknown): string {
|
|
|
240
240
|
return "property_management";
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* The dropdown value for a grant's category — the inverse of
|
|
245
|
+
* `permissionCategoryOf`.
|
|
246
|
+
*
|
|
247
|
+
* An edit STATES the subject type rather than offering it, because an identity's
|
|
248
|
+
* category cannot change after it is created. This is how a stored record is
|
|
249
|
+
* turned back into the value the form holds, and it is wider than the dropdown on
|
|
250
|
+
* purpose: records in the wild carry `service_provider`, which names a COMPANY
|
|
251
|
+
* and which the dropdown does not offer.
|
|
252
|
+
*
|
|
253
|
+
* Paired with `subjectOfIdentity` so the mapping lives in one place. `openEdit`
|
|
254
|
+
* used to repeat the whole links-and-type ternary inline, which is how the copy
|
|
255
|
+
* in `HidAccessPermissions.vue` was able to go stale without anything noticing.
|
|
256
|
+
*/
|
|
257
|
+
export function enrolmentSubjectOfCategory(category: unknown): string {
|
|
258
|
+
const value = String(category ?? "");
|
|
259
|
+
if (value === "resident") return "resident";
|
|
260
|
+
if (value === "service_provider_member") return "service_provider_staff";
|
|
261
|
+
if (value === "service_provider") return "service_provider";
|
|
262
|
+
return "property_management";
|
|
263
|
+
}
|
|
264
|
+
|
|
243
265
|
/* ── PROVIDER STAFF AS PICKER ROWS ─────────────────────────────────────── */
|
|
244
266
|
|
|
245
267
|
/**
|
|
@@ -142,11 +142,20 @@ export function applySelection(
|
|
|
142
142
|
|
|
143
143
|
/** Per-category totals for the dialog header, counted over the whole reader. */
|
|
144
144
|
export function countByCategory(state: HidAssignmentState): Record<THidPermissionCategory, number> {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
/*
|
|
146
|
+
* Every category seeded at zero, and the increment guarded.
|
|
147
|
+
* `counts[category] += 1` on a key this object does not hold yields NaN, so a
|
|
148
|
+
* category added to the union but missed here would print "NaN" beside a tab
|
|
149
|
+
* rather than a number. That is exactly what happened when
|
|
150
|
+
* `service_provider_member` was added.
|
|
151
|
+
*/
|
|
152
|
+
const counts = HID_PERMISSION_CATEGORIES.reduce(
|
|
153
|
+
(seeded, category) => ({ ...seeded, [category]: 0 }),
|
|
154
|
+
{} as Record<THidPermissionCategory, number>,
|
|
155
|
+
);
|
|
156
|
+
for (const value of state.values()) {
|
|
157
|
+
counts[value.category] = (counts[value.category] ?? 0) + 1;
|
|
158
|
+
}
|
|
150
159
|
return counts;
|
|
151
160
|
}
|
|
152
161
|
|
|
@@ -196,3 +205,55 @@ export function readCandidatePage(response: unknown): {
|
|
|
196
205
|
pageRange,
|
|
197
206
|
};
|
|
198
207
|
}
|
|
208
|
+
|
|
209
|
+
/* ── THE SUBJECT AN IDENTITY NAMES ─────────────────────────────────────── */
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* WHICH GRANT AN ENROLLED PERSON'S ACCESS IS FILED UNDER.
|
|
213
|
+
*
|
|
214
|
+
* Mirrors `permissionSubjectOfIdentity` in core, which is the authority. This
|
|
215
|
+
* repository cannot import it - layer-common does not depend on the core
|
|
216
|
+
* package - so there is a copy, and the point of putting it HERE is that there
|
|
217
|
+
* is only one.
|
|
218
|
+
*
|
|
219
|
+
* There used to be two: a local `subjectOf` in `HidAccessPermissions.vue` and
|
|
220
|
+
* the same ternary inline in `HidUserEnrollment.vue`. The first was stale. It
|
|
221
|
+
* read every `member` link as `property_management`, so granting a contractor
|
|
222
|
+
* from the permissions screen wrote a staff assignment - and the reconcile then
|
|
223
|
+
* derived `staff` from that category and rewrote the identity's `type`, moving
|
|
224
|
+
* the person out of the service-provider tab and into the member one. The
|
|
225
|
+
* screen for managing contractors reclassified them by being used.
|
|
226
|
+
*
|
|
227
|
+
* A `member` link is how BOTH a property-management member and a service
|
|
228
|
+
* provider's own employee are stored - the person's membership at this site -
|
|
229
|
+
* so the identity's `type` is the discriminator. `"contractor"` is written by
|
|
230
|
+
* the enrolment form and re-confirmed by `permissionIdentityType` on every
|
|
231
|
+
* reconcile, so the two agree instead of one overwriting the other.
|
|
232
|
+
*
|
|
233
|
+
* Returns null for a visitor and for an identity linked to nobody. Visitor
|
|
234
|
+
* access comes from `user_access_rules` rather than a group, so there is nothing
|
|
235
|
+
* to switch; an identity holding only an account link names no subject to
|
|
236
|
+
* assign. Both are still DRAWN by the permissions screen, disabled, with a
|
|
237
|
+
* reason - dropping them silently is what once made the pager say "1-9 of 9"
|
|
238
|
+
* over seven rows.
|
|
239
|
+
*/
|
|
240
|
+
export function subjectOfIdentity(
|
|
241
|
+
identity: Record<string, unknown> | null | undefined,
|
|
242
|
+
): { subjectId: string; category: THidPermissionCategory } | null {
|
|
243
|
+
if (!identity) return null;
|
|
244
|
+
if (identity.person) {
|
|
245
|
+
return { subjectId: String(identity.person), category: "resident" };
|
|
246
|
+
}
|
|
247
|
+
if (identity.member) {
|
|
248
|
+
return {
|
|
249
|
+
subjectId: String(identity.member),
|
|
250
|
+
category: String(identity.type ?? "") === "contractor"
|
|
251
|
+
? "service_provider_member"
|
|
252
|
+
: "property_management",
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (identity.serviceProvider) {
|
|
256
|
+
return { subjectId: String(identity.serviceProvider), category: "service_provider" };
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|