using Indotalent.ConfigBackEnd.Extensions; using Indotalent.Infrastructure.Database; using MediatR; using Microsoft.EntityFrameworkCore; namespace Indotalent.Features.Root.Home.Cqrs; public class SalaryReviewDto { public string? EmployeeName { get; set; } public string? Increment { get; set; } public string? Status { get; set; } } public class CareerPathDto { public string? EmployeeName { get; set; } public string? ToGrade { get; set; } public DateTime? Date { get; set; } } public class StaffPerformanceDto { public string? Name { get; set; } public string? Dept { get; set; } public string? LoS { get; set; } public int KpiScore { get; set; } public string? Grade { get; set; } public int ReviewJourney { get; set; } } public class ActiveTransferDto { public string? EmployeeName { get; set; } public string? ToLocation { get; set; } public int ImpactLevel { get; set; } } public class LeaveTicketDto { public string? TicketId { get; set; } public string? Status { get; set; } public string? DueInfo { get; set; } } public class CompStatsDto { public string? Label { get; set; } public string? Value { get; set; } public double Percentage { get; set; } public string? Color { get; set; } } public class CareerBenchmarkDto { public string? EmployeeName { get; set; } public string? Position { get; set; } public double SalaryRatio { get; set; } public string? Status { get; set; } public int StarRating { get; set; } } // Chart data DTOs public class HeadcountTrendDto { public string? Month { get; set; } public int Count { get; set; } public int NewHires { get; set; } } public class DeptDistributionDto { public string? Department { get; set; } public int Count { get; set; } } public class LeaveTypeBreakdownDto { public string? Type { get; set; } public int Count { get; set; } } public class RecentDealDto { public string? Date { get; set; } public string? Ref { get; set; } public string? Description { get; set; } public string? Value { get; set; } public string? Status { get; set; } } public class ActivityFeedDto { public string? Initials { get; set; } public string? AvatarColor { get; set; } public string? Title { get; set; } public string? Subtitle { get; set; } public string? TimeAgo { get; set; } public string? ChipLabel { get; set; } public string? ChipClass { get; set; } } public class DashboardDataResponse { public int TotalStaff { get; set; } public double FteValue { get; set; } public decimal AvgCostPerHire { get; set; } public int HeadcountForecast { get; set; } public double RetentionRate { get; set; } public double LeaversRate { get; set; } public double EngagementRate { get; set; } public double EmployeeExperienceNps { get; set; } public double EmployeeExperienceSat { get; set; } public double RoiPercent { get; set; } public double CertifiedPercent { get; set; } public int PipelineSourced { get; set; } public int PipelineInterviewed { get; set; } public int PipelineOffered { get; set; } public int TotalBranches { get; set; } public int TotalDepartments { get; set; } public int TopTalentCount { get; set; } public int CorePlayerCount { get; set; } public int LowPerfCount { get; set; } public List TopPerformers { get; set; } = new(); public List RecentTransfers { get; set; } = new(); public List PendingLeaves { get; set; } = new(); public List RecentAppraisals { get; set; } = new(); public List UpcomingPromotions { get; set; } = new(); public List CompensationStats { get; set; } = new(); public List CareerBenchmarks { get; set; } = new(); // Chart data public List HeadcountTrends { get; set; } = new(); public List DeptDistributions { get; set; } = new(); public List LeaveTypeBreakdowns { get; set; } = new(); public List RecentDeals { get; set; } = new(); public List ActivityFeeds { get; set; } = new(); // Additional KPIs public int TotalLeaveRequests { get; set; } public int TotalAppraisals { get; set; } public int TotalTransferRequests { get; set; } public int ActivePromotions { get; set; } public decimal TotalPayrollAmount { get; set; } public string FormattedAvgCost { get { if (AvgCostPerHire >= 1000000) return (AvgCostPerHire / 1000000m).ToString("F1") + "M"; if (AvgCostPerHire >= 1000) return (AvgCostPerHire / 1000m).ToString("F1") + "K"; return AvgCostPerHire.ToString("C0"); } } public string FormattedTotalPayroll { get { if (TotalPayrollAmount >= 1000000) return (TotalPayrollAmount / 1000000m).ToString("F1") + "M"; if (TotalPayrollAmount >= 1000) return (TotalPayrollAmount / 1000m).ToString("F1") + "K"; return TotalPayrollAmount.ToString("N0"); } } } public record GetDashboardQuery() : IRequest; public class DashboardHandler : IRequestHandler { private readonly AppDbContext _context; public DashboardHandler(AppDbContext context) => _context = context; public async Task Handle(GetDashboardQuery request, CancellationToken ct) { var today = DateTime.Today; // --- Employee counts --- var totalStaff = await _context.Employee.NotDeletedOnly().CountAsync(ct); var fullTimeStaff = await _context.Employee.NotDeletedOnly().CountAsync(x => x.EmploymentType == "Full-Time", ct); double fteValue = totalStaff > 0 ? (double)fullTimeStaff / totalStaff : 0; // --- Resigned / Leavers --- var resignedCount = await _context.Employee.NotDeletedOnly().CountAsync(x => x.ResignedDate != null && x.ResignedDate <= today, ct); double retentionRate = totalStaff > 0 ? Math.Round((double)(totalStaff - resignedCount) / totalStaff * 100, 1) : 0; double leaversRate = totalStaff > 0 ? Math.Round((double)resignedCount / totalStaff * 100, 1) : 0; // --- Average Cost per Hire (from appraisals) --- var avgSalary = await _context.Appraisal.NotDeletedOnly().AverageAsync(x => (decimal?)x.NewSalary, ct) ?? 0; var forecastCount = await _context.Employee.NotDeletedOnly().CountAsync(x => x.JoinedDate >= today.AddMonths(-3), ct); // --- Branches & Departments --- var totalBranches = await _context.Branch.NotDeletedOnly().CountAsync(ct); var totalDepts = await _context.Department.NotDeletedOnly().CountAsync(ct); // --- Totals --- var totalLeaveRequests = await _context.LeaveRequest.NotDeletedOnly().CountAsync(ct); var totalAppraisals = await _context.Appraisal.NotDeletedOnly().CountAsync(ct); var totalTransfers = await _context.Transfer.NotDeletedOnly().CountAsync(ct); var activePromotions = await _context.Promotion.NotDeletedOnly() .CountAsync(x => x.Status == "Active" || x.Status == "Pending", ct); // --- Evaluation / Performance data --- var allEvaluations = await _context.Evaluation .Include(x => x.Employee).ThenInclude(e => e!.Department) .Include(x => x.Employee).ThenInclude(e => e!.Grade) .NotDeletedOnly() .ToListAsync(ct); var rawEvaluationData = allEvaluations .OrderByDescending(x => x.CreatedAt) .Take(10) .ToList(); var performers = rawEvaluationData.Select(x => { int.TryParse(x.FinalScore, out var s); int.TryParse(x.Rating, out var r); return new StaffPerformanceDto { Name = $"{x.Employee?.FirstName} {x.Employee?.LastName}", Dept = x.Employee?.Department?.Name ?? "N/A", Grade = x.Employee?.Grade?.Name ?? "-", KpiScore = s, ReviewJourney = r > 0 ? r : 1, LoS = x.Employee?.JoinedDate.HasValue == true ? $"{(today.Year - x.Employee.JoinedDate.Value.Year)}y" : "0y" }; }).ToList(); // --- Engagement Rate: average final score from all evaluations --- var scoredEvaluations = allEvaluations .Select(x => { int.TryParse(x.FinalScore, out var s); return s; }) .Where(s => s > 0) .ToList(); double engagementRate = scoredEvaluations.Any() ? Math.Round(scoredEvaluations.Average(), 1) : 0; // --- Employee Experience (EX): NPS & SAT --- double employeeExperienceNps = 0; double employeeExperienceSat = 0; if (allEvaluations.Any()) { var ratingsWithValue = allEvaluations .Select(x => { int.TryParse(x.Rating, out var r); return r; }) .Where(r => r > 0) .ToList(); employeeExperienceNps = ratingsWithValue.Any() ? Math.Round(ratingsWithValue.Average() / 10.0 * 10.0, 1) : 0; employeeExperienceSat = scoredEvaluations.Any() ? Math.Round(scoredEvaluations.Average(), 0) : 0; } // --- ROI: derived from appraisal increment percentage average --- var avgIncrementPct = await _context.Appraisal .NotDeletedOnly() .AverageAsync(x => (double?)x.IncrementPercentage, ct) ?? 0; double roiPercent = Math.Round(avgIncrementPct, 1); // --- Certified % (Talent Scoring) --- int totalScoredEmployees = scoredEvaluations.Count; int certifiedCount = scoredEvaluations.Count(s => s >= 80); double certifiedPercent = totalScoredEmployees > 0 ? Math.Round((double)certifiedCount / totalScoredEmployees * 100, 0) : 0; // --- Recruitment Pipeline --- var employeesJoinedThisYear = await _context.Employee .NotDeletedOnly() .CountAsync(x => x.JoinedDate >= new DateTime(today.Year, 1, 1), ct); var totalEvaluationsThisYear = allEvaluations.Count(x => x.EvaluationDate >= new DateTime(today.Year, 1, 1)); var totalPromotionsThisYear = await _context.Promotion .NotDeletedOnly() .CountAsync(x => x.EffectiveDate >= new DateTime(today.Year, 1, 1), ct); int pipelineSourced = employeesJoinedThisYear + totalEvaluationsThisYear; int pipelineInterviewed = totalEvaluationsThisYear; int pipelineOffered = totalPromotionsThisYear; // --- Transfers --- var transfers = await _context.Transfer .Include(x => x.Employee).Include(x => x.ToBranch) .NotDeletedOnly() .OrderByDescending(x => x.CreatedAt) .Take(5) .Select(x => new ActiveTransferDto { EmployeeName = x.Employee != null ? x.Employee.FirstName : "User", ToLocation = x.ToBranch != null ? x.ToBranch.Name : "Other", ImpactLevel = x.Status == "Processed" ? 5 : 3 }).ToListAsync(ct); // --- Leave requests (latest 5, any status) --- var leaves = await _context.LeaveRequest .NotDeletedOnly() .OrderByDescending(x => x.CreatedAt) .Take(5) .Select(x => new LeaveTicketDto { TicketId = x.AutoNumber, Status = x.Status ?? "Submitted", DueInfo = x.StartDate.ToString("MMM dd") }).ToListAsync(ct); // --- Appraisals (materialize first for safe memory projection) --- var recentAppraisals = await _context.Appraisal .Include(x => x.Employee) .NotDeletedOnly() .OrderByDescending(x => x.CreatedAt) .Take(3) .ToListAsync(ct); var salaryReviews = recentAppraisals.Select(x => new SalaryReviewDto { EmployeeName = x.Employee != null ? x.Employee.FirstName : "Staff", Increment = x.IncrementPercentage.ToString("F1") + "%", Status = x.Status }).ToList(); // --- Promotions (materialize first) --- var upcomingPromotionsRaw = await _context.Promotion .Include(x => x.Employee) .NotDeletedOnly() .OrderByDescending(x => x.CreatedAt) .Take(3) .ToListAsync(ct); var promoDtos = upcomingPromotionsRaw.Select(x => new CareerPathDto { EmployeeName = x.Employee != null ? x.Employee.FirstName : "Staff", ToGrade = x.ToGrade, Date = x.EffectiveDate }).ToList(); // --- Payroll compensation stats --- var latestPayroll = await _context.PayrollProcess .NotDeletedOnly() .OrderByDescending(x => x.ToDate) .FirstOrDefaultAsync(ct); var compStats = new List(); if (latestPayroll != null) { var totalGross = latestPayroll.TotalGross > 0 ? latestPayroll.TotalGross : 1; compStats.Add(new CompStatsDto { Label = "Take Home Pay Net", Value = latestPayroll.TotalTakeHomePay.ToString("N0"), Percentage = (double)(latestPayroll.TotalTakeHomePay / totalGross) * 100, Color = "#3b82f6" }); compStats.Add(new CompStatsDto { Label = "Tax & Deductions", Value = latestPayroll.TotalDeduction.ToString("N0"), Percentage = (double)(latestPayroll.TotalDeduction / totalGross) * 100, Color = "#ef4444" }); compStats.Add(new CompStatsDto { Label = "Incomes/Allowances", Value = latestPayroll.TotalIncome.ToString("N0"), Percentage = (double)(latestPayroll.TotalIncome / totalGross) * 100, Color = "#10b981" }); } // --- Career benchmarks (materialize first) --- var benchmarks = await _context.Employee .Include(x => x.Grade) .Include(x => x.Designation) .NotDeletedOnly() .Where(x => x.Grade != null) .OrderByDescending(x => x.CreatedAt) .Take(5) .ToListAsync(ct); var careerBenchmarks = benchmarks.Select(x => { var ratio = x.Grade!.SalaryTo > 0 ? (double)(x.BasicSalary / x.Grade.SalaryTo) * 100 : 0; return new CareerBenchmarkDto { EmployeeName = x.FirstName, Position = x.Designation?.Name ?? "Staff", SalaryRatio = ratio, Status = ratio > 90 ? "Top Range" : ratio > 60 ? "Mid Range" : "Entry", StarRating = ratio > 80 ? 3 : ratio > 40 ? 2 : 1 }; }).ToList(); // --- CHART: Headcount Trend (last 6 months) --- var headcountTrends = new List(); for (int i = 5; i >= 0; i--) { var monthStart = new DateTime(today.Year, today.Month, 1).AddMonths(-i); var monthEnd = monthStart.AddMonths(1); var count = await _context.Employee.NotDeletedOnly() .CountAsync(x => x.JoinedDate < monthEnd && (x.ResignedDate == null || x.ResignedDate >= monthStart), ct); var newHires = await _context.Employee.NotDeletedOnly() .CountAsync(x => x.JoinedDate >= monthStart && x.JoinedDate < monthEnd, ct); headcountTrends.Add(new HeadcountTrendDto { Month = monthStart.ToString("MMM"), Count = count, NewHires = newHires }); } // --- CHART: Department Distribution --- var allDepartments = await _context.Department .NotDeletedOnly() .Select(x => x.Name) .ToListAsync(ct); var employeeDeptCounts = await _context.Employee .Include(x => x.Department) .NotDeletedOnly() .Where(x => x.Department != null) .Select(x => new { DeptName = x.Department!.Name }) .ToListAsync(ct); var deptDistributions = allDepartments .Select(dept => new DeptDistributionDto { Department = dept ?? "Other", Count = employeeDeptCounts.Count(e => e.DeptName == dept) }) .OrderByDescending(x => x.Count) .ThenBy(x => x.Department) .ToList(); // --- CHART: Leave Type Breakdown (materialize first, group in memory) --- var leaveTypeRawNames = await _context.LeaveRequest .Include(x => x.LeaveCategory) .NotDeletedOnly() .Where(x => x.LeaveCategory != null) .Select(x => new { CatName = x.LeaveCategory!.Name }) .ToListAsync(ct); var leaveTypeBreakdowns = leaveTypeRawNames .GroupBy(x => x.CatName) .Select(g => new LeaveTypeBreakdownDto { Type = g.Key ?? "Other", Count = g.Count() }) .OrderByDescending(x => x.Count) .ToList(); // --- Recent Deals (appraisals as deals) --- var recentDeals = recentAppraisals.Take(5).Select(x => new RecentDealDto { Date = x.CreatedAt?.ToString("MMM dd") ?? "-", Ref = x.AutoNumber ?? "-", Description = (x.Employee?.FirstName ?? "Staff") + " appraisal review", Value = x.NewSalary.ToString("N0"), Status = x.Status ?? "Pending" }).ToList(); // --- Activity Feed --- var activityFeeds = new List(); if (recentAppraisals.Any()) { var a = recentAppraisals.First(); activityFeeds.Add(new ActivityFeedDto { Initials = (a.Employee?.FirstName?.Length > 0 ? a.Employee.FirstName[..1] : "S").ToUpper(), AvatarColor = "#10b981", Title = $"Appraisal Completed — {a.Employee?.FirstName ?? "Staff"}", Subtitle = $"📋 Salary: {a.NewSalary:N0} · Increment: {a.IncrementPercentage:F1}%", TimeAgo = "1 hour ago", ChipLabel = a.Status ?? "Completed", ChipClass = "chip-green" }); } if (upcomingPromotionsRaw.Any()) { var p = upcomingPromotionsRaw.First(); activityFeeds.Add(new ActivityFeedDto { Initials = (p.Employee?.FirstName?.Length > 0 ? p.Employee.FirstName[..1] : "P").ToUpper(), AvatarColor = "#6366f1", Title = $"Promotion Scheduled — {p.Employee?.FirstName ?? "Staff"}", Subtitle = $"🏷️ To Grade: {p.ToGrade} · Effective: {p.EffectiveDate:dd MMM yyyy}", TimeAgo = "2 hours ago", ChipLabel = "Scheduled", ChipClass = "chip-indigo" }); } if (leaves.Any()) { var l = leaves.First(); activityFeeds.Add(new ActivityFeedDto { Initials = "LR", AvatarColor = "#f97316", Title = $"Leave Request Pending — #{l.TicketId}", Subtitle = $"📅 Status: {l.Status} · Due: {l.DueInfo}", TimeAgo = "3 hours ago", ChipLabel = "Pending", ChipClass = "chip-amber" }); } if (transfers.Any()) { var t = transfers.First(); activityFeeds.Add(new ActivityFeedDto { Initials = (t.EmployeeName?.Length > 0 ? t.EmployeeName[..1] : "T").ToUpper(), AvatarColor = "#2563eb", Title = $"Transfer Request — {t.EmployeeName}", Subtitle = $"📍 To: {t.ToLocation} · Impact Level: {t.ImpactLevel}/5", TimeAgo = "5 hours ago", ChipLabel = t.ImpactLevel > 3 ? "Processing" : "Pending", ChipClass = t.ImpactLevel > 3 ? "chip-blue" : "chip-indigo" }); } return new DashboardDataResponse { TotalStaff = totalStaff, FteValue = Math.Round(fteValue, 2), AvgCostPerHire = avgSalary, HeadcountForecast = forecastCount + 5, RetentionRate = retentionRate, LeaversRate = leaversRate, EngagementRate = engagementRate, EmployeeExperienceNps = employeeExperienceNps, EmployeeExperienceSat = employeeExperienceSat, RoiPercent = roiPercent, CertifiedPercent = certifiedPercent, PipelineSourced = pipelineSourced, PipelineInterviewed = pipelineInterviewed, PipelineOffered = pipelineOffered, TotalBranches = totalBranches, TotalDepartments = totalDepts, TotalLeaveRequests = totalLeaveRequests, TotalAppraisals = totalAppraisals, TotalTransferRequests = totalTransfers, ActivePromotions = activePromotions, TotalPayrollAmount = latestPayroll?.TotalGross ?? 0, TopPerformers = performers, RecentTransfers = transfers, PendingLeaves = leaves, RecentAppraisals = salaryReviews, UpcomingPromotions = promoDtos, CompensationStats = compStats, CareerBenchmarks = careerBenchmarks, HeadcountTrends = headcountTrends, DeptDistributions = deptDistributions, LeaveTypeBreakdowns = leaveTypeBreakdowns, RecentDeals = recentDeals, ActivityFeeds = activityFeeds, TopTalentCount = performers.Count(x => x.KpiScore >= 85), CorePlayerCount = performers.Count(x => x.KpiScore < 85 && x.KpiScore >= 60), LowPerfCount = performers.Count(x => x.KpiScore < 60) }; } }