@7365admin1/layer-common 4.65.0 → 4.65.2

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,23 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.65.2
4
+
5
+ ### Patch Changes
6
+
7
+ - dbd4245: Inventory Stock Movement: the Received column labels opening stock received in the month ("incl. N opening stock").
8
+
9
+ ## 4.65.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 5df1751: Signed-in QA sweep fixes (plain words and readable controls):
14
+ - Inventory Settings names every property it supplies (was "Current property" when the organisation's site list did not include it).
15
+ - Inventory item lines label the packing ("Code 001 · Bulb · Packing: 25") instead of a bare number.
16
+ - Dashboard Low Stock tile shows 0 and "Every item is above its alert level" instead of "No data to display".
17
+ - 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.
18
+ - A plain on/off switch now has a visible off track in light mode.
19
+ - Bell panel title "Notifications"; notification settings say "iService365"; the robot mini-map no longer shows "(placeholder)".
20
+
3
21
  ## 4.65.0
4
22
 
5
23
  ### Minor Changes
@@ -1755,11 +1773,11 @@
1755
1773
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
1756
1774
  own rule from two endpoints the console already calls, unprojected:
1757
1775
 
1758
- GET /api/members/user/:user/app/admin the Seven365 staff membership
1759
- GET /api/roles/id/:role that membership's role document
1776
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
1777
+ GET /api/roles/id/:role that membership's role document
1760
1778
 
1761
- owner = member.type === "admin" && role.type === "admin" && role.default === true
1762
- staff = member.type === "admin" && role.type === "admin"
1779
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
1780
+ staff = member.type === "admin" && role.type === "admin"
1763
1781
 
1764
1782
  `role.default` is the marker because it is the only property of a platform
1765
1783
  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({
@@ -112,6 +112,9 @@
112
112
  <template #item.locationType="{ value }">{{ value === "tenant_unit" ? "Tenant unit" : "Common area" }}</template>
113
113
  <template #item.unitCost="{ value }">{{ money(value) }}</template>
114
114
  <template #item.totalCost="{ value }">{{ money(value) }}</template>
115
+ <template #item.received="{ item }">
116
+ {{ item.received }}<span v-if="item.receivedOpening" class="d-block text-caption text-medium-emphasis">incl. {{ item.receivedOpening }} opening stock</span>
117
+ </template>
115
118
  <template #item.receivedCost="{ value }">{{ money(value) }}</template>
116
119
  <template #item.usedCost="{ value }">{{ money(value) }}</template>
117
120
  <template #item.siteUsedCost="{ value }">{{ money(value) }}</template>
@@ -181,7 +184,7 @@ const barWidth = (v: number) => {
181
184
  };
182
185
 
183
186
  const help: Record<string, string> = {
184
- movement: "For each item: stock at the start of the month, what came in, what was issued and returned, count differences, and stock at the end.",
187
+ movement: "For each item: stock at the start of the month, what came in (deliveries and opening stock), what was issued and returned, count differences, and stock at the end.",
185
188
  usage: "Every recorded usage: where it was used, for which tenant, by whom, and the cost. Use this for tenant billing. Tick records and mark them checked once verified.",
186
189
  technicians: "For each person: stock they held at the start, took, used, returned, and hold at the end of the month.",
187
190
  purchases: "Every delivery received, and opening stock entered with new items (Type: Opening stock), with totals by month and by supplier.",
@@ -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.2",
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
+ }