Skip to content

Commit d2508ec

Browse files
ayman-aboelabbasclaude
andcommitted
feat(admin-cms): user detail page, profile menu, and permission fixes
- Rebuild user detail page: avatar with initials fallback, active/inactive status chip, country name resolved via API (locale-aware), translated knowledge level and locale preference, roles as translated chips - Remove Assign Roles from detail page (no longer needed) - Fix RTL back arrow: arrow_forward in Arabic, arrow_back in English - Replace header username label with account_circle icon button that opens a mat-menu dropdown showing full name, email, and sign out action - Scope UserDelete permission to SuperAdmin only; Admin keeps ALL_PERMISSIONS without delete; add UserDelete to contracts and SUPER_ADMIN_PERMISSIONS - Fix paginator i18n: subscribe to translationLoadSuccess so labels update once the translation file finishes loading (not just on lang change) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ffdd00f commit d2508ec

9 files changed

Lines changed: 249 additions & 88 deletions

File tree

frontend/apps/admin-cms/src/app/core/auth/auth.service.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,13 @@ const ALL_PERMISSIONS: readonly CcePermission[] = [
3737
CcePermission.SettingsManage,
3838
];
3939

40+
const SUPER_ADMIN_PERMISSIONS: readonly CcePermission[] = [
41+
...ALL_PERMISSIONS,
42+
CcePermission.UserDelete,
43+
];
44+
4045
const PERMISSIONS_BY_ROLE: Record<CceAdminRole, readonly CcePermission[]> = {
41-
[CceAdminRole.SuperAdmin]: ALL_PERMISSIONS,
46+
[CceAdminRole.SuperAdmin]: SUPER_ADMIN_PERMISSIONS,
4247
[CceAdminRole.Admin]: ALL_PERMISSIONS,
4348
[CceAdminRole.ContentManager]: [
4449
CcePermission.ResourceCenterUpload,
@@ -104,11 +109,11 @@ export class AuthService {
104109
}
105110
this._accessToken.set(tokens.accessToken);
106111
localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken);
107-
// Fall back to ALL_PERMISSIONS when the API returns an empty roles array.
112+
// Fall back to SUPER_ADMIN_PERMISSIONS when the API returns an empty roles array.
108113
// Remove once the backend includes roles in the login/refresh response.
109114
const permissions = tokens.user.roles.length > 0
110115
? derivePermissions(tokens.user.roles)
111-
: ALL_PERMISSIONS;
116+
: SUPER_ADMIN_PERMISSIONS;
112117
this._currentUser.set({ ...tokens.user, permissions });
113118
const delay = new Date(tokens.accessTokenExpiresAtUtc).getTime() - Date.now() - 60_000;
114119
if (delay > 0) {

frontend/apps/admin-cms/src/app/core/layout/auth-toolbar.component.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,66 @@
11
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
22
import { MatButtonModule } from '@angular/material/button';
3+
import { MatDividerModule } from '@angular/material/divider';
34
import { MatIconModule } from '@angular/material/icon';
5+
import { MatMenuModule } from '@angular/material/menu';
46
import { TranslocoModule } from '@jsverse/transloco';
57
import { AuthService } from '../auth/auth.service';
68

79
@Component({
810
selector: 'cce-auth-toolbar',
911
standalone: true,
10-
imports: [MatButtonModule, MatIconModule, TranslocoModule],
12+
imports: [MatButtonModule, MatDividerModule, MatIconModule, MatMenuModule, TranslocoModule],
1113
template: `
1214
@if (isAuthenticated()) {
13-
<span class="cce-auth-toolbar__label">{{ userLabel() }}</span>
14-
<button mat-icon-button shellHeaderEnd (click)="signOut()" [attr.aria-label]="'account.logout.button' | transloco">
15-
<mat-icon>logout</mat-icon>
15+
<button mat-icon-button shellHeaderEnd [matMenuTriggerFor]="profileMenu"
16+
[attr.aria-label]="'account.profile' | transloco">
17+
<mat-icon>account_circle</mat-icon>
1618
</button>
19+
20+
<mat-menu #profileMenu="matMenu" class="cce-profile-menu">
21+
<div class="cce-profile-menu__header" (click)="$event.stopPropagation()">
22+
<span class="cce-profile-menu__name">{{ fullName() }}</span>
23+
<span class="cce-profile-menu__email">{{ email() }}</span>
24+
</div>
25+
<mat-divider />
26+
<button mat-menu-item (click)="signOut()">
27+
<mat-icon>logout</mat-icon>
28+
{{ 'account.logout.button' | transloco }}
29+
</button>
30+
</mat-menu>
1731
}
1832
`,
19-
styles: [`.cce-auth-toolbar__label { font-size: 0.875rem; opacity: 0.8; margin-inline-end: 0.25rem; }`],
33+
styles: [`
34+
.cce-profile-menu__header {
35+
display: flex;
36+
flex-direction: column;
37+
padding: 12px 16px;
38+
pointer-events: none;
39+
}
40+
.cce-profile-menu__name {
41+
font-weight: 600;
42+
font-size: 0.9rem;
43+
line-height: 1.4;
44+
}
45+
.cce-profile-menu__email {
46+
font-size: 0.8rem;
47+
color: rgba(0,0,0,0.55);
48+
}
49+
`],
2050
changeDetection: ChangeDetectionStrategy.OnPush,
2151
})
2252
export class AuthToolbarComponent {
2353
private readonly auth = inject(AuthService);
2454

2555
readonly isAuthenticated = this.auth.isAuthenticated;
26-
readonly userLabel = computed(() => {
56+
57+
readonly fullName = computed(() => {
2758
const u = this.auth.currentUser();
2859
return u ? `${u.firstName} ${u.lastName}`.trim() : '';
2960
});
3061

62+
readonly email = computed(() => this.auth.currentUser()?.emailAddress ?? '');
63+
3164
async signOut(): Promise<void> {
3265
await this.auth.signOut();
3366
}
Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<div class="cce-user-detail__header">
22
<a mat-button routerLink="/users">
3-
<mat-icon>arrow_back</mat-icon>
3+
<mat-icon>{{ localeService.locale() === 'ar' ? 'arrow_forward' : 'arrow_back' }}</mat-icon>
44
{{ 'users.detail.back' | transloco }}
55
</a>
66
</div>
77

88
@if (loading()) {
9-
<div class="cce-user-detail__status">{{ 'common.loading' | transloco }}</div>
9+
<mat-progress-bar mode="indeterminate" />
1010
}
1111

1212
@if (error(); as err) {
@@ -15,66 +15,80 @@
1515

1616
@if (user(); as u) {
1717
<mat-card class="cce-user-detail__card">
18-
<mat-card-header>
19-
<mat-card-title>{{ u.userName ?? u.email ?? u.id }}</mat-card-title>
20-
<mat-card-subtitle>{{ u.email ?? '—' }}</mat-card-subtitle>
21-
</mat-card-header>
18+
19+
<!-- Profile header -->
20+
<div class="cce-user-detail__profile">
21+
<div class="cce-user-detail__avatar">
22+
@if (u.avatarUrl) {
23+
<img [src]="u.avatarUrl" [alt]="u.userName ?? ''" />
24+
} @else {
25+
{{ initials() }}
26+
}
27+
</div>
28+
<div class="cce-user-detail__profile-info">
29+
<h2 class="cce-user-detail__name">{{ u.userName ?? u.email ?? u.id }}</h2>
30+
<p class="cce-user-detail__email">{{ u.email ?? '—' }}</p>
31+
<mat-chip-set>
32+
<mat-chip [class]="u.isActive ? 'cce-chip--active' : 'cce-chip--inactive'">
33+
{{ (u.isActive ? 'users.detail.status.active' : 'users.detail.status.inactive') | transloco }}
34+
</mat-chip>
35+
</mat-chip-set>
36+
</div>
37+
</div>
38+
39+
<mat-divider />
40+
2241
<mat-card-content>
2342
<dl class="cce-user-detail__list">
24-
<dt>{{ 'users.detail.field.locale' | transloco }}</dt>
25-
<dd>{{ u.localePreference }}</dd>
26-
<dt>{{ 'users.detail.field.knowledgeLevel' | transloco }}</dt>
27-
<dd>{{ u.knowledgeLevel }}</dd>
28-
<dt>{{ 'users.detail.field.interests' | transloco }}</dt>
43+
44+
<dt>{{ 'users.detail.field.roles' | transloco }}</dt>
2945
<dd>
30-
@if (u.interests.length) {
46+
@if (u.roles.length) {
3147
<mat-chip-set>
32-
@for (i of u.interests; track i) {
33-
<mat-chip>{{ i }}</mat-chip>
48+
@for (r of u.roles; track r) {
49+
<mat-chip>{{ r | roleLabel | transloco }}</mat-chip>
3450
}
3551
</mat-chip-set>
36-
} @else {
37-
38-
}
52+
} @else { — }
3953
</dd>
54+
4055
<dt>{{ 'users.detail.field.country' | transloco }}</dt>
41-
<dd>{{ u.countryId ?? '—' }}</dd>
42-
<dt>{{ 'users.detail.field.roles' | transloco }}</dt>
56+
<dd>{{ countryName() }}</dd>
57+
58+
<dt>{{ 'users.detail.field.locale' | transloco }}</dt>
59+
<dd>{{ ('common.locale.' + u.localePreference) | transloco }}</dd>
60+
61+
<dt>{{ 'users.detail.field.knowledgeLevel' | transloco }}</dt>
62+
<dd>{{ knowledgeLevelKey(u.knowledgeLevel) | transloco }}</dd>
63+
64+
<dt>{{ 'users.detail.field.interests' | transloco }}</dt>
4365
<dd>
44-
@if (u.roles.length) {
66+
@if (u.interests.length) {
4567
<mat-chip-set>
46-
@for (r of u.roles; track r) {
47-
<mat-chip>{{ r | roleLabel | transloco }}</mat-chip>
68+
@for (i of u.interests; track i) {
69+
<mat-chip>{{ i }}</mat-chip>
4870
}
4971
</mat-chip-set>
50-
} @else {
51-
52-
}
72+
} @else { — }
5373
</dd>
54-
<dt>{{ 'users.detail.field.isActive' | transloco }}</dt>
55-
<dd>{{ u.isActive ? '✓' : '✗' }}</dd>
74+
5675
</dl>
5776
</mat-card-content>
77+
78+
<mat-divider />
79+
5880
<mat-card-actions align="end">
5981
<button
6082
type="button"
6183
mat-button
6284
color="warn"
63-
*ccePermission="'User.Read'"
85+
*ccePermission="Permission.UserDelete"
6486
(click)="deleteUser()"
6587
>
6688
<mat-icon>delete</mat-icon>
6789
{{ 'users.delete.button' | transloco }}
6890
</button>
69-
<button
70-
type="button"
71-
mat-flat-button
72-
color="primary"
73-
*ccePermission="'Role.Assign'"
74-
(click)="openRoleAssign()"
75-
>
76-
{{ 'roleAssign.openButton' | transloco }}
77-
</button>
7891
</mat-card-actions>
92+
7993
</mat-card>
8094
}
Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
:host {
22
display: block;
33
padding: 1.5rem;
4-
max-width: 720px;
4+
max-width: 760px;
55
}
66

77
.cce-user-detail__header {
88
margin-bottom: 1rem;
99
}
1010

11-
.cce-user-detail__status {
12-
padding: 1rem;
13-
color: rgba(0, 0, 0, 0.6);
14-
}
15-
1611
.cce-user-detail__error {
1712
background: #fdecea;
1813
color: #b00020;
@@ -25,20 +20,86 @@
2520
margin-top: 0.5rem;
2621
}
2722

23+
// ── Profile header ───────────────────────────────────────────────────────────
24+
25+
.cce-user-detail__profile {
26+
display: flex;
27+
align-items: center;
28+
gap: 1.5rem;
29+
padding: 1.5rem 1.5rem 1.25rem;
30+
}
31+
32+
.cce-user-detail__avatar {
33+
width: 72px;
34+
height: 72px;
35+
border-radius: 50%;
36+
background: #1565c0;
37+
color: #fff;
38+
display: flex;
39+
align-items: center;
40+
justify-content: center;
41+
font-size: 1.75rem;
42+
font-weight: 600;
43+
flex-shrink: 0;
44+
overflow: hidden;
45+
46+
img {
47+
width: 100%;
48+
height: 100%;
49+
object-fit: cover;
50+
}
51+
}
52+
53+
.cce-user-detail__profile-info {
54+
display: flex;
55+
flex-direction: column;
56+
gap: 0.25rem;
57+
}
58+
59+
.cce-user-detail__name {
60+
margin: 0;
61+
font-size: 1.25rem;
62+
font-weight: 600;
63+
line-height: 1.3;
64+
}
65+
66+
.cce-user-detail__email {
67+
margin: 0;
68+
color: rgba(0, 0, 0, 0.6);
69+
font-size: 0.9rem;
70+
}
71+
72+
// ── Fields list ──────────────────────────────────────────────────────────────
73+
2874
.cce-user-detail__list {
2975
display: grid;
30-
grid-template-columns: 200px 1fr;
31-
row-gap: 0.75rem;
76+
grid-template-columns: 180px 1fr;
77+
row-gap: 1rem;
3278
column-gap: 1.5rem;
3379
margin: 0;
80+
padding: 0.5rem 0;
3481

3582
dt {
3683
font-weight: 600;
37-
color: rgba(0, 0, 0, 0.7);
84+
color: rgba(0, 0, 0, 0.6);
85+
font-size: 0.875rem;
3886
margin: 0;
87+
padding-top: 0.25rem;
3988
}
4089

4190
dd {
4291
margin: 0;
4392
}
4493
}
94+
95+
// ── Status chips ─────────────────────────────────────────────────────────────
96+
97+
::ng-deep .cce-chip--active.mat-mdc-chip {
98+
--mdc-chip-label-text-color: #1b5e20;
99+
background-color: #e8f5e9;
100+
}
101+
102+
::ng-deep .cce-chip--inactive.mat-mdc-chip {
103+
--mdc-chip-label-text-color: #b71c1c;
104+
background-color: #ffebee;
105+
}

0 commit comments

Comments
 (0)