"use client"; import { formatDateLong } from "~/lib/format.js"; export type HeatmapHoverData = { date: Date; totalH: number; pct: number; breakdown: { projectId: string; shortCode: string; projectName: string; orderType: string; hoursPerDay: number; responsiblePerson?: string | null; role?: string | null; status?: string; startDate?: string; endDate?: string; }[]; }; export type VacationHoverData = { type: string; startDate: Date | string; endDate: Date | string; note?: string | null; requestedBy?: { name?: string | null; email: string } | null; approvedBy?: { name?: string | null; email: string } | null; approvedAt?: Date | string | null; }; interface TimelineTooltipProps { heatmapTooltipRef: React.RefObject; heatmapTooltipPos: { left: number; top: number }; vacationTooltipRef: React.RefObject; vacationTooltipPos: { left: number; top: number }; heatmapHover: HeatmapHoverData | null; vacationHover: VacationHoverData | null; } export function TimelineTooltip({ heatmapTooltipRef, heatmapTooltipPos, vacationTooltipRef, vacationTooltipPos, heatmapHover, vacationHover, }: TimelineTooltipProps) { // When both are active, render a single merged tooltip using the heatmap position if (heatmapHover && vacationHover) { return (
{ // Wire both refs to the same element so position updates work from either handler (heatmapTooltipRef as React.MutableRefObject).current = el; (vacationTooltipRef as React.MutableRefObject).current = el; }} style={{ left: heatmapTooltipPos.left, top: heatmapTooltipPos.top, 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 */}
{formatDateLong(heatmapHover.date)} {heatmapHover.totalH.toFixed(1)}h · {Math.round(heatmapHover.pct)}%
{/* Project breakdown */}
{heatmapHover.breakdown.length > 0 ? ( heatmapHover.breakdown.slice(0, 6).map((entry) => (
{entry.shortCode ? `${entry.shortCode} · ` : ""} {entry.projectName}
{[ entry.role, entry.responsiblePerson ? `Lead: ${entry.responsiblePerson}` : null, entry.orderType, ].filter(Boolean).join(" · ")}
{entry.startDate && entry.endDate && (
{entry.startDate} → {entry.endDate} {entry.status && entry.status !== "CONFIRMED" && ( {entry.status} )}
)}
{entry.hoursPerDay}h
)) ) : (
No bookings on this day.
)}
{/* Vacation section — merged below */}
{vacationHover.type.replaceAll("_", " ")}
{formatDateLong(vacationHover.startDate)} to {formatDateLong(vacationHover.endDate)}
{vacationHover.note ? (
{vacationHover.note}
) : null}
); } // Heatmap only if (heatmapHover) { return (
{formatDateLong(heatmapHover.date)} {heatmapHover.totalH.toFixed(1)}h · {Math.round(heatmapHover.pct)}%
{heatmapHover.breakdown.length > 0 ? ( heatmapHover.breakdown.slice(0, 6).map((entry) => (
{entry.shortCode ? `${entry.shortCode} · ` : ""} {entry.projectName}
{[ entry.role, entry.responsiblePerson ? `Lead: ${entry.responsiblePerson}` : null, entry.orderType, ].filter(Boolean).join(" · ")}
{entry.startDate && entry.endDate && (
{entry.startDate} → {entry.endDate} {entry.status && entry.status !== "CONFIRMED" && ( {entry.status} )}
)}
{entry.hoursPerDay}h
)) ) : (
No bookings on this day.
)}
); } // Vacation only if (vacationHover) { return (
{vacationHover.type.replaceAll("_", " ")}
{formatDateLong(vacationHover.startDate)} to {formatDateLong(vacationHover.endDate)}
{vacationHover.note ? (
{vacationHover.note}
) : null}
); } return null; }