@7365admin1/layer-common 4.39.0 → 4.40.1
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 +22 -4
- package/components/CameraForm.vue +20 -6
- package/components/ClientInvitations.vue +35 -3
- package/components/VisitorManagement.vue +18 -5
- package/components/VisitorSocketPopUp.vue +2 -1
- package/composables/useANPRSettings.ts +45 -15
- package/composables/useServiceProviderInvitation.ts +14 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.40.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 783ffe2: Edit Camera: the device password is write-only. On edit the Password field is optional, starts blank with the placeholder "Saved · leave blank to keep", and is sent only when a new one is typed, so an admin can switch a camera's Status without re-typing its login. A status-only change now enables Submit. Password stays required when adding a camera.
|
|
8
|
+
|
|
9
|
+
## 4.40.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 3a96438: Client invitations: an accepted invitation whose site never reached the company's Sites list now says so in plain words and offers "Add <site> to your Sites" (`linkInvitationSite`, `PUT /service-provider-invitations/:id/link-site`). Needs core 3.100.0.
|
|
14
|
+
|
|
15
|
+
## 4.39.1
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- b1c2177: Visitor Mgmt: the Unregistered tab now appears on first load and after switching site, without a hard refresh. `useANPRSettings` takes a ref or getter for the site and keeps one entry per site; the Visitor Mgmt page reads its own site's ANPR switches instead of relying on the layout's plate toast, which kept the first site's switches after a site switch. A plate-toast link to the Unregistered tab is no longer bounced to Registered while the switches are still loading.
|
|
20
|
+
|
|
3
21
|
## 4.39.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
|
@@ -1447,11 +1465,11 @@
|
|
|
1447
1465
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
1448
1466
|
own rule from two endpoints the console already calls, unprojected:
|
|
1449
1467
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1468
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
1469
|
+
GET /api/roles/id/:role that membership's role document
|
|
1452
1470
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1471
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
1472
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
1455
1473
|
|
|
1456
1474
|
`role.default` is the marker because it is the only property of a platform
|
|
1457
1475
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -161,14 +161,21 @@
|
|
|
161
161
|
</v-col>
|
|
162
162
|
|
|
163
163
|
<v-col v-if="isANPR" cols="12">
|
|
164
|
-
|
|
164
|
+
<!-- Write-only: the saved password is never sent to the browser, so
|
|
165
|
+
on edit a blank field keeps it. Required only for a new camera. -->
|
|
166
|
+
<InputLabel
|
|
167
|
+
class="text-capitalize"
|
|
168
|
+
title="Password"
|
|
169
|
+
:required="!isEdit"
|
|
170
|
+
/>
|
|
165
171
|
<v-text-field
|
|
166
172
|
v-model.trim="camera.password"
|
|
167
173
|
type="password"
|
|
174
|
+
autocomplete="new-password"
|
|
168
175
|
class="filter-field"
|
|
169
176
|
density="comfortable"
|
|
170
|
-
:rules="[requiredRule]"
|
|
171
|
-
placeholder="Enter password"
|
|
177
|
+
:rules="isEdit ? [] : [requiredRule]"
|
|
178
|
+
:placeholder="isEdit ? 'Saved · leave blank to keep' : 'Enter password'"
|
|
172
179
|
/>
|
|
173
180
|
</v-col>
|
|
174
181
|
|
|
@@ -419,8 +426,12 @@ const camera = ref<TSiteCamera>({
|
|
|
419
426
|
|
|
420
427
|
camera.value.site = prop.site;
|
|
421
428
|
|
|
422
|
-
|
|
429
|
+
const isEdit = prop.mode === "edit";
|
|
430
|
+
|
|
431
|
+
if (isEdit) {
|
|
423
432
|
camera.value = JSON.parse(JSON.stringify(prop.camera));
|
|
433
|
+
// Never pre-filled: blank means "keep the saved password".
|
|
434
|
+
camera.value.password = "";
|
|
424
435
|
}
|
|
425
436
|
camera.value.site = prop.site;
|
|
426
437
|
camera.value.type = prop.type;
|
|
@@ -576,7 +587,9 @@ const hasChanges = computed(() => {
|
|
|
576
587
|
return (
|
|
577
588
|
camera.value.host !== prop.camera.host ||
|
|
578
589
|
camera.value.username !== prop.camera.username ||
|
|
579
|
-
|
|
590
|
+
// A typed password is a change; blank keeps the saved one.
|
|
591
|
+
!!camera.value.password ||
|
|
592
|
+
camera.value.status !== prop.camera.status ||
|
|
580
593
|
camera.value.category !== prop.camera.category ||
|
|
581
594
|
camera.value.guardPost !== prop.camera.guardPost ||
|
|
582
595
|
camera.value.direction !== prop.camera.direction ||
|
|
@@ -895,7 +908,8 @@ async function save(
|
|
|
895
908
|
const res = await updateSiteCamera(camera.value._id ?? "", {
|
|
896
909
|
...addressPayload(),
|
|
897
910
|
username: camera.value.username,
|
|
898
|
-
|
|
911
|
+
// Sent only when typed; left out, the server keeps the saved one.
|
|
912
|
+
...(camera.value.password ? { password: camera.value.password } : {}),
|
|
899
913
|
category: camera.value.category,
|
|
900
914
|
guardPost: camera.value.guardPost,
|
|
901
915
|
direction: camera.value.direction,
|
|
@@ -78,7 +78,17 @@
|
|
|
78
78
|
</div>
|
|
79
79
|
|
|
80
80
|
<div v-else-if="item.status === 'complete'" class="px-4 pb-4">
|
|
81
|
-
<
|
|
81
|
+
<v-btn
|
|
82
|
+
v-if="item.needsSiteLink"
|
|
83
|
+
variant="flat"
|
|
84
|
+
class="screen-btn-primary"
|
|
85
|
+
:loading="working[item._id] === 'link'"
|
|
86
|
+
:disabled="Boolean(working[item._id])"
|
|
87
|
+
@click="addToSites(item)"
|
|
88
|
+
>
|
|
89
|
+
Add {{ item.siteName }} to your Sites
|
|
90
|
+
</v-btn>
|
|
91
|
+
<NuxtLink v-else-if="item.onThisOrg" :to="`/org/${org}/sites`">Open Sites</NuxtLink>
|
|
82
92
|
<NuxtLink
|
|
83
93
|
v-else-if="item.providerOrgId && item.providerOrgId !== org"
|
|
84
94
|
:to="`/org/${item.providerOrgId}/sites`"
|
|
@@ -124,8 +134,13 @@ const tabs: Array<{ value: Tab; label: string }> = [
|
|
|
124
134
|
];
|
|
125
135
|
|
|
126
136
|
const route = useRoute();
|
|
127
|
-
const {
|
|
128
|
-
|
|
137
|
+
const {
|
|
138
|
+
getReceivedInvitations,
|
|
139
|
+
getInvitation,
|
|
140
|
+
acceptInvitation,
|
|
141
|
+
declineInvitation,
|
|
142
|
+
linkInvitationSite,
|
|
143
|
+
} = useServiceProviderInvitation();
|
|
129
144
|
|
|
130
145
|
const tab = ref<Tab>("pending");
|
|
131
146
|
const items = ref<TReceivedServiceProviderInvitation[]>([]);
|
|
@@ -180,6 +195,9 @@ function stateLine(item: TReceivedServiceProviderInvitation) {
|
|
|
180
195
|
if (item.onThisOrg) {
|
|
181
196
|
return `Accepted${on(item.answeredAt)} — ${item.siteName} is on your Sites list as a site you serve for ${item.orgName}.`;
|
|
182
197
|
}
|
|
198
|
+
if (item.needsSiteLink) {
|
|
199
|
+
return "This invitation was accepted, but the site wasn't added to your Sites. Add it now.";
|
|
200
|
+
}
|
|
183
201
|
if (!item.providerOrgId || item.providerOrgId === props.org) {
|
|
184
202
|
return `Accepted${on(item.answeredAt)}, but ${item.siteName} is not on this company's Sites list. Please contact Seven365 support so we can add it.`;
|
|
185
203
|
}
|
|
@@ -228,6 +246,20 @@ async function answer(item: TReceivedServiceProviderInvitation, action: "accept"
|
|
|
228
246
|
}
|
|
229
247
|
}
|
|
230
248
|
|
|
249
|
+
/** Accepted but never added to the Sites list: add it now, then show it. */
|
|
250
|
+
async function addToSites(item: TReceivedServiceProviderInvitation) {
|
|
251
|
+
errors[item._id] = "";
|
|
252
|
+
working[item._id] = "link";
|
|
253
|
+
try {
|
|
254
|
+
await linkInvitationSite(item._id, props.org);
|
|
255
|
+
await load();
|
|
256
|
+
} catch (err: any) {
|
|
257
|
+
errors[item._id] = err?.data?.message || "Could not add this site to your Sites.";
|
|
258
|
+
} finally {
|
|
259
|
+
working[item._id] = "";
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
231
263
|
// Opened from the bell: start on the tab the linked invitation is in.
|
|
232
264
|
if (linked.value) {
|
|
233
265
|
try {
|
|
@@ -1502,8 +1502,17 @@ const { org: orgId, site: siteId } = route.params as {
|
|
|
1502
1502
|
site: string;
|
|
1503
1503
|
};
|
|
1504
1504
|
|
|
1505
|
-
|
|
1505
|
+
// The page reads its own site's switches rather than relying on the layout's
|
|
1506
|
+
// toast to have fetched them - that copy could belong to the previous site.
|
|
1507
|
+
const {
|
|
1508
|
+
enableUnregistered,
|
|
1509
|
+
loaded: anprSettingsLoaded,
|
|
1510
|
+
ensureANPRSettings,
|
|
1511
|
+
} = useANPRSettings(siteId);
|
|
1506
1512
|
const enableUnregisteredTab = computed(() => enableUnregistered.value);
|
|
1513
|
+
ensureANPRSettings(true).catch(() => {
|
|
1514
|
+
// No settings = no Unregistered tab, same as before.
|
|
1515
|
+
});
|
|
1507
1516
|
|
|
1508
1517
|
const {
|
|
1509
1518
|
getVisitors,
|
|
@@ -2933,7 +2942,11 @@ watch([searchInput, dateFrom, dateTo, filterTypes, activeTab], () => {
|
|
|
2933
2942
|
const syncVisitorFiltersFromRoute = () => {
|
|
2934
2943
|
const requestedTab = (route.query.tab as string) || "registered";
|
|
2935
2944
|
const nextTab =
|
|
2936
|
-
|
|
2945
|
+
// Until the switches are read, keep a requested Unregistered tab (the plate
|
|
2946
|
+
// toast links here) instead of bouncing it to Registered.
|
|
2947
|
+
(anprSettingsLoaded.value &&
|
|
2948
|
+
!enableUnregisteredTab.value &&
|
|
2949
|
+
requestedTab === "unregistered") ||
|
|
2937
2950
|
(!showResidentTransactionsTab.value && requestedTab === "resident-transactions")
|
|
2938
2951
|
? "registered"
|
|
2939
2952
|
: requestedTab;
|
|
@@ -2992,9 +3005,9 @@ const { socketUnregisteredVisitorTrigger, visitorSocketData } =
|
|
|
2992
3005
|
useVisitorSocket();
|
|
2993
3006
|
|
|
2994
3007
|
watch(
|
|
2995
|
-
enableUnregisteredTab,
|
|
2996
|
-
(enabled) => {
|
|
2997
|
-
if (!enabled && activeTab.value === "unregistered") {
|
|
3008
|
+
[enableUnregisteredTab, anprSettingsLoaded],
|
|
3009
|
+
([enabled]) => {
|
|
3010
|
+
if (!enabled && anprSettingsLoaded.value && activeTab.value === "unregistered") {
|
|
2998
3011
|
activeTab.value = "registered";
|
|
2999
3012
|
}
|
|
3000
3013
|
},
|
|
@@ -137,8 +137,9 @@ const config = useRuntimeConfig();
|
|
|
137
137
|
const publicConfig = config.public as Record<string, string | undefined>;
|
|
138
138
|
const route = useRoute();
|
|
139
139
|
const { tab } = useVisitor();
|
|
140
|
+
// A getter, not the string: this component outlives site switches.
|
|
140
141
|
const { enableUnregistered, ensureANPRSettings } = useANPRSettings(
|
|
141
|
-
props.siteId
|
|
142
|
+
() => props.siteId
|
|
142
143
|
);
|
|
143
144
|
const enableUnregisteredSnackbar = computed(() => enableUnregistered.value);
|
|
144
145
|
const { socketUnregisteredVisitorTrigger, visitorSocketData } =
|
|
@@ -1,19 +1,50 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { computed, toValue, type MaybeRefOrGetter } from "vue";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* A site's ANPR switches (e.g. "Enable Unregistered Vehicles"), shared by every
|
|
5
|
+
* caller on the page: the Visitor Mgmt tabs, the plate toast in the layout, and
|
|
6
|
+
* Site Settings.
|
|
7
|
+
*
|
|
8
|
+
* `siteId` may be a ref or a getter. The layout's toast lives across site
|
|
9
|
+
* switches, so a plain string captured once kept answering for the FIRST site
|
|
10
|
+
* it saw - and the Visitor Mgmt page never fetched at all, so its
|
|
11
|
+
* "Unregistered" tab showed only when that stale copy happened to be for the
|
|
12
|
+
* right site (i.e. after a hard refresh on the page itself).
|
|
13
|
+
*
|
|
14
|
+
* One map keyed by site id: a key that is present means "fetched", even when
|
|
15
|
+
* the site has no settings yet (stored as `{}`).
|
|
16
|
+
*/
|
|
17
|
+
export default function (siteId?: MaybeRefOrGetter<string | undefined>) {
|
|
18
|
+
const site = computed(() => toValue(siteId) || "");
|
|
5
19
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
() =>
|
|
20
|
+
const bySite = useState<Record<string, Record<string, any>>>(
|
|
21
|
+
"anpr-settings-by-site",
|
|
22
|
+
() => ({})
|
|
9
23
|
);
|
|
10
24
|
|
|
25
|
+
const anprSettings = computed<Record<string, any> | null>({
|
|
26
|
+
get: () => bySite.value[site.value] ?? null,
|
|
27
|
+
set: (value) => {
|
|
28
|
+
bySite.value = { ...bySite.value, [site.value]: value ?? {} };
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** True once this site's settings have been read (or saved) at least once. */
|
|
33
|
+
const loaded = computed(() => site.value in bySite.value);
|
|
34
|
+
|
|
11
35
|
const enableUnregistered = computed(
|
|
12
36
|
() => anprSettings.value?.ANPRSwitches?.enableUnregistered === true
|
|
13
37
|
);
|
|
14
38
|
|
|
39
|
+
function store(forSite: unknown, response: Record<string, any> | null) {
|
|
40
|
+
const key = String(forSite ?? "");
|
|
41
|
+
if (!key) return;
|
|
42
|
+
bySite.value = { ...bySite.value, [key]: response ?? {} };
|
|
43
|
+
}
|
|
44
|
+
|
|
15
45
|
async function fetchANPRSettings(data?: any) {
|
|
16
|
-
const payload =
|
|
46
|
+
const payload =
|
|
47
|
+
data ?? (site.value ? { site: site.value, userType: "site" } : undefined);
|
|
17
48
|
|
|
18
49
|
if (!payload) {
|
|
19
50
|
return anprSettings.value;
|
|
@@ -27,23 +58,21 @@ export default function (siteId?: string) {
|
|
|
27
58
|
}
|
|
28
59
|
);
|
|
29
60
|
|
|
30
|
-
|
|
31
|
-
anprSettings.value = response;
|
|
32
|
-
}
|
|
61
|
+
store(payload.site, response);
|
|
33
62
|
|
|
34
63
|
return response;
|
|
35
64
|
}
|
|
36
65
|
|
|
37
66
|
async function ensureANPRSettings(force = false) {
|
|
38
|
-
if (!
|
|
67
|
+
if (!site.value) {
|
|
39
68
|
return anprSettings.value;
|
|
40
69
|
}
|
|
41
70
|
|
|
42
|
-
if (!force &&
|
|
71
|
+
if (!force && loaded.value) {
|
|
43
72
|
return anprSettings.value;
|
|
44
73
|
}
|
|
45
74
|
|
|
46
|
-
return await fetchANPRSettings({ site:
|
|
75
|
+
return await fetchANPRSettings({ site: site.value, userType: "site" });
|
|
47
76
|
}
|
|
48
77
|
|
|
49
78
|
async function getANPRSettingBySite(data: any) {
|
|
@@ -60,7 +89,7 @@ export default function (siteId?: string) {
|
|
|
60
89
|
);
|
|
61
90
|
|
|
62
91
|
if (response) {
|
|
63
|
-
|
|
92
|
+
store(data?.site, response);
|
|
64
93
|
}
|
|
65
94
|
|
|
66
95
|
return response;
|
|
@@ -68,6 +97,7 @@ export default function (siteId?: string) {
|
|
|
68
97
|
|
|
69
98
|
return {
|
|
70
99
|
anprSettings,
|
|
100
|
+
loaded,
|
|
71
101
|
enableUnregistered,
|
|
72
102
|
ensureANPRSettings,
|
|
73
103
|
getANPRSettingBySite,
|
|
@@ -48,6 +48,8 @@ export type TReceivedServiceProviderInvitation = {
|
|
|
48
48
|
providerOrgName?: string;
|
|
49
49
|
/** Accepted AND on the Sites list of the company on screen. */
|
|
50
50
|
onThisOrg?: boolean;
|
|
51
|
+
/** Accepted, but the site is on no company's Sites list: offer to add it. */
|
|
52
|
+
needsSiteLink?: boolean;
|
|
51
53
|
};
|
|
52
54
|
|
|
53
55
|
/** The words on screen. Kept beside the server's own list on purpose — a
|
|
@@ -171,6 +173,17 @@ export default function useServiceProviderInvitation() {
|
|
|
171
173
|
});
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
/**
|
|
177
|
+
* An accepted invitation whose site never reached this company's Sites
|
|
178
|
+
* list: add it now. Safe to repeat - the server adds it once.
|
|
179
|
+
*/
|
|
180
|
+
function linkInvitationSite(id: string, org: string) {
|
|
181
|
+
return useNuxtApp().$api<{ message: string }>(`${base}/${id}/link-site`, {
|
|
182
|
+
method: "PUT",
|
|
183
|
+
body: { org },
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
174
187
|
/**
|
|
175
188
|
* "Client invitations": one tab of the invitations sent to the signed-in
|
|
176
189
|
* account, plus the count in every tab. `org` is the company on screen.
|
|
@@ -200,6 +213,7 @@ export default function useServiceProviderInvitation() {
|
|
|
200
213
|
getInvitation,
|
|
201
214
|
declineInvitation,
|
|
202
215
|
acceptInvitation,
|
|
216
|
+
linkInvitationSite,
|
|
203
217
|
getReceivedInvitations,
|
|
204
218
|
};
|
|
205
219
|
}
|
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.40.1",
|
|
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.",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"build": "nuxt build .playground",
|
|
37
37
|
"generate": "nuxt generate .playground",
|
|
38
38
|
"preview": "nuxt preview .playground",
|
|
39
|
-
"test": "esbuild composables/useVisitorSocket.ts --format=esm --outfile=test/.build/useVisitorSocket.mjs --log-level=error && esbuild composables/usePlateToastWindow.ts --format=esm --outfile=test/.build/usePlateToastWindow.mjs --log-level=error && esbuild utils/client-invite-modules.ts --bundle --platform=node --format=esm --outfile=test/.build/client-invite-modules.mjs --log-level=error && esbuild utils/robot-display.ts --bundle --platform=node --format=esm --outfile=test/.build/robot-display.mjs --log-level=error && esbuild utils/building-map.ts --bundle --platform=node --format=esm --outfile=test/.build/building-map.mjs --log-level=error && esbuild utils/robot-building.ts --bundle --platform=node --format=esm --outfile=test/.build/robot-building.mjs --log-level=error && esbuild utils/robot-fleet-core.ts --bundle --platform=node --format=esm --outfile=test/.build/robot-fleet-core.mjs --log-level=error && node --experimental-strip-types --test \"test/*.test.mjs\" && yarn test:units",
|
|
39
|
+
"test": "esbuild composables/useANPRSettings.ts --format=esm --outfile=test/.build/useANPRSettings.mjs --log-level=error && esbuild composables/useVisitorSocket.ts --format=esm --outfile=test/.build/useVisitorSocket.mjs --log-level=error && esbuild composables/usePlateToastWindow.ts --format=esm --outfile=test/.build/usePlateToastWindow.mjs --log-level=error && esbuild utils/client-invite-modules.ts --bundle --platform=node --format=esm --outfile=test/.build/client-invite-modules.mjs --log-level=error && esbuild utils/robot-display.ts --bundle --platform=node --format=esm --outfile=test/.build/robot-display.mjs --log-level=error && esbuild utils/building-map.ts --bundle --platform=node --format=esm --outfile=test/.build/building-map.mjs --log-level=error && esbuild utils/robot-building.ts --bundle --platform=node --format=esm --outfile=test/.build/robot-building.mjs --log-level=error && esbuild utils/robot-fleet-core.ts --bundle --platform=node --format=esm --outfile=test/.build/robot-fleet-core.mjs --log-level=error && node --experimental-strip-types --test \"test/*.test.mjs\" && yarn test:units",
|
|
40
40
|
"test:units": "node --experimental-strip-types --test \"utils/*.test.ts\"",
|
|
41
41
|
"release": "yarn run build && changeset publish"
|
|
42
42
|
},
|