@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 { }
@AppLocalization.Text("common.index", "No.") @AppLocalization.Text("report.employee", "Employee") @AppLocalization.Text("report.assignedIssues", "Assigned Issues") @AppLocalization.Text("report.openOverdue", "Open / Overdue") @AppLocalization.Text("common.actualHours", "Actual Hours") @AppLocalization.Text("report.workload", "Workload") @AppLocalization.Text("report.trend", "Trend")
@AppLocalization.Number((currentPage - 1) * pageSize + entry.index + 1)
@(GetEmployeeName(emp).Length > 0 ? GetEmployeeName(emp)[0].ToString().ToUpper() : "U")

@GetEmployeeName(emp)

@@@emp.Username

@AppLocalization.Number(emp.AssignedIssues) @AppLocalization.Number(emp.OpenIssues) / @AppLocalization.Number(emp.OverdueIssues) @AppLocalization.Number(emp.ActualHours, "N1") @AppLocalization.Text("common.hoursAbbrev", "hrs") @GetWorkloadLabel(emp.WorkloadLevel) @GetTrendLabel(emp.Trend)

@AppLocalization.Text("report.noEmployeeData", "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 string GetEmployeeName(EmployeeProductivityReportDto employee) => Content.GetText(employee.FullName, employee.FullNameMy); private List EmployeeHeatmapRows => allEmployees.Take(6).Select(GetEmployeeName).ToList(); private List EmployeeHeatmapColumns => new() { AssignedColumnLabel, OpenColumnLabel, OverdueColumnLabel, HoursColumnLabel }; private string AssignedColumnLabel => AppLocalization.Text("report.assignedColumn", "Assigned"); private string OpenColumnLabel => AppLocalization.Text("report.openColumn", "Open"); private string OverdueColumnLabel => AppLocalization.Text("report.overdueColumn", "Overdue"); private string HoursColumnLabel => AppLocalization.Text("common.hours", "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(GetEmployeeName(emp), AssignedColumnLabel, Math.Min(100, emp.AssignedCount * 8), $"{AppLocalization.Number(emp.AssignedCount)} {AppLocalization.Text("report.assignedTasks", "assigned tasks").ToLowerInvariant()}"), new HeatmapCell(GetEmployeeName(emp), OpenColumnLabel, Math.Min(100, emp.OpenIssues * 12), $"{AppLocalization.Number(emp.OpenIssues)} {AppLocalization.Text("report.openIssuesSegment", "open issues").ToLowerInvariant()}"), new HeatmapCell(GetEmployeeName(emp), OverdueColumnLabel, Math.Min(100, emp.OverdueIssues * 20), $"{AppLocalization.Number(emp.OverdueIssues)} {AppLocalization.Text("report.overdueIssuesSegment", "overdue issues").ToLowerInvariant()}"), new HeatmapCell(GetEmployeeName(emp), HoursColumnLabel, Math.Min(100, (double)emp.ActualHours * 5), $"{AppLocalization.Number(emp.ActualHours, "N1")} {AppLocalization.Text("common.actualHours", "actual hours").ToLowerInvariant()}") }) .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"; var workload = AppLocalization.Text($"priority.{emp.WorkloadLevel.ToLower()}", emp.WorkloadLevel); return new TopRiskItem(GetEmployeeName(emp), $"{workload} · {AppLocalization.Number(emp.ActualHours, "N1")}h {AppLocalization.Text("report.tracked", "tracked")}", score, AppLocalization.Text("report.needsSupport", "needs support"), tone, $"{AppLocalization.Number(emp.OpenIssues)} {AppLocalization.Text("report.open", "open")} · {AppLocalization.Number(emp.OverdueIssues)} {AppLocalization.Text("status.overdue", "overdue")}"); }) .OrderByDescending(item => item.Score) .Take(5) .ToList(); private List EmployeeCompletionSegments => new() { new(AppLocalization.Text("report.completedTasks", "Completed tasks"), allEmployees.Sum(emp => emp.CompletedCount), "#10b981"), new(AppLocalization.Text("report.assignedTasks", "Assigned tasks"), allEmployees.Sum(emp => Math.Max(0, emp.AssignedCount - emp.CompletedCount)), "#3b82f6"), new(AppLocalization.Text("report.openIssuesSegment", "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(GetEmployeeName(emp), $"{AppLocalization.Number(emp.OpenIssues)} {AppLocalization.Text("report.openIssuesSegment", "open issues")}", Math.Min(100, emp.OverdueIssues * 25), $"{AppLocalization.Number(emp.OverdueIssues)} {AppLocalization.Text("status.overdue", "overdue")}", "rose", AppLocalization.Text("report.capacityRisk", "Capacity risk"))) .ToList(); private List EmployeeCompletionSparkline => allEmployees .OrderByDescending(emp => emp.AssignedCount) .Take(8) .Select(emp => new SparklinePoint(BuildShortName(GetEmployeeName(emp)), emp.CompletionRate)) .DefaultIfEmpty(new SparklinePoint(AppLocalization.Text("common.noDataYet", "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() { isLoading = true; allEmployees = new(); pagedEmployees = new(); reportPage = null; 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 .Where(employee => MatchesEmployeeSearch(employee, searchQuery)) .ToList(); } // 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) { if (string.IsNullOrWhiteSpace(searchQuery)) { reportPage = pagedResult; pagedEmployees = pagedResult.Items.ToList(); currentPage = pagedResult.Page; pageSize = pagedResult.PageSize; } else { var totalCount = allEmployees.Count; var totalPages = totalCount == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize); currentPage = totalPages > 0 ? Math.Min(currentPage, totalPages) : 1; pagedEmployees = allEmployees .Skip((currentPage - 1) * pageSize) .Take(pageSize) .ToList(); reportPage = new PagedResult { Items = pagedEmployees, TotalCount = totalCount, Page = currentPage, PageSize = pageSize, TotalPages = totalPages }; } } } catch (Exception ex) { Console.WriteLine($"Error loading employee report: {ex.Message}"); } finally { isLoading = false; } } private static bool MatchesEmployeeSearch(EmployeeProductivityReportDto employee, string? search) { if (string.IsNullOrWhiteSpace(search)) { return true; } var term = search.Trim(); return ContainsSearch(employee.FullName, term) || ContainsSearch(employee.FullNameMy, term) || ContainsSearch(employee.Username, term); } private static bool ContainsSearch(string? value, string search) => !string.IsNullOrWhiteSpace(value) && value.Contains(search, StringComparison.OrdinalIgnoreCase); private async Task DownloadExcel() { var rows = allEmployees.Select(emp => new object?[] { GetEmployeeName(emp), 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[] { GetEmployeeName(emp), 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 GetWorkloadLabel(string workload) => workload switch { "High" => AppLocalization.Text("report.workloadHigh", "High"), "Light" => AppLocalization.Text("report.workloadLight", "Light"), _ => AppLocalization.Text("report.workloadBalanced", "Balanced") }; 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 GetTrendLabel(string trend) => trend switch { "Improving" => AppLocalization.Text("report.trendImproving", "Improving"), "Declining" => AppLocalization.Text("report.trendDeclining", "Declining"), _ => AppLocalization.Text("report.trendStable", "Stable") }; 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]}."; } }