112 lines
3.6 KiB
TypeScript
112 lines
3.6 KiB
TypeScript
"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>
|
||
);
|
||
});
|