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

@AppLocalization.PageTitle("taskReport", "Task Report")

@AppLocalization.PageDescription("taskReport", "Analyze task activity and export task reports.")

@if (filterPeriod == "custom") {
}
@if (filterProjectId > 0 || !string.IsNullOrWhiteSpace(projectSearchInput)) { }
@if (showProjectSuggestions && FilteredProjects.Any()) {
  • @foreach (var p in FilteredProjects.Take(10)) {
  • }
}
@if (isLoading) {
@AppLocalization.Text("common.loading", "Loading data...")
} else if (!string.IsNullOrWhiteSpace(errorMessage)) {

@AppLocalization.Text("report.couldNotLoad", "Task report could not load.")

@errorMessage

} else {
@TaskMetricCard(AppLocalization.Text("report.total", "Total"), FilteredList.Count().ToString(), "file-text", "violet") @TaskMetricCard(AppLocalization.Text("status.done", "Completed"), FilteredList.Count(t => t.StatusId == AppTaskStatus.Done).ToString(), "check-circle-2", "emerald") @TaskMetricCard(AppLocalization.StatusLabel(AppTaskStatus.InProgress), FilteredList.Count(t => t.StatusId == AppTaskStatus.InProgress).ToString(), "timer", "blue") @TaskMetricCard(AppLocalization.Text("status.overdue", "Overdue"), FilteredList.Count(t => t.DueDate.Date < DateTime.Today && t.StatusId != AppTaskStatus.Done).ToString(), "alarm-clock", "rose")
@if (isChartView) {
} else {
@if (PagedList.Any()) { @foreach (var entry in PagedList.Select((t, index) => (t, index))) { var t = entry.t; @if (expandedTasks.Contains(t.TaskId)) { } } } else { }
@AppLocalization.Text("common.index", "No.") @AppLocalization.Text("common.task", "Task") @AppLocalization.Text("common.project", "Project") @AppLocalization.Text("common.status", "Status") @AppLocalization.Text("common.priority", "Priority") @AppLocalization.Text("report.health", "Health") @AppLocalization.Text("common.issue", "Issues")
@((currentPage - 1) * pageSize + entry.index + 1) @t.ProjectName @AppLocalization.StatusLabel(t.StatusId) @AppLocalization.PriorityLabel(t.PriorityId) @GetHealthLabel(t.TaskHealth) @t.IssueSummary
@AppLocalization.Text("common.assignee", "Assignee"): @(t.AssignedToUser ?? AppLocalization.Text("common.unassigned", "Unassigned"))
@AppLocalization.Text("common.due", "Due"): @DisplayFormats.Date(t.DueDate)
@AppLocalization.Text("report.openIssues", "Open issues"): @t.OpenIssues
@AppLocalization.Text("report.lastActivity", "Last activity"): @DisplayFormats.Date(t.LastActivity)

@AppLocalization.Text("report.noTasksFound", "No Tasks Found")

@AppLocalization.Text("report.noTasksFoundDesc", "There are no tasks matching your filters. Try adjusting your search query or filters.")

} }
@code { private bool isLoading = true; private bool isChartView = false; private List taskReports = new(); private HashSet expandedTasks = new(); private PagedResult? taskReportPage; private List projects = new(); private string? errorMessage; // Filters private string filterPeriod = "all"; private DateTime? filterStartDate; private DateTime? filterEndDate; private string? filterStatus = "uncompleted"; private int filterProjectId; private string projectSearchInput = string.Empty; private string projectSearchQuery = string.Empty; private bool showProjectSuggestions; private bool filterAssignToMe = false; private bool filterAssignToMyTeam = false; // Search and Pagination private string searchInput = ""; private string searchQuery = ""; private int currentPage = 1; private int pageSize = 10; private async Task ApplyAllFilters() { ApplyPeriodPreset(); searchQuery = searchInput; projectSearchQuery = projectSearchInput; var matched = projects.FirstOrDefault(p => p.Name.Equals(projectSearchInput, StringComparison.OrdinalIgnoreCase)); if (matched != null) filterProjectId = (int)matched.Id; else if (string.IsNullOrWhiteSpace(projectSearchInput)) filterProjectId = 0; currentPage = 1; await LoadTaskReport(); } private async Task ResetAllFilters() { searchInput = ""; searchQuery = ""; filterPeriod = "all"; filterStartDate = null; filterEndDate = null; filterStatus = "uncompleted"; filterProjectId = 0; projectSearchInput = string.Empty; projectSearchQuery = string.Empty; showProjectSuggestions = false; filterAssignToMe = false; filterAssignToMyTeam = false; currentPage = 1; await LoadTaskReport(); } private async Task HandlePageChanged(int page) { currentPage = page; await LoadTaskReport(); } private async Task HandlePageSizeChanged(int size) { pageSize = size; currentPage = 1; await LoadTaskReport(); } private IEnumerable FilteredList => taskReports; private int TotalPages => taskReportPage?.TotalPages ?? 0; private IEnumerable PagedList => taskReportPage?.Items ?? Enumerable.Empty(); private IEnumerable TaskInsights => new[] { $"{FilteredList.Count(t => t.TaskHealth == "Critical")} {AppLocalization.Text("report.criticalTasksNeedAttention", "critical tasks need attention.")}", $"{FilteredList.Sum(t => t.OpenIssues)} {AppLocalization.Text("report.openIssuesAttached", "open issues are attached to reported tasks.")}", $"{FilteredList.Count(t => t.OverdueIssues > 0)} {AppLocalization.Text("report.tasksHaveOverdueIssues", "tasks have overdue issues.")}" }; private int FilteredTaskCount => FilteredList.Count(); private int CompletedTaskCount => FilteredList.Count(t => t.StatusId == AppTaskStatus.Done); private int OpenTaskCount => FilteredTaskCount - CompletedTaskCount; private int HighPriorityCount => FilteredList.Count(t => t.PriorityId == TaskPriority.High); private double CompletionRate => FilteredTaskCount > 0 ? CompletedTaskCount / (double)FilteredTaskCount * 100 : 0; private string TaskStatusInsight => OpenTaskCount > CompletedTaskCount ? $"{OpenTaskCount} {AppLocalization.Text("report.tasksStillMoving", "tasks are still moving through execution.")}" : AppLocalization.Text("report.completedWorkLeading", "Completed work is leading the current filtered set."); private string PriorityInsight => HighPriorityCount > 0 ? $"{HighPriorityCount} {AppLocalization.Text("report.highPriorityVisible", "high-priority tasks should stay visible during planning review.")}" : AppLocalization.Text("report.noHighPriority", "No high-priority tasks in the current filter."); private string CompletionInsight => CompletionRate >= 70 ? AppLocalization.Text("report.completionHealthy", "Completion is healthy for the current filter.") : AppLocalization.Text("report.completionImprove", "Completion has room to improve; inspect open issues and overdue work."); private List TaskStatusItems => new() { new(AppLocalization.StatusLabel(AppTaskStatus.Todo), FilteredList.Count(t => t.StatusId == AppTaskStatus.Todo), "#94a3b8"), new(AppLocalization.StatusLabel(AppTaskStatus.InProgress), FilteredList.Count(t => t.StatusId == AppTaskStatus.InProgress), "#3b82f6"), new(AppLocalization.StatusLabel(AppTaskStatus.Done), CompletedTaskCount, "#10b981"), new(AppLocalization.Text("status.overdue", "Overdue"), FilteredList.Count(t => t.DueDate.Date < DateTime.Today && t.StatusId != AppTaskStatus.Done), "#ef4444") }; private List PrioritySegments => new() { new(AppLocalization.PriorityLabel(TaskPriority.High), HighPriorityCount, "#ef4444"), new(AppLocalization.PriorityLabel(TaskPriority.Medium), FilteredList.Count(t => t.PriorityId == TaskPriority.Medium), "#f59e0b"), new(AppLocalization.PriorityLabel(TaskPriority.Low), FilteredList.Count(t => t.PriorityId == TaskPriority.Low), "#3b82f6") }; private List CompletionSparkline => FilteredList .GroupBy(t => new DateTime(t.CreatedAt.Year, t.CreatedAt.Month, 1)) .OrderBy(g => g.Key) .TakeLast(8) .Select(g => new SparklinePoint(g.Key.ToString("MMM"), g.Count(t => t.StatusId == AppTaskStatus.Done))) .DefaultIfEmpty(new SparklinePoint(DateTime.Today.ToString("MMM"), CompletedTaskCount)) .ToList(); private List ProjectRiskItems => FilteredList .GroupBy(t => t.ProjectName) .Select(g => { var score = Math.Min(100, (g.Count() * 8) + (g.Sum(t => t.OpenIssues) * 12) + (g.Sum(t => t.OverdueIssues) * 18) + (g.Count(t => t.TaskHealth == "Critical") * 20)); var overdue = g.Sum(t => t.OverdueIssues); var tone = overdue > 0 ? "rose" : score >= 45 ? "amber" : "blue"; return new TopRiskItem( g.Key, $"{g.Count()} {AppLocalization.Text("common.task", "tasks")} · {g.Sum(t => t.OpenIssues)} {AppLocalization.Text("common.issue", "open issues")}", score, overdue > 0 ? $"{overdue} {AppLocalization.Text("status.overdue", "overdue")}" : $"{score:0} {AppLocalization.Text("report.risk", "risk")}", tone, $"{g.Count(t => t.PriorityId == TaskPriority.High)} {AppLocalization.Text("report.highPriorityTasks", "high-priority tasks")}"); }) .OrderByDescending(item => item.Score) .Take(5) .ToList(); private List TaskIssueRiskItems => FilteredList .OrderByDescending(t => (t.OpenIssues * 2) + (t.OverdueIssues * 3)) .Take(5) .Select(t => { var score = Math.Min(100, (t.OpenIssues * 20) + (t.OverdueIssues * 30)); var tone = t.OverdueIssues > 0 ? "rose" : t.OpenIssues > 0 ? "amber" : "emerald"; return new TopRiskItem( t.Title, t.ProjectName, score, $"{t.OpenIssues + t.OverdueIssues} {AppLocalization.Text("common.issue", "issues")}", tone, $"{AppLocalization.StatusLabel(t.StatusId)} · {AppLocalization.PriorityLabel(t.PriorityId)}"); }) .ToList(); private List TasksByProjectItems => FilteredList .GroupBy(t => t.ProjectName) .OrderByDescending(g => g.Count()) .Take(5) .Select(g => new ProgressListChartItem(g.Key, g.Count(), g.Count().ToString(), "#7c3aed")) .ToList(); private double TasksByProjectMax => Math.Max(1, TasksByProjectItems.Select(item => item.Value).DefaultIfEmpty(0).Max()); private List TasksWithMostIssuesItems => FilteredList .OrderByDescending(t => t.OpenIssues + t.OverdueIssues) .Take(5) .Select(t => new ProgressListChartItem(t.Title, t.OpenIssues + t.OverdueIssues, (t.OpenIssues + t.OverdueIssues).ToString(), "#7c3aed")) .ToList(); private double TasksWithMostIssuesMax => Math.Max(1, TasksWithMostIssuesItems.Select(item => item.Value).DefaultIfEmpty(0).Max()); protected override async Task OnInitializedAsync() { var client = ApiClient.CreateClient(); try { projects = await client.GetFromJsonAsync>("Project") ?? new(); } catch { } await LoadTaskReport(); } protected override async Task OnAfterRenderAsync(bool firstRender) { await JS.InvokeVoidAsync("initIcons"); } private IEnumerable FilteredProjects => string.IsNullOrWhiteSpace(projectSearchQuery) ? projects : projects.Where(p => p.Name.Contains(projectSearchQuery.Trim(), StringComparison.OrdinalIgnoreCase)); private void SetView(bool chartView) { isChartView = chartView; } private void ToggleExpanded(long taskId) { if (!expandedTasks.Remove(taskId)) { expandedTasks.Add(taskId); } } private void ShowProjectSuggestions() { projectSearchQuery = projectSearchInput; showProjectSuggestions = true; } private async Task HideProjectSuggestionsDelayed() { await Task.Delay(150); showProjectSuggestions = false; await InvokeAsync(StateHasChanged); } private void SelectProject(ProjectDto project) { filterProjectId = (int)project.Id; projectSearchInput = project.Name; projectSearchQuery = project.Name; showProjectSuggestions = false; } private void ClearProjectFilter() { filterProjectId = 0; projectSearchInput = string.Empty; projectSearchQuery = string.Empty; showProjectSuggestions = false; } 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 LoadTaskReport() { isLoading = true; projectSearchQuery = projectSearchInput; StateHasChanged(); var client = ApiClient.CreateClient(); try { errorMessage = null; var query = BuildTaskReportQuery(); var fullTask = client.GetFromJsonAsync>>(query); var pagedTask = client.GetFromJsonAsync>(BuildTaskReportQuery(includePaging: true)); await System.Threading.Tasks.Task.WhenAll(fullTask, pagedTask); if (fullTask.Result?.IsSuccess == true && fullTask.Result.Value != null) { taskReports = fullTask.Result.Value; } else if (fullTask.Result?.IsSuccess == false) { errorMessage = fullTask.Result.ErrorMessage ?? AppLocalization.Text("report.loadError", "The server returned an error while loading task report data."); } if (pagedTask.Result != null) { taskReportPage = pagedTask.Result; currentPage = taskReportPage.Page; pageSize = taskReportPage.PageSize; } } catch (Exception ex) { Console.WriteLine($"Task report error: {ex.Message}"); errorMessage = ex.Message; taskReports = new(); taskReportPage = null; } finally { isLoading = false; StateHasChanged(); } } private string BuildTaskReportQuery(bool includePaging = false) { var parts = new List(); if (filterStartDate.HasValue) parts.Add($"startDate={filterStartDate:yyyy-MM-dd}"); if (filterEndDate.HasValue) parts.Add($"endDate={filterEndDate:yyyy-MM-dd}"); if (!string.IsNullOrEmpty(filterStatus)) parts.Add($"status={Uri.EscapeDataString(filterStatus)}"); if (filterProjectId > 0) parts.Add($"projectId={filterProjectId}"); if (filterAssignToMe) parts.Add("assignedToMe=true"); if (filterAssignToMyTeam) parts.Add("assignedToMyTeam=true"); if (includePaging) { parts.Add($"page={currentPage}"); parts.Add($"limit={pageSize}"); } return "Report/tasks" + (parts.Count > 0 ? "?" + string.Join("&", parts) : ""); } private async Task DownloadExcel() { var rows = FilteredList.Select(t => new object?[] { t.Title, t.ProjectName, AppLocalization.StatusLabel(t.StatusId), AppLocalization.PriorityLabel(t.PriorityId), t.AssignedToUser ?? AppLocalization.Text("common.na", "Unassigned"), t.DueDate }); var workbookBytes = SpreadsheetReportBuilder.BuildTableWorkbook( AppLocalization.PageTitle("taskReport", "Task Report"), AppLocalization.PageTitle("taskReport", "Task Report"), new[] { AppLocalization.Text("common.task", "Task"), AppLocalization.Text("common.project", "Project"), AppLocalization.Text("common.status", "Status"), AppLocalization.Text("common.priority", "Priority"), AppLocalization.Text("common.assignee", "Assigned To"), AppLocalization.Text("common.dueDate", "Due Date") }, rows, new[] { $"{AppLocalization.Text("common.search", "Search")}: {(string.IsNullOrWhiteSpace(searchQuery) ? AppLocalization.Text("common.all", "All tasks") : searchQuery)}", $"{AppLocalization.Text("report.periodFilter", "Period")}: {GetPeriodLabel(filterPeriod)}", $"{AppLocalization.Text("common.status", "Status")}: {filterStatus switch { "0" => AppLocalization.Text("common.all", "All"), "1" => AppLocalization.StatusLabel(AppTaskStatus.Todo), "2" => AppLocalization.StatusLabel(AppTaskStatus.InProgress), "3" => AppLocalization.StatusLabel(AppTaskStatus.Done), "overdue" => AppLocalization.Text("status.overdue", "Overdue"), "uncompleted" => AppLocalization.Text("status.uncompleted", "Uncompleted"), _ => filterStatus }}", $"{AppLocalization.Text("common.project", "Project")}: {(filterProjectId > 0 ? projects.FirstOrDefault(p => p.Id == filterProjectId)?.Name ?? AppLocalization.Text("report.selectedProject", "Selected project") : AppLocalization.Text("common.all", "All projects"))}", $"{AppLocalization.Text("common.date", "Generated")}: {DisplayFormats.Date(DateTime.Today)}" }); await JS.InvokeVoidAsync( "downloadFileFromBase64", $"TaskReport_{DateTime.Today:yyyyMMdd}.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", Convert.ToBase64String(workbookBytes)); } private async Task DownloadPdf() { var summaryLines = new List { $"{AppLocalization.Text("common.search", "Search")}: {(string.IsNullOrWhiteSpace(searchQuery) ? AppLocalization.Text("common.all", "All tasks") : searchQuery)}", $"{AppLocalization.Text("report.periodFilter", "Period")}: {GetPeriodLabel(filterPeriod)}", $"{AppLocalization.Text("common.status", "Status")}: {filterStatus switch { "0" => AppLocalization.Text("common.all", "All"), "1" => AppLocalization.StatusLabel(AppTaskStatus.Todo), "2" => AppLocalization.StatusLabel(AppTaskStatus.InProgress), "3" => AppLocalization.StatusLabel(AppTaskStatus.Done), "overdue" => AppLocalization.Text("status.overdue", "Overdue"), "uncompleted" => AppLocalization.Text("status.uncompleted", "Uncompleted"), _ => filterStatus }}", $"{AppLocalization.Text("common.project", "Project")}: {(filterProjectId > 0 ? projects.FirstOrDefault(p => p.Id == filterProjectId)?.Name ?? AppLocalization.Text("report.selectedProject", "Selected project") : AppLocalization.Text("common.all", "All projects"))}", $"{AppLocalization.Text("common.date", "Generated")}: {DisplayFormats.Date(DateTime.Today)}" }; var rows = FilteredList.Select(t => new[] { t.Title, t.ProjectName, AppLocalization.StatusLabel(t.StatusId), AppLocalization.PriorityLabel(t.PriorityId), t.AssignedToUser ?? AppLocalization.Text("common.na", "Unassigned"), DisplayFormats.Date(t.DueDate) }); var pdfBytes = SimplePdfReportBuilder.BuildTableReport( AppLocalization.PageTitle("taskReport", "Task Report"), summaryLines, new[] { AppLocalization.Text("common.task", "Task"), AppLocalization.Text("common.project", "Project"), AppLocalization.Text("common.status", "Status"), AppLocalization.Text("common.priority", "Priority"), AppLocalization.Text("common.assignee", "Assigned To"), AppLocalization.Text("common.dueDate", "Due Date") }, rows); await JS.InvokeVoidAsync( "downloadFileFromBase64", $"TaskReport_{DateTime.Today:yyyyMMdd}.pdf", "application/pdf", Convert.ToBase64String(pdfBytes)); } private string GetStatusBadge(AppTaskStatus statusId) => statusId switch { AppTaskStatus.Todo => "bg-slate-100 text-slate-700", AppTaskStatus.InProgress => "bg-blue-100 text-blue-700", AppTaskStatus.Done => "bg-emerald-100 text-emerald-700", _ => "bg-slate-100 text-slate-600" }; private string GetPriorityBadge(TaskPriority priorityId) => priorityId switch { TaskPriority.Low => "bg-slate-100 text-slate-600", TaskPriority.Medium => "bg-amber-100 text-amber-700", TaskPriority.High => "bg-red-100 text-red-700", _ => "bg-slate-100 text-slate-600" }; private static string GetHealthLabel(string health) => health switch { "Critical" => AppLocalization.Text("report.healthCritical", "Critical"), "At Risk" => AppLocalization.Text("report.healthAtRisk", "At Risk"), _ => AppLocalization.Text("report.healthGood", "Healthy") }; private static string GetHealthBadge(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 string GetPeriodLabel(string period) => period switch { "daily" => AppLocalization.Text("report.daily", "Daily (Today)"), "monthly" => AppLocalization.Text("report.monthly", "Monthly (This Month)"), "yearly" => AppLocalization.Text("report.yearly", "Yearly (This Year)"), "custom" => AppLocalization.Text("report.customRange", "Custom Range"), _ => AppLocalization.Text("common.all", "All Time") }; private RenderFragment TaskMetricCard(string label, string value, string icon, string color) => builder => { builder.OpenElement(0, "div"); builder.AddAttribute(1, "class", $"rounded-xl border border-{color}-100 bg-{color}-50 p-5 shadow-sm"); builder.AddMarkupContent(2, $"

{label}

{value}

"); builder.CloseElement(); }; }