Spaces:
Running
Running
task-tracking-system / TaskTrackingSystem.WebApp /Components /Pages /Features /Reports /EmployeeReport.razor
| @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 | |
| <PageTitle>@AppLocalization.Text("report.employeeWorkloadTitle", "Employee Workload & Performance")</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.employeeWorkloadHeading", "Employee Workload & Performance")</h2> | |
| <p class="text-slate-500 mt-1">@AppLocalization.Text("report.employeeWorkloadDesc", "Monitor issue workload, execution health, and actual effort.")</p> | |
| </div> | |
| <div class="flex items-center space-x-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> | |
| <!-- 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("report.searchEmployee", "Search Employee")</label> | |
| <input @bind="searchInput" type="text" placeholder="@AppLocalization.Text("common.searchByName", "Search by name...")" 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("report.periodFilter", "Period Filter")</label> | |
| <select @bind="filterPeriod" 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("common.all", "All Time")</option> | |
| <option value="daily">Daily (Today)</option> | |
| <option value="monthly">Monthly (This Month)</option> | |
| <option value="yearly">Yearly (This Year)</option> | |
| <option value="custom">Custom Range</option> | |
| </select> | |
| </div> | |
| @if (filterPeriod == "custom") | |
| { | |
| <div> | |
| <label class="block text-xs font-medium text-slate-500 mb-1.5">Start Date</label> | |
| <input @bind="filterStartDate" type="date" 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" /> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-medium text-slate-500 mb-1.5">End Date</label> | |
| <input @bind="filterEndDate" type="date" 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" /> | |
| </div> | |
| } | |
| <div> | |
| <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.status", "Task Status")</label> | |
| <select @bind="filterStatus" 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.allTasks", "All Tasks")</option> | |
| <option value="1">@AppLocalization.StatusLabel(AppTaskStatus.Todo)</option> | |
| <option value="2">@AppLocalization.StatusLabel(AppTaskStatus.InProgress)</option> | |
| <option value="3">@AppLocalization.StatusLabel(AppTaskStatus.Done)</option> | |
| <option value="overdue">@AppLocalization.Text("status.overdue", "Overdue")</option> | |
| </select> | |
| </div> | |
| <button @onclick="ApplyAllFilters" 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> Search | |
| </button> | |
| <button @onclick="ResetAllFilters" 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="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i> 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> | |
| <!-- Content Area --> | |
| @if (isLoading) | |
| { | |
| <div class="flex items-center justify-center py-20"> | |
| <div class="flex items-center space-x-3 text-slate-500"> | |
| <svg class="animate-spin h-5 w-5" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> | |
| <span class="text-sm font-medium">@AppLocalization.Text("common.loading", "Loading data...")</span> | |
| </div> | |
| </div> | |
| } | |
| else | |
| { | |
| @if (isChartView) | |
| { | |
| <div class="grid grid-cols-1 gap-5 xl:grid-cols-12"> | |
| <div class="xl:col-span-7"> | |
| <WorkloadHeatmap Title="@AppLocalization.Text("report.workloadHeatmap", "Workload heatmap")" Subtitle="@AppLocalization.Text("report.workloadHeatmapDesc", "Capacity signals across assigned work, open issues, overdue issues, and hours.")" Rows="EmployeeHeatmapRows" Columns="EmployeeHeatmapColumns" Cells="EmployeeHeatmapCells" Insight="@AppLocalization.Text("report.workloadHeatmapInsight", "Darker cells mean someone may need support or scope adjustment.")" /> | |
| </div> | |
| <div class="xl:col-span-5"> | |
| <ReportChartCard Title="@AppLocalization.Text("report.capacitySupportList", "Capacity support list")" Subtitle="@AppLocalization.Text("report.capacitySupportDesc", "Highest support signals, phrased as capacity risk.")" Insight="@AppLocalization.Text("report.capacitySupportInsight", "Use this to route help and remove blockers, not to rank people negatively.")"> | |
| <TopRiskList Items="EmployeeCapacityRiskItems" EmptyText="@AppLocalization.Text("report.noCapacityRisks", "No capacity risks in the current filter.")" /> | |
| </ReportChartCard> | |
| </div> | |
| <div class="xl:col-span-5"> | |
| <ReportChartCard Title="@AppLocalization.Text("report.completedVsOpenIssues", "Completed vs open issues")" Subtitle="@AppLocalization.Text("report.teamExecutionBalance", "Team execution balance by issue state.")" Value="@CompletedIssueTotal.ToString()" ValueLabel="@AppLocalization.Text("report.completedTasks", "completed tasks")"> | |
| <StackedProgressBar Segments="EmployeeCompletionSegments" AriaLabel="Completed versus open employee workload" /> | |
| </ReportChartCard> | |
| </div> | |
| <div class="xl:col-span-4"> | |
| <ReportChartCard Title="@AppLocalization.Text("report.overdueSupport", "Overdue support")" Subtitle="@AppLocalization.Text("report.employeesWithOverdueLoad", "Employees with overdue issue load.")" Insight="@AppLocalization.Text("report.overdueSupportInsight", "Treat overdue counts as support requests.")"> | |
| <TopRiskList Items="EmployeeOverdueItems" EmptyText="@AppLocalization.Text("report.noOverdueSupport", "No overdue support needs found.")" /> | |
| </ReportChartCard> | |
| </div> | |
| <div class="xl:col-span-3"> | |
| <TrendSparklineCard Title="@AppLocalization.Text("report.completionPattern", "Completion pattern")" Subtitle="@AppLocalization.Text("report.completionPatternDesc", "Completion rate across visible employees.")" Value="@($"{AverageCompletionRate:0}%")" ValueLabel="@AppLocalization.Text("report.avgRate", "avg rate")" Points="EmployeeCompletionSparkline" Color="#3b82f6" Insight="@AppLocalization.Text("report.completionPatternInsight", "A quick view of whether completion is clustered or balanced.")" /> | |
| </div> | |
| </div> | |
| } | |
| else | |
| { | |
| <!-- List View --> | |
| <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden"> | |
| <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">No.</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Employee</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assigned Issues</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Open / Overdue</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Actual Hours</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Workload</th> | |
| <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Trend</th> | |
| </tr> | |
| </thead> | |
| <tbody class="divide-y divide-slate-100"> | |
| @if (pagedEmployees.Any()) | |
| { | |
| @foreach (var entry in pagedEmployees.Select((emp, index) => (emp, index))) | |
| { | |
| var emp = entry.emp; | |
| <tr class="hover:bg-slate-50/50 transition-colors"> | |
| <td class="px-6 py-4 text-sm font-semibold text-slate-500">@((currentPage - 1) * pageSize + entry.index + 1)</td> | |
| <td class="px-6 py-4"> | |
| <div class="flex items-center space-x-3"> | |
| <span class="flex h-9 w-9 items-center justify-center rounded-full bg-slate-100 text-slate-700 font-semibold text-xs"> | |
| @(emp.FullName.Length > 0 ? emp.FullName[0].ToString().ToUpper() : "U") | |
| </span> | |
| <div> | |
| <p class="text-sm font-semibold text-slate-900">@emp.FullName</p> | |
| <p class="text-xs text-slate-400">@@@emp.Username</p> | |
| </div> | |
| </div> | |
| </td> | |
| <td class="px-6 py-4 text-sm font-medium text-slate-900">@emp.AssignedIssues</td> | |
| <td class="px-6 py-4 text-sm font-medium"><span class="text-blue-600">@emp.OpenIssues</span> / <span class="text-rose-600">@emp.OverdueIssues</span></td> | |
| <td class="px-6 py-4 text-sm font-semibold text-slate-800">@emp.ActualHours.ToString("N1") hrs</td> | |
| <td class="px-6 py-4"><span class="inline-flex rounded-full px-2.5 py-0.5 text-xs font-semibold @GetWorkloadBadge(emp.WorkloadLevel)">@emp.WorkloadLevel</span></td> | |
| <td class="px-6 py-4"><span class="inline-flex rounded-full px-2.5 py-0.5 text-xs font-semibold @GetTrendBadge(emp.Trend)">@emp.Trend</span></td> | |
| </tr> | |
| } | |
| } | |
| else | |
| { | |
| <tr> | |
| <td colspan="7" class="px-6 py-12 text-center"> | |
| <p class="text-sm text-slate-400">No employee data found matching criteria.</p> | |
| </td> | |
| </tr> | |
| } | |
| </tbody> | |
| </table> | |
| <!-- Pagination --> | |
| <Pagination CurrentPage="currentPage" | |
| TotalPages="TotalPages" | |
| PageSize="pageSize" | |
| TotalCount="TotalCount" | |
| OnPageChanged="HandlePageChanged" | |
| OnPageSizeChanged="HandlePageSizeChanged" /> | |
| </div> | |
| } | |
| } | |
| </div> | |
| @code { | |
| private bool isLoading = true; | |
| private bool isChartView = false; | |
| private List<EmployeeProductivityReportDto> allEmployees = new(); | |
| private List<EmployeeProductivityReportDto> pagedEmployees = new(); | |
| private PagedResult<EmployeeProductivityReportDto>? 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<string> EmployeeHeatmapRows => allEmployees.Take(6).Select(emp => emp.FullName).ToList(); | |
| private List<string> 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<HeatmapCell> 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<TopRiskItem> 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<StackedSegment> 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<TopRiskItem> 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<SparklinePoint> 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<string>(); | |
| 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<Result<List<EmployeeProductivityReportDto>>>(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<string>(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<PagedResult<EmployeeProductivityReportDto>>(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<string> | |
| { | |
| $"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]}."; | |
| } | |
| } | |