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:
@@ -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();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user