User
add: language switch
8c88a89
Raw
History Blame Contribute Delete
31 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">Overdue Tasks</p>
<p class="mt-1 text-3xl font-bold text-rose-700">@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">@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">@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">@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" 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">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">All Priorities</option>
<option value="1">Low</option>
<option value="2">Medium</option>
<option value="3">High</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-slate-500 mb-1.5">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" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-50 transition-colors shadow-sm">
<i data-lucide="rotate-ccw" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("action.reset", "Reset")
</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="@criticalCount.ToString()" 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">@(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">@((currentPage - 1) * pageSize + entry.index + 1)</td>
<td class="px-6 py-4">
<div>
<p class="text-sm font-semibold text-slate-900">@task.Title</p>
@if (!string.IsNullOrEmpty(task.Description))
{
<p class="text-xs text-slate-400 mt-0.5 truncate max-w-[200px]">@task.Description</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>
@task.ProjectName
</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">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>
@task.DaysOverdue day@(task.DaysOverdue != 1 ? "s" : "")
</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>
Due in @daysDue day@(daysDue != 1 ? "s" : "")
</span>
}
</td>
<td class="px-6 py-4 text-sm text-slate-700">
<span class="font-semibold text-rose-600">@task.OverdueIssues</span> overdue
</td>
<td class="px-6 py-4">
<div class="text-sm font-medium text-slate-800">@(string.IsNullOrWhiteSpace(task.DelayReason) ? "No reason recorded" : task.DelayReason)</div>
<div class="mt-1 text-xs text-slate-500">Blocked by: @(string.IsNullOrWhiteSpace(task.BlockedBy) ? "None" : task.BlockedBy) · Escalation: @GetEscalationLabel(task.EscalationLevel)</div>
</td>
</tr>
}
}
else
{
<EmptyState ColSpan="8" Icon="check-circle" Title="No overdue tasks" Subtitle="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("Due soon", allOverdue.Count(t => t.DaysOverdue <= 0), "#f59e0b", "not overdue yet"),
new("1-3d", allOverdue.Count(t => t.DaysOverdue >= 1 && t.DaysOverdue <= 3), "#3b82f6", "early delay"),
new("4-7d", allOverdue.Count(t => t.DaysOverdue >= 4 && t.DaysOverdue <= 7), "#f59e0b", "needs action"),
new("8d+", allOverdue.Count(t => t.DaysOverdue >= 8), "#ef4444", "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, $"{overdue} overdue · {high} high priority", score, $"{g.Count()} tasks", score >= 70 ? "rose" : "amber", $"{g.Sum(t => t.OverdueIssues)} overdue issues");
})
.OrderByDescending(item => item.Score)
.Take(5)
.ToList();
private List<TopRiskItem> DelayAssigneeItems => allOverdue
.GroupBy(t => string.IsNullOrWhiteSpace(t.AssignedTo) ? "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, $"{overdue} overdue tasks", score, $"{score:0} risk", score >= 70 ? "rose" : "amber", "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", $"{g.Count()} 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;
}
// 2. Fetch paged list for the table view
var pagedParts = new List<string>(parts);
pagedParts.Add($"page={currentPage}");
pagedParts.Add($"limit={pageSize}");
var pagedUrl = "Report/overdue-critical" + (pagedParts.Count > 0 ? "?" + string.Join("&", pagedParts) : "");
var pagedResult = await client.GetFromJsonAsync<PagedResult<OverdueCriticalTaskDto>>(pagedUrl, Serialization.CaseInsensitive);
if (pagedResult != null)
{
overdueReportPage = pagedResult;
overdueReportTasks = pagedResult.Items.ToList();
currentPage = pagedResult.Page;
pageSize = pagedResult.PageSize;
}
}
catch (Exception ex)
{
Console.WriteLine($"Overdue report loading error: {ex.Message}");
}
}
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[]
{
task.Title,
task.ProjectName,
task.AssignedTo ?? "Unassigned",
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 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;
}