feat(platform): checkpoint current implementation state

This commit is contained in:
2026-04-01 07:42:03 +02:00
parent 3e53471f05
commit 8c5be51251
125 changed files with 10269 additions and 17808 deletions
@@ -42,6 +42,19 @@ type BudgetForecastRow = {
pctUsed: number;
activeAssignmentCount?: number;
calendarLocations?: BudgetForecastLocation[];
derivation?: {
periodStart: string;
periodEnd: string;
calendarContextCount: number;
holidayAwareAssignmentCount: number;
fallbackAssignmentCount: number;
baseBurnRateCents: number;
adjustedBurnRateCents: number;
publicHolidayDayEquivalent: number;
publicHolidayCostDeductionCents: number;
absenceDayEquivalent: number;
absenceCostDeductionCents: number;
} | null;
};
function formatCurrency(cents: number | undefined): string {
@@ -49,6 +62,11 @@ function formatCurrency(cents: number | undefined): string {
return `${(cents / 100).toLocaleString("de-DE", { maximumFractionDigits: 0 })} €`;
}
function formatDayEquivalent(value: number | undefined): string {
if (value === undefined) return "—";
return Number.isInteger(value) ? String(value) : value.toFixed(1);
}
function formatLocation(location: BudgetForecastLocation): string {
const parts = [
location.countryCode ?? location.countryName ?? null,
@@ -65,7 +83,7 @@ function SummaryCard({
}: {
label: string;
value: string;
helper: string;
helper?: string;
}) {
return (
<div className="rounded-2xl border border-gray-200 bg-gray-50/80 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/40">
@@ -73,7 +91,9 @@ function SummaryCard({
{label}
</div>
<div className="mt-1 text-sm font-semibold text-gray-900 dark:text-gray-100">{value}</div>
<div className="mt-0.5 text-[10px] leading-4 text-gray-500 dark:text-gray-400">{helper}</div>
{helper ? (
<div className="mt-0.5 text-[10px] leading-4 text-gray-500 dark:text-gray-400">{helper}</div>
) : null}
</div>
);
}
@@ -113,6 +133,9 @@ export function BudgetForecastWidget({ config, onConfigChange }: WidgetProps) {
acc.remainingCents += row.remainingCents ?? Math.max(0, row.budgetCents - row.spentCents);
acc.burnRate += row.burnRate;
acc.activeAssignmentCount += row.activeAssignmentCount ?? 0;
acc.baseBurnRateCents += row.derivation?.baseBurnRateCents ?? row.burnRate;
acc.publicHolidayCostDeductionCents += row.derivation?.publicHolidayCostDeductionCents ?? 0;
acc.absenceCostDeductionCents += row.derivation?.absenceCostDeductionCents ?? 0;
return acc;
}, {
budgetCents: 0,
@@ -120,6 +143,9 @@ export function BudgetForecastWidget({ config, onConfigChange }: WidgetProps) {
remainingCents: 0,
burnRate: 0,
activeAssignmentCount: 0,
baseBurnRateCents: 0,
publicHolidayCostDeductionCents: 0,
absenceCostDeductionCents: 0,
}), [rows]);
if (isLoading && !data) {
@@ -154,22 +180,26 @@ export function BudgetForecastWidget({ config, onConfigChange }: WidgetProps) {
<SummaryCard
label="Projects"
value={String(rows.length)}
helper={`${totals.activeAssignmentCount} active assignments in scope`}
{...(showDetails ? { helper: `${totals.activeAssignmentCount} active assignments in scope` } : {})}
/>
<SummaryCard
label="Budget"
value={formatCurrency(totals.budgetCents)}
helper={`${formatCurrency(totals.spentCents)} spent`}
{...(showDetails ? { helper: `${formatCurrency(totals.spentCents)} spent` } : {})}
/>
<SummaryCard
label="Remaining"
value={formatCurrency(totals.remainingCents)}
helper={`${rows.filter((row) => row.remainingCents !== undefined && row.remainingCents <= 0).length} exhausted`}
{...(showDetails
? { helper: `${rows.filter((row) => row.remainingCents !== undefined && row.remainingCents <= 0).length} exhausted` }
: {})}
/>
<SummaryCard
label="Burn / Month"
value={formatCurrency(totals.burnRate)}
helper="Holiday- and absence-adjusted active burn"
{...(showDetails ? {
helper: `Base ${formatCurrency(totals.baseBurnRateCents)} · Hol -${formatCurrency(totals.publicHolidayCostDeductionCents)} · Abs -${formatCurrency(totals.absenceCostDeductionCents)}`,
} : {})}
/>
</div>
<div className="overflow-auto flex-1">
@@ -200,15 +230,21 @@ export function BudgetForecastWidget({ config, onConfigChange }: WidgetProps) {
</div>
<div className="mt-1 text-[10px] font-normal leading-4 text-gray-500 dark:text-gray-400">
{row.clientName ?? "No client"}
{!showDetails && row.calendarLocations && row.calendarLocations.length > 0
? ` · ${formatLocation(row.calendarLocations[0]!)}`
: ""}
</div>
{showDetails ? (
<div className="mt-1 space-y-1 text-[10px] font-normal leading-4 text-gray-500 dark:text-gray-400">
<div className="grid gap-x-3 gap-y-0.5 sm:grid-cols-2">
<div>{row.activeAssignmentCount ?? 0} active assignments</div>
<div>Remaining {formatCurrency(row.remainingCents ?? Math.max(0, row.budgetCents - row.spentCents))}</div>
{row.derivation ? (
<>
<div>{row.derivation.calendarContextCount} calendar bases</div>
<div>
{row.derivation.holidayAwareAssignmentCount} holiday-aware
{row.derivation.fallbackAssignmentCount > 0 ? ` · ${row.derivation.fallbackAssignmentCount} fallback` : ""}
</div>
</>
) : null}
</div>
<div className="flex flex-wrap gap-1">
{row.calendarLocations && row.calendarLocations.length > 0 ? (
@@ -254,7 +290,22 @@ export function BudgetForecastWidget({ config, onConfigChange }: WidgetProps) {
</div>
{showDetails ? (
<div className="mt-1 space-y-0.5 text-[10px] leading-4 text-gray-500 dark:text-gray-400">
<div>{row.activeAssignmentCount ?? 0} active assignments</div>
{row.derivation ? (
<>
<div>
Base {formatCurrency(row.derivation.baseBurnRateCents)} {"->"} Adj {formatCurrency(row.derivation.adjustedBurnRateCents)}
</div>
<div>
Hol -{formatCurrency(row.derivation.publicHolidayCostDeductionCents)} ({formatDayEquivalent(row.derivation.publicHolidayDayEquivalent)}d) · Abs -{formatCurrency(row.derivation.absenceCostDeductionCents)} ({formatDayEquivalent(row.derivation.absenceDayEquivalent)}d)
</div>
<div>
{row.derivation.holidayAwareAssignmentCount} holiday-aware assignment{row.derivation.holidayAwareAssignmentCount === 1 ? "" : "s"}
{row.derivation.fallbackAssignmentCount > 0 ? ` · ${row.derivation.fallbackAssignmentCount} fallback` : ""}
</div>
</>
) : (
<div>{row.activeAssignmentCount ?? 0} active assignments</div>
)}
{(row.calendarLocations ?? []).slice(0, 3).map((location) => (
<div key={`${location.countryCode ?? location.countryName ?? "na"}:${location.federalState ?? "na"}:${location.metroCityName ?? "na"}`}>
{formatLocation(location)} · {location.activeAssignmentCount ?? 0}x · {formatCurrency(location.burnRateCents)}
@@ -53,6 +53,17 @@ function formatDemandSource(source: DemandDerivation["demandSource"] | undefined
return "No demand basis";
}
function renderCalendarBasis(derivation: DemandDerivation): string {
if (derivation.calendarLocations.length === 0) {
return "No location-based booking basis";
}
return derivation.calendarLocations
.slice(0, 2)
.map((location) => `${formatLocation(location)} (${formatHours(location.allocatedHours)})`)
.join(" · ");
}
export function DemandWidget({ config, onConfigChange }: WidgetProps) {
const showDetails = config.showDetails === true;
const groupBy = (config.groupBy as GroupBy) || "project";
@@ -198,21 +209,15 @@ export function DemandWidget({ config, onConfigChange }: WidgetProps) {
row.name
)}
</div>
{showDetails && groupBy === "project" && row.derivation ? (
{showDetails && row.derivation ? (
<div className="mt-1 space-y-0.5 text-[10px] leading-4 text-gray-500">
<div>
{row.derivation.periodStart} to {row.derivation.periodEnd}
</div>
<div>
{row.derivation.calendarLocations.length > 0
? row.derivation.calendarLocations
.slice(0, 2)
.map((location) =>
`${formatLocation(location)} (${formatHours(location.allocatedHours)})`,
)
.join(" · ")
: "No location-based booking basis"}
</div>
<div>{renderCalendarBasis(row.derivation)}</div>
{groupBy !== "project" ? (
<div>{formatHours(row.derivation.periodWorkingHoursBase)} per 1.0 FTE base</div>
) : null}
{row.derivation.calendarLocations.length > 2 ? (
<div>+ {row.derivation.calendarLocations.length - 2} more calendar contexts</div>
) : null}
@@ -221,7 +226,7 @@ export function DemandWidget({ config, onConfigChange }: WidgetProps) {
</td>
<td className="px-3 py-2 text-right align-top">
<div className="text-gray-700">{row.allocatedHours}h</div>
{showDetails && groupBy === "project" && row.derivation ? (
{showDetails && row.derivation ? (
<div className="mt-1 space-y-0.5 text-[10px] leading-4 text-gray-500">
<div>{row.derivation.calendarLocations.length} calendar basis{row.derivation.calendarLocations.length === 1 ? "" : "es"}</div>
<div>{row.resourceCount} resource{row.resourceCount === 1 ? "" : "s"} in scope</div>
@@ -262,7 +267,7 @@ export function DemandWidget({ config, onConfigChange }: WidgetProps) {
)}
<td className="px-3 py-2 text-right align-top text-gray-500">
<div>{row.resourceCount}</div>
{showDetails && groupBy === "project" && row.derivation?.calendarLocations.length ? (
{showDetails && row.derivation?.calendarLocations.length ? (
<div className="mt-1 text-[10px] leading-4 text-gray-500">
{row.derivation.calendarLocations.reduce((sum, location) => sum + location.resourceCount, 0)} resource entries across locations
</div>
@@ -7,10 +7,23 @@ type PeakTimesChartRow = {
label: string;
bookedHours: number;
capacityHours: number;
baseAvailableHours: number;
holidayHoursDeduction: number;
absenceDayEquivalent: number;
absenceHoursDeduction: number;
utilizationPct: number;
remainingHours: number;
overbookedHours: number;
isCurrentPeriod: boolean;
calendarContextCount: number;
calendarLocations: Array<{
countryCode: string | null;
countryName: string | null;
federalState: string | null;
metroCityName: string | null;
resourceCount: number;
effectiveAvailableHours: number;
}>;
};
interface PeakTimesChartProps {
@@ -26,6 +39,16 @@ function formatHours(value: number): string {
}).format(value);
}
function formatDayEquivalent(value: number): string {
return Number.isInteger(value) ? value.toFixed(0) : value.toFixed(1);
}
function formatLocation(input: PeakTimesChartRow["calendarLocations"][number]): string {
const parts = [input.countryCode ?? input.countryName, input.federalState, input.metroCityName]
.filter((part): part is string => Boolean(part));
return parts.length > 0 ? parts.join(" / ") : "No calendar context";
}
function utilizationBarTone(utilizationPct: number): string {
if (utilizationPct > 100) return "bg-red-500";
if (utilizationPct > 75) return "bg-emerald-500";
@@ -132,7 +155,19 @@ export default function PeakTimesChart({
key={row.period}
type="button"
className="group flex h-full min-w-0 flex-col items-center rounded-2xl px-1 text-left transition-colors"
title={`${row.label}: ${row.utilizationPct}% utilization, ${formatHours(row.bookedHours)}h booked, ${formatHours(row.capacityHours)}h capacity, ${formatHours(row.remainingHours)}h free, ${formatHours(row.overbookedHours)}h overbooked`}
title={[
`${row.label}: ${row.utilizationPct}% utilization`,
`${formatHours(row.bookedHours)}h booked`,
`${formatHours(row.capacityHours)}h effective capacity`,
`${formatHours(row.baseAvailableHours)}h base`,
`${formatHours(row.holidayHoursDeduction)}h holidays`,
`${formatHours(row.absenceHoursDeduction)}h absences (${formatDayEquivalent(row.absenceDayEquivalent)}d)`,
`${row.calendarContextCount} calendar base${row.calendarContextCount === 1 ? "" : "s"}`,
...row.calendarLocations.slice(0, 3).map((location) =>
`${formatLocation(location)}: ${location.resourceCount}x, ${formatHours(location.effectiveAvailableHours)}h capacity`),
`${formatHours(row.remainingHours)}h free`,
`${formatHours(row.overbookedHours)}h overbooked`,
].join(", ")}
onMouseEnter={() => setHoveredPeriod(row.period)}
onMouseLeave={() => setHoveredPeriod((current) => (current === row.period ? null : current))}
onClick={() => onSelectedPeriodChange?.(row.period)}
@@ -10,6 +10,51 @@ import { WidgetFilterBar, type WidgetFilter } from "~/components/dashboard/Widge
import { useWidgetFilterOptions } from "~/hooks/useWidgetFilterOptions.js";
import { formatMoney } from "~/lib/format.js";
type ProjectHealthRow = {
id: string;
projectName: string;
shortCode: string;
status: string;
clientId: string | null;
clientName: string | null;
budgetHealth: number;
staffingHealth: number;
timelineHealth: number;
compositeScore: number;
budgetCents?: number | null;
spentCents?: number;
remainingBudgetCents?: number | null;
budgetUtilizationPercent?: number | null;
demandHeadcountTotal?: number;
demandHeadcountFilled?: number;
demandHeadcountOpen?: number;
demandRequirementCount?: number;
plannedEndDate?: string | Date | null;
daysUntilEndDate?: number | null;
timelineStatus?: "ON_TRACK" | "DUE_SOON" | "OVERDUE" | "UNSCHEDULED" | null;
calendarLocations?: Array<{
countryCode?: string | null;
countryName?: string | null;
federalState?: string | null;
metroCityName?: string | null;
assignmentCount: number;
spentCents: number;
}>;
derivation?: {
periodStart: string;
periodEnd: string;
calendarContextCount: number;
holidayAwareAssignmentCount: number;
fallbackAssignmentCount: number;
baseSpentCents: number;
adjustedSpentCents: number;
publicHolidayDayEquivalent: number;
publicHolidayCostDeductionCents: number;
absenceDayEquivalent: number;
absenceCostDeductionCents: number;
} | null;
};
function healthDot(value: number): string {
if (value >= 70) return "bg-green-500";
if (value >= 40) return "bg-amber-400";
@@ -69,6 +114,11 @@ function formatLocation(location: {
return parts.length > 0 ? parts.join(" / ") : "No calendar context";
}
function formatDayEquivalent(value?: number | null): string {
if (value == null) return "—";
return Number.isInteger(value) ? String(value) : value.toFixed(1);
}
export function ProjectHealthWidget({ config, onConfigChange }: WidgetProps) {
const showDetails = config.showDetails === true;
const { clients } = useWidgetFilterOptions({ clients: true });
@@ -90,7 +140,7 @@ export function ProjectHealthWidget({ config, onConfigChange }: WidgetProps) {
const clientId = (config.clientId as string) ?? "";
const rows = useMemo(() => {
const all = data ?? [];
const all = (data ?? []) as ProjectHealthRow[];
return all.filter((r) => {
if (search && !r.projectName.toLowerCase().includes(search) && !r.shortCode.toLowerCase().includes(search)) return false;
if (clientId && r.clientId !== clientId) return false;
@@ -174,6 +224,22 @@ export function ProjectHealthWidget({ config, onConfigChange }: WidgetProps) {
<div>
Timeline: {formatShortDate(row.plannedEndDate)} · {formatTimeline(row.daysUntilEndDate, row.timelineStatus)}
</div>
{row.derivation ? (
<>
<div>
Spend basis: {row.derivation.calendarContextCount} calendar bases · {row.derivation.holidayAwareAssignmentCount} holiday-aware
{row.derivation.fallbackAssignmentCount > 0 ? ` · ${row.derivation.fallbackAssignmentCount} fallback` : ""}
</div>
<div>
Base {formatMoney(row.derivation.baseSpentCents)} {"->"} Effective {formatMoney(row.derivation.adjustedSpentCents)}
</div>
<div>
Holidays -{formatMoney(row.derivation.publicHolidayCostDeductionCents)} ({formatDayEquivalent(row.derivation.publicHolidayDayEquivalent)}d)
{" · "}
Absence -{formatMoney(row.derivation.absenceCostDeductionCents)} ({formatDayEquivalent(row.derivation.absenceDayEquivalent)}d)
</div>
</>
) : null}
{(row.calendarLocations ?? []).length > 0 ? (
<div>
Calendar basis: {(row.calendarLocations ?? [])