import type { PrismaClient } from "@capakraken/db"; import { VacationStatus } from "@capakraken/db"; import { AllocationStatus } from "@capakraken/shared"; import { buildSplitAllocationReadModel } from "../allocation/build-split-allocation-read-model.js"; import { calculateInclusiveDays } from "./shared.js"; import type { WeekdayAvailability } from "@capakraken/shared"; import { calculateEffectiveAllocationCostCents, loadDailyAvailabilityContexts, } from "./holiday-capacity.js"; function hasAvailability( resource: T | null | undefined, ): resource is T & { availability: WeekdayAvailability } { return resource !== null && resource !== undefined && resource.availability !== null && resource.availability !== undefined; } export async function getDashboardOverview(db: PrismaClient) { const [ totalResources, activeResources, totalProjects, allProjects, allDemandRequirements, allAssignments, budgetAssignments, recentActivity, allResources, approvedVacations, totalEstimates, ] = await Promise.all([ db.resource.count(), db.resource.count({ where: { isActive: true } }), db.project.count(), db.project.findMany({ select: { status: true, budgetCents: true } }), db.demandRequirement.findMany({ select: { id: true, projectId: true, startDate: true, endDate: true, hoursPerDay: true, percentage: true, role: true, roleId: true, headcount: true, status: true, metadata: true, createdAt: true, updatedAt: true, }, }), db.assignment.findMany({ select: { id: true, demandRequirementId: true, resourceId: true, projectId: true, startDate: true, endDate: true, hoursPerDay: true, percentage: true, role: true, roleId: true, dailyCostCents: true, status: true, metadata: true, createdAt: true, updatedAt: true, }, }), db.assignment.findMany({ where: { status: { not: "CANCELLED" } }, select: { startDate: true, endDate: true, dailyCostCents: true, resource: { select: { id: true, availability: true, countryId: true, federalState: true, metroCityId: true, country: { select: { code: true } }, metroCity: { select: { name: true } }, }, }, }, }), db.auditLog.findMany({ orderBy: { createdAt: "desc" }, take: 10, select: { id: true, entityType: true, action: true, createdAt: true }, }), db.resource.findMany({ select: { chapter: true, chargeabilityTarget: true }, }), db.vacation.count({ where: { status: VacationStatus.APPROVED } }), db.estimate.count(), ]); const planningReadModel = buildSplitAllocationReadModel({ demandRequirements: allDemandRequirements, assignments: allAssignments, }); const totalAllocations = planningReadModel.allocations.length; const activeAllocations = planningReadModel.allocations.filter( (allocation) => allocation.status !== AllocationStatus.CANCELLED, ).length; const contextStart = budgetAssignments.length > 0 ? new Date(Math.min(...budgetAssignments.map((assignment) => assignment.startDate.getTime()))) : new Date(); const contextEnd = budgetAssignments.length > 0 ? new Date(Math.max(...budgetAssignments.map((assignment) => assignment.endDate.getTime()))) : new Date(); const contexts = await loadDailyAvailabilityContexts( db, budgetAssignments .map((assignment) => assignment.resource) .filter(hasAvailability) .map((resource) => ({ id: resource.id, availability: resource.availability as unknown as WeekdayAvailability, countryId: resource.countryId, countryCode: resource.country?.code, federalState: resource.federalState, metroCityId: resource.metroCityId, metroCityName: resource.metroCity?.name, })), contextStart, contextEnd, ); const totalCostCents = budgetAssignments.reduce( (sum, assignment) => sum + ( hasAvailability(assignment.resource) ? calculateEffectiveAllocationCostCents({ availability: assignment.resource.availability as unknown as WeekdayAvailability, startDate: assignment.startDate, endDate: assignment.endDate, dailyCostCents: assignment.dailyCostCents ?? 0, periodStart: assignment.startDate, periodEnd: assignment.endDate, context: contexts.get(assignment.resource.id), }) : (assignment.dailyCostCents ?? 0) * calculateInclusiveDays(assignment.startDate, assignment.endDate) ), 0, ); const totalBudgetCents = allProjects.reduce( (sum, project) => sum + (project.budgetCents ?? 0), 0, ); const activeProjectCount = allProjects.filter((project) => project.status === "ACTIVE").length; const inactiveResourceCount = Math.max(totalResources - activeResources, 0); const inactiveProjectCount = Math.max(totalProjects - activeProjectCount, 0); const cancelledAllocations = Math.max(totalAllocations - activeAllocations, 0); const budgetedProjects = allProjects.filter((project) => (project.budgetCents ?? 0) > 0).length; const remainingBudgetCents = totalBudgetCents - totalCostCents; const avgUtilizationPercent = totalBudgetCents > 0 ? Math.round((totalCostCents / totalBudgetCents) * 100) : 0; const statusCountMap = new Map(); for (const project of allProjects) { statusCountMap.set( project.status, (statusCountMap.get(project.status) ?? 0) + 1, ); } const chapterMap = new Map< string, { resourceCount: number; chargeabilitySum: number } >(); for (const resource of allResources) { const chapter = resource.chapter ?? "Unassigned"; const existing = chapterMap.get(chapter) ?? { resourceCount: 0, chargeabilitySum: 0, }; chapterMap.set(chapter, { resourceCount: existing.resourceCount + 1, chargeabilitySum: existing.chargeabilitySum + (resource.chargeabilityTarget ?? 0), }); } return { totalResources, activeResources, inactiveResources: inactiveResourceCount, totalProjects, activeProjects: activeProjectCount, inactiveProjects: inactiveProjectCount, totalAllocations, activeAllocations, cancelledAllocations, approvedVacations, totalEstimates, budgetSummary: { totalBudgetCents, totalCostCents, avgUtilizationPercent, }, budgetBasis: { remainingBudgetCents, budgetedProjects, unbudgetedProjects: Math.max(totalProjects - budgetedProjects, 0), trackedAssignmentCount: budgetAssignments.length, windowStart: budgetAssignments.length > 0 ? contextStart : null, windowEnd: budgetAssignments.length > 0 ? contextEnd : null, }, recentActivity: recentActivity.map((activity) => ({ id: activity.id, entityType: activity.entityType, action: activity.action, createdAt: activity.createdAt, })), projectsByStatus: [...statusCountMap.entries()].map(([status, count]) => ({ status, count, })), chapterUtilization: [...chapterMap.entries()].map(([chapter, data]) => ({ chapter, resourceCount: data.resourceCount, avgChargeabilityTarget: data.resourceCount > 0 ? Math.round(data.chargeabilitySum / data.resourceCount) : 0, })), }; }