@syndew/oluso-admin-ui 0.0.0-stage → 1.30.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/LICENSE +76 -0
- package/README.md +29 -2
- package/fesm2022/syndew-oluso-admin-ui-entitlements-page.component-BeZtIUnk.mjs +149 -0
- package/fesm2022/syndew-oluso-admin-ui-entitlements-page.component-BeZtIUnk.mjs.map +1 -0
- package/fesm2022/syndew-oluso-admin-ui-license-status-page.component-JQmj_qhz.mjs +205 -0
- package/fesm2022/syndew-oluso-admin-ui-license-status-page.component-JQmj_qhz.mjs.map +1 -0
- package/fesm2022/syndew-oluso-admin-ui-platform-clients-page.component-ab3WDRsI.mjs +80 -0
- package/fesm2022/syndew-oluso-admin-ui-platform-clients-page.component-ab3WDRsI.mjs.map +1 -0
- package/fesm2022/syndew-oluso-admin-ui-search-results-page.component-EH82yuW3.mjs +543 -0
- package/fesm2022/syndew-oluso-admin-ui-search-results-page.component-EH82yuW3.mjs.map +1 -0
- package/fesm2022/syndew-oluso-admin-ui-syndew-oluso-admin-ui-BVs_bAxm.mjs +22987 -0
- package/fesm2022/syndew-oluso-admin-ui-syndew-oluso-admin-ui-BVs_bAxm.mjs.map +1 -0
- package/fesm2022/syndew-oluso-admin-ui-team-detail-page.component-CjrQ89qU.mjs +566 -0
- package/fesm2022/syndew-oluso-admin-ui-team-detail-page.component-CjrQ89qU.mjs.map +1 -0
- package/fesm2022/syndew-oluso-admin-ui-unauthorized-page.component-CWYFN6mp.mjs +73 -0
- package/fesm2022/syndew-oluso-admin-ui-unauthorized-page.component-CWYFN6mp.mjs.map +1 -0
- package/fesm2022/syndew-oluso-admin-ui-webhook-detail-page.component-Bh5dZwt-.mjs +728 -0
- package/fesm2022/syndew-oluso-admin-ui-webhook-detail-page.component-Bh5dZwt-.mjs.map +1 -0
- package/fesm2022/syndew-oluso-admin-ui.mjs +3 -0
- package/fesm2022/syndew-oluso-admin-ui.mjs.map +1 -0
- package/help/en/admin.api-scopes.detail.md +43 -0
- package/help/en/admin.api-scopes.md +37 -0
- package/help/en/admin.audit-logs.md +44 -0
- package/help/en/admin.clients.create.md +60 -0
- package/help/en/admin.clients.detail.md +61 -0
- package/help/en/admin.clients.md +48 -0
- package/help/en/admin.dashboard.md +28 -0
- package/help/en/admin.experience.journeys.builder.md +115 -0
- package/help/en/admin.experience.journeys.md +54 -0
- package/help/en/admin.experience.plugins.md +83 -0
- package/help/en/admin.experience.submissions.md +71 -0
- package/help/en/admin.feedback.detail.md +36 -0
- package/help/en/admin.feedback.md +42 -0
- package/help/en/admin.grants.md +42 -0
- package/help/en/admin.identity-providers.detail.md +46 -0
- package/help/en/admin.identity-providers.md +39 -0
- package/help/en/admin.identity-resources.md +47 -0
- package/help/en/admin.module-seats.md +45 -0
- package/help/en/admin.organizations.detail.md +51 -0
- package/help/en/admin.organizations.md +38 -0
- package/help/en/admin.platform.clients.create.md +34 -0
- package/help/en/admin.platform.clients.detail.md +28 -0
- package/help/en/admin.platform.clients.md +38 -0
- package/help/en/admin.resources.detail.md +44 -0
- package/help/en/admin.resources.md +43 -0
- package/help/en/admin.roles.md +33 -0
- package/help/en/admin.settings.license.md +39 -0
- package/help/en/admin.settings.md +70 -0
- package/help/en/admin.signing-keys.md +47 -0
- package/help/en/admin.tenants.detail.md +58 -0
- package/help/en/admin.tenants.md +46 -0
- package/help/en/admin.users.detail.md +41 -0
- package/help/en/admin.users.md +56 -0
- package/help/en/admin.webhooks.detail.md +41 -0
- package/help/en/admin.webhooks.md +37 -0
- package/package.json +51 -3
- package/types/syndew-oluso-admin-ui.d.ts +5786 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, signal, computed, Component } from '@angular/core';
|
|
3
|
+
import { CommonModule } from '@angular/common';
|
|
4
|
+
import * as i1 from '@angular/forms';
|
|
5
|
+
import { FormsModule } from '@angular/forms';
|
|
6
|
+
import { ActivatedRoute, Router } from '@angular/router';
|
|
7
|
+
import { Subject, debounceTime, distinctUntilChanged, switchMap, of } from 'rxjs';
|
|
8
|
+
import { ButtonComponent, BadgeComponent, IconComponent, ModalComponent } from '@syndew/oluso-ui-core';
|
|
9
|
+
import { c as TeamService, U as UserService, d as managedByLabel } from './syndew-oluso-admin-ui-syndew-oluso-admin-ui-BVs_bAxm.mjs';
|
|
10
|
+
|
|
11
|
+
class TeamDetailPageComponent {
|
|
12
|
+
route = inject(ActivatedRoute);
|
|
13
|
+
router = inject(Router);
|
|
14
|
+
teamService = inject(TeamService);
|
|
15
|
+
userService = inject(UserService);
|
|
16
|
+
teamId = signal('', ...(ngDevMode ? [{ debugName: "teamId" }] : /* istanbul ignore next */ []));
|
|
17
|
+
team = signal(null, ...(ngDevMode ? [{ debugName: "team" }] : /* istanbul ignore next */ []));
|
|
18
|
+
members = signal([], ...(ngDevMode ? [{ debugName: "members" }] : /* istanbul ignore next */ []));
|
|
19
|
+
memberUserById = signal(new Map(), ...(ngDevMode ? [{ debugName: "memberUserById" }] : /* istanbul ignore next */ []));
|
|
20
|
+
loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
21
|
+
loadingMembers = signal(false, ...(ngDevMode ? [{ debugName: "loadingMembers" }] : /* istanbul ignore next */ []));
|
|
22
|
+
saving = signal(false, ...(ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []));
|
|
23
|
+
rowBusy = signal(null, ...(ngDevMode ? [{ debugName: "rowBusy" }] : /* istanbul ignore next */ []));
|
|
24
|
+
showAddModal = signal(false, ...(ngDevMode ? [{ debugName: "showAddModal" }] : /* istanbul ignore next */ []));
|
|
25
|
+
showRemoveModal = signal(false, ...(ngDevMode ? [{ debugName: "showRemoveModal" }] : /* istanbul ignore next */ []));
|
|
26
|
+
selectedUser = signal(null, ...(ngDevMode ? [{ debugName: "selectedUser" }] : /* istanbul ignore next */ []));
|
|
27
|
+
userSearchResults = signal([], ...(ngDevMode ? [{ debugName: "userSearchResults" }] : /* istanbul ignore next */ []));
|
|
28
|
+
addMemberError = signal(null, ...(ngDevMode ? [{ debugName: "addMemberError" }] : /* istanbul ignore next */ []));
|
|
29
|
+
memberToRemove = signal(null, ...(ngDevMode ? [{ debugName: "memberToRemove" }] : /* istanbul ignore next */ []));
|
|
30
|
+
addMemberSearchTerm = '';
|
|
31
|
+
addMemberRole = 'Member';
|
|
32
|
+
userSearch$ = new Subject();
|
|
33
|
+
/** Archived teams, and teams a module maintains, can't be edited here. */
|
|
34
|
+
readOnly = computed(() => {
|
|
35
|
+
const t = this.team();
|
|
36
|
+
return !t || !t.isActive || !!t.managedBy;
|
|
37
|
+
}, ...(ngDevMode ? [{ debugName: "readOnly" }] : /* istanbul ignore next */ []));
|
|
38
|
+
managedByLabel = computed(() => managedByLabel(this.team()), ...(ngDevMode ? [{ debugName: "managedByLabel" }] : /* istanbul ignore next */ []));
|
|
39
|
+
memberUserIds = computed(() => new Set(this.members().map((m) => m.userId)), ...(ngDevMode ? [{ debugName: "memberUserIds" }] : /* istanbul ignore next */ []));
|
|
40
|
+
ngOnInit() {
|
|
41
|
+
this.userSearch$
|
|
42
|
+
.pipe(debounceTime(250), distinctUntilChanged(), switchMap((term) => {
|
|
43
|
+
if (!term.trim()) {
|
|
44
|
+
return of({ items: [], totalCount: 0, page: 1, pageSize: 10, totalPages: 0 });
|
|
45
|
+
}
|
|
46
|
+
return this.userService.getUsers({ page: 1, pageSize: 10 }, { search: term.trim() });
|
|
47
|
+
}))
|
|
48
|
+
.subscribe({
|
|
49
|
+
next: (resp) => this.userSearchResults.set(resp.items),
|
|
50
|
+
error: () => this.userSearchResults.set([]),
|
|
51
|
+
});
|
|
52
|
+
this.route.paramMap.subscribe((params) => {
|
|
53
|
+
const id = params.get('id');
|
|
54
|
+
if (!id)
|
|
55
|
+
return;
|
|
56
|
+
this.teamId.set(id);
|
|
57
|
+
this.loadTeam();
|
|
58
|
+
this.loadMembers();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
loadTeam() {
|
|
62
|
+
this.loading.set(true);
|
|
63
|
+
this.teamService.getById(this.teamId()).subscribe({
|
|
64
|
+
next: (t) => {
|
|
65
|
+
this.team.set(t);
|
|
66
|
+
this.loading.set(false);
|
|
67
|
+
},
|
|
68
|
+
error: () => {
|
|
69
|
+
this.team.set(null);
|
|
70
|
+
this.loading.set(false);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
loadMembers() {
|
|
75
|
+
this.loadingMembers.set(true);
|
|
76
|
+
this.teamService.getMembers(this.teamId()).subscribe({
|
|
77
|
+
next: (list) => {
|
|
78
|
+
this.members.set(list);
|
|
79
|
+
this.loadingMembers.set(false);
|
|
80
|
+
this.hydrateMemberNames(list);
|
|
81
|
+
},
|
|
82
|
+
error: () => this.loadingMembers.set(false),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// Resolve display names for the members in one go so the table doesn't
|
|
86
|
+
// show raw user IDs. Done after the membership list lands so we can do a
|
|
87
|
+
// single search query per page rather than N round-trips.
|
|
88
|
+
hydrateMemberNames(members) {
|
|
89
|
+
if (members.length === 0)
|
|
90
|
+
return;
|
|
91
|
+
this.userService.getUsers({ page: 1, pageSize: 200 }, {}).subscribe({
|
|
92
|
+
next: (resp) => {
|
|
93
|
+
const map = new Map();
|
|
94
|
+
for (const u of resp.items)
|
|
95
|
+
map.set(u.id, u);
|
|
96
|
+
this.memberUserById.set(map);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
memberDisplayName(m) {
|
|
101
|
+
const u = this.memberUserById().get(m.userId);
|
|
102
|
+
if (u)
|
|
103
|
+
return u.displayName || u.email || m.userId;
|
|
104
|
+
return m.userId;
|
|
105
|
+
}
|
|
106
|
+
openAddMemberModal() {
|
|
107
|
+
this.addMemberSearchTerm = '';
|
|
108
|
+
this.addMemberRole = 'Member';
|
|
109
|
+
this.selectedUser.set(null);
|
|
110
|
+
this.userSearchResults.set([]);
|
|
111
|
+
this.addMemberError.set(null);
|
|
112
|
+
this.showAddModal.set(true);
|
|
113
|
+
}
|
|
114
|
+
closeAddMemberModal() {
|
|
115
|
+
this.showAddModal.set(false);
|
|
116
|
+
}
|
|
117
|
+
onUserSearch(term) {
|
|
118
|
+
this.addMemberSearchTerm = term;
|
|
119
|
+
this.userSearch$.next(term);
|
|
120
|
+
}
|
|
121
|
+
selectUser(u) {
|
|
122
|
+
if (this.memberUserIds().has(u.id)) {
|
|
123
|
+
this.addMemberError.set(`${u.displayName || u.email} is already a member.`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.addMemberError.set(null);
|
|
127
|
+
this.selectedUser.set(u);
|
|
128
|
+
this.addMemberSearchTerm = u.displayName || u.email || '';
|
|
129
|
+
this.userSearchResults.set([]);
|
|
130
|
+
}
|
|
131
|
+
confirmAddMember() {
|
|
132
|
+
const user = this.selectedUser();
|
|
133
|
+
if (!user)
|
|
134
|
+
return;
|
|
135
|
+
this.saving.set(true);
|
|
136
|
+
this.addMemberError.set(null);
|
|
137
|
+
this.teamService.addMember(this.teamId(), { userId: user.id, role: this.addMemberRole }).subscribe({
|
|
138
|
+
next: () => {
|
|
139
|
+
this.saving.set(false);
|
|
140
|
+
this.closeAddMemberModal();
|
|
141
|
+
this.loadMembers();
|
|
142
|
+
},
|
|
143
|
+
error: (err) => {
|
|
144
|
+
this.saving.set(false);
|
|
145
|
+
this.addMemberError.set(err?.error?.message ?? 'Failed to add member');
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
onRoleChange(m, newRole) {
|
|
150
|
+
if (m.role === newRole)
|
|
151
|
+
return;
|
|
152
|
+
this.rowBusy.set(m.userId);
|
|
153
|
+
this.teamService.changeRole(this.teamId(), m.userId, { role: newRole }).subscribe({
|
|
154
|
+
next: (updated) => {
|
|
155
|
+
this.members.update((list) => list.map((x) => (x.id === m.id ? updated : x)));
|
|
156
|
+
this.rowBusy.set(null);
|
|
157
|
+
},
|
|
158
|
+
error: () => {
|
|
159
|
+
this.rowBusy.set(null);
|
|
160
|
+
this.loadMembers();
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
openRemoveModal(m) {
|
|
165
|
+
this.memberToRemove.set(m);
|
|
166
|
+
this.showRemoveModal.set(true);
|
|
167
|
+
}
|
|
168
|
+
closeRemoveModal() {
|
|
169
|
+
this.showRemoveModal.set(false);
|
|
170
|
+
this.memberToRemove.set(null);
|
|
171
|
+
}
|
|
172
|
+
confirmRemove() {
|
|
173
|
+
const m = this.memberToRemove();
|
|
174
|
+
if (!m)
|
|
175
|
+
return;
|
|
176
|
+
this.saving.set(true);
|
|
177
|
+
this.teamService.removeMember(this.teamId(), m.userId).subscribe({
|
|
178
|
+
next: () => {
|
|
179
|
+
this.saving.set(false);
|
|
180
|
+
this.closeRemoveModal();
|
|
181
|
+
this.loadMembers();
|
|
182
|
+
},
|
|
183
|
+
error: () => this.saving.set(false),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
onBack() {
|
|
187
|
+
this.router.navigate(['/teams']);
|
|
188
|
+
}
|
|
189
|
+
formatDate(iso) {
|
|
190
|
+
return new Date(iso).toLocaleDateString();
|
|
191
|
+
}
|
|
192
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TeamDetailPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
193
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: TeamDetailPageComponent, isStandalone: true, selector: "admin-team-detail-page", ngImport: i0, template: `
|
|
194
|
+
<div class="p-6 space-y-6">
|
|
195
|
+
<div class="flex items-center gap-3">
|
|
196
|
+
<ui-button variant="ghost" (clicked)="onBack()">
|
|
197
|
+
<ui-icon name="arrow-left" class="h-4 w-4 mr-2" />
|
|
198
|
+
Back
|
|
199
|
+
</ui-button>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
@if (loading()) {
|
|
203
|
+
<div class="flex items-center justify-center h-64 text-sm text-gray-500">Loading team…</div>
|
|
204
|
+
} @else if (!team()) {
|
|
205
|
+
<div class="text-sm text-red-600">Team not found.</div>
|
|
206
|
+
} @else {
|
|
207
|
+
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
|
208
|
+
<div class="flex items-start justify-between gap-4">
|
|
209
|
+
<div>
|
|
210
|
+
<div class="flex items-center gap-2">
|
|
211
|
+
<h1 class="text-2xl font-semibold text-gray-900">{{ team()!.name }}</h1>
|
|
212
|
+
<ui-badge [variant]="team()!.isActive ? 'success' : 'default'">
|
|
213
|
+
{{ team()!.isActive ? 'Active' : 'Archived' }}
|
|
214
|
+
</ui-badge>
|
|
215
|
+
@if (team()!.managedBy) {
|
|
216
|
+
<ui-badge variant="info">Managed by {{ managedByLabel() }}</ui-badge>
|
|
217
|
+
}
|
|
218
|
+
</div>
|
|
219
|
+
<p class="mt-1 text-sm text-gray-500 font-mono">{{ team()!.code }} · {{ team()!.type }} · {{ team()!.visibility }}</p>
|
|
220
|
+
@if (team()!.description) {
|
|
221
|
+
<p class="mt-2 text-sm text-gray-700">{{ team()!.description }}</p>
|
|
222
|
+
}
|
|
223
|
+
@if (team()!.managedBy) {
|
|
224
|
+
<p class="mt-2 text-sm text-gray-600">
|
|
225
|
+
{{ managedByLabel() }} creates this team and keeps its members in sync from its own records.
|
|
226
|
+
To change it, make the change in {{ managedByLabel() }}.
|
|
227
|
+
</p>
|
|
228
|
+
}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
234
|
+
<div class="flex items-center justify-between p-4 border-b border-gray-200">
|
|
235
|
+
<div>
|
|
236
|
+
<h2 class="text-sm font-medium text-gray-900">Members ({{ members().length }})</h2>
|
|
237
|
+
<p class="mt-0.5 text-xs text-gray-500">Users who belong to this team. Add members to use the team as an approver or distribution target.</p>
|
|
238
|
+
</div>
|
|
239
|
+
<ui-button (clicked)="openAddMemberModal()" [disabled]="readOnly()">
|
|
240
|
+
<ui-icon name="plus" class="h-4 w-4 mr-1.5" />
|
|
241
|
+
Add member
|
|
242
|
+
</ui-button>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
@if (loadingMembers()) {
|
|
246
|
+
<div class="px-4 py-8 text-sm text-center text-gray-500">Loading members…</div>
|
|
247
|
+
} @else if (members().length === 0) {
|
|
248
|
+
<div class="px-4 py-12 text-center">
|
|
249
|
+
<p class="text-sm text-gray-500">No members yet.</p>
|
|
250
|
+
<p class="mt-1 text-xs text-gray-400">Click "Add member" to assign users to this team.</p>
|
|
251
|
+
</div>
|
|
252
|
+
} @else {
|
|
253
|
+
<table class="w-full text-sm">
|
|
254
|
+
<thead class="bg-gray-50 text-xs uppercase tracking-wide text-gray-500">
|
|
255
|
+
<tr>
|
|
256
|
+
<th class="px-4 py-2 text-left">User</th>
|
|
257
|
+
<th class="px-4 py-2 text-left">Role</th>
|
|
258
|
+
<th class="px-4 py-2 text-left">Joined</th>
|
|
259
|
+
<th class="px-4 py-2 text-right">Actions</th>
|
|
260
|
+
</tr>
|
|
261
|
+
</thead>
|
|
262
|
+
<tbody class="divide-y divide-gray-100">
|
|
263
|
+
@for (m of members(); track m.id) {
|
|
264
|
+
<tr>
|
|
265
|
+
<td class="px-4 py-3">
|
|
266
|
+
<div class="font-medium text-gray-900">{{ memberDisplayName(m) }}</div>
|
|
267
|
+
<div class="text-xs text-gray-500 font-mono">{{ m.userId }}</div>
|
|
268
|
+
</td>
|
|
269
|
+
<td class="px-4 py-3">
|
|
270
|
+
<select
|
|
271
|
+
[ngModel]="m.role"
|
|
272
|
+
(ngModelChange)="onRoleChange(m, $event)"
|
|
273
|
+
[disabled]="readOnly() || rowBusy() === m.userId"
|
|
274
|
+
class="text-sm rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
|
|
275
|
+
>
|
|
276
|
+
<option value="Member">Member</option>
|
|
277
|
+
<option value="Lead">Lead</option>
|
|
278
|
+
</select>
|
|
279
|
+
</td>
|
|
280
|
+
<td class="px-4 py-3 text-gray-500">{{ formatDate(m.joinedAt) }}</td>
|
|
281
|
+
<td class="px-4 py-3 text-right">
|
|
282
|
+
<button
|
|
283
|
+
type="button"
|
|
284
|
+
(click)="openRemoveModal(m)"
|
|
285
|
+
[disabled]="readOnly() || rowBusy() === m.userId"
|
|
286
|
+
class="text-xs text-red-600 hover:text-red-800 disabled:opacity-50"
|
|
287
|
+
>
|
|
288
|
+
Remove
|
|
289
|
+
</button>
|
|
290
|
+
</td>
|
|
291
|
+
</tr>
|
|
292
|
+
}
|
|
293
|
+
</tbody>
|
|
294
|
+
</table>
|
|
295
|
+
}
|
|
296
|
+
</div>
|
|
297
|
+
}
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<ui-modal [open]="showAddModal()" title="Add member" (closed)="closeAddMemberModal()">
|
|
301
|
+
<div class="space-y-4">
|
|
302
|
+
<div>
|
|
303
|
+
<label class="block text-xs font-medium text-gray-700">User</label>
|
|
304
|
+
<input
|
|
305
|
+
type="text"
|
|
306
|
+
[ngModel]="addMemberSearchTerm"
|
|
307
|
+
(ngModelChange)="onUserSearch($event)"
|
|
308
|
+
placeholder="Search by name or email…"
|
|
309
|
+
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 text-sm"
|
|
310
|
+
autocomplete="off"
|
|
311
|
+
/>
|
|
312
|
+
@if (userSearchResults().length > 0) {
|
|
313
|
+
<div class="mt-1 max-h-48 overflow-y-auto border border-gray-200 rounded-md divide-y divide-gray-100">
|
|
314
|
+
@for (u of userSearchResults(); track u.id) {
|
|
315
|
+
<button
|
|
316
|
+
type="button"
|
|
317
|
+
(click)="selectUser(u)"
|
|
318
|
+
class="w-full px-3 py-2 text-left text-sm hover:bg-gray-50"
|
|
319
|
+
[class.bg-primary-50]="selectedUser()?.id === u.id"
|
|
320
|
+
>
|
|
321
|
+
<div class="font-medium text-gray-900">{{ u.displayName || u.email }}</div>
|
|
322
|
+
<div class="text-xs text-gray-500">{{ u.email }}</div>
|
|
323
|
+
</button>
|
|
324
|
+
}
|
|
325
|
+
</div>
|
|
326
|
+
}
|
|
327
|
+
@if (selectedUser()) {
|
|
328
|
+
<p class="mt-2 text-xs text-gray-600">Selected: <strong>{{ selectedUser()!.displayName || selectedUser()!.email }}</strong></p>
|
|
329
|
+
}
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<div>
|
|
333
|
+
<label class="block text-xs font-medium text-gray-700">Role</label>
|
|
334
|
+
<select
|
|
335
|
+
[(ngModel)]="addMemberRole"
|
|
336
|
+
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 text-sm"
|
|
337
|
+
>
|
|
338
|
+
<option value="Member">Member</option>
|
|
339
|
+
<option value="Lead">Lead</option>
|
|
340
|
+
</select>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
@if (addMemberError()) {
|
|
344
|
+
<p class="text-xs text-red-600">{{ addMemberError() }}</p>
|
|
345
|
+
}
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
<div modal-footer class="flex justify-end gap-2">
|
|
349
|
+
<ui-button variant="secondary" (clicked)="closeAddMemberModal()">Cancel</ui-button>
|
|
350
|
+
<ui-button (clicked)="confirmAddMember()" [disabled]="!selectedUser() || saving()">
|
|
351
|
+
{{ saving() ? 'Adding…' : 'Add member' }}
|
|
352
|
+
</ui-button>
|
|
353
|
+
</div>
|
|
354
|
+
</ui-modal>
|
|
355
|
+
|
|
356
|
+
<ui-modal [open]="showRemoveModal()" title="Remove member" (closed)="closeRemoveModal()">
|
|
357
|
+
@if (memberToRemove()) {
|
|
358
|
+
<p class="text-sm text-gray-700">
|
|
359
|
+
Remove <strong>{{ memberDisplayName(memberToRemove()!) }}</strong> from this team?
|
|
360
|
+
</p>
|
|
361
|
+
<p class="mt-2 text-xs text-gray-500">
|
|
362
|
+
The user keeps their account; only the team membership is ended.
|
|
363
|
+
</p>
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
<div modal-footer class="flex justify-end gap-2">
|
|
367
|
+
<ui-button variant="secondary" (clicked)="closeRemoveModal()">Cancel</ui-button>
|
|
368
|
+
<ui-button variant="danger" (clicked)="confirmRemove()" [disabled]="saving()">
|
|
369
|
+
{{ saving() ? 'Removing…' : 'Remove' }}
|
|
370
|
+
</ui-button>
|
|
371
|
+
</div>
|
|
372
|
+
</ui-modal>
|
|
373
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "loading", "type", "fullWidth"], outputs: ["clicked"] }, { kind: "component", type: BadgeComponent, selector: "ui-badge", inputs: ["variant", "size"] }, { kind: "component", type: IconComponent, selector: "ui-icon", inputs: ["name", "strokeWidth"] }, { kind: "component", type: ModalComponent, selector: "ui-modal", inputs: ["open", "title", "size", "showCloseButton", "closeOnBackdrop", "closeOnEscape", "scrollable"], outputs: ["closed"] }] });
|
|
374
|
+
}
|
|
375
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TeamDetailPageComponent, decorators: [{
|
|
376
|
+
type: Component,
|
|
377
|
+
args: [{
|
|
378
|
+
selector: 'admin-team-detail-page',
|
|
379
|
+
standalone: true,
|
|
380
|
+
imports: [CommonModule, FormsModule, ButtonComponent, BadgeComponent, IconComponent, ModalComponent],
|
|
381
|
+
template: `
|
|
382
|
+
<div class="p-6 space-y-6">
|
|
383
|
+
<div class="flex items-center gap-3">
|
|
384
|
+
<ui-button variant="ghost" (clicked)="onBack()">
|
|
385
|
+
<ui-icon name="arrow-left" class="h-4 w-4 mr-2" />
|
|
386
|
+
Back
|
|
387
|
+
</ui-button>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
@if (loading()) {
|
|
391
|
+
<div class="flex items-center justify-center h-64 text-sm text-gray-500">Loading team…</div>
|
|
392
|
+
} @else if (!team()) {
|
|
393
|
+
<div class="text-sm text-red-600">Team not found.</div>
|
|
394
|
+
} @else {
|
|
395
|
+
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
|
396
|
+
<div class="flex items-start justify-between gap-4">
|
|
397
|
+
<div>
|
|
398
|
+
<div class="flex items-center gap-2">
|
|
399
|
+
<h1 class="text-2xl font-semibold text-gray-900">{{ team()!.name }}</h1>
|
|
400
|
+
<ui-badge [variant]="team()!.isActive ? 'success' : 'default'">
|
|
401
|
+
{{ team()!.isActive ? 'Active' : 'Archived' }}
|
|
402
|
+
</ui-badge>
|
|
403
|
+
@if (team()!.managedBy) {
|
|
404
|
+
<ui-badge variant="info">Managed by {{ managedByLabel() }}</ui-badge>
|
|
405
|
+
}
|
|
406
|
+
</div>
|
|
407
|
+
<p class="mt-1 text-sm text-gray-500 font-mono">{{ team()!.code }} · {{ team()!.type }} · {{ team()!.visibility }}</p>
|
|
408
|
+
@if (team()!.description) {
|
|
409
|
+
<p class="mt-2 text-sm text-gray-700">{{ team()!.description }}</p>
|
|
410
|
+
}
|
|
411
|
+
@if (team()!.managedBy) {
|
|
412
|
+
<p class="mt-2 text-sm text-gray-600">
|
|
413
|
+
{{ managedByLabel() }} creates this team and keeps its members in sync from its own records.
|
|
414
|
+
To change it, make the change in {{ managedByLabel() }}.
|
|
415
|
+
</p>
|
|
416
|
+
}
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
422
|
+
<div class="flex items-center justify-between p-4 border-b border-gray-200">
|
|
423
|
+
<div>
|
|
424
|
+
<h2 class="text-sm font-medium text-gray-900">Members ({{ members().length }})</h2>
|
|
425
|
+
<p class="mt-0.5 text-xs text-gray-500">Users who belong to this team. Add members to use the team as an approver or distribution target.</p>
|
|
426
|
+
</div>
|
|
427
|
+
<ui-button (clicked)="openAddMemberModal()" [disabled]="readOnly()">
|
|
428
|
+
<ui-icon name="plus" class="h-4 w-4 mr-1.5" />
|
|
429
|
+
Add member
|
|
430
|
+
</ui-button>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
@if (loadingMembers()) {
|
|
434
|
+
<div class="px-4 py-8 text-sm text-center text-gray-500">Loading members…</div>
|
|
435
|
+
} @else if (members().length === 0) {
|
|
436
|
+
<div class="px-4 py-12 text-center">
|
|
437
|
+
<p class="text-sm text-gray-500">No members yet.</p>
|
|
438
|
+
<p class="mt-1 text-xs text-gray-400">Click "Add member" to assign users to this team.</p>
|
|
439
|
+
</div>
|
|
440
|
+
} @else {
|
|
441
|
+
<table class="w-full text-sm">
|
|
442
|
+
<thead class="bg-gray-50 text-xs uppercase tracking-wide text-gray-500">
|
|
443
|
+
<tr>
|
|
444
|
+
<th class="px-4 py-2 text-left">User</th>
|
|
445
|
+
<th class="px-4 py-2 text-left">Role</th>
|
|
446
|
+
<th class="px-4 py-2 text-left">Joined</th>
|
|
447
|
+
<th class="px-4 py-2 text-right">Actions</th>
|
|
448
|
+
</tr>
|
|
449
|
+
</thead>
|
|
450
|
+
<tbody class="divide-y divide-gray-100">
|
|
451
|
+
@for (m of members(); track m.id) {
|
|
452
|
+
<tr>
|
|
453
|
+
<td class="px-4 py-3">
|
|
454
|
+
<div class="font-medium text-gray-900">{{ memberDisplayName(m) }}</div>
|
|
455
|
+
<div class="text-xs text-gray-500 font-mono">{{ m.userId }}</div>
|
|
456
|
+
</td>
|
|
457
|
+
<td class="px-4 py-3">
|
|
458
|
+
<select
|
|
459
|
+
[ngModel]="m.role"
|
|
460
|
+
(ngModelChange)="onRoleChange(m, $event)"
|
|
461
|
+
[disabled]="readOnly() || rowBusy() === m.userId"
|
|
462
|
+
class="text-sm rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
|
|
463
|
+
>
|
|
464
|
+
<option value="Member">Member</option>
|
|
465
|
+
<option value="Lead">Lead</option>
|
|
466
|
+
</select>
|
|
467
|
+
</td>
|
|
468
|
+
<td class="px-4 py-3 text-gray-500">{{ formatDate(m.joinedAt) }}</td>
|
|
469
|
+
<td class="px-4 py-3 text-right">
|
|
470
|
+
<button
|
|
471
|
+
type="button"
|
|
472
|
+
(click)="openRemoveModal(m)"
|
|
473
|
+
[disabled]="readOnly() || rowBusy() === m.userId"
|
|
474
|
+
class="text-xs text-red-600 hover:text-red-800 disabled:opacity-50"
|
|
475
|
+
>
|
|
476
|
+
Remove
|
|
477
|
+
</button>
|
|
478
|
+
</td>
|
|
479
|
+
</tr>
|
|
480
|
+
}
|
|
481
|
+
</tbody>
|
|
482
|
+
</table>
|
|
483
|
+
}
|
|
484
|
+
</div>
|
|
485
|
+
}
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
<ui-modal [open]="showAddModal()" title="Add member" (closed)="closeAddMemberModal()">
|
|
489
|
+
<div class="space-y-4">
|
|
490
|
+
<div>
|
|
491
|
+
<label class="block text-xs font-medium text-gray-700">User</label>
|
|
492
|
+
<input
|
|
493
|
+
type="text"
|
|
494
|
+
[ngModel]="addMemberSearchTerm"
|
|
495
|
+
(ngModelChange)="onUserSearch($event)"
|
|
496
|
+
placeholder="Search by name or email…"
|
|
497
|
+
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 text-sm"
|
|
498
|
+
autocomplete="off"
|
|
499
|
+
/>
|
|
500
|
+
@if (userSearchResults().length > 0) {
|
|
501
|
+
<div class="mt-1 max-h-48 overflow-y-auto border border-gray-200 rounded-md divide-y divide-gray-100">
|
|
502
|
+
@for (u of userSearchResults(); track u.id) {
|
|
503
|
+
<button
|
|
504
|
+
type="button"
|
|
505
|
+
(click)="selectUser(u)"
|
|
506
|
+
class="w-full px-3 py-2 text-left text-sm hover:bg-gray-50"
|
|
507
|
+
[class.bg-primary-50]="selectedUser()?.id === u.id"
|
|
508
|
+
>
|
|
509
|
+
<div class="font-medium text-gray-900">{{ u.displayName || u.email }}</div>
|
|
510
|
+
<div class="text-xs text-gray-500">{{ u.email }}</div>
|
|
511
|
+
</button>
|
|
512
|
+
}
|
|
513
|
+
</div>
|
|
514
|
+
}
|
|
515
|
+
@if (selectedUser()) {
|
|
516
|
+
<p class="mt-2 text-xs text-gray-600">Selected: <strong>{{ selectedUser()!.displayName || selectedUser()!.email }}</strong></p>
|
|
517
|
+
}
|
|
518
|
+
</div>
|
|
519
|
+
|
|
520
|
+
<div>
|
|
521
|
+
<label class="block text-xs font-medium text-gray-700">Role</label>
|
|
522
|
+
<select
|
|
523
|
+
[(ngModel)]="addMemberRole"
|
|
524
|
+
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 text-sm"
|
|
525
|
+
>
|
|
526
|
+
<option value="Member">Member</option>
|
|
527
|
+
<option value="Lead">Lead</option>
|
|
528
|
+
</select>
|
|
529
|
+
</div>
|
|
530
|
+
|
|
531
|
+
@if (addMemberError()) {
|
|
532
|
+
<p class="text-xs text-red-600">{{ addMemberError() }}</p>
|
|
533
|
+
}
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
<div modal-footer class="flex justify-end gap-2">
|
|
537
|
+
<ui-button variant="secondary" (clicked)="closeAddMemberModal()">Cancel</ui-button>
|
|
538
|
+
<ui-button (clicked)="confirmAddMember()" [disabled]="!selectedUser() || saving()">
|
|
539
|
+
{{ saving() ? 'Adding…' : 'Add member' }}
|
|
540
|
+
</ui-button>
|
|
541
|
+
</div>
|
|
542
|
+
</ui-modal>
|
|
543
|
+
|
|
544
|
+
<ui-modal [open]="showRemoveModal()" title="Remove member" (closed)="closeRemoveModal()">
|
|
545
|
+
@if (memberToRemove()) {
|
|
546
|
+
<p class="text-sm text-gray-700">
|
|
547
|
+
Remove <strong>{{ memberDisplayName(memberToRemove()!) }}</strong> from this team?
|
|
548
|
+
</p>
|
|
549
|
+
<p class="mt-2 text-xs text-gray-500">
|
|
550
|
+
The user keeps their account; only the team membership is ended.
|
|
551
|
+
</p>
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
<div modal-footer class="flex justify-end gap-2">
|
|
555
|
+
<ui-button variant="secondary" (clicked)="closeRemoveModal()">Cancel</ui-button>
|
|
556
|
+
<ui-button variant="danger" (clicked)="confirmRemove()" [disabled]="saving()">
|
|
557
|
+
{{ saving() ? 'Removing…' : 'Remove' }}
|
|
558
|
+
</ui-button>
|
|
559
|
+
</div>
|
|
560
|
+
</ui-modal>
|
|
561
|
+
`,
|
|
562
|
+
}]
|
|
563
|
+
}] });
|
|
564
|
+
|
|
565
|
+
export { TeamDetailPageComponent };
|
|
566
|
+
//# sourceMappingURL=syndew-oluso-admin-ui-team-detail-page.component-CjrQ89qU.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syndew-oluso-admin-ui-team-detail-page.component-CjrQ89qU.mjs","sources":["../../../../libs/admin-ui/src/lib/pages/teams/team-detail-page.component.ts"],"sourcesContent":["import { Component, inject, signal, computed } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { Subject, debounceTime, distinctUntilChanged, switchMap, of } from 'rxjs';\nimport {\n ButtonComponent,\n BadgeComponent,\n IconComponent,\n ModalComponent,\n} from '@syndew/oluso-ui-core';\nimport { TeamService } from '../../services/team.service';\nimport { UserService } from '../../services/user.service';\nimport { Team, TeamMembership, TeamMemberRole, managedByLabel } from '../../models/team.model';\nimport { UserDto } from '../../models/user.model';\n\n@Component({\n selector: 'admin-team-detail-page',\n standalone: true,\n imports: [CommonModule, FormsModule, ButtonComponent, BadgeComponent, IconComponent, ModalComponent],\n template: `\n <div class=\"p-6 space-y-6\">\n <div class=\"flex items-center gap-3\">\n <ui-button variant=\"ghost\" (clicked)=\"onBack()\">\n <ui-icon name=\"arrow-left\" class=\"h-4 w-4 mr-2\" />\n Back\n </ui-button>\n </div>\n\n @if (loading()) {\n <div class=\"flex items-center justify-center h-64 text-sm text-gray-500\">Loading team…</div>\n } @else if (!team()) {\n <div class=\"text-sm text-red-600\">Team not found.</div>\n } @else {\n <div class=\"bg-white border border-gray-200 rounded-lg p-6\">\n <div class=\"flex items-start justify-between gap-4\">\n <div>\n <div class=\"flex items-center gap-2\">\n <h1 class=\"text-2xl font-semibold text-gray-900\">{{ team()!.name }}</h1>\n <ui-badge [variant]=\"team()!.isActive ? 'success' : 'default'\">\n {{ team()!.isActive ? 'Active' : 'Archived' }}\n </ui-badge>\n @if (team()!.managedBy) {\n <ui-badge variant=\"info\">Managed by {{ managedByLabel() }}</ui-badge>\n }\n </div>\n <p class=\"mt-1 text-sm text-gray-500 font-mono\">{{ team()!.code }} · {{ team()!.type }} · {{ team()!.visibility }}</p>\n @if (team()!.description) {\n <p class=\"mt-2 text-sm text-gray-700\">{{ team()!.description }}</p>\n }\n @if (team()!.managedBy) {\n <p class=\"mt-2 text-sm text-gray-600\">\n {{ managedByLabel() }} creates this team and keeps its members in sync from its own records.\n To change it, make the change in {{ managedByLabel() }}.\n </p>\n }\n </div>\n </div>\n </div>\n\n <div class=\"bg-white border border-gray-200 rounded-lg\">\n <div class=\"flex items-center justify-between p-4 border-b border-gray-200\">\n <div>\n <h2 class=\"text-sm font-medium text-gray-900\">Members ({{ members().length }})</h2>\n <p class=\"mt-0.5 text-xs text-gray-500\">Users who belong to this team. Add members to use the team as an approver or distribution target.</p>\n </div>\n <ui-button (clicked)=\"openAddMemberModal()\" [disabled]=\"readOnly()\">\n <ui-icon name=\"plus\" class=\"h-4 w-4 mr-1.5\" />\n Add member\n </ui-button>\n </div>\n\n @if (loadingMembers()) {\n <div class=\"px-4 py-8 text-sm text-center text-gray-500\">Loading members…</div>\n } @else if (members().length === 0) {\n <div class=\"px-4 py-12 text-center\">\n <p class=\"text-sm text-gray-500\">No members yet.</p>\n <p class=\"mt-1 text-xs text-gray-400\">Click \"Add member\" to assign users to this team.</p>\n </div>\n } @else {\n <table class=\"w-full text-sm\">\n <thead class=\"bg-gray-50 text-xs uppercase tracking-wide text-gray-500\">\n <tr>\n <th class=\"px-4 py-2 text-left\">User</th>\n <th class=\"px-4 py-2 text-left\">Role</th>\n <th class=\"px-4 py-2 text-left\">Joined</th>\n <th class=\"px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n <tbody class=\"divide-y divide-gray-100\">\n @for (m of members(); track m.id) {\n <tr>\n <td class=\"px-4 py-3\">\n <div class=\"font-medium text-gray-900\">{{ memberDisplayName(m) }}</div>\n <div class=\"text-xs text-gray-500 font-mono\">{{ m.userId }}</div>\n </td>\n <td class=\"px-4 py-3\">\n <select\n [ngModel]=\"m.role\"\n (ngModelChange)=\"onRoleChange(m, $event)\"\n [disabled]=\"readOnly() || rowBusy() === m.userId\"\n class=\"text-sm rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500\"\n >\n <option value=\"Member\">Member</option>\n <option value=\"Lead\">Lead</option>\n </select>\n </td>\n <td class=\"px-4 py-3 text-gray-500\">{{ formatDate(m.joinedAt) }}</td>\n <td class=\"px-4 py-3 text-right\">\n <button\n type=\"button\"\n (click)=\"openRemoveModal(m)\"\n [disabled]=\"readOnly() || rowBusy() === m.userId\"\n class=\"text-xs text-red-600 hover:text-red-800 disabled:opacity-50\"\n >\n Remove\n </button>\n </td>\n </tr>\n }\n </tbody>\n </table>\n }\n </div>\n }\n </div>\n\n <ui-modal [open]=\"showAddModal()\" title=\"Add member\" (closed)=\"closeAddMemberModal()\">\n <div class=\"space-y-4\">\n <div>\n <label class=\"block text-xs font-medium text-gray-700\">User</label>\n <input\n type=\"text\"\n [ngModel]=\"addMemberSearchTerm\"\n (ngModelChange)=\"onUserSearch($event)\"\n placeholder=\"Search by name or email…\"\n class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 text-sm\"\n autocomplete=\"off\"\n />\n @if (userSearchResults().length > 0) {\n <div class=\"mt-1 max-h-48 overflow-y-auto border border-gray-200 rounded-md divide-y divide-gray-100\">\n @for (u of userSearchResults(); track u.id) {\n <button\n type=\"button\"\n (click)=\"selectUser(u)\"\n class=\"w-full px-3 py-2 text-left text-sm hover:bg-gray-50\"\n [class.bg-primary-50]=\"selectedUser()?.id === u.id\"\n >\n <div class=\"font-medium text-gray-900\">{{ u.displayName || u.email }}</div>\n <div class=\"text-xs text-gray-500\">{{ u.email }}</div>\n </button>\n }\n </div>\n }\n @if (selectedUser()) {\n <p class=\"mt-2 text-xs text-gray-600\">Selected: <strong>{{ selectedUser()!.displayName || selectedUser()!.email }}</strong></p>\n }\n </div>\n\n <div>\n <label class=\"block text-xs font-medium text-gray-700\">Role</label>\n <select\n [(ngModel)]=\"addMemberRole\"\n class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 text-sm\"\n >\n <option value=\"Member\">Member</option>\n <option value=\"Lead\">Lead</option>\n </select>\n </div>\n\n @if (addMemberError()) {\n <p class=\"text-xs text-red-600\">{{ addMemberError() }}</p>\n }\n </div>\n\n <div modal-footer class=\"flex justify-end gap-2\">\n <ui-button variant=\"secondary\" (clicked)=\"closeAddMemberModal()\">Cancel</ui-button>\n <ui-button (clicked)=\"confirmAddMember()\" [disabled]=\"!selectedUser() || saving()\">\n {{ saving() ? 'Adding…' : 'Add member' }}\n </ui-button>\n </div>\n </ui-modal>\n\n <ui-modal [open]=\"showRemoveModal()\" title=\"Remove member\" (closed)=\"closeRemoveModal()\">\n @if (memberToRemove()) {\n <p class=\"text-sm text-gray-700\">\n Remove <strong>{{ memberDisplayName(memberToRemove()!) }}</strong> from this team?\n </p>\n <p class=\"mt-2 text-xs text-gray-500\">\n The user keeps their account; only the team membership is ended.\n </p>\n }\n\n <div modal-footer class=\"flex justify-end gap-2\">\n <ui-button variant=\"secondary\" (clicked)=\"closeRemoveModal()\">Cancel</ui-button>\n <ui-button variant=\"danger\" (clicked)=\"confirmRemove()\" [disabled]=\"saving()\">\n {{ saving() ? 'Removing…' : 'Remove' }}\n </ui-button>\n </div>\n </ui-modal>\n `,\n})\nexport class TeamDetailPageComponent {\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n private readonly teamService = inject(TeamService);\n private readonly userService = inject(UserService);\n\n readonly teamId = signal<string>('');\n readonly team = signal<Team | null>(null);\n readonly members = signal<TeamMembership[]>([]);\n readonly memberUserById = signal<Map<string, UserDto>>(new Map());\n readonly loading = signal(true);\n readonly loadingMembers = signal(false);\n readonly saving = signal(false);\n readonly rowBusy = signal<string | null>(null);\n\n readonly showAddModal = signal(false);\n readonly showRemoveModal = signal(false);\n readonly selectedUser = signal<UserDto | null>(null);\n readonly userSearchResults = signal<UserDto[]>([]);\n readonly addMemberError = signal<string | null>(null);\n readonly memberToRemove = signal<TeamMembership | null>(null);\n\n addMemberSearchTerm = '';\n addMemberRole: TeamMemberRole = 'Member';\n\n private readonly userSearch$ = new Subject<string>();\n\n /** Archived teams, and teams a module maintains, can't be edited here. */\n readonly readOnly = computed(() => {\n const t = this.team();\n return !t || !t.isActive || !!t.managedBy;\n });\n readonly managedByLabel = computed(() => managedByLabel(this.team()));\n\n readonly memberUserIds = computed(() => new Set(this.members().map((m) => m.userId)));\n\n ngOnInit(): void {\n this.userSearch$\n .pipe(\n debounceTime(250),\n distinctUntilChanged(),\n switchMap((term) => {\n if (!term.trim()) {\n return of({ items: [] as UserDto[], totalCount: 0, page: 1, pageSize: 10, totalPages: 0 });\n }\n return this.userService.getUsers({ page: 1, pageSize: 10 }, { search: term.trim() });\n }),\n )\n .subscribe({\n next: (resp) => this.userSearchResults.set(resp.items),\n error: () => this.userSearchResults.set([]),\n });\n\n this.route.paramMap.subscribe((params) => {\n const id = params.get('id');\n if (!id) return;\n this.teamId.set(id);\n this.loadTeam();\n this.loadMembers();\n });\n }\n\n private loadTeam(): void {\n this.loading.set(true);\n this.teamService.getById(this.teamId()).subscribe({\n next: (t) => {\n this.team.set(t);\n this.loading.set(false);\n },\n error: () => {\n this.team.set(null);\n this.loading.set(false);\n },\n });\n }\n\n private loadMembers(): void {\n this.loadingMembers.set(true);\n this.teamService.getMembers(this.teamId()).subscribe({\n next: (list) => {\n this.members.set(list);\n this.loadingMembers.set(false);\n this.hydrateMemberNames(list);\n },\n error: () => this.loadingMembers.set(false),\n });\n }\n\n // Resolve display names for the members in one go so the table doesn't\n // show raw user IDs. Done after the membership list lands so we can do a\n // single search query per page rather than N round-trips.\n private hydrateMemberNames(members: TeamMembership[]): void {\n if (members.length === 0) return;\n this.userService.getUsers({ page: 1, pageSize: 200 }, {}).subscribe({\n next: (resp) => {\n const map = new Map<string, UserDto>();\n for (const u of resp.items) map.set(u.id, u);\n this.memberUserById.set(map);\n },\n });\n }\n\n memberDisplayName(m: TeamMembership): string {\n const u = this.memberUserById().get(m.userId);\n if (u) return u.displayName || u.email || m.userId;\n return m.userId;\n }\n\n openAddMemberModal(): void {\n this.addMemberSearchTerm = '';\n this.addMemberRole = 'Member';\n this.selectedUser.set(null);\n this.userSearchResults.set([]);\n this.addMemberError.set(null);\n this.showAddModal.set(true);\n }\n\n closeAddMemberModal(): void {\n this.showAddModal.set(false);\n }\n\n onUserSearch(term: string): void {\n this.addMemberSearchTerm = term;\n this.userSearch$.next(term);\n }\n\n selectUser(u: UserDto): void {\n if (this.memberUserIds().has(u.id)) {\n this.addMemberError.set(`${u.displayName || u.email} is already a member.`);\n return;\n }\n this.addMemberError.set(null);\n this.selectedUser.set(u);\n this.addMemberSearchTerm = u.displayName || u.email || '';\n this.userSearchResults.set([]);\n }\n\n confirmAddMember(): void {\n const user = this.selectedUser();\n if (!user) return;\n this.saving.set(true);\n this.addMemberError.set(null);\n this.teamService.addMember(this.teamId(), { userId: user.id, role: this.addMemberRole }).subscribe({\n next: () => {\n this.saving.set(false);\n this.closeAddMemberModal();\n this.loadMembers();\n },\n error: (err) => {\n this.saving.set(false);\n this.addMemberError.set(err?.error?.message ?? 'Failed to add member');\n },\n });\n }\n\n onRoleChange(m: TeamMembership, newRole: TeamMemberRole): void {\n if (m.role === newRole) return;\n this.rowBusy.set(m.userId);\n this.teamService.changeRole(this.teamId(), m.userId, { role: newRole }).subscribe({\n next: (updated) => {\n this.members.update((list) => list.map((x) => (x.id === m.id ? updated : x)));\n this.rowBusy.set(null);\n },\n error: () => {\n this.rowBusy.set(null);\n this.loadMembers();\n },\n });\n }\n\n openRemoveModal(m: TeamMembership): void {\n this.memberToRemove.set(m);\n this.showRemoveModal.set(true);\n }\n\n closeRemoveModal(): void {\n this.showRemoveModal.set(false);\n this.memberToRemove.set(null);\n }\n\n confirmRemove(): void {\n const m = this.memberToRemove();\n if (!m) return;\n this.saving.set(true);\n this.teamService.removeMember(this.teamId(), m.userId).subscribe({\n next: () => {\n this.saving.set(false);\n this.closeRemoveModal();\n this.loadMembers();\n },\n error: () => this.saving.set(false),\n });\n }\n\n onBack(): void {\n this.router.navigate(['/teams']);\n }\n\n formatDate(iso: string): string {\n return new Date(iso).toLocaleDateString();\n }\n}\n"],"names":[],"mappings":"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"}
|