@7365admin1/layer-common 4.70.1 → 4.71.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 +16 -4
- package/components/InventoryStockTab.vue +16 -4
- package/components/MemberMain.vue +8 -0
- package/components/MemberServiceProviders.vue +5 -0
- package/composables/useInventory.ts +1 -1
- package/composables/useServiceProvider.ts +2 -0
- package/package.json +1 -1
- package/utils/inventory-legacy.ts +10 -0
- package/utils/last-active.ts +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.71.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0a25e73: Inventory: show "Copy from old Equipment list" only when the store's sites have old Equipment records not yet copied.
|
|
8
|
+
|
|
9
|
+
## 4.71.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 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).
|
|
14
|
+
|
|
3
15
|
## 4.70.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -1855,11 +1867,11 @@
|
|
|
1855
1867
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
1856
1868
|
own rule from two endpoints the console already calls, unprojected:
|
|
1857
1869
|
|
|
1858
|
-
|
|
1859
|
-
|
|
1870
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
1871
|
+
GET /api/roles/id/:role that membership's role document
|
|
1860
1872
|
|
|
1861
|
-
|
|
1862
|
-
|
|
1873
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
1874
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
1863
1875
|
|
|
1864
1876
|
`role.default` is the marker because it is the only property of a platform
|
|
1865
1877
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
<v-list density="compact">
|
|
33
33
|
<v-list-item prepend-icon="mdi-plus" title="Add one item" @click="editItem(null)" />
|
|
34
34
|
<v-list-item prepend-icon="mdi-file-excel-outline" title="Import from Excel" @click="showImport = true" />
|
|
35
|
-
<v-list-item prepend-icon="mdi-content-copy" title="Copy from old Equipment list" @click="showLegacy = true" />
|
|
35
|
+
<v-list-item v-if="legacyToCopy" prepend-icon="mdi-content-copy" title="Copy from old Equipment list" @click="showLegacy = true" />
|
|
36
36
|
</v-list>
|
|
37
37
|
</v-menu>
|
|
38
38
|
<v-spacer />
|
|
@@ -104,6 +104,7 @@ import useFile from "../composables/useFile";
|
|
|
104
104
|
import useUtils from "../composables/useUtils";
|
|
105
105
|
import { inventoryItemMeta } from "../utils/inventory-store-label";
|
|
106
106
|
import { inventoryControls } from "../utils/inventory-presets";
|
|
107
|
+
import { hasLegacyEquipmentToCopy } from "../utils/inventory-legacy";
|
|
107
108
|
|
|
108
109
|
const props = defineProps({
|
|
109
110
|
store: { type: Object as PropType<TInventoryStore>, required: true },
|
|
@@ -113,7 +114,7 @@ const props = defineProps({
|
|
|
113
114
|
const ctl = computed(() => inventoryControls(props.store.access));
|
|
114
115
|
const emit = defineEmits(["changed"]);
|
|
115
116
|
|
|
116
|
-
const { getItems } = useInventory();
|
|
117
|
+
const { getItems, getLegacyEquipment } = useInventory();
|
|
117
118
|
const { getFileUrl } = useFile();
|
|
118
119
|
const { debounce } = useUtils();
|
|
119
120
|
|
|
@@ -179,15 +180,26 @@ function openItem(item: TInventoryItem) {
|
|
|
179
180
|
const showImport = ref(false);
|
|
180
181
|
const showReceiveImport = ref(false);
|
|
181
182
|
const showLegacy = ref(false);
|
|
183
|
+
// Offer the old Equipment copy only where this store's sites have old records not yet copied.
|
|
184
|
+
const legacyToCopy = ref(false);
|
|
185
|
+
async function checkLegacy() {
|
|
186
|
+
if (!props.store.access.canManage) return (legacyToCopy.value = false);
|
|
187
|
+
try {
|
|
188
|
+
legacyToCopy.value = hasLegacyEquipmentToCopy(await getLegacyEquipment(props.store._id));
|
|
189
|
+
} catch {
|
|
190
|
+
legacyToCopy.value = false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
182
193
|
|
|
183
194
|
function done() {
|
|
184
195
|
load();
|
|
196
|
+
checkLegacy();
|
|
185
197
|
emit("changed");
|
|
186
198
|
}
|
|
187
199
|
|
|
188
200
|
watch(page, load);
|
|
189
201
|
watch(lowOnly, () => (page.value === 1 ? load() : (page.value = 1)));
|
|
190
202
|
watch(search, debounce(() => (page.value === 1 ? load() : (page.value = 1)), 400));
|
|
191
|
-
watch(() => props.store._id, () => (page.value === 1 ? load() : (page.value = 1)));
|
|
192
|
-
onMounted(load);
|
|
203
|
+
watch(() => props.store._id, () => (checkLegacy(), page.value === 1 ? load() : (page.value = 1)));
|
|
204
|
+
onMounted(() => (load(), checkLegacy()));
|
|
193
205
|
</script>
|
|
@@ -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
|
];
|
|
@@ -65,7 +65,7 @@ export default function useInventory() {
|
|
|
65
65
|
api<Blob>(`/stores/${store}/reports/${report}/export`, { query, responseType: "blob" }),
|
|
66
66
|
checkUsage: (store: string, ids: string[], checked: boolean) =>
|
|
67
67
|
api<{ updated: number }>(`/stores/${store}/usage/check`, { method: "POST", body: { ids, checked } }),
|
|
68
|
-
getLegacyEquipment: (store: string) => api<{ serviceType: string; count: number }[]>(`/stores/${store}/legacy-equipment`),
|
|
68
|
+
getLegacyEquipment: (store: string) => api<{ serviceType: string; count: number; pending?: number }[]>(`/stores/${store}/legacy-equipment`),
|
|
69
69
|
importLegacyEquipment: (store: string, serviceType: string, commit: boolean) =>
|
|
70
70
|
api<{ committed: boolean; ready: number; skipped: number; errors: number; rows: { row: number; name: string; status: string; message: string }[] }>(
|
|
71
71
|
`/stores/${store}/legacy-equipment/import`,
|
|
@@ -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.
|
|
5
|
+
"version": "4.71.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.",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type TLegacyGroup = { count: number; pending?: number };
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether "Copy from old Equipment list" has anything to bring in: false for a
|
|
5
|
+
* site that never had an old Equipment list, and false once every old name is
|
|
6
|
+
* already in the store. An API without `pending` falls back to `count`.
|
|
7
|
+
*/
|
|
8
|
+
export function hasLegacyEquipmentToCopy(groups: TLegacyGroup[] | null | undefined) {
|
|
9
|
+
return (groups ?? []).some((g) => (g.pending ?? g.count) > 0);
|
|
10
|
+
}
|
|
@@ -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
|
+
}
|