feat(planning): ship holiday-aware planning and assistant upgrades
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { formatDateLong } from "~/lib/format.js";
|
||||
import { formatCents, formatDateLong } from "~/lib/format.js";
|
||||
|
||||
function getVacationTitle(vacation: VacationHoverData): string {
|
||||
if (vacation.type === "PUBLIC_HOLIDAY" && vacation.note) {
|
||||
return vacation.note;
|
||||
}
|
||||
return vacation.type.replaceAll("_", " ");
|
||||
}
|
||||
|
||||
export type HeatmapHoverData = {
|
||||
date: Date;
|
||||
@@ -30,6 +37,23 @@ export type VacationHoverData = {
|
||||
approvedAt?: Date | string | null;
|
||||
};
|
||||
|
||||
export type DemandHoverData = {
|
||||
roleName: string;
|
||||
roleColor: string;
|
||||
projectName: string;
|
||||
projectShortCode?: string | null;
|
||||
requestedHeadcount: number;
|
||||
unfilledHeadcount: number;
|
||||
startDate: Date | string;
|
||||
endDate: Date | string;
|
||||
hoursPerDay: number;
|
||||
totalHours: number;
|
||||
percentage?: number;
|
||||
status?: string;
|
||||
totalCostCents?: number;
|
||||
dailyCostCents?: number;
|
||||
};
|
||||
|
||||
interface TimelineTooltipProps {
|
||||
heatmapTooltipRef: React.RefObject<HTMLDivElement | null>;
|
||||
heatmapTooltipPos: { left: number; top: number };
|
||||
@@ -37,6 +61,9 @@ interface TimelineTooltipProps {
|
||||
vacationTooltipPos: { left: number; top: number };
|
||||
heatmapHover: HeatmapHoverData | null;
|
||||
vacationHover: VacationHoverData | null;
|
||||
demandTooltipRef?: React.RefObject<HTMLDivElement | null>;
|
||||
demandTooltipPos?: { left: number; top: number };
|
||||
demandHover?: DemandHoverData | null;
|
||||
}
|
||||
|
||||
export function TimelineTooltip({
|
||||
@@ -46,7 +73,87 @@ export function TimelineTooltip({
|
||||
vacationTooltipPos,
|
||||
heatmapHover,
|
||||
vacationHover,
|
||||
demandTooltipRef,
|
||||
demandTooltipPos,
|
||||
demandHover,
|
||||
}: TimelineTooltipProps) {
|
||||
const vacationTitle = vacationHover ? getVacationTitle(vacationHover) : null;
|
||||
|
||||
if (demandHover && demandTooltipRef && demandTooltipPos) {
|
||||
return (
|
||||
<div
|
||||
ref={demandTooltipRef}
|
||||
style={{
|
||||
left: demandTooltipPos.left,
|
||||
top: demandTooltipPos.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"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="inline-block h-2 w-2 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: demandHover.roleColor }}
|
||||
/>
|
||||
<span className="truncate font-semibold">{demandHover.roleName}</span>
|
||||
</div>
|
||||
<div className="truncate text-[11px] text-gray-400">
|
||||
{demandHover.projectShortCode ? `${demandHover.projectShortCode} · ` : ""}
|
||||
{demandHover.projectName}
|
||||
</div>
|
||||
</div>
|
||||
{demandHover.status ? (
|
||||
<span className="text-[10px] uppercase tracking-wide text-amber-300">
|
||||
{demandHover.status}
|
||||
</span>
|
||||
) : null}
|
||||
</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}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-500">Open</div>
|
||||
<div className="font-medium text-amber-300">{demandHover.unfilledHeadcount}</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>
|
||||
{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>
|
||||
) : 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
|
||||
? ` · ${formatCents(demandHover.dailyCostCents)}/d`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// When both are active, render a single merged tooltip using the heatmap position
|
||||
if (heatmapHover && vacationHover) {
|
||||
return (
|
||||
@@ -114,14 +221,12 @@ export function TimelineTooltip({
|
||||
<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">
|
||||
{vacationHover.type.replaceAll("_", " ")}
|
||||
</span>
|
||||
<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.note && vacationHover.type !== "PUBLIC_HOLIDAY" ? (
|
||||
<div className="mt-1 text-[11px] text-amber-200/60">{vacationHover.note}</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -200,11 +305,11 @@ export function TimelineTooltip({
|
||||
}}
|
||||
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">{vacationHover.type.replaceAll("_", " ")}</div>
|
||||
<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.note && vacationHover.type !== "PUBLIC_HOLIDAY" ? (
|
||||
<div className="mt-2 text-[11px] text-amber-100/80">{vacationHover.note}</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user