@7365admin1/layer-common 4.82.1-staging.476 → 4.82.1-staging.478
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.
|
@@ -1176,6 +1176,33 @@ select option {
|
|
|
1176
1176
|
}
|
|
1177
1177
|
}
|
|
1178
1178
|
|
|
1179
|
+
/* Phones: tables restack into one card per row (Vuetify mobile mode, set in
|
|
1180
|
+
plugins/vuetify.ts), so the 720px grid floor above must not apply. */
|
|
1181
|
+
@media (max-width: 599.98px) {
|
|
1182
|
+
.app-table-scroll > * {
|
|
1183
|
+
min-width: 0;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
.v-data-table__tr--mobile {
|
|
1187
|
+
border-bottom: 1px solid var(--border);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
.v-data-table__tr--mobile > td {
|
|
1191
|
+
grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
|
|
1192
|
+
column-gap: 12px;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
.v-data-table__td-title {
|
|
1196
|
+
color: var(--muted);
|
|
1197
|
+
font-size: 12px;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
.v-data-table__td-value {
|
|
1201
|
+
min-width: 0;
|
|
1202
|
+
overflow-wrap: anywhere;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1179
1206
|
/* ------------------------------------------------------------------ */
|
|
1180
1207
|
/* Field label (`components/InputLabel.vue`, 125 caller files). */
|
|
1181
1208
|
/* */
|
|
@@ -22,21 +22,10 @@
|
|
|
22
22
|
</template>
|
|
23
23
|
</PageHeader>
|
|
24
24
|
|
|
25
|
-
<!--
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
:key="c.orgId"
|
|
30
|
-
type="button"
|
|
31
|
-
class="sp-chip"
|
|
32
|
-
:class="{ 'sp-chip--active': company === c.orgId }"
|
|
33
|
-
:aria-pressed="company === c.orgId"
|
|
34
|
-
@click="company = company === c.orgId ? '' : c.orgId"
|
|
35
|
-
>
|
|
36
|
-
<span class="sp-chip__name">{{ companyChipText(c.name, c.people) }}</span>
|
|
37
|
-
<span class="sp-chip__type">{{ c.typeLabel }}<template v-if="c.invitedVia"> · via {{ c.invitedVia }}</template></span>
|
|
38
|
-
</button>
|
|
39
|
-
</div>
|
|
25
|
+
<!-- Who is on the list at a glance; pick one company with the Company filter. -->
|
|
26
|
+
<p v-if="companies.length" class="sp-summary mb-3" data-test="sp-members-summary">
|
|
27
|
+
{{ summaryText }}
|
|
28
|
+
</p>
|
|
40
29
|
|
|
41
30
|
<AppCard class="table-card">
|
|
42
31
|
<CardToolbar :count="countText">
|
|
@@ -75,6 +64,25 @@
|
|
|
75
64
|
aria-label="Service"
|
|
76
65
|
data-test="sp-members-type"
|
|
77
66
|
/>
|
|
67
|
+
<v-autocomplete
|
|
68
|
+
v-model="company"
|
|
69
|
+
:items="companyItems"
|
|
70
|
+
item-title="title"
|
|
71
|
+
item-value="value"
|
|
72
|
+
density="compact"
|
|
73
|
+
variant="outlined"
|
|
74
|
+
hide-details
|
|
75
|
+
clearable
|
|
76
|
+
placeholder="All companies"
|
|
77
|
+
no-data-text="No company for this service"
|
|
78
|
+
class="sp-company-filter"
|
|
79
|
+
aria-label="Company"
|
|
80
|
+
data-test="sp-members-company"
|
|
81
|
+
>
|
|
82
|
+
<template #item="{ props: itemProps, item }">
|
|
83
|
+
<v-list-item v-bind="itemProps" :subtitle="item.raw.subtitle" />
|
|
84
|
+
</template>
|
|
85
|
+
</v-autocomplete>
|
|
78
86
|
<AppButton
|
|
79
87
|
variant="icon"
|
|
80
88
|
icon="mdi-refresh"
|
|
@@ -151,10 +159,11 @@ import type {
|
|
|
151
159
|
TSiteProviderMember,
|
|
152
160
|
} from "../composables/useServiceProvider";
|
|
153
161
|
import {
|
|
154
|
-
|
|
162
|
+
companyFilterItems,
|
|
155
163
|
concernMailto,
|
|
156
164
|
filterProviderMembers,
|
|
157
165
|
providerMembersEmptyText,
|
|
166
|
+
providerSummaryText,
|
|
158
167
|
serviceTypeOptions,
|
|
159
168
|
} from "../utils/site-provider-members";
|
|
160
169
|
import { spDay } from "../utils/sp-inviter-row";
|
|
@@ -189,7 +198,7 @@ const loading = ref(false);
|
|
|
189
198
|
const exporting = ref(false);
|
|
190
199
|
const search = ref("");
|
|
191
200
|
const type = ref("");
|
|
192
|
-
const company = ref("");
|
|
201
|
+
const company = ref<string | null>("");
|
|
193
202
|
const snackbar = ref(false);
|
|
194
203
|
const message = ref("");
|
|
195
204
|
|
|
@@ -209,9 +218,18 @@ const typeItems = computed(() => [
|
|
|
209
218
|
...serviceTypeOptions(items.value),
|
|
210
219
|
]);
|
|
211
220
|
|
|
221
|
+
const companyItems = computed(() => companyFilterItems(companies.value, type.value));
|
|
222
|
+
const summaryText = computed(() => providerSummaryText(companies.value));
|
|
223
|
+
// A company of another service cannot match; drop it when the service changes.
|
|
224
|
+
watch(type, () => {
|
|
225
|
+
if (company.value && !companyItems.value.some((c) => c.value === company.value)) {
|
|
226
|
+
company.value = "";
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
212
230
|
const filters = computed(() => ({
|
|
213
231
|
type: type.value,
|
|
214
|
-
company: company.value,
|
|
232
|
+
company: company.value || "",
|
|
215
233
|
search: search.value,
|
|
216
234
|
}));
|
|
217
235
|
|
|
@@ -294,41 +312,21 @@ onMounted(async () => {
|
|
|
294
312
|
</script>
|
|
295
313
|
|
|
296
314
|
<style scoped>
|
|
297
|
-
.sp-
|
|
298
|
-
display: flex;
|
|
299
|
-
flex-wrap: wrap;
|
|
300
|
-
gap: 8px;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
.sp-chip {
|
|
304
|
-
display: flex;
|
|
305
|
-
flex-direction: column;
|
|
306
|
-
align-items: flex-start;
|
|
307
|
-
padding: 8px 12px;
|
|
308
|
-
border: 1px solid var(--border);
|
|
309
|
-
border-radius: var(--r-inner);
|
|
310
|
-
background: var(--card);
|
|
311
|
-
color: var(--text);
|
|
312
|
-
text-align: left;
|
|
313
|
-
cursor: pointer;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
.sp-chip--active {
|
|
317
|
-
border-color: var(--accent-strong);
|
|
318
|
-
box-shadow: inset 0 0 0 1px var(--accent-strong);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
.sp-chip__name {
|
|
322
|
-
font-weight: 600;
|
|
315
|
+
.sp-summary {
|
|
323
316
|
font-size: 14px;
|
|
317
|
+
color: var(--muted);
|
|
324
318
|
}
|
|
325
319
|
|
|
326
|
-
.sp-chip__type,
|
|
327
320
|
.sp-sub {
|
|
328
321
|
font-size: 12px;
|
|
329
322
|
color: var(--muted);
|
|
330
323
|
}
|
|
331
324
|
|
|
325
|
+
.sp-company-filter {
|
|
326
|
+
min-width: 220px;
|
|
327
|
+
max-width: 300px;
|
|
328
|
+
}
|
|
329
|
+
|
|
332
330
|
.sp-type-filter {
|
|
333
331
|
min-width: 170px;
|
|
334
332
|
max-width: 220px;
|
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.82.1-staging.
|
|
5
|
+
"version": "4.82.1-staging.478",
|
|
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.",
|
package/plugins/vuetify.ts
CHANGED
|
@@ -114,6 +114,12 @@ export default defineNuxtPlugin((app) => {
|
|
|
114
114
|
|
|
115
115
|
const vuetify = createVuetify({
|
|
116
116
|
defaults: {
|
|
117
|
+
// Phones (owner 2026-10-02, "most pages are not mobile responsive"): below
|
|
118
|
+
// 600px every data table shows each row as a stacked card (label: value)
|
|
119
|
+
// instead of a wide grid scrolled sideways. Tablets and desktop unchanged.
|
|
120
|
+
VDataTable: { mobileBreakpoint: "sm" },
|
|
121
|
+
VDataTableServer: { mobileBreakpoint: "sm" },
|
|
122
|
+
VDataTableVirtual: { mobileBreakpoint: "sm" },
|
|
117
123
|
VTextField: {
|
|
118
124
|
variant: "outlined",
|
|
119
125
|
density: "comfortable",
|
|
@@ -43,6 +43,43 @@ export function companyChipText(name: string, people: number): string {
|
|
|
43
43
|
return `${name || "Unnamed company"} · ${people} ${people === 1 ? "person" : "people"}`;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The Company filter (owner 2026-10-02: a proper filter instead of a row of
|
|
48
|
+
* company chips): one entry per company, narrowed to the chosen service,
|
|
49
|
+
* named with its service, head count and who brought it in.
|
|
50
|
+
*/
|
|
51
|
+
export function companyFilterItems(
|
|
52
|
+
companies: Array<{ orgId: string; name?: string; type?: string; typeLabel?: string; people: number; invitedVia?: string }>,
|
|
53
|
+
type = "",
|
|
54
|
+
): Array<{ title: string; value: string; subtitle: string }> {
|
|
55
|
+
return companies
|
|
56
|
+
.filter((c) => !type || c.type === type)
|
|
57
|
+
.map((c) => ({
|
|
58
|
+
title: c.name || "Unnamed company",
|
|
59
|
+
value: c.orgId,
|
|
60
|
+
subtitle: [
|
|
61
|
+
c.typeLabel || c.type || "",
|
|
62
|
+
`${c.people} ${c.people === 1 ? "person" : "people"}`,
|
|
63
|
+
c.invitedVia ? `via ${c.invitedVia}` : "",
|
|
64
|
+
]
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.join(" · "),
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** "7 companies · 37 people · 1 company has not added anyone yet" */
|
|
71
|
+
export function providerSummaryText(companies: Array<{ people: number }>): string {
|
|
72
|
+
const people = companies.reduce((n, c) => n + (c.people || 0), 0);
|
|
73
|
+
const idle = companies.filter((c) => !c.people).length;
|
|
74
|
+
return [
|
|
75
|
+
`${companies.length} ${companies.length === 1 ? "company" : "companies"}`,
|
|
76
|
+
`${people} ${people === 1 ? "person" : "people"}`,
|
|
77
|
+
idle ? `${idle} ${idle === 1 ? "company has" : "companies have"} not added anyone yet` : "",
|
|
78
|
+
]
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.join(" · ");
|
|
81
|
+
}
|
|
82
|
+
|
|
46
83
|
/** What an empty table says: nothing on site, nobody added yet, or the filters. */
|
|
47
84
|
export function providerMembersEmptyText({
|
|
48
85
|
companies,
|