Files
CapaKraken/apps/web/src/components/timeline/TimelineToolbar.tsx
T

236 lines
8.4 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 { clsx } from "clsx";
import { useRef, useState } from "react";
import { trpc } from "~/lib/trpc/client.js";
import { ProjectCombobox } from "~/components/ui/ProjectCombobox.js";
import { ResourceCombobox } from "~/components/ui/ResourceCombobox.js";
import { TimelineFilter, type TimelineFilters } from "./TimelineFilter.js";
import { TimelineQuickFilters } from "./TimelineQuickFilters.js";
interface TimelineToolbarProps {
viewMode: "resource" | "project";
onViewModeChange: (mode: "resource" | "project") => void;
filters: TimelineFilters;
onFiltersChange: (f: TimelineFilters) => void;
filterOpen: boolean;
onFilterOpenChange: (open: boolean) => void;
resourceCount: number;
projectCount: number;
totalAllocCount: number;
onNavigateBack: () => void;
onNavigateToday: () => void;
onNavigateForward: () => void;
canUndo?: boolean;
canRedo?: boolean;
onUndo?: () => void;
onRedo?: () => void;
}
export function TimelineToolbar({
viewMode,
onViewModeChange,
filters,
onFiltersChange,
filterOpen,
onFilterOpenChange,
resourceCount,
projectCount,
totalAllocCount,
onNavigateBack,
onNavigateToday,
onNavigateForward,
canUndo,
canRedo,
onUndo,
onRedo,
}: TimelineToolbarProps) {
const activeFilterCount =
filters.clientIds.length +
filters.chapters.length +
filters.eids.length +
filters.projectIds.length +
filters.countryCodes.length;
const filterAnchorRef = useRef<HTMLDivElement | null>(null);
// Track selected resource ID for the combobox (separate from the EID-based filter)
const [selectedResourceId, setSelectedResourceId] = useState<string | null>(null);
// Look up resource to get EID when selected
const { data: resourceLookup } = trpc.resource.directory.useQuery(
{ limit: 500 },
{ staleTime: 60_000 },
);
function handleProjectChange(id: string | null) {
onFiltersChange({ ...filters, projectIds: id ? [id] : [] });
}
function handleResourceChange(id: string | null) {
setSelectedResourceId(id);
if (!id) {
onFiltersChange({ ...filters, eids: [] });
return;
}
const resources = (resourceLookup?.resources ?? []) as Array<{ id: string; eid: string }>;
const resource = resources.find((r) => r.id === id);
if (resource?.eid) {
onFiltersChange({ ...filters, eids: [resource.eid] });
}
}
function clearQuickFilters() {
onFiltersChange({
...filters,
clientIds: [],
chapters: [],
eids: [],
projectIds: [],
});
}
return (
<div className="app-toolbar relative z-20 flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-3">
<ProjectCombobox
value={filters.projectIds[0] ?? null}
onChange={handleProjectChange}
placeholder="Filter by project..."
className="min-w-[220px]"
/>
<ResourceCombobox
value={selectedResourceId}
onChange={handleResourceChange}
placeholder="Filter by resource..."
className="min-w-[180px]"
/>
<div className="text-sm text-gray-500 dark:text-gray-400">
{viewMode === "resource"
? `${resourceCount} resources \u00B7 ${totalAllocCount} allocations`
: `${projectCount} projects`}
</div>
</div>
<div className="flex flex-wrap items-center justify-end gap-2">
<TimelineQuickFilters filters={filters} onChange={onFiltersChange} />
{activeFilterCount > 0 && (
<button
type="button"
onClick={clearQuickFilters}
className="rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-600 transition hover:border-gray-400 hover:bg-gray-50 hover:text-gray-900 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-100"
>
Clear filters
</button>
)}
{/* Timeline navigation */}
<div className="flex items-center gap-1">
<button
type="button"
onClick={onNavigateBack}
className="rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition hover:border-gray-400 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800"
title="Previous 4 weeks"
>
</button>
<button
type="button"
onClick={onNavigateToday}
className="rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition hover:border-gray-400 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800"
>
Today
</button>
<button
type="button"
onClick={onNavigateForward}
className="rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition hover:border-gray-400 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800"
title="Next 4 weeks"
>
</button>
</div>
{/* Undo / Redo */}
{(onUndo ?? onRedo) && (
<div className="flex items-center gap-1">
<button
type="button"
onClick={onUndo}
disabled={!canUndo}
title="Undo (Ctrl+Z)"
className="rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition hover:border-gray-400 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800"
>
</button>
<button
type="button"
onClick={onRedo}
disabled={!canRedo}
title="Redo (Ctrl+Shift+Z / Ctrl+Y)"
className="rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition hover:border-gray-400 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800"
>
</button>
</div>
)}
{/* View mode toggle */}
<div className="flex overflow-hidden rounded-xl border border-gray-300 bg-white text-sm dark:border-gray-600 dark:bg-gray-900">
<button
type="button"
onClick={() => onViewModeChange("resource")}
className={clsx(
"px-3 py-2 transition-colors",
viewMode === "resource"
? "bg-brand-600 text-white"
: "text-gray-700 hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-gray-800",
)}
>
Resource view
</button>
<button
type="button"
onClick={() => onViewModeChange("project")}
className={clsx(
"border-l border-gray-300 px-3 py-2 transition-colors dark:border-gray-600",
viewMode === "project"
? "bg-brand-600 text-white"
: "text-gray-700 hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-gray-800",
)}
>
Project view
</button>
</div>
{/* Filter */}
<div ref={filterAnchorRef} className="relative">
<button
type="button"
onClick={() => onFilterOpenChange(!filterOpen)}
className={clsx(
"flex items-center gap-2 rounded-xl border px-3 py-2 text-sm transition-colors",
filterOpen || activeFilterCount > 0
? "border-brand-300 bg-brand-50 text-brand-700 dark:border-brand-700 dark:bg-brand-950/40 dark:text-brand-200"
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800",
)}
>
Filter
{activeFilterCount > 0 && (
<span className="flex h-4 w-4 items-center justify-center rounded-full bg-brand-600 text-xs text-white">
{activeFilterCount}
</span>
)}
</button>
<TimelineFilter
anchorRef={filterAnchorRef}
filters={filters}
onChange={onFiltersChange}
isOpen={filterOpen}
onClose={() => onFilterOpenChange(false)}
/>
</div>
</div>
</div>
);
}