Spaces:
Runtime error
Runtime error
task-tracking-system / TaskTrackingSystem.WebApp /Components /Pages /Features /Reports /OverdueTasksReport.razor
Download TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/OverdueTasksReport.razor from Yuruyuruu/task-tracking-system: direct link, hf CLI and curl.
- Browser
- Download file 34.3 kB
-
https://huggingface.co/spaces/Yuruyuruu/task-tracking-system/resolve/main/TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/OverdueTasksReport.razor
- Command line
-
hf download hf://spaces/Yuruyuruu/task-tracking-system/TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/OverdueTasksReport.razor
-
curl -L -o OverdueTasksReport.razor https://huggingface.co/spaces/Yuruyuruu/task-tracking-system/resolve/main/TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/OverdueTasksReport.razor
34.3 kB
| @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 | |
| <PageTitle>@AppLocalization.Text("report.overdueTasksPageTitle", "Overdue & Delayed Tasks Report")</PageTitle> | |
| <div class="space-y-6"> | |
| <!-- Page Header --> | |
| <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4"> | |
| <div> | |
| <h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.Text("report.overdueTasksHeading", "Overdue & Delayed Tasks")</h2> | |
| <p class="text-slate-500 mt-1">@AppLocalization.Text("report.overdueTasksDesc", "Track tasks that have passed their due date or are at risk of delay.")</p> | |
| </div> | |
| <div class="flex flex-wrap items-center gap-3"> | |
| <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm"> | |
| <button @onclick="() => SetView(false)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(!isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors"> | |
| <i data-lucide="list" class="h-3.5 w-3.5 mr-1"></i> @AppLocalization.Text("common.listView", "List View") | |
| </button> | |
| <button @onclick="() => SetView(true)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors"> | |
| <i data-lucide="bar-chart-3" class="h-3.5 w-3.5 mr-1"></i> @AppLocalization.Text("common.chartView", "Chart View") | |
| </button> | |
| </div> | |
| <button @onclick="DownloadExcel" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors shadow-sm"> | |
| <i data-lucide="download" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("common.exportExcel", "Export Excel") | |
| </button> | |
| <button @onclick="DownloadPdf" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors shadow-sm"> | |
| <i data-lucide="file-text" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("common.exportPdf", "Export PDF") | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Summary Cards --> | |
| @if (!isLoading) | |
| { | |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> | |
| <div class="rounded-xl border border-rose-100 bg-rose-50 p-5 shadow-sm"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-xs font-semibold uppercase tracking-wider text-rose-500">@AppLocalization.Text("dashboard.overdueTasks", "Overdue Tasks")</p> | |
| <p class="mt-1 text-3xl font-bold text-rose-700">@AppLocalization.Number(overdueCount)</p> | |
| </div> | |
| <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-rose-100"> | |
| <i data-lucide="alert-circle" class="h-6 w-6 text-rose-600"></i> | |
| </span> | |
| </div> | |
| </div> | |
| <div class="rounded-xl border border-amber-100 bg-amber-50 p-5 shadow-sm"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-xs font-semibold uppercase tracking-wider text-amber-500">@AppLocalization.Text("report.highPriorityOverdue", "High Priority Overdue")</p> | |
| <p class="mt-1 text-3xl font-bold text-amber-700">@AppLocalization.Number(criticalCount)</p> | |
| </div> | |
| <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-amber-100"> | |
| <i data-lucide="flame" class="h-6 w-6 text-amber-600"></i> | |
| </span> | |
| </div> | |
| </div> | |
| <div class="rounded-xl border border-orange-100 bg-orange-50 p-5 shadow-sm"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-xs font-semibold uppercase tracking-wider text-orange-500">@AppLocalization.Text("report.dueWithin3Days", "Due Within 3 Days")</p> | |
| <p class="mt-1 text-3xl font-bold text-orange-700">@AppLocalization.Number(dueSoonCount)</p> | |
| </div> | |
| <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-orange-100"> | |
| <i data-lucide="clock" class="h-6 w-6 text-orange-600"></i> | |
| </span> | |
| </div> | |
| </div> | |
| <div class="rounded-xl border border-violet-100 bg-violet-50 p-5 shadow-sm"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-xs font-semibold uppercase tracking-wider text-violet-500">@AppLocalization.Text("report.avgDaysOverdue", "Avg Days Overdue")</p> | |
| <p class="mt-1 text-3xl font-bold text-violet-700">@AppLocalization.Number(avgDaysOverdue)</p> | |
| </div> | |
| <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-violet-100"> | |
| <i data-lucide="calendar-x" class="h-6 w-6 text-violet-600"></i> | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| } | |
| <!-- Filters --> | |
| <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm"> | |
| <div class="flex flex-wrap items-end gap-4"> | |
| <div class="flex-1 min-w-[200px]"> | |
| <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.searchTasks", "Search Tasks")</label> | |
| <input @bind="searchInput" @bind:event="oninput" type="text" placeholder="@AppLocalization.Text("common.searchByTaskAssigneeProject", "Search by title, assignee, or project...")" | |
| class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" /> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.priority", "Priority")</label> | |
| <select @bind="filterPriority" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all"> | |
| <option value="0">@AppLocalization.Text("common.allPriorities", "All Priorities")</option> | |
| <option value="1">@AppLocalization.Text("priority.low", "Low")</option> | |
| <option value="2">@AppLocalization.Text("priority.medium", "Medium")</option> | |
| <option value="3">@AppLocalization.Text("priority.high", "High")</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("report.delayType", "Delay Type")</label> | |
| <select @bind="filterDelayType" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all"> | |
| <option value="all">@AppLocalization.Text("report.allDelayed", "All Delayed")</option> | |
| <option value="overdue">@AppLocalization.Text("report.overduePastDue", "Overdue (Past Due)")</option> | |
| <option value="soon">@AppLocalization.Text("report.dueWithin3Days", "Due Within 3 Days")</option> | |
| </select> | |
| </div> | |
| <button @onclick="ApplyFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-5 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm"> | |
| <i data-lucide="search" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("action.search", "Search") | |
| </button> | |
| <button @onclick="ResetFilters" title="@AppLocalization.Text("action.reset", "Reset")" aria-label="@AppLocalization.Text("action.reset", "Reset")" class="inline-flex items-center justify-center rounded-lg border border-slate-200 bg-white p-2 text-slate-600 hover:bg-slate-50 transition-colors shadow-sm"> | |
| <i data-lucide="rotate-ccw" class="h-4 w-4"></i> | |
| </button> | |
| <div class="flex flex-wrap items-center gap-6 w-full border-t border-slate-100 pt-3 mt-1"> | |
| <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none"> | |
| <input type="checkbox" @bind="filterAssignToMe" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" /> | |
| <span>@AppLocalization.Text("report.assignedToMe", "Assigned to me")</span> | |
| </label> | |
| <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none"> | |
| <input type="checkbox" @bind="filterAssignToMyTeam" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" /> | |
| <span>@AppLocalization.Text("report.assignedToMyTeam", "Assigned to my team")</span> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| @if (isLoading) | |
| { | |
| <LoadingSpinner Message="@AppLocalization.Text("report.loadingOverdueTasks", "Loading overdue tasks...")" /> | |
| } | |
| else | |
| { | |
| @if (isChartView) | |
| { | |
| <div class="grid grid-cols-1 gap-5 xl:grid-cols-12"> | |
| <div class="xl:col-span-7"> | |
| <ReportChartCard Title="@AppLocalization.Text("report.agingBuckets", "Aging buckets")" Subtitle="@AppLocalization.Text("report.agingBucketsDesc", "How long delayed work has been waiting.")" Insight="@AgingInsight"> | |
| <AgingBucketChart Buckets="AgingBuckets" /> | |
| </ReportChartCard> | |
| </div> | |
| <div class="xl:col-span-5"> | |
| <RadialMetricCard Title="@AppLocalization.Text("report.highPriorityOverdue", "High-priority overdue")" Subtitle="@AppLocalization.Text("report.highPriorityOverdueDesc", "Critical work that needs immediate support.")" Percent="@HighPriorityOverduePercent" Value="@AppLocalization.Number(criticalCount)" ValueLabel="@AppLocalization.Text("priority.high", "high priority")" CenterLabel="@AppLocalization.Text("report.critical", "critical")" Color="#ef4444" Insight="@AppLocalization.Text("report.overdueInsight", "Phrase these as capacity risks so teams can unblock work, not assign blame.")" /> | |
| </div> | |
| <div class="xl:col-span-6"> | |
| <ReportChartCard Title="@AppLocalization.Text("report.delaysByProject", "Delays by project")" Subtitle="@AppLocalization.Text("report.delaysByProjectDesc", "Project areas where delay concentration is highest.")" Insight="@AppLocalization.Text("report.delaysByProjectInsight", "Use this to rebalance planning pressure across projects.")"> | |
| <TopRiskList Items="DelayProjectItems" EmptyText="@AppLocalization.Text("report.noProjectDelay", "No project delay concentration found.")" /> | |
| </ReportChartCard> | |
| </div> | |
| <div class="xl:col-span-6"> | |
| <ReportChartCard Title="@AppLocalization.Text("report.delaysByAssignee", "Delays by assignee")" Subtitle="@AppLocalization.Text("report.delaysByAssigneeDesc", "People who may need support or scope adjustment.")" Insight="@AppLocalization.Text("report.delaysByAssigneeInsight", "Support signal only: high counts should trigger help, not blame.")"> | |
| <TopRiskList Items="DelayAssigneeItems" EmptyText="@AppLocalization.Text("report.noAssigneeDelay", "No assignee delay concentration found.")" /> | |
| </ReportChartCard> | |
| </div> | |
| <div class="xl:col-span-12"> | |
| <RiskMatrixCard Title="@AppLocalization.Text("report.severityVsDaysOverdue", "Severity vs days overdue")" Subtitle="@AppLocalization.Text("report.severityVsDaysOverdueDesc", "High priority and older delays sit in the upper-right risk zone.")" Points="OverdueRiskMatrix" Insight="@AppLocalization.Text("report.severityMatrixInsight", "The matrix combines priority severity with age of delay to show triage order.")" /> | |
| </div> | |
| </div> | |
| } | |
| else | |
| { | |
| <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden"> | |
| <div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between"> | |
| <h3 class="text-sm font-semibold text-slate-700"> | |
| @AppLocalization.Text("report.showingDelayedTasks", "Showing") <span class="text-rose-600 font-bold">@AppLocalization.Number(overdueReportPage?.TotalCount ?? 0)</span> @AppLocalization.Text("report.delayedOrOverdueTasks", "delayed / overdue task(s)") | |
| </h3> | |
| </div> | |
| <table class="w-full"> | |
| <thead> | |
| <tr class="border-b border-slate-100 bg-slate-50/60"> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.index", "No.")</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.task", "Task")</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.project", "Project")</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.assignee", "Assignee")</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.dueDate", "Due Date")</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("report.daysOverdue", "Days Overdue")</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.issue", "Issues")</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("report.delayEscalation", "Delay / Escalation")</th> | |
| </tr> | |
| </thead> | |
| <tbody class="divide-y divide-slate-100"> | |
| @if (overdueReportTasks.Any()) | |
| { | |
| @foreach (var entry in overdueReportTasks.Select((task, index) => (task, index))) | |
| { | |
| var task = entry.task; | |
| <tr class="hover:bg-slate-50/50 transition-colors"> | |
| <td class="px-6 py-4 text-sm font-semibold text-slate-500 align-top">@AppLocalization.Number((currentPage - 1) * pageSize + entry.index + 1)</td> | |
| <td class="px-6 py-4"> | |
| <div> | |
| <p class="text-sm font-semibold text-slate-900">@Content.GetText(task.Title, task.TitleMy)</p> | |
| @if (!string.IsNullOrEmpty(Content.GetText(task.Description, task.DescriptionMy))) | |
| { | |
| <p class="text-xs text-slate-400 mt-0.5 truncate max-w-[200px]">@Content.GetText(task.Description, task.DescriptionMy)</p> | |
| } | |
| </div> | |
| </td> | |
| <td class="px-6 py-4"> | |
| <span class="inline-flex items-center gap-1 text-sm text-slate-600"> | |
| <i data-lucide="folder" class="h-3.5 w-3.5 text-violet-500"></i> | |
| @Content.GetText(task.ProjectName, task.ProjectNameMy) | |
| </span> | |
| </td> | |
| <td class="px-6 py-4"> | |
| @if (!string.IsNullOrEmpty(task.AssignedTo)) | |
| { | |
| <div class="flex items-center gap-2"> | |
| <span class="flex h-7 w-7 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-xs font-bold"> | |
| @(task.AssignedTo.Length > 0 ? task.AssignedTo[0].ToString().ToUpper() : "") | |
| </span> | |
| <span class="text-sm text-slate-700">@task.AssignedTo</span> | |
| </div> | |
| } | |
| else | |
| { | |
| <span class="text-xs text-slate-400">@AppLocalization.Text("common.unassigned", "Unassigned")</span> | |
| } | |
| </td> | |
| <td class="px-6 py-4"> | |
| <span class="text-sm font-medium @(task.DaysOverdue > 0 ? "text-rose-600" : "text-amber-600")"> | |
| @DisplayFormats.Date(task.DueDate) | |
| </span> | |
| </td> | |
| <td class="px-6 py-4"> | |
| @if (task.DaysOverdue > 0) | |
| { | |
| <span class="inline-flex items-center gap-1 rounded-full bg-rose-50 px-2.5 py-1 text-xs font-semibold text-rose-700"> | |
| <i data-lucide="alert-triangle" class="h-3 w-3"></i> | |
| @AppLocalization.Number(task.DaysOverdue) @AppLocalization.Text("report.days", "days") | |
| </span> | |
| } | |
| else | |
| { | |
| var daysDue = Math.Abs(task.DaysOverdue); | |
| <span class="inline-flex items-center gap-1 rounded-full bg-amber-50 px-2.5 py-1 text-xs font-semibold text-amber-700"> | |
| <i data-lucide="clock" class="h-3 w-3"></i> | |
| @AppLocalization.Text("report.dueIn", "Due in") @AppLocalization.Number(daysDue) @AppLocalization.Text("report.days", "days") | |
| </span> | |
| } | |
| </td> | |
| <td class="px-6 py-4 text-sm text-slate-700"> | |
| <span class="font-semibold text-rose-600">@AppLocalization.Number(task.OverdueIssues)</span> @AppLocalization.Text("common.overdue", "overdue") | |
| </td> | |
| <td class="px-6 py-4"> | |
| <div class="text-sm font-medium text-slate-800">@GetDelayReasonLabel(task)</div> | |
| <div class="mt-1 text-xs text-slate-500">@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)</div> | |
| </td> | |
| </tr> | |
| } | |
| } | |
| else | |
| { | |
| <EmptyState ColSpan="8" Icon="check-circle" Title="@AppLocalization.Text("report.noOverdueTasks", "No overdue tasks")" Subtitle="@AppLocalization.Text("report.noOverdueTasksDesc", "Great! No tasks are overdue or at risk based on the current filters.")" /> | |
| } | |
| </tbody> | |
| </table> | |
| <!-- Pagination --> | |
| <Pagination CurrentPage="currentPage" | |
| TotalPages="TotalPages" | |
| PageSize="pageSize" | |
| TotalCount="@(overdueReportPage?.TotalCount ?? 0)" | |
| OnPageChanged="HandlePageChanged" | |
| OnPageSizeChanged="HandlePageSizeChanged" /> | |
| </div> | |
| } | |
| } | |
| </div> | |
| @code { | |
| private bool isLoading = true; | |
| private bool isChartView = false; | |
| private List<OverdueCriticalTaskDto> allOverdue = new(); | |
| private List<OverdueCriticalTaskDto> overdueReportTasks = new(); | |
| private PagedResult<OverdueCriticalTaskDto>? 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<ReportChartItem> 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<TopRiskItem> 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<TopRiskItem> 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<RiskMatrixPoint> 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<string>(); | |
| 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<Result<List<OverdueCriticalTaskDto>>>(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<OverdueCriticalTaskDto>() | |
| : allOverdue | |
| .Skip((normalizedPage - 1) * normalizedPageSize) | |
| .Take(normalizedPageSize) | |
| .ToList(); | |
| overdueReportPage = new PagedResult<OverdueCriticalTaskDto> | |
| { | |
| 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<string>(); | |
| 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<string> | |
| { | |
| $"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; | |
| } | |