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
@@ -4,8 +4,8 @@ import { useState, useEffect, useMemo } from "react";
import { useDebounce } from "~/hooks/useDebounce.js";
import { AnimatedModal } from "~/components/ui/AnimatedModal.js";
import { useInvalidatePlanningViews } from "~/hooks/useInvalidatePlanningViews.js";
import { AllocationStatus } from "@capakraken/shared";
import type { AllocationWithDetails, RecurrencePattern } from "@capakraken/shared";
import { AllocationStatus } from "@nexus/shared";
import type { AllocationWithDetails, RecurrencePattern } from "@nexus/shared";
import { trpc } from "~/lib/trpc/client.js";
import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js";
import { toDateInputValue } from "~/lib/format.js";
@@ -26,7 +26,8 @@ interface AllocationModalProps {
export function AllocationModal({ allocation, onClose, onSuccess }: AllocationModalProps) {
const isEditing = Boolean(allocation);
const initialEntryKind: EntryKind = allocation && !allocation.resourceId ? "demand" : "assignment";
const initialEntryKind: EntryKind =
allocation && !allocation.resourceId ? "demand" : "assignment";
const [entryKind, setEntryKind] = useState<EntryKind>(initialEntryKind);
const isDemandEntry = entryKind === "demand";
@@ -57,14 +58,8 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
{ isActive: true, limit: 500 },
{ staleTime: 60_000 },
);
const { data: projects } = trpc.project.list.useQuery(
{ limit: 500 },
{ staleTime: 60_000 },
);
const { data: rolesData } = trpc.role.list.useQuery(
{ isActive: true },
{ staleTime: 60_000 },
);
const { data: projects } = trpc.project.list.useQuery({ limit: 500 }, { staleTime: 60_000 });
const { data: rolesData } = trpc.role.list.useQuery({ isActive: true }, { staleTime: 60_000 });
// Fetch existing allocations for the selected resource+project to detect overlaps
const shouldCheckOverlap = !isDemandEntry && !!resourceId && !!projectId;
@@ -85,20 +80,26 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
const shouldCheckConflicts =
!isDemandEntry &&
!!debouncedResourceId &&
conflictCheckStart !== null && !isNaN(conflictCheckStart.getTime()) &&
conflictCheckEnd !== null && !isNaN(conflictCheckEnd.getTime()) &&
conflictCheckStart !== null &&
!isNaN(conflictCheckStart.getTime()) &&
conflictCheckEnd !== null &&
!isNaN(conflictCheckEnd.getTime()) &&
debouncedHoursPerDay > 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data: conflictResult, isFetching: checkingConflicts } = (trpc.allocation.checkConflicts.useQuery as any)(
{
resourceId: debouncedResourceId,
startDate: conflictCheckStart,
endDate: conflictCheckEnd,
hoursPerDay: debouncedHoursPerDay,
excludeAssignmentId: isEditing && allocation?.id ? allocation.id : undefined,
},
{ enabled: shouldCheckConflicts, staleTime: 15_000 },
) as { data: import("@capakraken/shared").AllocationConflictCheckResult | undefined; isFetching: boolean };
const { data: conflictResult, isFetching: checkingConflicts } =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(trpc.allocation.checkConflicts.useQuery as any)(
{
resourceId: debouncedResourceId,
startDate: conflictCheckStart,
endDate: conflictCheckEnd,
hoursPerDay: debouncedHoursPerDay,
excludeAssignmentId: isEditing && allocation?.id ? allocation.id : undefined,
},
{ enabled: shouldCheckConflicts, staleTime: 15_000 },
) as {
data: import("@nexus/shared").AllocationConflictCheckResult | undefined;
isFetching: boolean;
};
const overlapWarning = useMemo(() => {
if (!shouldCheckOverlap || !existingAllocations || !startDate || !endDate) return null;
@@ -106,7 +107,17 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
const formEnd = new Date(endDate);
if (isNaN(formStart.getTime()) || isNaN(formEnd.getTime())) return null;
const allocList = (existingAllocations as { allocations?: Array<{ id: string; resourceId?: string | null; startDate: string | Date; endDate: string | Date }> }).allocations ?? [];
const allocList =
(
existingAllocations as {
allocations?: Array<{
id: string;
resourceId?: string | null;
startDate: string | Date;
endDate: string | Date;
}>;
}
).allocations ?? [];
for (const existing of allocList) {
// Skip the allocation being edited
if (isEditing && allocation && existing.id === allocation.id) continue;
@@ -121,7 +132,15 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
}
}
return null;
}, [shouldCheckOverlap, existingAllocations, startDate, endDate, isEditing, allocation, resourceId]);
}, [
shouldCheckOverlap,
existingAllocations,
startDate,
endDate,
isEditing,
allocation,
resourceId,
]);
const invalidatePlanningViews = useInvalidatePlanningViews();
@@ -185,7 +204,17 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
useEffect(() => {
setServerError(null);
setOverbookingAcknowledged(false);
}, [resourceId, projectId, roleId, roleFreeText, startDate, endDate, hoursPerDay, status, entryKind]);
}, [
resourceId,
projectId,
roleId,
roleFreeText,
startDate,
endDate,
hoursPerDay,
status,
entryKind,
]);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
@@ -222,7 +251,7 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
// Determine role string from roleId if set
const rolesList = rolesData ?? [];
const selectedRole = rolesList.find((r) => r.id === roleId);
const roleString = selectedRole ? selectedRole.name : (roleFreeText || undefined);
const roleString = selectedRole ? selectedRole.name : roleFreeText || undefined;
const percentage = Math.min(100, Math.round((hoursPerDay / 8) * 100));
@@ -230,12 +259,14 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
updateMutation.mutate({
id: getPlanningEntryMutationId(allocation),
data: {
resourceId: isDemandEntry ? undefined : (resourceId || undefined),
resourceId: isDemandEntry ? undefined : resourceId || undefined,
projectId,
role: roleString,
roleId: roleId || undefined,
headcount: isDemandEntry ? headcount : 1,
...(isDemandEntry && budgetEur ? { budgetCents: Math.round(parseFloat(budgetEur) * 100) } : {}),
...(isDemandEntry && budgetEur
? { budgetCents: Math.round(parseFloat(budgetEur) * 100) }
: {}),
startDate: start,
endDate: end,
hoursPerDay,
@@ -279,18 +310,22 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
"w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm dark:bg-gray-900 dark:text-gray-100";
const labelClass = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1";
const resourceList = (resources?.resources ?? []) as Array<{ id: string; displayName: string; eid: string }>;
const projectList = (projects?.projects ?? []) as Array<{ id: string; name: string; shortCode: string }>;
const resourceList = (resources?.resources ?? []) as Array<{
id: string;
displayName: string;
eid: string;
}>;
const projectList = (projects?.projects ?? []) as Array<{
id: string;
name: string;
shortCode: string;
}>;
const rolesList = (rolesData ?? []) as Array<{ id: string; name: string; color: string | null }>;
const entryLabel = isDemandEntry ? "Open Demand" : "Assignment";
return (
<AnimatedModal open={true} onClose={onClose} maxWidth="max-w-xl" className="mx-4">
<div
role="dialog"
aria-modal="true"
data-testid="allocation-modal"
>
<div role="dialog" aria-modal="true" data-testid="allocation-modal">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
@@ -333,7 +368,9 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
{isDemandEntry && (
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<label className="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap">Headcount:</label>
<label className="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap">
Headcount:
</label>
<input
type="number"
value={headcount}
@@ -344,7 +381,9 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
/>
</div>
<div className="flex items-center gap-2">
<label className="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap">Budget (EUR):</label>
<label className="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap">
Budget (EUR):
</label>
<input
type="number"
value={budgetEur}
@@ -363,7 +402,8 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
{!isDemandEntry && (
<div>
<label htmlFor="modal-resource" className={labelClass}>
Resource <span className="text-red-500">*</span><InfoTooltip content="The person to assign. Their LCR determines the daily cost of this allocation." />
Resource <span className="text-red-500">*</span>
<InfoTooltip content="The person to assign. Their LCR determines the daily cost of this allocation." />
</label>
<select
id="modal-resource"
@@ -385,7 +425,8 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
{/* Project */}
<div>
<label htmlFor="modal-project" className={labelClass}>
Project <span className="text-red-500">*</span><InfoTooltip content="The project this time block is allocated to. Costs roll up to the project budget." />
Project <span className="text-red-500">*</span>
<InfoTooltip content="The project this time block is allocated to. Costs roll up to the project budget." />
</label>
<select
id="modal-project"
@@ -405,7 +446,10 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
{/* Role */}
<div>
<label htmlFor="modal-role" className={labelClass}>Role<InfoTooltip content="Role for this allocation. Pick a predefined role or type a custom one." /></label>
<label htmlFor="modal-role" className={labelClass}>
Role
<InfoTooltip content="Role for this allocation. Pick a predefined role or type a custom one." />
</label>
<select
id="modal-role"
value={roleId}
@@ -434,35 +478,43 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
{/* Dates */}
<div>
<div className="flex items-center justify-between mb-1">
<span className={labelClass}>Date Range <span className="text-red-500">*</span></span>
<DateRangePresets onSelect={(s, e) => { setStartDate(s); setEndDate(e); }} />
<span className={labelClass}>
Date Range <span className="text-red-500">*</span>
</span>
<DateRangePresets
onSelect={(s, e) => {
setStartDate(s);
setEndDate(e);
}}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="modal-start" className={labelClass}>
Start Date <InfoTooltip content="First day of this allocation period (inclusive)." />
</label>
<DateInput
id="modal-start"
value={startDate}
onChange={setStartDate}
className={inputClass}
required
/>
</div>
<div>
<label htmlFor="modal-end" className={labelClass}>
End Date <InfoTooltip content="Last day of this allocation period (inclusive)." />
</label>
<DateInput
id="modal-end"
value={endDate}
onChange={setEndDate}
min={startDate}
className={inputClass}
required
/>
</div>
<div>
<label htmlFor="modal-start" className={labelClass}>
Start Date{" "}
<InfoTooltip content="First day of this allocation period (inclusive)." />
</label>
<DateInput
id="modal-start"
value={startDate}
onChange={setStartDate}
className={inputClass}
required
/>
</div>
<div>
<label htmlFor="modal-end" className={labelClass}>
End Date <InfoTooltip content="Last day of this allocation period (inclusive)." />
</label>
<DateInput
id="modal-end"
value={endDate}
onChange={setEndDate}
min={startDate}
className={inputClass}
required
/>
</div>
</div>
</div>
@@ -470,7 +522,8 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="modal-hours" className={labelClass}>
Hours / Day<InfoTooltip content="Working hours per day. Total cost = LCR x hours/day x working days. Vacation days are excluded." />
Hours / Day
<InfoTooltip content="Working hours per day. Total cost = LCR x hours/day x working days. Vacation days are excluded." />
</label>
<input
id="modal-hours"
@@ -485,7 +538,8 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
</div>
<div>
<label htmlFor="modal-status" className={labelClass}>
Status<InfoTooltip content="PROPOSED = draft/request · CONFIRMED = approved · ACTIVE = in progress · COMPLETED = done · CANCELLED = removed." />
Status
<InfoTooltip content="PROPOSED = draft/request · CONFIRMED = approved · ACTIVE = in progress · COMPLETED = done · CANCELLED = removed." />
</label>
<select
id="modal-status"
@@ -514,7 +568,10 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
}}
className="rounded border-gray-300 dark:border-gray-600"
/>
<span className="font-medium text-gray-700 dark:text-gray-300">Recurring schedule</span><InfoTooltip content="Enable to repeat this allocation on specific days (e.g. every Monday/Wednesday). Hours per day applies on active days only." />
<span className="font-medium text-gray-700 dark:text-gray-300">
Recurring schedule
</span>
<InfoTooltip content="Enable to repeat this allocation on specific days (e.g. every Monday/Wednesday). Hours per day applies on active days only." />
</label>
{isRecurring && (
<div className="mt-2">
@@ -548,7 +605,12 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
)}
{!conflictResult && checkingConflicts && (
<ConflictWarningPanel
result={{ isOverbooking: false, overbooking: null, vacationOverlap: [], hasVacationOverlap: false }}
result={{
isOverbooking: false,
overbooking: null,
vacationOverlap: [],
hasVacationOverlap: false,
}}
isLoading={true}
acknowledged={false}
onAcknowledge={() => {}}
@@ -568,7 +630,11 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
<button
type="submit"
disabled={isPending || hasUnacknowledgedOverbooking}
title={hasUnacknowledgedOverbooking ? "Acknowledge the overbooking warning above to proceed" : undefined}
title={
hasUnacknowledgedOverbooking
? "Acknowledge the overbooking warning above to proceed"
: undefined
}
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50"
>
{isPending ? "Saving…" : "Save"}