Files
Nexus/apps/web/src/components/allocations/ConflictWarningPanel.tsx
T
Hartmut 3e8df09cd8 feat(web): overbooking and vacation conflict warnings in AllocationModal
- New ConflictWarningPanel component: amber box with per-day overbooking
  table (capacity / already booked / new / overage) and sky-blue info box
  for vacation overlap. Overbooking section has an 'I understand' checkbox
  that must be ticked before Save is enabled; vacation overlap is
  informational only.
- AllocationModal: fires allocation.checkConflicts reactively when
  resourceId, dates and hoursPerDay are all set. Shows ConflictWarningPanel
  between form body and footer. Passes allowOverbooking: true to the
  createAssignment mutation when the user acknowledges. Acknowledgment
  resets whenever key fields change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 10:15:37 +02:00

131 lines
5.9 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 { useState } from "react";
import type { AllocationConflictCheckResult } from "@capakraken/shared";
const INITIAL_ROWS_SHOWN = 5;
interface ConflictWarningPanelProps {
result: AllocationConflictCheckResult;
isLoading: boolean;
acknowledged: boolean;
onAcknowledge: (v: boolean) => void;
}
export function ConflictWarningPanel({
result,
isLoading,
acknowledged,
onAcknowledge,
}: ConflictWarningPanelProps) {
const [showAllDays, setShowAllDays] = useState(false);
if (isLoading) {
return (
<div className="flex items-center gap-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/40 px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
<span className="inline-block h-3 w-3 shrink-0 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
Checking availability
</div>
);
}
const hasAny = result.isOverbooking || result.hasVacationOverlap;
if (!hasAny) return null;
const conflictDays = result.overbooking?.conflictDays ?? [];
const visibleDays = showAllDays ? conflictDays : conflictDays.slice(0, INITIAL_ROWS_SHOWN);
const hiddenCount = conflictDays.length - INITIAL_ROWS_SHOWN;
return (
<div className="space-y-3">
{/* ── Overbooking ─────────────────────────────────────────────── */}
{result.isOverbooking && result.overbooking && (
<div className="rounded-lg border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-950/30 p-4 text-sm">
<p className="font-semibold text-amber-800 dark:text-amber-300">
Overbooking on {result.overbooking.totalConflictDays} day
{result.overbooking.totalConflictDays !== 1 ? "s" : ""}
{" "}(up to {result.overbooking.maxOverbookPercent}% over capacity)
</p>
<p className="mt-1 text-amber-700 dark:text-amber-400">
The resource already has allocations that exceed their daily capacity on the following days.
You can still save check the box below to confirm.
</p>
{/* Day-by-day table */}
<div className="mt-3 overflow-x-auto">
<table className="w-full text-xs text-left text-amber-800 dark:text-amber-300">
<thead>
<tr className="border-b border-amber-200 dark:border-amber-800">
<th className="pb-1 pr-4 font-medium">Date</th>
<th className="pb-1 pr-4 font-medium text-right">Capacity</th>
<th className="pb-1 pr-4 font-medium text-right">Booked</th>
<th className="pb-1 pr-4 font-medium text-right">New</th>
<th className="pb-1 font-medium text-right">Over</th>
</tr>
</thead>
<tbody>
{visibleDays.map((day) => (
<tr key={day.date} className="border-b border-amber-100 dark:border-amber-900/50 last:border-0">
<td className="py-1 pr-4">{day.date}</td>
<td className="py-1 pr-4 text-right">{day.availableHours}h</td>
<td className="py-1 pr-4 text-right">{day.existingHours}h</td>
<td className="py-1 pr-4 text-right">{day.requestedHours}h</td>
<td className="py-1 text-right font-semibold text-red-600 dark:text-red-400">
+{day.overageHours.toFixed(1)}h
</td>
</tr>
))}
</tbody>
</table>
</div>
{hiddenCount > 0 && (
<button
type="button"
onClick={() => setShowAllDays((v) => !v)}
className="mt-2 text-xs font-medium text-amber-700 dark:text-amber-400 underline underline-offset-2"
>
{showAllDays ? "Show less" : `Show ${hiddenCount} more day${hiddenCount !== 1 ? "s" : ""}`}
</button>
)}
{/* Acknowledgment checkbox */}
<label className="mt-3 flex cursor-pointer items-center gap-2 select-none">
<input
type="checkbox"
checked={acknowledged}
onChange={(e) => onAcknowledge(e.target.checked)}
className="rounded border-amber-400 dark:border-amber-600 text-amber-600 focus:ring-amber-500"
/>
<span className="text-amber-800 dark:text-amber-300 text-xs font-medium">
I understand and want to proceed with overbooking
</span>
</label>
</div>
)}
{/* ── Vacation overlap ─────────────────────────────────────────── */}
{result.hasVacationOverlap && (
<div className="rounded-lg border border-sky-200 dark:border-sky-800 bg-sky-50 dark:bg-sky-950/30 p-4 text-sm">
<p className="font-semibold text-sky-800 dark:text-sky-300">
Resource has approved leave during this period
</p>
<p className="mt-1 text-sky-700 dark:text-sky-400">
Vacation days are excluded from billable hours and daily cost calculations.
</p>
<ul className="mt-2 space-y-1">
{result.vacationOverlap.map((v, i) => (
<li key={i} className="flex items-center gap-2 text-xs text-sky-700 dark:text-sky-400">
<span className="inline-block h-1.5 w-1.5 shrink-0 rounded-full bg-sky-400" />
<span className="font-medium capitalize">{v.type.replace(/_/g, " ").toLowerCase()}</span>
{v.isHalfDay && <span className="text-sky-500">(half-day)</span>}
<span>{v.startDate === v.endDate ? v.startDate : `${v.startDate} ${v.endDate}`}</span>
</li>
))}
</ul>
</div>
)}
</div>
);
}