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
@@ -7,7 +7,7 @@ import { formatCents, formatDate } from "~/lib/format.js";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { FillOpenDemandModal } from "~/components/allocations/FillOpenDemandModal.js";
import { AllocationModal } from "~/components/allocations/AllocationModal.js";
import type { AllocationWithDetails } from "@capakraken/shared";
import type { AllocationWithDetails } from "@nexus/shared";
import type { OpenDemandAssignment } from "~/components/timeline/TimelineProjectPanel.js";
import { usePermissions } from "~/hooks/usePermissions.js";
import { trpc } from "~/lib/trpc/client.js";
@@ -28,7 +28,12 @@ interface DemandRow {
unfilledHeadcount: number;
status: string;
project?: { id: string; name: string; shortCode: string };
assignments?: Array<{ dailyCostCents: number; startDate: Date | string; endDate: Date | string; status: string }>;
assignments?: Array<{
dailyCostCents: number;
startDate: Date | string;
endDate: Date | string;
status: string;
}>;
}
interface ProjectDemandsTableProps {
@@ -80,10 +85,12 @@ export function ProjectDemandsTable({ demands, project }: ProjectDemandsTablePro
<thead className="bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Role <InfoTooltip content="The role or skill profile required for this demand position." />
Role{" "}
<InfoTooltip content="The role or skill profile required for this demand position." />
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Period <InfoTooltip content="Time range during which this role is needed on the project." />
Period{" "}
<InfoTooltip content="Time range during which this role is needed on the project." />
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<span className="inline-flex items-center justify-end gap-0.5">
@@ -92,16 +99,19 @@ export function ProjectDemandsTable({ demands, project }: ProjectDemandsTablePro
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<span className="inline-flex items-center justify-end gap-0.5">
Hours/Day <InfoTooltip content="Planned working hours per day for this demand position." />
Hours/Day{" "}
<InfoTooltip content="Planned working hours per day for this demand position." />
</span>
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<span className="inline-flex items-center justify-end gap-0.5">
Budget <InfoTooltip content="Allocated role budget vs. booked cost from assignments." />
Budget{" "}
<InfoTooltip content="Allocated role budget vs. booked cost from assignments." />
</span>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status <InfoTooltip content="PROPOSED = requested, CONFIRMED = approved, ACTIVE = ongoing, COMPLETED = filled, CANCELLED = removed." />
Status{" "}
<InfoTooltip content="PROPOSED = requested, CONFIRMED = approved, ACTIVE = ongoing, COMPLETED = filled, CANCELLED = removed." />
</th>
{canEdit && (
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
@@ -112,9 +122,15 @@ export function ProjectDemandsTable({ demands, project }: ProjectDemandsTablePro
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
{allDemands.map((demand) => {
const isFillable = demand.status !== "CANCELLED" && demand.status !== "COMPLETED" && demand.unfilledHeadcount > 0;
const isFillable =
demand.status !== "CANCELLED" &&
demand.status !== "COMPLETED" &&
demand.unfilledHeadcount > 0;
return (
<tr key={demand.id} className="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors">
<tr
key={demand.id}
className="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors"
>
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
{demand.roleEntity?.name ?? demand.role ?? "Unassigned"}
</td>
@@ -125,38 +141,51 @@ export function ProjectDemandsTable({ demands, project }: ProjectDemandsTablePro
<span className="font-medium">{demand.unfilledHeadcount}</span>
<span className="text-gray-400"> / {demand.requestedHeadcount}</span>
</td>
<td className="px-4 py-3 text-sm text-right text-gray-900 dark:text-gray-100">{demand.hoursPerDay}h</td>
<td className="px-4 py-3 text-sm text-right text-gray-900 dark:text-gray-100">
{demand.hoursPerDay}h
</td>
<td className="px-4 py-3 text-right text-sm">
{demand.budgetCents && demand.budgetCents > 0 ? (() => {
// Calculate booked cost from assignments
const bookedCents = (demand.assignments ?? [])
.filter((a) => a.status !== "CANCELLED")
.reduce((sum, a) => {
const s = new Date(a.startDate);
const e = new Date(a.endDate);
let days = 0;
const cur = new Date(s);
while (cur <= e) { if (cur.getDay() !== 0 && cur.getDay() !== 6) days++; cur.setDate(cur.getDate() + 1); }
return sum + a.dailyCostCents * days;
}, 0);
const remainCents = demand.budgetCents! - bookedCents;
return (
<div>
<div className="text-gray-900 dark:text-gray-100">
{formatCents(demand.budgetCents!)} EUR
{demand.budgetCents && demand.budgetCents > 0 ? (
(() => {
// Calculate booked cost from assignments
const bookedCents = (demand.assignments ?? [])
.filter((a) => a.status !== "CANCELLED")
.reduce((sum, a) => {
const s = new Date(a.startDate);
const e = new Date(a.endDate);
let days = 0;
const cur = new Date(s);
while (cur <= e) {
if (cur.getDay() !== 0 && cur.getDay() !== 6) days++;
cur.setDate(cur.getDate() + 1);
}
return sum + a.dailyCostCents * days;
}, 0);
const remainCents = demand.budgetCents! - bookedCents;
return (
<div>
<div className="text-gray-900 dark:text-gray-100">
{formatCents(demand.budgetCents!)} EUR
</div>
<div
className={`text-xs ${remainCents < 0 ? "text-red-500" : "text-gray-400"}`}
>
{bookedCents > 0 ? `${formatCents(bookedCents)} booked` : ""}
{remainCents < 0
? ` (${formatCents(Math.abs(remainCents))} over)`
: ""}
</div>
</div>
<div className={`text-xs ${remainCents < 0 ? "text-red-500" : "text-gray-400"}`}>
{bookedCents > 0 ? `${formatCents(bookedCents)} booked` : ""}
{remainCents < 0 ? ` (${formatCents(Math.abs(remainCents))} over)` : ""}
</div>
</div>
);
})() : (
);
})()
) : (
<span className="text-gray-400 text-xs"></span>
)}
</td>
<td className="px-4 py-3">
<span className={`inline-block px-2 py-0.5 text-xs rounded-full ${ALLOC_STATUS_COLORS[demand.status] ?? "bg-gray-100 text-gray-600"}`}>
<span
className={`inline-block px-2 py-0.5 text-xs rounded-full ${ALLOC_STATUS_COLORS[demand.status] ?? "bg-gray-100 text-gray-600"}`}
>
{demand.status}
</span>
</td>
@@ -173,23 +202,35 @@ export function ProjectDemandsTable({ demands, project }: ProjectDemandsTablePro
{isFillable && (
<button
type="button"
onClick={() => setFillTarget({
id: demand.id,
projectId: demand.projectId,
roleId: demand.roleId,
role: demand.role,
headcount: demand.headcount,
...(demand.budgetCents ? { budgetCents: demand.budgetCents } : {}),
startDate: new Date(demand.startDate),
endDate: new Date(demand.endDate),
hoursPerDay: demand.hoursPerDay,
roleEntity: demand.roleEntity ?? null,
project,
})}
onClick={() =>
setFillTarget({
id: demand.id,
projectId: demand.projectId,
roleId: demand.roleId,
role: demand.role,
headcount: demand.headcount,
...(demand.budgetCents ? { budgetCents: demand.budgetCents } : {}),
startDate: new Date(demand.startDate),
endDate: new Date(demand.endDate),
hoursPerDay: demand.hoursPerDay,
roleEntity: demand.roleEntity ?? null,
project,
})
}
className="inline-flex items-center gap-1 text-xs font-medium text-brand-600 hover:text-brand-800 dark:text-brand-400 dark:hover:text-brand-200"
>
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
<svg
className="h-3.5 w-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
/>
</svg>
Assign
</button>
@@ -208,7 +249,10 @@ export function ProjectDemandsTable({ demands, project }: ProjectDemandsTablePro
{canEdit && (
<p>
Create staffing entries via{" "}
<Link href="/allocations" className="text-brand-600 hover:underline dark:text-brand-400">
<Link
href="/allocations"
className="text-brand-600 hover:underline dark:text-brand-400"
>
Allocations New Planning Entry
</Link>
.
@@ -221,16 +265,28 @@ export function ProjectDemandsTable({ demands, project }: ProjectDemandsTablePro
{fillTarget && (
<FillOpenDemandModal
allocation={fillTarget as never}
onClose={() => { setFillTarget(null); handleMutationSuccess(); }}
onSuccess={() => { setFillTarget(null); handleMutationSuccess(); }}
onClose={() => {
setFillTarget(null);
handleMutationSuccess();
}}
onSuccess={() => {
setFillTarget(null);
handleMutationSuccess();
}}
/>
)}
{editTarget && (
<AllocationModal
allocation={editTarget}
onClose={() => { setEditTarget(null); handleMutationSuccess(); }}
onSuccess={() => { setEditTarget(null); handleMutationSuccess(); }}
onClose={() => {
setEditTarget(null);
handleMutationSuccess();
}}
onSuccess={() => {
setEditTarget(null);
handleMutationSuccess();
}}
/>
)}
</>