Skip to content

Commit 58e1c62

Browse files
author
Diogo Ferraz
committed
feat(projects): add Project Members page with backend integration and PrimeNG column filters
1 parent 27f7859 commit 58e1c62

5 files changed

Lines changed: 538 additions & 0 deletions

File tree

src/app/app.routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ import { AdminDashboardComponent } from './features/admin/components/admin-dashb
1919
import { adminRoleGuard } from './core/auth/guards/admin-role.guard';
2020
import { MyActivityComponent } from './features/activity/components/my-activity/my-activity.component';
2121
import { UserProfileSecurityComponent } from './features/profile/components/user-profile-security/user-profile-security.component';
22+
import { ProjectMembersComponent } from './features/projects/components/project-members/project-members.component';
2223

2324
export const routes: Routes = [
2425
{ path: '', component: LandingPageComponent },
2526
{ path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] },
2627
{ path: 'projects', component: ProjectListComponent, canActivate: [authGuard] },
2728
{ path: 'projects/kanban', component: ProjectKanbanComponent, canActivate: [authGuard] },
2829
{ path: 'projects/create', component: ProjectCreateComponent, canActivate: [authGuard] },
30+
{ path: 'projects/members', component: ProjectMembersComponent, canActivate: [authGuard] },
2931
{ path: 'projects/details', component: ProjectDetailsComponent, canActivate: [authGuard] },
3032
{ path: 'tasks', component: TaskItemListComponent, canActivate: [authGuard] },
3133
{ path: 'search', component: SearchFiltersComponent, canActivate: [authGuard] },

src/app/core/layout/component/app-menu/app-menu.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export class AppMenuComponent {
2626
items: [
2727
{ label: 'All Projects', icon: 'pi pi-fw pi-list', routerLink: ['/projects'] },
2828
{ label: 'Project Details', icon: 'pi pi-fw pi-folder-open', routerLink: ['/projects/details'] },
29+
{ label: 'Project Members', icon: 'pi pi-fw pi-users', routerLink: ['/projects/members'] },
2930
{ label: 'Create Project', icon: 'pi pi-fw pi-plus', routerLink: ['/projects/create'] },
3031
{ label: 'Kanban Board', icon: 'pi pi-fw pi-th-large', routerLink: ['/projects/kanban'] }
3132
]
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<div class="project-members-page">
2+
<p-card class="members-header-card">
3+
<div class="members-header">
4+
<div class="members-header__title">
5+
<h2>Project Members</h2>
6+
<p>Review project membership, ownership, and collaborator visibility for any project.</p>
7+
<div class="members-header__meta" *ngIf="selectedProject as project">
8+
<span class="meta-pill">
9+
<i class="pi pi-folder"></i>
10+
{{ project.name }}
11+
</span>
12+
<span class="meta-pill">
13+
<i class="pi pi-user"></i>
14+
By {{ project.createdByUserName || 'Unknown' }}
15+
</span>
16+
<span class="meta-pill">
17+
<i class="pi pi-calendar"></i>
18+
Created {{ project.createdAt | date:'mediumDate' }}
19+
</span>
20+
<span class="meta-pill" *ngIf="isPreviewMode">
21+
<i class="pi pi-eye"></i>
22+
Preview mode
23+
</span>
24+
</div>
25+
</div>
26+
27+
<div class="members-header__controls">
28+
<p-dropdown
29+
inputId="membersProjectPicker"
30+
[options]="projects"
31+
optionLabel="name"
32+
optionValue="id"
33+
[(ngModel)]="selectedProjectId"
34+
[filter]="true"
35+
filterBy="name"
36+
[showClear]="false"
37+
[disabled]="isLoadingProjects || projects.length === 0 || isLoadingMembers"
38+
placeholder="Select project"
39+
(onChange)="onProjectChange($event.value)">
40+
</p-dropdown>
41+
42+
<button pButton type="button" icon="pi pi-refresh" class="p-button-outlined" (click)="refresh()" [loading]="isLoadingProjects || isLoadingMembers"></button>
43+
</div>
44+
</div>
45+
</p-card>
46+
47+
<div class="members-kpis">
48+
<p-card class="kpi-card"><span class="kpi-card__label">Total Members</span><span class="kpi-card__value">{{ totalMembers }}</span></p-card>
49+
<p-card class="kpi-card"><span class="kpi-card__label">Owners</span><span class="kpi-card__value">{{ ownerCount }}</span></p-card>
50+
<p-card class="kpi-card"><span class="kpi-card__label">Collaborators</span><span class="kpi-card__value">{{ collaboratorCount }}</span></p-card>
51+
<p-card class="kpi-card"><span class="kpi-card__label">Visible</span><span class="kpi-card__value">{{ visibleCount }}</span></p-card>
52+
</div>
53+
54+
<p-card class="members-table-card">
55+
<div class="members-inline-error" *ngIf="errorMessage">
56+
<i class="pi pi-exclamation-triangle"></i>
57+
{{ errorMessage }}
58+
<span *ngIf="previewDetail">· {{ previewDetail }}</span>
59+
</div>
60+
61+
<p-table
62+
#dt
63+
[value]="members"
64+
[loading]="isLoadingMembers"
65+
[rowHover]="true"
66+
[tableStyle]="{ 'min-width': '42rem' }"
67+
(onFilter)="onTableFilter($event)">
68+
<ng-template pTemplate="caption">
69+
<div class="table-caption">
70+
<button pButton type="button" icon="pi pi-filter-slash" label="Clear Filters" class="p-button-text" (click)="clearTableFilters(dt)"></button>
71+
</div>
72+
</ng-template>
73+
74+
<ng-template pTemplate="header">
75+
<tr>
76+
<th style="min-width: 18rem;">
77+
<div class="column-head">
78+
Member
79+
<p-columnFilter type="text" field="displayName" display="menu"></p-columnFilter>
80+
</div>
81+
</th>
82+
<th style="min-width: 10rem;">
83+
<div class="column-head">
84+
Role In Project
85+
<p-columnFilter field="isOwner" matchMode="equals" display="menu" [showMatchModes]="false" [showOperator]="false" [showAddButton]="false">
86+
<ng-template pTemplate="filter" let-value let-filter="filterCallback">
87+
<p-dropdown
88+
[options]="roleFilterOptions"
89+
optionLabel="label"
90+
optionValue="value"
91+
[ngModel]="value"
92+
[appendTo]="'body'"
93+
[showClear]="false"
94+
placeholder="Role"
95+
(onChange)="filter($event.value)">
96+
</p-dropdown>
97+
</ng-template>
98+
</p-columnFilter>
99+
</div>
100+
</th>
101+
<th style="min-width: 12rem;">
102+
<div class="column-head">
103+
User Id
104+
<p-columnFilter type="text" field="userId" display="menu"></p-columnFilter>
105+
</div>
106+
</th>
107+
</tr>
108+
</ng-template>
109+
110+
<ng-template pTemplate="body" let-member>
111+
<tr>
112+
<td>
113+
<div class="member-cell">
114+
<p-avatar [label]="getInitials(member.displayName)" shape="circle"></p-avatar>
115+
<span>{{ member.displayName }}</span>
116+
</div>
117+
</td>
118+
<td>
119+
<p-tag [value]="member.isOwner ? 'Owner' : 'Member'" [severity]="member.isOwner ? 'info' : 'contrast'"></p-tag>
120+
</td>
121+
<td><code>{{ member.userId }}</code></td>
122+
</tr>
123+
</ng-template>
124+
125+
<ng-template pTemplate="emptymessage">
126+
<tr>
127+
<td colspan="3" class="empty-cell">
128+
<div class="empty-state">
129+
<i class="pi pi-users"></i>
130+
<p>{{ members.length === 0 ? 'No members found for this project.' : 'No members match current filters.' }}</p>
131+
</div>
132+
</td>
133+
</tr>
134+
</ng-template>
135+
</p-table>
136+
</p-card>
137+
</div>
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
.project-members-page {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 2rem;
5+
}
6+
7+
.members-header {
8+
display: flex;
9+
justify-content: space-between;
10+
align-items: flex-start;
11+
flex-wrap: wrap;
12+
gap: 1rem;
13+
}
14+
15+
.members-header__title h2 {
16+
margin: 0;
17+
}
18+
19+
.members-header__title p {
20+
margin: 0.3rem 0 0;
21+
color: var(--text-color-secondary);
22+
}
23+
24+
.members-header__meta {
25+
margin-top: 0.75rem;
26+
display: flex;
27+
flex-wrap: wrap;
28+
gap: 0.5rem;
29+
}
30+
31+
.meta-pill {
32+
display: inline-flex;
33+
align-items: center;
34+
gap: 0.35rem;
35+
border: 1px solid var(--surface-border);
36+
border-radius: 999px;
37+
padding: 0.22rem 0.6rem;
38+
font-size: 0.8rem;
39+
color: var(--text-color-secondary);
40+
background: color-mix(in srgb, var(--surface-100) 75%, transparent);
41+
}
42+
43+
.members-header__controls {
44+
display: inline-flex;
45+
align-items: center;
46+
gap: 0.45rem;
47+
flex-wrap: wrap;
48+
}
49+
50+
:host ::ng-deep .members-header__controls .p-dropdown {
51+
min-width: 14rem;
52+
}
53+
54+
.members-kpis {
55+
display: grid;
56+
grid-template-columns: repeat(4, minmax(0, 1fr));
57+
gap: 1rem;
58+
}
59+
60+
.kpi-card {
61+
min-height: 6.1rem;
62+
}
63+
64+
.kpi-card__label {
65+
display: block;
66+
font-size: 0.8rem;
67+
color: var(--text-color-secondary);
68+
margin-bottom: 0.35rem;
69+
}
70+
71+
.kpi-card__value {
72+
font-size: 1.7rem;
73+
font-weight: 600;
74+
}
75+
76+
:host ::ng-deep .kpi-card .p-card-body,
77+
:host ::ng-deep .kpi-card .p-card-content {
78+
height: 100%;
79+
}
80+
81+
.members-inline-error {
82+
border: 1px solid color-mix(in srgb, var(--red-500) 30%, transparent);
83+
background: color-mix(in srgb, var(--red-100) 60%, transparent);
84+
color: var(--red-700);
85+
border-radius: 10px;
86+
padding: 0.7rem 0.9rem;
87+
margin-bottom: 0.85rem;
88+
display: inline-flex;
89+
align-items: center;
90+
gap: 0.45rem;
91+
}
92+
93+
.table-caption {
94+
display: flex;
95+
align-items: center;
96+
gap: 0.5rem;
97+
flex-wrap: wrap;
98+
}
99+
100+
.column-head {
101+
display: inline-flex;
102+
align-items: center;
103+
gap: 0.45rem;
104+
}
105+
106+
.member-cell {
107+
display: inline-flex;
108+
align-items: center;
109+
gap: 0.55rem;
110+
}
111+
112+
.empty-cell {
113+
padding: 2rem 1rem;
114+
}
115+
116+
.empty-state {
117+
display: flex;
118+
flex-direction: column;
119+
align-items: center;
120+
gap: 0.45rem;
121+
color: var(--text-color-secondary);
122+
}
123+
124+
.empty-state i {
125+
font-size: 1.3rem;
126+
}
127+
128+
@media (max-width: 1200px) {
129+
.members-kpis {
130+
grid-template-columns: repeat(2, minmax(0, 1fr));
131+
}
132+
}
133+
134+
@media (max-width: 992px) {
135+
:host ::ng-deep .members-header__controls .p-dropdown {
136+
min-width: 0;
137+
width: 100%;
138+
}
139+
}
140+
141+
@media (max-width: 768px) {
142+
.members-kpis {
143+
grid-template-columns: 1fr;
144+
}
145+
}

0 commit comments

Comments
 (0)