@7365admin1/layer-common 4.0.1 → 4.0.3-staging.220
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 +101 -0
- package/components/AccessCardQrTagging.vue +0 -4
- package/components/AccessManagement.vue +0 -4
- package/components/BuildingManagement/buildings.vue +0 -4
- package/components/BulletinBoardManagement.vue +0 -3
- package/components/CameraForm.vue +2 -5
- package/components/CameraMain.vue +3 -6
- package/components/ClientDetailForm.vue +65 -5
- package/components/ClientMain.vue +201 -59
- package/components/DashboardMain.vue +27 -8
- package/components/DocumentManagement.vue +0 -4
- package/components/HidAccessLogDashboard.vue +114 -34
- package/components/HidReaderManagement.vue +34 -11
- package/components/HidReaderUserRoster.vue +376 -0
- package/components/HidUserEnrollment.vue +6 -0
- package/components/HidUserMapping.vue +583 -0
- package/components/IncidentReport/Authorities.vue +0 -4
- package/components/IncidentReport/IncidentInformation.vue +0 -4
- package/components/IncidentReport/IncidentInformationDownload.vue +0 -4
- package/components/IncidentReport/affectedEntities.vue +0 -4
- package/components/MemberMain.vue +20 -2
- package/components/RolePermissionFormCreate.vue +6 -1
- package/components/RolePermissionFormPreviewUpdate.vue +22 -11
- package/components/RolePermissionMain.vue +39 -4
- package/components/VehicleManagement.vue +0 -4
- package/components/VisitorManagement.vue +0 -3
- package/components/VisitorsReportPreview.vue +0 -3
- package/composables/useBulletinBoardPermission.ts +6 -1
- package/composables/useConsoleTier.ts +73 -0
- package/composables/useHidAmico.ts +18 -1
- package/composables/useHidNavigation.ts +8 -1
- package/composables/useMember.ts +0 -5
- package/composables/useSettingsPermission.ts +7 -1
- package/package.json +2 -2
- package/pages/[org]/[site]/access-mgmt/hid-user-mapping/index.vue +23 -0
- package/pages/[org]/[site]/access-mgmt/hid-users/index.vue +1 -1
- package/utils/client-subscription.test.ts +95 -0
- package/utils/console-tier.test.ts +87 -0
- package/utils/console-tier.ts +67 -0
- package/utils/data.test.ts +84 -0
- package/utils/data.ts +92 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,106 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ce3ece5: Suspend and reactivate a client from the staff console, owner-only, and fix
|
|
8
|
+
four controls on the console screens that failed without saying so.
|
|
9
|
+
|
|
10
|
+
## Suspend / reactivate (owner decision 4 and 9)
|
|
11
|
+
|
|
12
|
+
`ClientMain.vue` already called `PATCH /api/organizations/:id/status`. The
|
|
13
|
+
backend for it merged as `iservice365-core` #1884 and is now owner-gated
|
|
14
|
+
(`requirePlatformOwner`), so the screen has to know which tier it is drawing
|
|
15
|
+
for - and it had no way to tell the owner from ordinary Seven365 staff.
|
|
16
|
+
|
|
17
|
+
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
18
|
+
own rule from two endpoints the console already calls, unprojected:
|
|
19
|
+
|
|
20
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
21
|
+
GET /api/roles/id/:role that membership's role document
|
|
22
|
+
|
|
23
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
24
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
25
|
+
|
|
26
|
+
`role.default` is the marker because it is the only property of a platform
|
|
27
|
+
staff role no API caller can set - `role.controller.ts` validates create and
|
|
28
|
+
update with Joi object schemas that do not list it, `role.repo.ts` never writes
|
|
29
|
+
it, `MRole` defaults it to `false`, and only `user.service.ts
|
|
30
|
+
createDefaultUser()` sets it at API boot. A role NAME proves nothing: staging
|
|
31
|
+
carries an ordinary organisation role merely named "Super Admin".
|
|
32
|
+
|
|
33
|
+
**This decides what is DRAWN, and nothing else.** The server re-decides on
|
|
34
|
+
every call from the session id, so a browser that flips the flag gets a visible
|
|
35
|
+
button and a 401. Anything the front end cannot positively prove is `none` -
|
|
36
|
+
it fails closed.
|
|
37
|
+
|
|
38
|
+
The suspend confirmation now says what actually happens in the owner's terms:
|
|
39
|
+
everyone who belongs to the client is blocked from signing in - staff,
|
|
40
|
+
residents and guards - nothing is deleted, and it is reversible. Reactivating
|
|
41
|
+
confirms without a warning. After either, the list re-reads and the row moves
|
|
42
|
+
to the tab matching its new status; the state shown comes from the existing
|
|
43
|
+
`utils/client-subscription.ts` derivation, not a second copy of the rule.
|
|
44
|
+
|
|
45
|
+
## Four controls that failed silently
|
|
46
|
+
|
|
47
|
+
- **`ClientMain.vue` - the row menu had no background.** It teleports to
|
|
48
|
+
`<body>`, which is outside the element Vuetify puts `v-theme--light` /
|
|
49
|
+
`v-theme--dark` on, and every token in `tokens.css` is declared under
|
|
50
|
+
`[class*="v-theme--"]`. `background: var(--card)` therefore resolved to
|
|
51
|
+
nothing: measured `rgba(0, 0, 0, 0)` in both themes, with the table's own
|
|
52
|
+
text showing through the menu items. Its border, ink and hover were dead for
|
|
53
|
+
the same reason. The menu now names its theme on itself.
|
|
54
|
+
- **`ClientMain.vue` - a refused suspend said nothing.** Both handlers ended in
|
|
55
|
+
`console.error`, so pressing Suspend looked exactly like pressing nothing -
|
|
56
|
+
which is what an ordinary staff member now gets from the owner-gated
|
|
57
|
+
endpoint. The refusal is shown on the dialog, which stays open.
|
|
58
|
+
- **`ClientDetailForm.vue` - a failed save and a failed load both drew as
|
|
59
|
+
normal.** `handleSubmit` swallowed the error into `console.error`; the two
|
|
60
|
+
loaders had no `catch` at all, so a failed request rejected inside the
|
|
61
|
+
watcher and the drawer drew every field blank and an empty site table - an
|
|
62
|
+
error shown as "this client has nothing in it". Both now say so. An
|
|
63
|
+
organisation with no service application says that too, instead of an empty
|
|
64
|
+
list.
|
|
65
|
+
- **`MemberMain.vue` - two error paths.** A refused role change set the
|
|
66
|
+
snackbar's text but never opened it, so it said nothing at all; a failed
|
|
67
|
+
status change with no `message` on the error drew an empty red snackbar.
|
|
68
|
+
|
|
69
|
+
No endpoint, request shape or response shape changed, and nothing new is called
|
|
70
|
+
that did not already exist.
|
|
71
|
+
|
|
72
|
+
- 6d654d6: Remove a duplicate `:site` attribute on `HidReaderManagement.vue`.
|
|
73
|
+
|
|
74
|
+
`HidReaderForm` was given `:site` twice - `props.site` and then a bare `site`, which is
|
|
75
|
+
not a name that exists in that component's scope. Vite's Vue plugin treats a duplicate
|
|
76
|
+
attribute as a compile error, so every consuming application's `nuxt build` failed on
|
|
77
|
+
this file. The layer's own playground build does not import the component, which is why
|
|
78
|
+
it was not caught at publish time.
|
|
79
|
+
|
|
80
|
+
- c4683d1: Promo codes can be edited, turned off and removed from the staff console.
|
|
81
|
+
|
|
82
|
+
`usePromoCode()` gains `getById`, `update`, `updateStatus` and `remove`, matching the
|
|
83
|
+
`PUT /api/promo-codes/:id`, `PATCH /api/promo-codes/:id/status` and
|
|
84
|
+
`DELETE /api/promo-codes/:id` endpoints. `update` never sends `code`, which the server
|
|
85
|
+
treats as immutable because subscriptions and invoices quote a code by its text.
|
|
86
|
+
|
|
87
|
+
`utils/promo-code-form.ts` holds the editor's rules - loading a record into the form,
|
|
88
|
+
building the request body, and refusing what the API refuses, in wording a person can
|
|
89
|
+
act on.
|
|
90
|
+
|
|
91
|
+
- 780112d: Stop the Roles & Permissions screen firing an id-less request on every visit, and
|
|
92
|
+
remove a dead, malformed member-lookup function.
|
|
93
|
+
|
|
94
|
+
`RolePermissionMain.vue` fetched a role by id as soon as the component mounted,
|
|
95
|
+
before any role had been picked, sending `GET /api/roles/id` with no id on the
|
|
96
|
+
end. The real fetch already runs correctly once a row is clicked (a `watchEffect`
|
|
97
|
+
handles that); the mount-time fetch was just extra noise hitting the API for
|
|
98
|
+
nothing. It now waits for a real id (`immediate: false`).
|
|
99
|
+
|
|
100
|
+
Also removes `useMember.getByMemberId`, which built the malformed path
|
|
101
|
+
`/api//user/${user}` and had no caller anywhere in this package or the 11 web
|
|
102
|
+
apps.
|
|
103
|
+
|
|
3
104
|
## 4.0.1
|
|
4
105
|
|
|
5
106
|
### Patch Changes
|
|
@@ -138,10 +138,6 @@
|
|
|
138
138
|
// into the consuming apps but not reliably into the layer's own components.
|
|
139
139
|
import { readListPage } from "../utils/list-page";
|
|
140
140
|
|
|
141
|
-
definePageMeta({
|
|
142
|
-
middleware: ["01-auth", "02-org"],
|
|
143
|
-
memberOnly: true,
|
|
144
|
-
});
|
|
145
141
|
const props = defineProps({
|
|
146
142
|
headers: {
|
|
147
143
|
type: Array as PropType<Array<Record<string, any>>>,
|
|
@@ -162,10 +162,6 @@
|
|
|
162
162
|
// into the consuming apps but not reliably into the layer's own components.
|
|
163
163
|
import { levelCount } from "../../utils/data";
|
|
164
164
|
|
|
165
|
-
definePageMeta({
|
|
166
|
-
middleware: ["01-auth", "02-org"],
|
|
167
|
-
memberOnly: true,
|
|
168
|
-
});
|
|
169
165
|
|
|
170
166
|
const props = defineProps({
|
|
171
167
|
headers: {
|
|
@@ -89,9 +89,6 @@ import { APP_CONSTANTS } from '../constants/app';
|
|
|
89
89
|
// is a ReferenceError that blanks the screen in every consuming app.
|
|
90
90
|
import { readListPage } from '../utils/list-page';
|
|
91
91
|
|
|
92
|
-
definePageMeta({
|
|
93
|
-
memberOnly: true,
|
|
94
|
-
})
|
|
95
92
|
|
|
96
93
|
const props = defineProps({
|
|
97
94
|
siteId: {
|
|
@@ -130,6 +130,7 @@
|
|
|
130
130
|
|
|
131
131
|
<script setup lang="ts">
|
|
132
132
|
import useSiteSettings from "../composables/useSiteSettings";
|
|
133
|
+
import { cameraErrorConverter } from "../utils/data";
|
|
133
134
|
|
|
134
135
|
const prop = defineProps({
|
|
135
136
|
title: {
|
|
@@ -298,11 +299,7 @@ async function submit() {
|
|
|
298
299
|
|
|
299
300
|
emit("success", `${prop.title} added successfully!`);
|
|
300
301
|
} catch (error: any) {
|
|
301
|
-
|
|
302
|
-
"error",
|
|
303
|
-
error.response?._data?.message || "Failed to add camera"
|
|
304
|
-
);
|
|
305
|
-
emit("error", error.response?._data?.message || "Failed to add camera");
|
|
302
|
+
emit("error", cameraErrorConverter(error, prop.type));
|
|
306
303
|
} finally {
|
|
307
304
|
disable.value = false;
|
|
308
305
|
}
|
|
@@ -248,6 +248,7 @@
|
|
|
248
248
|
<script setup lang="ts">
|
|
249
249
|
import type { PropType } from "vue";
|
|
250
250
|
import useSiteSettings from "../composables/useSiteSettings";
|
|
251
|
+
import { cameraErrorConverter } from "../utils/data";
|
|
251
252
|
|
|
252
253
|
const events = defineEmits(["update:value", "row-click"]);
|
|
253
254
|
|
|
@@ -363,8 +364,7 @@ function openDialogDelete() {
|
|
|
363
364
|
}
|
|
364
365
|
|
|
365
366
|
function handleError(msg: string) {
|
|
366
|
-
|
|
367
|
-
showMessage(text, "error");
|
|
367
|
+
showMessage(msg, "error");
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
function handleSuccess() {
|
|
@@ -416,10 +416,7 @@ async function submitDelete() {
|
|
|
416
416
|
await getCameraRefresh();
|
|
417
417
|
dialogDelete.value = false;
|
|
418
418
|
} catch (error: any) {
|
|
419
|
-
showMessage(
|
|
420
|
-
error?.response?._data?.message || "Failed to delete camera.",
|
|
421
|
-
"error"
|
|
422
|
-
);
|
|
419
|
+
showMessage(cameraErrorConverter(error, prop.type), "error");
|
|
423
420
|
}
|
|
424
421
|
}
|
|
425
422
|
</script>
|
|
@@ -9,10 +9,15 @@
|
|
|
9
9
|
|
|
10
10
|
<div class="actions">
|
|
11
11
|
<button class="btn-cancel" @click="$emit('cancel')">Cancel</button>
|
|
12
|
-
<button class="btn-submit" @click="handleSubmit">
|
|
12
|
+
<button class="btn-submit" :disabled="saving" @click="handleSubmit">
|
|
13
|
+
{{ saving ? 'Saving...' : 'Submit' }}
|
|
14
|
+
</button>
|
|
13
15
|
</div>
|
|
14
16
|
</div>
|
|
15
17
|
|
|
18
|
+
<p v-if="loadError" class="form-error" role="alert">{{ loadError }}</p>
|
|
19
|
+
<p v-else-if="formError" class="form-error" role="alert">{{ formError }}</p>
|
|
20
|
+
|
|
16
21
|
<!-- ===== CLIENT INFORMATION ===== -->
|
|
17
22
|
<section class="section">
|
|
18
23
|
<h3>Client Information</h3>
|
|
@@ -133,7 +138,12 @@
|
|
|
133
138
|
<!-- ===== INCLUDED APPLICATION ===== -->
|
|
134
139
|
<section class="section">
|
|
135
140
|
<h3>Included Application</h3>
|
|
136
|
-
|
|
141
|
+
<!-- A plain organisation has no service application of its own, so the
|
|
142
|
+
list is legitimately empty. An empty <ul> reads as a broken box. -->
|
|
143
|
+
<p v-if="form.includedApps.length === 0" class="app-none">
|
|
144
|
+
No service application recorded for this client.
|
|
145
|
+
</p>
|
|
146
|
+
<ul v-else class="app-list">
|
|
137
147
|
<li v-for="(app, idx) in form.includedApps" :key="idx">{{ app }}</li>
|
|
138
148
|
</ul>
|
|
139
149
|
</section>
|
|
@@ -266,6 +276,10 @@ const errors = reactive({
|
|
|
266
276
|
const phonePrefix = ref('+65')
|
|
267
277
|
const phoneNumber = ref('')
|
|
268
278
|
|
|
279
|
+
const saving = ref(false)
|
|
280
|
+
const loadError = ref('')
|
|
281
|
+
const formError = ref('')
|
|
282
|
+
|
|
269
283
|
/* ── FORM STATE ── */
|
|
270
284
|
const form = reactive({
|
|
271
285
|
name: '',
|
|
@@ -348,6 +362,9 @@ async function handleSubmit() {
|
|
|
348
362
|
|
|
349
363
|
form.phone = `${phonePrefix.value} ${phoneNumber.value}`
|
|
350
364
|
|
|
365
|
+
saving.value = true
|
|
366
|
+
formError.value = ''
|
|
367
|
+
|
|
351
368
|
try {
|
|
352
369
|
if (userId.value) {
|
|
353
370
|
await userApi.updateUserFieldById(userId.value, 'name', form.name)
|
|
@@ -359,8 +376,16 @@ async function handleSubmit() {
|
|
|
359
376
|
})
|
|
360
377
|
|
|
361
378
|
emit('submit', { ...form })
|
|
362
|
-
} catch (error) {
|
|
363
|
-
console.error
|
|
379
|
+
} catch (error: any) {
|
|
380
|
+
// A failed save used to reach `console.error` and nothing else, so Submit
|
|
381
|
+
// looked exactly like a button that did nothing - the screen stayed put
|
|
382
|
+
// with the typed values still in it, which reads as "not saved yet".
|
|
383
|
+
formError.value =
|
|
384
|
+
error?.response?._data?.message ??
|
|
385
|
+
error?.data?.message ??
|
|
386
|
+
'Could not save this client. Nothing was changed.'
|
|
387
|
+
} finally {
|
|
388
|
+
saving.value = false
|
|
364
389
|
}
|
|
365
390
|
}
|
|
366
391
|
const userId = ref('')
|
|
@@ -419,7 +444,19 @@ watch(
|
|
|
419
444
|
async client => {
|
|
420
445
|
if (!client?._id) return
|
|
421
446
|
|
|
422
|
-
|
|
447
|
+
loadError.value = ''
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
await Promise.all([loadOrganization(), loadSites()])
|
|
451
|
+
} catch (error: any) {
|
|
452
|
+
// Neither loader caught anything, so a failed request rejected inside
|
|
453
|
+
// this watcher and the drawer drew every field blank and an empty site
|
|
454
|
+
// table - an error shown as "this client has nothing in it".
|
|
455
|
+
loadError.value =
|
|
456
|
+
error?.response?._data?.message ??
|
|
457
|
+
error?.data?.message ??
|
|
458
|
+
'Could not load this client. Nothing here has been saved or changed.'
|
|
459
|
+
}
|
|
423
460
|
},
|
|
424
461
|
{
|
|
425
462
|
immediate: true
|
|
@@ -654,6 +691,29 @@ watch(
|
|
|
654
691
|
color: var(--err);
|
|
655
692
|
}
|
|
656
693
|
|
|
694
|
+
/* A failed load or a failed save, said once at the top of the form where the
|
|
695
|
+
reader already is. `--err-bg` fill + `--err` ink, the design's error pair. */
|
|
696
|
+
.form-error {
|
|
697
|
+
margin: 0 28px 18px;
|
|
698
|
+
padding: 10px 12px;
|
|
699
|
+
border-radius: 8px;
|
|
700
|
+
background: var(--err-bg);
|
|
701
|
+
color: var(--err);
|
|
702
|
+
font-size: 13px;
|
|
703
|
+
font-weight: 500;
|
|
704
|
+
line-height: 1.45;
|
|
705
|
+
}
|
|
706
|
+
.btn-submit:disabled {
|
|
707
|
+
opacity: 0.7;
|
|
708
|
+
cursor: not-allowed;
|
|
709
|
+
}
|
|
710
|
+
/* `--text2` is the reading token for secondary copy; `--muted` measures below
|
|
711
|
+
AA at this size. */
|
|
712
|
+
.app-none {
|
|
713
|
+
font-size: 13.5px;
|
|
714
|
+
color: var(--text2);
|
|
715
|
+
}
|
|
716
|
+
|
|
657
717
|
/*
|
|
658
718
|
* MEASURED at 360: the form overflowed the viewport by 147px in both themes.
|
|
659
719
|
* Its two-column grid, its 28px side padding and its header row are all fixed,
|