@7365admin1/layer-common 4.35.1-staging.427 → 4.35.1-staging.428
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/assets/css/primitives.css +13 -0
- package/components/DashboardMain.vue +14 -7
- package/components/InventoryItemDetail.vue +2 -1
- package/components/InventorySettingsTab.vue +2 -5
- package/components/InventoryStockTab.vue +2 -1
- package/components/NotificationBell.vue +1 -1
- package/components/NotificationSettings.vue +1 -1
- package/components/Robot/MiniMap.vue +1 -1
- package/package.json +1 -1
- package/utils/dashboard.ts +11 -0
- package/utils/inventory-store-label.ts +27 -0
|
@@ -982,6 +982,19 @@ select option {
|
|
|
982
982
|
background: var(--accent);
|
|
983
983
|
}
|
|
984
984
|
|
|
985
|
+
/*
|
|
986
|
+
* A plain `v-switch` (no `.app-switch`). Vuetify paints its OFF track with the
|
|
987
|
+
* theme's surface-variant, which this theme sets to near-white (251,251,248),
|
|
988
|
+
* so on a white card an off switch showed only its thumb and did not read as
|
|
989
|
+
* a switch at all (Inventory > Settings, Site Settings, Online Forms...). Give
|
|
990
|
+
* the off track a visible tint in both themes; when ON, Vuetify's `bg-*`
|
|
991
|
+
* colour class (`!important`) still wins, so the checked colour is unchanged.
|
|
992
|
+
*/
|
|
993
|
+
.v-switch:not(.app-switch) .v-switch__track {
|
|
994
|
+
background-color: rgba(var(--v-theme-on-surface), 0.26);
|
|
995
|
+
opacity: 1;
|
|
996
|
+
}
|
|
997
|
+
|
|
985
998
|
.app-switch .v-selection-control__input {
|
|
986
999
|
position: absolute;
|
|
987
1000
|
inset: 0;
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
<span class="text-blue-grey">Balance: {{ item.qty }}</span>
|
|
32
32
|
</div>
|
|
33
33
|
</div>
|
|
34
|
-
<div v-else class="print-kpi-meta mt-2">
|
|
34
|
+
<div v-else class="print-kpi-meta mt-2">
|
|
35
|
+
{{ card.raw?.basis === "below-alert-level" ? "0 · every item is above its alert level" : "No supplies recorded" }}
|
|
36
|
+
</div>
|
|
35
37
|
</template>
|
|
36
38
|
<template v-else>
|
|
37
39
|
<div class="print-kpi-value">{{ card.value }}</div>
|
|
@@ -401,6 +403,11 @@
|
|
|
401
403
|
<span class="text-medium-emphasis">Balance: {{ item.qty }}</span>
|
|
402
404
|
</div>
|
|
403
405
|
</div>
|
|
406
|
+
<!-- Nothing low: say so with the number, not "No data" - that reads as "not set up". -->
|
|
407
|
+
<div v-else-if="card.raw?.basis === 'below-alert-level'" class="d-flex flex-column">
|
|
408
|
+
<span class="text-h5 font-weight-bold text-high-emphasis">0</span>
|
|
409
|
+
<span class="text-body-2 text-medium-emphasis">Every item is above its alert level</span>
|
|
410
|
+
</div>
|
|
404
411
|
<DashboardEmptyState v-else compact />
|
|
405
412
|
</template>
|
|
406
413
|
</AppKpiTile>
|
|
@@ -1230,7 +1237,7 @@ import type { CookieOptions } from "#app";
|
|
|
1230
1237
|
// Explicitly, not by auto-import: every other component in this layer reaches
|
|
1231
1238
|
// its utils by relative path (`StatusChip.vue`, `CameraWall.vue`,
|
|
1232
1239
|
// `plugins/vuetify.ts`), and a bare call resolved to nothing when driven.
|
|
1233
|
-
import { buildWorkOrderStatus } from "../utils/dashboard";
|
|
1240
|
+
import { buildWorkOrderStatus, chartTopValue } from "../utils/dashboard";
|
|
1234
1241
|
import { permissionsDenied } from "../utils/module-routes";
|
|
1235
1242
|
import { grantsPermission } from "../utils/permission-spellings";
|
|
1236
1243
|
import { workerTerm, workerTitle } from "../utils/worker-terms";
|
|
@@ -2155,8 +2162,8 @@ const customizeWidgets = computed(() => {
|
|
|
2155
2162
|
},
|
|
2156
2163
|
{
|
|
2157
2164
|
key: "activity",
|
|
2158
|
-
title: "
|
|
2159
|
-
description: "
|
|
2165
|
+
title: "Activity chart",
|
|
2166
|
+
description: "Work orders opened and closed over the chosen period.",
|
|
2160
2167
|
},
|
|
2161
2168
|
{
|
|
2162
2169
|
key: "upcomingEvents",
|
|
@@ -2217,8 +2224,8 @@ const customizeWidgets = computed(() => {
|
|
|
2217
2224
|
},
|
|
2218
2225
|
{
|
|
2219
2226
|
key: "activity",
|
|
2220
|
-
title: "
|
|
2221
|
-
description: "
|
|
2227
|
+
title: "Activity chart",
|
|
2228
|
+
description: "Work orders opened and closed over the chosen period.",
|
|
2222
2229
|
},
|
|
2223
2230
|
{
|
|
2224
2231
|
key: "activePatrol",
|
|
@@ -2714,7 +2721,7 @@ const secondarySeries = computed(() =>
|
|
|
2714
2721
|
const primaryLegendLabel = computed(() => activityChart.value.primaryLabel);
|
|
2715
2722
|
const secondaryLegendLabel = computed(() => activityChart.value.secondaryLabel);
|
|
2716
2723
|
const chartMax = computed(() =>
|
|
2717
|
-
|
|
2724
|
+
chartTopValue([...primarySeries.value, ...secondarySeries.value])
|
|
2718
2725
|
);
|
|
2719
2726
|
const hasChartData = computed(
|
|
2720
2727
|
() => primarySeries.value.some(Boolean) || secondarySeries.value.some(Boolean)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<div class="flex-grow-1">
|
|
13
13
|
<div class="text-h5 mb-1" :class="item.lowStock ? 'text-error' : ''">{{ item.onHand }} {{ item.unit }} in store</div>
|
|
14
14
|
<div class="text-body-2 text-medium-emphasis">
|
|
15
|
-
{{ [item
|
|
15
|
+
{{ [inventoryItemMeta(item), item.reorderLevel ? `Alert at ${item.reorderLevel}` : "", item.unitCost !== undefined ? `$${Number(item.unitCost).toFixed(2)} each` : ""].filter(Boolean).join(" · ") }}
|
|
16
16
|
</div>
|
|
17
17
|
<div v-if="item.description" class="text-body-2 mt-2">{{ item.description }}</div>
|
|
18
18
|
</div>
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
import useInventory from "../composables/useInventory";
|
|
78
78
|
import useFile from "../composables/useFile";
|
|
79
79
|
import useUtils from "../composables/useUtils";
|
|
80
|
+
import { inventoryItemMeta } from "../utils/inventory-store-label";
|
|
80
81
|
|
|
81
82
|
const show = defineModel<boolean>({ default: false });
|
|
82
83
|
const props = defineProps({
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
<script setup lang="ts">
|
|
40
40
|
import useInventory from "../composables/useInventory";
|
|
41
41
|
import useUtils from "../composables/useUtils";
|
|
42
|
+
import { inventorySiteOptions } from "../utils/inventory-store-label";
|
|
42
43
|
|
|
43
44
|
const props = defineProps({
|
|
44
45
|
store: { type: Object as PropType<TInventoryStore>, required: true },
|
|
@@ -57,11 +58,7 @@ const sites = ref<string[]>([]);
|
|
|
57
58
|
const showInResidentApp = ref(false);
|
|
58
59
|
const showCostInResidentApp = ref(false);
|
|
59
60
|
|
|
60
|
-
const siteItems = computed(() =>
|
|
61
|
-
const known = new Map(props.orgSites.map((s) => [s._id, s.name]));
|
|
62
|
-
for (const id of props.store.sites) if (!known.has(id)) known.set(id, "Current property");
|
|
63
|
-
return [...known].map(([_id, name]) => ({ _id, name }));
|
|
64
|
-
});
|
|
61
|
+
const siteItems = computed(() => inventorySiteOptions(props.orgSites, props.store));
|
|
65
62
|
|
|
66
63
|
function reset() {
|
|
67
64
|
name.value = props.store.name;
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
</v-avatar>
|
|
68
68
|
<div>
|
|
69
69
|
<div class="font-weight-medium">{{ item.name }}</div>
|
|
70
|
-
<div class="text-caption text-medium-emphasis">{{
|
|
70
|
+
<div class="text-caption text-medium-emphasis">{{ inventoryItemMeta(item) }}</div>
|
|
71
71
|
</div>
|
|
72
72
|
</div>
|
|
73
73
|
</template>
|
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
import useInventory from "../composables/useInventory";
|
|
103
103
|
import useFile from "../composables/useFile";
|
|
104
104
|
import useUtils from "../composables/useUtils";
|
|
105
|
+
import { inventoryItemMeta } from "../utils/inventory-store-label";
|
|
105
106
|
|
|
106
107
|
const props = defineProps({
|
|
107
108
|
store: { type: Object as PropType<TInventoryStore>, required: true },
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
<v-col cols="12" md="8" class="screen-hint intro-copy">
|
|
10
10
|
Choose what you want to hear about, and how. These settings are yours
|
|
11
|
-
alone and apply everywhere you use
|
|
11
|
+
alone and apply everywhere you use iService365 — on this site and on your
|
|
12
12
|
phone.
|
|
13
13
|
</v-col>
|
|
14
14
|
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
</RobotFloorPlan>
|
|
50
50
|
|
|
51
51
|
<div class="hero-mini__foot">
|
|
52
|
-
<span>{{ shownMap ? `Robot map · ${shownMap.label}` : "
|
|
52
|
+
<span>{{ shownMap ? `Robot map · ${shownMap.label}` : "Sample layout, not this building's map" }}</span>
|
|
53
53
|
<span v-if="type === 'security'"><span class="hero-mini__legend" aria-hidden="true" />Patrol routes</span>
|
|
54
54
|
<span v-if="showRoutes && hasRoutes"><span class="hero-mini__legend hero-mini__legend--lane" aria-hidden="true" />Robot lanes</span>
|
|
55
55
|
<span v-if="floor !== null" class="hero-mini__count">{{ robots.length }} on {{ floorName(floor) }}</span>
|
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.35.1-staging.
|
|
5
|
+
"version": "4.35.1-staging.428",
|
|
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/dashboard.ts
CHANGED
|
@@ -51,3 +51,14 @@ export function buildWorkOrderStatus(
|
|
|
51
51
|
{ label: "Pending", value: pending, percent: (pending / max) * 100, tone: "warn" },
|
|
52
52
|
];
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The activity chart's top value. The six y-axis labels are max x 0, .2 ... 1
|
|
57
|
+
* rounded, so a raw max of 1 printed "0 0 0 1 1 1" - the same number on four
|
|
58
|
+
* lines. Rounding the top up to a multiple of 5 makes every label a distinct
|
|
59
|
+
* whole number (0 1 2 3 4 5, 0 2 4 6 8 10, ...).
|
|
60
|
+
*/
|
|
61
|
+
export function chartTopValue(values: number[]) {
|
|
62
|
+
const max = Math.max(0, ...values.filter((v) => Number.isFinite(v)));
|
|
63
|
+
return Math.max(5, Math.ceil(max / 5) * 5);
|
|
64
|
+
}
|
|
@@ -18,3 +18,30 @@ export function inventoryStoreLabel(store: TStoreLike, all: TStoreLike[]) {
|
|
|
18
18
|
const sameKind = all.filter((s) => s.ownerType === store.ownerType).length;
|
|
19
19
|
return sameKind > 1 && store.orgName ? `${kind} · ${store.orgName}` : kind;
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The properties a store can be set to supply, by name. The organisation's own
|
|
24
|
+
* site list does not always contain the store's current property (a manager
|
|
25
|
+
* can run a store at a site another organisation owns), so the store's own
|
|
26
|
+
* resolved names fill the gap - a chip must never read "Current property"
|
|
27
|
+
* when the store already knows it is Seventh Condominium.
|
|
28
|
+
*/
|
|
29
|
+
export function inventorySiteOptions(
|
|
30
|
+
orgSites: { _id: string; name: string }[],
|
|
31
|
+
store: { sites: string[]; siteNames?: { _id: string; name?: string }[] },
|
|
32
|
+
) {
|
|
33
|
+
const known = new Map(orgSites.map((s) => [s._id, s.name]));
|
|
34
|
+
for (const id of store.sites) {
|
|
35
|
+
if (!known.has(id)) known.set(id, store.siteNames?.find((s) => s._id === id)?.name || "Another property");
|
|
36
|
+
}
|
|
37
|
+
return [...known].map(([_id, name]) => ({ _id, name }));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type TItemLike = { code?: string; category?: string; packing?: string };
|
|
41
|
+
|
|
42
|
+
/** "Code 001 · Bulb · Packing: 25" - each figure says what it is, never a bare number. */
|
|
43
|
+
export function inventoryItemMeta(item: TItemLike) {
|
|
44
|
+
return [item.code && `Code ${item.code}`, item.category, item.packing && `Packing: ${item.packing}`]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(" · ");
|
|
47
|
+
}
|