Files
CapaKraken/apps/web/src/components/timeline/ShiftPreviewTooltip.tsx
T

112 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { clsx } from "clsx";
import { memo } from "react";
import type { ShiftPreviewData } from "~/hooks/useTimelineDrag.js";
import { formatDate } from "~/lib/format.js";
import { usePermissions } from "~/hooks/usePermissions.js";
interface ShiftPreviewTooltipProps {
preview: ShiftPreviewData;
projectName: string;
newStartDate: Date;
newEndDate: Date;
isLoading?: boolean;
}
function formatCents(cents: number): string {
const abs = Math.abs(cents);
const str = (abs / 100).toLocaleString("de-DE", { minimumFractionDigits: 0 });
return `${cents < 0 ? "" : "+"}${str}`;
}
export const ShiftPreviewTooltip = memo(function ShiftPreviewTooltip({
preview,
projectName,
newStartDate,
newEndDate,
isLoading,
}: ShiftPreviewTooltipProps) {
const { canViewCosts } = usePermissions();
const dateStr = `${formatDate(newStartDate)}${formatDate(newEndDate)}`;
return (
<div
className={clsx(
"bg-white border rounded-xl shadow-2xl p-3 min-w-56 max-w-72 text-sm",
preview.valid ? "border-gray-200" : "border-red-300",
)}
>
{/* Header */}
<div className="font-semibold text-gray-900 truncate mb-2">{projectName}</div>
<div className="text-xs text-gray-500 mb-3 font-mono">{dateStr}</div>
{isLoading ? (
<div className="text-xs text-gray-400 animate-pulse">Calculating...</div>
) : (
<>
{/* Cost delta */}
{canViewCosts && preview.deltaCents !== 0 && (
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-gray-500">Cost delta</span>
<span
className={clsx(
"text-xs font-mono font-medium",
preview.deltaCents > 0 ? "text-red-600" : "text-green-600",
)}
>
{formatCents(preview.deltaCents)}
</span>
</div>
)}
{/* Budget utilization */}
{canViewCosts && (
<div className="flex items-center justify-between mb-3">
<span className="text-xs text-gray-500">Budget after</span>
<span
className={clsx(
"text-xs font-medium",
preview.wouldExceedBudget
? "text-red-600"
: preview.budgetUtilizationAfter > 85
? "text-yellow-600"
: "text-gray-700",
)}
>
{preview.budgetUtilizationAfter.toFixed(1)}%
</span>
</div>
)}
{/* Conflicts */}
{preview.conflictCount > 0 && (
<div className="mb-2 text-xs text-yellow-700 bg-yellow-50 rounded-lg px-2 py-1.5">
{preview.conflictCount} availability conflict{preview.conflictCount > 1 ? "s" : ""}
</div>
)}
{/* Errors */}
{preview.errors.map((err, i) => (
<div key={i} className="mb-1 text-xs text-red-700 bg-red-50 rounded-lg px-2 py-1.5">
{err}
</div>
))}
{/* Warnings */}
{preview.warnings.slice(0, 2).map((warn, i) => (
<div key={i} className="mb-1 text-xs text-yellow-700 bg-yellow-50 rounded-lg px-2 py-1">
{warn}
</div>
))}
{/* Action hint */}
<div className="mt-2 text-xs text-gray-400 text-center">
{preview.valid ? "Release to apply shift" : "Cannot apply — resolve errors first"}
</div>
</>
)}
</div>
);
});