@7365admin1/layer-common 4.97.1-staging.571 → 4.97.1-staging.573

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.
@@ -206,6 +206,13 @@
206
206
  </div>
207
207
  </div>
208
208
  </v-card>
209
+ <!-- How long the pass lasts, from the site's setting; the same words
210
+ as the resident app's invite form. -->
211
+ <div v-if="hidQrUsage" class="text-caption text-primary font-weight-medium mt-2">
212
+ {{ hidQrUsage === "singleUse"
213
+ ? "HID: Single use, expires after the first entry"
214
+ : "HID: Valid all day, until 11:59 pm" }}
215
+ </div>
209
216
  <v-alert
210
217
  v-if="!hidPortalName"
211
218
  type="warning"
@@ -341,6 +348,12 @@ const props = defineProps({
341
348
  type: String,
342
349
  default: "",
343
350
  },
351
+ // The site's HID pass usage, for the hint under the HID QR Code card. Null
352
+ // shows no hint (not loaded, or a caller that does not pass it).
353
+ hidQrUsage: {
354
+ type: String as PropType<"wholeDay" | "singleUse" | null>,
355
+ default: null,
356
+ },
344
357
  excludedCardIds: {
345
358
  type: Array as PropType<string[]>,
346
359
  default: () => [],
@@ -55,6 +55,7 @@
55
55
  <RobotSiteFloors :site="site" :robots="all" :states="states as Record<string, Record<string, any>>" :map-floors="mapFloors" @saved="floorsSaved" />
56
56
  </div>
57
57
  <RobotSiteFleetCore :site="site" />
58
+ <RobotSiteSharedSpace :site="site" editable />
58
59
  </template>
59
60
 
60
61
  <script setup lang="ts">
@@ -62,6 +63,7 @@ import { computed, ref, toRef, watch } from "vue";
62
63
  import RobotFleetCoreButton from "./FleetCoreButton.vue";
63
64
  import RobotSiteFloors from "./SiteFloors.vue";
64
65
  import RobotSiteFleetCore from "./SiteFleetCore.vue";
66
+ import RobotSiteSharedSpace from "./SiteSharedSpace.vue";
65
67
  import useRobotBuilding from "../../composables/useRobotBuilding";
66
68
  import useRobotLive, { siteStatesFeed } from "../../composables/useRobotLive";
67
69
  import { robotSetupSteps, setupProgress, STATUS_TEXT, type TSetupStatus, type TSetupTarget } from "../../utils/robot-setup";
@@ -15,6 +15,7 @@
15
15
  </div>
16
16
 
17
17
  <RobotSummaryStrip :key="`${site}:${type}`" class="hero-summary--six" :cells="cells" label="Site summary" />
18
+ <RobotSiteSharedSpace :key="`ss:${site}`" :site="site" />
18
19
 
19
20
  <div class="hero-split">
20
21
  <div class="hero-split__main hero-split__main--fleet">
@@ -85,6 +86,7 @@
85
86
  <script setup lang="ts">
86
87
  import { computed, onBeforeUnmount, ref, watch } from "vue";
87
88
  import RobotSummaryStrip from "./SummaryStrip.vue";
89
+ import RobotSiteSharedSpace from "./SiteSharedSpace.vue";
88
90
  import RobotTable from "./Table.vue";
89
91
  import RobotStatusChip from "./StatusChip.vue";
90
92
  import RobotBattery from "./Battery.vue";
@@ -0,0 +1,89 @@
1
+ <template>
2
+ <!--
3
+ ROBOTS "SHARED SPACE" (owner 2026-10-19): one status line on the site robots page for everyone at the site,
4
+ plus the Super Admin switch on the Setup page (`editable`). Off (default) = one robot moves at a time.
5
+ On = both robots may move while HERO Fleet Core says it is coordinating them (it stops the security robot
6
+ near the cleaning robot); without that word the site falls back to one robot at a time.
7
+ Server: GET /api/robots/sites/:site/coordination; switch = site feature setting `robotSharedSpace`.
8
+ -->
9
+ <section class="ssp" :class="{ 'ssp--panel': editable }" aria-label="Robot coordination">
10
+ <h3 v-if="editable" class="hero-title ssp__title">Shared space (HERO Fleet Core coordinates)</h3>
11
+ <div class="ssp__line" :class="`hero-tone--${TONE[line.tone]}`" role="status" data-test="shared-space-line">{{ line.text }}</div>
12
+ <template v-if="editable && loaded">
13
+ <v-switch v-model="enabled" color="primary" density="compact" hide-details inset label="Let both robots work at the same time" />
14
+ <p class="hero-muted ssp__note">
15
+ HERO Fleet Core stops the security robot when it comes within about 1.2 m of the cleaning robot and restarts it
16
+ when clear. It cannot stop the cleaning robot: that robot's own sensors and no-go zones still keep it away.
17
+ Switch on only after the on-site calibration and test. Stop, return and emergency stop always work.
18
+ </p>
19
+ <div class="ssp__act">
20
+ <v-btn size="small" color="primary" variant="flat" :loading="saving" :disabled="!loaded || saving || enabled === saved" @click="save">Save</v-btn>
21
+ </div>
22
+ <div v-if="message" class="hero-muted" role="status">{{ message }}</div>
23
+ <div v-if="error" class="hero-notice hero-notice--error" role="alert">{{ error }}</div>
24
+ </template>
25
+ </section>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { computed, onBeforeUnmount, ref, watch } from "vue";
30
+ import { robotErrorMessage } from "../../utils/robot-display";
31
+ import { sharedSpaceLine, type TCoordination } from "../../utils/robot-shared-space";
32
+ import useSiteFeatureSettings from "../../composables/useSiteFeatureSettings";
33
+
34
+ const props = withDefaults(defineProps<{ site: string; editable?: boolean }>(), { editable: false });
35
+ const { getSiteCoordination } = useRobots();
36
+ const { saveSiteFeature } = useSiteFeatureSettings();
37
+ const TONE = { neutral: "neutral", ok: "success", warn: "warning" } as const;
38
+
39
+ const status = ref<TCoordination | null>(null);
40
+ const loaded = ref(false);
41
+ const saved = ref(false);
42
+ const enabled = ref(false);
43
+ const saving = ref(false);
44
+ const message = ref("");
45
+ const error = ref("");
46
+ const line = computed(() => sharedSpaceLine(status.value));
47
+
48
+ async function load() {
49
+ try {
50
+ const res = await getSiteCoordination(props.site);
51
+ status.value = res;
52
+ if (!loaded.value || saved.value !== res.sharedSpace) {
53
+ saved.value = res.sharedSpace;
54
+ if (!saving.value) enabled.value = res.sharedSpace;
55
+ }
56
+ loaded.value = true;
57
+ } catch {
58
+ // An older server has no coordination route: say nothing rather than a wrong line.
59
+ status.value = { sharedSpace: false, coordinating: false };
60
+ }
61
+ }
62
+
63
+ async function save() {
64
+ error.value = message.value = "";
65
+ saving.value = true;
66
+ try {
67
+ await saveSiteFeature(props.site, "robotSharedSpace", { enabled: enabled.value });
68
+ message.value = enabled.value ? "Saved. Both robots may work together while HERO Fleet Core is coordinating." : "Saved. One robot moves at a time.";
69
+ await load();
70
+ } catch (e) {
71
+ error.value = robotErrorMessage(e, "Could not save the shared space setting.");
72
+ } finally {
73
+ saving.value = false;
74
+ }
75
+ }
76
+
77
+ const timer = setInterval(load, 10_000);
78
+ onBeforeUnmount(() => clearInterval(timer));
79
+ watch(() => props.site, () => { loaded.value = false; load(); }, { immediate: true });
80
+ </script>
81
+
82
+ <style scoped>
83
+ .ssp { margin: 8px 0; display: grid; gap: 6px; }
84
+ .ssp--panel { margin-top: 16px; padding: 12px; background: var(--hero-surface); box-shadow: var(--hero-shadow-sm); }
85
+ .ssp__title { font-size: 16px; }
86
+ .ssp__line { font-size: 13px; padding: 6px 10px; border-radius: 6px; overflow-wrap: anywhere; }
87
+ .ssp__note { font-size: 12px; margin: 0; }
88
+ .ssp__act { display: flex; gap: 6px; }
89
+ </style>
@@ -254,6 +254,7 @@
254
254
  :loading="entryPassSettingsPending || siteDataPending" :site-id="prop.site"
255
255
  :unit-id="visitor.unit || null" :visitor-type="prop.type" :hid-qr-code-enabled="hidQrCodePassAllowed"
256
256
  :hid-qr-printer-configured="hasHidQrPrinterConfig"
257
+ :hid-qr-usage="hidQrUsage"
257
258
  :hid-reader-name="hidQrReader?.name || ''"
258
259
  :hid-portal-name="hidQrReader?.portalName || ''"
259
260
  @update:available-qr-count="(val) => availableQrCount = val" />
@@ -622,7 +623,7 @@ import useSiteEntryPassSettings from "../composables/useSiteEntryPassSettings";
622
623
  import useAccessManagement from "../composables/useAccessManagement";
623
624
  import usePeople from "../composables/usePeople";
624
625
  import useHidAmico from "../composables/useHidAmico";
625
- import { minutesUntilEndOfDay } from "../utils/hid-qr-validity";
626
+ import { hidQrUsageFromSettings, minutesUntilEndOfDay } from "../utils/hid-qr-validity";
626
627
 
627
628
  const prop = defineProps({
628
629
  type: {
@@ -659,7 +660,7 @@ type AutofillSource = "nric" | "contact" | "vehicleNumber" | null;
659
660
  const currentAutofillSource = ref<AutofillSource>(null);
660
661
 
661
662
  const { requiredRule, debounce, UTCToLocalTIme, maskNRIC } = useUtils();
662
- const { getSiteById } = useSiteSettings();
663
+ const { getSiteById, getOvernightParkingAvailabilityV2 } = useSiteSettings();
663
664
  const { currentUser } = useLocalAuth();
664
665
  const { getBlocksSelection, getLevelsSelection, getUnitSelection } =
665
666
  useBuilding();
@@ -979,6 +980,12 @@ const hidQrCodePassAllowed = computed(() => {
979
980
  );
980
981
  });
981
982
 
983
+ // The site's Whole day / Single usage choice for HID visitor passes, shown
984
+ // under the HID QR Code card as the resident app's invite form shows it.
985
+ // Null until read, and when it cannot be read: then no hint, not a guess.
986
+ // Loaded by the watcher below `siteData`.
987
+ const hidQrUsage = ref<"wholeDay" | "singleUse" | null>(null);
988
+
982
989
  const showEntryPassInformation = computed(() => {
983
990
  return Boolean(
984
991
  entryPassSettings.value?.data?.settings?.nfcPass ||
@@ -1591,6 +1598,23 @@ watch(
1591
1598
  { immediate: true },
1592
1599
  );
1593
1600
 
1601
+ // Below `siteData` for the same reason as the reader watcher above:
1602
+ // `hidQrCodePassAllowed` reads it. Only fetched where HID QR is offered.
1603
+ watch(
1604
+ [hidQrCodePassAllowed, () => prop.site],
1605
+ async ([allowed, site]) => {
1606
+ hidQrUsage.value = null;
1607
+ if (!allowed || !site) return;
1608
+ try {
1609
+ const settings = await getOvernightParkingAvailabilityV2(String(site), "site");
1610
+ hidQrUsage.value = hidQrUsageFromSettings(settings);
1611
+ } catch {
1612
+ hidQrUsage.value = null;
1613
+ }
1614
+ },
1615
+ { immediate: true },
1616
+ );
1617
+
1594
1618
  const {
1595
1619
  data: blocksListData,
1596
1620
  refresh: refreshBlockListData,
@@ -652,6 +652,13 @@ export default function useRobots() {
652
652
  function undoSiteFloors(site: string, version: number) {
653
653
  return useNuxtApp().$api<Record<string, any>>(`${sitePath(site)}/floors/undo`, { method: "POST", query: { appSlug }, body: { version } });
654
654
  }
655
+ /** Shared space status line: the site switch + whether HERO Fleet Core coordinates the robots now (anyone at the site). */
656
+ function getSiteCoordination(site: string) {
657
+ return useNuxtApp().$api<{ sharedSpace: boolean; coordinating: boolean; reason: string; separationM: number | null; held: boolean; at: string | null }>(
658
+ `${sitePath(site)}/coordination`,
659
+ { query: { appSlug } },
660
+ );
661
+ }
655
662
  function getSiteFleetCore(site: string) {
656
663
  return useNuxtApp().$api<Record<string, any>>(`${sitePath(site)}/fleet-core`, { query: { appSlug } });
657
664
  }
@@ -666,6 +673,7 @@ export default function useRobots() {
666
673
  getSiteFloors,
667
674
  saveSiteFloors,
668
675
  undoSiteFloors,
676
+ getSiteCoordination,
669
677
  getSiteFleetCore,
670
678
  saveSiteFleetCore,
671
679
  createSiteRelayKey,
@@ -16,6 +16,7 @@ export const SITE_FEATURE_KEYS = [
16
16
  "dataRetention",
17
17
  "maStaffRegistration",
18
18
  "visitorPassSms",
19
+ "robotSharedSpace",
19
20
  ] as const;
20
21
 
21
22
  export type TSiteFeatureKey = (typeof SITE_FEATURE_KEYS)[number];
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.571",
5
+ "version": "4.97.1-staging.573",
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.",
@@ -4,6 +4,20 @@
4
4
  * (`minutesUntilEndOfDay` in its VisitorForm.tsx). Never less than 1: the API
5
5
  * refuses a zero validity.
6
6
  */
7
+ /**
8
+ * How long the site's HID visitor pass lasts: the `usage` on the HID entry of
9
+ * `visitorQrCodes` (Site Settings > Resident Visitors). Anything but
10
+ * "singleUse" is the whole visit day - the same reading as core's
11
+ * `getHidVisitorQrUsage` and the resident app's invite form.
12
+ */
13
+ export function hidQrUsageFromSettings(settings: unknown): "wholeDay" | "singleUse" {
14
+ const codes = (settings as { visitorQrCodes?: unknown } | null)?.visitorQrCodes;
15
+ const hid = Array.isArray(codes)
16
+ ? codes.find((code) => (code as { type?: unknown } | null)?.type === "hid")
17
+ : undefined;
18
+ return (hid as { usage?: unknown } | undefined)?.usage === "singleUse" ? "singleUse" : "wholeDay";
19
+ }
20
+
7
21
  export function minutesUntilEndOfDay(now: Date = new Date()) {
8
22
  const midnight = new Date(now);
9
23
  midnight.setHours(24, 0, 0, 0);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Robots "Shared space" status line (core `GET /api/robots/sites/:site/coordination`, 2026-10-19).
3
+ * Off = one robot moves at a time (the 15 Oct rule). On = both may move while HERO Fleet Core
4
+ * says it is coordinating them; otherwise the site falls back to one robot at a time.
5
+ */
6
+ export type TCoordination = {
7
+ sharedSpace: boolean;
8
+ coordinating: boolean;
9
+ reason?: string;
10
+ separationM?: number | null;
11
+ held?: boolean;
12
+ };
13
+
14
+ export function sharedSpaceLine(c: TCoordination | null | undefined): { text: string; tone: "neutral" | "ok" | "warn" } {
15
+ if (!c) return { text: "Robot coordination: checking…", tone: "neutral" };
16
+ if (!c.sharedSpace) return { text: "One robot moves at a time at this site.", tone: "neutral" };
17
+ if (!c.coordinating) {
18
+ return {
19
+ text: `Shared space is on, but HERO Fleet Core is not coordinating now${c.reason ? ` (${c.reason})` : ""} - one robot moves at a time.`,
20
+ tone: "warn",
21
+ };
22
+ }
23
+ const apart = typeof c.separationM === "number" ? ` - robots ${c.separationM.toFixed(1)} m apart` : "";
24
+ const held = c.held ? " · security robot held until the cleaning robot is clear" : "";
25
+ return { text: `Shared space: HERO Fleet Core is coordinating both robots${apart}${held}.`, tone: "ok" };
26
+ }