@7365admin1/layer-common 4.70.1 → 4.71.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.71.0
4
+
5
+ ### Minor Changes
6
+
7
+ - fe27542: Members list and Members → Service Providers: a "Last active" column (last sign-in: "Today, 9:05 AM", "Yesterday, …", or the date; "—" until the person next signs in).
8
+
3
9
  ## 4.70.1
4
10
 
5
11
  ### Patch Changes
@@ -1855,11 +1861,11 @@
1855
1861
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
1856
1862
  own rule from two endpoints the console already calls, unprojected:
1857
1863
 
1858
- GET /api/members/user/:user/app/admin the Seven365 staff membership
1859
- GET /api/roles/id/:role that membership's role document
1864
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
1865
+ GET /api/roles/id/:role that membership's role document
1860
1866
 
1861
- owner = member.type === "admin" && role.type === "admin" && role.default === true
1862
- staff = member.type === "admin" && role.type === "admin"
1867
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
1868
+ staff = member.type === "admin" && role.type === "admin"
1863
1869
 
1864
1870
  `role.default` is the marker because it is the only property of a platform
1865
1871
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -113,6 +113,9 @@
113
113
  <template #item.dateInvited="{ item }">
114
114
  {{ formatDateInvited(item.dateInvited) }}
115
115
  </template>
116
+ <template #item.lastLoginAt="{ item }">
117
+ <span data-test="member-last-active">{{ formatLastActive(item.lastLoginAt) }}</span>
118
+ </template>
116
119
  <template #item.action-table="{ item }">
117
120
  <!--
118
121
  `props.assignRole` belongs in this test. The menu's only item that
@@ -331,6 +334,7 @@ import {
331
334
  safeReturnTo,
332
335
  withCurrentRole,
333
336
  } from "../utils/role";
337
+ import { formatLastActive } from "../utils/last-active";
334
338
 
335
339
  const props = defineProps({
336
340
  /** The design's page title. Every app that mounts this screen calls it
@@ -415,6 +419,10 @@ const props = defineProps({
415
419
 
416
420
  value: "dateInvited",
417
421
  },
422
+ {
423
+ title: "Last active",
424
+ value: "lastLoginAt",
425
+ },
418
426
  {
419
427
  title: "Action",
420
428
  value: "action-table",
@@ -111,6 +111,9 @@
111
111
  <template #item.addedAt="{ item }">
112
112
  {{ spDay(item.addedAt) || "-" }}
113
113
  </template>
114
+ <template #item.lastLoginAt="{ item }">
115
+ <span data-test="sp-last-active">{{ formatLastActive(item.lastLoginAt) }}</span>
116
+ </template>
114
117
  <template #item.status="{ item }">
115
118
  <StatusChip
116
119
  :tone="item.status === 'active' ? 'ok' : 'neutral'"
@@ -155,6 +158,7 @@ import {
155
158
  serviceTypeOptions,
156
159
  } from "../utils/site-provider-members";
157
160
  import { spDay } from "../utils/sp-inviter-row";
161
+ import { formatLastActive } from "../utils/last-active";
158
162
  import { saveBlob } from "../composables/useInventory";
159
163
 
160
164
  const props = defineProps({
@@ -195,6 +199,7 @@ const headers = [
195
199
  { title: "Service", key: "typeLabel", sortable: true },
196
200
  { title: "Role at this site", key: "role", sortable: true },
197
201
  { title: "Added", key: "addedAt", sortable: true },
202
+ { title: "Last active", key: "lastLoginAt", sortable: true },
198
203
  { title: "Status", key: "status", sortable: true },
199
204
  { title: "", key: "actions", sortable: false, align: "end" as const },
200
205
  ];
@@ -11,6 +11,8 @@ export type TSiteProviderMember = {
11
11
  role: string;
12
12
  status: string;
13
13
  addedAt?: string | null;
14
+ /** Last sign-in ("Last active"); absent until the next sign-in. */
15
+ lastLoginAt?: string | null;
14
16
  /** a subcontractor: the provider that brought its company onto the site */
15
17
  invitedVia?: string;
16
18
  };
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.70.1",
5
+ "version": "4.71.0",
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.",
@@ -0,0 +1,16 @@
1
+ /**
2
+ * "Last active" on the Members lists: the last time the person signed in
3
+ * (server field `lastLoginAt`). Never signed in since the field existed -> "—".
4
+ */
5
+ export function formatLastActive(value?: string | Date | null, now: Date = new Date()): string {
6
+ if (!value) return "—";
7
+ const date = new Date(value);
8
+ if (Number.isNaN(date.getTime())) return "—";
9
+
10
+ const time = date.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true });
11
+ const dayStart = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
12
+ const days = Math.round((dayStart(now) - dayStart(date)) / 86400000);
13
+ if (days === 0) return `Today, ${time}`;
14
+ if (days === 1) return `Yesterday, ${time}`;
15
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
16
+ }