@page "/" @page "/Home" @page "/dashboard" @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using TaskTrackingSystem.Shared.Enums @using TaskTrackingSystem.Shared.Models.Project @using TaskTrackingSystem.Shared.Models.Task @using TaskTrackingSystem.Shared.Models.User @using TaskTrackingSystem.Shared.Models.Dashboard @rendermode @(new InteractiveServerRenderMode(prerender: false)) @attribute [Microsoft.AspNetCore.Authorization.Authorize] @inject ApiClientService ApiClient @inject IJSRuntime JS @inject AuthenticationStateProvider AuthStateProvider @inject MenuAuthorizationService MenuAuthorization @inject NavigationManager NavigationManager Dashboard

Overview

Welcome back, @currentUserFullName

Projects, users, and team progress at a glance.

@if (canCreateTask) { New Task } Reports

Project Activity

Active vs completed tasks per month.

@ActivityRangeLabel
@for (var i = 0; i < 5; i++) { var y = 60 + (i * 55); } @for (var i = 0; i < 6; i++) { var x = 60 + (i * 125); } @for (var i = 0; i < ActivityMonths.Count; i++) { var index = i; var point = ActivityPoints[i]; } @if (hoveredActivity is not null) {
@hoveredActivity.Value.Label
Completed: @hoveredActivity.Value.Completed
Active: @hoveredActivity.Value.Active
}
@foreach (var label in ActivityLabels) { @label }

All Projects

A quick snapshot of the current project list.

View all
@foreach (var project in ProjectListItems) { var progress = GetProjectProgress(project.Id);

@project.Name

@GetProjectBadgeLabel(project)

@DisplayFormats.Date(project.StartDate) to @DisplayFormats.Date(project.EndDate)

@progress.CompletedTasksCount/@progress.TotalTasksCount tasks
@progress.CompletionPercentage%
}

Calendar

Today and upcoming project starts.

@CalendarMonthLabel
@foreach (var day in WeekdayLabels) { @day }
@foreach (var cell in CalendarCells) {
@cell.Date.Day
@foreach (var dot in cell.ProjectDots) { }
}
Events
@foreach (var item in UpcomingProjectStarts) {
@item.Name
@item.DateLabel
}

Project Timeline

Browse every project in a gantt view and adjust the month window below.

View all
@TimelineRangeLabel
@if (TimelineProjects.Count > 0) {
} else {
No projects to display.
}
@code { private DashboardSummaryDto summary = new(); private List taskStatuses = new(); private List projectProgress = new(); private List taskList = new(); private List usersList = new(); private List projectsList = new(); private string currentUserName = "User"; private string currentUserFullName = "User"; private bool canCreateTask = false; private string timelineStartMonthValue = string.Empty; private string timelineEndMonthValue = string.Empty; private bool _dataLoaded = false; private bool _shouldRenderCharts = false; private int? hoveredActivityIndex; private int? hoveredSliceIndex; private readonly string[] WeekdayLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; private List ActivityMonths => BuildActivityMonths(); private List ActivityLabels => ActivityMonths.Select(month => month.Label).ToList(); private string ActivityRangeLabel => $"{ActivityMonths.First().Label} - {ActivityMonths.Last().Label}"; private string ActivityLinePath => BuildActivityPath(ActivityMonths); private string ActivityAreaPath => BuildActivityAreaPath(ActivityMonths); private List ActivityPoints => BuildActivityPoints(); private List DonutSlices => BuildDonutSlices(); private List DistributionLegend => BuildDistributionLegend(); private List DistributionChartItems => DistributionLegend .Select(item => new ChartDataPoint(item.Label, item.Count, item.Color)) .ToList(); private int TotalTasksCount => taskList.Count; private int ActiveTasksCount => taskList.Count(t => t.StatusId != AppTaskStatus.Done); private int CompletedTasksCount => taskList.Count(t => t.StatusId == AppTaskStatus.Done); private int CompletionRate => TotalTasksCount > 0 ? (int)Math.Round((double)CompletedTasksCount / TotalTasksCount * 100) : 0; private string UsersDeltaText => BuildDeltaText(GetUsersThisMonth() - GetUsersPreviousMonth()); private string ProjectsDeltaText => BuildDeltaText(GetProjectsThisMonth() - GetProjectsPreviousMonth()); private string TasksDeltaText => BuildDeltaText(GetActiveTasksThisMonth() - GetActiveTasksPreviousMonth()); private string CompletionDeltaText => BuildCompletionDeltaText(); private List SummaryMetrics => new() { new("users", UsersDeltaText, summary.TotalUsers.ToString(), "Total users", "flex h-12 w-12 items-center justify-center rounded-lg bg-teal-700 text-white dark:bg-teal-600", "text-sm font-semibold text-emerald-600 dark:text-emerald-300"), new("folder-kanban", ProjectsDeltaText, projectsList.Count.ToString(), "Total projects", "flex h-12 w-12 items-center justify-center rounded-2xl bg-sky-500 text-white", "text-sm font-semibold text-emerald-500"), new("check-square", TasksDeltaText, ActiveTasksCount.ToString(), "Active tasks", "flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-500 text-white", "text-sm font-semibold text-emerald-500"), new("trending-up", CompletionDeltaText, $"{CompletionRate}%", "Completion rate", "flex h-12 w-12 items-center justify-center rounded-2xl bg-amber-500 text-white", "text-sm font-semibold text-emerald-500") }; private string CalendarMonthLabel => DateTime.Today.ToString("MMMM yyyy"); private List CalendarCells => BuildCalendarCells(); private List UpcomingProjectStarts => BuildUpcomingProjectStarts(); private List FeaturedProjects => TimelineProjects.Take(5).ToList(); private List ProjectListItems => projectsList.OrderBy(p => p.StartDate).ThenByDescending(p => p.CreatedAt).ToList(); private List TimelineProjects => projectsList.OrderBy(p => p.StartDate).ThenByDescending(p => p.CreatedAt).ToList(); private List TimelineMonths => BuildTimelineMonths(); private string TimelineRangeLabel => TryGetTimelineRange(out var range) ? $"{range.Start:MMM yyyy} - {range.End:MMM yyyy}" : string.Empty; protected override async Task OnInitializedAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var user = authState.User; var codes = await MenuAuthorization.GetCurrentAccessCodesAsync(user); if (codes.Contains("Projects_Create") && !codes.Contains("Roles_List")) { NavigationManager.NavigateTo("/dashboard/manager"); return; } else if (!codes.Contains("Projects_Create") && !codes.Contains("Roles_List")) { NavigationManager.NavigateTo("/dashboard/employee"); return; } canCreateTask = codes.Contains("Tasks_Create"); await LoadDashboardData(); } private async Task LoadDashboardData() { var client = ApiClient.CreateClient(); try { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var user = authState.User; currentUserName = user.Identity?.Name ?? "User"; var summaryResult = await client.GetFromJsonAsync>("Dashboard/summary", Serialization.CaseInsensitive); if (summaryResult?.IsSuccess == true && summaryResult.Value != null) { summary = summaryResult.Value; } var tasksOverviewResult = await client.GetFromJsonAsync>>("Dashboard/tasks-overview", Serialization.CaseInsensitive); if (tasksOverviewResult?.IsSuccess == true && tasksOverviewResult.Value != null) { taskStatuses = tasksOverviewResult.Value; } var progressResult = await client.GetFromJsonAsync>>("Dashboard/project-progress", Serialization.CaseInsensitive); if (progressResult?.IsSuccess == true && progressResult.Value != null) { projectProgress = progressResult.Value; } taskList = await client.GetFromJsonAsync>("Task", Serialization.CaseInsensitive) ?? new(); usersList = await client.GetFromJsonAsync>("User", Serialization.CaseInsensitive) ?? new(); projectsList = await client.GetFromJsonAsync>("Project", Serialization.CaseInsensitive) ?? new(); InitializeTimelineRange(); currentUserFullName = ResolveCurrentUserFullName(); _dataLoaded = true; _shouldRenderCharts = true; StateHasChanged(); } catch (Exception ex) { Console.WriteLine($"Dashboard load error: {ex.Message}"); } } protected override async Task OnAfterRenderAsync(bool firstRender) { await JS.InvokeVoidAsync("initIcons"); if (_shouldRenderCharts && _dataLoaded) { _shouldRenderCharts = false; await RenderChartsAsync(); } } private async Task RenderChartsAsync() { try { var ganttData = TimelineProjects.Select(p => { var progress = GetProjectProgress(p.Id); return new { id = p.Id.ToString(), name = p.Name, start = new DateTimeOffset(DateTime.SpecifyKind(p.StartDate.Date, DateTimeKind.Utc)).ToUnixTimeMilliseconds(), end = new DateTimeOffset(DateTime.SpecifyKind(p.EndDate.Date, DateTimeKind.Utc)).ToUnixTimeMilliseconds(), completed = progress.CompletionPercentage / 100.0, color = GetProjectColor(p.Id) }; }).ToList(); if (ganttData.Count > 0 && TryGetTimelineRange(out var timelineRange)) { await JS.InvokeVoidAsync("renderGanttChart", "project-timeline-chart", ganttData, new { height = 420, minWidth = Math.Max(900, GetTimelineMonthSpan(timelineRange.Start, timelineRange.End) * 110), xAxisMin = ToUnixMilliseconds(timelineRange.Start), xAxisMax = ToUnixMilliseconds(timelineRange.End) }); } } catch (Exception ex) { Console.WriteLine($"Error rendering Highcharts: {ex.Message}"); } } private string ResolveCurrentUserFullName() { var user = usersList.FirstOrDefault(u => string.Equals(u.Username, currentUserName, StringComparison.OrdinalIgnoreCase)); if (user != null) { return $"{user.FirstName} {user.LastName}".Trim(); } return currentUserName; } private List BuildActivityMonths() { var now = DateTime.Today; var start = new DateTime(now.Year, now.Month, 1).AddMonths(-5); var months = new List(); for (var i = 0; i < 6; i++) { var monthStart = start.AddMonths(i); var nextMonth = monthStart.AddMonths(1); var active = taskList.Count(task => task.CreatedAt >= monthStart && task.CreatedAt < nextMonth && task.StatusId != AppTaskStatus.Done); var completed = taskList.Count(task => task.CompletedAt.HasValue && task.CompletedAt.Value >= monthStart && task.CompletedAt.Value < nextMonth); months.Add(new ActivityMonthStat(GetMonthLabel(monthStart), active, completed)); } return months; } private string BuildActivityPath(IReadOnlyList months) { if (months.Count == 0) { return string.Empty; } const double left = 48; const double right = 730; const double top = 48; const double bottom = 280; const double maxValue = 20; double X(int i) => left + (i * ((right - left) / (months.Count - 1))); double Y(double value) => bottom - Math.Min(value, maxValue) / maxValue * (bottom - top); var points = new List(); for (var i = 0; i < months.Count; i++) { points.Add($"{X(i):0},{Y(months[i].Active):0}"); } var path = $"M {points[0]}"; for (var i = 1; i < points.Count; i++) { path += $" L {points[i]}"; } return path; } private string BuildActivityAreaPath(IReadOnlyList months) { if (months.Count == 0) { return string.Empty; } const double left = 48; const double right = 730; const double top = 48; const double bottom = 280; const double maxValue = 20; double X(int i) => left + (i * ((right - left) / (months.Count - 1))); double Y(double value) => bottom - Math.Min(value, maxValue) / maxValue * (bottom - top); var points = new List(); for (var i = 0; i < months.Count; i++) { points.Add($"{X(i):0},{Y(months[i].Active):0}"); } var path = $"M {points[0]}"; for (var i = 1; i < points.Count; i++) { path += $" L {points[i]}"; } return $"{path} L {X(months.Count - 1):0},{bottom:0} L {X(0):0},{bottom:0} Z"; } private List BuildActivityPoints() { var months = ActivityMonths; var points = new List(); if (months.Count == 0) { return points; } const double left = 48; const double right = 730; const double top = 48; const double bottom = 280; var maxValue = Math.Max(months.Max(m => m.Active), 1); double X(int i) => left + (i * ((right - left) / (months.Count - 1))); double Y(double value) => bottom - Math.Min(value, maxValue) / (double)maxValue * (bottom - top); for (var i = 0; i < months.Count; i++) { points.Add(new ActivityPoint(months[i].Label, X(i), Y(months[i].Active), months[i].Active, months[i].Completed)); } return points; } private List BuildDonutSlices() { // 1. Calculate counts entirely from the same taskList to fix data mismatches var todoCount = taskList.Count(t => t.StatusId == AppTaskStatus.Todo); var inProgressCount = taskList.Count(t => t.StatusId == AppTaskStatus.InProgress); var overdueCount = taskList.Count(t => t.DueDate.Date < DateTime.Today && t.StatusId != AppTaskStatus.Done); var rawCounts = new[] { CompletedTasksCount, // Already calculated from taskList inProgressCount, todoCount, overdueCount }; var colors = new[] { "#0f766e", "#38bdf8", "#94a3b8", "#dc2626" }; // 2. Adjust total so it maps 100% cleanly to the math displayed var total = Math.Max(TotalTasksCount, 1); var circumference = 2 * Math.PI * 74; // 2πr for r=74 var slices = new List(); var cumulativeArc = 0.0; for (var i = 0; i < rawCounts.Length; i++) { if (rawCounts[i] <= 0) { // Arc length 0 with a full-circumference gap → invisible. // Avoids stroke-linecap="round" rendering a phantom dot. slices.Add(new DistributionSlice(GetDistributionLabel(i), 0, colors[i], $"0 {circumference:0.##}", 0)); continue; } var fraction = rawCounts[i] / (double)total; var arcLength = circumference * fraction; // dasharray: "{draw} {gap}" where gap = circumference - arcLength so the // pattern period equals exactly one full revolution. This prevents any // slice from bleeding into the next revolution of the ring. var dash = $"{arcLength:0.##} {(circumference - arcLength):0.##}"; // dashOffset = -cumulativeArc: // A negative offset shifts the start of the dash pattern forward along the // path by cumulativeArc pixels, so this slice begins exactly where the // previous slice ended. (The circle starts at 12 o'clock via rotate=-90.) var dashOffset = -cumulativeArc; slices.Add(new DistributionSlice(GetDistributionLabel(i), rawCounts[i], colors[i], dash, dashOffset)); cumulativeArc += arcLength; } return slices; } private static string GetDistributionLabel(int index) => index switch { 0 => "Completed", 1 => "In Progress", 2 => "To Do", _ => "Overdue" }; private List BuildDistributionLegend() { return DonutSlices.Select(slice => new DistributionLegendItem(slice.Label, slice.Count, slice.Color)).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 projectStarts = projectsList .Select(project => new { project.StartDate, Color = GetProjectColor(project.Id) }) .Where(item => item.StartDate.Date >= start.Date && item.StartDate.Date < start.AddDays(42)) .ToList(); var cells = new List(); for (var i = 0; i < 42; i++) { var date = start.AddDays(i); var markers = projectStarts .Where(item => item.StartDate.Date == date.Date) .Select(item => item.Color) .ToList(); cells.Add(new CalendarCell(date, date.Month == today.Month, date.Date == today.Date, markers)); } return cells; } private List BuildUpcomingProjectStarts() { return projectsList .Where(project => project.StartDate.Date >= DateTime.Today) .OrderBy(project => project.StartDate) .Select(project => new CalendarEventItem(project.Name, project.StartDate.ToString("MMM d"), GetProjectColor(project.Id))) .ToList(); } private List BuildTimelineMonths() { var featured = FeaturedProjects; if (featured.Count == 0) { return new List(); } var start = new DateTime(featured.Min(p => p.StartDate).Year, featured.Min(p => p.StartDate).Month, 1); var end = new DateTime(featured.Max(p => p.EndDate).Year, featured.Max(p => p.EndDate).Month, 1); var months = new List(); var current = start; while (current <= end) { months.Add(current); current = current.AddMonths(1); } return months; } private List BuildTimelineRows() { var months = TimelineMonths; if (months.Count == 0) { return new List(); } var first = months.First(); var n = months.Count; // total columns — each flex-1 month slot = 1/n of bar width return FeaturedProjects.Select(project => { var startIndex = ((project.StartDate.Year - first.Year) * 12) + project.StartDate.Month - first.Month; var endIndex = ((project.EndDate.Year - first.Year) * 12) + project.EndDate.Month - first.Month; startIndex = Math.Clamp(startIndex, 0, n - 1); endIndex = Math.Clamp(endIndex, startIndex, n - 1); // Use n as divisor so positions match the flex-1 month columns. var left = (startIndex / (double)n) * 100; var width = ((endIndex - startIndex + 1) / (double)n) * 100; width = Math.Max(2, width); var progress = GetProjectProgress(project.Id); return new TimelineRow( project.Name, $"{DisplayFormats.Date(project.StartDate)} – {DisplayFormats.Date(project.EndDate)}", GetProjectColor(project.Id), left, width, progress.CompletionPercentage); }).ToList(); } private string GetTimelineTodayMarkerLeft() { var months = TimelineMonths; if (months.Count == 0) return "0%"; var first = months.First(); var n = months.Count; var monthIndex = ((DateTime.Today.Year - first.Year) * 12) + DateTime.Today.Month - first.Month; // Clamp to visible range; if today is outside the timeline keep it at the edge. monthIndex = Math.Clamp(monthIndex, 0, n - 1); // Add fractional day-of-month so the line sits at the correct intra-month position. var dayFraction = (DateTime.Today.Day - 1.0) / DateTime.DaysInMonth(DateTime.Today.Year, DateTime.Today.Month); return $"{((monthIndex + dayFraction) / n * 100):0.##}%"; } private void InitializeTimelineRange() { if (TimelineProjects.Count == 0) { var currentMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1); timelineStartMonthValue = currentMonth.ToString("yyyy-MM"); timelineEndMonthValue = currentMonth.ToString("yyyy-MM"); return; } var start = new DateTime(TimelineProjects.Min(p => p.StartDate).Year, TimelineProjects.Min(p => p.StartDate).Month, 1); var end = new DateTime(TimelineProjects.Max(p => p.EndDate).Year, TimelineProjects.Max(p => p.EndDate).Month, 1); timelineStartMonthValue = start.ToString("yyyy-MM"); timelineEndMonthValue = end.ToString("yyyy-MM"); } private Task OnTimelineStartMonthChanged(ChangeEventArgs args) { timelineStartMonthValue = args.Value?.ToString() ?? timelineStartMonthValue; NormalizeTimelineRange(); _shouldRenderCharts = true; StateHasChanged(); return Task.CompletedTask; } private Task OnTimelineEndMonthChanged(ChangeEventArgs args) { timelineEndMonthValue = args.Value?.ToString() ?? timelineEndMonthValue; NormalizeTimelineRange(); _shouldRenderCharts = true; StateHasChanged(); return Task.CompletedTask; } private void NormalizeTimelineRange() { if (!TryParseMonthValue(timelineStartMonthValue, out var start) || !TryParseMonthValue(timelineEndMonthValue, out var end)) { return; } if (start > end) { (start, end) = (end, start); } timelineStartMonthValue = start.ToString("yyyy-MM"); timelineEndMonthValue = end.ToString("yyyy-MM"); } private bool TryGetTimelineRange(out (DateTime Start, DateTime End) range) { if (!TryParseMonthValue(timelineStartMonthValue, out var start) || !TryParseMonthValue(timelineEndMonthValue, out var end)) { range = default; return false; } if (start > end) { (start, end) = (end, start); } var startOfMonth = new DateTime(start.Year, start.Month, 1); var endOfMonth = new DateTime(end.Year, end.Month, DateTime.DaysInMonth(end.Year, end.Month)); range = (startOfMonth, endOfMonth); return true; } private static bool TryParseMonthValue(string? value, out DateTime month) { month = default; if (string.IsNullOrWhiteSpace(value)) { return false; } return DateTime.TryParseExact($"{value}-01", "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out month); } private static long ToUnixMilliseconds(DateTime value) { var utc = DateTime.SpecifyKind(value.Date, DateTimeKind.Utc); return new DateTimeOffset(utc).ToUnixTimeMilliseconds(); } private static int GetTimelineMonthSpan(DateTime start, DateTime end) { var span = ((end.Year - start.Year) * 12) + end.Month - start.Month + 1; return Math.Max(span, 1); } private ProjectProgressDto GetProjectProgress(long projectId) { return projectProgress.FirstOrDefault(item => item.ProjectId == projectId) ?? new ProjectProgressDto { ProjectId = projectId, ProjectName = string.Empty, TotalTasksCount = taskList.Count(task => task.ProjectId == projectId), CompletedTasksCount = taskList.Count(task => task.ProjectId == projectId && task.StatusId == AppTaskStatus.Done), CompletionPercentage = 0 }; } private string GetProjectBadgeLabel(ProjectDto project) { return project.StartDate.Date > DateTime.Today ? "Upcoming" : "Active"; } private string GetProjectBadgeClasses(ProjectDto project) { return project.StartDate.Date > DateTime.Today ? "rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold text-amber-700" : "rounded-full border border-violet-200 bg-violet-50 px-3 py-1 text-xs font-semibold text-violet-700"; } private string GetProjectColor(long projectId) { var palette = new[] { "#0f766e", "#38bdf8", "#10b981", "#b45309", "#64748b" }; var index = (int)(projectId % palette.Length); return palette[index]; } private string GetCalendarCellClasses(CalendarCell cell) { var classes = new List { "min-h-[62px] rounded-2xl border p-2" }; if (cell.IsToday) { classes.Add("border-violet-200 bg-violet-50"); } else if (cell.IsCurrentMonth) { classes.Add("border-slate-200 bg-white"); } else { classes.Add("border-slate-100 bg-slate-50/70"); } return string.Join(' ', classes); } private void SetHoveredActivity(int index) { hoveredActivityIndex = index; } private void ClearHoveredActivity() { hoveredActivityIndex = null; } private void SetHoveredSlice(int index) { hoveredSliceIndex = index; } private void ClearHoveredSlice() { hoveredSliceIndex = null; } private ActivityMonthStat? hoveredActivity => hoveredActivityIndex.HasValue && hoveredActivityIndex.Value < ActivityMonths.Count ? ActivityMonths[hoveredActivityIndex.Value] : null; private DistributionSlice? hoveredSlice => hoveredSliceIndex.HasValue && hoveredSliceIndex.Value < DonutSlices.Count ? DonutSlices[hoveredSliceIndex.Value] : null; private string HoveredActivityStyle { get { if (!hoveredActivityIndex.HasValue || hoveredActivityIndex.Value >= ActivityPoints.Count) { return string.Empty; } var point = ActivityPoints[hoveredActivityIndex.Value]; return $"left: {point.X / 760d * 100:0.##}%; top: {point.Y / 340d * 100:0.##}%; transform: translate(-50%, -110%);"; } } private string BuildDeltaText(int delta) { if (delta > 0) { return $"+{delta} this month"; } if (delta < 0) { return $"{delta} this month"; } return "0 this month"; } private string BuildCompletionDeltaText() { var current = GetCompletionRateForMonth(DateTime.Today); var previous = GetCompletionRateForMonth(DateTime.Today.AddMonths(-1)); var delta = current - previous; if (delta > 0) { return $"+{delta}% vs last mo"; } if (delta < 0) { return $"{delta}% vs last mo"; } return "0% vs last mo"; } private int GetUsersThisMonth() => usersList.Count(user => user.CreatedAt.Year == DateTime.Today.Year && user.CreatedAt.Month == DateTime.Today.Month); private int GetUsersPreviousMonth() { var previous = DateTime.Today.AddMonths(-1); return usersList.Count(user => user.CreatedAt.Year == previous.Year && user.CreatedAt.Month == previous.Month); } private int GetProjectsThisMonth() => projectsList.Count(project => project.CreatedAt.Year == DateTime.Today.Year && project.CreatedAt.Month == DateTime.Today.Month); private int GetProjectsPreviousMonth() { var previous = DateTime.Today.AddMonths(-1); return projectsList.Count(project => project.CreatedAt.Year == previous.Year && project.CreatedAt.Month == previous.Month); } private int GetActiveTasksThisMonth() => taskList.Count(task => task.CreatedAt.Year == DateTime.Today.Year && task.CreatedAt.Month == DateTime.Today.Month && task.StatusId != AppTaskStatus.Done); private int GetActiveTasksPreviousMonth() { var previous = DateTime.Today.AddMonths(-1); return taskList.Count(task => task.CreatedAt.Year == previous.Year && task.CreatedAt.Month == previous.Month && task.StatusId != AppTaskStatus.Done); } private int GetCompletionRateForMonth(DateTime date) { var monthTasks = taskList.Where(task => task.CreatedAt.Year == date.Year && task.CreatedAt.Month == date.Month).ToList(); if (monthTasks.Count == 0) { return 0; } return (int)Math.Round(monthTasks.Count(task => task.StatusId == AppTaskStatus.Done) / (double)monthTasks.Count * 100); } private string GetMonthLabel(DateTime date) => date.ToString("MMM"); private readonly record struct ActivityMonthStat(string Label, int Active, int Completed); private readonly record struct ActivityPoint(string Label, double X, double Y, int Active, int Completed); private readonly record struct DistributionSlice(string Label, int Count, string Color, string DashArray, double DashOffset); private readonly record struct DistributionLegendItem(string Label, int Count, string Color); private readonly record struct CalendarCell(DateTime Date, bool IsCurrentMonth, bool IsToday, List ProjectDots); private readonly record struct CalendarEventItem(string Name, string DateLabel, string Color); private readonly record struct TimelineRow(string Name, string DateLabel, string Color, double Left, double Width, double CompletionPercentage); }