@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.
- package/components/EntryPassInformation.vue +13 -0
- package/components/Robot/Setup.vue +2 -0
- package/components/Robot/SiteFleet.vue +2 -0
- package/components/Robot/SiteSharedSpace.vue +89 -0
- package/components/VisitorForm.vue +26 -2
- package/composables/useRobots.ts +8 -0
- package/composables/useSiteFeatureSettings.ts +1 -0
- package/package.json +1 -1
- package/utils/hid-qr-validity.ts +14 -0
- package/utils/robot-shared-space.ts +26 -0
|
@@ -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,
|
package/composables/useRobots.ts
CHANGED
|
@@ -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,
|
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.
|
|
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.",
|
package/utils/hid-qr-validity.ts
CHANGED
|
@@ -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
|
+
}
|