@page "/reports/employees" @using Microsoft.AspNetCore.Authorization @using TaskTrackingSystem.Shared @using TaskTrackingSystem.Shared.Models.User @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 @AppLocalization.Text("report.employeeWorkloadTitle", "Employee Workload & Performance")

@AppLocalization.Text("report.employeeWorkloadHeading", "Employee Workload & Performance")

@AppLocalization.Text("report.employeeWorkloadDesc", "Monitor issue workload, execution health, and actual effort.")

@if (filterPeriod == "custom") {
}
@if (isLoading) {
@AppLocalization.Text("common.loading", "Loading data...")
} else { @if (isChartView) {
} else {
@if (pagedEmployees.Any()) { @foreach (var entry in pagedEmployees.Select((emp, index) => (emp, index))) { var emp = entry.emp; } } else { }
No. Employee Assigned Issues Open / Overdue Actual Hours Workload Trend
@((currentPage - 1) * pageSize + entry.index + 1)
@(emp.FullName.Length > 0 ? emp.FullName[0].ToString().ToUpper() : "U")

@emp.FullName

@@@emp.Username

@emp.AssignedIssues @emp.OpenIssues / @emp.OverdueIssues @emp.ActualHours.ToString("N1") hrs @emp.WorkloadLevel @emp.Trend

No employee data found matching criteria.

} }
@code { private bool isLoading = true; private bool isChartView = false; private List allEmployees = new(); private List pagedEmployees = 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 EmployeeHeatmapRows => allEmployees.Take(6).Select(emp => emp.FullName).ToList(); private List EmployeeHeatmapColumns => new() { "Assigned", "Open", "Overdue", "Hours" }; private int CompletedIssueTotal => allEmployees.Sum(emp => emp.CompletedCount); private double AverageCompletionRate => allEmployees.Any() ? allEmployees.Average(emp => emp.CompletionRate) : 0; private List EmployeeHeatmapCells => allEmployees.Take(6) .SelectMany(emp => new[] { new HeatmapCell(emp.FullName, "Assigned", Math.Min(100, emp.AssignedCount * 8), $"{emp.AssignedCount} assigned tasks"), new HeatmapCell(emp.FullName, "Open", Math.Min(100, emp.OpenIssues * 12), $"{emp.OpenIssues} open issues"), new HeatmapCell(emp.FullName, "Overdue", Math.Min(100, emp.OverdueIssues * 20), $"{emp.OverdueIssues} overdue issues"), new HeatmapCell(emp.FullName, "Hours", Math.Min(100, (double)emp.ActualHours * 5), $"{emp.ActualHours:N1} actual hours") }) .ToList(); private List EmployeeCapacityRiskItems => allEmployees .Select(emp => { var score = Math.Min(100, (emp.OpenIssues * 14) + (emp.OverdueIssues * 24) + (emp.AssignedCount * 4) + (double)emp.ActualHours); var tone = score >= 70 ? "rose" : score >= 45 ? "amber" : "blue"; return new TopRiskItem(emp.FullName, $"{emp.WorkloadLevel} · {emp.ActualHours:N1}h tracked", score, "needs support", tone, $"{emp.OpenIssues} open · {emp.OverdueIssues} overdue"); }) .OrderByDescending(item => item.Score) .Take(5) .ToList(); private List EmployeeCompletionSegments => new() { new("Completed tasks", allEmployees.Sum(emp => emp.CompletedCount), "#10b981"), new("Assigned tasks", allEmployees.Sum(emp => Math.Max(0, emp.AssignedCount - emp.CompletedCount)), "#3b82f6"), new("Open issues", allEmployees.Sum(emp => emp.OpenIssues), "#f59e0b") }; private List EmployeeOverdueItems => allEmployees .Where(emp => emp.OverdueIssues > 0) .OrderByDescending(emp => emp.OverdueIssues) .Take(5) .Select(emp => new TopRiskItem(emp.FullName, $"{emp.OpenIssues} open issues", Math.Min(100, emp.OverdueIssues * 25), $"{emp.OverdueIssues} overdue", "rose", "Capacity risk")) .ToList(); private List EmployeeCompletionSparkline => allEmployees .OrderByDescending(emp => emp.AssignedCount) .Take(8) .Select(emp => new SparklinePoint(BuildShortName(emp.FullName), emp.CompletionRate)) .DefaultIfEmpty(new SparklinePoint("No data", 0)) .ToList(); // Search and Pagination private string searchInput = ""; private string searchQuery = ""; private int currentPage = 1; private int pageSize = 10; private int TotalPages => reportPage?.TotalPages ?? 0; private int TotalCount => reportPage?.TotalCount ?? 0; protected override async Task OnInitializedAsync() { await LoadEmployeeReportData(); 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 LoadEmployeeReportData(); } private async Task ResetAllFilters() { searchInput = ""; searchQuery = ""; filterPeriod = "all"; filterStartDate = null; filterEndDate = null; filterStatus = "0"; filterAssignToMe = false; filterAssignToMyTeam = false; currentPage = 1; await LoadEmployeeReportData(); } private async Task HandlePageChanged(int page) { currentPage = page; await LoadEmployeeReportData(); } private async Task HandlePageSizeChanged(int size) { pageSize = size; currentPage = 1; await LoadEmployeeReportData(); } 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 LoadEmployeeReportData() { 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 and exports var fullUrl = "Report/employee-productivity" + (parts.Count > 0 ? "?" + string.Join("&", parts) : ""); var fullResult = await client.GetFromJsonAsync>>(fullUrl, Serialization.CaseInsensitive); if (fullResult != null && fullResult.IsSuccess && fullResult.Value != null) { allEmployees = fullResult.Value; } // 2. Fetch paged list for table pagination var pagedParts = new List(parts); pagedParts.Add($"page={currentPage}"); pagedParts.Add($"limit={pageSize}"); var pagedUrl = "Report/employee-productivity" + (pagedParts.Count > 0 ? "?" + string.Join("&", pagedParts) : ""); var pagedResult = await client.GetFromJsonAsync>(pagedUrl, Serialization.CaseInsensitive); if (pagedResult != null) { reportPage = pagedResult; pagedEmployees = pagedResult.Items.ToList(); currentPage = pagedResult.Page; pageSize = pagedResult.PageSize; } } catch (Exception ex) { Console.WriteLine($"Error loading employee report: {ex.Message}"); } } private async Task DownloadExcel() { var rows = allEmployees.Select(emp => new object?[] { emp.FullName, emp.Username, emp.AssignedIssues, emp.OpenIssues, emp.OverdueIssues, emp.ActualHours, emp.WorkloadLevel, emp.Trend }); var workbookBytes = SpreadsheetReportBuilder.BuildTableWorkbook( "Employee Workload", "Employee Workload & Performance Report", new[] { "Employee", "Username", "Assigned Issues", "Open Issues", "Overdue Issues", "Actual Hours", "Workload", "Trend" }, rows, new[] { $"Search: {(string.IsNullOrWhiteSpace(searchQuery) ? "All employees" : searchQuery)}", $"Period: {filterPeriod}", $"Task 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", $"EmployeeWorkloadPerformance_{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 employees" : searchQuery)}", $"Period: {filterPeriod}", $"Task Status: {filterStatus switch { "0" => "All", "1" => "To Do", "2" => "In Progress", "3" => "Done", "overdue" => "Overdue", _ => filterStatus }}", $"Generated: {DisplayFormats.Date(DateTime.Today)}" }; var rows = allEmployees.Select(emp => new[] { emp.FullName, emp.Username, emp.AssignedIssues.ToString(), emp.OpenIssues.ToString(), emp.OverdueIssues.ToString(), emp.ActualHours.ToString("N1"), emp.WorkloadLevel, emp.Trend }); var pdfBytes = SimplePdfReportBuilder.BuildTableReport( "Employee Workload & Performance Report", summaryLines, new[] { "Employee", "Username", "Assigned Issues", "Open", "Overdue", "Hours", "Workload", "Trend" }, rows); await JS.InvokeVoidAsync( "downloadFileFromBase64", $"EmployeeWorkloadPerformance_{DateTime.Today:yyyyMMdd}.pdf", "application/pdf", Convert.ToBase64String(pdfBytes)); } private static string GetWorkloadBadge(string workload) => workload switch { "High" => "bg-rose-50 text-rose-700", "Light" => "bg-slate-100 text-slate-600", _ => "bg-emerald-50 text-emerald-700" }; private static string GetTrendBadge(string trend) => trend switch { "Improving" => "bg-emerald-50 text-emerald-700", "Declining" => "bg-rose-50 text-rose-700", _ => "bg-blue-50 text-blue-700" }; private static string BuildShortName(string fullName) { var parts = fullName.Split(' ', StringSplitOptions.RemoveEmptyEntries); return parts.Length == 0 ? "User" : parts.Length == 1 ? parts[0] : $"{parts[0]} {parts[^1][0]}."; } }