@page "/reports/projects" @using Microsoft.AspNetCore.Authorization @using TaskTrackingSystem.Shared @using TaskTrackingSystem.Shared.Models.Project @using TaskTrackingSystem.Shared.Models.Task @using TaskTrackingSystem.Shared.Models.Report @using System.Text @rendermode @(new InteractiveServerRenderMode(prerender: false)) @attribute [Authorize] @inject ApiClientService ApiClient @inject IJSRuntime JS @inject AuthenticationStateProvider AuthStateProvider @inject MenuAuthorizationService MenuAuthorization @AppLocalization.Text("report.projectProgressTitle", "Project Progress Report")

@AppLocalization.Text("report.projectProgressHeading", "Project Progress")

@AppLocalization.Text("report.projectProgressDesc", "View project health using both planning tasks and daily execution issues.")

@if (filterPeriod == "custom") {
}
@if (isLoading) {
@AppLocalization.Text("common.loading", "Loading data...")
} else {

Total Active Projects

@totalActiveProjects

Ahead of Schedule

@aheadOfScheduleCount

At Risk

@atRiskCount

@if (isChartView) {
@if (!ProjectQuadrantPoints.Any()) {
No project risk data available.
} else {
Risk
Review now
Low completion · high risk
Watch closely
High completion · high risk
Recover plan
Low completion · low risk
On track
High completion · low risk
@foreach (var point in ProjectQuadrantPoints) {
}
0% complete Completion 100%
Color Healthy Watch At risk Critical
}
} else {
@if (pagedProjects.Any()) { @foreach (var entry in pagedProjects.Select((proj, index) => (proj, index))) { var proj = entry.proj; } } else { }
No. Project Name Start Date End Date Open Issues Blocked / Overdue Health Last Activity
@((currentPage - 1) * pageSize + entry.index + 1) @proj.ProjectName @DisplayFormats.Date(proj.StartDate) @DisplayFormats.Date(proj.EndDate) @proj.OpenIssues @proj.BlockedIssues / @proj.OverdueIssues @proj.ProjectHealth @DisplayFormats.Date(proj.LastActivity)

No project data found matching criteria.

} }
@code { private bool isLoading = true; private bool isChartView = false; private List allProjects = new(); private List pagedProjects = new(); private PagedResult? reportPage; // Filters private string filterPeriod = "all"; private DateTime? filterStartDate; private DateTime? filterEndDate; private string filterStatus = "0"; private bool filterAssignToMe = false; private bool filterAssignToMyTeam = false; private List ProjectHealthMatrix => allProjects .Select(project => new RiskMatrixPoint( project.ProjectName, GetProjectSeverity(project), GetProjectUrgency(project), 1, project.ProjectHealth == "Critical" ? "#ef4444" : project.IsAtRisk ? "#f59e0b" : "#3b82f6", $"{project.Progress:0}% complete, {project.BlockedIssues} blocked, {project.OverdueIssues} overdue")) .ToList(); private List ProjectQuadrantPoints => allProjects .OrderByDescending(project => GetProjectRiskScore(project)) .Take(10) .Select(project => { var risk = GetProjectRiskScore(project); var left = Math.Clamp(project.Progress, 2, 98); var top = Math.Clamp(100 - risk, 2, 98); var color = project.ProjectHealth == "Critical" ? "#ef4444" : project.IsAtRisk ? "#f59e0b" : risk > 35 ? "#3b82f6" : "#10b981"; var tooltip = $"{project.ProjectName}\n{project.Progress:0}% complete\nRisk score: {risk:0}\n{project.BlockedIssues} blocked, {project.OverdueIssues} overdue, {project.OpenIssues} open"; return new ProjectQuadrantPoint(project.ProjectName, left, top, color, tooltip); }) .ToList(); private List AtRiskProjectItems => allProjects .Select(project => { var score = GetProjectRiskScore(project); var tone = project.ProjectHealth == "Critical" || project.BlockedIssues > 0 ? "rose" : project.IsAtRisk ? "amber" : "blue"; return new TopRiskItem(project.ProjectName, $"{project.Progress:0}% complete · {project.OpenIssues} open issues", score, project.ProjectHealth, tone, $"{project.OverdueIssues} overdue · {project.BlockedIssues} blocked"); }) .OrderByDescending(item => item.Score) .Take(5) .ToList(); private List ProjectTimelineItems => allProjects .OrderBy(project => project.StartDate) .Take(8) .Select(project => new TimelineChartItem(project.ProjectName, project.StartDate, project.EndDate, project.Progress, project.IsAtRisk ? "#f59e0b" : "#3b82f6")) .ToList(); private List ProjectIssueSegments => new() { new("Open issues", allProjects.Sum(project => project.OpenIssues), "#3b82f6"), new("Overdue issues", allProjects.Sum(project => project.OverdueIssues), "#ef4444"), new("Blocked issues", allProjects.Sum(project => project.BlockedIssues), "#f59e0b") }; // Search and Pagination private string searchInput = ""; private string searchQuery = ""; private int currentPage = 1; private int pageSize = 10; // Card stats private int totalActiveProjects = 0; private int aheadOfScheduleCount = 0; private int atRiskCount = 0; private int TotalPages => reportPage?.TotalPages ?? 0; private int TotalCount => reportPage?.TotalCount ?? 0; protected override async Task OnInitializedAsync() { await LoadProjectReportData(); isLoading = false; } protected override async Task OnAfterRenderAsync(bool firstRender) { await JS.InvokeVoidAsync("initIcons"); } private void SetView(bool chartView) { isChartView = chartView; } private async Task ApplyAllFilters() { ApplyPeriodPreset(); searchQuery = searchInput; currentPage = 1; await LoadProjectReportData(); } private async Task ResetAllFilters() { searchInput = ""; searchQuery = ""; filterPeriod = "all"; filterStartDate = null; filterEndDate = null; filterStatus = "0"; filterAssignToMe = false; filterAssignToMyTeam = false; currentPage = 1; await LoadProjectReportData(); } private async Task HandlePageChanged(int page) { currentPage = page; await LoadProjectReportData(); } private async Task HandlePageSizeChanged(int size) { pageSize = size; currentPage = 1; await LoadProjectReportData(); } private void ApplyPeriodPreset() { var today = DateTime.Today; if (filterPeriod == "daily") { filterStartDate = today; filterEndDate = today; } else if (filterPeriod == "monthly") { filterStartDate = new DateTime(today.Year, today.Month, 1); filterEndDate = new DateTime(today.Year, today.Month, DateTime.DaysInMonth(today.Year, today.Month)); } else if (filterPeriod == "yearly") { filterStartDate = new DateTime(today.Year, 1, 1); filterEndDate = new DateTime(today.Year, 12, 31); } else { filterStartDate = null; filterEndDate = null; } } private async Task LoadProjectReportData() { var client = ApiClient.CreateClient(); try { var parts = new List(); if (!string.IsNullOrWhiteSpace(searchQuery)) parts.Add($"search={Uri.EscapeDataString(searchQuery)}"); if (filterStartDate.HasValue) parts.Add($"startDate={filterStartDate:yyyy-MM-dd}"); if (filterEndDate.HasValue) parts.Add($"endDate={filterEndDate:yyyy-MM-dd}"); if (!string.IsNullOrWhiteSpace(filterStatus) && filterStatus != "0") parts.Add($"status={Uri.EscapeDataString(filterStatus)}"); if (filterAssignToMe) parts.Add("assignedToMe=true"); if (filterAssignToMyTeam) parts.Add("assignedToMyTeam=true"); // 1. Fetch full matching list for chart view, cards summary, and exports var fullUrl = "Report/project-progress" + (parts.Count > 0 ? "?" + string.Join("&", parts) : ""); var fullResult = await client.GetFromJsonAsync>>(fullUrl, Serialization.CaseInsensitive); if (fullResult != null && fullResult.IsSuccess && fullResult.Value != null) { allProjects = fullResult.Value; totalActiveProjects = allProjects.Count; aheadOfScheduleCount = allProjects.Count(m => m.IsAhead); atRiskCount = allProjects.Count(m => m.IsAtRisk); } // 2. Fetch paged list for table pagination var pagedParts = new List(parts); pagedParts.Add($"page={currentPage}"); pagedParts.Add($"limit={pageSize}"); var pagedUrl = "Report/project-progress" + (pagedParts.Count > 0 ? "?" + string.Join("&", pagedParts) : ""); var pagedResult = await client.GetFromJsonAsync>(pagedUrl, Serialization.CaseInsensitive); if (pagedResult != null) { reportPage = pagedResult; pagedProjects = pagedResult.Items.ToList(); currentPage = pagedResult.Page; pageSize = pagedResult.PageSize; } } catch (Exception ex) { Console.WriteLine($"Error loading project progress report: {ex.Message}"); } } private async Task DownloadExcel() { var rows = allProjects.Select(proj => new object?[] { proj.ProjectName, proj.StartDate, proj.EndDate, proj.OpenIssues, proj.OverdueIssues, proj.BlockedIssues, proj.ProjectHealth, proj.ActualHours }); var workbookBytes = SpreadsheetReportBuilder.BuildTableWorkbook( "Project Progress", "Project Progress Report", new[] { "Project", "Start Date", "End Date", "Open Issues", "Overdue Issues", "Blocked Issues", "Health", "Actual Hours" }, rows, new[] { $"Search: {(string.IsNullOrWhiteSpace(searchQuery) ? "All projects" : searchQuery)}", $"Period: {filterPeriod}", $"Status: {filterStatus switch { "0" => "All", "1" => "To Do", "2" => "In Progress", "3" => "Done", "overdue" => "Overdue", _ => filterStatus }}", $"Generated: {DisplayFormats.Date(DateTime.Today)}" }); await JS.InvokeVoidAsync( "downloadFileFromBase64", $"ProjectProgressReport_{DateTime.Today:yyyyMMdd}.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", Convert.ToBase64String(workbookBytes)); } private async Task DownloadPdf() { var summaryLines = new List { $"Search: {(string.IsNullOrWhiteSpace(searchQuery) ? "All projects" : searchQuery)}", $"Period: {filterPeriod}", $"Status: {filterStatus switch { "0" => "All", "1" => "To Do", "2" => "In Progress", "3" => "Done", "overdue" => "Overdue", _ => filterStatus }}", $"Generated: {DisplayFormats.Date(DateTime.Today)}" }; var rows = allProjects.Select(proj => new[] { proj.ProjectName, DisplayFormats.Date(proj.StartDate), DisplayFormats.Date(proj.EndDate), proj.OpenIssues.ToString(), proj.OverdueIssues.ToString(), proj.BlockedIssues.ToString(), proj.ProjectHealth, proj.ActualHours.ToString("N1") }); var pdfBytes = SimplePdfReportBuilder.BuildTableReport( "Project Progress Report", summaryLines, new[] { "Project", "Start Date", "End Date", "Open", "Overdue", "Blocked", "Health", "Hours" }, rows); await JS.InvokeVoidAsync( "downloadFileFromBase64", $"ProjectProgressReport_{DateTime.Today:yyyyMMdd}.pdf", "application/pdf", Convert.ToBase64String(pdfBytes)); } private static string GetProjectHealthBadge(string health) => health switch { "Critical" => "bg-rose-50 text-rose-700", "At Risk" => "bg-amber-50 text-amber-700", _ => "bg-emerald-50 text-emerald-700" }; private static double GetProjectRiskScore(ProjectProgressReportDto project) { var issueRisk = (project.OpenIssues * 8) + (project.OverdueIssues * 16) + (project.BlockedIssues * 22); var progressRisk = Math.Max(0, 65 - project.Progress) * 0.45; var healthRisk = project.ProjectHealth == "Critical" ? 25 : project.IsAtRisk ? 15 : 0; return Math.Min(100, issueRisk + progressRisk + healthRisk); } private static int GetProjectSeverity(ProjectProgressReportDto project) { if (project.ProjectHealth == "Critical" || project.BlockedIssues > 0) { return 3; } return project.IsAtRisk || project.OverdueIssues > 0 ? 2 : 1; } private static int GetProjectUrgency(ProjectProgressReportDto project) { if (project.Progress < 35 || project.OverdueIssues > 2) { return 3; } return project.Progress < 70 || project.OpenIssues > 0 ? 2 : 1; } private sealed record ProjectQuadrantPoint(string Label, double Left, double Top, string Color, string Tooltip); }