import { Search, X } from "lucide-react"; import { useMemo, useState, type CSSProperties } from "react"; import { createPortal } from "react-dom"; import { allocatedHoursInRange, buildAllocationBarSegment, formatAllocationEffort, formatHoursAmount, hoursPerDayFromAllocation, projectBarStyle, roundToHalfHour, visibleWeekdayCount, type EffortDisplayMode, } from "../../planner/allocationTimeline"; import type { Allocation, Person, Project } from "../../types"; import { Avatar } from "../ui/Avatar"; export interface CloneModeContext { allocation: Allocation; projectId: number; projectName: string; } type CloneTargetMode = "people" | "projects"; interface AllocationCloneModeProps { context: CloneModeContext; people: Person[]; projects: Project[]; allocations: Allocation[]; dayDates: Date[]; effortMode: EffortDisplayMode; onCloneToPerson: (personId: number) => void; onCloneToProject: (projectId: number) => void; onExit: () => void; } function freeHoursLabel( person: Person, personAllocations: Allocation[], dayDates: Date[], rangeStart: string, rangeEnd: string, ): { label: string; tone: "free" | "over" | "off" } { const weekdayCount = visibleWeekdayCount(dayDates, rangeStart, rangeEnd); if (weekdayCount <= 0) return { label: "Off", tone: "off" }; const capacity = roundToHalfHour((person.weekly_capacity_hrs / 5) * weekdayCount); let allocated = 0; for (const allocation of personAllocations) { const overlapStart = allocation.start_date > rangeStart ? allocation.start_date : rangeStart; const overlapEnd = allocation.end_date < rangeEnd ? allocation.end_date : rangeEnd; if (overlapStart > overlapEnd) continue; const days = visibleWeekdayCount(dayDates, overlapStart, overlapEnd); allocated += allocatedHoursInRange(allocation.allocation_pct, person.weekly_capacity_hrs, days); } const free = roundToHalfHour(capacity - allocated); if (free < 0) return { label: `${formatHoursAmount(Math.abs(free))} over`, tone: "over" }; if (free === 0) return { label: "0h free", tone: "off" }; return { label: `${formatHoursAmount(free)} free`, tone: "free" }; } export function AllocationCloneMode({ context, people, projects, allocations, dayDates, effortMode, onCloneToPerson, onCloneToProject, onExit, }: AllocationCloneModeProps) { const [targetMode, setTargetMode] = useState("people"); const [search, setSearch] = useState(""); const sourcePerson = people.find((person) => person.id === context.allocation.person_id); const weekdayCount = visibleWeekdayCount( dayDates, context.allocation.start_date, context.allocation.end_date, ); const cloningLabel = sourcePerson ? formatAllocationEffort( effortMode, context.allocation.allocation_pct, sourcePerson.weekly_capacity_hrs, weekdayCount, ) : `${context.allocation.allocation_pct}%`; const assignmentLabel = weekdayCount === 1 ? "1 weekday assignment" : `${weekdayCount} weekday assignment${weekdayCount === 1 ? "" : "s"}`; const term = search.trim().toLowerCase(); const filteredPeople = useMemo( () => people.filter((person) => { if (!person.is_active) return false; if (!term) return true; return ( person.name.toLowerCase().includes(term) || person.role.name.toLowerCase().includes(term) || person.email.toLowerCase().includes(term) ); }), [people, term], ); const filteredProjects = useMemo( () => projects.filter((project) => { if (!project.is_active) return false; if (!term) return true; return project.name.toLowerCase().includes(term) || project.type.toLowerCase().includes(term); }), [projects, term], ); const allocationsByPerson = useMemo(() => { const map = new Map(); for (const allocation of allocations) { if (!map.has(allocation.person_id)) map.set(allocation.person_id, []); map.get(allocation.person_id)!.push(allocation); } return map; }, [allocations]); const totalDays = dayDates.length; const panel = ( <>
CLONE MODE {" | "} {assignmentLabel} from {sourcePerson?.name ?? "Unknown"} {sourcePerson ? ` | ${sourcePerson.role.name}` : ""} {" | "} Cloning {cloningLabel}
setSearch(event.target.value)} placeholder={targetMode === "people" ? "Search people…" : "Search projects…"} type="search" value={search} />
{targetMode === "people" ? filteredPeople.map((person) => { const personAllocations = allocationsByPerson.get(person.id) ?? []; const capacity = freeHoursLabel( person, personAllocations, dayDates, context.allocation.start_date, context.allocation.end_date, ); const isSource = person.id === context.allocation.person_id; return (
{person.name} {person.role.name}
{personAllocations.map((allocation) => { const segment = buildAllocationBarSegment( dayDates, allocation.start_date, allocation.end_date, ); if (!segment) return null; const project = projects.find((item) => item.id === allocation.project_id); return (
); })} {capacity.label}
); }) : filteredProjects.map((project) => { const isSource = project.id === context.projectId; return (
{project.name} {project.type}
); })}
); return createPortal(panel, document.body); } export function cloneModeEffortSummary(allocation: Allocation, person: Person): string { const daily = roundToHalfHour( hoursPerDayFromAllocation(allocation.allocation_pct, person.weekly_capacity_hrs), ); return formatHoursAmount(daily); }