Skip to content

Commit ff39652

Browse files
author
Diogo Ferraz
committed
feat(activity): add role-restricted Activity Log page for admin/project managers
1 parent 58e1c62 commit ff39652

6 files changed

Lines changed: 656 additions & 0 deletions

File tree

src/app/app.routes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ 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';
2222
import { ProjectMembersComponent } from './features/projects/components/project-members/project-members.component';
23+
import { ActivityLogComponent } from './features/activity/components/activity-log/activity-log.component';
24+
import { managerOrAdminGuard } from './core/auth/guards/manager-or-admin.guard';
2325

2426
export const routes: Routes = [
2527
{ path: '', component: LandingPageComponent },
@@ -34,6 +36,7 @@ export const routes: Routes = [
3436
{ path: 'docs', component: ProjectDocsComponent, canActivate: [authGuard] },
3537
{ path: 'calendar', component: TaskCalendarComponent, canActivate: [authGuard] },
3638
{ path: 'activity/my', component: MyActivityComponent, canActivate: [authGuard] },
39+
{ path: 'activity/log', component: ActivityLogComponent, canActivate: [authGuard, managerOrAdminGuard] },
3740
{ path: 'profile', component: UserProfileSecurityComponent, canActivate: [authGuard] },
3841
{ path: 'admin', component: AdminDashboardComponent, canActivate: [authGuard, adminRoleGuard] },
3942
{ path: 'tasks/create', component: TaskItemCreateComponent, canActivate: [authGuard] },
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { inject } from '@angular/core';
2+
import { CanActivateFn, Router } from '@angular/router';
3+
import { AuthService } from '../services/auth.service';
4+
5+
export const managerOrAdminGuard: CanActivateFn = () => {
6+
const authService = inject(AuthService);
7+
const router = inject(Router);
8+
9+
if (authService.hasAnyRole(['Administrator', 'ProjectManager'])) {
10+
return true;
11+
}
12+
13+
return router.createUrlTree(['/dashboard']);
14+
};
15+

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
@@ -43,6 +43,7 @@ export class AppMenuComponent {
4343
label: 'Insights',
4444
items: [
4545
{ label: 'My Activity', icon: 'pi pi-fw pi-history', routerLink: ['/activity/my'] },
46+
{ label: 'Activity Log', icon: 'pi pi-fw pi-database', routerLink: ['/activity/log'], visible: this.authService.hasAnyRole(['Administrator', 'ProjectManager']) },
4647
{ label: 'Profile & Security', icon: 'pi pi-fw pi-user-edit', routerLink: ['/profile'] },
4748
{ label: 'Search & Filters', icon: 'pi pi-fw pi-search', routerLink: ['/search'] },
4849
{ label: 'Calendar', icon: 'pi pi-fw pi-calendar', routerLink: ['/calendar'] },
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<div class="activity-log-page">
2+
<p-card class="activity-log-header-card">
3+
<div class="activity-log-header">
4+
<div class="activity-log-header__title">
5+
<h2>Activity Log</h2>
6+
<p>Global audit feed for project and task events across the workspace.</p>
7+
</div>
8+
9+
<div class="activity-log-header__meta">
10+
<span class="meta-pill">
11+
<i class="pi pi-bolt"></i>
12+
{{ isLiveConnected ? 'Live connected' : 'Live disconnected' }}
13+
</span>
14+
<span class="meta-pill" *ngIf="isPreviewMode">
15+
<i class="pi pi-eye"></i>
16+
Preview mode
17+
</span>
18+
<span class="meta-pill" *ngIf="previewDetail">{{ previewDetail }}</span>
19+
</div>
20+
</div>
21+
</p-card>
22+
23+
<div class="activity-log-kpis">
24+
<p-card class="kpi-card"><span class="kpi-card__label">Total Events</span><span class="kpi-card__value">{{ totalEvents }}</span></p-card>
25+
<p-card class="kpi-card"><span class="kpi-card__label">Visible Events</span><span class="kpi-card__value">{{ visibleEvents }}</span></p-card>
26+
<p-card class="kpi-card"><span class="kpi-card__label">Today</span><span class="kpi-card__value">{{ todayEvents }}</span></p-card>
27+
<p-card class="kpi-card"><span class="kpi-card__label">Project Events</span><span class="kpi-card__value">{{ projectEvents }}</span></p-card>
28+
</div>
29+
30+
<p-card class="activity-log-table-card">
31+
<div class="activity-log-inline-error" *ngIf="errorMessage">
32+
<i class="pi pi-exclamation-triangle"></i>
33+
{{ errorMessage }}
34+
</div>
35+
36+
<p-table
37+
#dt
38+
[value]="filteredRows"
39+
[loading]="isLoading"
40+
[rowHover]="true"
41+
[paginator]="true"
42+
[rows]="15"
43+
[rowsPerPageOptions]="[15, 30, 60]"
44+
[tableStyle]="{ 'min-width': '72rem' }"
45+
[globalFilterFields]="['actorDisplayName', 'projectName', 'taskTitle', 'typeLabel', 'entityType']"
46+
(onFilter)="onTableFilter($event)">
47+
<ng-template pTemplate="caption">
48+
<div class="table-caption">
49+
<button pButton type="button" icon="pi pi-filter-slash" label="Clear Filters" class="p-button-text" (click)="clearFilters(dt)"></button>
50+
<button pButton type="button" icon="pi pi-upload" label="Export CSV" class="p-button-text" (click)="exportCsv(dt)"></button>
51+
52+
<div class="caption-date-filters">
53+
<p-calendar
54+
[(ngModel)]="dateFrom"
55+
[showIcon]="true"
56+
[appendTo]="'body'"
57+
dateFormat="yy-mm-dd"
58+
placeholder="From"
59+
(onSelect)="onDateRangeChanged(dt)"
60+
(onClearClick)="onDateRangeChanged(dt)">
61+
</p-calendar>
62+
63+
<p-calendar
64+
[(ngModel)]="dateTo"
65+
[showIcon]="true"
66+
[appendTo]="'body'"
67+
dateFormat="yy-mm-dd"
68+
placeholder="To"
69+
(onSelect)="onDateRangeChanged(dt)"
70+
(onClearClick)="onDateRangeChanged(dt)">
71+
</p-calendar>
72+
</div>
73+
74+
<span class="table-search">
75+
<i class="pi pi-search"></i>
76+
<input pInputText type="text" [(ngModel)]="tableSearch" (input)="onGlobalFilter(dt, $event)" placeholder="Search activity..." />
77+
</span>
78+
</div>
79+
</ng-template>
80+
81+
<ng-template pTemplate="header">
82+
<tr>
83+
<th style="min-width: 12rem;">
84+
<div class="column-head">
85+
Actor
86+
<p-columnFilter type="text" field="actorDisplayName" display="menu"></p-columnFilter>
87+
</div>
88+
</th>
89+
<th style="min-width: 9rem;">
90+
<div class="column-head">
91+
Entity
92+
<p-columnFilter field="entityType" matchMode="equals" display="menu" [showMatchModes]="false" [showOperator]="false" [showAddButton]="false">
93+
<ng-template pTemplate="filter" let-value let-filter="filterCallback">
94+
<p-dropdown
95+
[options]="entityTypeOptions"
96+
optionLabel="label"
97+
optionValue="value"
98+
[ngModel]="value"
99+
[appendTo]="'body'"
100+
[showClear]="false"
101+
placeholder="Entity"
102+
(onChange)="filter($event.value)">
103+
</p-dropdown>
104+
</ng-template>
105+
</p-columnFilter>
106+
</div>
107+
</th>
108+
<th style="min-width: 14rem;">
109+
<div class="column-head">
110+
Event Type
111+
<p-columnFilter field="type" matchMode="equals" display="menu" [showMatchModes]="false" [showOperator]="false" [showAddButton]="false">
112+
<ng-template pTemplate="filter" let-value let-filter="filterCallback">
113+
<p-dropdown
114+
[options]="typeOptions"
115+
optionLabel="label"
116+
optionValue="value"
117+
[ngModel]="value"
118+
[appendTo]="'body'"
119+
[showClear]="false"
120+
placeholder="Type"
121+
(onChange)="filter($event.value)">
122+
</p-dropdown>
123+
</ng-template>
124+
</p-columnFilter>
125+
</div>
126+
</th>
127+
<th style="min-width: 14rem;">
128+
<div class="column-head">
129+
Project
130+
<p-columnFilter type="text" field="projectName" display="menu"></p-columnFilter>
131+
</div>
132+
</th>
133+
<th style="min-width: 16rem;">Task</th>
134+
<th style="min-width: 12rem;">Occurred At</th>
135+
</tr>
136+
</ng-template>
137+
138+
<ng-template pTemplate="body" let-row>
139+
<tr>
140+
<td>{{ row.actorDisplayName || '-' }}</td>
141+
<td>
142+
<p-tag [value]="row.entityType" [severity]="row.entityType === 'Project' ? 'info' : 'contrast'"></p-tag>
143+
</td>
144+
<td>{{ row.typeLabel }}</td>
145+
<td>{{ row.projectName || '-' }}</td>
146+
<td>{{ row.taskTitle || '-' }}</td>
147+
<td>{{ row.occurredAtDate | date:'MMM d, y, h:mm a' }}</td>
148+
</tr>
149+
</ng-template>
150+
151+
<ng-template pTemplate="emptymessage">
152+
<tr>
153+
<td colspan="6" class="empty-cell">
154+
<div class="empty-state">
155+
<i class="pi pi-history"></i>
156+
<p>No activity found for current filters.</p>
157+
</div>
158+
</td>
159+
</tr>
160+
</ng-template>
161+
</p-table>
162+
</p-card>
163+
</div>
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
.activity-log-page {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 2rem;
5+
}
6+
7+
.activity-log-header {
8+
display: flex;
9+
justify-content: space-between;
10+
align-items: flex-start;
11+
flex-wrap: wrap;
12+
gap: 1rem;
13+
}
14+
15+
.activity-log-header__title h2 {
16+
margin: 0;
17+
}
18+
19+
.activity-log-header__title p {
20+
margin: 0.3rem 0 0;
21+
color: var(--text-color-secondary);
22+
}
23+
24+
.activity-log-header__meta {
25+
display: flex;
26+
flex-wrap: wrap;
27+
gap: 0.5rem;
28+
}
29+
30+
.meta-pill {
31+
display: inline-flex;
32+
align-items: center;
33+
gap: 0.35rem;
34+
border: 1px solid var(--surface-border);
35+
border-radius: 999px;
36+
padding: 0.22rem 0.6rem;
37+
font-size: 0.8rem;
38+
color: var(--text-color-secondary);
39+
background: color-mix(in srgb, var(--surface-100) 75%, transparent);
40+
}
41+
42+
.activity-log-kpis {
43+
display: grid;
44+
grid-template-columns: repeat(4, minmax(0, 1fr));
45+
gap: 1rem;
46+
}
47+
48+
.kpi-card {
49+
min-height: 6.1rem;
50+
}
51+
52+
.kpi-card__label {
53+
display: block;
54+
font-size: 0.8rem;
55+
color: var(--text-color-secondary);
56+
margin-bottom: 0.35rem;
57+
}
58+
59+
.kpi-card__value {
60+
font-size: 1.7rem;
61+
font-weight: 600;
62+
}
63+
64+
:host ::ng-deep .kpi-card .p-card-body,
65+
:host ::ng-deep .kpi-card .p-card-content {
66+
height: 100%;
67+
}
68+
69+
.activity-log-inline-error {
70+
border: 1px solid color-mix(in srgb, var(--red-500) 30%, transparent);
71+
background: color-mix(in srgb, var(--red-100) 60%, transparent);
72+
color: var(--red-700);
73+
border-radius: 10px;
74+
padding: 0.7rem 0.9rem;
75+
margin-bottom: 0.85rem;
76+
display: inline-flex;
77+
align-items: center;
78+
gap: 0.45rem;
79+
}
80+
81+
.table-caption {
82+
display: flex;
83+
align-items: center;
84+
gap: 0.5rem;
85+
flex-wrap: wrap;
86+
}
87+
88+
.caption-date-filters {
89+
display: inline-flex;
90+
align-items: center;
91+
gap: 0.45rem;
92+
}
93+
94+
.table-search {
95+
margin-left: auto;
96+
position: relative;
97+
display: inline-flex;
98+
align-items: center;
99+
}
100+
101+
.table-search i {
102+
position: absolute;
103+
left: 0.65rem;
104+
color: var(--text-color-secondary);
105+
}
106+
107+
.table-search input {
108+
padding-left: 2rem;
109+
min-width: 18rem;
110+
}
111+
112+
.column-head {
113+
display: inline-flex;
114+
align-items: center;
115+
gap: 0.45rem;
116+
}
117+
118+
.empty-cell {
119+
padding: 2rem 1rem;
120+
}
121+
122+
.empty-state {
123+
display: flex;
124+
flex-direction: column;
125+
align-items: center;
126+
gap: 0.45rem;
127+
color: var(--text-color-secondary);
128+
}
129+
130+
.empty-state i {
131+
font-size: 1.3rem;
132+
}
133+
134+
@media (max-width: 1200px) {
135+
.activity-log-kpis {
136+
grid-template-columns: repeat(2, minmax(0, 1fr));
137+
}
138+
}
139+
140+
@media (max-width: 992px) {
141+
.table-search {
142+
margin-left: 0;
143+
width: 100%;
144+
}
145+
146+
.table-search input {
147+
width: 100%;
148+
min-width: 0;
149+
}
150+
151+
.caption-date-filters {
152+
width: 100%;
153+
display: grid;
154+
grid-template-columns: repeat(2, minmax(0, 1fr));
155+
}
156+
}
157+
158+
@media (max-width: 768px) {
159+
.activity-log-kpis {
160+
grid-template-columns: 1fr;
161+
}
162+
163+
.caption-date-filters {
164+
grid-template-columns: 1fr;
165+
}
166+
}

0 commit comments

Comments
 (0)