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
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user