@page "/reports/overdue" @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using TaskTrackingSystem.Shared @using TaskTrackingSystem.Shared.Models.Task @using TaskTrackingSystem.Shared.Models.User @using TaskTrackingSystem.Shared.Models.Project @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.overdueTasksPageTitle", "Overdue & Delayed Tasks Report")

@AppLocalization.Text("report.overdueTasksHeading", "Overdue & Delayed Tasks")

@AppLocalization.Text("report.overdueTasksDesc", "Track tasks that have passed their due date or are at risk of delay.")

@if (!isLoading) {

@AppLocalization.Text("dashboard.overdueTasks", "Overdue Tasks")

@AppLocalization.Number(overdueCount)

@AppLocalization.Text("report.highPriorityOverdue", "High Priority Overdue")

@AppLocalization.Number(criticalCount)

@AppLocalization.Text("report.dueWithin3Days", "Due Within 3 Days")

@AppLocalization.Number(dueSoonCount)

@AppLocalization.Text("report.avgDaysOverdue", "Avg Days Overdue")

@AppLocalization.Number(avgDaysOverdue)

}
@if (isLoading) { } else { @if (isChartView) {
} else {

@AppLocalization.Text("report.showingDelayedTasks", "Showing") @AppLocalization.Number(overdueReportPage?.TotalCount ?? 0) @AppLocalization.Text("report.delayedOrOverdueTasks", "delayed / overdue task(s)")

@if (overdueReportTasks.Any()) { @foreach (var entry in overdueReportTasks.Select((task, index) => (task, index))) { var task = entry.task; } } else { }
@AppLocalization.Text("common.index", "No.") @AppLocalization.Text("common.task", "Task") @AppLocalization.Text("common.project", "Project") @AppLocalization.Text("common.assignee", "Assignee") @AppLocalization.Text("common.dueDate", "Due Date") @AppLocalization.Text("report.daysOverdue", "Days Overdue") @AppLocalization.Text("common.issue", "Issues") @AppLocalization.Text("report.delayEscalation", "Delay / Escalation")
@AppLocalization.Number((currentPage - 1) * pageSize + entry.index + 1)

@Content.GetText(task.Title, task.TitleMy)

@if (!string.IsNullOrEmpty(Content.GetText(task.Description, task.DescriptionMy))) {

@Content.GetText(task.Description, task.DescriptionMy)

}
@Content.GetText(task.ProjectName, task.ProjectNameMy) @if (!string.IsNullOrEmpty(task.AssignedTo)) {
@(task.AssignedTo.Length > 0 ? task.AssignedTo[0].ToString().ToUpper() : "") @task.AssignedTo
} else { @AppLocalization.Text("common.unassigned", "Unassigned") }
@DisplayFormats.Date(task.DueDate) @if (task.DaysOverdue > 0) { @AppLocalization.Number(task.DaysOverdue) @AppLocalization.Text("report.days", "days") } else { var daysDue = Math.Abs(task.DaysOverdue); @AppLocalization.Text("report.dueIn", "Due in") @AppLocalization.Number(daysDue) @AppLocalization.Text("report.days", "days") } @AppLocalization.Number(task.OverdueIssues) @AppLocalization.Text("common.overdue", "overdue")
@GetDelayReasonLabel(task)
@AppLocalization.Text("report.blockedBy", "Blocked by: ")@(string.IsNullOrWhiteSpace(Content.GetText(task.BlockedBy, task.BlockedByMy)) ? AppLocalization.Text("common.none", "None") : Content.GetText(task.BlockedBy, task.BlockedByMy)) · @AppLocalization.Text("report.escalation", "Escalation: ")@GetEscalationLabel(task.EscalationLevel)
} }
@code { private bool isLoading = true; private bool isChartView = false; private List allOverdue = new(); private List overdueReportTasks = new(); private PagedResult? overdueReportPage; // Filters private string searchInput = ""; private string appliedSearch = ""; private int filterPriority = 0; private int appliedPriority = 0; private string filterDelayType = "all"; private string appliedDelayType = "all"; private bool filterAssignToMe = false; private bool filterAssignToMyTeam = false; // Pagination private int currentPage = 1; private int pageSize = 10; // Summary stats private int overdueCount = 0; private int criticalCount = 0; private int dueSoonCount = 0; private int avgDaysOverdue = 0; private bool canUpdateTask = false; private int TotalPages => overdueReportPage?.TotalPages ?? 0; private double HighPriorityOverduePercent => overdueCount > 0 ? criticalCount / (double)overdueCount * 100 : 0; private string AgingInsight => avgDaysOverdue > 7 ? $"Average delay is {avgDaysOverdue} days; focus on older buckets first." : "Most delayed work is still in a manageable window."; private List AgingBuckets => new() { new(AppLocalization.Text("report.dueSoon", "Due soon"), allOverdue.Count(t => t.DaysOverdue <= 0), "#f59e0b", AppLocalization.Text("report.notOverdueYet", "not overdue yet")), new(AppLocalization.Text("report.1to3Days", "1-3d"), allOverdue.Count(t => t.DaysOverdue >= 1 && t.DaysOverdue <= 3), "#3b82f6", AppLocalization.Text("report.earlyDelay", "early delay")), new(AppLocalization.Text("report.4to7Days", "4-7d"), allOverdue.Count(t => t.DaysOverdue >= 4 && t.DaysOverdue <= 7), "#f59e0b", AppLocalization.Text("report.needsAction", "needs action")), new(AppLocalization.Text("report.8PlusDays", "8d+"), allOverdue.Count(t => t.DaysOverdue >= 8), "#ef4444", AppLocalization.Text("report.highRisk", "high risk")) }; private List DelayProjectItems => allOverdue .GroupBy(t => t.ProjectName) .Select(g => { var overdue = g.Count(t => t.DaysOverdue > 0); var high = g.Count(t => t.PriorityName == "High"); var score = Math.Min(100, (overdue * 18) + (high * 15) + g.Sum(t => Math.Max(0, t.DaysOverdue))); return new TopRiskItem(g.Key, $"{AppLocalization.Number(overdue)} {AppLocalization.Text("status.overdue", "overdue")} · {AppLocalization.Number(high)} {AppLocalization.Text("priority.high", "high priority")}", score, $"{AppLocalization.Number(g.Count())} {AppLocalization.Text("report.tasks", "tasks")}", score >= 70 ? "rose" : "amber", $"{AppLocalization.Number(g.Sum(t => t.OverdueIssues))} {AppLocalization.Text("report.overdueIssuesSegment", "overdue issues")}"); }) .OrderByDescending(item => item.Score) .Take(5) .ToList(); private List DelayAssigneeItems => allOverdue .GroupBy(t => string.IsNullOrWhiteSpace(t.AssignedTo) ? AppLocalization.Text("common.unassigned", "Unassigned") : t.AssignedTo!) .Select(g => { var overdue = g.Count(t => t.DaysOverdue > 0); var score = Math.Min(100, (overdue * 20) + g.Sum(t => Math.Max(0, t.DaysOverdue))); return new TopRiskItem(g.Key, $"{AppLocalization.Number(overdue)} {AppLocalization.Text("report.overdueTasks", "overdue tasks")}", score, $"{AppLocalization.Number(score)} {AppLocalization.Text("report.risk", "risk")}", score >= 70 ? "rose" : "amber", AppLocalization.Text("report.capacitySupportNeeded", "Capacity support may be needed")); }) .OrderByDescending(item => item.Score) .Take(5) .ToList(); private List OverdueRiskMatrix => allOverdue .GroupBy(t => new { Severity = GetPrioritySeverity(t.PriorityName), Urgency = GetOverdueUrgency(t.DaysOverdue), t.ProjectName }) .Select(g => new RiskMatrixPoint(g.Key.ProjectName, g.Key.Severity, g.Key.Urgency, g.Count(), g.Key.Severity * g.Key.Urgency >= 9 ? "#ef4444" : "#f59e0b", $"{AppLocalization.Number(g.Count())} {AppLocalization.Text("report.delayedTasksCount", "delayed task(s)")}")) .ToList(); protected override async Task OnInitializedAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); canUpdateTask = await MenuAuthorization.HasAccessCodeAsync(authState.User, "Tasks_Update"); await LoadOverdueReport(); isLoading = false; } protected override async Task OnAfterRenderAsync(bool firstRender) { await JS.InvokeVoidAsync("initIcons"); } private async Task LoadOverdueReport() { var client = ApiClient.CreateClient(); try { var parts = new List(); if (!string.IsNullOrWhiteSpace(appliedSearch)) parts.Add($"search={Uri.EscapeDataString(appliedSearch)}"); if (appliedPriority > 0) parts.Add($"priorityId={appliedPriority}"); if (!string.IsNullOrWhiteSpace(appliedDelayType)) parts.Add($"delayType={Uri.EscapeDataString(appliedDelayType)}"); if (filterAssignToMe) parts.Add("assignedToMe=true"); if (filterAssignToMyTeam) parts.Add("assignedToMyTeam=true"); // 1. Fetch full matching list once to compute summary cards and for exports var fullUrl = "Report/overdue-critical" + (parts.Count > 0 ? "?" + string.Join("&", parts) : ""); var fullResult = await client.GetFromJsonAsync>>(fullUrl, Serialization.CaseInsensitive); if (fullResult != null && fullResult.IsSuccess && fullResult.Value != null) { allOverdue = fullResult.Value; var today = DateTime.Today; var now = DateTime.UtcNow; overdueCount = allOverdue.Count(t => t.DueDate < now); criticalCount = allOverdue.Count(t => t.PriorityName == "High" && t.DaysOverdue > 0); dueSoonCount = allOverdue.Count(t => t.DueDate >= now && t.DueDate <= now.AddDays(3)); avgDaysOverdue = overdueCount > 0 ? (int)allOverdue.Where(t => t.DueDate < now).Average(t => t.DaysOverdue) : 0; } ApplyLocalPagination(); } catch (Exception ex) { Console.WriteLine($"Overdue report loading error: {ex.Message}"); } } private void ApplyLocalPagination() { var normalizedPageSize = Math.Clamp(pageSize <= 0 ? 10 : pageSize, 1, 100); var totalCount = allOverdue.Count; var totalPages = totalCount == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)normalizedPageSize); var normalizedPage = Math.Max(currentPage, 1); if (totalPages > 0 && normalizedPage > totalPages) { normalizedPage = totalPages; } overdueReportTasks = totalPages == 0 ? new List() : allOverdue .Skip((normalizedPage - 1) * normalizedPageSize) .Take(normalizedPageSize) .ToList(); overdueReportPage = new PagedResult { Items = overdueReportTasks, TotalCount = totalCount, Page = normalizedPage, PageSize = normalizedPageSize, TotalPages = totalPages }; currentPage = normalizedPage; pageSize = normalizedPageSize; } private async Task ApplyFilters() { appliedSearch = searchInput; appliedPriority = filterPriority; appliedDelayType = filterDelayType; currentPage = 1; await LoadOverdueReport(); } private async Task ResetFilters() { searchInput = ""; appliedSearch = ""; filterPriority = 0; appliedPriority = 0; filterDelayType = "all"; appliedDelayType = "all"; filterAssignToMe = false; filterAssignToMyTeam = false; currentPage = 1; await LoadOverdueReport(); } private void SetView(bool chartView) { isChartView = chartView; } private async Task HandlePageChanged(int page) { currentPage = page; await LoadOverdueReport(); } private async Task HandlePageSizeChanged(int size) { pageSize = size; currentPage = 1; await LoadOverdueReport(); } private async Task DownloadExcel() { var client = ApiClient.CreateClient(); try { var parts = new List(); if (!string.IsNullOrWhiteSpace(appliedSearch)) parts.Add($"search={Uri.EscapeDataString(appliedSearch)}"); if (appliedPriority > 0) parts.Add($"priorityId={appliedPriority}"); var url = "Report/overdue-critical/excel" + (parts.Count > 0 ? "?" + string.Join("&", parts) : ""); var response = await client.GetAsync(url); if (response.IsSuccessStatusCode) { var bytes = await response.Content.ReadAsByteArrayAsync(); await JS.InvokeVoidAsync( "downloadFileFromBase64", $"OverdueTasksReport_{DateTime.Today:yyyyMMdd}.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", Convert.ToBase64String(bytes)); } } catch (Exception ex) { Console.WriteLine($"Excel download error: {ex.Message}"); } } private async Task DownloadPdf() { var summaryLines = new List { $"Search: {(string.IsNullOrWhiteSpace(appliedSearch) ? "All tasks" : appliedSearch)}", $"Priority: {appliedPriority switch { 0 => "All", 1 => "Low", 2 => "Medium", 3 => "High", _ => appliedPriority.ToString() }}", $"Delay Type: {appliedDelayType switch { "all" => "All delayed tasks", "overdue" => "Overdue", "soon" => "Due within 3 days", _ => appliedDelayType }}", $"Generated: {DisplayFormats.Date(DateTime.Today)}" }; var rows = allOverdue.Select(task => { return new[] { Content.GetText(task.Title, task.TitleMy), Content.GetText(task.ProjectName, task.ProjectNameMy), string.IsNullOrWhiteSpace(Content.GetText(task.AssignedTo, task.AssignedToMy)) ? "Unassigned" : Content.GetText(task.AssignedTo, task.AssignedToMy), DisplayFormats.Date(task.DueDate), task.DaysOverdue.ToString(), task.PriorityName, task.StatusName }; }); var pdfBytes = SimplePdfReportBuilder.BuildTableReport( "Overdue Tasks Report", summaryLines, new[] { "Task", "Project", "Assignee", "Due Date", "Days Overdue", "Priority", "Status" }, rows); await JS.InvokeVoidAsync( "downloadFileFromBase64", $"OverdueTasksReport_{DateTime.Today:yyyyMMdd}.pdf", "application/pdf", Convert.ToBase64String(pdfBytes)); } private static string GetEscalationLabel(int level) => level switch { 1 => "Team Lead", 2 => "PM", 3 => "Manager", _ => "None" }; private string GetDelayReasonLabel(OverdueCriticalTaskDto task) { var reason = Content.GetText(task.DelayReason, task.DelayReasonMy); if (string.IsNullOrWhiteSpace(reason)) { return AppLocalization.Text("report.noReasonRecorded", "No reason recorded"); } return reason.Trim() switch { "Past due date" => AppLocalization.Text("report.pastDueDate", "Past due date"), "High priority due soon" => AppLocalization.Text("report.highPriorityDueSoon", "High priority due soon"), _ => reason }; } private static int GetPrioritySeverity(string priorityName) => priorityName switch { "High" => 3, "Medium" => 2, _ => 1 }; private static int GetOverdueUrgency(int daysOverdue) => daysOverdue >= 8 ? 3 : daysOverdue >= 4 ? 2 : 1; }