@7365admin1/layer-common 4.35.1-staging.385 → 4.36.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 +10 -4
- package/assets/css/primitives.css +0 -43
- package/assets/css/robot-hero.css +2 -2
- package/components/AccessCardAddForm.vue +4 -5
- package/components/AccessCardAssignToUnitForm.vue +2 -3
- package/components/AccessCardPreviewDialog.vue +3 -4
- package/components/AppSelect.vue +3 -77
- package/components/CameraForm.vue +6 -20
- package/components/DashboardMain.vue +60 -66
- package/components/Dialog/UpdateMoreAction.vue +1 -11
- package/components/DocumentForm.vue +112 -407
- package/components/DocumentManagement.vue +12 -213
- package/components/EntryPass/QrTemplatePreview.vue +25 -21
- package/components/EntryPassInformation.vue +24 -119
- package/components/Facility/BookingSetup.vue +0 -63
- package/components/HidAccessLogDashboard.vue +17 -74
- package/components/HidIntercomManagement.vue +14 -180
- package/components/HidQrCodeConfiguration.vue +195 -17
- package/components/HidReaderManagement.vue +0 -398
- package/components/HidReaderUserRoster.vue +3 -44
- package/components/HidUserEnrollment.vue +155 -845
- package/components/InvitationClientForm.vue +1 -19
- package/components/Layout/NavigationDrawer.vue +1 -4
- package/components/NotificationBell.vue +7 -59
- package/components/Robot/BuildingMap.vue +9 -16
- package/components/Robot/LiftQueue.vue +4 -9
- package/components/Robot/MapEditor.vue +8 -1
- package/components/RolePermissionFormCreate.vue +3 -4
- package/components/RolePermissionFormPreviewUpdate.vue +5 -12
- package/components/RolePermissionMain.vue +0 -1
- package/components/ServiceProviderInvitationPrompt.vue +155 -0
- package/components/ServiceProviderMain.vue +30 -178
- package/components/VisitorForm.vue +0 -12
- package/components/VisitorManagement.vue +10 -285
- package/components/VisitorSocketPopUp.vue +1 -2
- package/composables/useANPRSettings.ts +15 -45
- package/composables/useAccessManagement.ts +0 -5
- package/composables/useCleaningPermission.ts +0 -2
- package/composables/useCommonPermission.ts +0 -32
- package/composables/useDocument.ts +0 -7
- package/composables/useFacility.ts +0 -11
- package/composables/useHidAmico.ts +0 -159
- package/composables/useHidNavigation.ts +1 -25
- package/composables/usePeople.ts +0 -19
- package/composables/usePropertyPermission.ts +0 -19
- package/composables/useRobotBuilding.ts +3 -22
- package/composables/useRobots.ts +0 -13
- package/composables/useSecurityPermission.ts +0 -19
- package/composables/useServiceProvider.ts +0 -3
- package/composables/useServiceProviderInvitation.ts +3 -101
- package/composables/useSiteEntryPassSettings.ts +0 -26
- package/composables/useWebUsb.ts +11 -14
- package/package.json +2 -2
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
- package/plugins/API.ts +0 -11
- package/types/document.d.ts +0 -14
- package/types/facility.d.ts +0 -4
- package/types/people.d.ts +1 -28
- package/utils/module-routes.ts +1 -8
- package/utils/nav-sections.ts +2 -8
- package/utils/notification-target.ts +8 -277
- package/utils/permission-labels.ts +2 -20
- package/components/ClientInvitations.vue +0 -291
- package/components/HidAccessPermissions.vue +0 -424
- package/components/HidProfileQrCode.vue +0 -332
- package/composables/useHidReaderSelection.ts +0 -50
- package/composables/useSiteCategory.ts +0 -45
- package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +0 -23
- package/utils/dashboard-widget-prefs.ts +0 -54
- package/utils/hid-permission-assignments.ts +0 -190
- package/utils/hid-reader-selection.ts +0 -52
- package/utils/occupancy-role.ts +0 -109
- package/utils/robot-fleet-core.ts +0 -57
- package/utils/sp-inviter-row.ts +0 -97
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.36.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 230f9f0: Building map editor: drag to move, resize handles, drag wall/door ends and zone corners, add/remove zone corners, turn doors and lift shafts, flip a door's swing, multi-select (Shift+click, box select), snap to grid, keyboard nudge/delete/escape and redo. A whole drag is one undo step.
|
|
8
|
+
|
|
3
9
|
## 4.35.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -1418,11 +1424,11 @@
|
|
|
1418
1424
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
1419
1425
|
own rule from two endpoints the console already calls, unprojected:
|
|
1420
1426
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1427
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
1428
|
+
GET /api/roles/id/:role that membership's role document
|
|
1423
1429
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1430
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
1431
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
1426
1432
|
|
|
1427
1433
|
`role.default` is the marker because it is the only property of a platform
|
|
1428
1434
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -775,49 +775,6 @@ select option {
|
|
|
775
775
|
box-shadow: var(--shadow-menu);
|
|
776
776
|
}
|
|
777
777
|
|
|
778
|
-
/* The filter box of a `searchable` AppSelect. Sticky rather than scrolled
|
|
779
|
-
away with the options: the list it filters is long enough to need scrolling,
|
|
780
|
-
which is the only reason the box is there. */
|
|
781
|
-
.app-select__search {
|
|
782
|
-
position: sticky;
|
|
783
|
-
top: -6px;
|
|
784
|
-
z-index: 1;
|
|
785
|
-
display: flex;
|
|
786
|
-
align-items: center;
|
|
787
|
-
gap: 6px;
|
|
788
|
-
margin: -6px -6px 4px;
|
|
789
|
-
padding: 8px 10px;
|
|
790
|
-
border-bottom: 1px solid var(--border);
|
|
791
|
-
background: var(--card);
|
|
792
|
-
color: var(--muted);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
.app-select__search-input {
|
|
796
|
-
flex: 1;
|
|
797
|
-
min-width: 0;
|
|
798
|
-
border: none;
|
|
799
|
-
outline: none;
|
|
800
|
-
background: none;
|
|
801
|
-
font-family: var(--font-sans);
|
|
802
|
-
font-size: var(--fs-cell);
|
|
803
|
-
font-weight: 500;
|
|
804
|
-
color: var(--text);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
.app-select__search-input::placeholder {
|
|
808
|
-
color: var(--muted);
|
|
809
|
-
font-weight: 500;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
.app-select__empty {
|
|
813
|
-
margin: 0;
|
|
814
|
-
padding: 10px 8px;
|
|
815
|
-
font-family: var(--font-sans);
|
|
816
|
-
font-size: var(--fs-cell);
|
|
817
|
-
color: var(--muted);
|
|
818
|
-
text-align: center;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
778
|
.app-select__option {
|
|
822
779
|
display: flex;
|
|
823
780
|
align-items: center;
|
|
@@ -951,7 +951,6 @@
|
|
|
951
951
|
.hero-bmap__wall { stroke: var(--hero-text); stroke-width: 3; opacity: 0.8; }
|
|
952
952
|
.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
953
|
.hero-bmap__doorline { stroke: var(--hero-accent); stroke-width: 3; }
|
|
954
|
-
.hero-bmap__doorline--open { stroke: rgb(var(--v-theme-success)); stroke-dasharray: 2 2; }
|
|
955
954
|
.hero-bmap__anchor { position: absolute; width: 0; height: 0; transform-style: preserve-3d; }
|
|
956
955
|
.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
956
|
.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); }
|
|
@@ -986,7 +985,6 @@
|
|
|
986
985
|
.hero-bmap__legend { position: absolute; left: 14px; bottom: 12px; z-index: 3; font-size: 11px; opacity: 0.65; display: flex; gap: 14px; white-space: nowrap; }
|
|
987
986
|
.hero-bmap__key { display: inline-block; width: 8px; height: 8px; vertical-align: middle; margin-right: 5px; }
|
|
988
987
|
.hero-bmap__key--door { background: var(--hero-accent); }
|
|
989
|
-
.hero-bmap__key--dooropen { background: rgb(var(--v-theme-success)); }
|
|
990
988
|
.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
989
|
.hero-bmap__key--bot { background: var(--hero-text); }
|
|
992
990
|
.hero-bmap__key--route { width: 12px; height: 0; border-top: 2px dashed var(--hero-accent); }
|
|
@@ -1012,6 +1010,8 @@
|
|
|
1012
1010
|
.hero .hero-input.hero-editor__floor { width: auto; min-height: 32px; padding: 4px 26px 4px 8px; }
|
|
1013
1011
|
.hero .hero-input.hero-editor__zone { width: 220px; min-height: 32px; }
|
|
1014
1012
|
.hero-editor__hint { font-size: 12px; opacity: 0.7; order: 1; flex-basis: 100%; } /* own line: switching tools never moves the plan */
|
|
1013
|
+
.hero .v-btn.hero-editor__fs { margin-left: auto; }
|
|
1014
|
+
.hero .v-btn.hero-editor__fs .v-btn__content { gap: 8px; }
|
|
1015
1015
|
.hero-editor__notice { padding: 10px 12px; font-size: 12.5px; border-left: 2px solid var(--hero-accent); background: var(--hero-surface); }
|
|
1016
1016
|
.hero-editor__notice--ok { border-left-color: rgb(var(--v-theme-success)); }
|
|
1017
1017
|
.hero-editor__split { display: flex; flex-wrap: wrap; gap: 24px; align-items: stretch; flex: 1; }
|
|
@@ -433,10 +433,7 @@ const {
|
|
|
433
433
|
bulkPhysicalAccessCard: _bulkPhysicalAccessCard,
|
|
434
434
|
getBlockLevelUnitList: _getBlockLevelUnitList,
|
|
435
435
|
} = useAccessManagement();
|
|
436
|
-
const {
|
|
437
|
-
getBySiteId: _getEntryPassSettings,
|
|
438
|
-
getEncryptedAcmUrl: _getEncryptedAcmUrl,
|
|
439
|
-
} = useSiteEntryPassSettings();
|
|
436
|
+
const { getBySiteId: _getEntryPassSettings } = useSiteEntryPassSettings();
|
|
440
437
|
const { getFileUrl } = useFile();
|
|
441
438
|
|
|
442
439
|
const config = useRuntimeConfig();
|
|
@@ -591,7 +588,9 @@ watch(uploadMode, (mode) => {
|
|
|
591
588
|
|
|
592
589
|
onMounted(async () => {
|
|
593
590
|
try {
|
|
594
|
-
const acmUrl = await
|
|
591
|
+
const { encrypted: acmUrl } = await $fetch<{ encrypted: string }>(
|
|
592
|
+
"/api/encrypt-acm-url"
|
|
593
|
+
);
|
|
595
594
|
encryptedAcmUrl.value = acmUrl;
|
|
596
595
|
const [doorLevels, liftLevels, groups] = await Promise.all([
|
|
597
596
|
_getDoorAccessLevels(acmUrl),
|
|
@@ -203,7 +203,6 @@ const {
|
|
|
203
203
|
getBlockLevelUnitList: _getBlockLevelUnitList,
|
|
204
204
|
} = useAccessManagement();
|
|
205
205
|
const { requiredRule } = useUtils();
|
|
206
|
-
const { getEncryptedAcmUrl } = useSiteEntryPassSettings();
|
|
207
206
|
|
|
208
207
|
const route = useRoute();
|
|
209
208
|
const siteId = computed(() => props.siteId || (route.params.site as string));
|
|
@@ -295,7 +294,7 @@ onMounted(async () => {
|
|
|
295
294
|
|
|
296
295
|
const [blockLevelResult, acmResult] = await Promise.allSettled([
|
|
297
296
|
_getBlockLevelUnitList(siteId.value),
|
|
298
|
-
|
|
297
|
+
$fetch<{ encrypted: string }>("/api/encrypt-acm-url"),
|
|
299
298
|
]);
|
|
300
299
|
|
|
301
300
|
// Blocks / Levels / Units
|
|
@@ -310,7 +309,7 @@ onMounted(async () => {
|
|
|
310
309
|
|
|
311
310
|
// ACM levels (door + lift)
|
|
312
311
|
if (acmResult.status === "fulfilled") {
|
|
313
|
-
const acmUrl = acmResult.value;
|
|
312
|
+
const acmUrl = acmResult.value.encrypted;
|
|
314
313
|
try {
|
|
315
314
|
const [doorLevels, liftLevels] = await Promise.all([
|
|
316
315
|
_getDoorAccessLevels(acmUrl),
|
|
@@ -403,7 +403,6 @@ const emit = defineEmits<{
|
|
|
403
403
|
}>();
|
|
404
404
|
|
|
405
405
|
const { getResidents, assignUser } = useAccessManagement();
|
|
406
|
-
const { getEncryptedAcmUrl } = useSiteEntryPassSettings();
|
|
407
406
|
|
|
408
407
|
const historyDialog = ref(false);
|
|
409
408
|
const showAssignForm = ref(false);
|
|
@@ -476,12 +475,12 @@ async function openAssignForm() {
|
|
|
476
475
|
if (!props.unit?._id || !props.orgId || !props.siteId) return;
|
|
477
476
|
peopleLoading.value = true;
|
|
478
477
|
try {
|
|
479
|
-
const [residentsRes,
|
|
478
|
+
const [residentsRes, acmRes] = await Promise.all([
|
|
480
479
|
getResidents({ orgId: props.orgId, siteId: props.siteId, unitId: props.unit._id }),
|
|
481
|
-
|
|
480
|
+
$fetch<{ encrypted: string }>("/api/encrypt-acm-url"),
|
|
482
481
|
]);
|
|
483
482
|
peopleItems.value = residentsRes?.data ?? [];
|
|
484
|
-
encryptedAcmUrl.value =
|
|
483
|
+
encryptedAcmUrl.value = acmRes?.encrypted ?? "";
|
|
485
484
|
} catch {
|
|
486
485
|
peopleItems.value = [];
|
|
487
486
|
} finally {
|
package/components/AppSelect.vue
CHANGED
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
<template>
|
|
19
19
|
<v-menu
|
|
20
20
|
v-if="!multiple"
|
|
21
|
-
|
|
22
|
-
:close-on-content-click="!searchable"
|
|
21
|
+
:close-on-content-click="true"
|
|
23
22
|
:disabled="disabled"
|
|
24
23
|
location="bottom start"
|
|
25
24
|
offset="4"
|
|
@@ -54,21 +53,8 @@
|
|
|
54
53
|
</template>
|
|
55
54
|
|
|
56
55
|
<div class="app-select__menu" role="listbox">
|
|
57
|
-
<div v-if="searchable" class="app-select__search">
|
|
58
|
-
<v-icon icon="mdi-magnify" size="16" />
|
|
59
|
-
<input
|
|
60
|
-
ref="singleSearchInput"
|
|
61
|
-
v-model="search"
|
|
62
|
-
type="text"
|
|
63
|
-
class="app-select__search-input"
|
|
64
|
-
:placeholder="searchPlaceholder"
|
|
65
|
-
aria-label="Search options"
|
|
66
|
-
@keydown.enter.prevent="selectFirstMatch"
|
|
67
|
-
@keydown.esc.stop="singleOpen = false"
|
|
68
|
-
>
|
|
69
|
-
</div>
|
|
70
56
|
<button
|
|
71
|
-
v-for="opt in
|
|
57
|
+
v-for="opt in normalized"
|
|
72
58
|
:key="String(opt.value)"
|
|
73
59
|
type="button"
|
|
74
60
|
class="app-select__option"
|
|
@@ -80,9 +66,6 @@
|
|
|
80
66
|
<span class="app-select__option-text">{{ opt.title }}</span>
|
|
81
67
|
<v-icon v-if="isSingleOn(opt)" icon="mdi-check" size="17" />
|
|
82
68
|
</button>
|
|
83
|
-
<p v-if="searchable && !visibleOptions.length" class="app-select__empty">
|
|
84
|
-
{{ normalized.length ? "No matches" : "Nothing to choose from" }}
|
|
85
|
-
</p>
|
|
86
69
|
</div>
|
|
87
70
|
</v-menu>
|
|
88
71
|
|
|
@@ -139,18 +122,8 @@
|
|
|
139
122
|
</template>
|
|
140
123
|
|
|
141
124
|
<div class="app-select__menu">
|
|
142
|
-
<div v-if="searchable" class="app-select__search">
|
|
143
|
-
<v-icon icon="mdi-magnify" size="16" />
|
|
144
|
-
<input
|
|
145
|
-
v-model="search"
|
|
146
|
-
type="text"
|
|
147
|
-
class="app-select__search-input"
|
|
148
|
-
:placeholder="searchPlaceholder"
|
|
149
|
-
aria-label="Search options"
|
|
150
|
-
>
|
|
151
|
-
</div>
|
|
152
125
|
<button
|
|
153
|
-
v-for="opt in
|
|
126
|
+
v-for="opt in normalized"
|
|
154
127
|
:key="String(opt.value)"
|
|
155
128
|
type="button"
|
|
156
129
|
class="app-select__option"
|
|
@@ -165,9 +138,6 @@
|
|
|
165
138
|
/>
|
|
166
139
|
<span class="app-select__option-text">{{ opt.title }}</span>
|
|
167
140
|
</button>
|
|
168
|
-
<p v-if="searchable && !visibleOptions.length" class="app-select__empty">
|
|
169
|
-
{{ normalized.length ? "No matches" : "Nothing to choose from" }}
|
|
170
|
-
</p>
|
|
171
141
|
</div>
|
|
172
142
|
</v-menu>
|
|
173
143
|
</template>
|
|
@@ -198,13 +168,6 @@ const props = defineProps({
|
|
|
198
168
|
returnObject: { type: Boolean, default: false },
|
|
199
169
|
clearable: { type: Boolean, default: false },
|
|
200
170
|
placeholder: { type: String, default: "" },
|
|
201
|
-
/**
|
|
202
|
-
* A filter box at the top of the popover. Opt-in, so the 44 consumers that
|
|
203
|
-
* do not ask for it render exactly the DOM they render today - a list short
|
|
204
|
-
* enough to read does not want a search field in front of it.
|
|
205
|
-
*/
|
|
206
|
-
searchable: { type: Boolean, default: false },
|
|
207
|
-
searchPlaceholder: { type: String, default: "Search" },
|
|
208
171
|
});
|
|
209
172
|
|
|
210
173
|
const normalized = computed(() =>
|
|
@@ -215,38 +178,6 @@ const normalized = computed(() =>
|
|
|
215
178
|
)
|
|
216
179
|
);
|
|
217
180
|
|
|
218
|
-
/* ---- search ---------------------------------------------------------- */
|
|
219
|
-
|
|
220
|
-
const search = ref("");
|
|
221
|
-
const singleOpen = ref(false);
|
|
222
|
-
const singleSearchInput = ref<HTMLInputElement | null>(null);
|
|
223
|
-
|
|
224
|
-
/* Case-insensitive substring over what the option READS AS, not its value:
|
|
225
|
-
the title is the only part the user can see to type. */
|
|
226
|
-
const visibleOptions = computed(() => {
|
|
227
|
-
const term = search.value.trim().toLowerCase();
|
|
228
|
-
if (!props.searchable || !term) return normalized.value;
|
|
229
|
-
return normalized.value.filter((opt) =>
|
|
230
|
-
String(opt.title ?? "").toLowerCase().includes(term)
|
|
231
|
-
);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
/* A stale term would hide the list the next time the popover opens, so it is
|
|
235
|
-
dropped on every open - and the caret goes into the box, since opening a
|
|
236
|
-
searchable select is almost always a prelude to typing. */
|
|
237
|
-
watch(singleOpen, async (open) => {
|
|
238
|
-
if (!props.searchable) return;
|
|
239
|
-
search.value = "";
|
|
240
|
-
if (!open) return;
|
|
241
|
-
await nextTick();
|
|
242
|
-
singleSearchInput.value?.focus();
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
function selectFirstMatch() {
|
|
246
|
-
const [first] = visibleOptions.value;
|
|
247
|
-
if (first) selectSingle(first);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
181
|
/* A model entry is either the plain value or - under `return-object` - the
|
|
251
182
|
source item, so everything compares on this one key. */
|
|
252
183
|
const keyOf = (v: any) =>
|
|
@@ -274,11 +205,6 @@ const isSingleOn = (opt: any) =>
|
|
|
274
205
|
function selectSingle(opt: any) {
|
|
275
206
|
if (props.disabled) return;
|
|
276
207
|
model.value = props.returnObject ? opt.raw : String(opt.value);
|
|
277
|
-
// `close-on-content-click` has to be OFF while searching - it fires on the
|
|
278
|
-
// click that puts the caret in the search box, so the popover would shut the
|
|
279
|
-
// moment it was typed into. With it off, picking an option has to close the
|
|
280
|
-
// popover by hand.
|
|
281
|
-
if (props.searchable) singleOpen.value = false;
|
|
282
208
|
}
|
|
283
209
|
|
|
284
210
|
function clearSingle() {
|
|
@@ -161,21 +161,14 @@
|
|
|
161
161
|
</v-col>
|
|
162
162
|
|
|
163
163
|
<v-col v-if="isANPR" cols="12">
|
|
164
|
-
|
|
165
|
-
on edit a blank field keeps it. Required only for a new camera. -->
|
|
166
|
-
<InputLabel
|
|
167
|
-
class="text-capitalize"
|
|
168
|
-
title="Password"
|
|
169
|
-
:required="!isEdit"
|
|
170
|
-
/>
|
|
164
|
+
<InputLabel class="text-capitalize" title="Password" required />
|
|
171
165
|
<v-text-field
|
|
172
166
|
v-model.trim="camera.password"
|
|
173
167
|
type="password"
|
|
174
|
-
autocomplete="new-password"
|
|
175
168
|
class="filter-field"
|
|
176
169
|
density="comfortable"
|
|
177
|
-
:rules="
|
|
178
|
-
|
|
170
|
+
:rules="[requiredRule]"
|
|
171
|
+
placeholder="Enter password"
|
|
179
172
|
/>
|
|
180
173
|
</v-col>
|
|
181
174
|
|
|
@@ -426,12 +419,8 @@ const camera = ref<TSiteCamera>({
|
|
|
426
419
|
|
|
427
420
|
camera.value.site = prop.site;
|
|
428
421
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (isEdit) {
|
|
422
|
+
if (prop.mode === "edit") {
|
|
432
423
|
camera.value = JSON.parse(JSON.stringify(prop.camera));
|
|
433
|
-
// Never pre-filled: blank means "keep the saved password".
|
|
434
|
-
camera.value.password = "";
|
|
435
424
|
}
|
|
436
425
|
camera.value.site = prop.site;
|
|
437
426
|
camera.value.type = prop.type;
|
|
@@ -587,9 +576,7 @@ const hasChanges = computed(() => {
|
|
|
587
576
|
return (
|
|
588
577
|
camera.value.host !== prop.camera.host ||
|
|
589
578
|
camera.value.username !== prop.camera.username ||
|
|
590
|
-
|
|
591
|
-
!!camera.value.password ||
|
|
592
|
-
camera.value.status !== prop.camera.status ||
|
|
579
|
+
camera.value.password !== prop.camera.password ||
|
|
593
580
|
camera.value.category !== prop.camera.category ||
|
|
594
581
|
camera.value.guardPost !== prop.camera.guardPost ||
|
|
595
582
|
camera.value.direction !== prop.camera.direction ||
|
|
@@ -908,8 +895,7 @@ async function save(
|
|
|
908
895
|
const res = await updateSiteCamera(camera.value._id ?? "", {
|
|
909
896
|
...addressPayload(),
|
|
910
897
|
username: camera.value.username,
|
|
911
|
-
|
|
912
|
-
...(camera.value.password ? { password: camera.value.password } : {}),
|
|
898
|
+
password: camera.value.password,
|
|
913
899
|
category: camera.value.category,
|
|
914
900
|
guardPost: camera.value.guardPost,
|
|
915
901
|
direction: camera.value.direction,
|
|
@@ -317,7 +317,7 @@
|
|
|
317
317
|
cols="12"
|
|
318
318
|
sm="6"
|
|
319
319
|
md="4"
|
|
320
|
-
|
|
320
|
+
lg="3"
|
|
321
321
|
class="d-flex"
|
|
322
322
|
>
|
|
323
323
|
<!--
|
|
@@ -1107,19 +1107,14 @@
|
|
|
1107
1107
|
{{ widget.description }}
|
|
1108
1108
|
</div>
|
|
1109
1109
|
</div>
|
|
1110
|
-
<!--
|
|
1111
|
-
`app-switch` is the platform toggle (primitives.css): blue
|
|
1112
|
-
track + white knob right when ON, grey knob left when OFF, in
|
|
1113
|
-
both themes. The old blue-grey colour drew ON as near-black on the
|
|
1114
|
-
dark card and grey on the light one - on and off looked alike.
|
|
1115
|
-
-->
|
|
1116
1110
|
<v-switch
|
|
1117
1111
|
v-model="draftVisibleWidgetKeys"
|
|
1118
1112
|
:value="widget.key"
|
|
1119
|
-
|
|
1113
|
+
color="blue-grey-darken-4"
|
|
1114
|
+
inset
|
|
1120
1115
|
hide-details
|
|
1121
1116
|
density="compact"
|
|
1122
|
-
|
|
1117
|
+
aria-label="Toggle dashboard widget"
|
|
1123
1118
|
/>
|
|
1124
1119
|
</div>
|
|
1125
1120
|
</v-card>
|
|
@@ -1141,7 +1136,7 @@
|
|
|
1141
1136
|
</v-btn>
|
|
1142
1137
|
<v-btn
|
|
1143
1138
|
variant="flat"
|
|
1144
|
-
color="
|
|
1139
|
+
color="blue-grey-darken-4"
|
|
1145
1140
|
class="text-none font-weight-bold px-6"
|
|
1146
1141
|
style="min-width: 110px"
|
|
1147
1142
|
@click="submitCustomize"
|
|
@@ -1232,12 +1227,6 @@ import type { CookieOptions } from "#app";
|
|
|
1232
1227
|
// `plugins/vuetify.ts`), and a bare call resolved to nothing when driven.
|
|
1233
1228
|
import { buildWorkOrderStatus } from "../utils/dashboard";
|
|
1234
1229
|
import { permissionsDenied } from "../utils/module-routes";
|
|
1235
|
-
import { grantsPermission } from "../utils/permission-spellings";
|
|
1236
|
-
import {
|
|
1237
|
-
hiddenDashboardWidgets,
|
|
1238
|
-
saveHiddenDashboardWidgets,
|
|
1239
|
-
type TDashboardWidgetPrefs,
|
|
1240
|
-
} from "../utils/dashboard-widget-prefs";
|
|
1241
1230
|
|
|
1242
1231
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
1243
1232
|
|
|
@@ -1493,20 +1482,21 @@ function canViewWidget(key: string) {
|
|
|
1493
1482
|
return false;
|
|
1494
1483
|
}
|
|
1495
1484
|
|
|
1496
|
-
|
|
1497
|
-
// `*`, means everything. core seeds default roles with `permissions: []`, so
|
|
1498
|
-
// an exact `includes` test showed those accounts an empty dashboard while
|
|
1499
|
-
// every menu in the app let them in. `grantsPermission` also accepts every
|
|
1500
|
-
// shipped spelling of a grant (`work-orders:` / `workOrder:` / ...).
|
|
1501
|
-
if (!permissions.length || permissions.includes("*")) return true;
|
|
1485
|
+
if (permissions.includes("*")) return true;
|
|
1502
1486
|
|
|
1503
1487
|
if (bySchedule) {
|
|
1504
1488
|
return hasSchedulePermission(permissions, "see-all-schedules");
|
|
1505
1489
|
}
|
|
1506
1490
|
|
|
1491
|
+
if (isPropertyMode.value && modulePermissions) {
|
|
1492
|
+
return modulePermissions.some((permission) =>
|
|
1493
|
+
permissions.includes(permission)
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1507
1497
|
if (modulePermissions) {
|
|
1508
1498
|
return modulePermissions.some((permission) =>
|
|
1509
|
-
|
|
1499
|
+
permissions.includes(permission)
|
|
1510
1500
|
);
|
|
1511
1501
|
}
|
|
1512
1502
|
|
|
@@ -2059,28 +2049,33 @@ const securityDefaultWidgetKeys = [
|
|
|
2059
2049
|
];
|
|
2060
2050
|
|
|
2061
2051
|
/**
|
|
2062
|
-
* THE CUSTOMISE CHOICE, REMEMBERED
|
|
2052
|
+
* THE CUSTOMISE CHOICE, REMEMBERED.
|
|
2063
2053
|
*
|
|
2064
|
-
*
|
|
2065
|
-
*
|
|
2066
|
-
* is no preferences API on the server). What is stored and why it is keyed by
|
|
2067
|
-
* user and app is in `utils/dashboard-widget-prefs.ts`.
|
|
2054
|
+
* Hiding a widget worked and then forgot: nothing was written anywhere, so a
|
|
2055
|
+
* reload brought every panel back and the dialog was decoration.
|
|
2068
2056
|
*
|
|
2069
|
-
*
|
|
2070
|
-
*
|
|
2057
|
+
* A COOKIE, and specifically the layer's own `cookieConfig` - the same
|
|
2058
|
+
* mechanism `sid`, `user`, `landing-page` and the light/dark preference already
|
|
2059
|
+
* travel on (see `composables/useThemePreference.ts` for the full reasoning).
|
|
2060
|
+
* localStorage would be per-origin, and this is one component shipped into
|
|
2061
|
+
* seven applications on seven hostnames. No new storage mechanism, and no
|
|
2062
|
+
* invented endpoint - there is no save API on the server for this.
|
|
2063
|
+
*
|
|
2064
|
+
* One cookie per dashboard MODE, because the three modes have different widget
|
|
2065
|
+
* sets and a security member's choice must not decide what a hygiene member
|
|
2066
|
+
* sees.
|
|
2071
2067
|
*/
|
|
2072
|
-
const
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
hiddenDashboardWidgets(widgetPrefsCookie.value, widgetPrefsUser, widgetPrefsApp)
|
|
2068
|
+
const widgetPrefsCookie = useCookie<string[] | null>(
|
|
2069
|
+
`dashboard-widgets-${
|
|
2070
|
+
isPropertyMode.value
|
|
2071
|
+
? "property"
|
|
2072
|
+
: isSecurityMode.value
|
|
2073
|
+
? "security"
|
|
2074
|
+
: "service"
|
|
2075
|
+
}`,
|
|
2076
|
+
runtimeConfig.public.cookieConfig as CookieOptions<string[] | null> & {
|
|
2077
|
+
readonly?: false;
|
|
2078
|
+
}
|
|
2084
2079
|
);
|
|
2085
2080
|
|
|
2086
2081
|
const defaultVisibleWidgetKeys = computed(() => {
|
|
@@ -2101,10 +2096,26 @@ const defaultVisibleWidgetKeys = computed(() => {
|
|
|
2101
2096
|
watch(
|
|
2102
2097
|
defaultVisibleWidgetKeys,
|
|
2103
2098
|
(keys) => {
|
|
2104
|
-
|
|
2105
|
-
|
|
2099
|
+
const nextKeys = [...keys];
|
|
2100
|
+
if (!visibleWidgetKeys.value.length) {
|
|
2101
|
+
// A remembered choice wins on first paint, filtered against the keys this
|
|
2102
|
+
// mode actually has so a stale cookie cannot resurrect a removed widget.
|
|
2103
|
+
// `Array.isArray` rather than a length check: "the user hid everything"
|
|
2104
|
+
// and "there is no cookie" are different answers.
|
|
2105
|
+
const remembered = Array.isArray(widgetPrefsCookie.value)
|
|
2106
|
+
? widgetPrefsCookie.value.filter((k) => nextKeys.includes(k))
|
|
2107
|
+
: null;
|
|
2108
|
+
const initial = remembered ?? nextKeys;
|
|
2109
|
+
visibleWidgetKeys.value = initial;
|
|
2110
|
+
draftVisibleWidgetKeys.value = [...initial];
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
visibleWidgetKeys.value = visibleWidgetKeys.value.filter((k) =>
|
|
2114
|
+
nextKeys.includes(k)
|
|
2115
|
+
);
|
|
2116
|
+
draftVisibleWidgetKeys.value = draftVisibleWidgetKeys.value.filter((k) =>
|
|
2117
|
+
nextKeys.includes(k)
|
|
2106
2118
|
);
|
|
2107
|
-
draftVisibleWidgetKeys.value = [...visibleWidgetKeys.value];
|
|
2108
2119
|
},
|
|
2109
2120
|
{ immediate: true }
|
|
2110
2121
|
);
|
|
@@ -2535,13 +2546,6 @@ const visibleKpiCards = computed(() =>
|
|
|
2535
2546
|
kpiCards.value.filter((card) => isWidgetVisible(card.key))
|
|
2536
2547
|
);
|
|
2537
2548
|
|
|
2538
|
-
// Four tiles fill a row at `lg`; hiding one in Customize left a hole where it
|
|
2539
|
-
// had been. Fewer tiles share the row instead.
|
|
2540
|
-
const kpiColLg = computed(() => {
|
|
2541
|
-
const shown = visibleKpiCards.value.length;
|
|
2542
|
-
return shown >= 4 || shown === 0 ? 3 : 12 / shown;
|
|
2543
|
-
});
|
|
2544
|
-
|
|
2545
2549
|
/**
|
|
2546
2550
|
* Nothing to draw, and which of the two reasons it is.
|
|
2547
2551
|
*
|
|
@@ -3595,20 +3599,10 @@ function resetCustomizeDraft() {
|
|
|
3595
3599
|
}
|
|
3596
3600
|
|
|
3597
3601
|
function submitCustomize() {
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
visibleWidgetKeys.value = defaultVisibleWidgetKeys.value.filter(
|
|
3603
|
-
(k) => !hidden.includes(k)
|
|
3604
|
-
);
|
|
3605
|
-
// The write is the whole point - see `widgetPrefsCookie`.
|
|
3606
|
-
widgetPrefsCookie.value = saveHiddenDashboardWidgets(
|
|
3607
|
-
widgetPrefsCookie.value,
|
|
3608
|
-
widgetPrefsUser,
|
|
3609
|
-
widgetPrefsApp,
|
|
3610
|
-
hidden
|
|
3611
|
-
);
|
|
3602
|
+
visibleWidgetKeys.value = [...draftVisibleWidgetKeys.value];
|
|
3603
|
+
// The write is the whole point - see `widgetPrefsCookie`. Saving on Submit
|
|
3604
|
+
// rather than on every switch keeps Cancel meaning cancel.
|
|
3605
|
+
widgetPrefsCookie.value = [...draftVisibleWidgetKeys.value];
|
|
3612
3606
|
widgetSearch.value = "";
|
|
3613
3607
|
customizeDialog.value = false;
|
|
3614
3608
|
}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
Close
|
|
24
24
|
</v-btn>
|
|
25
25
|
|
|
26
|
-
<v-menu v-if="canUpdate || canDelete
|
|
26
|
+
<v-menu v-if="canUpdate || canDelete" contained>
|
|
27
27
|
<template #activator="{ props }">
|
|
28
28
|
<v-btn
|
|
29
29
|
variant="flat"
|
|
@@ -42,16 +42,6 @@
|
|
|
42
42
|
render-verified either. Only the row type and the destructive ink
|
|
43
43
|
change. -->
|
|
44
44
|
<v-list class="pa-0 update-more__list">
|
|
45
|
-
<!--
|
|
46
|
-
Screen-specific rows, above Edit and the destructive one.
|
|
47
|
-
|
|
48
|
-
Additive: a screen that passes nothing gets the menu it always
|
|
49
|
-
had, and `$slots.actions` is undefined unless a caller actually
|
|
50
|
-
defines the slot - so the button does not appear over an empty
|
|
51
|
-
list when a screen has neither permission nor extra rows.
|
|
52
|
-
-->
|
|
53
|
-
<slot name="actions" />
|
|
54
|
-
|
|
55
45
|
<v-list-item v-if="canUpdate" @click="emit('edit')">
|
|
56
46
|
<v-list-item-title>
|
|
57
47
|
{{ editButtonLabel }}
|