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
@@ -1,5 +1,6 @@
"use client";
import { GERMAN_FEDERAL_STATES } from "@capakraken/shared";
import { createPortal } from "react-dom";
import { formatCents, formatDateLong } from "~/lib/format.js";
@@ -33,11 +34,73 @@ export type VacationHoverData = {
startDate: Date | string;
endDate: Date | string;
note?: string | null;
scope?: string | null;
calendarName?: string | null;
sourceType?: string | null;
countryCode?: string | null;
countryName?: string | null;
federalState?: string | null;
metroCityName?: string | null;
requestedBy?: { name?: string | null; email: string } | null;
approvedBy?: { name?: string | null; email: string } | null;
approvedAt?: Date | string | null;
};
function formatHolidayScope(scope?: string | null): string | null {
switch (scope) {
case "COUNTRY":
return "Country";
case "STATE":
return "State";
case "CITY":
return "City";
default:
return scope ? scope.replaceAll("_", " ") : null;
}
}
function formatHolidaySourceType(sourceType?: string | null): string | null {
switch (sourceType) {
case "SYSTEM":
return "Built-in";
case "CALENDAR":
return "Calendar";
default:
return sourceType ? sourceType.replaceAll("_", " ") : null;
}
}
function formatHolidayStateName(
federalState?: string | null,
countryCode?: string | null,
): string | null {
if (!federalState) {
return null;
}
if (countryCode === "DE") {
return GERMAN_FEDERAL_STATES[federalState] ?? federalState;
}
return federalState;
}
function buildHolidayLocationBasis(vacation: VacationHoverData): string | null {
const parts = [
vacation.countryName
? `Country: ${vacation.countryName}`
: vacation.countryCode
? `Country: ${vacation.countryCode}`
: null,
formatHolidayStateName(vacation.federalState, vacation.countryCode)
? `State: ${formatHolidayStateName(vacation.federalState, vacation.countryCode)}`
: null,
vacation.metroCityName ? `City: ${vacation.metroCityName}` : null,
].filter(Boolean);
return parts.length > 0 ? parts.join(" · ") : null;
}
export type DemandHoverData = {
roleName: string;
roleColor: string;
@@ -67,6 +130,142 @@ interface TimelineTooltipProps {
demandHover?: DemandHoverData | null;
}
function renderTooltipPortal(content: React.ReactNode) {
return typeof document === "undefined" ? content : createPortal(content, document.body);
}
function TooltipSurface({
children,
position,
tooltipRef,
className,
dataTestId,
backgroundColor,
}: {
children: React.ReactNode;
position: { left: number; top: number };
tooltipRef?: React.Ref<HTMLDivElement>;
className: string;
dataTestId?: string;
backgroundColor: string;
}) {
return (
<div
ref={tooltipRef}
data-testid={dataTestId}
style={{
left: position.left,
top: position.top,
backgroundColor,
}}
className={className}
>
{children}
</div>
);
}
function TooltipMetric({
label,
value,
valueClassName = "font-medium text-gray-100",
}: {
label: string;
value: React.ReactNode;
valueClassName?: string;
}) {
return (
<div>
<div className="text-gray-500">{label}</div>
<div className={valueClassName}>{value}</div>
</div>
);
}
function HeatmapBreakdownList({ breakdown }: { breakdown: HeatmapHoverData["breakdown"] }) {
if (breakdown.length === 0) {
return <div className="text-[11px] text-gray-400">No bookings on this day.</div>;
}
return (
<>
{breakdown.slice(0, 6).map((entry) => (
<div
key={`${entry.projectId}-${entry.shortCode}`}
className="flex items-start justify-between gap-3"
>
<div className="min-w-0">
<div className="truncate font-medium text-white">
{entry.shortCode ? `${entry.shortCode} · ` : ""}
{entry.projectName}
</div>
<div className="truncate text-[11px] text-gray-400">
{[
entry.role,
entry.responsiblePerson ? `Lead: ${entry.responsiblePerson}` : null,
entry.orderType,
].filter(Boolean).join(" · ")}
</div>
{entry.startDate && entry.endDate ? (
<div className="text-[10px] text-gray-500">
{entry.startDate} {entry.endDate}
{entry.status && entry.status !== "CONFIRMED" ? (
<span className="ml-1 uppercase text-amber-400">{entry.status}</span>
) : null}
</div>
) : null}
</div>
<span className="whitespace-nowrap text-[11px] font-semibold text-gray-200">
{entry.hoursPerDay}h
</span>
</div>
))}
</>
);
}
function VacationSummary({
vacation,
title,
className,
}: {
vacation: VacationHoverData;
title: string;
className?: string;
}) {
const isPublicHoliday = vacation.type === "PUBLIC_HOLIDAY";
const scopeLabel = formatHolidayScope(vacation.scope);
const sourceLabel = formatHolidaySourceType(vacation.sourceType);
const holidayMeta = [scopeLabel, sourceLabel].filter(Boolean).join(" · ");
const holidayLocationBasis = isPublicHoliday ? buildHolidayLocationBasis(vacation) : null;
return (
<div className={className}>
<div className="flex items-center gap-1.5">
<span className="inline-block h-2 w-2 flex-shrink-0 rounded-full bg-amber-500" />
<span className="font-semibold text-amber-300">{title}</span>
</div>
<div className="mt-0.5 text-[11px] text-amber-200/80">
{formatDateLong(vacation.startDate)} to {formatDateLong(vacation.endDate)}
</div>
{holidayMeta ? (
<div className="mt-1 text-[11px] text-amber-100/75">{holidayMeta}</div>
) : null}
{holidayLocationBasis ? (
<div className="mt-1 text-[11px] text-amber-100/85">{holidayLocationBasis}</div>
) : null}
{isPublicHoliday && vacation.calendarName ? (
<div className="mt-1 text-[11px] text-amber-200/60">
Calendar: {vacation.calendarName}
</div>
) : null}
{vacation.note && !isPublicHoliday ? (
<div className="mt-1 text-[11px] text-amber-200/60">{vacation.note}</div>
) : null}
</div>
);
}
export function TimelineTooltip({
heatmapTooltipRef,
heatmapTooltipPos,
@@ -79,18 +278,14 @@ export function TimelineTooltip({
demandHover,
}: TimelineTooltipProps) {
const vacationTitle = vacationHover ? getVacationTitle(vacationHover) : null;
const renderTooltip = (content: React.ReactNode) =>
typeof document === "undefined" ? content : createPortal(content, document.body);
if (demandHover && demandTooltipRef && demandTooltipPos) {
return renderTooltip(
<div
ref={demandTooltipRef}
style={{
left: demandTooltipPos.left,
top: demandTooltipPos.top,
backgroundColor: "rgba(3, 7, 18, 0.96)",
}}
return renderTooltipPortal(
<TooltipSurface
tooltipRef={demandTooltipRef}
dataTestId="timeline-demand-tooltip"
position={demandTooltipPos}
backgroundColor="rgba(3, 7, 18, 0.96)"
className="fixed z-40 max-w-sm pointer-events-none rounded-xl border border-gray-800 bg-gray-950/96 px-3 py-2 text-xs text-white shadow-2xl"
>
<div className="flex items-start justify-between gap-3">
@@ -115,73 +310,58 @@ export function TimelineTooltip({
</div>
<div className="mt-2 grid grid-cols-2 gap-x-4 gap-y-1.5 text-[11px]">
<div>
<div className="text-gray-500">Requested</div>
<div className="font-medium text-gray-100">
{demandHover.requestedHeadcount} {demandHover.requestedHeadcount === 1 ? "seat" : "seats"}
</div>
</div>
<div>
<div className="text-gray-500">Open</div>
<div className="font-medium text-amber-300">
{demandHover.unfilledHeadcount} {demandHover.unfilledHeadcount === 1 ? "seat" : "seats"}
</div>
</div>
<div>
<div className="text-gray-500">Range</div>
<div className="font-medium text-gray-100">
{formatDateLong(demandHover.startDate)} to {formatDateLong(demandHover.endDate)}
</div>
</div>
<div>
<div className="text-gray-500">Load</div>
<div className="font-medium text-gray-100">
{demandHover.hoursPerDay}h/day · {demandHover.totalHours}h
</div>
</div>
<TooltipMetric
label="Requested"
value={`${demandHover.requestedHeadcount} ${demandHover.requestedHeadcount === 1 ? "seat" : "seats"}`}
/>
<TooltipMetric
label="Open"
value={`${demandHover.unfilledHeadcount} ${demandHover.unfilledHeadcount === 1 ? "seat" : "seats"}`}
valueClassName="font-medium text-amber-300"
/>
<TooltipMetric
label="Range"
value={`${formatDateLong(demandHover.startDate)} to ${formatDateLong(demandHover.endDate)}`}
/>
<TooltipMetric
label="Load"
value={`${demandHover.hoursPerDay}h/day · ${demandHover.totalHours}h`}
/>
{typeof demandHover.percentage === "number" && demandHover.percentage > 0 ? (
<div>
<div className="text-gray-500">Allocation</div>
<div className="font-medium text-gray-100">{demandHover.percentage}%</div>
</div>
<TooltipMetric label="Allocation" value={`${demandHover.percentage}%`} />
) : null}
{typeof demandHover.totalCostCents === "number" && demandHover.totalCostCents > 0 ? (
<div>
<div className="text-gray-500">Cost</div>
<div className="font-medium text-gray-100">
{formatCents(demandHover.totalCostCents)} EUR
{typeof demandHover.dailyCostCents === "number" && demandHover.dailyCostCents > 0
<TooltipMetric
label="Cost"
value={`${formatCents(demandHover.totalCostCents)} EUR${
typeof demandHover.dailyCostCents === "number" && demandHover.dailyCostCents > 0
? ` · ${formatCents(demandHover.dailyCostCents)}/d`
: ""}
</div>
</div>
: ""
}`}
/>
) : null}
</div>
<div className="mt-2 border-t border-gray-800/90 pt-2 text-[10px] uppercase tracking-[0.14em] text-gray-500">
Click for details and actions
</div>
</div>,
</TooltipSurface>,
);
}
// When both are active, render a single merged tooltip using the heatmap position
if (heatmapHover && vacationHover) {
return renderTooltip(
<div
ref={(el) => {
// Wire both refs to the same element so position updates work from either handler
return renderTooltipPortal(
<TooltipSurface
tooltipRef={(el) => {
(heatmapTooltipRef as React.MutableRefObject<HTMLDivElement | null>).current = el;
(vacationTooltipRef as React.MutableRefObject<HTMLDivElement | null>).current = el;
}}
style={{
left: heatmapTooltipPos.left,
top: heatmapTooltipPos.top,
backgroundColor: "rgba(3, 7, 18, 0.96)",
// Keep the merged tooltip attached to the heatmap pointer path only.
(vacationTooltipRef as React.MutableRefObject<HTMLDivElement | null>).current = null;
}}
position={heatmapTooltipPos}
backgroundColor="rgba(3, 7, 18, 0.96)"
className="fixed z-40 max-w-sm pointer-events-none rounded-xl border border-gray-800 bg-gray-950/96 px-3 py-2 text-xs text-white shadow-2xl"
>
{/* Date + hours header */}
<div className="flex items-center justify-between gap-3">
<span className="font-semibold">{formatDateLong(heatmapHover.date)}</span>
<span className="text-[11px] text-gray-300">
@@ -189,72 +369,26 @@ export function TimelineTooltip({
</span>
</div>
{/* Project breakdown */}
<div className="mt-2 space-y-1.5">
{heatmapHover.breakdown.length > 0 ? (
heatmapHover.breakdown.slice(0, 6).map((entry) => (
<div
key={`${entry.projectId}-${entry.shortCode}`}
className="flex items-start justify-between gap-3"
>
<div className="min-w-0">
<div className="truncate font-medium text-white">
{entry.shortCode ? `${entry.shortCode} · ` : ""}
{entry.projectName}
</div>
<div className="truncate text-[11px] text-gray-400">
{[
entry.role,
entry.responsiblePerson ? `Lead: ${entry.responsiblePerson}` : null,
entry.orderType,
].filter(Boolean).join(" · ")}
</div>
{entry.startDate && entry.endDate && (
<div className="text-[10px] text-gray-500">
{entry.startDate} {entry.endDate}
{entry.status && entry.status !== "CONFIRMED" && (
<span className="ml-1 uppercase text-amber-400">{entry.status}</span>
)}
</div>
)}
</div>
<span className="whitespace-nowrap text-[11px] font-semibold text-gray-200">
{entry.hoursPerDay}h
</span>
</div>
))
) : (
<div className="text-[11px] text-gray-400">No bookings on this day.</div>
)}
<HeatmapBreakdownList breakdown={heatmapHover.breakdown} />
</div>
{/* Vacation section — merged below */}
<div className="mt-2 pt-2 border-t border-amber-700/40">
<div className="flex items-center gap-1.5">
<span className="inline-block w-2 h-2 rounded-full bg-amber-500 flex-shrink-0" />
<span className="font-semibold text-amber-300">{vacationTitle}</span>
</div>
<div className="mt-0.5 text-[11px] text-amber-200/80">
{formatDateLong(vacationHover.startDate)} to {formatDateLong(vacationHover.endDate)}
</div>
{vacationHover.note && vacationHover.type !== "PUBLIC_HOLIDAY" ? (
<div className="mt-1 text-[11px] text-amber-200/60">{vacationHover.note}</div>
) : null}
</div>
</div>,
<VacationSummary
vacation={vacationHover}
title={vacationTitle ?? "Vacation"}
className="mt-2 border-t border-amber-700/40 pt-2"
/>
</TooltipSurface>,
);
}
// Heatmap only
if (heatmapHover) {
return renderTooltip(
<div
ref={heatmapTooltipRef}
style={{
left: heatmapTooltipPos.left,
top: heatmapTooltipPos.top,
backgroundColor: "rgba(3, 7, 18, 0.96)",
}}
return renderTooltipPortal(
<TooltipSurface
tooltipRef={heatmapTooltipRef}
position={heatmapTooltipPos}
backgroundColor="rgba(3, 7, 18, 0.96)"
className="fixed z-40 max-w-sm pointer-events-none rounded-xl border border-gray-800 bg-gray-950/96 px-3 py-2 text-xs text-white shadow-2xl"
>
<div className="flex items-center justify-between gap-3">
@@ -264,66 +398,23 @@ export function TimelineTooltip({
</span>
</div>
<div className="mt-2 space-y-1.5">
{heatmapHover.breakdown.length > 0 ? (
heatmapHover.breakdown.slice(0, 6).map((entry) => (
<div
key={`${entry.projectId}-${entry.shortCode}`}
className="flex items-start justify-between gap-3"
>
<div className="min-w-0">
<div className="truncate font-medium text-white">
{entry.shortCode ? `${entry.shortCode} · ` : ""}
{entry.projectName}
</div>
<div className="truncate text-[11px] text-gray-400">
{[
entry.role,
entry.responsiblePerson ? `Lead: ${entry.responsiblePerson}` : null,
entry.orderType,
].filter(Boolean).join(" · ")}
</div>
{entry.startDate && entry.endDate && (
<div className="text-[10px] text-gray-500">
{entry.startDate} {entry.endDate}
{entry.status && entry.status !== "CONFIRMED" && (
<span className="ml-1 uppercase text-amber-400">{entry.status}</span>
)}
</div>
)}
</div>
<span className="whitespace-nowrap text-[11px] font-semibold text-gray-200">
{entry.hoursPerDay}h
</span>
</div>
))
) : (
<div className="text-[11px] text-gray-400">No bookings on this day.</div>
)}
<HeatmapBreakdownList breakdown={heatmapHover.breakdown} />
</div>
</div>,
</TooltipSurface>,
);
}
// Vacation only
if (vacationHover) {
return renderTooltip(
<div
ref={vacationTooltipRef}
style={{
left: vacationTooltipPos.left,
top: vacationTooltipPos.top,
backgroundColor: "rgba(120, 53, 15, 0.95)",
}}
return renderTooltipPortal(
<TooltipSurface
tooltipRef={vacationTooltipRef}
position={vacationTooltipPos}
backgroundColor="rgba(120, 53, 15, 0.95)"
className="fixed z-40 max-w-xs pointer-events-none rounded-xl border border-amber-700/50 bg-amber-950/95 px-3 py-2 text-xs text-amber-50 shadow-2xl"
>
<div className="font-semibold">{vacationTitle}</div>
<div className="mt-1 text-[11px] text-amber-100/90">
{formatDateLong(vacationHover.startDate)} to {formatDateLong(vacationHover.endDate)}
</div>
{vacationHover.note && vacationHover.type !== "PUBLIC_HOLIDAY" ? (
<div className="mt-2 text-[11px] text-amber-100/80">{vacationHover.note}</div>
) : null}
</div>,
<VacationSummary vacation={vacationHover} title={vacationTitle ?? "Vacation"} />
</TooltipSurface>,
);
}