@7365admin1/layer-common 4.75.0 → 4.76.0
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 +16 -4
- package/assets/css/robot-hero.css +1 -0
- package/components/MemberMain.vue +6 -3
- package/components/Robot/Detail.vue +13 -0
- package/package.json +1 -1
- package/utils/robot-display.ts +68 -0
- package/utils/role.ts +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.76.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 5fafd9a: Robot detail: a cleaning robot whose brand reports them (Ounitech) now shows Clean water and Dirty water (Low / Medium / High with the %), a "Tanks and covers" line ("Tanks and covers OK", or which cover is open / tank not fitted) and, while it cleans, "This job" (% done and m² cleaned of planned). Warning tags for clean water low, dirty water full and any open cover or missing tank. Nothing is shown for brands that do not report them. The rule (`cleaningReadings`) is part of the robot rules synced to the mobile apps; "What this robot can do" lists "water levels and tanks".
|
|
8
|
+
|
|
9
|
+
## 4.75.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- b84da6e: Organization Members: keep the "Fix roles from other sites" banner at organisation level, worded for the organisation ("N members hold a role created for one particular site…"), without the per-site "Show them" filter. 4.75.0 had hidden the whole banner there.
|
|
14
|
+
|
|
3
15
|
## 4.75.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -1902,11 +1914,11 @@
|
|
|
1902
1914
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
1903
1915
|
own rule from two endpoints the console already calls, unprojected:
|
|
1904
1916
|
|
|
1905
|
-
|
|
1906
|
-
|
|
1917
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
1918
|
+
GET /api/roles/id/:role that membership's role document
|
|
1907
1919
|
|
|
1908
|
-
|
|
1909
|
-
|
|
1920
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
1921
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
1910
1922
|
|
|
1911
1923
|
`role.default` is the marker because it is the only property of a platform
|
|
1912
1924
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -806,6 +806,7 @@
|
|
|
806
806
|
.hero-strip { grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); }
|
|
807
807
|
.hero-strip .hero-fact { padding: 8px 12px; }
|
|
808
808
|
.hero-strip .hero-fact__value { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
809
|
+
.hero-cleaning-warnings { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0; }
|
|
809
810
|
.hero-detail__main { display: grid; grid-template-columns: minmax(0, 11fr) minmax(0, 9fr); gap: 20px; align-items: start; }
|
|
810
811
|
.hero-detail__main--single { grid-template-columns: minmax(0, 1fr); }
|
|
811
812
|
.hero-detail__cam, .hero-detail__map { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
their access; the company assigns each one a role for this site.
|
|
32
32
|
Shown while any remain (server count), gone at 0. -->
|
|
33
33
|
<v-alert
|
|
34
|
-
v-if="props.assignRole && otherSiteBanner
|
|
34
|
+
v-if="props.assignRole && otherSiteBanner"
|
|
35
35
|
type="warning"
|
|
36
36
|
variant="tonal"
|
|
37
37
|
density="compact"
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
<div class="d-flex align-center flex-wrap ga-2">
|
|
42
42
|
<span class="flex-grow-1">{{ otherSiteBanner }}</span>
|
|
43
43
|
<AppButton
|
|
44
|
+
v-if="!orgLevel"
|
|
44
45
|
data-test="member-other-site-filter"
|
|
45
46
|
@click="toggleOtherSite()"
|
|
46
47
|
>
|
|
@@ -578,7 +579,7 @@ const {
|
|
|
578
579
|
const { data: otherSiteReq, refresh: refreshOtherSite } = useAsyncData(
|
|
579
580
|
`members-other-site-${props.type}-${props.orgId}-${props.siteId}-${props.status}`,
|
|
580
581
|
() =>
|
|
581
|
-
props.assignRole && props.orgId
|
|
582
|
+
props.assignRole && props.orgId
|
|
582
583
|
? _getAll({
|
|
583
584
|
status: props.status,
|
|
584
585
|
org: props.orgId,
|
|
@@ -591,8 +592,10 @@ const { data: otherSiteReq, refresh: refreshOtherSite } = useAsyncData(
|
|
|
591
592
|
: Promise.resolve(null),
|
|
592
593
|
{ watch: [() => props.status, () => props.orgId, () => props.siteId] }
|
|
593
594
|
);
|
|
595
|
+
// At organisation level there is no "this site": the count says what is wrong
|
|
596
|
+
// and points to "Fix roles from other sites" (no per-site filter there).
|
|
594
597
|
const otherSiteBanner = computed(() =>
|
|
595
|
-
otherSiteBannerText(otherSiteReq.value?.total)
|
|
598
|
+
otherSiteBannerText(otherSiteReq.value?.total, orgLevel.value)
|
|
596
599
|
);
|
|
597
600
|
watch(otherSiteBanner, (text) => {
|
|
598
601
|
// the last one fixed: back to the full list
|
|
@@ -56,6 +56,17 @@
|
|
|
56
56
|
</div>
|
|
57
57
|
</div>
|
|
58
58
|
|
|
59
|
+
<!-- A scrubber's water, tanks and current job (brands that report them); warnings as tags. -->
|
|
60
|
+
<div v-if="cleaning.warnings.length" class="hero-cleaning-warnings" role="status" aria-label="Needs attention">
|
|
61
|
+
<span v-for="w in cleaning.warnings" :key="w" class="hero-tag hero-tag--outline">{{ w }}</span>
|
|
62
|
+
</div>
|
|
63
|
+
<div v-if="cleaning.facts.length" class="hero-facts hero-strip" role="list" aria-label="Water and tanks">
|
|
64
|
+
<div v-for="f in cleaning.facts" :key="f.label" class="hero-fact" role="listitem">
|
|
65
|
+
<div class="hero-fact__label">{{ f.label }}</div>
|
|
66
|
+
<div class="hero-fact__value" :title="f.value">{{ f.value }}</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
59
70
|
<!-- Above the fold: camera | map, with the commands under the map they act on. -->
|
|
60
71
|
<div v-if="!noInterface" class="hero-detail__main" :class="{ 'hero-detail__main--single': !hasCamera }">
|
|
61
72
|
<section v-if="hasCamera" class="hero-detail__cam" aria-label="Camera">
|
|
@@ -305,6 +316,7 @@ import {
|
|
|
305
316
|
robotAbilities,
|
|
306
317
|
robotErrorMessage,
|
|
307
318
|
robotFacts,
|
|
319
|
+
cleaningReadings,
|
|
308
320
|
robotStatus,
|
|
309
321
|
showSpotClean,
|
|
310
322
|
sinceText,
|
|
@@ -420,6 +432,7 @@ const stripFacts = computed(() =>
|
|
|
420
432
|
.filter((f): f is NonNullable<typeof f> => !!f)
|
|
421
433
|
.map((f) => (f.label === "Robot reports" ? { ...f, label: "Doing now" } : f)),
|
|
422
434
|
);
|
|
435
|
+
const cleaning = computed(() => cleaningReadings(live.value as any));
|
|
423
436
|
const identity = computed(() => {
|
|
424
437
|
const model = facts.value.find((f) => f.label === "Model")?.value;
|
|
425
438
|
const serial = facts.value.find((f) => f.label === "Serial")?.value;
|
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.76.0",
|
|
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/robot-display.ts
CHANGED
|
@@ -626,6 +626,8 @@ export type TLiveRobotState = {
|
|
|
626
626
|
capabilities?: Record<string, unknown>;
|
|
627
627
|
/** Plain-words facts for this brand + model (core `robotManifest`). */
|
|
628
628
|
profile?: TRobotProfile;
|
|
629
|
+
/** A scrubber's water, tank and job readings (core `TRobotCleaning`, brands with `waterTanks`). */
|
|
630
|
+
cleaning?: TRobotCleaning | null;
|
|
629
631
|
lastSeenAt?: string | null;
|
|
630
632
|
/** Epoch ms of the last answer that was read (set by `mergeLiveStates`). */
|
|
631
633
|
readAt?: number;
|
|
@@ -1358,6 +1360,71 @@ export function lastKnownLabel(at: unknown, now: number = Date.now()): string {
|
|
|
1358
1360
|
|
|
1359
1361
|
export type TRobotFact = { label: string; value: string; dim: boolean };
|
|
1360
1362
|
|
|
1363
|
+
/** Core's `TRobotCleaning`: levels are "low" | "medium" | "high"; parts ok=false = open / not fitted. */
|
|
1364
|
+
export type TRobotCleaning = {
|
|
1365
|
+
cleanWater?: { level?: string; percent?: number | null } | null;
|
|
1366
|
+
dirtyWater?: { level?: string; percent?: number | null } | null;
|
|
1367
|
+
parts?: Array<{ part?: string; ok?: boolean }> | null;
|
|
1368
|
+
job?: { percent?: number | null; areaDone?: number | null; areaPlanned?: number | null } | null;
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
export type TCleaningReadings = {
|
|
1372
|
+
/** Clean water · Dirty water · Tanks and covers · Job - only the ones the robot reported. */
|
|
1373
|
+
facts: TRobotFact[];
|
|
1374
|
+
/** Warning chips: "Clean water low", "Dirty water full", "Top cover open". */
|
|
1375
|
+
warnings: string[];
|
|
1376
|
+
};
|
|
1377
|
+
|
|
1378
|
+
const LEVEL_TEXT: Readonly<Record<string, string>> = { low: "Low", medium: "Medium", high: "High" };
|
|
1379
|
+
const PART_PROBLEM: Readonly<Record<string, string>> = {
|
|
1380
|
+
"top cover": "Top cover open",
|
|
1381
|
+
"clean-water tank": "Clean-water tank not fitted",
|
|
1382
|
+
"dirty-water tank": "Dirty-water tank not fitted",
|
|
1383
|
+
"dust box": "Dust box not fitted",
|
|
1384
|
+
};
|
|
1385
|
+
const m2 = (n: number) => `${Number.isInteger(n) ? n : n.toFixed(1)} m²`;
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* A cleaning robot's water, tanks and current job in plain words, for the
|
|
1389
|
+
* detail screen (web and phones). Capability-driven: nothing unless the brand +
|
|
1390
|
+
* model reports `waterTanks` and the robot answered online; a reading the robot
|
|
1391
|
+
* did not send is left out (hide, never grey).
|
|
1392
|
+
*
|
|
1393
|
+
* Clean water low and dirty water full (high) are warnings, as is any open
|
|
1394
|
+
* cover or missing tank. The job line shows only while it cleans.
|
|
1395
|
+
*/
|
|
1396
|
+
export function cleaningReadings(live: TLiveRobotState | null | undefined): TCleaningReadings {
|
|
1397
|
+
const none: TCleaningReadings = { facts: [], warnings: [] };
|
|
1398
|
+
if (!live?.ok || live.online === false || live.capabilities?.waterTanks !== true) return none;
|
|
1399
|
+
const c = live.cleaning;
|
|
1400
|
+
if (!c || typeof c !== "object") return none;
|
|
1401
|
+
const facts: TRobotFact[] = [];
|
|
1402
|
+
const warnings: string[] = [];
|
|
1403
|
+
const water = (label: string, w: TRobotCleaning["cleanWater"]) => {
|
|
1404
|
+
const text = LEVEL_TEXT[String(w?.level ?? "")];
|
|
1405
|
+
if (!text) return "";
|
|
1406
|
+
const pct = finite(w?.percent) ? ` · ${Math.round(w!.percent as number)}%` : "";
|
|
1407
|
+
facts.push({ label, value: `${text}${pct}`, dim: false });
|
|
1408
|
+
return String(w!.level);
|
|
1409
|
+
};
|
|
1410
|
+
if (water("Clean water", c.cleanWater) === "low") warnings.push("Clean water low");
|
|
1411
|
+
if (water("Dirty water", c.dirtyWater) === "high") warnings.push("Dirty water full");
|
|
1412
|
+
const parts = (Array.isArray(c.parts) ? c.parts : []).filter((p) => p && typeof p.part === "string" && typeof p.ok === "boolean");
|
|
1413
|
+
if (parts.length) {
|
|
1414
|
+
const problems = parts.filter((p) => !p.ok).map((p) => PART_PROBLEM[p.part as string] ?? `${firstUp(String(p.part))} not fitted`);
|
|
1415
|
+
facts.push({ label: "Tanks and covers", value: problems.length ? problems.join(" · ") : "Tanks and covers OK", dim: false });
|
|
1416
|
+
warnings.push(...problems);
|
|
1417
|
+
}
|
|
1418
|
+
const j = c.job;
|
|
1419
|
+
if (j && typeof j === "object") {
|
|
1420
|
+
const bits: string[] = [];
|
|
1421
|
+
if (finite(j.percent)) bits.push(`${Math.round(j.percent as number)}% done`);
|
|
1422
|
+
if (finite(j.areaDone)) bits.push(finite(j.areaPlanned) ? `${m2(j.areaDone as number)} of ${m2(j.areaPlanned as number)}` : `${m2(j.areaDone as number)} cleaned`);
|
|
1423
|
+
if (bits.length) facts.push({ label: "This job", value: bits.join(" · "), dim: false });
|
|
1424
|
+
}
|
|
1425
|
+
return { facts, warnings };
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1361
1428
|
/**
|
|
1362
1429
|
* The detail screen's facts grid (README §3): Connection · Battery · Floor ·
|
|
1363
1430
|
* Live position · Robot reports · Model · Serial · Last seen. A fact the brand
|
|
@@ -1625,6 +1692,7 @@ export function robotAbilities(input: {
|
|
|
1625
1692
|
on("floorReporting") && "floor",
|
|
1626
1693
|
on("readLocation") && "live position",
|
|
1627
1694
|
on("thermalCamera") && "heat readings",
|
|
1695
|
+
on("waterTanks") && "water levels and tanks",
|
|
1628
1696
|
on("patrolHistory") && (on("thermalCamera") ? "past patrols with photos and temperatures" : "past patrols with photos"),
|
|
1629
1697
|
].filter(Boolean) as string[];
|
|
1630
1698
|
const p = input?.profile ?? {};
|
package/utils/role.ts
CHANGED
|
@@ -82,9 +82,14 @@ export function memberRoleChip(
|
|
|
82
82
|
* re-assign a role for the right site, nobody's access is cut). `null` at 0 so
|
|
83
83
|
* the banner disappears once every member here has this site's role.
|
|
84
84
|
*/
|
|
85
|
-
export function otherSiteBannerText(count: unknown): string | null {
|
|
85
|
+
export function otherSiteBannerText(count: unknown, orgLevel = false): string | null {
|
|
86
86
|
const n = typeof count === "number" && Number.isFinite(count) ? Math.floor(count) : 0;
|
|
87
87
|
if (n <= 0) return null;
|
|
88
|
+
if (orgLevel) {
|
|
89
|
+
return n === 1
|
|
90
|
+
? "1 member holds a role created for one particular site. Fix roles from other sites gives them the right role."
|
|
91
|
+
: `${n} members hold a role created for one particular site. Fix roles from other sites gives each one the right role.`;
|
|
92
|
+
}
|
|
88
93
|
return n === 1
|
|
89
94
|
? "1 member here has a role from another site. Assign them a role for this site."
|
|
90
95
|
: `${n} members here have a role from another site. Assign each one a role for this site.`;
|