@7365admin1/layer-common 4.49.0 → 4.51.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 +71 -6
- package/components/MemberMain.vue +61 -3
- package/components/Robot/BuildingMap.vue +78 -26
- package/components/Robot/MapEditor.vue +890 -175
- package/components/Robot/MiniMap.vue +16 -1
- package/components/Robot/SiteFleet.vue +23 -1
- package/composables/useMember.ts +3 -0
- package/composables/useRobots.ts +15 -1
- package/package.json +2 -2
- package/utils/rmf-editor.ts +756 -0
- package/utils/robot-building.ts +692 -170
- package/utils/robot-floorplan.ts +9 -8
- package/utils/role.ts +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.51.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ac247e2: Members: a banner "N members here have a role from another site. Assign each one a role for this site." with a one-click filter to list them (server `roleState=other-site`); disappears at 0. `useMember().getAll` accepts `roleState`.
|
|
8
|
+
|
|
9
|
+
## 4.50.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- d7f1732: HERO map editor - Fleet Core (Open-RMF) parity: Waypoint, Lane, Fiducial and Measure tools; lanes on route graphs 0-8 (graph picker, names, robot brands), two-way / one-way, speed limit, facing, shared-space group; waypoint settings (charger, parking, holding, pass-through, cleaning zone, dock name, spawn, pick-up / drop-off, merge radius); waypoints drag their lanes, deleting one deletes its lanes; the four door types with hinge / swing / degrees / leaf ratio; lift shafts at any angle with cabin doors per floor (which door opens where) and size fields; fiducial level alignment and scale by measurement (plan and drawing); plain-language checks (duplicate names, lanes through walls, charger without a dock, lanes that lost a waypoint, lift floors with no door); zoom and pan; the drawing area is the building's real size (plans larger than 40 x 26 m are no longer cut off; turned plans); import a building file or a route map into a graph. The editor now saves the map's `rmf` part. The 3D view draws door types, cabin doors, turned lifts, the building's real shape and a Routes toggle; the mini-map shows robot lanes.
|
|
14
|
+
|
|
3
15
|
## 4.49.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -1533,11 +1545,11 @@
|
|
|
1533
1545
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
1534
1546
|
own rule from two endpoints the console already calls, unprojected:
|
|
1535
1547
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1548
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
1549
|
+
GET /api/roles/id/:role that membership's role document
|
|
1538
1550
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1551
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
1552
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
1541
1553
|
|
|
1542
1554
|
`role.default` is the marker because it is the only property of a platform
|
|
1543
1555
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -741,6 +741,13 @@
|
|
|
741
741
|
.hero-mini__foot { font-size: 11px; opacity: 0.6; display: flex; gap: 12px; white-space: nowrap; }
|
|
742
742
|
.hero-mini__foot > span:first-child { overflow: hidden; text-overflow: ellipsis; }
|
|
743
743
|
.hero-mini__legend { display: inline-block; width: 12px; height: 0; border-top: 2px dashed var(--hero-accent); vertical-align: middle; margin-right: 5px; }
|
|
744
|
+
.hero-mini__legend--lane { border-top-style: solid; border-top-color: rgb(var(--v-theme-info)); }
|
|
745
|
+
.hero-mini__toggle { font: inherit; font-size: 10.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; padding: 4px 8px; min-height: 28px; color: var(--hero-text); background: transparent; border: 1px solid var(--hero-divider); cursor: pointer; }
|
|
746
|
+
.hero-mini__toggle[aria-pressed="true"] { background: var(--hero-text); color: var(--hero-bg); }
|
|
747
|
+
.hero-mini__lane { stroke: rgb(var(--v-theme-info)); stroke-width: 1.5; opacity: 0.8; }
|
|
748
|
+
.hero-mini__lane--oneway { stroke-dasharray: 5 3; }
|
|
749
|
+
.hero-mini__wp { position: absolute; width: 6px; height: 6px; margin: -3px 0 0 -3px; border-radius: 50%; background: rgb(var(--v-theme-info)); outline: 1.5px solid var(--hero-bg); pointer-events: none; }
|
|
750
|
+
.hero-mini__wp--charger { background: rgb(var(--v-theme-success)); border-radius: 1px; }
|
|
744
751
|
.hero-mini__count { margin-left: auto; font-variant-numeric: tabular-nums; }
|
|
745
752
|
|
|
746
753
|
/* Robot detail. */
|
|
@@ -952,6 +959,14 @@
|
|
|
952
959
|
.hero-bmap__zone { fill: color-mix(in srgb, var(--hero-text) 10%, transparent); stroke: var(--hero-text); stroke-width: 1; stroke-dasharray: 4 3; }
|
|
953
960
|
.hero-bmap__doorline { stroke: var(--hero-accent); stroke-width: 3; }
|
|
954
961
|
.hero-bmap__doorline--open { stroke: rgb(var(--v-theme-success)); stroke-dasharray: 2 2; }
|
|
962
|
+
.hero-bmap__doorswing { fill: none; stroke: var(--hero-accent); stroke-width: 1; stroke-dasharray: 3 2; opacity: 0.75; }
|
|
963
|
+
.hero-bmap__cabindoor { stroke: rgb(var(--v-theme-success)); stroke-width: 3; }
|
|
964
|
+
.hero-bmap__lane { stroke: var(--hero-accent); stroke-width: 1.5; opacity: 0.85; }
|
|
965
|
+
.hero-bmap__lane--oneway { stroke-dasharray: 5 3; }
|
|
966
|
+
.hero-bmap__wp { position: absolute; width: 6px; height: 6px; margin: -3px 0 0 -3px; border-radius: 50%; background: var(--hero-accent); outline: 1.5px solid var(--hero-bg); }
|
|
967
|
+
.hero-bmap__wp--charger { background: rgb(var(--v-theme-success)); border-radius: 1px; width: 8px; height: 8px; margin: -4px 0 0 -4px; }
|
|
968
|
+
.hero-bmap__wp--parking { background: rgb(var(--v-theme-info)); }
|
|
969
|
+
.hero-bmap__wpname { position: absolute; left: 6px; top: -6px; transform-origin: 0 0; white-space: nowrap; font-size: 9px; font-weight: 700; padding: 0 4px; background: var(--hero-bg); color: var(--hero-text); }
|
|
955
970
|
.hero-bmap__anchor { position: absolute; width: 0; height: 0; transform-style: preserve-3d; }
|
|
956
971
|
.hero-bmap__routetag { position: absolute; left: -6px; top: -30px; transform-origin: 0 0; white-space: nowrap; font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; font-weight: 800; padding: 1px 6px; background: var(--hero-accent); color: #fff; }
|
|
957
972
|
.hero-bmap__cp { position: absolute; left: -6px; top: -6px; width: 12px; height: 12px; box-sizing: border-box; border: 2px solid var(--hero-accent); background: var(--hero-bg); display: grid; place-items: center; font-size: 7px; font-weight: 800; color: var(--hero-accent); }
|
|
@@ -990,6 +1005,7 @@
|
|
|
990
1005
|
.hero-bmap__key--shaft { background: color-mix(in srgb, var(--hero-accent) 30%, transparent); border: 1px solid var(--hero-accent); box-sizing: border-box; }
|
|
991
1006
|
.hero-bmap__key--bot { background: var(--hero-text); }
|
|
992
1007
|
.hero-bmap__key--route { width: 12px; height: 0; border-top: 2px dashed var(--hero-accent); }
|
|
1008
|
+
.hero-bmap__key--lane { width: 12px; height: 0; border-top: 2px solid var(--hero-accent); }
|
|
993
1009
|
.hero-bmap__aside { display: flex; flex-direction: column; gap: 2px; font-size: 12px; flex: 1 1 260px; max-width: 360px; min-width: 0; }
|
|
994
1010
|
.hero-bmap__asidetitle { border-top: 2px solid var(--hero-divider); padding-top: 8px; margin-bottom: 6px; }
|
|
995
1011
|
.hero-bmap__legendrow { display: grid; grid-template-columns: 36px 1fr; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--hero-divider); }
|
|
@@ -1020,17 +1036,23 @@
|
|
|
1020
1036
|
align-self: flex-start; /* a taller side list must not stretch the plan under the pointer */
|
|
1021
1037
|
min-width: 0;
|
|
1022
1038
|
position: relative;
|
|
1023
|
-
|
|
1024
|
-
background-color: var(--hero-surface);
|
|
1025
|
-
background-image: linear-gradient(var(--hero-divider) 1px, transparent 1px), linear-gradient(90deg, var(--hero-divider) 1px, transparent 1px);
|
|
1026
|
-
background-size: 5% 8.9%;
|
|
1027
|
-
background-repeat: repeat; /* Vuetify resets every element to no-repeat */
|
|
1039
|
+
height: max(480px, calc(100vh - 330px));
|
|
1040
|
+
background-color: color-mix(in srgb, var(--hero-surface) 60%, var(--hero-bg));
|
|
1028
1041
|
border: 1px solid var(--hero-divider);
|
|
1029
1042
|
overflow: hidden;
|
|
1030
1043
|
cursor: crosshair;
|
|
1031
1044
|
touch-action: none; /* a finger drags shapes, not the page */
|
|
1032
1045
|
user-select: none;
|
|
1033
1046
|
}
|
|
1047
|
+
/* The building's frame at its real shape, fitted into the viewport; zoom and pan resize / move it. */
|
|
1048
|
+
.hero-editor__canvas {
|
|
1049
|
+
position: absolute;
|
|
1050
|
+
background-color: var(--hero-surface);
|
|
1051
|
+
background-image: linear-gradient(var(--hero-divider) 1px, transparent 1px), linear-gradient(90deg, var(--hero-divider) 1px, transparent 1px);
|
|
1052
|
+
background-repeat: repeat; /* Vuetify resets every element to no-repeat */
|
|
1053
|
+
outline: 1px solid var(--hero-divider);
|
|
1054
|
+
}
|
|
1055
|
+
.hero-editor__scale { position: absolute; right: 8px; bottom: 6px; font-size: 11px; opacity: 0.6; pointer-events: none; font-variant-numeric: tabular-nums; }
|
|
1034
1056
|
.hero-editor__plan--default { cursor: default; }
|
|
1035
1057
|
.hero-editor__plan--pointer { cursor: pointer; }
|
|
1036
1058
|
.hero-editor__plan--move { cursor: move; }
|
|
@@ -1050,7 +1072,7 @@
|
|
|
1050
1072
|
.hero-editor__measurebox { display: flex; flex-direction: column; gap: 6px; padding: 8px; border: 1px dashed var(--hero-accent); font-size: 12px; }
|
|
1051
1073
|
.hero-editor__measurein { width: 110px; min-height: 32px; }
|
|
1052
1074
|
.hero-editor__svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
|
|
1053
|
-
.hero-editor__wall { stroke: var(--hero-text); stroke-width:
|
|
1075
|
+
.hero-editor__wall { stroke: var(--hero-text); stroke-width: 3; stroke-linecap: square; }
|
|
1054
1076
|
.hero-editor__zone-shape { fill: color-mix(in srgb, var(--hero-text) 10%, transparent); stroke: var(--hero-text); stroke-width: 1; stroke-dasharray: 4 3; }
|
|
1055
1077
|
line.hero-editor__sel { stroke: var(--hero-accent); }
|
|
1056
1078
|
polygon.hero-editor__sel { stroke: var(--hero-accent); stroke-width: 2; stroke-dasharray: none; }
|
|
@@ -1080,6 +1102,49 @@ line.hero-editor__door.hero-editor__sel { stroke: var(--hero-text); }
|
|
|
1080
1102
|
.hero-editor__act:hover { background: color-mix(in srgb, var(--hero-text) 7%, transparent); }
|
|
1081
1103
|
.hero-editor__act:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
1082
1104
|
.hero-editor__act--del { color: var(--hero-accent); }
|
|
1105
|
+
/* Robot routes: lanes, waypoints, fiducials, measurements (HERO Fleet Core building data). */
|
|
1106
|
+
.hero-editor__lane { stroke: var(--hero-accent); stroke-width: 3; stroke-linecap: round; opacity: 0.9; }
|
|
1107
|
+
.hero-editor__lane--other { stroke: var(--hero-text); stroke-width: 1.5; stroke-dasharray: 4 3; opacity: 0.45; }
|
|
1108
|
+
.hero-editor__lane--preview { stroke-dasharray: 5 4; opacity: 0.7; }
|
|
1109
|
+
line.hero-editor__lane.hero-editor__sel { stroke: var(--hero-text); stroke-width: 4; opacity: 1; }
|
|
1110
|
+
.hero-editor__arrow { position: absolute; width: 0; height: 0; border-top: 6px solid transparent; border-bottom: 6px solid transparent; border-left: 11px solid var(--hero-accent); pointer-events: none; }
|
|
1111
|
+
.hero-editor__arrow--other { border-left-color: color-mix(in srgb, var(--hero-text) 45%, transparent); }
|
|
1112
|
+
.hero-editor__wp { position: absolute; width: 12px; height: 12px; margin: -6px 0 0 -6px; box-sizing: border-box; border-radius: 50%; background: var(--hero-surface); border: 2.5px solid var(--hero-accent); pointer-events: none; }
|
|
1113
|
+
.hero-editor__wp--charger { background: rgb(var(--v-theme-success)); border-color: var(--hero-text); border-radius: 2px; }
|
|
1114
|
+
.hero-editor__wp--parking { background: rgb(var(--v-theme-info)); border-color: var(--hero-text); }
|
|
1115
|
+
.hero-editor__wp--holding { background: var(--hero-accent); }
|
|
1116
|
+
.hero-editor__wp--from { box-shadow: 0 0 0 4px color-mix(in srgb, var(--hero-accent) 35%, transparent); }
|
|
1117
|
+
div.hero-editor__wp.hero-editor__sel, div.hero-editor__fid.hero-editor__sel { outline: 2px solid var(--hero-text); outline-offset: 2px; }
|
|
1118
|
+
.hero-editor__wpname { position: absolute; left: 12px; top: -9px; white-space: nowrap; font-size: 10.5px; font-weight: 700; line-height: 1.4; padding: 0 4px; background: var(--hero-surface); color: var(--hero-text); border: 1px solid var(--hero-divider); }
|
|
1119
|
+
.hero-editor__fid { position: absolute; width: 16px; height: 16px; margin: -8px 0 0 -8px; box-sizing: border-box; border: 2px solid var(--hero-text); border-radius: 50%; pointer-events: none; background: linear-gradient(var(--hero-text), var(--hero-text)) center / 2px 100% no-repeat, linear-gradient(var(--hero-text), var(--hero-text)) center / 100% 2px no-repeat; }
|
|
1120
|
+
.hero-editor__mlabel { position: absolute; transform: translate(-50%, -130%); white-space: nowrap; font-size: 10.5px; font-weight: 700; padding: 0 4px; background: var(--hero-accent); color: #fff; pointer-events: none; }
|
|
1121
|
+
.hero-editor__cabindoor { position: absolute; height: 4px; background: rgb(var(--v-theme-success)); pointer-events: none; }
|
|
1122
|
+
.hero-editor__cabindoor--shut { background: color-mix(in srgb, var(--hero-text) 30%, transparent); }
|
|
1123
|
+
.hero-editor__lift { transform-origin: center; }
|
|
1124
|
+
.hero-editor__lift--passes { opacity: 0.45; border-style: dashed; }
|
|
1125
|
+
.hero-editor__graphpick { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; }
|
|
1126
|
+
.hero-editor__zoomread { min-width: 56px; font-variant-numeric: tabular-nums; }
|
|
1127
|
+
.hero-editor__field { display: flex; flex-direction: column; gap: 3px; font-size: 11.5px; }
|
|
1128
|
+
.hero-editor__field > span { opacity: 0.75; }
|
|
1129
|
+
.hero .hero-editor__field .hero-input { min-height: 32px; width: 100%; }
|
|
1130
|
+
.hero-editor__row3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(84px, 1fr)); gap: 8px; }
|
|
1131
|
+
.hero-editor__sub { font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.7; margin-top: 4px; }
|
|
1132
|
+
.hero-editor__cabinrow { display: grid; grid-template-columns: 1fr 1fr auto; gap: 4px; align-items: center; padding-bottom: 6px; border-bottom: 1px dashed var(--hero-divider); }
|
|
1133
|
+
.hero-editor__cabinw { display: flex; align-items: center; gap: 4px; font-size: 11.5px; }
|
|
1134
|
+
.hero .hero-editor__cabinrow .hero-input { min-height: 30px; padding: 3px 6px; width: 100%; min-width: 0; }
|
|
1135
|
+
.hero-editor__grid2 { border-collapse: collapse; font-size: 11.5px; }
|
|
1136
|
+
.hero-editor__grid2 th, .hero-editor__grid2 td { padding: 3px 6px; border-bottom: 1px solid var(--hero-divider); text-align: center; }
|
|
1137
|
+
.hero-editor__grid2 th:first-child, .hero-editor__grid2 td:first-child { text-align: left; font-weight: 700; }
|
|
1138
|
+
.hero-editor__checks { display: flex; flex-direction: column; gap: 4px; }
|
|
1139
|
+
.hero-editor__checks label, .hero-editor__check2 { display: flex; gap: 6px; align-items: center; cursor: pointer; }
|
|
1140
|
+
.hero-editor__check { font: inherit; font-size: 12px; text-align: left; padding: 6px 8px; color: inherit; background: color-mix(in srgb, rgb(var(--v-theme-warning)) 12%, transparent); border: 0; border-left: 3px solid rgb(var(--v-theme-warning)); cursor: pointer; }
|
|
1141
|
+
.hero-editor__check--error { background: color-mix(in srgb, rgb(var(--v-theme-error)) 12%, transparent); border-left-color: rgb(var(--v-theme-error)); }
|
|
1142
|
+
.hero-editor__graphrow { display: grid; grid-template-columns: 1fr 1fr auto auto; gap: 4px; align-items: center; padding-left: 4px; border-left: 3px solid transparent; }
|
|
1143
|
+
.hero-editor__graphrow--on { border-left-color: var(--hero-accent); }
|
|
1144
|
+
.hero .hero-editor__graphrow .hero-input { min-height: 30px; padding: 3px 6px; width: 100%; min-width: 0; }
|
|
1145
|
+
.hero-editor__details summary { cursor: pointer; font-size: 11.5px; opacity: 0.8; }
|
|
1146
|
+
.hero-editor__details[open] { display: flex; flex-direction: column; gap: 6px; }
|
|
1147
|
+
.hero-editor__list { display: flex; flex-direction: column; max-height: 260px; overflow-y: auto; }
|
|
1083
1148
|
.hero-editor__cursor { position: absolute; width: 14px; height: 14px; margin: -7px 0 0 -7px; border: 2px solid var(--hero-accent); pointer-events: none; }
|
|
1084
1149
|
.hero-editor__aside { font-size: 12px; display: flex; flex-direction: column; gap: 8px; flex: 1 1 260px; max-width: 360px; min-width: 0; align-self: stretch; }
|
|
1085
1150
|
.hero-editor__asidetitle { border-top: 2px solid var(--hero-divider); padding-top: 8px; }
|
|
@@ -27,6 +27,28 @@
|
|
|
27
27
|
</template>
|
|
28
28
|
</PageHeader>
|
|
29
29
|
|
|
30
|
+
<!-- Owner decision 2026-09-25: members holding ANOTHER site's role keep
|
|
31
|
+
their access; the company assigns each one a role for this site.
|
|
32
|
+
Shown while any remain (server count), gone at 0. -->
|
|
33
|
+
<v-alert
|
|
34
|
+
v-if="props.assignRole && otherSiteBanner"
|
|
35
|
+
type="warning"
|
|
36
|
+
variant="tonal"
|
|
37
|
+
density="compact"
|
|
38
|
+
class="mb-3"
|
|
39
|
+
data-test="member-other-site-banner"
|
|
40
|
+
>
|
|
41
|
+
<div class="d-flex align-center flex-wrap ga-2">
|
|
42
|
+
<span class="flex-grow-1">{{ otherSiteBanner }}</span>
|
|
43
|
+
<AppButton
|
|
44
|
+
data-test="member-other-site-filter"
|
|
45
|
+
@click="toggleOtherSite()"
|
|
46
|
+
>
|
|
47
|
+
{{ onlyOtherSite ? "Show all members" : "Show them" }}
|
|
48
|
+
</AppButton>
|
|
49
|
+
</div>
|
|
50
|
+
</v-alert>
|
|
51
|
+
|
|
30
52
|
<AppCard class="table-card">
|
|
31
53
|
<CardToolbar :count="pageRange">
|
|
32
54
|
<template #tabs>
|
|
@@ -278,7 +300,7 @@
|
|
|
278
300
|
</template>
|
|
279
301
|
|
|
280
302
|
<script setup lang="ts">
|
|
281
|
-
import { memberRoleChip, withCurrentRole } from "../utils/role";
|
|
303
|
+
import { memberRoleChip, otherSiteBannerText, withCurrentRole } from "../utils/role";
|
|
282
304
|
|
|
283
305
|
const props = defineProps({
|
|
284
306
|
/** The design's page title. Every app that mounts this screen calls it
|
|
@@ -372,6 +394,7 @@ const props = defineProps({
|
|
|
372
394
|
});
|
|
373
395
|
|
|
374
396
|
const items = ref<Array<Record<string, any>>>([]);
|
|
397
|
+
const onlyOtherSite = ref(false);
|
|
375
398
|
const page = ref(1);
|
|
376
399
|
const pages = ref(10);
|
|
377
400
|
const pageRange = ref("-- - -- of --");
|
|
@@ -444,12 +467,47 @@ const {
|
|
|
444
467
|
page: page.value,
|
|
445
468
|
type: props.type,
|
|
446
469
|
siteId: props.siteId,
|
|
470
|
+
roleState: onlyOtherSite.value ? "other-site" : "",
|
|
447
471
|
}),
|
|
448
472
|
{
|
|
449
|
-
watch: [() => props.status, () => props.orgId, () => props.siteId, headerSearch],
|
|
473
|
+
watch: [() => props.status, () => props.orgId, () => props.siteId, headerSearch, onlyOtherSite],
|
|
450
474
|
}
|
|
451
475
|
);
|
|
452
476
|
|
|
477
|
+
/*
|
|
478
|
+
* "N members here have a role from another site" — the count is the server's
|
|
479
|
+
* (`roleState=other-site`, `total`), for this org / site / app, so it is right
|
|
480
|
+
* however many pages the list has. Refreshed with the list (after an Assign
|
|
481
|
+
* Role the list refreshes, and so does this).
|
|
482
|
+
*/
|
|
483
|
+
const { data: otherSiteReq, refresh: refreshOtherSite } = useAsyncData(
|
|
484
|
+
`members-other-site-${props.type}-${props.orgId}-${props.siteId}-${props.status}`,
|
|
485
|
+
() =>
|
|
486
|
+
props.assignRole && props.orgId
|
|
487
|
+
? _getAll({
|
|
488
|
+
status: props.status,
|
|
489
|
+
org: props.orgId,
|
|
490
|
+
page: 1,
|
|
491
|
+
limit: 1,
|
|
492
|
+
type: props.type,
|
|
493
|
+
siteId: props.siteId,
|
|
494
|
+
roleState: "other-site",
|
|
495
|
+
}).catch(() => null)
|
|
496
|
+
: Promise.resolve(null),
|
|
497
|
+
{ watch: [() => props.status, () => props.orgId, () => props.siteId] }
|
|
498
|
+
);
|
|
499
|
+
const otherSiteBanner = computed(() =>
|
|
500
|
+
otherSiteBannerText(otherSiteReq.value?.total)
|
|
501
|
+
);
|
|
502
|
+
watch(otherSiteBanner, (text) => {
|
|
503
|
+
// the last one fixed: back to the full list
|
|
504
|
+
if (!text && onlyOtherSite.value) onlyOtherSite.value = false;
|
|
505
|
+
});
|
|
506
|
+
function toggleOtherSite() {
|
|
507
|
+
page.value = 1;
|
|
508
|
+
onlyOtherSite.value = !onlyOtherSite.value;
|
|
509
|
+
}
|
|
510
|
+
|
|
453
511
|
const loading = computed(() => getAllReqStatus.value === "pending");
|
|
454
512
|
|
|
455
513
|
watch(headerSearch, () => {
|
|
@@ -567,7 +625,7 @@ async function updateMemberRole() {
|
|
|
567
625
|
props.orgId
|
|
568
626
|
);
|
|
569
627
|
await setMember({ mode: "assign-role" });
|
|
570
|
-
await getAll();
|
|
628
|
+
await Promise.all([getAll(), refreshOtherSite()]);
|
|
571
629
|
} catch (error: any) {
|
|
572
630
|
// `message.value` alone sets the snackbar's TEXT and never opens it, so a
|
|
573
631
|
// refused role change said nothing at all - the dialog just closed and the
|
|
@@ -78,6 +78,9 @@
|
|
|
78
78
|
<div class="hero-bmap__seg" role="group" aria-label="View angle">
|
|
79
79
|
<button v-for="(a, id) in MAP_ANGLES" :key="id" type="button" :aria-pressed="cam.angle === id" @click="cam = presetCamera(cam, id)">{{ a.label }}</button>
|
|
80
80
|
</div>
|
|
81
|
+
<div class="hero-bmap__seg" role="group" aria-label="Show">
|
|
82
|
+
<button type="button" :aria-pressed="showRoutes" title="Show the robots' lanes and waypoints" @click="showRoutes = !showRoutes">Routes</button>
|
|
83
|
+
</div>
|
|
81
84
|
<div class="hero-bmap__seg" role="group" aria-label="Floor">
|
|
82
85
|
<button type="button" :aria-pressed="focus === null" @click="focus = null">All</button>
|
|
83
86
|
<button v-for="f in floors" :key="f" type="button" :aria-pressed="focus === f" @click="focus = f">{{ floorName(f) }}</button>
|
|
@@ -98,6 +101,8 @@
|
|
|
98
101
|
<div
|
|
99
102
|
class="hero-bmap__stack"
|
|
100
103
|
:style="{
|
|
104
|
+
width: `${plane.w}px`,
|
|
105
|
+
height: `${plane.h}px`,
|
|
101
106
|
'--shift': `${stack.shift}px`,
|
|
102
107
|
transform: `rotateX(${cam.tilt}deg) rotateZ(${cam.turn}deg) translateZ(${stack.shift}px)`,
|
|
103
108
|
transition: drag ? 'none' : 'transform .6s ease',
|
|
@@ -120,13 +125,26 @@
|
|
|
120
125
|
<span class="hero-bmap__door" style="left: 42%; top: 22%" aria-hidden="true" />
|
|
121
126
|
<span class="hero-bmap__door" style="left: 58%; top: 30%" aria-hidden="true" />
|
|
122
127
|
</template>
|
|
123
|
-
<svg v-if="f.shapes.length" class="hero-plan__svg" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
|
|
128
|
+
<svg v-if="f.shapes.length || f.cabin.length" class="hero-plan__svg" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
|
|
124
129
|
<template v-for="s in f.shapes" :key="s.id">
|
|
125
130
|
<polygon v-if="s.kind === 'zone'" class="hero-bmap__zone" :points="svgPoints(s)" vector-effect="non-scaling-stroke" />
|
|
126
131
|
<line v-else-if="s.kind === 'wall'" class="hero-bmap__wall" :x1="s.points[0]!.x" :y1="s.points[0]!.y" :x2="s.points[1]!.x" :y2="s.points[1]!.y" vector-effect="non-scaling-stroke" />
|
|
127
|
-
<
|
|
132
|
+
<template v-else-if="s.kind === 'door'">
|
|
133
|
+
<polyline v-for="(g, gi) in f.glyphs[s.id] ?? []" :key="gi" class="hero-bmap__doorswing" :points="svgPoints({ points: g })" vector-effect="non-scaling-stroke" />
|
|
134
|
+
<line class="hero-bmap__doorline" :class="{ 'hero-bmap__doorline--open': openDoors.has(String(s.meta?.name)) }" :x1="s.points[0]!.x" :y1="s.points[0]!.y" :x2="s.points[1]!.x" :y2="s.points[1]!.y" vector-effect="non-scaling-stroke" />
|
|
135
|
+
</template>
|
|
128
136
|
</template>
|
|
137
|
+
<line v-for="(c, ci) in f.cabin" :key="`c${ci}`" class="hero-bmap__cabindoor" :x1="c.a.x" :y1="c.a.y" :x2="c.b.x" :y2="c.b.y" vector-effect="non-scaling-stroke" />
|
|
129
138
|
</svg>
|
|
139
|
+
<template v-if="showRoutes && (f.routes2.lanes.length || f.routes2.waypoints.length)">
|
|
140
|
+
<svg class="hero-plan__svg" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
|
|
141
|
+
<line v-for="(l, li) in f.routes2.lanes" :key="`ln${li}`" class="hero-bmap__lane" :class="{ 'hero-bmap__lane--oneway': l.oneWay }" :x1="l.a.x" :y1="l.a.y" :x2="l.b.x" :y2="l.b.y" vector-effect="non-scaling-stroke" />
|
|
142
|
+
</svg>
|
|
143
|
+
<span v-for="(w, wi) in f.routes2.waypoints" :key="`wp${wi}`" class="hero-bmap__wp" :class="`hero-bmap__wp--${w.kind}`" :style="{ left: `${w.x}%`, top: `${w.y}%` }" :title="w.name" aria-hidden="true" />
|
|
144
|
+
<div v-for="(w, wi) in f.routes2.waypoints.filter((x) => x.name && x.kind !== 'point')" :key="`wn${wi}`" class="hero-bmap__anchor" :style="{ left: `${w.x}%`, top: `${w.y}%` }">
|
|
145
|
+
<div class="hero-bmap__wpname" :style="{ transform: `translateZ(6px) ${unskew}` }">{{ w.name }}</div>
|
|
146
|
+
</div>
|
|
147
|
+
</template>
|
|
130
148
|
|
|
131
149
|
<template v-if="type === 'security'">
|
|
132
150
|
<svg class="hero-plan__svg" viewBox="0 0 400 300" preserveAspectRatio="none" aria-hidden="true">
|
|
@@ -163,7 +181,7 @@
|
|
|
163
181
|
v-for="s in shafts"
|
|
164
182
|
:key="s.id"
|
|
165
183
|
class="hero-bmap__shaft"
|
|
166
|
-
:style="{ left: `${s.x}
|
|
184
|
+
:style="{ left: `${s.x}px`, top: `${s.y}px`, transform: `translateZ(${s.z}px) rotateZ(${s.rot}deg)`, opacity: look.shaftOpacity }"
|
|
167
185
|
>
|
|
168
186
|
<span class="hero-bmap__shaftwall" :style="{ width: `${s.w}px`, height: `${s.h}px` }" />
|
|
169
187
|
<span class="hero-bmap__shaftwall hero-bmap__shaftwall--y" :style="{ width: `${s.h}px`, height: `${s.d}px` }" />
|
|
@@ -177,6 +195,7 @@
|
|
|
177
195
|
<span><i class="hero-bmap__key hero-bmap__key--shaft" />Lift shaft</span>
|
|
178
196
|
<span><i class="hero-bmap__key hero-bmap__key--bot" />Robot (live)</span>
|
|
179
197
|
<span v-if="type === 'security'"><i class="hero-bmap__key hero-bmap__key--route" />Patrol route · numbered checkpoints</span>
|
|
198
|
+
<span v-if="showRoutes && hasLanes"><i class="hero-bmap__key hero-bmap__key--lane" />Robot lanes · waypoints</span>
|
|
180
199
|
</div>
|
|
181
200
|
</div>
|
|
182
201
|
|
|
@@ -206,7 +225,8 @@ import RobotMapEditor from "./MapEditor.vue";
|
|
|
206
225
|
import RobotReconnecting from "./Reconnecting.vue";
|
|
207
226
|
import useRobotBuilding from "../../composables/useRobotBuilding";
|
|
208
227
|
import useFloorPlanImages from "../../composables/useFloorPlanImages";
|
|
209
|
-
import { floorPlanFor
|
|
228
|
+
import { floorPlanFor } from "../../utils/robot-floorplan";
|
|
229
|
+
import { floorAspect, floorDoors, placementInWorld, planBoxIn, routeOverlay, viewerLifts } from "../../utils/rmf-editor";
|
|
210
230
|
import useRobotLive, { siteStatesFeed } from "../../composables/useRobotLive";
|
|
211
231
|
import { openDoorNames } from "../../utils/robot-fleet-core";
|
|
212
232
|
import {
|
|
@@ -228,6 +248,7 @@ import {
|
|
|
228
248
|
import {
|
|
229
249
|
buildingFloors,
|
|
230
250
|
cameraReadout,
|
|
251
|
+
doorGlyph,
|
|
231
252
|
defaultCamera,
|
|
232
253
|
dragCamera,
|
|
233
254
|
dragMode,
|
|
@@ -237,7 +258,6 @@ import {
|
|
|
237
258
|
planeLook,
|
|
238
259
|
presetCamera,
|
|
239
260
|
resetCamera,
|
|
240
|
-
shapeBox,
|
|
241
261
|
shapesFromMap,
|
|
242
262
|
stackMetrics,
|
|
243
263
|
startDrag,
|
|
@@ -319,34 +339,66 @@ function floorImage(floorNo: number) {
|
|
|
319
339
|
const p = floorPlanFor(saved.value, floorNo);
|
|
320
340
|
const url = planUrls.value[floorNo];
|
|
321
341
|
if (!p || !url) return null;
|
|
322
|
-
|
|
323
|
-
|
|
342
|
+
// The plan sits in the floor's own frame (world metres); older maps use the 40 x 26 m frame.
|
|
343
|
+
const pf = saved.value?.rmf?.levels?.find((l: any) => l.floorNo === floorNo)?.planFrame;
|
|
344
|
+
const f = pf ? { minX: pf.minX, maxY: pf.maxY, w: pf.maxX - pf.minX, h: pf.maxY - pf.minY } : { minX: 0, maxY: 0, w: 40, h: 26 };
|
|
345
|
+
const b = planBoxIn(f, placementInWorld(p as typeof p & { space?: string }, pf));
|
|
346
|
+
return { url, style: { left: `${b.left}%`, top: `${b.top}%`, width: `${b.width}%`, height: `${b.height}%`, transform: `rotate(${b.rotateDeg}deg)`, transformOrigin: "0 0" } };
|
|
347
|
+
}
|
|
348
|
+
/** Each floor's doors with their real type (the saved map's rmf part), for the swing / slide drawing, and the lift cabin doors that open there. */
|
|
349
|
+
function floorDoorParts(floorNo: number) {
|
|
350
|
+
const fd = fleet.value ? null : floorDoors(map.value, floorNo);
|
|
351
|
+
const own = shapes.value.filter((s) => s.kind !== "lift" && s.floorNo === floorNo);
|
|
352
|
+
if (!fd) return { shapes: own, glyphs: {} as Record<string, Array<Array<{ x: number; y: number }>>>, cabin: [] as Array<{ a: { x: number; y: number }; b: { x: number; y: number } }> };
|
|
353
|
+
const size = { w: fd.frame.w, h: fd.frame.h };
|
|
354
|
+
return { shapes: [...own.filter((s) => s.kind !== "door"), ...fd.doors], glyphs: Object.fromEntries(fd.doors.map((d) => [d.id, doorGlyph(d, size)])), cabin: fd.cabin };
|
|
324
355
|
}
|
|
325
356
|
const floors3d = computed(() =>
|
|
326
|
-
floors.value.map((floorNo) =>
|
|
327
|
-
floorNo
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
357
|
+
floors.value.map((floorNo) => {
|
|
358
|
+
const parts = floorDoorParts(floorNo);
|
|
359
|
+
return {
|
|
360
|
+
floorNo,
|
|
361
|
+
plan: floorImage(floorNo),
|
|
362
|
+
dim: floorOpacity(cam.value.tilt, focus.value, floorNo),
|
|
363
|
+
shapes: parts.shapes,
|
|
364
|
+
glyphs: parts.glyphs,
|
|
365
|
+
cabin: parts.cabin,
|
|
366
|
+
routes: props.type === "security" ? floorRoutes(floorNo) : [],
|
|
367
|
+
routes2: routeOverlay(map.value, floorNo),
|
|
368
|
+
robots: mapRobots(rows.value, states.value, floorNo),
|
|
369
|
+
};
|
|
370
|
+
}),
|
|
334
371
|
);
|
|
372
|
+
/** Lanes and waypoints: shown by default once the building has any. */
|
|
373
|
+
const hasLanes = computed(() => floors3d.value.some((f) => f.routes2.lanes.length || f.routes2.waypoints.length));
|
|
374
|
+
const showRoutes = ref(true);
|
|
375
|
+
/** The floor plane keeps the building's real shape (inside 400 x 300 px). */
|
|
376
|
+
const plane = computed(() => {
|
|
377
|
+
const a = fleet.value ? 40 / 26 : floorAspect(map.value);
|
|
378
|
+
return a >= 4 / 3 ? { w: 400, h: Math.max(60, Math.round(400 / a)) } : { w: Math.max(60, Math.round(300 * a)), h: 300 };
|
|
379
|
+
});
|
|
335
380
|
const legend = computed(() => [...floors3d.value].reverse());
|
|
336
381
|
/** The editor opens on the busiest floor (the handoff's "best floor"). */
|
|
337
382
|
const startFloor = computed(() => pickFloor(floors.value, null, ids.value, states.value));
|
|
338
383
|
|
|
339
|
-
/**
|
|
384
|
+
/**
|
|
385
|
+
* Each saved lift shaft: two red walls from its lowest served floor to its
|
|
386
|
+
* highest, at its real size and angle - placed by its top-left corner turned
|
|
387
|
+
* round its centre, so the shaft turns on the spot.
|
|
388
|
+
*/
|
|
340
389
|
const shafts = computed(() =>
|
|
341
|
-
|
|
342
|
-
.
|
|
343
|
-
.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
390
|
+
viewerLifts(map.value).map((l, i) => {
|
|
391
|
+
const served = l.floors.map((f) => floors.value.indexOf(f)).filter((k) => k >= 0);
|
|
392
|
+
const lo = served.length ? Math.min(...served) : 0;
|
|
393
|
+
const hi = served.length ? Math.max(...served) : floors.value.length - 1;
|
|
394
|
+
const w = Math.max(12, (l.w / 100) * plane.value.w);
|
|
395
|
+
const d = Math.max(12, (l.d / 100) * plane.value.h);
|
|
396
|
+
const rot = -l.yawDeg;
|
|
397
|
+
const t = (rot * Math.PI) / 180;
|
|
398
|
+
const cx = (l.x / 100) * plane.value.w;
|
|
399
|
+
const cy = (l.y / 100) * plane.value.h;
|
|
400
|
+
return { id: `${l.name}-${i}`, x: cx + (-w / 2) * Math.cos(t) - (-d / 2) * Math.sin(t), y: cy + (-w / 2) * Math.sin(t) + (-d / 2) * Math.cos(t), rot, z: lo * FLOOR_GAP_PX, w, d, h: (hi - lo) * FLOOR_GAP_PX + 8 };
|
|
401
|
+
}),
|
|
350
402
|
);
|
|
351
403
|
|
|
352
404
|
const unplaced = computed(() => unplacedRobots(rows.value, states.value));
|
|
@@ -394,7 +446,7 @@ function onMove(e: MouseEvent) {
|
|
|
394
446
|
if (d) cam.value = dragCamera(d.start, d.mode, e.clientX - d.x, e.clientY - d.y);
|
|
395
447
|
}
|
|
396
448
|
|
|
397
|
-
const svgPoints = (s:
|
|
449
|
+
const svgPoints = (s: { points: Array<{ x: number; y: number }> }) => s.points.map((p) => `${p.x},${p.y}`).join(" ");
|
|
398
450
|
|
|
399
451
|
/* ---- full screen + edit ---- */
|
|
400
452
|
const viewer = ref<HTMLElement | null>(null);
|