@7365admin1/layer-common 4.148.0 → 4.148.2
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 +30 -4
- package/components/AnprCameraBadge.vue +15 -2
- package/components/CameraForm.vue +73 -17
- package/components/CameraMain.vue +12 -3
- package/components/Snackbar.vue +36 -0
- package/components/TableMain.vue +12 -1
- package/components/VehicleAddSelection.vue +11 -4
- package/components/VehicleManagement.vue +83 -30
- package/components/VisitorDataFromScannedQRCodeForm.vue +6 -1
- package/components/VisitorForm.vue +47 -17
- package/components/VisitorManagement.vue +145 -8
- package/composables/useVisitorCameraChoices.ts +61 -0
- package/package.json +1 -1
- package/types/camera.d.ts +2 -0
- package/utils/anpr-direction.ts +7 -6
- package/utils/vehicle-actions.ts +23 -5
- package/utils/visitor-barrier.ts +42 -0
- package/utils/visitor-camera.ts +80 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { defaultGateForSite, visitorCameras, visitorGateChoices } from "../utils/visitor-camera";
|
|
2
|
+
import useSiteSettings from "./useSiteSettings";
|
|
3
|
+
import useLocalAuth from "./useLocalAuth";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The site's ANPR cameras a visit can be checked at (active Entry, Both Entry
|
|
7
|
+
* and Exit, Visitors - see `utils/visitor-camera.ts`) and the signed-in user's
|
|
8
|
+
* default gate there (`defaultCameraGate: [{ site, cameraGateNumber }]` on
|
|
9
|
+
* their user, picked on the visitor list). The cameras are read in the
|
|
10
|
+
* background; a failed read shows no camera icon and the server uses the
|
|
11
|
+
* site's first camera.
|
|
12
|
+
*/
|
|
13
|
+
export default function useVisitorCameraChoices(site: () => string) {
|
|
14
|
+
const { getAllSiteCameras } = useSiteSettings();
|
|
15
|
+
const { currentUser } = useLocalAuth();
|
|
16
|
+
const cameras = ref<unknown[]>([]);
|
|
17
|
+
|
|
18
|
+
async function load() {
|
|
19
|
+
const id = site();
|
|
20
|
+
if (!id) {
|
|
21
|
+
cameras.value = [];
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const res = await getAllSiteCameras({ site: id, type: "anpr", limit: 100 });
|
|
26
|
+
cameras.value = Array.isArray((res as any)?.items) ? (res as any).items : [];
|
|
27
|
+
} catch {
|
|
28
|
+
cameras.value = [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
watch(site, load, { immediate: true });
|
|
33
|
+
|
|
34
|
+
/** The user's default gate at this site (sent as `anprGate`), or undefined. */
|
|
35
|
+
const defaultGate = computed(() =>
|
|
36
|
+
defaultGateForSite((currentUser.value as any)?.defaultCameraGate, site())
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/** Save (or clear, `null`) the default gate at this site, on the user. */
|
|
40
|
+
async function saveDefaultGate(gate: number | null) {
|
|
41
|
+
const res = await useNuxtApp().$api<Record<string, any>>("/api/users/default-camera-gate", {
|
|
42
|
+
method: "PATCH",
|
|
43
|
+
body: { site: site(), gate },
|
|
44
|
+
});
|
|
45
|
+
if (currentUser.value) {
|
|
46
|
+
(currentUser.value as any).defaultCameraGate = Array.isArray(res?.defaultCameraGate)
|
|
47
|
+
? res.defaultCameraGate
|
|
48
|
+
: [];
|
|
49
|
+
}
|
|
50
|
+
return res;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
/** How many active Entry / Both / Visitors cameras (the icon shows with more than one). */
|
|
55
|
+
count: computed(() => visitorCameras(cameras.value).length),
|
|
56
|
+
choices: computed(() => visitorGateChoices(cameras.value)),
|
|
57
|
+
defaultGate,
|
|
58
|
+
saveDefaultGate,
|
|
59
|
+
reload: load,
|
|
60
|
+
};
|
|
61
|
+
}
|
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.148.
|
|
5
|
+
"version": "4.148.2",
|
|
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/types/camera.d.ts
CHANGED
package/utils/anpr-direction.ts
CHANGED
|
@@ -12,8 +12,11 @@ export const isAllowCameraDirection = (direction: unknown): boolean =>
|
|
|
12
12
|
String(direction ?? "").trim().toLowerCase(),
|
|
13
13
|
);
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* ANPR camera types that carry blocked plates: every type, Exit included
|
|
17
|
+
* (owner, 2026-10-23). Mirrors `BLOCK_CAMERA_DIRECTIONS` in iservice365-core.
|
|
18
|
+
*/
|
|
19
|
+
export const BLOCK_CAMERA_DIRECTIONS = ["entry", "both", "residents", "visitors", "exit"];
|
|
17
20
|
|
|
18
21
|
export const isBlockCameraDirection = (direction: unknown): boolean =>
|
|
19
22
|
BLOCK_CAMERA_DIRECTIONS.includes(
|
|
@@ -48,8 +51,8 @@ export const listsFilledByDirectionChange = (
|
|
|
48
51
|
* Mirrors `listsClearedByDirectionChange` in iservice365-core, which refuses
|
|
49
52
|
* the change unless it is confirmed, so the form asks first.
|
|
50
53
|
*
|
|
51
|
-
* -
|
|
52
|
-
*
|
|
54
|
+
* - Entry / Both / Residents to Visitors or Exit: the allowed list. Both
|
|
55
|
+
* keep their blocklist (every camera holds the blocked plates).
|
|
53
56
|
* - Anything else: nothing.
|
|
54
57
|
*/
|
|
55
58
|
export const listsClearedByDirectionChange = (
|
|
@@ -61,8 +64,6 @@ export const listsClearedByDirectionChange = (
|
|
|
61
64
|
|
|
62
65
|
if (!after || after === before) return { allow: false, block: false };
|
|
63
66
|
|
|
64
|
-
if (after === "exit") return { allow: true, block: true };
|
|
65
|
-
|
|
66
67
|
return {
|
|
67
68
|
allow: isAllowCameraDirection(before) && !isAllowCameraDirection(after),
|
|
68
69
|
block: false,
|
package/utils/vehicle-actions.ts
CHANGED
|
@@ -27,7 +27,11 @@ export type VehicleActionPermissions = {
|
|
|
27
27
|
canUpdateVehicle?: boolean;
|
|
28
28
|
canDeleteVehicle?: boolean;
|
|
29
29
|
canApproveVehicle?: boolean;
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Block / Unblock, and every write on a Block Listed plate (type blocklist):
|
|
32
|
+
* Edit, Delete and Restore on it alone, Approve together with
|
|
33
|
+
* `canApproveVehicle`. Omitted = today's rule (Update / Delete / Approve).
|
|
34
|
+
*/
|
|
31
35
|
canManageBlocklist?: boolean;
|
|
32
36
|
};
|
|
33
37
|
|
|
@@ -89,13 +93,27 @@ export function vehiclePlateActions(
|
|
|
89
93
|
permissions.canManageBlocklist === undefined
|
|
90
94
|
? canUpdate
|
|
91
95
|
: !!permissions.canManageBlocklist;
|
|
96
|
+
// A Block Listed plate's Edit and Delete follow Manage Blocklist when the
|
|
97
|
+
// host passes it (owner, 2026-10-23); other plates keep their own boxes.
|
|
98
|
+
const blocklistGoverned =
|
|
99
|
+
type === "blocklist" && permissions.canManageBlocklist !== undefined;
|
|
100
|
+
const canEdit = blocklistGoverned ? canBlock : canUpdate;
|
|
101
|
+
const canDelete = blocklistGoverned
|
|
102
|
+
? canBlock
|
|
103
|
+
: !!permissions.canDeleteVehicle;
|
|
104
|
+
// Approve a pending Block Listed plate: Manage Blocklist AND Approve Vehicle.
|
|
105
|
+
const canApprove = blocklistGoverned
|
|
106
|
+
? canBlock && !!permissions.canApproveVehicle
|
|
107
|
+
: !!permissions.canApproveVehicle;
|
|
108
|
+
// Restore (a write, through the approve endpoint): Manage Blocklist alone.
|
|
109
|
+
const canRestore = blocklistGoverned ? canBlock : !!permissions.canApproveVehicle;
|
|
92
110
|
const actions: VehicleAction[] = [];
|
|
93
111
|
|
|
94
|
-
if (
|
|
112
|
+
if (canEdit && (isAllowed || isBlocked)) {
|
|
95
113
|
actions.push({ key: "edit", title: "Edit Vehicle", icon: "mdi-pencil" });
|
|
96
114
|
}
|
|
97
115
|
|
|
98
|
-
if (
|
|
116
|
+
if (canApprove && isPending) {
|
|
99
117
|
actions.push({
|
|
100
118
|
key: "approve",
|
|
101
119
|
title: "Approve",
|
|
@@ -109,7 +127,7 @@ export function vehiclePlateActions(
|
|
|
109
127
|
// with "You do not have permission to approve vehicles." without it. It
|
|
110
128
|
// used to be offered to everyone, so a role without the right saw Restore,
|
|
111
129
|
// pressed it and was refused - and it could not be hidden on that refusal.
|
|
112
|
-
if (
|
|
130
|
+
if (canRestore && isRestorable) {
|
|
113
131
|
actions.push({
|
|
114
132
|
key: "restore",
|
|
115
133
|
title: "Restore",
|
|
@@ -144,7 +162,7 @@ export function vehiclePlateActions(
|
|
|
144
162
|
});
|
|
145
163
|
}
|
|
146
164
|
|
|
147
|
-
if (
|
|
165
|
+
if (canDelete && (isAllowed || isBlocked)) {
|
|
148
166
|
actions.push({
|
|
149
167
|
key: "delete",
|
|
150
168
|
title: "Delete",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the screen says after a visitor is added, registered or checked in
|
|
3
|
+
* (owner, 2026-10-23): the action, then whether the ANPR barrier opened -
|
|
4
|
+
* "Opening Barrier Failed." in red. Mirrors isecure365's `visitor-barrier.ts`.
|
|
5
|
+
* The server reports `barrier: "opened" | "failed"`, absent when no barrier
|
|
6
|
+
* was asked to open, and `barrierCamera`: the camera's type and gate, "Entry
|
|
7
|
+
* Gate 1" - absent from older servers and for a camera with no gate saved.
|
|
8
|
+
*/
|
|
9
|
+
export type TBarrierOutcome = "opened" | "failed";
|
|
10
|
+
|
|
11
|
+
export const VISITOR_ACTION_SUCCESS = {
|
|
12
|
+
add: "Add Visitor Success.",
|
|
13
|
+
register: "Register Visitor Success.",
|
|
14
|
+
checkIn: "Check In Visitor Success.",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export const BARRIER_MESSAGES: Record<TBarrierOutcome, string> = {
|
|
18
|
+
opened: "Opening Barrier Success.",
|
|
19
|
+
failed: "Opening Barrier Failed.",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The barrier line: "Opening Barrier Success at Entry Gate 1." - or the plain
|
|
24
|
+
* "Opening Barrier Success." when the camera is not named.
|
|
25
|
+
*/
|
|
26
|
+
export function barrierMessage(outcome: TBarrierOutcome, camera?: string): string {
|
|
27
|
+
const plain = BARRIER_MESSAGES[outcome];
|
|
28
|
+
const name = String(camera ?? "").trim();
|
|
29
|
+
return name ? `${plain.replace(/\.$/, "")} at ${name}.` : plain;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The camera named in a server reply ("Entry Gate 1"), if it has one. */
|
|
33
|
+
export function barrierCameraOf(data: unknown): string | undefined {
|
|
34
|
+
const value = (data as { barrierCamera?: unknown } | null | undefined)?.barrierCamera;
|
|
35
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The barrier outcome in a server reply, if it has one. */
|
|
39
|
+
export function barrierOf(data: unknown): TBarrierOutcome | undefined {
|
|
40
|
+
const value = (data as { barrier?: unknown } | null | undefined)?.barrier;
|
|
41
|
+
return value === "opened" || value === "failed" ? value : undefined;
|
|
42
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ANPR camera a visit is checked at for a blocked plate and whose barrier
|
|
3
|
+
* opens for it (owner, 2026-10-23). Mirrors isecure365's
|
|
4
|
+
* `visitor-camera.helpers.ts`.
|
|
5
|
+
*
|
|
6
|
+
* Only an ACTIVE camera of type Entry, Both Entry and Exit or Visitors can do
|
|
7
|
+
* that. When a site has more than one, the user may pick a default from the
|
|
8
|
+
* visitor list (a camera icon beside the pagination); it is saved per site on
|
|
9
|
+
* their user as `defaultCameraGate: [{ site, cameraGateNumber }]` and sent as
|
|
10
|
+
* `anprGate` with add, register and guest check-in. None picked, or a gate no
|
|
11
|
+
* longer there: the server uses the site's first camera.
|
|
12
|
+
*/
|
|
13
|
+
export const VISITOR_CAMERA_DIRECTIONS = ["entry", "both", "visitors"];
|
|
14
|
+
|
|
15
|
+
const DIRECTION_LABELS: Record<string, string> = {
|
|
16
|
+
entry: "Entry",
|
|
17
|
+
both: "Both Entry and Exit",
|
|
18
|
+
visitors: "Visitors",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type TVisitorCameraChoice = { value: number; title: string };
|
|
22
|
+
|
|
23
|
+
type TCameraLike = { _id?: unknown; direction?: unknown; host?: unknown; gate?: unknown; status?: unknown };
|
|
24
|
+
|
|
25
|
+
/** The port in a camera's address; the protocol's default when none is written. */
|
|
26
|
+
export function cameraPort(host: unknown): string {
|
|
27
|
+
const text = String(host ?? "").trim();
|
|
28
|
+
if (!text) return "";
|
|
29
|
+
const withoutScheme = text.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
|
|
30
|
+
const hostPart = withoutScheme.split(/[/?#]/)[0] ?? "";
|
|
31
|
+
const port = hostPart.match(/:(\d+)$/)?.[1];
|
|
32
|
+
if (port) return port;
|
|
33
|
+
return /^https:\/\//i.test(text) ? "443" : "80";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function gateOf(camera: { gate?: unknown } | null | undefined): number | null {
|
|
37
|
+
const gate = Number(camera?.gate);
|
|
38
|
+
return Number.isInteger(gate) && gate >= 1 ? gate : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** "Entry Gate 1"; a camera with no gate saved yet: "Entry:8001". */
|
|
42
|
+
export function visitorCameraLabel(camera: TCameraLike): string {
|
|
43
|
+
const direction = String(camera?.direction ?? "").toLowerCase();
|
|
44
|
+
const type = DIRECTION_LABELS[direction] ?? direction;
|
|
45
|
+
const gate = gateOf(camera);
|
|
46
|
+
if (gate) return `${type} Gate ${gate}`;
|
|
47
|
+
const port = cameraPort(camera?.host);
|
|
48
|
+
return port ? `${type}:${port}` : type;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The site's active Entry, Both and Visitors ANPR cameras. */
|
|
52
|
+
export function visitorCameras(cameras: unknown): TCameraLike[] {
|
|
53
|
+
const list = Array.isArray(cameras) ? (cameras as TCameraLike[]) : [];
|
|
54
|
+
return list.filter(
|
|
55
|
+
(camera) =>
|
|
56
|
+
Boolean(camera?._id) &&
|
|
57
|
+
String(camera?.status ?? "active") === "active" &&
|
|
58
|
+
VISITOR_CAMERA_DIRECTIONS.includes(String(camera?.direction ?? "").toLowerCase()),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The gates that can be picked as the default ("Entry Gate 1"); a camera with no gate cannot. */
|
|
63
|
+
export function visitorGateChoices(cameras: unknown): TVisitorCameraChoice[] {
|
|
64
|
+
return visitorCameras(cameras)
|
|
65
|
+
.filter((camera) => gateOf(camera) !== null)
|
|
66
|
+
.map((camera) => ({ value: gateOf(camera) as number, title: visitorCameraLabel(camera) }))
|
|
67
|
+
.sort((a, b) => a.value - b.value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The user's default gate at this site, or undefined. */
|
|
71
|
+
export function defaultGateForSite(saved: unknown, siteId: unknown): number | undefined {
|
|
72
|
+
if (!siteId || !Array.isArray(saved)) return undefined;
|
|
73
|
+
const entry = (saved as Array<{ site?: unknown; cameraGateNumber?: unknown }>).find(
|
|
74
|
+
(item) => String(item?.site ?? "") === String(siteId),
|
|
75
|
+
);
|
|
76
|
+
const gate = Number(entry?.cameraGateNumber);
|
|
77
|
+
return Number.isInteger(gate) && gate >= 1 ? gate : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const VISITOR_CAMERA_LABEL = "Default ANPR Camera";
|