User commited on
Commit
691d646
·
1 Parent(s): e40d850

add: admin dashboard

Browse files
TaskTrackingSystem.WebApi/Program.cs CHANGED
@@ -128,6 +128,188 @@ static async System.Threading.Tasks.Task EnsureSeedDataAsync(WebApplication app)
128
  backlogMenu.UpdatedAt = DateTime.UtcNow;
129
  }
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  // Soft-delete existing tasks that belong to deleted projects
132
  var orphanedTasks = await db.Tasks
133
  .Where(t => t.IsDeleted != true && t.Project.IsDeleted == true)
 
128
  backlogMenu.UpdatedAt = DateTime.UtcNow;
129
  }
130
 
131
+ var dashboardMenu = await db.Menus.FirstOrDefaultAsync(m => m.MenuCode == "DASHBOARD");
132
+ if (dashboardMenu == null)
133
+ {
134
+ dashboardMenu = new Menu
135
+ {
136
+ MenuCode = "DASHBOARD",
137
+ MenuName = "Dashboard",
138
+ MenuUrl = null,
139
+ Icon = "layout-grid",
140
+ Visible = true,
141
+ OrderNo = 0,
142
+ IsDeleted = false,
143
+ CreatedAt = DateTime.UtcNow
144
+ };
145
+ db.Menus.Add(dashboardMenu);
146
+ await db.SaveChangesAsync();
147
+ }
148
+ else
149
+ {
150
+ dashboardMenu.MenuName = "Dashboard";
151
+ dashboardMenu.MenuUrl = null;
152
+ dashboardMenu.Icon = "layout-grid";
153
+ dashboardMenu.Visible = true;
154
+ dashboardMenu.OrderNo = 0;
155
+ dashboardMenu.IsDeleted = false;
156
+ dashboardMenu.UpdatedAt = DateTime.UtcNow;
157
+ }
158
+
159
+ var adminDashboardMenu = await db.Menus.FirstOrDefaultAsync(m => m.MenuCode == "DASHBOARD_ADMIN");
160
+ if (adminDashboardMenu == null)
161
+ {
162
+ adminDashboardMenu = new Menu
163
+ {
164
+ MenuCode = "DASHBOARD_ADMIN",
165
+ ParentMenuId = dashboardMenu.MenuId,
166
+ MenuName = "Admin Dashboard",
167
+ MenuUrl = "/dashboard",
168
+ Icon = "layout-dashboard",
169
+ Visible = true,
170
+ OrderNo = 1,
171
+ IsDeleted = false,
172
+ CreatedAt = DateTime.UtcNow
173
+ };
174
+ db.Menus.Add(adminDashboardMenu);
175
+ }
176
+ else
177
+ {
178
+ adminDashboardMenu.ParentMenuId = dashboardMenu.MenuId;
179
+ adminDashboardMenu.MenuName = "Admin Dashboard";
180
+ adminDashboardMenu.MenuUrl = "/dashboard";
181
+ adminDashboardMenu.Icon = "layout-dashboard";
182
+ adminDashboardMenu.Visible = true;
183
+ adminDashboardMenu.OrderNo = 1;
184
+ adminDashboardMenu.IsDeleted = false;
185
+ adminDashboardMenu.UpdatedAt = DateTime.UtcNow;
186
+ }
187
+
188
+ var managerDashboardMenu = await db.Menus.FirstOrDefaultAsync(m => m.MenuCode == "DASHBOARD_MANAGER");
189
+ if (managerDashboardMenu == null)
190
+ {
191
+ managerDashboardMenu = new Menu
192
+ {
193
+ MenuCode = "DASHBOARD_MANAGER",
194
+ ParentMenuId = dashboardMenu.MenuId,
195
+ MenuName = "Manager Dashboard",
196
+ MenuUrl = "/dashboard/manager",
197
+ Icon = "layout-dashboard",
198
+ Visible = true,
199
+ OrderNo = 2,
200
+ IsDeleted = false,
201
+ CreatedAt = DateTime.UtcNow
202
+ };
203
+ db.Menus.Add(managerDashboardMenu);
204
+ }
205
+ else
206
+ {
207
+ managerDashboardMenu.ParentMenuId = dashboardMenu.MenuId;
208
+ managerDashboardMenu.MenuName = "Manager Dashboard";
209
+ managerDashboardMenu.MenuUrl = "/dashboard/manager";
210
+ managerDashboardMenu.Icon = "layout-dashboard";
211
+ managerDashboardMenu.Visible = true;
212
+ managerDashboardMenu.OrderNo = 2;
213
+ managerDashboardMenu.IsDeleted = false;
214
+ managerDashboardMenu.UpdatedAt = DateTime.UtcNow;
215
+ }
216
+
217
+ var employeeDashboardMenu = await db.Menus.FirstOrDefaultAsync(m => m.MenuCode == "DASHBOARD_EMPLOYEE");
218
+ if (employeeDashboardMenu == null)
219
+ {
220
+ employeeDashboardMenu = new Menu
221
+ {
222
+ MenuCode = "DASHBOARD_EMPLOYEE",
223
+ ParentMenuId = dashboardMenu.MenuId,
224
+ MenuName = "Employee Dashboard",
225
+ MenuUrl = "/dashboard/employee",
226
+ Icon = "layout-dashboard",
227
+ Visible = true,
228
+ OrderNo = 3,
229
+ IsDeleted = false,
230
+ CreatedAt = DateTime.UtcNow
231
+ };
232
+ db.Menus.Add(employeeDashboardMenu);
233
+ }
234
+ else
235
+ {
236
+ employeeDashboardMenu.ParentMenuId = dashboardMenu.MenuId;
237
+ employeeDashboardMenu.MenuName = "Employee Dashboard";
238
+ employeeDashboardMenu.MenuUrl = "/dashboard/employee";
239
+ employeeDashboardMenu.Icon = "layout-dashboard";
240
+ employeeDashboardMenu.Visible = true;
241
+ employeeDashboardMenu.OrderNo = 3;
242
+ employeeDashboardMenu.IsDeleted = false;
243
+ employeeDashboardMenu.UpdatedAt = DateTime.UtcNow;
244
+ }
245
+
246
+ await db.SaveChangesAsync();
247
+
248
+ var adminRole = await db.Roles.FirstOrDefaultAsync(r => r.Name == "Admin" && !r.IsDeleted);
249
+ if (adminRole != null)
250
+ {
251
+ var adminDashboardAccess = await db.RoleMenus.FirstOrDefaultAsync(rm => rm.RoleId == adminRole.Id && rm.MenuId == adminDashboardMenu.MenuId);
252
+ if (adminDashboardAccess == null)
253
+ {
254
+ db.RoleMenus.Add(new RoleMenu
255
+ {
256
+ RoleId = adminRole.Id,
257
+ MenuId = adminDashboardMenu.MenuId,
258
+ IsDeleted = false,
259
+ CreatedAt = DateTime.UtcNow
260
+ });
261
+ }
262
+ else
263
+ {
264
+ adminDashboardAccess.IsDeleted = false;
265
+ adminDashboardAccess.UpdatedAt = DateTime.UtcNow;
266
+ }
267
+ }
268
+
269
+ var managerRole = await db.Roles.FirstOrDefaultAsync(r => r.Name == "Manager" && !r.IsDeleted);
270
+ if (managerRole != null)
271
+ {
272
+ var managerDashboardAccess = await db.RoleMenus.FirstOrDefaultAsync(rm => rm.RoleId == managerRole.Id && rm.MenuId == managerDashboardMenu.MenuId);
273
+ if (managerDashboardAccess == null)
274
+ {
275
+ db.RoleMenus.Add(new RoleMenu
276
+ {
277
+ RoleId = managerRole.Id,
278
+ MenuId = managerDashboardMenu.MenuId,
279
+ IsDeleted = false,
280
+ CreatedAt = DateTime.UtcNow
281
+ });
282
+ }
283
+ else
284
+ {
285
+ managerDashboardAccess.IsDeleted = false;
286
+ managerDashboardAccess.UpdatedAt = DateTime.UtcNow;
287
+ }
288
+ }
289
+
290
+ var employeeRole = await db.Roles.FirstOrDefaultAsync(r => r.Name == "Employee" && !r.IsDeleted);
291
+ if (employeeRole != null)
292
+ {
293
+ var employeeDashboardAccess = await db.RoleMenus.FirstOrDefaultAsync(rm => rm.RoleId == employeeRole.Id && rm.MenuId == employeeDashboardMenu.MenuId);
294
+ if (employeeDashboardAccess == null)
295
+ {
296
+ db.RoleMenus.Add(new RoleMenu
297
+ {
298
+ RoleId = employeeRole.Id,
299
+ MenuId = employeeDashboardMenu.MenuId,
300
+ IsDeleted = false,
301
+ CreatedAt = DateTime.UtcNow
302
+ });
303
+ }
304
+ else
305
+ {
306
+ employeeDashboardAccess.IsDeleted = false;
307
+ employeeDashboardAccess.UpdatedAt = DateTime.UtcNow;
308
+ }
309
+ }
310
+
311
+ await db.SaveChangesAsync();
312
+
313
  // Soft-delete existing tasks that belong to deleted projects
314
  var orphanedTasks = await db.Tasks
315
  .Where(t => t.IsDeleted != true && t.Project.IsDeleted == true)
TaskTrackingSystem.WebApp/AccountEndpoints.cs CHANGED
@@ -23,6 +23,7 @@ public static class AccountEndpoints
23
  private static async Task<IResult> LoginAsync(
24
  HttpContext context,
25
  IHttpClientFactory httpClientFactory,
 
26
  [FromForm] string usernameOrEmail,
27
  [FromForm] string password,
28
  [FromForm] bool? rememberMe,
@@ -60,12 +61,14 @@ public static class AccountEndpoints
60
  }
61
 
62
  await SignInUserAsync(context, result.Value, rememberMe ?? false);
63
- return Results.Redirect(GetSafeReturnUrl(returnUrl));
 
64
  }
65
 
66
  private static async Task<IResult> RegisterAsync(
67
  HttpContext context,
68
  IHttpClientFactory httpClientFactory,
 
69
  [FromForm] RegisterDto registerDto)
70
  {
71
  HttpResponseMessage response;
@@ -87,7 +90,8 @@ public static class AccountEndpoints
87
  if (result?.IsSuccess == true && result.Value != null && !string.IsNullOrWhiteSpace(result.Value.Username))
88
  {
89
  await SignInUserAsync(context, result.Value, false);
90
- return Results.Redirect("/dashboard");
 
91
  }
92
  else if (result != null && !string.IsNullOrEmpty(result.ErrorMessage))
93
  {
@@ -305,13 +309,69 @@ public static class AccountEndpoints
305
  return Results.Redirect($"/login?error={error}{safeReturnUrl}");
306
  }
307
 
308
- private static string GetSafeReturnUrl(string? returnUrl)
309
  {
310
  if (string.IsNullOrWhiteSpace(returnUrl) || !returnUrl.StartsWith('/') || returnUrl.StartsWith("//"))
311
  {
312
- return "/dashboard";
313
  }
314
 
315
  return returnUrl;
316
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  }
 
23
  private static async Task<IResult> LoginAsync(
24
  HttpContext context,
25
  IHttpClientFactory httpClientFactory,
26
+ MenuAuthorizationService menuAuthorization,
27
  [FromForm] string usernameOrEmail,
28
  [FromForm] string password,
29
  [FromForm] bool? rememberMe,
 
61
  }
62
 
63
  await SignInUserAsync(context, result.Value, rememberMe ?? false);
64
+ var landingPage = await ResolveLandingPageAsync(menuAuthorization, result.Value);
65
+ return Results.Redirect(GetSafeReturnUrl(returnUrl, landingPage));
66
  }
67
 
68
  private static async Task<IResult> RegisterAsync(
69
  HttpContext context,
70
  IHttpClientFactory httpClientFactory,
71
+ MenuAuthorizationService menuAuthorization,
72
  [FromForm] RegisterDto registerDto)
73
  {
74
  HttpResponseMessage response;
 
90
  if (result?.IsSuccess == true && result.Value != null && !string.IsNullOrWhiteSpace(result.Value.Username))
91
  {
92
  await SignInUserAsync(context, result.Value, false);
93
+ var landingPage = await ResolveLandingPageAsync(menuAuthorization, result.Value);
94
+ return Results.Redirect(GetSafeReturnUrl(null, landingPage));
95
  }
96
  else if (result != null && !string.IsNullOrEmpty(result.ErrorMessage))
97
  {
 
309
  return Results.Redirect($"/login?error={error}{safeReturnUrl}");
310
  }
311
 
312
+ private static string GetSafeReturnUrl(string? returnUrl, string landingPage)
313
  {
314
  if (string.IsNullOrWhiteSpace(returnUrl) || !returnUrl.StartsWith('/') || returnUrl.StartsWith("//"))
315
  {
316
+ return landingPage;
317
  }
318
 
319
  return returnUrl;
320
  }
321
+
322
+ private static async Task<string> ResolveLandingPageAsync(MenuAuthorizationService menuAuthorization, AuthResponseDto authResult)
323
+ {
324
+ var principal = BuildTemporaryPrincipal(authResult);
325
+ var menus = await menuAuthorization.GetUserMenusAsync(principal);
326
+ var dashboardMenu = FindFirstDashboardMenu(menus);
327
+ return dashboardMenu ?? "/dashboard";
328
+ }
329
+
330
+ private static ClaimsPrincipal BuildTemporaryPrincipal(AuthResponseDto authResult)
331
+ {
332
+ var claims = new List<Claim>
333
+ {
334
+ new(ClaimTypes.Name, authResult.Username),
335
+ new(ClaimTypes.Email, authResult.Email),
336
+ new(ClaimTypes.Role, authResult.RoleName),
337
+ new("role_id", authResult.RoleId.ToString()),
338
+ new("jwt_token", authResult.Token)
339
+ };
340
+
341
+ return new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme));
342
+ }
343
+
344
+ private static string? FindFirstDashboardMenu(IEnumerable<TaskTrackingSystem.Shared.Models.Menu.MenuDto> menus)
345
+ {
346
+ foreach (var menu in menus)
347
+ {
348
+ var found = FindFirstDashboardMenu(menu);
349
+ if (!string.IsNullOrWhiteSpace(found))
350
+ {
351
+ return found;
352
+ }
353
+ }
354
+
355
+ return null;
356
+ }
357
+
358
+ private static string? FindFirstDashboardMenu(TaskTrackingSystem.Shared.Models.Menu.MenuDto menu)
359
+ {
360
+ if (!string.IsNullOrWhiteSpace(menu.MenuUrl) &&
361
+ menu.MenuUrl.StartsWith("/dashboard", StringComparison.OrdinalIgnoreCase))
362
+ {
363
+ return menu.MenuUrl;
364
+ }
365
+
366
+ foreach (var subMenu in menu.SubMenus)
367
+ {
368
+ var found = FindFirstDashboardMenu(subMenu);
369
+ if (!string.IsNullOrWhiteSpace(found))
370
+ {
371
+ return found;
372
+ }
373
+ }
374
+
375
+ return null;
376
+ }
377
  }
TaskTrackingSystem.WebApp/Components/Layout/MainLayout.razor CHANGED
@@ -89,7 +89,7 @@
89
  <p class="text-center mt-2 max-w-sm text-sm" style="color: var(--text-muted);">
90
  You do not have permission to access this page. Please contact your system administrator.
91
  </p>
92
- <a href="/dashboard" class="mt-6 inline-flex items-center rounded-lg px-4 py-2.5 text-sm font-medium text-white transition-colors shadow-sm hover:opacity-90" style="background-color: var(--brand-primary);">
93
  <i data-lucide="arrow-left" class="h-4 w-4 mr-2"></i>
94
  Back to Dashboard
95
  </a>
@@ -106,6 +106,7 @@
106
  [Inject] private Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider AuthStateProvider { get; set; } = default!;
107
 
108
  private bool isAuthorizedPage = true;
 
109
 
110
  private bool IsDashboardPage =>
111
  string.IsNullOrEmpty(Navigation.ToBaseRelativePath(Navigation.Uri)) ||
@@ -118,6 +119,9 @@
118
  "reports" => "Reports",
119
  "roles" => "Roles & Access",
120
  "users" => "Users",
 
 
 
121
  _ => "Workspace"
122
  };
123
 
@@ -159,6 +163,7 @@
159
  }
160
 
161
  var menus = await MenuAuthorization.GetUserMenusAsync(user);
 
162
  var allowed = MenuAuthorization.IsRouteAllowed(user, relativePath);
163
  Console.WriteLine($"[DEBUG MainLayout] User: {user.Identity?.Name}, Path: {relativePath}, Allowed: {allowed}");
164
  if (menus != null)
@@ -188,4 +193,38 @@
188
  {
189
  Navigation.LocationChanged -= OnLocationChanged;
190
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  }
 
89
  <p class="text-center mt-2 max-w-sm text-sm" style="color: var(--text-muted);">
90
  You do not have permission to access this page. Please contact your system administrator.
91
  </p>
92
+ <a href="@dashboardHref" class="mt-6 inline-flex items-center rounded-lg px-4 py-2.5 text-sm font-medium text-white transition-colors shadow-sm hover:opacity-90" style="background-color: var(--brand-primary);">
93
  <i data-lucide="arrow-left" class="h-4 w-4 mr-2"></i>
94
  Back to Dashboard
95
  </a>
 
106
  [Inject] private Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider AuthStateProvider { get; set; } = default!;
107
 
108
  private bool isAuthorizedPage = true;
109
+ private string dashboardHref = "/dashboard";
110
 
111
  private bool IsDashboardPage =>
112
  string.IsNullOrEmpty(Navigation.ToBaseRelativePath(Navigation.Uri)) ||
 
119
  "reports" => "Reports",
120
  "roles" => "Roles & Access",
121
  "users" => "Users",
122
+ "dashboard" => "Dashboard",
123
+ "dashboard/manager" => "Manager Dashboard",
124
+ "dashboard/employee" => "Employee Dashboard",
125
  _ => "Workspace"
126
  };
127
 
 
163
  }
164
 
165
  var menus = await MenuAuthorization.GetUserMenusAsync(user);
166
+ dashboardHref = ResolveDashboardHref(menus);
167
  var allowed = MenuAuthorization.IsRouteAllowed(user, relativePath);
168
  Console.WriteLine($"[DEBUG MainLayout] User: {user.Identity?.Name}, Path: {relativePath}, Allowed: {allowed}");
169
  if (menus != null)
 
193
  {
194
  Navigation.LocationChanged -= OnLocationChanged;
195
  }
196
+
197
+ private static string ResolveDashboardHref(IEnumerable<TaskTrackingSystem.Shared.Models.Menu.MenuDto> menus)
198
+ {
199
+ foreach (var menu in menus)
200
+ {
201
+ var found = ResolveDashboardHref(menu);
202
+ if (!string.IsNullOrWhiteSpace(found))
203
+ {
204
+ return found;
205
+ }
206
+ }
207
+
208
+ return "/dashboard";
209
+ }
210
+
211
+ private static string? ResolveDashboardHref(TaskTrackingSystem.Shared.Models.Menu.MenuDto menu)
212
+ {
213
+ if (!string.IsNullOrWhiteSpace(menu.MenuUrl) &&
214
+ menu.MenuUrl.StartsWith("/dashboard", StringComparison.OrdinalIgnoreCase))
215
+ {
216
+ return menu.MenuUrl;
217
+ }
218
+
219
+ foreach (var subMenu in menu.SubMenus)
220
+ {
221
+ var found = ResolveDashboardHref(subMenu);
222
+ if (!string.IsNullOrWhiteSpace(found))
223
+ {
224
+ return found;
225
+ }
226
+ }
227
+
228
+ return null;
229
+ }
230
  }
TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor CHANGED
@@ -109,14 +109,13 @@
109
 
110
  private List<MenuDto> ProcessMenus(List<MenuDto> rawMenus)
111
  {
112
- var result = new List<MenuDto>();
113
-
114
- foreach (var m in rawMenus)
115
- {
116
- result.Add(m);
117
- }
118
-
119
- return result;
120
  }
121
 
122
  private async Task RefreshMenusAsync()
 
109
 
110
  private List<MenuDto> ProcessMenus(List<MenuDto> rawMenus)
111
  {
112
+ return rawMenus
113
+ .Where(menu => !string.Equals(menu.MenuCode, "ROLE_LAYOUTS", StringComparison.OrdinalIgnoreCase) &&
114
+ !string.Equals(menu.MenuName, "Role Layouts", StringComparison.OrdinalIgnoreCase))
115
+ .OrderBy(menu => string.Equals(menu.MenuCode, "DASHBOARD", StringComparison.OrdinalIgnoreCase) ? 0 : 1)
116
+ .ThenBy(menu => menu.OrderNo)
117
+ .ThenBy(menu => menu.MenuName)
118
+ .ToList();
 
119
  }
120
 
121
  private async Task RefreshMenusAsync()
TaskTrackingSystem.WebApp/Components/Pages/EmployeeDashboard.razor ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/dashboard/employee"
2
+ @using Microsoft.AspNetCore.Authorization
3
+ @rendermode @(new InteractiveServerRenderMode(prerender: false))
4
+ @attribute [Authorize(Roles = "Employee")]
5
+
6
+ <PageTitle>Employee Dashboard</PageTitle>
7
+
8
+ <div class="space-y-6 text-slate-900">
9
+ <div class="rounded-[32px] border border-slate-200 bg-white px-8 py-8 shadow-sm shadow-slate-200/60">
10
+ <p class="text-sm font-semibold uppercase tracking-[0.22em] text-emerald-600">Work View</p>
11
+ <h2 class="mt-2 text-4xl font-bold tracking-tight text-slate-900">Welcome back, team member</h2>
12
+ <p class="mt-3 max-w-2xl text-sm text-slate-600">
13
+ This dashboard is reserved for personal tasks, due dates, and daily progress.
14
+ </p>
15
+ </div>
16
+
17
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
18
+ <div class="rounded-[24px] border border-slate-200 bg-white p-5 shadow-sm shadow-slate-200/50">
19
+ <p class="text-sm font-semibold text-slate-500">In Progress</p>
20
+ <p class="mt-4 text-4xl font-bold text-slate-900">--</p>
21
+ <p class="mt-2 text-sm text-slate-500">Placeholder metric</p>
22
+ </div>
23
+ <div class="rounded-[24px] border border-slate-200 bg-white p-5 shadow-sm shadow-slate-200/50">
24
+ <p class="text-sm font-semibold text-slate-500">To Do</p>
25
+ <p class="mt-4 text-4xl font-bold text-slate-900">--</p>
26
+ <p class="mt-2 text-sm text-slate-500">Placeholder metric</p>
27
+ </div>
28
+ <div class="rounded-[24px] border border-slate-200 bg-white p-5 shadow-sm shadow-slate-200/50">
29
+ <p class="text-sm font-semibold text-slate-500">Due Soon</p>
30
+ <p class="mt-4 text-4xl font-bold text-slate-900">--</p>
31
+ <p class="mt-2 text-sm text-slate-500">Placeholder metric</p>
32
+ </div>
33
+ <div class="rounded-[24px] border border-slate-200 bg-white p-5 shadow-sm shadow-slate-200/50">
34
+ <p class="text-sm font-semibold text-slate-500">Completed</p>
35
+ <p class="mt-4 text-4xl font-bold text-slate-900">--</p>
36
+ <p class="mt-2 text-sm text-slate-500">Placeholder metric</p>
37
+ </div>
38
+ </div>
39
+
40
+ <div class="rounded-[28px] border border-slate-200 bg-white p-6 shadow-sm shadow-slate-200/60">
41
+ <h3 class="text-2xl font-bold text-slate-900">Employee dashboard shell</h3>
42
+ <p class="mt-2 text-sm text-slate-500">
43
+ We’ll plug the real task list, calendar, and progress widgets into this page next.
44
+ </p>
45
+ </div>
46
+ </div>
TaskTrackingSystem.WebApp/Components/Pages/Home.razor CHANGED
@@ -3,28 +3,28 @@
3
  @page "/dashboard"
4
  @using Microsoft.AspNetCore.Authorization
5
  @using Microsoft.AspNetCore.Components.Authorization
 
 
6
  @using TaskTrackingSystem.Shared.Models.Task
7
  @using TaskTrackingSystem.Shared.Models.User
8
- @using TaskTrackingSystem.Shared.Models.Project
9
  @using TaskTrackingSystem.Shared.Models.Dashboard
10
- @using TaskTrackingSystem.Shared.Enums
11
  @rendermode @(new InteractiveServerRenderMode(prerender: false))
12
- @attribute [Microsoft.AspNetCore.Authorization.Authorize]
13
  @inject ApiClientService ApiClient
14
  @inject IJSRuntime JS
15
  @inject AuthenticationStateProvider AuthStateProvider
16
  @inject MenuAuthorizationService MenuAuthorization
17
 
18
- <PageTitle>Overview</PageTitle>
19
 
20
  <div class="space-y-8 text-slate-900">
21
  <div class="rounded-[32px] border border-slate-200 bg-white px-8 py-8 text-slate-900 shadow-sm shadow-slate-200/60">
22
  <div class="flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
23
  <div>
24
  <p class="text-sm font-semibold uppercase tracking-[0.22em] text-violet-600">Overview</p>
25
- <h2 class="mt-2 text-4xl font-bold tracking-tight text-slate-900">Welcome back, @currentUserName</h2>
26
  <p class="mt-3 max-w-2xl text-sm text-slate-600">
27
- Projects, tasks, and team activity at a glance.
28
  </p>
29
  </div>
30
  <div class="flex flex-wrap gap-3">
@@ -49,7 +49,7 @@
49
  <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-violet-500 text-white">
50
  <i data-lucide="users" class="h-5 w-5"></i>
51
  </div>
52
- <span class="text-sm font-semibold text-emerald-500">+12 this month</span>
53
  </div>
54
  <p class="mt-5 text-4xl font-bold">@summary.TotalUsers</p>
55
  <p class="mt-2 text-sm text-slate-500">Total users</p>
@@ -60,9 +60,9 @@
60
  <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-sky-500 text-white">
61
  <i data-lucide="folder-kanban" class="h-5 w-5"></i>
62
  </div>
63
- <span class="text-sm font-semibold text-emerald-500">+3 this month</span>
64
  </div>
65
- <p class="mt-5 text-4xl font-bold">@summary.ActiveProjectsCount</p>
66
  <p class="mt-2 text-sm text-slate-500">Total projects</p>
67
  </div>
68
 
@@ -71,7 +71,7 @@
71
  <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-500 text-white">
72
  <i data-lucide="check-square" class="h-5 w-5"></i>
73
  </div>
74
- <span class="text-sm font-semibold text-emerald-500">+24 this week</span>
75
  </div>
76
  <p class="mt-5 text-4xl font-bold">@ActiveTasksCount</p>
77
  <p class="mt-2 text-sm text-slate-500">Active tasks</p>
@@ -82,7 +82,7 @@
82
  <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-amber-500 text-white">
83
  <i data-lucide="trending-up" class="h-5 w-5"></i>
84
  </div>
85
- <span class="text-sm font-semibold text-emerald-500">+5% vs last mo</span>
86
  </div>
87
  <p class="mt-5 text-4xl font-bold">@CompletionRate%</p>
88
  <p class="mt-2 text-sm text-slate-500">Completion rate</p>
@@ -94,41 +94,59 @@
94
  <div class="flex items-start justify-between gap-4">
95
  <div>
96
  <h3 class="text-2xl font-bold text-slate-900">Project Activity</h3>
97
- <p class="mt-1 text-sm text-slate-500">Created and completed tasks over the last six months.</p>
98
  </div>
99
  <div class="text-sm text-slate-500">@ActivityRangeLabel</div>
100
  </div>
101
 
102
  <div class="mt-6">
103
- <svg viewBox="0 0 760 340" class="h-[340px] w-full">
104
- <defs>
105
- <linearGradient id="activityFill" x1="0" x2="0" y1="0" y2="1">
106
- <stop offset="0%" stop-color="#7c3aed" stop-opacity="0.18" />
107
- <stop offset="100%" stop-color="#7c3aed" stop-opacity="0" />
108
- </linearGradient>
109
- </defs>
110
-
111
- @for (var i = 0; i < 5; i++)
112
- {
113
- var y = 60 + (i * 55);
114
- <line x1="48" y1="@y" x2="730" y2="@y" stroke="#e2e8f0" stroke-width="1" />
115
- }
 
116
 
117
- @for (var i = 0; i < 6; i++)
118
- {
119
- var x = 60 + (i * 125);
120
- <line x1="@x" y1="40" x2="@x" y2="280" stroke="#e2e8f0" stroke-width="1" />
121
- }
122
 
123
- <path d="@ActivityAreaPath" fill="url(#activityFill)" />
124
- <path d="@ActivityLinePath" fill="none" stroke="#7c3aed" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
125
 
126
- @foreach (var point in ActivityPoints)
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  {
128
- <circle cx="@point.X" cy="@point.Y" r="5" fill="#7c3aed" stroke="#ddd6fe" stroke-width="3" />
 
 
 
 
 
129
  }
130
-
131
- </svg>
132
 
133
  <div class="mt-4 flex items-center justify-between text-xs text-slate-500">
134
  @foreach (var label in ActivityLabels)
@@ -147,8 +165,10 @@
147
  <div class="relative">
148
  <svg viewBox="0 0 220 220" class="h-56 w-56">
149
  <circle cx="110" cy="110" r="74" fill="none" stroke="#e5e7eb" stroke-width="24" />
150
- @foreach (var slice in DonutSlices)
151
  {
 
 
152
  <circle cx="110" cy="110" r="74"
153
  fill="none"
154
  stroke="@slice.Color"
@@ -156,19 +176,35 @@
156
  stroke-linecap="round"
157
  stroke-dasharray="@slice.DashArray"
158
  stroke-dashoffset="@slice.DashOffset"
159
- transform="rotate(-90 110 110)" />
 
 
160
  }
161
  </svg>
162
- <div class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center">
163
- <div class="text-3xl font-bold text-slate-900">@TotalTasksCount</div>
164
- <div class="text-xs text-slate-500">tasks</div>
165
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  </div>
167
  </div>
168
 
169
  <div class="mt-3 space-y-3">
170
- @foreach (var item in DistributionLegend)
171
  {
 
172
  <div class="flex items-center justify-between rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3">
173
  <div class="flex items-center gap-3">
174
  <span class="h-3 w-3 rounded-full" style="background-color: @item.Color"></span>
@@ -181,54 +217,138 @@
181
  </div>
182
  </div>
183
 
184
- <div class="grid grid-cols-1 gap-5 xl:grid-cols-3">
185
- <div class="rounded-[28px] border border-slate-200 bg-white p-6 text-slate-900 shadow-sm shadow-slate-200/60 xl:col-span-2">
186
- <div class="flex items-center justify-between">
187
  <div>
188
- <h3 class="text-xl font-bold text-slate-900">Recent Projects</h3>
189
- <p class="mt-1 text-sm text-slate-500">What the team is working on right now.</p>
190
  </div>
 
191
  </div>
192
 
193
- <div class="mt-5 space-y-3">
194
- @foreach (var project in projectsList.OrderByDescending(p => p.CreatedAt).Take(4))
195
- {
196
- <div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4">
197
- <div class="flex items-center justify-between gap-4">
198
- <div class="min-w-0">
199
- <p class="truncate text-sm font-semibold text-slate-900">@project.Name</p>
200
- <p class="mt-1 text-xs text-slate-500">@DisplayFormats.Date(project.StartDate) to @DisplayFormats.Date(project.EndDate)</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  </div>
202
- <span class="text-xs font-semibold text-slate-600">@TaskCountForProject(project.Id) tasks</span>
203
  </div>
204
- </div>
205
- }
 
 
 
 
 
 
206
  </div>
207
  </div>
208
 
209
  <div class="rounded-[28px] border border-slate-200 bg-white p-6 text-slate-900 shadow-sm shadow-slate-200/60">
210
- <h3 class="text-xl font-bold text-slate-900">Team Snapshot</h3>
211
- <p class="mt-1 text-sm text-slate-500">People with the most assigned work.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
- <div class="mt-5 space-y-4">
214
- @foreach (var person in TeamSnapshot)
215
  {
216
- <div>
217
- <div class="flex items-center justify-between text-sm">
218
- <span class="font-semibold text-slate-900">@person.Name</span>
219
- <span class="text-slate-500">@person.Count tasks</span>
 
 
 
 
 
220
  </div>
221
- <div class="mt-2 h-2 overflow-hidden rounded-full bg-slate-200">
222
- <div class="h-full rounded-full bg-emerald-500" style="width: @person.BarWidth%"></div>
 
 
 
 
 
 
 
 
 
 
 
223
  </div>
224
  </div>
225
  }
226
  </div>
 
 
227
 
228
- <a href="/users" class="mt-5 inline-flex w-full items-center justify-center rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm font-semibold text-slate-700 shadow-sm transition-colors hover:bg-slate-50">
229
- View team
 
 
 
 
 
 
230
  </a>
231
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  </div>
233
  </div>
234
 
@@ -240,26 +360,40 @@
240
  private List<UserDto> usersList = new();
241
  private List<ProjectDto> projectsList = new();
242
  private string currentUserName = "User";
 
243
  private bool canCreateTask = false;
244
 
245
- private List<string> ActivityLabels => ActivityMonths.Select(m => m.Label).ToList();
 
246
 
247
- private IReadOnlyList<ActivityMonth> ActivityMonths =>
248
- GetActivityMonths();
249
 
250
- private string ActivityRangeLabel =>
251
- $"{ActivityMonths.First().Label} - {ActivityMonths.Last().Label}";
252
-
253
- private string ActivityLinePath => BuildActivityPath(ActivityMonths, false);
254
- private string ActivityAreaPath => BuildActivityPath(ActivityMonths, true);
255
- private IReadOnlyList<ActivityPoint> ActivityPoints => BuildActivityPoints();
256
- private IReadOnlyList<DistributionSlice> DonutSlices => BuildDonutSlices();
257
- private IReadOnlyList<DistributionItem> DistributionLegend => BuildDistributionLegend();
258
  private int TotalTasksCount => taskList.Count;
259
  private int ActiveTasksCount => taskList.Count(t => t.StatusId != AppTaskStatus.Done);
260
  private int CompletedTasksCount => taskList.Count(t => t.StatusId == AppTaskStatus.Done);
261
  private int CompletionRate => TotalTasksCount > 0 ? (int)Math.Round((double)CompletedTasksCount / TotalTasksCount * 100) : 0;
262
- private IEnumerable<(string Name, int Count, int BarWidth)> TeamSnapshot => BuildTeamSnapshot();
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
  protected override async Task OnInitializedAsync()
265
  {
@@ -276,6 +410,7 @@
276
  var authState = await AuthStateProvider.GetAuthenticationStateAsync();
277
  var user = authState.User;
278
  currentUserName = user.Identity?.Name ?? "User";
 
279
  var summaryResult = await client.GetFromJsonAsync<Result<DashboardSummaryDto>>("Dashboard/summary", Serialization.CaseInsensitive);
280
  if (summaryResult?.IsSuccess == true && summaryResult.Value != null)
281
  {
@@ -297,6 +432,8 @@
297
  taskList = await client.GetFromJsonAsync<List<TaskDto>>("Task", Serialization.CaseInsensitive) ?? new();
298
  usersList = await client.GetFromJsonAsync<List<UserDto>>("User", Serialization.CaseInsensitive) ?? new();
299
  projectsList = await client.GetFromJsonAsync<List<ProjectDto>>("Project", Serialization.CaseInsensitive) ?? new();
 
 
300
  }
301
  catch (Exception ex)
302
  {
@@ -309,27 +446,44 @@
309
  await JS.InvokeVoidAsync("initIcons");
310
  }
311
 
312
- private static string GetMonthLabel(DateTime date) => date.ToString("MMM");
 
 
 
 
 
 
 
 
 
313
 
314
- private IReadOnlyList<ActivityMonth> GetActivityMonths()
315
  {
316
- var months = new List<ActivityMonth>();
317
  var now = DateTime.Today;
318
  var start = new DateTime(now.Year, now.Month, 1).AddMonths(-5);
 
319
 
320
  for (var i = 0; i < 6; i++)
321
  {
322
  var monthStart = start.AddMonths(i);
323
  var nextMonth = monthStart.AddMonths(1);
324
- var created = taskList.Count(task => task.CreatedAt >= monthStart && task.CreatedAt < nextMonth);
325
- var completed = taskList.Count(task => task.CompletedAt.HasValue && task.CompletedAt.Value >= monthStart && task.CompletedAt.Value < nextMonth);
326
- months.Add(new ActivityMonth(GetMonthLabel(monthStart), created, completed));
 
 
 
 
 
 
 
 
327
  }
328
 
329
  return months;
330
  }
331
 
332
- private string BuildActivityPath(IReadOnlyList<ActivityMonth> months, bool filled)
333
  {
334
  if (months.Count == 0)
335
  {
@@ -345,35 +499,56 @@
345
  double X(int i) => left + (i * ((right - left) / (months.Count - 1)));
346
  double Y(double value) => bottom - Math.Min(value, maxValue) / maxValue * (bottom - top);
347
 
348
- var values = months.Select(m => (double)m.Created).ToList();
349
  var points = new List<string>();
350
-
351
  for (var i = 0; i < months.Count; i++)
352
  {
353
- points.Add($"{X(i):0},{Y(values[i]):0}");
354
  }
355
 
356
- var linePath = $"M {points[0]}";
357
- if (points.Count > 1)
358
  {
359
- for (var i = 1; i < points.Count; i++)
360
- {
361
- linePath += $" L {points[i]}";
362
- }
 
 
 
 
 
 
 
363
  }
364
 
365
- if (!filled)
 
 
 
 
 
 
 
 
 
 
366
  {
367
- return linePath;
368
  }
369
 
370
- return $"{linePath} L {X(months.Count - 1):0},{bottom:0} L {X(0):0},{bottom:0} Z";
 
 
 
 
 
 
371
  }
372
 
373
- private IReadOnlyList<ActivityPoint> BuildActivityPoints()
374
  {
375
- var points = new List<ActivityPoint>();
376
  var months = ActivityMonths;
 
377
  if (months.Count == 0)
378
  {
379
  return points;
@@ -383,28 +558,21 @@
383
  const double right = 730;
384
  const double top = 48;
385
  const double bottom = 280;
386
- var maxValue = Math.Max(months.Max(m => m.Created), 1);
 
387
  double X(int i) => left + (i * ((right - left) / (months.Count - 1)));
388
  double Y(double value) => bottom - Math.Min(value, maxValue) / (double)maxValue * (bottom - top);
389
 
390
  for (var i = 0; i < months.Count; i++)
391
  {
392
- points.Add(new ActivityPoint(X(i), Y(months[i].Created)));
393
  }
394
 
395
  return points;
396
  }
397
 
398
- private IReadOnlyList<DistributionSlice> BuildDonutSlices()
399
  {
400
- var colors = new[]
401
- {
402
- "#10b981",
403
- "#8b5cf6",
404
- "#06b6d4",
405
- "#ef4444"
406
- };
407
-
408
  var counts = new[]
409
  {
410
  CompletedTasksCount,
@@ -413,6 +581,8 @@
413
  Math.Max(taskList.Count(t => t.DueDate.Date < DateTime.Today && t.StatusId != AppTaskStatus.Done), 0)
414
  };
415
 
 
 
416
  var total = counts.Sum();
417
  if (total <= 0)
418
  {
@@ -422,53 +592,322 @@
422
  var circumference = 2 * Math.PI * 74;
423
  var slices = new List<DistributionSlice>();
424
  var offset = 0.0;
 
425
  for (var i = 0; i < counts.Length; i++)
426
  {
427
- var value = counts[i];
428
- var fraction = value / (double)total;
429
  var dash = $"{circumference * fraction:0.##} {circumference:0.##}";
430
  var dashOffset = circumference - (circumference * offset);
431
- slices.Add(new DistributionSlice(colors[i], dash, dashOffset));
432
  offset += fraction;
433
  }
 
434
  return slices;
435
  }
436
 
437
- private IReadOnlyList<DistributionItem> BuildDistributionLegend()
438
  {
439
- return new List<DistributionItem>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  {
441
- new("Completed", CompletedTasksCount, "#10b981"),
442
- new("In Progress", taskStatuses.FirstOrDefault(s => s.StatusId == AppTaskStatus.InProgress)?.TaskCount ?? 0, "#8b5cf6"),
443
- new("To Do", taskStatuses.FirstOrDefault(s => s.StatusId == AppTaskStatus.Todo)?.TaskCount ?? 0, "#06b6d4"),
444
- new("Overdue", taskList.Count(t => t.DueDate.Date < DateTime.Today && t.StatusId != AppTaskStatus.Done), "#ef4444")
445
- };
 
 
 
 
 
446
  }
447
 
448
- private IEnumerable<(string Name, int Count, int BarWidth)> BuildTeamSnapshot()
449
  {
450
- var relevantUsers = usersList
451
- .Where(u => !u.Username.Equals(currentUserName, StringComparison.OrdinalIgnoreCase))
452
- .Take(5)
 
 
453
  .ToList();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
 
455
- var maxTasks = Math.Max(taskList.Count, 1);
456
- return relevantUsers
457
- .Select(user =>
 
458
  {
459
- var count = taskList.Count(task => task.AssignedTo == user.Id);
460
- var barWidth = Math.Max(10, (int)Math.Round((double)count / maxTasks * 100));
461
- return (Name: $"{user.FirstName} {user.LastName}", Count: count, BarWidth: barWidth);
462
- })
463
- .OrderByDescending(item => item.Count)
464
- .ToList();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
  }
466
 
467
- private int TaskCountForProject(long projectId) =>
468
- taskList.Count(task => task.ProjectId == projectId);
469
 
470
- private readonly record struct ActivityMonth(string Label, int Created, int Completed);
471
- private readonly record struct ActivityPoint(double X, double Y);
472
- private readonly record struct DistributionSlice(string Color, string DashArray, double DashOffset);
473
- private readonly record struct DistributionItem(string Label, int Count, string Color);
 
 
 
474
  }
 
3
  @page "/dashboard"
4
  @using Microsoft.AspNetCore.Authorization
5
  @using Microsoft.AspNetCore.Components.Authorization
6
+ @using TaskTrackingSystem.Shared.Enums
7
+ @using TaskTrackingSystem.Shared.Models.Project
8
  @using TaskTrackingSystem.Shared.Models.Task
9
  @using TaskTrackingSystem.Shared.Models.User
 
10
  @using TaskTrackingSystem.Shared.Models.Dashboard
 
11
  @rendermode @(new InteractiveServerRenderMode(prerender: false))
12
+ @attribute [Microsoft.AspNetCore.Authorization.Authorize(Roles = "Admin")]
13
  @inject ApiClientService ApiClient
14
  @inject IJSRuntime JS
15
  @inject AuthenticationStateProvider AuthStateProvider
16
  @inject MenuAuthorizationService MenuAuthorization
17
 
18
+ <PageTitle>Dashboard</PageTitle>
19
 
20
  <div class="space-y-8 text-slate-900">
21
  <div class="rounded-[32px] border border-slate-200 bg-white px-8 py-8 text-slate-900 shadow-sm shadow-slate-200/60">
22
  <div class="flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
23
  <div>
24
  <p class="text-sm font-semibold uppercase tracking-[0.22em] text-violet-600">Overview</p>
25
+ <h2 class="mt-2 text-4xl font-bold tracking-tight text-slate-900">Welcome back, @currentUserFullName</h2>
26
  <p class="mt-3 max-w-2xl text-sm text-slate-600">
27
+ Projects, users, and team progress at a glance.
28
  </p>
29
  </div>
30
  <div class="flex flex-wrap gap-3">
 
49
  <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-violet-500 text-white">
50
  <i data-lucide="users" class="h-5 w-5"></i>
51
  </div>
52
+ <span class="text-sm font-semibold text-emerald-500">@UsersDeltaText</span>
53
  </div>
54
  <p class="mt-5 text-4xl font-bold">@summary.TotalUsers</p>
55
  <p class="mt-2 text-sm text-slate-500">Total users</p>
 
60
  <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-sky-500 text-white">
61
  <i data-lucide="folder-kanban" class="h-5 w-5"></i>
62
  </div>
63
+ <span class="text-sm font-semibold text-emerald-500">@ProjectsDeltaText</span>
64
  </div>
65
+ <p class="mt-5 text-4xl font-bold">@projectsList.Count</p>
66
  <p class="mt-2 text-sm text-slate-500">Total projects</p>
67
  </div>
68
 
 
71
  <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-500 text-white">
72
  <i data-lucide="check-square" class="h-5 w-5"></i>
73
  </div>
74
+ <span class="text-sm font-semibold text-emerald-500">@TasksDeltaText</span>
75
  </div>
76
  <p class="mt-5 text-4xl font-bold">@ActiveTasksCount</p>
77
  <p class="mt-2 text-sm text-slate-500">Active tasks</p>
 
82
  <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-amber-500 text-white">
83
  <i data-lucide="trending-up" class="h-5 w-5"></i>
84
  </div>
85
+ <span class="text-sm font-semibold text-emerald-500">@CompletionDeltaText</span>
86
  </div>
87
  <p class="mt-5 text-4xl font-bold">@CompletionRate%</p>
88
  <p class="mt-2 text-sm text-slate-500">Completion rate</p>
 
94
  <div class="flex items-start justify-between gap-4">
95
  <div>
96
  <h3 class="text-2xl font-bold text-slate-900">Project Activity</h3>
97
+ <p class="mt-1 text-sm text-slate-500">Active vs completed tasks per month.</p>
98
  </div>
99
  <div class="text-sm text-slate-500">@ActivityRangeLabel</div>
100
  </div>
101
 
102
  <div class="mt-6">
103
+ <div class="relative">
104
+ <svg viewBox="0 0 760 340" class="h-[340px] w-full">
105
+ <defs>
106
+ <linearGradient id="activityFill" x1="0" x2="0" y1="0" y2="1">
107
+ <stop offset="0%" stop-color="#7c3aed" stop-opacity="0.18" />
108
+ <stop offset="100%" stop-color="#7c3aed" stop-opacity="0" />
109
+ </linearGradient>
110
+ </defs>
111
+
112
+ @for (var i = 0; i < 5; i++)
113
+ {
114
+ var y = 60 + (i * 55);
115
+ <line x1="48" y1="@y" x2="730" y2="@y" stroke="#e2e8f0" stroke-width="1" />
116
+ }
117
 
118
+ @for (var i = 0; i < 6; i++)
119
+ {
120
+ var x = 60 + (i * 125);
121
+ <line x1="@x" y1="40" x2="@x" y2="280" stroke="#e2e8f0" stroke-width="1" />
122
+ }
123
 
124
+ <path d="@ActivityAreaPath" fill="url(#activityFill)" />
125
+ <path d="@ActivityLinePath" fill="none" stroke="#7c3aed" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
126
 
127
+ @for (var i = 0; i < ActivityMonths.Count; i++)
128
+ {
129
+ var index = i;
130
+ var point = ActivityPoints[i];
131
+ <circle cx="@point.X" cy="@point.Y" r="5"
132
+ fill="#7c3aed"
133
+ stroke="#ddd6fe"
134
+ stroke-width="3"
135
+ @onmouseenter="() => SetHoveredActivity(index)"
136
+ @onmouseleave="ClearHoveredActivity" />
137
+ }
138
+ </svg>
139
+
140
+ @if (hoveredActivity is not null)
141
  {
142
+ <div class="pointer-events-none absolute z-20 rounded-2xl border border-slate-200 bg-white px-4 py-3 shadow-lg shadow-slate-200/70"
143
+ style="@HoveredActivityStyle">
144
+ <div class="text-sm font-semibold text-slate-900">@hoveredActivity.Value.Label</div>
145
+ <div class="mt-2 text-sm text-emerald-600">Completed: @hoveredActivity.Value.Completed</div>
146
+ <div class="mt-1 text-sm text-violet-600">Active: @hoveredActivity.Value.Active</div>
147
+ </div>
148
  }
149
+ </div>
 
150
 
151
  <div class="mt-4 flex items-center justify-between text-xs text-slate-500">
152
  @foreach (var label in ActivityLabels)
 
165
  <div class="relative">
166
  <svg viewBox="0 0 220 220" class="h-56 w-56">
167
  <circle cx="110" cy="110" r="74" fill="none" stroke="#e5e7eb" stroke-width="24" />
168
+ @for (var i = 0; i < DonutSlices.Count; i++)
169
  {
170
+ var index = i;
171
+ var slice = DonutSlices[i];
172
  <circle cx="110" cy="110" r="74"
173
  fill="none"
174
  stroke="@slice.Color"
 
176
  stroke-linecap="round"
177
  stroke-dasharray="@slice.DashArray"
178
  stroke-dashoffset="@slice.DashOffset"
179
+ transform="rotate(-90 110 110)"
180
+ @onmouseenter="() => SetHoveredSlice(index)"
181
+ @onmouseleave="ClearHoveredSlice" />
182
  }
183
  </svg>
184
+
185
+ @if (hoveredSlice is not null)
186
+ {
187
+ <div class="pointer-events-none absolute inset-0 flex items-center justify-center">
188
+ <div class="rounded-2xl border border-slate-200 bg-white px-4 py-3 shadow-lg shadow-slate-200/70">
189
+ <div class="text-sm font-semibold text-slate-900">@hoveredSlice.Value.Label</div>
190
+ <div class="mt-1 text-sm text-slate-500">@hoveredSlice.Value.Count tasks</div>
191
+ </div>
192
+ </div>
193
+ }
194
+ else
195
+ {
196
+ <div class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center">
197
+ <div class="text-3xl font-bold text-slate-900">@TotalTasksCount</div>
198
+ <div class="text-xs text-slate-500">tasks</div>
199
+ </div>
200
+ }
201
  </div>
202
  </div>
203
 
204
  <div class="mt-3 space-y-3">
205
+ @for (var i = 0; i < DistributionLegend.Count; i++)
206
  {
207
+ var item = DistributionLegend[i];
208
  <div class="flex items-center justify-between rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3">
209
  <div class="flex items-center gap-3">
210
  <span class="h-3 w-3 rounded-full" style="background-color: @item.Color"></span>
 
217
  </div>
218
  </div>
219
 
220
+ <div class="grid grid-cols-1 gap-5 xl:grid-cols-[minmax(0,1.25fr)_minmax(340px,0.75fr)]">
221
+ <div class="rounded-[28px] border border-slate-200 bg-white p-6 text-slate-900 shadow-sm shadow-slate-200/60">
222
+ <div class="flex items-center justify-between gap-4">
223
  <div>
224
+ <h3 class="text-2xl font-bold text-slate-900">Project Timeline</h3>
225
+ <p class="mt-1 text-sm text-slate-500">Showcasing five projects across the current timeline.</p>
226
  </div>
227
+ <div class="text-sm text-slate-500">@TimelineRangeLabel</div>
228
  </div>
229
 
230
+ <div class="mt-5 overflow-hidden rounded-2xl border border-slate-200 bg-slate-50">
231
+ <div class="grid border-b border-slate-200 bg-white text-center text-xs font-semibold uppercase tracking-wider text-slate-500" style="@TimelineColumnsStyle">
232
+ <div class="px-3 py-3 text-left">Project</div>
233
+ @foreach (var month in TimelineMonths)
234
+ {
235
+ <div class="px-2 py-3">@month.ToString("MMM")</div>
236
+ }
237
+ </div>
238
+
239
+ <div class="divide-y divide-slate-200">
240
+ @foreach (var row in TimelineRows)
241
+ {
242
+ <div class="grid items-center gap-0 px-3 py-4" style="@TimelineColumnsStyle">
243
+ <div class="pr-3">
244
+ <div class="text-sm font-semibold text-slate-900">@row.Name</div>
245
+ <div class="mt-1 text-xs text-slate-500">@row.DateLabel</div>
246
+ </div>
247
+ <div class="relative h-8" style="@TimelineRowSpanStyle">
248
+ <div class="absolute inset-y-3 left-0 right-0 rounded-full bg-slate-200"></div>
249
+ <div class="absolute inset-y-3 rounded-full" style="left: @row.Left%; width: @row.Width%; min-width: 28px; background-color: @row.Color;"></div>
250
+ <div class="absolute inset-y-3 left-0 right-0 border-l-2 border-violet-400/60" style="left: @TodayMarkerLeft;"></div>
251
  </div>
 
252
  </div>
253
+ }
254
+ </div>
255
+
256
+ <div class="flex flex-wrap items-center gap-4 px-4 py-4 text-xs text-slate-500">
257
+ <div class="flex items-center gap-2"><span class="h-4 w-1 rounded-full bg-violet-400"></span> Today</div>
258
+ <div class="flex items-center gap-2"><span class="h-4 w-4 rounded-full bg-slate-300"></span> Planned span</div>
259
+ <div class="flex items-center gap-2"><span class="h-4 w-4 rounded-full bg-sky-400"></span> Project window</div>
260
+ </div>
261
  </div>
262
  </div>
263
 
264
  <div class="rounded-[28px] border border-slate-200 bg-white p-6 text-slate-900 shadow-sm shadow-slate-200/60">
265
+ <div class="flex items-start justify-between gap-4">
266
+ <div>
267
+ <h3 class="text-2xl font-bold text-slate-900">Calendar</h3>
268
+ <p class="mt-1 text-sm text-slate-500">Today and upcoming project starts.</p>
269
+ </div>
270
+ <div class="text-right">
271
+ <div class="text-sm font-semibold text-violet-600">@CalendarMonthLabel</div>
272
+ <div class="text-xs text-slate-500">@DateTime.Now.ToString("h:mm tt")</div>
273
+ </div>
274
+ </div>
275
+
276
+ <div class="mt-5 grid grid-cols-7 gap-2 text-center text-xs font-semibold uppercase tracking-wider text-slate-400">
277
+ @foreach (var day in WeekdayLabels)
278
+ {
279
+ <span>@day</span>
280
+ }
281
+ </div>
282
 
283
+ <div class="mt-3 grid grid-cols-7 gap-2">
284
+ @foreach (var cell in CalendarCells)
285
  {
286
+ <div class="@GetCalendarCellClasses(cell)">
287
+ <div class="flex h-full flex-col justify-between">
288
+ <div class="text-sm font-semibold @(cell.IsCurrentMonth ? "text-slate-900" : "text-slate-400")">@cell.Date.Day</div>
289
+ <div class="flex items-center gap-1">
290
+ @foreach (var dot in cell.ProjectDots)
291
+ {
292
+ <span class="h-1.5 w-1.5 rounded-full" style="background-color: @dot;"></span>
293
+ }
294
+ </div>
295
  </div>
296
+ </div>
297
+ }
298
+ </div>
299
+
300
+ <div class="mt-6 space-y-3">
301
+ <div class="text-xs font-semibold uppercase tracking-wider text-slate-400">Events</div>
302
+ @foreach (var item in UpcomingProjectStarts)
303
+ {
304
+ <div class="flex items-start gap-3 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3">
305
+ <span class="mt-1 h-8 w-1.5 rounded-full" style="background-color: @item.Color"></span>
306
+ <div class="min-w-0">
307
+ <div class="truncate text-sm font-semibold text-slate-900">@item.Name</div>
308
+ <div class="text-xs text-slate-500">@item.DateLabel</div>
309
  </div>
310
  </div>
311
  }
312
  </div>
313
+ </div>
314
+ </div>
315
 
316
+ <div class="rounded-[28px] border border-slate-200 bg-white p-6 text-slate-900 shadow-sm shadow-slate-200/60">
317
+ <div class="flex items-center justify-between gap-4">
318
+ <div>
319
+ <h3 class="text-2xl font-bold text-slate-900">All Projects</h3>
320
+ <p class="mt-1 text-sm text-slate-500">A quick snapshot of the current project list.</p>
321
+ </div>
322
+ <a href="/projects/all" class="inline-flex items-center rounded-2xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-violet-700 shadow-sm transition-colors hover:bg-violet-50">
323
+ View all
324
  </a>
325
  </div>
326
+
327
+ <div class="mt-5 space-y-3">
328
+ @foreach (var project in FeaturedProjects)
329
+ {
330
+ var progress = GetProjectProgress(project.Id);
331
+ <div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4">
332
+ <div class="flex items-start justify-between gap-4">
333
+ <div class="min-w-0">
334
+ <div class="flex flex-wrap items-center gap-2">
335
+ <p class="truncate text-sm font-semibold text-slate-900">@project.Name</p>
336
+ <span class="@GetProjectBadgeClasses(project)" >@GetProjectBadgeLabel(project)</span>
337
+ </div>
338
+ <p class="mt-1 text-xs text-slate-500">@DisplayFormats.Date(project.StartDate) to @DisplayFormats.Date(project.EndDate)</p>
339
+ </div>
340
+ <div class="text-right">
341
+ <div class="text-sm font-semibold text-slate-900">@progress.CompletedTasksCount/@progress.TotalTasksCount tasks</div>
342
+ <div class="text-xs text-slate-500">@progress.CompletionPercentage%</div>
343
+ </div>
344
+ </div>
345
+
346
+ <div class="mt-3 h-2 overflow-hidden rounded-full bg-slate-200">
347
+ <div class="h-full rounded-full bg-violet-600" style="width: @progress.CompletionPercentage%"></div>
348
+ </div>
349
+ </div>
350
+ }
351
+ </div>
352
  </div>
353
  </div>
354
 
 
360
  private List<UserDto> usersList = new();
361
  private List<ProjectDto> projectsList = new();
362
  private string currentUserName = "User";
363
+ private string currentUserFullName = "User";
364
  private bool canCreateTask = false;
365
 
366
+ private int? hoveredActivityIndex;
367
+ private int? hoveredSliceIndex;
368
 
369
+ private readonly string[] WeekdayLabels = ["M", "T", "W", "T", "F", "S", "S"];
 
370
 
371
+ private List<ActivityMonthStat> ActivityMonths => BuildActivityMonths();
372
+ private List<string> ActivityLabels => ActivityMonths.Select(month => month.Label).ToList();
373
+ private string ActivityRangeLabel => $"{ActivityMonths.First().Label} - {ActivityMonths.Last().Label}";
374
+ private string ActivityLinePath => BuildActivityPath(ActivityMonths);
375
+ private string ActivityAreaPath => BuildActivityAreaPath(ActivityMonths);
376
+ private List<ActivityPoint> ActivityPoints => BuildActivityPoints();
377
+ private List<DistributionSlice> DonutSlices => BuildDonutSlices();
378
+ private List<DistributionLegendItem> DistributionLegend => BuildDistributionLegend();
379
  private int TotalTasksCount => taskList.Count;
380
  private int ActiveTasksCount => taskList.Count(t => t.StatusId != AppTaskStatus.Done);
381
  private int CompletedTasksCount => taskList.Count(t => t.StatusId == AppTaskStatus.Done);
382
  private int CompletionRate => TotalTasksCount > 0 ? (int)Math.Round((double)CompletedTasksCount / TotalTasksCount * 100) : 0;
383
+ private string UsersDeltaText => BuildDeltaText(GetUsersThisMonth() - GetUsersPreviousMonth());
384
+ private string ProjectsDeltaText => BuildDeltaText(GetProjectsThisMonth() - GetProjectsPreviousMonth());
385
+ private string TasksDeltaText => BuildDeltaText(GetActiveTasksThisMonth() - GetActiveTasksPreviousMonth());
386
+ private string CompletionDeltaText => BuildCompletionDeltaText();
387
+ private string CalendarMonthLabel => DateTime.Today.ToString("MMMM yyyy");
388
+ private List<CalendarCell> CalendarCells => BuildCalendarCells();
389
+ private List<CalendarEventItem> UpcomingProjectStarts => BuildUpcomingProjectStarts();
390
+ private List<ProjectDto> FeaturedProjects => projectsList.OrderBy(p => p.StartDate).ThenByDescending(p => p.CreatedAt).Take(5).ToList();
391
+ private List<TimelineRow> TimelineRows => BuildTimelineRows();
392
+ private List<DateTime> TimelineMonths => BuildTimelineMonths();
393
+ private string TimelineRangeLabel => TimelineMonths.Count > 0 ? $"{TimelineMonths.First():MMM yyyy} - {TimelineMonths.Last():MMM yyyy}" : string.Empty;
394
+ private string TimelineColumnsStyle => $"grid-template-columns: 180px repeat({TimelineMonths.Count}, minmax(44px, 1fr));";
395
+ private string TimelineRowSpanStyle => $"grid-column: 2 / span {TimelineMonths.Count};";
396
+ private string TodayMarkerLeft => GetTimelineTodayMarkerLeft();
397
 
398
  protected override async Task OnInitializedAsync()
399
  {
 
410
  var authState = await AuthStateProvider.GetAuthenticationStateAsync();
411
  var user = authState.User;
412
  currentUserName = user.Identity?.Name ?? "User";
413
+
414
  var summaryResult = await client.GetFromJsonAsync<Result<DashboardSummaryDto>>("Dashboard/summary", Serialization.CaseInsensitive);
415
  if (summaryResult?.IsSuccess == true && summaryResult.Value != null)
416
  {
 
432
  taskList = await client.GetFromJsonAsync<List<TaskDto>>("Task", Serialization.CaseInsensitive) ?? new();
433
  usersList = await client.GetFromJsonAsync<List<UserDto>>("User", Serialization.CaseInsensitive) ?? new();
434
  projectsList = await client.GetFromJsonAsync<List<ProjectDto>>("Project", Serialization.CaseInsensitive) ?? new();
435
+
436
+ currentUserFullName = ResolveCurrentUserFullName();
437
  }
438
  catch (Exception ex)
439
  {
 
446
  await JS.InvokeVoidAsync("initIcons");
447
  }
448
 
449
+ private string ResolveCurrentUserFullName()
450
+ {
451
+ var user = usersList.FirstOrDefault(u => string.Equals(u.Username, currentUserName, StringComparison.OrdinalIgnoreCase));
452
+ if (user != null)
453
+ {
454
+ return $"{user.FirstName} {user.LastName}".Trim();
455
+ }
456
+
457
+ return currentUserName;
458
+ }
459
 
460
+ private List<ActivityMonthStat> BuildActivityMonths()
461
  {
 
462
  var now = DateTime.Today;
463
  var start = new DateTime(now.Year, now.Month, 1).AddMonths(-5);
464
+ var months = new List<ActivityMonthStat>();
465
 
466
  for (var i = 0; i < 6; i++)
467
  {
468
  var monthStart = start.AddMonths(i);
469
  var nextMonth = monthStart.AddMonths(1);
470
+ var active = taskList.Count(task =>
471
+ task.CreatedAt >= monthStart &&
472
+ task.CreatedAt < nextMonth &&
473
+ task.StatusId != AppTaskStatus.Done);
474
+
475
+ var completed = taskList.Count(task =>
476
+ task.CompletedAt.HasValue &&
477
+ task.CompletedAt.Value >= monthStart &&
478
+ task.CompletedAt.Value < nextMonth);
479
+
480
+ months.Add(new ActivityMonthStat(GetMonthLabel(monthStart), active, completed));
481
  }
482
 
483
  return months;
484
  }
485
 
486
+ private string BuildActivityPath(IReadOnlyList<ActivityMonthStat> months)
487
  {
488
  if (months.Count == 0)
489
  {
 
499
  double X(int i) => left + (i * ((right - left) / (months.Count - 1)));
500
  double Y(double value) => bottom - Math.Min(value, maxValue) / maxValue * (bottom - top);
501
 
 
502
  var points = new List<string>();
 
503
  for (var i = 0; i < months.Count; i++)
504
  {
505
+ points.Add($"{X(i):0},{Y(months[i].Active):0}");
506
  }
507
 
508
+ var path = $"M {points[0]}";
509
+ for (var i = 1; i < points.Count; i++)
510
  {
511
+ path += $" L {points[i]}";
512
+ }
513
+
514
+ return path;
515
+ }
516
+
517
+ private string BuildActivityAreaPath(IReadOnlyList<ActivityMonthStat> months)
518
+ {
519
+ if (months.Count == 0)
520
+ {
521
+ return string.Empty;
522
  }
523
 
524
+ const double left = 48;
525
+ const double right = 730;
526
+ const double top = 48;
527
+ const double bottom = 280;
528
+ const double maxValue = 20;
529
+
530
+ double X(int i) => left + (i * ((right - left) / (months.Count - 1)));
531
+ double Y(double value) => bottom - Math.Min(value, maxValue) / maxValue * (bottom - top);
532
+
533
+ var points = new List<string>();
534
+ for (var i = 0; i < months.Count; i++)
535
  {
536
+ points.Add($"{X(i):0},{Y(months[i].Active):0}");
537
  }
538
 
539
+ var path = $"M {points[0]}";
540
+ for (var i = 1; i < points.Count; i++)
541
+ {
542
+ path += $" L {points[i]}";
543
+ }
544
+
545
+ return $"{path} L {X(months.Count - 1):0},{bottom:0} L {X(0):0},{bottom:0} Z";
546
  }
547
 
548
+ private List<ActivityPoint> BuildActivityPoints()
549
  {
 
550
  var months = ActivityMonths;
551
+ var points = new List<ActivityPoint>();
552
  if (months.Count == 0)
553
  {
554
  return points;
 
558
  const double right = 730;
559
  const double top = 48;
560
  const double bottom = 280;
561
+ var maxValue = Math.Max(months.Max(m => m.Active), 1);
562
+
563
  double X(int i) => left + (i * ((right - left) / (months.Count - 1)));
564
  double Y(double value) => bottom - Math.Min(value, maxValue) / (double)maxValue * (bottom - top);
565
 
566
  for (var i = 0; i < months.Count; i++)
567
  {
568
+ points.Add(new ActivityPoint(months[i].Label, X(i), Y(months[i].Active), months[i].Active, months[i].Completed));
569
  }
570
 
571
  return points;
572
  }
573
 
574
+ private List<DistributionSlice> BuildDonutSlices()
575
  {
 
 
 
 
 
 
 
 
576
  var counts = new[]
577
  {
578
  CompletedTasksCount,
 
581
  Math.Max(taskList.Count(t => t.DueDate.Date < DateTime.Today && t.StatusId != AppTaskStatus.Done), 0)
582
  };
583
 
584
+ var colors = new[] { "#10b981", "#8b5cf6", "#06b6d4", "#ef4444" };
585
+
586
  var total = counts.Sum();
587
  if (total <= 0)
588
  {
 
592
  var circumference = 2 * Math.PI * 74;
593
  var slices = new List<DistributionSlice>();
594
  var offset = 0.0;
595
+
596
  for (var i = 0; i < counts.Length; i++)
597
  {
598
+ var fraction = counts[i] / (double)total;
 
599
  var dash = $"{circumference * fraction:0.##} {circumference:0.##}";
600
  var dashOffset = circumference - (circumference * offset);
601
+ slices.Add(new DistributionSlice(GetDistributionLabel(i), counts[i], colors[i], dash, dashOffset));
602
  offset += fraction;
603
  }
604
+
605
  return slices;
606
  }
607
 
608
+ private static string GetDistributionLabel(int index) => index switch
609
  {
610
+ 0 => "Completed",
611
+ 1 => "In Progress",
612
+ 2 => "To Do",
613
+ _ => "Overdue"
614
+ };
615
+
616
+ private List<DistributionLegendItem> BuildDistributionLegend()
617
+ {
618
+ return DonutSlices.Select(slice => new DistributionLegendItem(slice.Label, slice.Count, slice.Color)).ToList();
619
+ }
620
+
621
+ private List<CalendarCell> BuildCalendarCells()
622
+ {
623
+ var today = DateTime.Today;
624
+ var firstOfMonth = new DateTime(today.Year, today.Month, 1);
625
+ var start = firstOfMonth.AddDays(-(int)((((int)firstOfMonth.DayOfWeek + 6) % 7)));
626
+ var projectStarts = projectsList
627
+ .Select(project => new { project.StartDate, Color = GetProjectColor(project.Id) })
628
+ .Where(item => item.StartDate.Date >= start.Date && item.StartDate.Date < start.AddDays(42))
629
+ .ToList();
630
+
631
+ var cells = new List<CalendarCell>();
632
+ for (var i = 0; i < 42; i++)
633
  {
634
+ var date = start.AddDays(i);
635
+ var markers = projectStarts
636
+ .Where(item => item.StartDate.Date == date.Date)
637
+ .Select(item => item.Color)
638
+ .ToList();
639
+
640
+ cells.Add(new CalendarCell(date, date.Month == today.Month, date.Date == today.Date, markers));
641
+ }
642
+
643
+ return cells;
644
  }
645
 
646
+ private List<CalendarEventItem> BuildUpcomingProjectStarts()
647
  {
648
+ return projectsList
649
+ .Where(project => project.StartDate.Date >= DateTime.Today)
650
+ .OrderBy(project => project.StartDate)
651
+ .Take(4)
652
+ .Select(project => new CalendarEventItem(project.Name, project.StartDate.ToString("MMM d"), GetProjectColor(project.Id)))
653
  .ToList();
654
+ }
655
+
656
+ private List<DateTime> BuildTimelineMonths()
657
+ {
658
+ var featured = FeaturedProjects;
659
+ if (featured.Count == 0)
660
+ {
661
+ return new List<DateTime>();
662
+ }
663
+
664
+ var start = new DateTime(featured.Min(p => p.StartDate).Year, featured.Min(p => p.StartDate).Month, 1);
665
+ var end = new DateTime(featured.Max(p => p.EndDate).Year, featured.Max(p => p.EndDate).Month, 1);
666
+
667
+ var months = new List<DateTime>();
668
+ var current = start;
669
+ while (current <= end)
670
+ {
671
+ months.Add(current);
672
+ current = current.AddMonths(1);
673
+ }
674
+
675
+ return months;
676
+ }
677
+
678
+ private List<TimelineRow> BuildTimelineRows()
679
+ {
680
+ var months = TimelineMonths;
681
+ if (months.Count == 0)
682
+ {
683
+ return new List<TimelineRow>();
684
+ }
685
+
686
+ var first = months.First();
687
+ var monthSpan = Math.Max(months.Count - 1, 1);
688
+
689
+ return FeaturedProjects.Select(project =>
690
+ {
691
+ var startIndex = ((project.StartDate.Year - first.Year) * 12) + project.StartDate.Month - first.Month;
692
+ var endIndex = ((project.EndDate.Year - first.Year) * 12) + project.EndDate.Month - first.Month;
693
+ startIndex = Math.Clamp(startIndex, 0, monthSpan);
694
+ endIndex = Math.Clamp(endIndex, startIndex, monthSpan);
695
+
696
+ var left = (startIndex / (double)monthSpan) * 100;
697
+ var width = Math.Max(6, ((endIndex - startIndex + 1) / (double)(monthSpan + 1)) * 100);
698
+ var progress = GetProjectProgress(project.Id);
699
+
700
+ return new TimelineRow(
701
+ project.Name,
702
+ $"{DisplayFormats.Date(project.StartDate)} to {DisplayFormats.Date(project.EndDate)}",
703
+ GetProjectColor(project.Id),
704
+ left,
705
+ width,
706
+ progress.CompletionPercentage);
707
+ }).ToList();
708
+ }
709
+
710
+ private string GetTimelineTodayMarkerLeft()
711
+ {
712
+ var months = TimelineMonths;
713
+ if (months.Count == 0)
714
+ {
715
+ return "0%";
716
+ }
717
+
718
+ var first = months.First();
719
+ var monthSpan = Math.Max(months.Count - 1, 1);
720
+ var currentIndex = ((DateTime.Today.Year - first.Year) * 12) + DateTime.Today.Month - first.Month;
721
+ currentIndex = Math.Clamp(currentIndex, 0, monthSpan);
722
+ return $"{(currentIndex / (double)monthSpan) * 100:0.##}%";
723
+ }
724
 
725
+ private ProjectProgressDto GetProjectProgress(long projectId)
726
+ {
727
+ return projectProgress.FirstOrDefault(item => item.ProjectId == projectId)
728
+ ?? new ProjectProgressDto
729
  {
730
+ ProjectId = projectId,
731
+ ProjectName = string.Empty,
732
+ TotalTasksCount = taskList.Count(task => task.ProjectId == projectId),
733
+ CompletedTasksCount = taskList.Count(task => task.ProjectId == projectId && task.StatusId == AppTaskStatus.Done),
734
+ CompletionPercentage = 0
735
+ };
736
+ }
737
+
738
+ private string GetProjectBadgeLabel(ProjectDto project)
739
+ {
740
+ return project.StartDate.Date > DateTime.Today ? "Upcoming" : "Active";
741
+ }
742
+
743
+ private string GetProjectBadgeClasses(ProjectDto project)
744
+ {
745
+ return project.StartDate.Date > DateTime.Today
746
+ ? "rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold text-amber-700"
747
+ : "rounded-full border border-violet-200 bg-violet-50 px-3 py-1 text-xs font-semibold text-violet-700";
748
+ }
749
+
750
+ private string GetProjectColor(long projectId)
751
+ {
752
+ var palette = new[]
753
+ {
754
+ "#7c3aed",
755
+ "#0ea5e9",
756
+ "#10b981",
757
+ "#f59e0b",
758
+ "#f97316"
759
+ };
760
+
761
+ var index = (int)(projectId % palette.Length);
762
+ return palette[index];
763
+ }
764
+
765
+ private string GetCalendarCellClasses(CalendarCell cell)
766
+ {
767
+ var classes = new List<string>
768
+ {
769
+ "min-h-[62px] rounded-2xl border p-2"
770
+ };
771
+
772
+ if (cell.IsToday)
773
+ {
774
+ classes.Add("border-violet-200 bg-violet-50");
775
+ }
776
+ else if (cell.IsCurrentMonth)
777
+ {
778
+ classes.Add("border-slate-200 bg-white");
779
+ }
780
+ else
781
+ {
782
+ classes.Add("border-slate-100 bg-slate-50/70");
783
+ }
784
+
785
+ return string.Join(' ', classes);
786
+ }
787
+
788
+ private void SetHoveredActivity(int index)
789
+ {
790
+ hoveredActivityIndex = index;
791
+ }
792
+
793
+ private void ClearHoveredActivity()
794
+ {
795
+ hoveredActivityIndex = null;
796
+ }
797
+
798
+ private void SetHoveredSlice(int index)
799
+ {
800
+ hoveredSliceIndex = index;
801
+ }
802
+
803
+ private void ClearHoveredSlice()
804
+ {
805
+ hoveredSliceIndex = null;
806
+ }
807
+
808
+ private ActivityMonthStat? hoveredActivity =>
809
+ hoveredActivityIndex.HasValue && hoveredActivityIndex.Value < ActivityMonths.Count
810
+ ? ActivityMonths[hoveredActivityIndex.Value]
811
+ : null;
812
+
813
+ private DistributionSlice? hoveredSlice =>
814
+ hoveredSliceIndex.HasValue && hoveredSliceIndex.Value < DonutSlices.Count
815
+ ? DonutSlices[hoveredSliceIndex.Value]
816
+ : null;
817
+
818
+ private string HoveredActivityStyle
819
+ {
820
+ get
821
+ {
822
+ if (!hoveredActivityIndex.HasValue || hoveredActivityIndex.Value >= ActivityPoints.Count)
823
+ {
824
+ return string.Empty;
825
+ }
826
+
827
+ var point = ActivityPoints[hoveredActivityIndex.Value];
828
+ return $"left: {point.X / 760d * 100:0.##}%; top: {point.Y / 340d * 100:0.##}%; transform: translate(-50%, -110%);";
829
+ }
830
+ }
831
+
832
+ private string BuildDeltaText(int delta)
833
+ {
834
+ if (delta > 0)
835
+ {
836
+ return $"+{delta} this month";
837
+ }
838
+
839
+ if (delta < 0)
840
+ {
841
+ return $"{delta} this month";
842
+ }
843
+
844
+ return "0 this month";
845
+ }
846
+
847
+ private string BuildCompletionDeltaText()
848
+ {
849
+ var current = GetCompletionRateForMonth(DateTime.Today);
850
+ var previous = GetCompletionRateForMonth(DateTime.Today.AddMonths(-1));
851
+ var delta = current - previous;
852
+
853
+ if (delta > 0)
854
+ {
855
+ return $"+{delta}% vs last mo";
856
+ }
857
+
858
+ if (delta < 0)
859
+ {
860
+ return $"{delta}% vs last mo";
861
+ }
862
+
863
+ return "0% vs last mo";
864
+ }
865
+
866
+ private int GetUsersThisMonth() =>
867
+ usersList.Count(user => user.CreatedAt.Year == DateTime.Today.Year && user.CreatedAt.Month == DateTime.Today.Month);
868
+
869
+ private int GetUsersPreviousMonth()
870
+ {
871
+ var previous = DateTime.Today.AddMonths(-1);
872
+ return usersList.Count(user => user.CreatedAt.Year == previous.Year && user.CreatedAt.Month == previous.Month);
873
+ }
874
+
875
+ private int GetProjectsThisMonth() =>
876
+ projectsList.Count(project => project.CreatedAt.Year == DateTime.Today.Year && project.CreatedAt.Month == DateTime.Today.Month);
877
+
878
+ private int GetProjectsPreviousMonth()
879
+ {
880
+ var previous = DateTime.Today.AddMonths(-1);
881
+ return projectsList.Count(project => project.CreatedAt.Year == previous.Year && project.CreatedAt.Month == previous.Month);
882
+ }
883
+
884
+ private int GetActiveTasksThisMonth() =>
885
+ taskList.Count(task => task.CreatedAt.Year == DateTime.Today.Year && task.CreatedAt.Month == DateTime.Today.Month && task.StatusId != AppTaskStatus.Done);
886
+
887
+ private int GetActiveTasksPreviousMonth()
888
+ {
889
+ var previous = DateTime.Today.AddMonths(-1);
890
+ return taskList.Count(task => task.CreatedAt.Year == previous.Year && task.CreatedAt.Month == previous.Month && task.StatusId != AppTaskStatus.Done);
891
+ }
892
+
893
+ private int GetCompletionRateForMonth(DateTime date)
894
+ {
895
+ var monthTasks = taskList.Where(task => task.CreatedAt.Year == date.Year && task.CreatedAt.Month == date.Month).ToList();
896
+ if (monthTasks.Count == 0)
897
+ {
898
+ return 0;
899
+ }
900
+
901
+ return (int)Math.Round(monthTasks.Count(task => task.StatusId == AppTaskStatus.Done) / (double)monthTasks.Count * 100);
902
  }
903
 
904
+ private string GetMonthLabel(DateTime date) => date.ToString("MMM");
 
905
 
906
+ private readonly record struct ActivityMonthStat(string Label, int Active, int Completed);
907
+ private readonly record struct ActivityPoint(string Label, double X, double Y, int Active, int Completed);
908
+ private readonly record struct DistributionSlice(string Label, int Count, string Color, string DashArray, double DashOffset);
909
+ private readonly record struct DistributionLegendItem(string Label, int Count, string Color);
910
+ private readonly record struct CalendarCell(DateTime Date, bool IsCurrentMonth, bool IsToday, List<string> ProjectDots);
911
+ private readonly record struct CalendarEventItem(string Name, string DateLabel, string Color);
912
+ private readonly record struct TimelineRow(string Name, string DateLabel, string Color, double Left, double Width, double CompletionPercentage);
913
  }
TaskTrackingSystem.WebApp/Components/Pages/Login.razor CHANGED
@@ -4,6 +4,7 @@
4
  @attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous]
5
  @inject NavigationManager Navigation
6
  @inject AuthenticationStateProvider AuthStateProvider
 
7
  @inject IJSRuntime JS
8
 
9
  <PageTitle>Sign In — TaskFlow</PageTitle>
@@ -114,7 +115,7 @@
114
  var authState = await AuthStateProvider.GetAuthenticationStateAsync();
115
  if (authState.User.Identity?.IsAuthenticated == true)
116
  {
117
- Navigation.NavigateTo("/dashboard", forceLoad: true);
118
  }
119
  }
120
  }
@@ -126,4 +127,56 @@
126
  showPassword = !showPassword;
127
  await JS.InvokeVoidAsync("initIcons");
128
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  }
 
4
  @attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous]
5
  @inject NavigationManager Navigation
6
  @inject AuthenticationStateProvider AuthStateProvider
7
+ @inject MenuAuthorizationService MenuAuthorization
8
  @inject IJSRuntime JS
9
 
10
  <PageTitle>Sign In — TaskFlow</PageTitle>
 
115
  var authState = await AuthStateProvider.GetAuthenticationStateAsync();
116
  if (authState.User.Identity?.IsAuthenticated == true)
117
  {
118
+ Navigation.NavigateTo(await ResolveLandingPageAsync(authState.User), forceLoad: true);
119
  }
120
  }
121
  }
 
127
  showPassword = !showPassword;
128
  await JS.InvokeVoidAsync("initIcons");
129
  }
130
+
131
+ private async Task<string> ResolveLandingPageAsync(System.Security.Claims.ClaimsPrincipal user)
132
+ {
133
+ try
134
+ {
135
+ var menus = await MenuAuthorization.GetUserMenusAsync(user);
136
+ var landing = FindFirstDashboardMenu(menus);
137
+ if (!string.IsNullOrWhiteSpace(landing))
138
+ {
139
+ return landing;
140
+ }
141
+ }
142
+ catch
143
+ {
144
+ }
145
+
146
+ return "/dashboard";
147
+ }
148
+
149
+ private static string? FindFirstDashboardMenu(IEnumerable<TaskTrackingSystem.Shared.Models.Menu.MenuDto> menus)
150
+ {
151
+ foreach (var menu in menus)
152
+ {
153
+ var found = FindFirstDashboardMenu(menu);
154
+ if (!string.IsNullOrWhiteSpace(found))
155
+ {
156
+ return found;
157
+ }
158
+ }
159
+
160
+ return null;
161
+ }
162
+
163
+ private static string? FindFirstDashboardMenu(TaskTrackingSystem.Shared.Models.Menu.MenuDto menu)
164
+ {
165
+ if (!string.IsNullOrWhiteSpace(menu.MenuUrl) &&
166
+ menu.MenuUrl.StartsWith("/dashboard", StringComparison.OrdinalIgnoreCase))
167
+ {
168
+ return menu.MenuUrl;
169
+ }
170
+
171
+ foreach (var subMenu in menu.SubMenus)
172
+ {
173
+ var found = FindFirstDashboardMenu(subMenu);
174
+ if (!string.IsNullOrWhiteSpace(found))
175
+ {
176
+ return found;
177
+ }
178
+ }
179
+
180
+ return null;
181
+ }
182
  }
TaskTrackingSystem.WebApp/Components/Pages/ManagerDashboard.razor ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/dashboard/manager"
2
+ @using Microsoft.AspNetCore.Authorization
3
+ @rendermode @(new InteractiveServerRenderMode(prerender: false))
4
+ @attribute [Authorize(Roles = "Manager")]
5
+
6
+ <PageTitle>Manager Dashboard</PageTitle>
7
+
8
+ <div class="space-y-6 text-slate-900">
9
+ <div class="rounded-[32px] border border-slate-200 bg-white px-8 py-8 shadow-sm shadow-slate-200/60">
10
+ <p class="text-sm font-semibold uppercase tracking-[0.22em] text-sky-600">Team View</p>
11
+ <h2 class="mt-2 text-4xl font-bold tracking-tight text-slate-900">Welcome back, manager</h2>
12
+ <p class="mt-3 max-w-2xl text-sm text-slate-600">
13
+ This dashboard is reserved for project tracking, team health, and workload planning.
14
+ </p>
15
+ </div>
16
+
17
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
18
+ <div class="rounded-[24px] border border-slate-200 bg-white p-5 shadow-sm shadow-slate-200/50">
19
+ <p class="text-sm font-semibold text-slate-500">Projects</p>
20
+ <p class="mt-4 text-4xl font-bold text-slate-900">--</p>
21
+ <p class="mt-2 text-sm text-slate-500">Placeholder metric</p>
22
+ </div>
23
+ <div class="rounded-[24px] border border-slate-200 bg-white p-5 shadow-sm shadow-slate-200/50">
24
+ <p class="text-sm font-semibold text-slate-500">Team Members</p>
25
+ <p class="mt-4 text-4xl font-bold text-slate-900">--</p>
26
+ <p class="mt-2 text-sm text-slate-500">Placeholder metric</p>
27
+ </div>
28
+ <div class="rounded-[24px] border border-slate-200 bg-white p-5 shadow-sm shadow-slate-200/50">
29
+ <p class="text-sm font-semibold text-slate-500">Pending Tasks</p>
30
+ <p class="mt-4 text-4xl font-bold text-slate-900">--</p>
31
+ <p class="mt-2 text-sm text-slate-500">Placeholder metric</p>
32
+ </div>
33
+ <div class="rounded-[24px] border border-slate-200 bg-white p-5 shadow-sm shadow-slate-200/50">
34
+ <p class="text-sm font-semibold text-slate-500">Risk Items</p>
35
+ <p class="mt-4 text-4xl font-bold text-slate-900">--</p>
36
+ <p class="mt-2 text-sm text-slate-500">Placeholder metric</p>
37
+ </div>
38
+ </div>
39
+
40
+ <div class="rounded-[28px] border border-slate-200 bg-white p-6 shadow-sm shadow-slate-200/60">
41
+ <h3 class="text-2xl font-bold text-slate-900">Manager dashboard shell</h3>
42
+ <p class="mt-2 text-sm text-slate-500">
43
+ We’ll plug the real charts, team views, and project blocks into this page next.
44
+ </p>
45
+ </div>
46
+ </div>