feat(planning): ship holiday-aware planning and assistant upgrades

This commit is contained in:
2026-03-28 22:49:28 +01:00
parent 2a005794e7
commit 4f48afe7b4
151 changed files with 17738 additions and 1940 deletions
@@ -1,8 +1,18 @@
import type { PrismaClient } from "@capakraken/db";
import { AllocationStatus } from "@capakraken/shared";
import { buildSplitAllocationReadModel } from "../allocation/build-split-allocation-read-model.js";
import { listAssignmentBookings } from "../allocation/list-assignment-bookings.js";
import { calculateInclusiveDays } from "./shared.js";
import type { WeekdayAvailability } from "@capakraken/shared";
import {
calculateEffectiveAllocationCostCents,
loadDailyAvailabilityContexts,
} from "./holiday-capacity.js";
function hasAvailability<T extends { availability?: unknown }>(
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 [
@@ -12,7 +22,7 @@ export async function getDashboardOverview(db: PrismaClient) {
allProjects,
allDemandRequirements,
allAssignments,
budgetBookings,
budgetAssignments,
recentActivity,
allResources,
] = await Promise.all([
@@ -58,7 +68,25 @@ export async function getDashboardOverview(db: PrismaClient) {
updatedAt: true,
},
}),
listAssignmentBookings(db, {}),
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,
@@ -77,12 +105,46 @@ export async function getDashboardOverview(db: PrismaClient) {
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 = budgetBookings.reduce(
(sum, booking) =>
sum +
(booking.dailyCostCents ?? 0) *
calculateInclusiveDays(booking.startDate, booking.endDate),
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,
);
@@ -90,6 +152,12 @@ export async function getDashboardOverview(db: PrismaClient) {
(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
@@ -125,16 +193,26 @@ export async function getDashboardOverview(db: PrismaClient) {
return {
totalResources,
activeResources,
inactiveResources: inactiveResourceCount,
totalProjects,
activeProjects: allProjects.filter((project) => project.status === "ACTIVE")
.length,
activeProjects: activeProjectCount,
inactiveProjects: inactiveProjectCount,
totalAllocations,
activeAllocations,
cancelledAllocations,
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,