@7365admin1/layer-common 4.79.0 → 4.80.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.80.1
4
+
5
+ ### Patch Changes
6
+
7
+ - dbfc623: Robot detail: each command's outcome line names its command ("Return to charger: …"), and a fault that was already showing when the command was sent is reported as an earlier fault to clear first, not as the new command's result.
8
+
9
+ ## 4.80.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 200cb0b: Service Providers > Invitations: the status line says who must act while an invitation waits ("Waiting for a Seven365 Super Admin to approve - the email is sent after approval") and, once approved, whether the email went out ("Email sent 30 Sept, 14:14" or "Email failed: <reason> - use Resend", in red). Reads `emailStatus` from core 3.134+; older rows keep "Sent - waiting for their answer".
14
+
3
15
  ## 4.79.0
4
16
 
5
17
  ### Minor Changes
@@ -1932,11 +1944,11 @@
1932
1944
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
1933
1945
  own rule from two endpoints the console already calls, unprojected:
1934
1946
 
1935
- GET /api/members/user/:user/app/admin the Seven365 staff membership
1936
- GET /api/roles/id/:role that membership's role document
1947
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
1948
+ GET /api/roles/id/:role that membership's role document
1937
1949
 
1938
- owner = member.type === "admin" && role.type === "admin" && role.default === true
1939
- staff = member.type === "admin" && role.type === "admin"
1950
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
1951
+ staff = member.type === "admin" && role.type === "admin"
1940
1952
 
1941
1953
  `role.default` is the marker because it is the only property of a platform
1942
1954
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -633,17 +633,18 @@ watch([reportsPosition, online], ([r, o], old) => {
633
633
  const busy = ref<TRobotCommandKind | "spot" | null>(null);
634
634
  /** The last command's real outcome, kept on screen (a toast vanishes in 2.6 s). */
635
635
  const commandResult = ref<{ text: string; warn: boolean } | null>(null);
636
- const watching = ref<{ kind: string; at: number; what?: string } | null>(null);
636
+ const watching = ref<{ kind: string; at: number; what?: string; label?: string; faultBefore?: string } | null>(null);
637
637
  watch([lastAt, now], () => {
638
638
  const w = watching.value;
639
639
  const at = lastAt.value?.getTime() ?? 0;
640
640
  // Only a reading taken after the command was accepted counts (never the optimistic status).
641
641
  if (!w || at <= w.at + 300) return;
642
- const out = commandOutcome(w.kind, polled.value?.ok ? polled.value : null, Date.now() - w.at, { name: record.value?.name, what: w.what });
642
+ const out = commandOutcome(w.kind, polled.value?.ok ? polled.value : null, Date.now() - w.at, { name: record.value?.name, what: w.what, faultBefore: w.faultBefore });
643
643
  if (!out) return;
644
644
  // Keep following (Leaving the charger… -> On the way to P4… -> Arrived at P4) until it is done.
645
645
  if (out.done) watching.value = null;
646
- commandResult.value = { text: out.text, warn: !out.ok };
646
+ // Each line names its own command, so a new command never looks like the last one's result.
647
+ commandResult.value = { text: w.label ? `${w.label}: ${out.text}` : out.text, warn: !out.ok };
647
648
  if (!out.ok) optimistic.value = null;
648
649
  });
649
650
  watch(
@@ -656,6 +657,8 @@ watch(
656
657
  async function run(kind: TRobotCommandKind | "spot", body: Record<string, unknown>, what?: string) {
657
658
  const name = record.value?.name ?? "the robot";
658
659
  const before = statusMode.value;
660
+ // A fault already showing is not the result of this command (commandOutcome).
661
+ const faultBefore = polled.value?.ok && typeof polled.value.fault?.message === "string" ? polled.value.fault.message : undefined;
659
662
  const docked = before === "CHARGED" || before === "CHARGING_CYCLE" || /At charger/.test(String(live.value?.ok ? live.value.statusName ?? "" : ""));
660
663
  busy.value = kind;
661
664
  optimistic.value = { mode: optimisticMode(kind, before), after: Number.POSITIVE_INFINITY };
@@ -673,7 +676,7 @@ async function run(kind: TRobotCommandKind | "spot", body: Record<string, unknow
673
676
  // "Accepted" is not "done": watch real readings for up to 30 s (commandOutcome).
674
677
  const outcomeKind = body.kind === "go-to-point" ? "go-to-point" : kind;
675
678
  if (commandOutcome(outcomeKind, { mode: "" }, COMMAND_WATCH_MS) !== null) {
676
- watching.value = { kind: String(outcomeKind), at: Date.now(), what };
679
+ watching.value = { kind: String(outcomeKind), at: Date.now(), what, label, faultBefore };
677
680
  fastUntil.value = Date.now() + COMMAND_FOLLOW_MS;
678
681
  commandResult.value = { text: sentText(docked), warn: false };
679
682
  } else commandResult.value = { text: `${label} → ${name} · accepted.`, warn: false };
@@ -140,7 +140,11 @@
140
140
  :status="item.viewStatus || item.statusRaw || item.status"
141
141
  :label="item.status"
142
142
  />
143
- <div v-if="item.statusLine" class="mt-1 sp-sub">{{ item.statusLine }}</div>
143
+ <div
144
+ v-if="item.statusLine"
145
+ class="mt-1 sp-sub"
146
+ :class="{ 'text-error': item.statusLine.startsWith('Email failed') }"
147
+ >{{ item.statusLine }}</div>
144
148
  <!-- Was `text-caption text-medium-emphasis` - Roboto 12px at
145
149
  Vuetify's 60% opacity. The design's sub-line is `--muted`. -->
146
150
  <div v-if="item.rejectionReason" class="mt-1 sp-reject-reason">
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.79.0",
5
+ "version": "4.80.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.",
@@ -1823,7 +1823,7 @@ export function commandOutcome(
1823
1823
  kind: unknown,
1824
1824
  reading: { online?: unknown; mode?: unknown; statusName?: unknown; fault?: { message?: unknown } | null } | null | undefined,
1825
1825
  elapsedMs: number,
1826
- input: { name?: unknown; what?: unknown } = {},
1826
+ input: { name?: unknown; what?: unknown; faultBefore?: unknown } = {},
1827
1827
  ): { ok: boolean; text: string; done: boolean } | null {
1828
1828
  const started = STARTED[String(kind)];
1829
1829
  if (!started || !reading) return null;
@@ -1837,6 +1837,10 @@ export function commandOutcome(
1837
1837
  }
1838
1838
  if (elapsedMs < COMMAND_WATCH_MS) return null;
1839
1839
  const fault = typeof reading.fault?.message === "string" ? reading.fault.message : "";
1840
+ // The fault was already there when this command was sent: it is not this command's result.
1841
+ if (fault && fault === input.faultBefore) {
1842
+ return { ok: false, done: true, text: `${who} is still stopped by an earlier fault (${fault}). Clear it on the robot or in its maker's app, then try again.` };
1843
+ }
1840
1844
  const reason = reading.online === false ? "it stopped reporting" : fault || (said && !RESTING.test(said) ? said : "");
1841
1845
  return {
1842
1846
  ok: false,
@@ -19,8 +19,19 @@ export type TSentInvitation = {
19
19
  createdAt?: string | null;
20
20
  answeredAt?: string | null;
21
21
  onSite?: boolean;
22
+ /** The latest invitation-email attempt, from the server (core 3.134+). */
23
+ emailStatus?: { state?: "sent" | "failed"; at?: string | null; reason?: string } | null;
22
24
  };
23
25
 
26
+ /** "30 Sep, 14:14" - when the email went out. */
27
+ export function spDayTime(value?: string | null): string {
28
+ if (!value) return "";
29
+ const d = new Date(value);
30
+ return Number.isNaN(d.getTime())
31
+ ? ""
32
+ : d.toLocaleString("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
33
+ }
34
+
24
35
  const ANSWERED: Record<string, string> = {
25
36
  complete: "Accepted",
26
37
  declined: "Declined",
@@ -91,7 +102,20 @@ export function sentStatusLine(row: TSentInvitation): string {
91
102
  return `Accepted${when ? ` on ${when}` : ""} · no longer on this site`;
92
103
  }
93
104
  if (ANSWERED[status]) return when ? `${ANSWERED[status]} on ${when}` : "";
94
- if (status === "awaiting-approval") return "Seven365 checks it before it is sent";
95
- if (status === "pending") return "Sent - waiting for their answer";
105
+ // Who has to act, and that nothing is emailed until they do.
106
+ if (status === "awaiting-approval") {
107
+ return "Waiting for a Seven365 Super Admin to approve - the email is sent after approval";
108
+ }
109
+ if (status === "pending") {
110
+ const email = row.emailStatus;
111
+ if (email?.state === "failed") {
112
+ return `Email failed: ${email.reason || "could not be sent"} - use Resend`;
113
+ }
114
+ if (email?.state === "sent") {
115
+ const when = spDayTime(email.at);
116
+ return `Email sent${when ? ` ${when}` : ""} - waiting for their answer`;
117
+ }
118
+ return "Sent - waiting for their answer";
119
+ }
96
120
  return "";
97
121
  }