@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.
- package/components/AreaMain.vue +11 -5
- package/components/BillingMain.vue +12 -53
- package/components/BulletinBoardManagement.vue +9 -13
- package/components/ClientMain.vue +4 -0
- package/components/InvitationMain.vue +8 -0
- package/components/MemberMain.vue +8 -0
- package/components/RolePermissionMain.vue +13 -0
- package/components/SiteSettings.vue +5 -5
- package/composables/useKey.ts +20 -16
- package/package.json +1 -1
- package/pages/unauthorized.vue +4 -3
package/components/AreaMain.vue
CHANGED
|
@@ -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
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
: "
|
|
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
|
-
}
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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="
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
}
|
|
@@ -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
|
}
|
package/composables/useKey.ts
CHANGED
|
@@ -132,21 +132,25 @@ export default function useKey() {
|
|
|
132
132
|
keyLog.value = data || new MKeyLog();
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
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.
|
|
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.",
|
package/pages/unauthorized.vue
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
The "
|
|
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
|
|
16
|
-
|
|
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"
|