@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/components/AccessCardQrTagging.vue +0 -4
  3. package/components/AccessManagement.vue +0 -4
  4. package/components/BuildingManagement/buildings.vue +0 -4
  5. package/components/BulletinBoardManagement.vue +0 -3
  6. package/components/CameraForm.vue +2 -5
  7. package/components/CameraMain.vue +3 -6
  8. package/components/ClientDetailForm.vue +65 -5
  9. package/components/ClientMain.vue +201 -59
  10. package/components/DashboardMain.vue +27 -8
  11. package/components/DocumentManagement.vue +0 -4
  12. package/components/HidAccessLogDashboard.vue +114 -34
  13. package/components/HidReaderManagement.vue +34 -11
  14. package/components/HidReaderUserRoster.vue +376 -0
  15. package/components/HidUserEnrollment.vue +6 -0
  16. package/components/HidUserMapping.vue +583 -0
  17. package/components/IncidentReport/Authorities.vue +0 -4
  18. package/components/IncidentReport/IncidentInformation.vue +0 -4
  19. package/components/IncidentReport/IncidentInformationDownload.vue +0 -4
  20. package/components/IncidentReport/affectedEntities.vue +0 -4
  21. package/components/MemberMain.vue +20 -2
  22. package/components/RolePermissionFormCreate.vue +6 -1
  23. package/components/RolePermissionFormPreviewUpdate.vue +22 -11
  24. package/components/RolePermissionMain.vue +39 -4
  25. package/components/VehicleManagement.vue +0 -4
  26. package/components/VisitorManagement.vue +0 -3
  27. package/components/VisitorsReportPreview.vue +0 -3
  28. package/composables/useBulletinBoardPermission.ts +6 -1
  29. package/composables/useConsoleTier.ts +73 -0
  30. package/composables/useHidAmico.ts +18 -1
  31. package/composables/useHidNavigation.ts +8 -1
  32. package/composables/useMember.ts +0 -5
  33. package/composables/useSettingsPermission.ts +7 -1
  34. package/package.json +2 -2
  35. package/pages/[org]/[site]/access-mgmt/hid-user-mapping/index.vue +23 -0
  36. package/pages/[org]/[site]/access-mgmt/hid-users/index.vue +1 -1
  37. package/utils/client-subscription.test.ts +95 -0
  38. package/utils/console-tier.test.ts +87 -0
  39. package/utils/console-tier.ts +67 -0
  40. package/utils/data.test.ts +84 -0
  41. 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
@@ -180,10 +180,6 @@
180
180
  <script setup lang="ts">
181
181
  import QrcodeVue from "qrcode.vue";
182
182
 
183
- definePageMeta({
184
- middleware: ["01-auth", "02-org"],
185
- memberOnly: true,
186
- });
187
183
 
188
184
  const props = defineProps({
189
185
  canViewAllQrTagging: {
@@ -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
- console.log(
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
- const text = prop.type === "ip" ? "CCTV camera already exist" : msg;
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">Submit</button>
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
- <ul class="app-list">
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(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
- await Promise.all([loadOrganization(), loadSites()])
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,