fix(web): reuse project combobox in timeline popovers

This commit is contained in:
2026-03-30 13:34:59 +02:00
parent 9268a38df4
commit f0bea6235d
13 changed files with 525 additions and 203 deletions
@@ -1,13 +1,14 @@
"use client";
import { clsx } from "clsx";
import { useEffect, useRef, useState } from "react";
import { useState } from "react";
import { createPortal } from "react-dom";
import { AllocationStatus } from "@capakraken/shared";
import { trpc } from "~/lib/trpc/client.js";
import { useInvalidateTimeline } from "~/hooks/useInvalidatePlanningViews.js";
import { useViewportPopover } from "~/hooks/useViewportPopover.js";
import { DateInput } from "~/components/ui/DateInput.js";
import { ProjectCombobox } from "~/components/ui/ProjectCombobox.js";
interface NewAllocationPopoverProps {
resourceId: string;
@@ -46,7 +47,6 @@ export function NewAllocationPopover({
});
const invalidateTimeline = useInvalidateTimeline();
const [search, setSearch] = useState("");
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
suggestedProjectId ?? null,
);
@@ -54,17 +54,6 @@ export function NewAllocationPopover({
const [hoursPerDay, setHoursPerDay] = useState(8);
const [start, setStart] = useState(toDateInput(startDate));
const [end, setEnd] = useState(toDateInput(endDate));
const [dropdownOpen, setDropdownOpen] = useState(!suggestedProjectId);
const { data: projectsData } = trpc.project.list.useQuery(
{ search, limit: 20 },
{ staleTime: 30_000 },
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const projects = (projectsData?.projects ?? []) as Array<{ id: string; name: string; orderType?: string }>;
const selectedProject = projects.find((p) => p.id === selectedProjectId)
?? (suggestedProjectId ? projects.find((p) => p.id === suggestedProjectId) : null);
const createMutation = trpc.timeline.quickAssign.useMutation({
onSuccess: () => {
@@ -126,41 +115,12 @@ export function NewAllocationPopover({
{/* Project picker */}
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Project</label>
{selectedProject && !dropdownOpen ? (
<div
className="flex items-center gap-2 border border-brand-300 dark:border-sky-700 rounded-lg px-3 py-2 cursor-pointer bg-brand-50 dark:bg-sky-950/30"
onClick={() => { setDropdownOpen(true); setSearch(""); }}
>
<span className="text-sm text-gray-800 dark:text-gray-200 truncate flex-1">{selectedProject.name}</span>
<span className="text-xs text-gray-400 dark:text-gray-500"></span>
</div>
) : (
<div className="relative">
<input
autoFocus={dropdownOpen}
type="text"
placeholder="Search projects…"
value={search}
onChange={(e) => setSearch(e.target.value)}
onFocus={() => setDropdownOpen(true)}
className="w-full border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-400"
/>
{dropdownOpen && projects.length > 0 && (
<div className="absolute top-full left-0 right-0 z-50 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg dark:shadow-black/40 mt-1 max-h-44 overflow-y-auto">
{projects.map((p) => (
<button
key={p.id}
type="button"
onClick={() => { setSelectedProjectId(p.id); setDropdownOpen(false); setSearch(""); }}
className="w-full text-left px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-2 border-b border-gray-50 dark:border-gray-700 last:border-0"
>
<span className="text-sm text-gray-800 dark:text-gray-200 truncate">{p.name}</span>
</button>
))}
</div>
)}
</div>
)}
<ProjectCombobox
value={selectedProjectId}
onChange={setSelectedProjectId}
placeholder="Search project…"
className="w-full"
/>
</div>
{/* Role */}