rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled

rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)

Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
This commit was merged in pull request #61.
This commit is contained in:
2026-05-21 16:28:40 +02:00
committed by Hartmut
parent d9a7ec0338
commit b41c1d2501
943 changed files with 24548 additions and 16832 deletions
@@ -1,7 +1,10 @@
import { PermissionKey, type WeekdayAvailability } from "@capakraken/shared";
import { PermissionKey, type WeekdayAvailability } from "@nexus/shared";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { calculateEffectiveBookedHours, loadResourceDailyAvailabilityContexts } from "../lib/resource-capacity.js";
import {
calculateEffectiveBookedHours,
loadResourceDailyAvailabilityContexts,
} from "../lib/resource-capacity.js";
import { fmtEur } from "../lib/format-utils.js";
import { planningReadProcedure, requirePermission } from "../trpc.js";
import { createDateRange, round1, toIsoDate } from "./staffing-shared.js";
@@ -22,13 +25,11 @@ type BestProjectResourceInput = {
roleName?: string | undefined;
};
type BestProjectResourceDbClient =
Parameters<typeof loadResourceDailyAvailabilityContexts>[0]
& {
assignment: {
findMany: (args: Record<string, unknown>) => Promise<unknown[]>;
};
type BestProjectResourceDbClient = Parameters<typeof loadResourceDailyAvailabilityContexts>[0] & {
assignment: {
findMany: (args: Record<string, unknown>) => Promise<unknown[]>;
};
};
type BestProjectResourceAssignmentRecord = {
resourceId: string;
@@ -75,7 +76,9 @@ async function queryBestProjectResource(
resource: {
isActive: true,
...(input.chapter ? { chapter: { contains: input.chapter, mode: "insensitive" } } : {}),
...(input.roleName ? { areaRole: { name: { contains: input.roleName, mode: "insensitive" } } } : {}),
...(input.roleName
? { areaRole: { name: { contains: input.roleName, mode: "insensitive" } } }
: {}),
},
},
select: {
@@ -103,9 +106,9 @@ async function queryBestProjectResource(
},
orderBy: [{ resourceId: "asc" }, { startDate: "asc" }],
});
const projectAssignments = (Array.isArray(projectAssignmentsResult)
? projectAssignmentsResult
: []) as BestProjectResourceAssignmentRecord[];
const projectAssignments = (
Array.isArray(projectAssignmentsResult) ? projectAssignmentsResult : []
) as BestProjectResourceAssignmentRecord[];
if (projectAssignments.length === 0) {
return {
@@ -154,9 +157,9 @@ async function queryBestProjectResource(
},
orderBy: [{ resourceId: "asc" }, { startDate: "asc" }],
});
const overlappingAssignments = (Array.isArray(overlappingAssignmentsResult)
? overlappingAssignmentsResult
: []) as BestProjectResourceOverlapAssignmentRecord[];
const overlappingAssignments = (
Array.isArray(overlappingAssignmentsResult) ? overlappingAssignmentsResult : []
) as BestProjectResourceOverlapAssignmentRecord[];
const assignmentsByResourceId = new Map<string, typeof overlappingAssignments>();
for (const assignment of overlappingAssignments) {
@@ -181,81 +184,90 @@ async function queryBestProjectResource(
input.endDate,
);
const candidates = resources.map((resource) => {
const availability = resource.availability as unknown as WeekdayAvailability;
const context = contexts.get(resource.id);
const assignments = assignmentsByResourceId.get(resource.id) ?? [];
const capacity = buildResourceCapacitySummary({
availability,
periodStart: input.startDate,
periodEnd: input.endDate,
context,
bookings: assignments,
});
const projectHours = (assignmentsOnProjectByResourceId.get(resource.id) ?? []).reduce(
(sum, assignment) =>
sum + calculateEffectiveBookedHours({
availability,
startDate: assignment.startDate,
endDate: assignment.endDate,
hoursPerDay: assignment.hoursPerDay,
periodStart: input.startDate,
periodEnd: input.endDate,
context,
}),
0,
);
const candidates = resources
.map((resource) => {
const availability = resource.availability as unknown as WeekdayAvailability;
const context = contexts.get(resource.id);
const assignments = assignmentsByResourceId.get(resource.id) ?? [];
const capacity = buildResourceCapacitySummary({
availability,
periodStart: input.startDate,
periodEnd: input.endDate,
context,
bookings: assignments,
});
const projectHours = (assignmentsOnProjectByResourceId.get(resource.id) ?? []).reduce(
(sum, assignment) =>
sum +
calculateEffectiveBookedHours({
availability,
startDate: assignment.startDate,
endDate: assignment.endDate,
hoursPerDay: assignment.hoursPerDay,
periodStart: input.startDate,
periodEnd: input.endDate,
context,
}),
0,
);
return {
id: resource.id,
eid: resource.eid,
name: resource.displayName,
role: resource.areaRole?.name ?? null,
chapter: resource.chapter ?? null,
country: resource.country?.name ?? resource.country?.code ?? null,
countryCode: resource.country?.code ?? null,
federalState: resource.federalState ?? null,
metroCity: resource.metroCity?.name ?? null,
lcrCents: resource.lcrCents ?? null,
lcr: resource.lcrCents != null ? fmtEur(resource.lcrCents) : null,
baseWorkingDays: capacity.baseWorkingDays,
workingDays: capacity.workingDays,
excludedCapacityDays: capacity.excludedCapacityDays,
baseAvailableHours: capacity.baseAvailableHours,
availableHours: capacity.availableHours,
bookedHours: capacity.bookedHours,
remainingHours: capacity.remainingHours,
remainingHoursPerDay: capacity.remainingHoursPerDay,
projectHours: round1(projectHours),
assignmentCount: assignments.length,
holidaySummary: {
count: capacity.holidaySummary.count,
workdayCount: capacity.holidaySummary.workdayCount,
hoursDeduction: capacity.holidaySummary.hoursDeduction,
holidayDates: capacity.holidaySummary.holidayDates,
},
absenceSummary: {
dayEquivalent: capacity.absenceSummary.dayEquivalent,
hoursDeduction: capacity.absenceSummary.hoursDeduction,
},
capacityBreakdown: capacity.capacityBreakdown,
};
}).filter((candidate) => candidate.remainingHoursPerDay >= input.minHoursPerDay);
return {
id: resource.id,
eid: resource.eid,
name: resource.displayName,
role: resource.areaRole?.name ?? null,
chapter: resource.chapter ?? null,
country: resource.country?.name ?? resource.country?.code ?? null,
countryCode: resource.country?.code ?? null,
federalState: resource.federalState ?? null,
metroCity: resource.metroCity?.name ?? null,
lcrCents: resource.lcrCents ?? null,
lcr: resource.lcrCents != null ? fmtEur(resource.lcrCents) : null,
baseWorkingDays: capacity.baseWorkingDays,
workingDays: capacity.workingDays,
excludedCapacityDays: capacity.excludedCapacityDays,
baseAvailableHours: capacity.baseAvailableHours,
availableHours: capacity.availableHours,
bookedHours: capacity.bookedHours,
remainingHours: capacity.remainingHours,
remainingHoursPerDay: capacity.remainingHoursPerDay,
projectHours: round1(projectHours),
assignmentCount: assignments.length,
holidaySummary: {
count: capacity.holidaySummary.count,
workdayCount: capacity.holidaySummary.workdayCount,
hoursDeduction: capacity.holidaySummary.hoursDeduction,
holidayDates: capacity.holidaySummary.holidayDates,
},
absenceSummary: {
dayEquivalent: capacity.absenceSummary.dayEquivalent,
hoursDeduction: capacity.absenceSummary.hoursDeduction,
},
capacityBreakdown: capacity.capacityBreakdown,
};
})
.filter((candidate) => candidate.remainingHoursPerDay >= input.minHoursPerDay);
candidates.sort((left, right) => {
if (input.rankingMode === "highest_remaining_hours_per_day") {
return right.remainingHoursPerDay - left.remainingHoursPerDay
|| right.remainingHours - left.remainingHours
|| (left.lcrCents ?? Number.MAX_SAFE_INTEGER) - (right.lcrCents ?? Number.MAX_SAFE_INTEGER);
return (
right.remainingHoursPerDay - left.remainingHoursPerDay ||
right.remainingHours - left.remainingHours ||
(left.lcrCents ?? Number.MAX_SAFE_INTEGER) - (right.lcrCents ?? Number.MAX_SAFE_INTEGER)
);
}
if (input.rankingMode === "highest_remaining_hours") {
return right.remainingHours - left.remainingHours
|| right.remainingHoursPerDay - left.remainingHoursPerDay
|| (left.lcrCents ?? Number.MAX_SAFE_INTEGER) - (right.lcrCents ?? Number.MAX_SAFE_INTEGER);
return (
right.remainingHours - left.remainingHours ||
right.remainingHoursPerDay - left.remainingHoursPerDay ||
(left.lcrCents ?? Number.MAX_SAFE_INTEGER) - (right.lcrCents ?? Number.MAX_SAFE_INTEGER)
);
}
return (left.lcrCents ?? Number.MAX_SAFE_INTEGER) - (right.lcrCents ?? Number.MAX_SAFE_INTEGER)
|| right.remainingHoursPerDay - left.remainingHoursPerDay
|| right.remainingHours - left.remainingHours;
return (
(left.lcrCents ?? Number.MAX_SAFE_INTEGER) - (right.lcrCents ?? Number.MAX_SAFE_INTEGER) ||
right.remainingHoursPerDay - left.remainingHoursPerDay ||
right.remainingHours - left.remainingHours
);
});
return {
@@ -283,7 +295,9 @@ export const staffingBestProjectResourceProcedures = {
startDate: z.coerce.date(),
endDate: z.coerce.date(),
minHoursPerDay: z.number().min(0).default(3),
rankingMode: z.enum(["lowest_lcr", "highest_remaining_hours_per_day", "highest_remaining_hours"]).default("lowest_lcr"),
rankingMode: z
.enum(["lowest_lcr", "highest_remaining_hours_per_day", "highest_remaining_hours"])
.default("lowest_lcr"),
chapter: z.string().optional(),
roleName: z.string().optional(),
}),
@@ -301,37 +315,45 @@ export const staffingBestProjectResourceProcedures = {
endDate: z.coerce.date().optional(),
durationDays: z.number().int().min(1).optional(),
minHoursPerDay: z.number().min(0).default(3),
rankingMode: z.enum(["lowest_lcr", "highest_remaining_hours_per_day", "highest_remaining_hours"]).default("lowest_lcr"),
rankingMode: z
.enum(["lowest_lcr", "highest_remaining_hours_per_day", "highest_remaining_hours"])
.default("lowest_lcr"),
chapter: z.string().optional(),
roleName: z.string().optional(),
}),
)
.query(async ({ ctx, input }) => {
requirePermission(ctx, PermissionKey.VIEW_COSTS);
const project = await findUniqueOrThrow(ctx.db.project.findUnique({
where: { id: input.projectId },
select: {
id: true,
name: true,
shortCode: true,
status: true,
responsiblePerson: true,
},
}), "Project");
const project = await findUniqueOrThrow(
ctx.db.project.findUnique({
where: { id: input.projectId },
select: {
id: true,
name: true,
shortCode: true,
status: true,
responsiblePerson: true,
},
}),
"Project",
);
const { startDate, endDate } = createDateRange({
startDate: input.startDate,
endDate: input.endDate,
durationDays: input.durationDays,
});
const result = await queryBestProjectResource(ctx.db as unknown as BestProjectResourceDbClient, {
projectId: input.projectId,
startDate,
endDate,
minHoursPerDay: input.minHoursPerDay,
rankingMode: input.rankingMode,
...(input.chapter ? { chapter: input.chapter } : {}),
...(input.roleName ? { roleName: input.roleName } : {}),
});
const result = await queryBestProjectResource(
ctx.db as unknown as BestProjectResourceDbClient,
{
projectId: input.projectId,
startDate,
endDate,
minHoursPerDay: input.minHoursPerDay,
rankingMode: input.rankingMode,
...(input.chapter ? { chapter: input.chapter } : {}),
...(input.roleName ? { roleName: input.roleName } : {}),
},
);
return {
...result,