@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 @AppLocalization.Text("dashboard.managerTitle", "Manager Dashboard")

@AppLocalization.Text("dashboard.teamView", "Team View")

@AppLocalization.Text("dashboard.myProjects", "My Projects")

@AppLocalization.Text("dashboard.managerIntro", "Track your team, project health, and weekly task progress in one place.") @if (summary.PendingTasksCount > 0) { @AppLocalization.Text("dashboard.pendingTasksNotice", "{count} tasks are still pending.").Replace("{count}", AppLocalization.Number(summary.PendingTasksCount)) }

@if (isLoading) {
@AppLocalization.Text("dashboard.loadingDashboard", "Loading dashboard...")
} else if (!string.IsNullOrWhiteSpace(errorMessage)) {
@errorMessage
} else {
@WeeklyRangeLabel

@CalendarMonthLabel

@CalendarTodayLabel

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

@AppLocalization.Text("dashboard.upcomingDeadlines", "Upcoming Deadlines")

@AppLocalization.Text("dashboard.projectsEndingSoon", "Projects ending soon")

@if (UpcomingDeadlines.Any()) { @foreach (var item in UpcomingDeadlines) {
@Content.GetText(item.Name, item.NameMy)
@AppLocalization.LocalizeDigits(item.EndDateLabel)
@item.DaysLeftLabel
} } else {
@AppLocalization.Text("dashboard.noUpcomingDeadlines", "No upcoming deadlines.")
}

@AppLocalization.Text("dashboard.myProjects", "My Projects")

@AppLocalization.Text("dashboard.clickViewTasks", "Click \"View Tasks\" to drill in")

@AppLocalization.Text("dashboard.viewAll", "View all")
@if (ProjectCards.Any()) { @foreach (var project in ProjectCards) {

@Content.GetText(project.Name, project.NameMy)

@project.StatusLabel
@AppLocalization.Number(project.Completed)/@AppLocalization.Number(project.Total) @AppLocalization.Text("dashboard.tasksUnit", "tasks") @AppLocalization.Number(project.Overdue) @AppLocalization.Text("status.overdue", "overdue") @AppLocalization.Number(project.TeamSize) @AppLocalization.Text("dashboard.teamMembers", "members")
@AppLocalization.Number(project.ProgressPercentage, "0")%
@AppLocalization.Text("dashboard.complete", "Complete")
} } else {
@AppLocalization.Text("dashboard.noProjectsForAccount", "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}" : GetMonthYearLabel(DateTime.Today); private List SummaryMetrics => BuildSummaryMetrics(); private List ProjectCards => BuildProjectCards(); private List TeamProgress => BuildTeamProgress(); private List TeamProgressItems => TeamProgress .Select(member => new ProgressListChartItem( member.FullName, member.CompletionPercentage, $"{AppLocalization.Number(member.Completed)}/{AppLocalization.Number(member.Total)}", member.Color, $"{member.Label} - {AppLocalization.Number(member.CompletionPercentage, "0")}%", member.Initials)) .ToList(); private List WeeklyBarGroups => WeeklyProgress .Select(week => new BarChartGroup( week.Label, new[] { new ChartDataPoint(AppLocalization.Text("status.done", "Completed"), week.Completed, "#3b82f6"), new ChartDataPoint(AppLocalization.Text("dashboard.pending", "Pending"), week.Pending, "#14b8a6") }, week.DateRangeLabel)) .ToList(); private List YTicks => BuildYAxisTicks(); private List CalendarCells => BuildCalendarCells(); private List UpcomingDeadlines => BuildUpcomingDeadlines(); private string[] WeekdayLabels => AppLocalization.IsBurmeseCulture ? ["တနင်္ဂနွေ", "တနင်္လာ", "အင်္ဂါ", "ဗုဒ္ဓဟူး", "ကြာသပတေး", "သောကြာ", "စနေ"] : ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; private string CalendarMonthLabel => GetMonthYearLabel(DateTime.Today); private string CalendarTodayLabel => GetDisplayDateLabel(DateTime.Today); 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", AppLocalization.Text("dashboard.allActive", "all active"), AppLocalization.Number(activeProjects), AppLocalization.Text("dashboard.activeProjects", "Active Projects"), "rounded-2xl bg-violet-700 p-3 text-white", "text-sm font-semibold text-emerald-700"), new("check-square", AppLocalization.Text("dashboard.acrossProjects", "across projects"), AppLocalization.Number(totalTasks), AppLocalization.Text("dashboard.totalTasksMetric", "Total Tasks"), "rounded-2xl bg-cyan-600 p-3 text-white", "text-sm font-semibold text-emerald-700"), new("users", AppLocalization.Text("dashboard.yourTeam", "your team"), AppLocalization.Number(teamMembers), AppLocalization.Text("dashboard.teamMembers", "Team Members"), "rounded-2xl bg-emerald-700 p-3 text-white", "text-sm font-semibold text-emerald-700"), new("triangle-alert", AppLocalization.Text("dashboard.needsAttention", "needs attention"), AppLocalization.Number(overdueTasks), AppLocalization.Text("dashboard.overdueTasks", "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 ? AppLocalization.Text("dashboard.atRisk", "At Risk") : isUpcoming ? AppLocalization.Text("dashboard.upcoming", "Upcoming") : AppLocalization.Text("dashboard.active", "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, project.NameMy, 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 displayName = Content.GetUserFullName(user); var initials = AppLocalization.IsBurmeseCulture ? BuildInitialsFromDisplayName(displayName) : BuildInitials(user.FirstName, user.LastName); var label = $"{AppLocalization.Number(total)} {AppLocalization.Text("dashboard.tasksUnit", "tasks")}"; return new TeamProgressViewModel( user.Id, displayName, 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 ? AppLocalization.Text("dashboard.today", "Today") : AppLocalization.Text("dashboard.daysLeft", "{count}d left").Replace("{count}", AppLocalization.Number(daysLeft)); var labelClass = daysLeft <= 14 ? "text-amber-500 font-semibold" : "text-slate-500"; return new DeadlineItemViewModel( project.Id, project.Name, project.NameMy, 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( GetWeekLabel(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 $"{AppLocalization.MonthName(start, true)} {AppLocalization.Number(start.Day)} - {AppLocalization.Number(end.Day)}"; } return $"{AppLocalization.MonthName(start, true)} {AppLocalization.Number(start.Day)} - {AppLocalization.MonthName(end, true)} {AppLocalization.Number(end.Day)}"; } private static string GetWeekLabel(int index) => AppLocalization.IsBurmeseCulture ? AppLocalization.Number(index) : $"W{index}"; 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 static string BuildInitialsFromDisplayName(string displayName) { var parts = displayName .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (parts.Length >= 2) { return $"{parts[0][0]}{parts[^1][0]}"; } return displayName.Length > 1 ? displayName[..2] : displayName; } private string GetProjectColor(long projectId) { var palette = new[] { "#2563eb", "#0ea5e9", "#10b981", "#f59e0b", "#f97316" }; return palette[(int)(Math.Abs(projectId) % palette.Length)]; } private string GetDisplayDateLabel(DateTime date) => AppLocalization.IsBurmeseCulture ? $"{AppLocalization.MonthName(date, true)} {AppLocalization.Number(date.Day)}" : date.ToString("dddd, d"); private string GetMonthYearLabel(DateTime date) => AppLocalization.IsBurmeseCulture ? $"{AppLocalization.Number(date.Year)} {AppLocalization.MonthName(date)}" : date.ToString("MMMM yyyy"); 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, string? NameMy, 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? NameMy, string EndDateLabel, string DaysLeftLabel, string DaysLeftClass, string Color); }