feat(platform): harden access scoping and delivery baseline
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
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";
|
||||
@@ -25,6 +26,8 @@ export async function getDashboardOverview(db: PrismaClient) {
|
||||
budgetAssignments,
|
||||
recentActivity,
|
||||
allResources,
|
||||
approvedVacations,
|
||||
totalEstimates,
|
||||
] = await Promise.all([
|
||||
db.resource.count(),
|
||||
db.resource.count({ where: { isActive: true } }),
|
||||
@@ -95,6 +98,8 @@ export async function getDashboardOverview(db: PrismaClient) {
|
||||
db.resource.findMany({
|
||||
select: { chapter: true, chargeabilityTarget: true },
|
||||
}),
|
||||
db.vacation.count({ where: { status: VacationStatus.APPROVED } }),
|
||||
db.estimate.count(),
|
||||
]);
|
||||
|
||||
const planningReadModel = buildSplitAllocationReadModel({
|
||||
@@ -200,6 +205,8 @@ export async function getDashboardOverview(db: PrismaClient) {
|
||||
totalAllocations,
|
||||
activeAllocations,
|
||||
cancelledAllocations,
|
||||
approvedVacations,
|
||||
totalEstimates,
|
||||
budgetSummary: {
|
||||
totalBudgetCents,
|
||||
totalCostCents,
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface ProjectHealthRow {
|
||||
id: string;
|
||||
projectName: string;
|
||||
shortCode: string;
|
||||
status: string;
|
||||
clientId: string | null;
|
||||
clientName: string | null;
|
||||
budgetHealth: number;
|
||||
@@ -74,6 +75,7 @@ export async function getDashboardProjectHealth(
|
||||
id: true,
|
||||
name: true,
|
||||
shortCode: true,
|
||||
status: true,
|
||||
budgetCents: true,
|
||||
endDate: true,
|
||||
clientId: true,
|
||||
@@ -232,6 +234,7 @@ export async function getDashboardProjectHealth(
|
||||
id: p.id,
|
||||
projectName: p.name,
|
||||
shortCode: p.shortCode,
|
||||
status: p.status,
|
||||
clientId: p.clientId,
|
||||
clientName: p.client?.name ?? null,
|
||||
budgetHealth,
|
||||
|
||||
@@ -12,6 +12,31 @@ interface SkillEntry {
|
||||
level?: number;
|
||||
}
|
||||
|
||||
export interface SkillGapSummaryRoleGap {
|
||||
role: string;
|
||||
needed: number;
|
||||
filled: number;
|
||||
gap: number;
|
||||
fillRate: number;
|
||||
}
|
||||
|
||||
export interface SkillSupplySummaryRow {
|
||||
skill: string;
|
||||
resourceCount: number;
|
||||
}
|
||||
|
||||
export interface ResourcesByRoleSummaryRow {
|
||||
role: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface DashboardSkillGapSummary {
|
||||
roleGaps: SkillGapSummaryRoleGap[];
|
||||
totalOpenPositions: number;
|
||||
skillSupplyTop10: SkillSupplySummaryRow[];
|
||||
resourcesByRole: ResourcesByRoleSummaryRow[];
|
||||
}
|
||||
|
||||
export async function getDashboardSkillGaps(
|
||||
db: PrismaClient,
|
||||
): Promise<SkillGapRow[]> {
|
||||
@@ -87,3 +112,86 @@ export async function getDashboardSkillGaps(
|
||||
rows.sort((a, b) => a.gap - b.gap);
|
||||
return rows.slice(0, 10);
|
||||
}
|
||||
|
||||
export async function getDashboardSkillGapSummary(
|
||||
db: PrismaClient,
|
||||
): Promise<DashboardSkillGapSummary> {
|
||||
const now = new Date();
|
||||
|
||||
const demands = await db.demandRequirement.findMany({
|
||||
where: {
|
||||
project: { status: { in: ["ACTIVE", "DRAFT"] } },
|
||||
status: { not: "CANCELLED" },
|
||||
endDate: { gte: now },
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
headcount: true,
|
||||
roleEntity: { select: { name: true } },
|
||||
_count: { select: { assignments: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const demandByRole = new Map<string, { needed: number; filled: number }>();
|
||||
for (const demand of demands) {
|
||||
const roleName = demand.roleEntity?.name ?? demand.role ?? "Unknown";
|
||||
const existing = demandByRole.get(roleName) ?? { needed: 0, filled: 0 };
|
||||
existing.needed += demand.headcount;
|
||||
existing.filled += Math.min(demand._count.assignments, demand.headcount);
|
||||
demandByRole.set(roleName, existing);
|
||||
}
|
||||
|
||||
const resources = await db.resource.findMany({
|
||||
where: { isActive: true },
|
||||
select: {
|
||||
skills: true,
|
||||
areaRole: { select: { name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const skillSupply = new Map<string, number>();
|
||||
const supplyByRole = new Map<string, number>();
|
||||
|
||||
for (const resource of resources) {
|
||||
const rawSkills = Array.isArray(resource.skills)
|
||||
? resource.skills as Array<Record<string, unknown>>
|
||||
: [];
|
||||
for (const entry of rawSkills) {
|
||||
const skillName = typeof entry.skill === "string"
|
||||
? entry.skill
|
||||
: typeof entry.name === "string"
|
||||
? entry.name
|
||||
: null;
|
||||
if (!skillName) continue;
|
||||
skillSupply.set(skillName.toLowerCase(), (skillSupply.get(skillName.toLowerCase()) ?? 0) + 1);
|
||||
}
|
||||
|
||||
const roleName = resource.areaRole?.name;
|
||||
if (roleName) {
|
||||
supplyByRole.set(roleName, (supplyByRole.get(roleName) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const roleGaps = [...demandByRole.entries()]
|
||||
.map(([role, { needed, filled }]) => ({
|
||||
role,
|
||||
needed,
|
||||
filled,
|
||||
gap: needed - filled,
|
||||
fillRate: needed > 0 ? Math.round((filled / needed) * 100) : 100,
|
||||
}))
|
||||
.filter((gap) => gap.gap > 0)
|
||||
.sort((left, right) => right.gap - left.gap);
|
||||
|
||||
return {
|
||||
roleGaps,
|
||||
totalOpenPositions: roleGaps.reduce((sum, gap) => sum + gap.gap, 0),
|
||||
skillSupplyTop10: [...skillSupply.entries()]
|
||||
.sort((left, right) => right[1] - left[1])
|
||||
.slice(0, 10)
|
||||
.map(([skill, resourceCount]) => ({ skill, resourceCount })),
|
||||
resourcesByRole: [...supplyByRole.entries()]
|
||||
.sort((left, right) => right[1] - left[1])
|
||||
.map(([role, count]) => ({ role, count })),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,7 +37,9 @@ export {
|
||||
|
||||
export {
|
||||
getDashboardSkillGaps,
|
||||
getDashboardSkillGapSummary,
|
||||
type SkillGapRow,
|
||||
type DashboardSkillGapSummary,
|
||||
} from "./get-skill-gaps.js";
|
||||
|
||||
export {
|
||||
|
||||
Reference in New Issue
Block a user