@7365admin1/layer-common 4.65.0 → 4.65.1

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.65.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 5df1751: Signed-in QA sweep fixes (plain words and readable controls):
8
+ - Inventory Settings names every property it supplies (was "Current property" when the organisation's site list did not include it).
9
+ - Inventory item lines label the packing ("Code 001 · Bulb · Packing: 25") instead of a bare number.
10
+ - Dashboard Low Stock tile shows 0 and "Every item is above its alert level" instead of "No data to display".
11
+ - Dashboard activity chart y-axis shows distinct whole numbers (was "0 0 0 1 1 1"); the Customize entry is called "Activity chart" and describes what it draws.
12
+ - A plain on/off switch now has a visible off track in light mode.
13
+ - Bell panel title "Notifications"; notification settings say "iService365"; the robot mini-map no longer shows "(placeholder)".
14
+
3
15
  ## 4.65.0
4
16
 
5
17
  ### Minor Changes
@@ -1755,11 +1767,11 @@
1755
1767
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
1756
1768
  own rule from two endpoints the console already calls, unprojected:
1757
1769
 
1758
- GET /api/members/user/:user/app/admin the Seven365 staff membership
1759
- GET /api/roles/id/:role that membership's role document
1770
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
1771
+ GET /api/roles/id/:role that membership's role document
1760
1772
 
1761
- owner = member.type === "admin" && role.type === "admin" && role.default === true
1762
- staff = member.type === "admin" && role.type === "admin"
1773
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
1774
+ staff = member.type === "admin" && role.type === "admin"
1763
1775
 
1764
1776
  `role.default` is the marker because it is the only property of a platform
1765
1777
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -939,6 +939,19 @@ select option {
939
939
  background: var(--accent);
940
940
  }
941
941
 
942
+ /*
943
+ * A plain `v-switch` (no `.app-switch`). Vuetify paints its OFF track with the
944
+ * theme's surface-variant, which this theme sets to near-white (251,251,248),
945
+ * so on a white card an off switch showed only its thumb and did not read as
946
+ * a switch at all (Inventory > Settings, Site Settings, Online Forms...). Give
947
+ * the off track a visible tint in both themes; when ON, Vuetify's `bg-*`
948
+ * colour class (`!important`) still wins, so the checked colour is unchanged.
949
+ */
950
+ .v-switch:not(.app-switch) .v-switch__track {
951
+ background-color: rgba(var(--v-theme-on-surface), 0.26);
952
+ opacity: 1;
953
+ }
954
+
942
955
  .app-switch .v-selection-control__input {
943
956
  position: absolute;
944
957
  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">No supplies recorded</div>
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: "This Week Activity (Line Chart)",
2159
- description: "Compare incidents and work orders over time.",
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: "This Week Activity (Line Chart)",
2221
- description: "Compare incidents and work orders over time.",
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
- Math.max(1, ...primarySeries.value, ...secondarySeries.value)
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.code && `Code ${item.code}`, item.category, item.packing, item.reorderLevel ? `Alert at ${item.reorderLevel}` : "", item.unitCost !== undefined ? `$${Number(item.unitCost).toFixed(2)} each` : ""].filter(Boolean).join(" · ") }}
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">{{ [item.code, item.category, item.packing].filter(Boolean).join(" · ") }}</div>
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 },
@@ -30,7 +30,7 @@
30
30
 
31
31
  <v-card :width="440" elevation="0" class="notif-panel">
32
32
  <div class="notif-panel__head">
33
- <span>Notification</span>
33
+ <span>Notifications</span>
34
34
  <v-btn
35
35
  v-if="unread > 0"
36
36
  variant="text"
@@ -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 iService — on this site and on your
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}` : "Floor plan from robot (placeholder)" }}</span>
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.65.0",
5
+ "version": "4.65.1",
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.",
@@ -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
+ }