"use client"; import { useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; 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 { OpenDemandAssignment } from "~/components/timeline/TimelineProjectPanel.js"; import { usePermissions } from "~/hooks/usePermissions.js"; import { trpc } from "~/lib/trpc/client.js"; import { ALLOCATION_STATUS_BADGE as ALLOC_STATUS_COLORS } from "~/lib/status-styles.js"; interface DemandRow { id: string; projectId: string; role: string | null; roleId: string | null; roleEntity?: { id: string; name: string; color: string | null } | null; startDate: Date | string; endDate: Date | string; hoursPerDay: number; headcount: number; budgetCents?: number; requestedHeadcount: number; unfilledHeadcount: number; status: string; project?: { id: string; name: string; shortCode: string }; assignments?: Array<{ dailyCostCents: number; startDate: Date | string; endDate: Date | string; status: string }>; } interface ProjectDemandsTableProps { demands: DemandRow[]; project: { id: string; name: string; shortCode: string }; } export function ProjectDemandsTable({ demands, project }: ProjectDemandsTableProps) { const [fillTarget, setFillTarget] = useState(null); const [editTarget, setEditTarget] = useState(null); const { canEdit } = usePermissions(); const router = useRouter(); const utils = trpc.useUtils(); function handleMutationSuccess() { // Invalidate budget status so BudgetStatusCard refetches void utils.timeline.getBudgetStatus.invalidate(); void utils.allocation.listView.invalidate(); router.refresh(); } const activeDemands = demands.filter((d) => d.status !== "CANCELLED" && d.status !== "COMPLETED"); const allDemands = demands; return ( <>

Open Demands ({allDemands.length})

{activeDemands.length > 0 && ( {activeDemands.reduce((sum, d) => sum + d.unfilledHeadcount, 0)} seats unfilled )} {canEdit && ( + New in Allocations )}
{canEdit && ( )} {allDemands.map((demand) => { const isFillable = demand.status !== "CANCELLED" && demand.status !== "COMPLETED" && demand.unfilledHeadcount > 0; return ( {canEdit && ( )} ); })}
Role Period Headcount Hours/Day Budget Status Actions
{demand.roleEntity?.name ?? demand.role ?? "Unassigned"} {formatDate(demand.startDate)} → {formatDate(demand.endDate)} {demand.unfilledHeadcount} / {demand.requestedHeadcount} {demand.hoursPerDay}h {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 (
{formatCents(demand.budgetCents!)} EUR
{bookedCents > 0 ? `${formatCents(bookedCents)} booked` : ""} {remainCents < 0 ? ` (${formatCents(Math.abs(remainCents))} over)` : ""}
); })() : ( )}
{demand.status}
{isFillable && ( )}
{allDemands.length === 0 && (

No open demands for this project.

{canEdit && (

Create staffing entries via{" "} Allocations → New Planning Entry .

)}
)}
{fillTarget && ( { setFillTarget(null); handleMutationSuccess(); }} onSuccess={() => { setFillTarget(null); handleMutationSuccess(); }} /> )} {editTarget && ( { setEditTarget(null); handleMutationSuccess(); }} onSuccess={() => { setEditTarget(null); handleMutationSuccess(); }} /> )} ); }