@page "/dashboard/manager" @using Microsoft.AspNetCore.Authorization @rendermode @(new InteractiveServerRenderMode(prerender: false)) @attribute [Authorize] @inject ApiClientService ApiClient @inject NavigationManager Navigation @inject IJSRuntime JS @inject AuthenticationStateProvider AuthStateProvider Manager Dashboard

Team View

My Projects

Track your team, project health, and weekly task progress in one place. @if (summary.PendingTasksCount > 0) { @summary.PendingTasksCount tasks are still pending. }

@if (isLoading) {
Loading dashboard...
} else if (!string.IsNullOrWhiteSpace(errorMessage)) {
@errorMessage
} else {
@WeeklyRangeLabel

@CalendarMonthLabel

@CalendarTodayLabel

@foreach (var day in WeekdayLabels) { @day }
@foreach (var cell in CalendarCells) {
@cell.Date.Day
@foreach (var dot in cell.DeadlineDots) { }
}

Upcoming Deadlines

Projects ending soon

@if (UpcomingDeadlines.Any()) { @foreach (var item in UpcomingDeadlines) {
@item.Name
ends @item.EndDateLabel
@item.DaysLeftLabel
} } else {
No upcoming deadlines.
}

My Projects

Click "View Tasks" to drill in

View all
@if (ProjectCards.Any()) { @foreach (var project in ProjectCards) {

@project.Name

@project.StatusLabel
@project.Completed/@project.Total tasks @project.Overdue overdue @project.TeamSize members
@project.ProgressPercentage.ToString("F0")%
Complete
} } else {
No projects available for this account.
}
}
@code { private const double SvgWidth = 760; private const double PlotLeft = 56; private const double PlotRight = 724; private const double PlotTop = 40; private const double PlotBottom = 200; private const double BarWidth = 18; private bool isLoading = true; private string? errorMessage; private List projects = new(); private List tasks = new(); private List users = new(); private List projectProgress = new(); private DashboardSummaryDto summary = new(); private int? hoveredWeekIndex; private List WeeklyProgress => BuildWeeklyProgress(); private WeeklyProgressViewModel? hoveredWeek => hoveredWeekIndex is int index && index >= 0 && index < WeeklyProgress.Count ? WeeklyProgress[index] : null; private string HoveredWeekTooltipStyle => hoveredWeek is null ? string.Empty : $"left: {hoveredWeek.CenterPercent:F2}%; top: 34px; transform: translateX(-50%);"; private string WeeklyRangeLabel => WeeklyProgress.Count > 0 ? $"{WeeklyProgress.First().DateRangeLabel} - {WeeklyProgress.Last().DateRangeLabel}" : DateTime.Today.ToString("MMMM yyyy"); private List SummaryMetrics => BuildSummaryMetrics(); private List ProjectCards => BuildProjectCards(); private List TeamProgress => BuildTeamProgress(); private List TeamProgressItems => TeamProgress .Select(member => new ProgressListChartItem( member.FullName, member.CompletionPercentage, $"{member.Completed}/{member.Total}", member.Color, $"{member.Label} - {member.CompletionPercentage:F0}%", member.Initials)) .ToList(); private List WeeklyBarGroups => WeeklyProgress .Select(week => new BarChartGroup( week.Label, new[] { new ChartDataPoint("Completed", week.Completed, "#3b82f6"), new ChartDataPoint("Pending", week.Pending, "#14b8a6") }, week.DateRangeLabel)) .ToList(); private List YTicks => BuildYAxisTicks(); private List CalendarCells => BuildCalendarCells(); private List UpcomingDeadlines => BuildUpcomingDeadlines(); private readonly string[] WeekdayLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; private string CalendarMonthLabel => DateTime.Today.ToString("MMMM yyyy"); private string CalendarTodayLabel => DateTime.Today.ToString("dddd, d"); private string CurrentTimeLabel => DateTime.Now.ToString("h:mm tt"); private string TimeZoneLabel { get { var offset = TimeZoneInfo.Local.GetUtcOffset(DateTime.Now); var sign = offset >= TimeSpan.Zero ? "+" : "-"; var hours = Math.Abs(offset.Hours); var minutes = Math.Abs(offset.Minutes); return minutes == 0 ? $"GMT{sign}{hours}" : $"GMT{sign}{hours}:{minutes:00}"; } } protected override async Task OnInitializedAsync() { await LoadDashboardData(); } protected override async Task OnAfterRenderAsync(bool firstRender) { await JS.InvokeVoidAsync("initIcons"); } private async Task LoadDashboardData() { isLoading = true; errorMessage = null; try { var client = ApiClient.CreateClient(); var summaryTask = client.GetFromJsonAsync>("Dashboard/summary", Serialization.CaseInsensitive); var progressTask = client.GetFromJsonAsync>>("Dashboard/project-progress", Serialization.CaseInsensitive); var projectsTask = client.GetFromJsonAsync>("Project", Serialization.CaseInsensitive); var tasksTask = client.GetFromJsonAsync>("Task", Serialization.CaseInsensitive); var usersTask = client.GetFromJsonAsync>("User", Serialization.CaseInsensitive); await Task.WhenAll(summaryTask, progressTask, projectsTask, tasksTask, usersTask); var summaryResult = await summaryTask; if (summaryResult?.IsSuccess == true) { summary = summaryResult.Value ?? new DashboardSummaryDto(); } var progressResult = await progressTask; if (progressResult?.IsSuccess == true) { projectProgress = progressResult.Value?.ToList() ?? new List(); } projects = (await projectsTask) ?? new List(); tasks = (await tasksTask) ?? new List(); users = (await usersTask) ?? new List(); } catch (Exception ex) { errorMessage = $"Failed to load dashboard data: {ex.Message}"; } finally { isLoading = false; } } private void ViewProjectTasks(long projectId) { Navigation.NavigateTo($"/projects/{projectId}/tasks"); } private void SetHoveredWeek(int index) { hoveredWeekIndex = index; StateHasChanged(); } private void ClearHoveredWeek() { if (hoveredWeekIndex is null) { return; } hoveredWeekIndex = null; StateHasChanged(); } private List BuildSummaryMetrics() { var activeProjects = summary.ActiveProjectsCount > 0 ? summary.ActiveProjectsCount : projects.Count; var totalTasks = tasks.Count; var teamMembers = summary.TotalUsers > 0 ? summary.TotalUsers : users.Count; var overdueTasks = tasks.Count(task => task.DueDate.Date < DateTime.Today && task.StatusId != AppTaskStatus.Done); return new List { new("folder-kanban", "all active", activeProjects.ToString(), "Active Projects", "rounded-2xl bg-violet-700 p-3 text-white", "text-sm font-semibold text-emerald-700"), new("check-square", "across projects", totalTasks.ToString(), "Total Tasks", "rounded-2xl bg-cyan-600 p-3 text-white", "text-sm font-semibold text-emerald-700"), new("users", "your team", teamMembers.ToString(), "Team Members", "rounded-2xl bg-emerald-700 p-3 text-white", "text-sm font-semibold text-emerald-700"), new("triangle-alert", "needs attention", overdueTasks.ToString(), "Overdue Tasks", "rounded-2xl bg-rose-600 p-3 text-white", "text-sm font-semibold text-rose-600") }; } private List BuildProjectCards() { var progressByProject = projectProgress.ToDictionary(item => item.ProjectId, item => item); var palette = new[] { "#2563eb", "#0ea5e9", "#10b981", "#f59e0b", "#f97316" }; return projects .OrderByDescending(project => progressByProject.TryGetValue(project.Id, out var progress) ? progress.CompletionPercentage : 0) .ThenBy(project => project.EndDate) .Select((project, index) => { progressByProject.TryGetValue(project.Id, out var progress); var projectTasks = tasks.Where(task => task.ProjectId == project.Id).ToList(); var total = progress?.TotalTasksCount ?? projectTasks.Count; var completed = progress?.CompletedTasksCount ?? projectTasks.Count(task => task.StatusId == AppTaskStatus.Done); var percentage = total > 0 ? Math.Round((double)completed / total * 100, 2) : 0; var overdue = projectTasks.Count(task => task.DueDate.Date < DateTime.Today && task.StatusId != AppTaskStatus.Done); var teamSize = projectTasks .Where(task => task.AssignedTo.HasValue) .Select(task => task.AssignedTo!.Value) .Distinct() .Count(); var isOverdue = project.EndDate.Date < DateTime.Today && percentage < 100; var isUpcoming = project.StartDate.Date > DateTime.Today; var statusLabel = isOverdue ? "At Risk" : isUpcoming ? "Upcoming" : "Active"; var statusClass = isOverdue ? "rounded-full border border-rose-500/30 bg-rose-500/10 px-3 py-1 text-xs font-semibold text-rose-300" : isUpcoming ? "rounded-full border border-amber-500/30 bg-amber-500/10 px-3 py-1 text-xs font-semibold text-amber-300" : "rounded-full border border-emerald-500/30 bg-emerald-500/10 px-3 py-1 text-xs font-semibold text-emerald-300"; return new ProjectCardViewModel( project.Id, project.Name, completed, total, overdue, teamSize, percentage, statusLabel, statusClass, palette[index % palette.Length]); }) .ToList(); } private List BuildTeamProgress() { var palette = new[] { "#2563eb", "#0ea5e9", "#10b981", "#f59e0b", "#f97316", "#ec4899" }; return users .Select((user, index) => { var userTasks = tasks.Where(task => task.AssignedTo == user.Id).ToList(); var total = userTasks.Count; if (total == 0) { return null; } var completed = userTasks.Count(task => task.StatusId == AppTaskStatus.Done); var percentage = Math.Round((double)completed / total * 100, 2); var initials = BuildInitials(user.FirstName, user.LastName); var label = total == 1 ? "1 task" : $"{total} tasks"; return new TeamProgressViewModel( user.Id, $"{user.FirstName} {user.LastName}".Trim(), initials, completed, total, percentage, label, palette[index % palette.Length]); }) .Where(item => item is not null) .Cast() .OrderByDescending(item => item.Total) .ThenBy(item => item.FullName) .ToList(); } private List BuildCalendarCells() { var today = DateTime.Today; var firstOfMonth = new DateTime(today.Year, today.Month, 1); var start = firstOfMonth.AddDays(-(int)firstOfMonth.DayOfWeek); var deadlineMap = projects .Where(project => project.EndDate.Date >= start.Date && project.EndDate.Date < start.AddDays(42)) .GroupBy(project => project.EndDate.Date) .ToDictionary(group => group.Key, group => group.Select(project => GetProjectColor(project.Id)).ToList()); var cells = new List(); for (var i = 0; i < 42; i++) { var date = start.AddDays(i); deadlineMap.TryGetValue(date.Date, out var dots); cells.Add(new CalendarCellViewModel( date, date.Month == today.Month, date.Date == today.Date, dots ?? new List())); } return cells; } private List BuildUpcomingDeadlines() { return projects .Where(project => project.EndDate.Date >= DateTime.Today) .OrderBy(project => project.EndDate) .Select(project => { var daysLeft = (project.EndDate.Date - DateTime.Today).Days; var label = daysLeft == 0 ? "Today" : daysLeft == 1 ? "1d" : $"{daysLeft}d"; var labelClass = daysLeft <= 14 ? "text-amber-500 font-semibold" : "text-slate-500"; return new DeadlineItemViewModel( project.Id, project.Name, project.EndDate.ToString("yyyy-MM-dd"), label, labelClass, GetProjectColor(project.Id)); }) .ToList(); } private List BuildWeeklyProgress() { var today = DateTime.Today; var currentWeekStart = StartOfWeek(today); var weekStarts = Enumerable.Range(-3, 4) .Select(offset => currentWeekStart.AddDays(offset * 7)) .ToList(); var rows = new List(); var maxTotal = 1; foreach (var weekStart in weekStarts) { var weekEnd = weekStart.AddDays(7); var weekTasks = tasks.Where(task => task.CreatedAt >= weekStart && task.CreatedAt < weekEnd).ToList(); var completed = weekTasks.Count(task => task.StatusId == AppTaskStatus.Done); var pending = weekTasks.Count - completed; maxTotal = Math.Max(maxTotal, completed + pending); rows.Add(new WeeklyProgressViewModel( $"W{rows.Count + 1}", weekStart, weekEnd.AddDays(-1), completed, pending, 0, 0, 0, 0, PlotBottom, 0, 0, FormatDateRange(weekStart, weekEnd.AddDays(-1)))); } if (rows.Count == 0) { return rows; } var slotWidth = (PlotRight - PlotLeft) / 4.0; for (var i = 0; i < rows.Count; i++) { var row = rows[i]; var total = row.Completed + row.Pending; var completedHeight = total > 0 ? Math.Round(row.Completed / (double)maxTotal * (PlotBottom - PlotTop), 2) : 0; var pendingHeight = total > 0 ? Math.Round(row.Pending / (double)maxTotal * (PlotBottom - PlotTop), 2) : 0; var centerX = PlotLeft + (slotWidth * i) + (slotWidth / 2); var completedX = centerX - 22; var pendingX = centerX + 4; var hoverX = PlotLeft + (slotWidth * i) + 10; var hoverWidth = slotWidth - 20; rows[i] = row with { CompletedHeight = completedHeight, PendingHeight = pendingHeight, CompletedX = completedX, PendingX = pendingX, HoverX = hoverX, HoverWidth = hoverWidth, LabelX = centerX, CenterPercent = centerX / SvgWidth * 100 }; } return rows; } private List BuildYAxisTicks() { var max = Math.Max(1, WeeklyProgress.Max(item => item.Completed + item.Pending)); var interval = Math.Max(1, (int)Math.Ceiling(max / 4.0)); var highest = interval * 4; return new List { new(0, "0"), new(highest * 0.25, (highest * 0.25).ToString("0")), new(highest * 0.5, (highest * 0.5).ToString("0")), new(highest * 0.75, (highest * 0.75).ToString("0")), new(highest, highest.ToString("0")) }; } private double TickToY(double tickValue) { var max = Math.Max(1, WeeklyProgress.Max(item => item.Completed + item.Pending)); var chartHeight = PlotBottom - PlotTop; return PlotBottom - (Math.Min(tickValue, max) / max * chartHeight); } private static DateTime StartOfWeek(DateTime date) { var diff = (int)date.DayOfWeek; return date.AddDays(-diff).Date; } private static string FormatDateRange(DateTime start, DateTime end) { if (start.Month == end.Month && start.Year == end.Year) { return $"{start:MMM d} - {end:dd}"; } return $"{start:MMM d} - {end:MMM d}"; } private static string BuildInitials(string firstName, string lastName) { var first = string.IsNullOrWhiteSpace(firstName) ? string.Empty : firstName.Trim()[0].ToString().ToUpperInvariant(); var last = string.IsNullOrWhiteSpace(lastName) ? string.Empty : lastName.Trim()[0].ToString().ToUpperInvariant(); return $"{first}{last}"; } private string GetProjectColor(long projectId) { var palette = new[] { "#2563eb", "#0ea5e9", "#10b981", "#f59e0b", "#f97316" }; return palette[(int)(Math.Abs(projectId) % palette.Length)]; } private string GetCalendarCellClasses(CalendarCellViewModel cell) { var classes = new List { "min-h-[62px] rounded-2xl border p-2" }; if (cell.IsToday) { classes.Add("border-violet-600 bg-violet-600"); } else if (cell.IsCurrentMonth) { classes.Add("border-slate-200 bg-white"); } else { classes.Add("border-slate-100 bg-slate-50 text-slate-400"); } return string.Join(" ", classes); } private sealed record ProjectCardViewModel( long Id, string Name, int Completed, int Total, int Overdue, int TeamSize, double ProgressPercentage, string StatusLabel, string StatusClass, string Color); private sealed record TeamProgressViewModel( long UserId, string FullName, string Initials, int Completed, int Total, double CompletionPercentage, string Label, string Color); private sealed record WeeklyProgressViewModel( string Label, DateTime StartDate, DateTime EndDate, int Completed, int Pending, double CompletedHeight, double PendingHeight, double CompletedX, double PendingX, double BaseY, double HoverX, double HoverWidth, string DateRangeLabel) { public double LabelX { get; init; } public double CenterPercent { get; init; } } private sealed record YAxisTickViewModel(double Value, string Label); private sealed record CalendarCellViewModel( DateTime Date, bool IsCurrentMonth, bool IsToday, List DeadlineDots); private sealed record DeadlineItemViewModel( long ProjectId, string Name, string EndDateLabel, string DaysLeftLabel, string DaysLeftClass, string Color); }