@7365admin1/layer-common 4.137.0 → 4.139.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.139.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4a51215: Super Admin Client List: "Cancel Invite" now asks for confirmation first and shows an error if cancelling fails.
8
+
9
+ ## 4.138.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 57644ea: All-modules audit fixes: Pass & Key add/edit/delete now report real failures instead of always "success"; area QR codes open each trade app's own schedule page (M&E, pest, pool, landscape no longer open Hygiene); Members, Roles and Invitations lists get a search box again; Bulletin Board list shows created / start-end / expiry dates in every app; ANPR camera settings show an error when saving fails; Service Provider Billing says plainly it is not available yet instead of an empty table; the "unauthorized" page now says you don't have permission (not "must be a member").
14
+
3
15
  ## 4.137.0
4
16
 
5
17
  ### Minor Changes
@@ -3377,11 +3389,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
3377
3389
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
3378
3390
  own rule from two endpoints the console already calls, unprojected:
3379
3391
 
3380
- GET /api/members/user/:user/app/admin the Seven365 staff membership
3381
- GET /api/roles/id/:role that membership's role document
3392
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
3393
+ GET /api/roles/id/:role that membership's role document
3382
3394
 
3383
- owner = member.type === "admin" && role.type === "admin" && role.default === true
3384
- staff = member.type === "admin" && role.type === "admin"
3395
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
3396
+ staff = member.type === "admin" && role.type === "admin"
3385
3397
 
3386
3398
  `role.default` is the marker because it is the only property of a platform
3387
3399
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -759,14 +759,20 @@ const onShowQRAction = () => {
759
759
  const id =
760
760
  (selectedItem.value as any)?._id || (selectedItem.value as any)?.id;
761
761
  const runtimeConfig = useRuntimeConfig ? useRuntimeConfig() : undefined;
762
- const origin =
763
- runtimeConfig?.public?.APP_HYGIENE ||
764
- (typeof window !== "undefined" && window.location?.origin
762
+ // The QR opens this app's own schedule page (M&E, pest, pool, landscape
763
+ // pass their route); only Hygiene uses the configured Hygiene host.
764
+ const scheduleSegment = props.scheduleRoute || "cleaning-schedule";
765
+ const currentOrigin =
766
+ typeof window !== "undefined" && window.location?.origin
765
767
  ? window.location.origin
766
- : "https://hygiene.app.iservice365.org");
768
+ : "";
769
+ const origin =
770
+ (scheduleSegment === "cleaning-schedule" && runtimeConfig?.public?.APP_HYGIENE) ||
771
+ currentOrigin ||
772
+ "https://hygiene.app.iservice365.org";
767
773
  qrPath.value = `${origin}/${props.orgId}/${
768
774
  props.site
769
- }/cleaning-schedule?qrcode=true&id=${encodeURIComponent(
775
+ }/${scheduleSegment}?qrcode=true&id=${encodeURIComponent(
770
776
  id
771
777
  )}&site=${encodeURIComponent(props.site)}`;
772
778
  };
@@ -1,62 +1,21 @@
1
1
  <template>
2
2
  <div>
3
3
  <!--
4
- THE DESIGN'S PAGE SCAFFOLD (same shape as Service Providers).
5
-
6
- Placement only: the same refresh control, the same range text, the same
7
- pager bound to the same `page`, and the table, its columns and its cell
8
- templates are untouched. The screen simply gains the page title it has
9
- never had, and its card + toolbar are the design's rather than Vuetify's.
4
+ Service-provider billing is not built yet. This screen used to draw an
5
+ empty table with a refresh button that did nothing, which read as
6
+ "there are no bills". It now says plainly what is (not) here.
10
7
  -->
11
8
  <PageHeader title="Billing" />
12
9
 
13
- <AppCard class="table-card">
14
- <CardToolbar :count="pageRange">
15
- <template #left>
16
- <AppButton variant="icon" icon="mdi-refresh" aria-label="Refresh" />
17
- </template>
18
-
19
- <template #right>
20
- <local-pagination v-model="page" :length="pages" />
21
- </template>
22
- </CardToolbar>
23
-
24
- <v-data-table
25
- :headers="headers"
26
- :items="items"
27
- item-value="_id"
28
- items-per-page="20"
29
- fixed-header
30
- hide-default-footer
31
- hide-default-header
32
- style="max-height: calc(100vh - (180px))"
33
- >
34
- <template #item.permissions="{ value }">
35
- <span class="text-caption font-weight-bold text-capitalize">
36
- permissions
37
- </span>
38
- <StatusChip :dot="false" tone="neutral" :label="`${value.length}`" />
39
- </template>
40
-
41
- <template #item.nature="{ item }">
42
- <span class="text-capitalize">
43
- {{ item.nature }}
44
- </span>
45
- </template>
46
- </v-data-table>
10
+ <AppCard class="pa-6">
11
+ <p class="text-body-1 mb-2">
12
+ Service provider billing is not available yet.
13
+ </p>
14
+ <p class="text-body-2 text-medium-emphasis mb-0">
15
+ Bills from your service providers will appear here once this feature is
16
+ switched on. For billing questions today, please contact your service
17
+ provider directly.
18
+ </p>
47
19
  </AppCard>
48
20
  </div>
49
21
  </template>
50
-
51
- <script setup lang="ts">
52
- const page = ref(1);
53
- const pages = ref(0);
54
- const pageRange = ref("-- - -- of --");
55
-
56
- const items = ref<Array<Record<string, any>>>([]);
57
-
58
- const headers = [
59
- { title: "Name", value: "name" },
60
- { title: "Nature", value: "nature" },
61
- ];
62
- </script>
@@ -13,7 +13,7 @@
13
13
 
14
14
  <TableMain :headers="headers" :items="items" v-model:search="searchInput"
15
15
  :loading="getAnnouncementPending" :page="page" :pages="pages" :pageRange="pageRange"
16
- @refresh="getAnnouncementsRefresh" :show-header="APP_CONSTANTS.RESIDENT === props.recipients" @update:page="handleUpdatePage" @row-click="handleRowClick">
16
+ @refresh="getAnnouncementsRefresh" show-header @update:page="handleUpdatePage" @row-click="handleRowClick">
17
17
  <!--
18
18
  The tabs move ONTO the toolbar row (the design draws them there)
19
19
  instead of a stacked strip above the table. Same options, same
@@ -83,7 +83,6 @@
83
83
  </template>
84
84
  <script setup lang="ts">
85
85
  import useBulletin from '../composables/useBulletin';
86
- import { APP_CONSTANTS } from '../constants/app';
87
86
  // Imported by relative path, not left to Nuxt's auto-import: this layer's
88
87
  // components always import their utils explicitly, and a missing import here
89
88
  // is a ReferenceError that blanks the screen in every consuming app.
@@ -141,18 +140,15 @@ const { debounce } = useUtils()
141
140
 
142
141
 
143
142
  const headers = computed(() => {
144
- const arr = [
143
+ // Every app shows when an announcement was posted and when it runs/ends.
144
+ // These used to appear only for recipients="resident", which no app passes,
145
+ // so staff saw bare titles with no dates.
146
+ return [
145
147
  { title: "Title", value: "title", align: "start" },
146
- ]
147
-
148
- if (props.recipients === APP_CONSTANTS.RESIDENT) {
149
- arr.push({ title: "Date Created", value: "createdAt", align: "start" });
150
- arr.push({ title: "Start Date/End Date", value: "duration", align: "start" });
151
- arr.push({ title: "No Expiration", value: "noExpiration", align: "start" });
152
- arr.push({ title: "", value: "actions", align: "center" });
153
- }
154
-
155
- return arr;
148
+ { title: "Date Created", value: "createdAt", align: "start" },
149
+ { title: "Start Date/End Date", value: "duration", align: "start" },
150
+ { title: "No Expiration", value: "noExpiration", align: "start" },
151
+ ];
156
152
 
157
153
  });
158
154
  // THE SERVER PAGES THIS LIST. `getAll` is called with `page`, so `items` is
@@ -767,10 +767,14 @@ function handleSuccess() { dialog.value = false; fetchData() }
767
767
 
768
768
  /* ── CANCEL INVITE ── */
769
769
  async function cancelInvite(item: any) {
770
+ // ponytail: native confirm/alert - this console page has no snackbar of its own.
771
+ if (!window.confirm(`Cancel the invitation for ${item.email}? They will no longer be able to use the invite link.`)) return
770
772
  try {
771
773
  cancelLoadingId.value = item._id
772
774
  await cancelUserInvitation(item._id)
773
775
  await fetchData()
776
+ } catch (error: any) {
777
+ window.alert(error?.data?.message || error?.message || 'Could not cancel the invitation. Please try again.')
774
778
  } finally {
775
779
  cancelLoadingId.value = null
776
780
  }
@@ -27,6 +27,14 @@
27
27
  </template>
28
28
 
29
29
  <template #right>
30
+ <AppField
31
+ v-model="headerSearch"
32
+ search
33
+ compact
34
+ placeholder="Search"
35
+ aria-label="Search"
36
+ klass="app-toolbar__search"
37
+ />
30
38
  <AppButton
31
39
  variant="icon"
32
40
  icon="mdi-refresh"
@@ -85,6 +85,14 @@
85
85
  </template>
86
86
 
87
87
  <template #right>
88
+ <AppField
89
+ v-model="headerSearch"
90
+ search
91
+ compact
92
+ placeholder="Search"
93
+ aria-label="Search"
94
+ klass="app-toolbar__search"
95
+ />
88
96
  <AppButton
89
97
  variant="icon"
90
98
  icon="mdi-refresh"
@@ -32,6 +32,14 @@
32
32
  </template>
33
33
 
34
34
  <template #right>
35
+ <AppField
36
+ v-model="headerSearch"
37
+ search
38
+ compact
39
+ placeholder="Search"
40
+ aria-label="Search"
41
+ klass="app-toolbar__search"
42
+ />
35
43
  <local-pagination
36
44
  v-model="page"
37
45
  :length="pages"
@@ -446,6 +454,11 @@ const {
446
454
  }
447
455
  );
448
456
 
457
+ // A new search starts from the first page.
458
+ watch(headerSearch, () => {
459
+ page.value = 1;
460
+ });
461
+
449
462
  const loading = computed(() => getRoleReqStatus.value === "pending");
450
463
 
451
464
  watchEffect(() => {
@@ -541,11 +541,6 @@ const saveANPRSettings = async () => {
541
541
  try {
542
542
  isSaving.value = true;
543
543
 
544
- // Replace this with your actual API endpoint / store action
545
- // await $fetch(`/api/sites/${siteId.value}/camera-settings`, {
546
- // method: 'POST',
547
- // body: cameraSettings.value
548
- // })
549
544
  const data = {
550
545
  site: props.siteId,
551
546
  ANPRSwitches: ANPRSwitches.value,
@@ -556,6 +551,11 @@ const saveANPRSettings = async () => {
556
551
  show("Saved successfully", "success");
557
552
  } catch (error) {
558
553
  console.error("Failed to save settings:", error);
554
+ show(
555
+ (error as any)?.data?.message ||
556
+ "Could not save the camera (ANPR) settings. Please try again.",
557
+ "error"
558
+ );
559
559
  } finally {
560
560
  isSaving.value = false;
561
561
  }
@@ -132,21 +132,25 @@ export default function useKey() {
132
132
  keyLog.value = data || new MKeyLog();
133
133
  }
134
134
 
135
- function addKeyLog(_keyLog: TKeyLog) {
135
+ // Await each request so a failed save reaches the caller's catch instead of showing "success".
136
+ async function addKeyLog(_keyLog: TKeyLog) {
136
137
  isAdding.value = true;
137
138
  delete _keyLog._id;
138
- const data = useNuxtApp().$api<{
139
- message: string | undefined;
140
- }>(keyLogApiUrl, {
141
- method: "POST",
142
- body: JSON.stringify(_keyLog),
143
- });
144
- isAdding.value = false;
145
- return data.message;
139
+ try {
140
+ const data = await useNuxtApp().$api<{
141
+ message: string | undefined;
142
+ }>(keyLogApiUrl, {
143
+ method: "POST",
144
+ body: JSON.stringify(_keyLog),
145
+ });
146
+ return data?.message;
147
+ } finally {
148
+ isAdding.value = false;
149
+ }
146
150
  }
147
151
 
148
- function getKeyById(id: string) {
149
- const data = useNuxtApp().$api<TKeyLog | undefined>(
152
+ async function getKeyById(id: string) {
153
+ const data = await useNuxtApp().$api<TKeyLog | undefined>(
150
154
  `${keyLogApiUrl}/${id}`,
151
155
  {
152
156
  method: "GET",
@@ -158,22 +162,22 @@ export default function useKey() {
158
162
  return data;
159
163
  }
160
164
 
161
- function updateKeyById(id: string, payload: TKeyLog) {
165
+ async function updateKeyById(id: string, payload: TKeyLog) {
162
166
  payload.codes =
163
167
  payload.codes instanceof Array && payload.codes.length > 0
164
168
  ? payload.codes
165
169
  : [];
166
- const data = useNuxtApp().$api<{
170
+ const data = await useNuxtApp().$api<{
167
171
  message: { acknowledged: boolean };
168
172
  }>(`${keyLogApiUrl}/${id}`, {
169
173
  method: "PUT",
170
174
  body: payload,
171
175
  });
172
- return data.message;
176
+ return data?.message;
173
177
  }
174
178
 
175
- function deleteKeyById(id: string) {
176
- const data = useNuxtApp().$api<{ message: TKeyLog }>(
179
+ async function deleteKeyById(id: string) {
180
+ const data = await useNuxtApp().$api<{ message: TKeyLog }>(
177
181
  `${keyLogApiUrl}/${id}`,
178
182
  {
179
183
  method: "DELETE",
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.137.0",
5
+ "version": "4.139.0",
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.",
@@ -1,5 +1,6 @@
1
1
  <!--
2
- The "you are not a member of an organization" gate.
2
+ The "no permission for this page" gate (wording only; non-members use
3
+ require-organization-membership).
3
4
 
4
5
  Was Vuetify's chrome: `text-subtitle-1` for the sentence, `text-subtitle-2`
5
6
  on the buttons, and `variant="tonal" rounded="xl"` - a 24px-cornered pill in
@@ -12,8 +13,8 @@
12
13
  <template>
13
14
  <v-row no-gutters class="fill-height" align="center" justify="center">
14
15
  <v-col cols="12" lg="6" md="6" sm="6" class="text-center gate-page__message">
15
- You must be a member of an organization to view this page. Please join or
16
- contact your administrator for access.
16
+ You don't have permission to open this page. If you need it for your
17
+ work, ask your administrator to give your role access.
17
18
  <v-row no-gutters justify="center" class="mt-2">
18
19
  <v-btn
19
20
  class="screen-btn-primary"