@page "/home" @using Microsoft.AspNetCore.Authorization @using Indotalent.Infrastructure.Authorization.Identity @using Indotalent.Features.Root.Home.Cqrs @using Indotalent.Features.Root.Home @attribute [Authorize(Roles = $"{ApplicationRoles.Admin},{ApplicationRoles.Member}")] @inject HomeService HomeService @inject IJSRuntime JS HRM Dashboard
@* HEADER *@

HRM Dashboard

@if (_isLoading) {
} else { @* ROW 1: 12-col KPI layout *@
@* Key Metrics Card (spans 2 rows) *@

HRM Overview

Key Metrics

Total Staff

@_data.TotalStaff.ToString("N0")

FTE: @_data.FteValue.ToString("F2")

Avg Cost/Hire

@_data.FormattedAvgCost

@_data.TotalAppraisals apps

Retention Rate

@_data.RetentionRate%

Leavers: @_data.LeaversRate%

Engagement

@_data.EngagementRate%

Survey-based
Payroll: @_data.FormattedTotalPayroll FY @DateTime.Now.Year
@* 8 Small KPI Cards (row 1, cols 6-12) *@

Total Staff

@_data.TotalStaff.ToString("N0")

Retention

@_data.RetentionRate%

Engagement

@_data.EngagementRate%

Branches

@_data.TotalBranches

Avg Cost/Hire

@_data.FormattedAvgCost

Appraisals

@_data.TotalAppraisals.ToString("N0")

Leave Reqs

@_data.TotalLeaveRequests.ToString("N0")

Payroll

@_data.FormattedTotalPayroll

@* Metrics Matrix *@

HRM Metrics Matrix

Key indicators across all categories

@{ var mmItems = new[] { new { Title = "Total Staff", Sub = _data.TotalStaff.ToString("N0"), ColorClass = "mm-c1" }, new { Title = "Retention Rate", Sub = $"{_data.RetentionRate}%", ColorClass = "mm-c2" }, new { Title = "Engagement", Sub = $"{_data.EngagementRate}%", ColorClass = "mm-c3" }, new { Title = "Branches", Sub = _data.TotalBranches.ToString(), ColorClass = "mm-c4" }, new { Title = "Avg Cost/Hire", Sub = _data.FormattedAvgCost ?? "-", ColorClass = "mm-c5" }, new { Title = "Departments", Sub = _data.TotalDepartments.ToString(), ColorClass = "mm-c6" }, new { Title = "Transfer Reqs", Sub = _data.TotalTransferRequests.ToString("N0"), ColorClass = "mm-c7" }, new { Title = "Payroll", Sub = _data.FormattedTotalPayroll ?? "0", ColorClass = "mm-c8" }, new { Title = "Appraisals", Sub = _data.TotalAppraisals.ToString("N0"), ColorClass = "mm-c9" }, new { Title = "Promotions", Sub = _data.ActivePromotions.ToString(), ColorClass = "mm-c10" }, new { Title = "Leave Requests", Sub = _data.TotalLeaveRequests.ToString("N0"), ColorClass = "mm-c11" }, new { Title = "Top Talent", Sub = _data.TopTalentCount.ToString(), ColorClass = "mm-c12" }, new { Title = "Core Players", Sub = _data.CorePlayerCount.ToString(), ColorClass = "mm-c13" }, new { Title = "Low Perf.", Sub = _data.LowPerfCount.ToString(), ColorClass = "mm-c14" }, new { Title = "FTE Value", Sub = _data.FteValue.ToString("F2"), ColorClass = "mm-c15" }, new { Title = "EX NPS", Sub = _data.EmployeeExperienceNps.ToString("F1"), ColorClass = "mm-c16" } }; } @foreach (var item in mmItems) {

@item.Title

@item.Sub

}
@* ROW 2: Headcount Trend + Department Distribution *@

Headcount Trend

Monthly staff count vs new hires

Staff New Hires

Department Distribution

Staff by department

@{ var deptList = _data.DeptDistributions?.Take(6).ToList() ?? new List(); var doughnutColors = new[] { "#2563eb", "#f97316", "#ec4899", "#8b5cf6", "#06b6d4", "#10b981" }; var deptTotal = deptList.Sum(x => x.Count); if (deptTotal == 0) deptTotal = 1; for (int i = 0; i < deptList.Count; i++) { var d = deptList[i]; var dpct = (int)Math.Round((double)d.Count / deptTotal * 100); var dcolor = i < doughnutColors.Length ? doughnutColors[i] : "#94a3b8";
@d.Department @dpct%
} } @if (!deptList.Any()) {
No data
}
@* ROW 3: Pipeline Summary + Talent Segments + Leave Breakdown *@

HR Pipeline Summary

As of @DateTime.Now.ToString("MMMM dd, yyyy")

Total Employees

@_data.TotalStaff.ToString("N0")

Appraisals

@_data.TotalAppraisals.ToString("N0")

Total Payroll

@_data.FormattedTotalPayroll

Retention@_data.RetentionRate% Engagement@_data.EngagementRate%

Talent Segments

Performance by category

Top Talent

@_data.TopTalentCount

Core Players

@_data.CorePlayerCount

Low Performance

@_data.LowPerfCount

Active Promotions

@_data.ActivePromotions

Forecast: @_data.HeadcountForecast FTEFTE: @_data.FteValue.ToString("F2")

Leave Type Breakdown

By category this period

@{ var leaveBreakdowns = _data.LeaveTypeBreakdowns?.Take(6).ToList() ?? new List(); var totalLeaves = leaveBreakdowns.Sum(x => x.Count); if (totalLeaves == 0) totalLeaves = 1; foreach (var lb in leaveBreakdowns.Take(6)) { var lpct = (int)Math.Round((double)lb.Count / totalLeaves * 100);
+@lb.Count

@lb.Type

} }
@* ROW 4: Recent Appraisals + HRM Activity *@

Recent Appraisals

Latest salary reviews

@{ var dealList = _data.RecentDeals ?? new List(); } @if (dealList.Any()) { @foreach (var deal in dealList) { } } else { }
DateRef#DescriptionValueStatus
@deal.Date @deal.Ref @deal.Description @deal.Value @deal.Status
No appraisal data available
Active: @_data.TotalAppraisals reviews Payroll: @_data.FormattedTotalPayroll

HRM Activity

Real-time HR updates

Live
@{ var feedList = _data.ActivityFeeds ?? new List(); } @if (feedList.Any()) { @foreach (var act in feedList) {
@act.Initials

@((MarkupString)(act.Title ?? ""))

@act.Subtitle

@act.TimeAgo

@act.ChipLabel
} } else {

No recent activity

}
@* ROW 5: Recruitment Pipeline + Top Performers + Compensation Breakdown *@

Recruitment Pipeline

@DateTime.Now.Year performance

@{ var sourced = _data.PipelineSourced > 0 ? _data.PipelineSourced : 1; var interviewed = _data.PipelineInterviewed > 0 ? _data.PipelineInterviewed : 1; var pipelineItems = new[] { ("Sourced", _data.PipelineSourced, _data.PipelineSourced > 0 ? _data.PipelineSourced : 1, "#2563eb"), ("Interviewed", _data.PipelineInterviewed, _data.PipelineSourced > 0 ? _data.PipelineSourced : 1, "#f97316"), ("Offered", _data.PipelineOffered, _data.PipelineInterviewed > 0 ? _data.PipelineInterviewed : 1, "#ec4899"), ("Fill Rate", _data.PipelineOffered, _data.PipelineOffered > 0 ? _data.PipelineOffered : 1, "#10b981") }; } @foreach (var (label, val, max, color) in pipelineItems) { var pct = max > 0 ? (double)val / max * 100 : 0; pct = Math.Min(pct, 100);

@label

@val
}
Target: @_data.HeadcountForecast FTE On Track

Top Performers

This period ranking

@{ var perfColors = new[] { "#f97316", "#f59e0b", "#6366f1", "#8b5cf6" }; var performerList = _data.TopPerformers?.Take(4).ToList() ?? new List(); } @for (int i = 0; i < performerList.Count; i++) { var p = performerList[i];

@p.Name

KPI: @p.KpiScore

@p.Grade
}
Top Talent: @_data.TopTalentCountAvg KPI: @(performerList.Any() ? performerList.Average(x => x.KpiScore).ToString("F0") : "-")

Compensation Breakdown

Payroll composition

@{ var compList = _data.CompensationStats ?? new List(); } @foreach (var stat in compList) {

@stat.Label

@stat.Value

@($"{stat.Percentage:F0}%")
} @if (!compList.Any()) {

No payroll data available

}
@* ROW 6: Active Transfers + Pending Leave Requests *@

Active Transfers

As of @DateTime.Now.ToString("MMM dd, yyyy")

@{ var transferList = _data.RecentTransfers ?? new List(); } @if (transferList.Any()) { @foreach (var tr in transferList) { } } else { }
EmployeeLocationImpact
@tr.EmployeeName @tr.ToLocation @tr.ImpactLevel/5
No active transfers
Total Transfers: @_data.TotalTransferRequests.ToString("N0")

Latest Leave Requests

Recent submissions

@{ var leaveList = _data.PendingLeaves ?? new List(); } @if (leaveList.Any()) { @foreach (var lv in leaveList) { @{ var chipClass = lv.Status switch { "Approved" => "chip-green", "Rejected" => "chip-red", "Cancelled" => "chip-red", _ => "chip-amber" }; } } } else { }
Request IDStatusDue
#@lv.TicketId@lv.Status @lv.DueInfo
No leave requests found
Total Leaves: @_data.TotalLeaveRequests.ToString("N0") Showing: @leaveList.Count records
}
@code { private DashboardDataResponse _data = new(); private bool _isLoading = true; private bool _chartsInitialized = false; protected override async Task OnInitializedAsync() { _isLoading = true; try { var res = await HomeService.GetDashboardStatsAsync(); if (res?.IsSuccess == true && res.Value != null) _data = res.Value; } finally { _isLoading = false; } } protected override async Task OnAfterRenderAsync(bool firstRender) { if (!_isLoading && !_chartsInitialized) { _chartsInitialized = true; await InitCharts(); } } private async Task InitCharts() { try { // Headcount Trend Bar Chart var hcLabels = _data.HeadcountTrends?.Select(x => x.Month).ToList() ?? new List(); await JS.InvokeVoidAsync("HRMCharts.renderBarChart", "headcountChart", hcLabels, new[] { new { label = "Staff", data = _data.HeadcountTrends?.Select(x => (object)x.Count).ToList() ?? new List(), backgroundColor = "#2563eb", borderRadius = 4, barPercentage = 0.4 }, new { label = "New Hires", data = _data.HeadcountTrends?.Select(x => (object)x.NewHires).ToList() ?? new List(), backgroundColor = "#f97316", borderRadius = 4, barPercentage = 0.4 } }); // Leave Type Doughnut Chart var lbList = _data.LeaveTypeBreakdowns ?? new List(); await JS.InvokeVoidAsync("HRMCharts.renderDoughnutChart", "leaveTypeChart", lbList.Take(6).Select(x => x.Type).ToList(), lbList.Take(6).Select(x => (object)x.Count).ToList(), new[] { "#2563eb", "#f97316", "#ec4899", "#8b5cf6", "#06b6d4", "#10b981" }.Take(lbList.Count).ToList()); // Department Distribution Bar Chart var deptList2 = _data.DeptDistributions ?? new List(); await JS.InvokeVoidAsync("HRMCharts.renderBarChart", "deptChart", deptList2.Select(x => x.Department).ToList(), new[] { new { label = "Staff", data = deptList2.Select(x => (object)x.Count).ToList(), backgroundColor = deptList2.Select((_, i) => (object)(i % 2 == 0 ? "#2563eb" : "#f97316")).ToList(), borderRadius = 4, barPercentage = 0.5 } }); } catch { } } }