@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.
@@ -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.0",
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
@@ -28,5 +28,7 @@ declare type TSiteCamera = {
28
28
  direction: "entry" | "exit" | "both" | "none";
29
29
  name?: string;
30
30
  guardPost: number | null;
31
+ /** ANPR cameras: the gate (1-20) it watches; one camera per gate at a site. */
32
+ gate?: number | null;
31
33
  status: string;
32
34
  };
@@ -12,8 +12,11 @@ export const isAllowCameraDirection = (direction: unknown): boolean =>
12
12
  String(direction ?? "").trim().toLowerCase(),
13
13
  );
14
14
 
15
- /** ANPR camera types that carry blocked plates: every type except Exit. */
16
- export const BLOCK_CAMERA_DIRECTIONS = ["entry", "both", "residents", "visitors"];
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
- * - To Exit: the allowed list and the blocklist.
52
- * - Entry / Both / Residents to Visitors: the allowed list.
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,
@@ -27,7 +27,11 @@ export type VehicleActionPermissions = {
27
27
  canUpdateVehicle?: boolean;
28
28
  canDeleteVehicle?: boolean;
29
29
  canApproveVehicle?: boolean;
30
- /** Block / Unblock. Omitted = allowed (today's behaviour). */
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 (canUpdate && (isAllowed || isBlocked)) {
112
+ if (canEdit && (isAllowed || isBlocked)) {
95
113
  actions.push({ key: "edit", title: "Edit Vehicle", icon: "mdi-pencil" });
96
114
  }
97
115
 
98
- if (permissions.canApproveVehicle && isPending) {
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 (permissions.canApproveVehicle && isRestorable) {
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 (permissions.canDeleteVehicle && (isAllowed || isBlocked)) {
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";