@7365admin1/layer-common 4.137.0 → 4.138.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 +10 -4
- package/components/AreaMain.vue +11 -5
- package/components/BillingMain.vue +12 -53
- package/components/BulletinBoardManagement.vue +9 -13
- 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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.138.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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").
|
|
8
|
+
|
|
3
9
|
## 4.137.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -3377,11 +3383,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
|
|
|
3377
3383
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
3378
3384
|
own rule from two endpoints the console already calls, unprojected:
|
|
3379
3385
|
|
|
3380
|
-
|
|
3381
|
-
|
|
3386
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
3387
|
+
GET /api/roles/id/:role that membership's role document
|
|
3382
3388
|
|
|
3383
|
-
|
|
3384
|
-
|
|
3389
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
3390
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
3385
3391
|
|
|
3386
3392
|
`role.default` is the marker because it is the only property of a platform
|
|
3387
3393
|
staff role no API caller can set - `role.controller.ts` validates create and
|
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
|
|
@@ -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
|
}
|
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.
|
|
5
|
+
"version": "4.138.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.",
|
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"
|