chore(repo): initialize planarchy workspace

This commit is contained in:
2026-03-14 14:31:09 +01:00
commit dd55d0e78b
769 changed files with 166461 additions and 0 deletions
@@ -0,0 +1,111 @@
"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>
);
});