@7365admin1/layer-common 4.97.1-staging.582 → 4.97.1-staging.584

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.
@@ -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(() => {
@@ -544,11 +544,6 @@ const saveANPRSettings = async () => {
544
544
  try {
545
545
  isSaving.value = true;
546
546
 
547
- // Replace this with your actual API endpoint / store action
548
- // await $fetch(`/api/sites/${siteId.value}/camera-settings`, {
549
- // method: 'POST',
550
- // body: cameraSettings.value
551
- // })
552
547
  const data = {
553
548
  site: props.siteId,
554
549
  ANPRSwitches: ANPRSwitches.value,
@@ -559,6 +554,11 @@ const saveANPRSettings = async () => {
559
554
  show("Saved successfully", "success");
560
555
  } catch (error) {
561
556
  console.error("Failed to save settings:", error);
557
+ show(
558
+ (error as any)?.data?.message ||
559
+ "Could not save the camera (ANPR) settings. Please try again.",
560
+ "error"
561
+ );
562
562
  } finally {
563
563
  isSaving.value = false;
564
564
  }
@@ -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.97.1-staging.582",
5
+ "version": "4.97.1-staging.584",
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"