{dates.map((date, i) => {
const t = date.getTime();
const totalH = hoursOnDay(allocs, t);
@@ -677,9 +912,11 @@ function renderLoadGraph(allocs: TimelineAssignmentEntry[], dates: Date[], CELL_
key={i}
className={clsx(
"absolute bottom-0 rounded-t-sm",
- totalH > 12 ? "bg-red-500 opacity-80"
- : totalH > 8 ? "bg-amber-400 opacity-80"
- : "bg-brand-500 opacity-80",
+ totalH > 12
+ ? "bg-red-500 opacity-80"
+ : totalH > 8
+ ? "bg-amber-400 opacity-80"
+ : "bg-brand-500 opacity-80",
)}
style={{ left: i * CELL_WIDTH + 3, width: CELL_WIDTH - 6, height: totalBarH }}
/>
@@ -691,13 +928,20 @@ function renderLoadGraph(allocs: TimelineAssignmentEntry[], dates: Date[], CELL_
// ─── Heatmap-mode: utilisation colour overlay ────────────────────────────────
-function renderHeatmapOverlay(allocs: TimelineAssignmentEntry[], dates: Date[], CELL_WIDTH: number, heatmapScheme: HeatmapColorScheme) {
+function renderHeatmapOverlay(
+ allocs: TimelineAssignmentEntry[],
+ dates: Date[],
+ CELL_WIDTH: number,
+ heatmapScheme: HeatmapColorScheme,
+) {
const REF_H = 8;
return dates.map((date, i) => {
const t = date.getTime();
const totalH = allocs.reduce((sum, a) => {
- const s = new Date(a.startDate); s.setHours(0, 0, 0, 0);
- const e = new Date(a.endDate); e.setHours(0, 0, 0, 0);
+ const s = new Date(a.startDate);
+ s.setHours(0, 0, 0, 0);
+ const e = new Date(a.endDate);
+ e.setHours(0, 0, 0, 0);
return t >= s.getTime() && t <= e.getTime() ? sum + a.hoursPerDay : sum;
}, 0);
const bg = heatmapBgColor((totalH / REF_H) * 100, heatmapScheme);
@@ -722,6 +966,11 @@ function renderDailyBars(
allocDragState: AllocDragState,
onAllocMouseDown: (e: React.MouseEvent, info: AllocMouseDownInfo) => void,
onAllocTouchStart: (e: React.TouchEvent, info: AllocMouseDownInfo) => void,
+ onAllocationContextMenu: (
+ info: { allocationId: string; projectId: string },
+ anchorX: number,
+ anchorY: number,
+ ) => void,
toLeft: (d: Date) => number,
toWidth: (s: Date, e: Date) => number,
totalCanvasWidth: number,
@@ -734,9 +983,16 @@ function renderDailyBars(
const covering = allocs.filter((a) => {
const isDragged = allocDragState.isActive && allocDragState.allocationId === a.id;
- const s = new Date(isDragged && allocDragState.currentStartDate ? allocDragState.currentStartDate : a.startDate);
- const e = new Date(isDragged && allocDragState.currentEndDate ? allocDragState.currentEndDate : a.endDate);
- s.setHours(0, 0, 0, 0); e.setHours(0, 0, 0, 0);
+ const s = new Date(
+ isDragged && allocDragState.currentStartDate
+ ? allocDragState.currentStartDate
+ : a.startDate,
+ );
+ const e = new Date(
+ isDragged && allocDragState.currentEndDate ? allocDragState.currentEndDate : a.endDate,
+ );
+ s.setHours(0, 0, 0, 0);
+ e.setHours(0, 0, 0, 0);
return t >= s.getTime() && t <= e.getTime();
});
@@ -747,20 +1003,33 @@ function renderDailyBars(
let stackedH = 0;
const segs: React.ReactNode[] = covering.map((alloc) => {
- const colors = ORDER_TYPE_COLORS[alloc.project.orderType] ?? { bg: "bg-gray-400", text: "text-white", light: "" };
- const segH = Math.max(2, Math.min(
- BAR_AREA - stackedH,
- Math.round((alloc.hoursPerDay / REF_H) * BAR_AREA),
- ));
+ const colors = ORDER_TYPE_COLORS[alloc.project.orderType] ?? {
+ bg: "bg-gray-400",
+ text: "text-white",
+ light: "",
+ };
+ const segH = Math.max(
+ 2,
+ Math.min(BAR_AREA - stackedH, Math.round((alloc.hoursPerDay / REF_H) * BAR_AREA)),
+ );
const bottom = 4 + stackedH;
stackedH += segH;
const isBeingDragged = allocDragState.isActive && allocDragState.allocationId === alloc.id;
- const dispStart = new Date(isBeingDragged && allocDragState.currentStartDate ? allocDragState.currentStartDate : alloc.startDate);
- const dispEnd = new Date(isBeingDragged && allocDragState.currentEndDate ? allocDragState.currentEndDate : alloc.endDate);
- dispStart.setHours(0, 0, 0, 0); dispEnd.setHours(0, 0, 0, 0);
+ const dispStart = new Date(
+ isBeingDragged && allocDragState.currentStartDate
+ ? allocDragState.currentStartDate
+ : alloc.startDate,
+ );
+ const dispEnd = new Date(
+ isBeingDragged && allocDragState.currentEndDate
+ ? allocDragState.currentEndDate
+ : alloc.endDate,
+ );
+ dispStart.setHours(0, 0, 0, 0);
+ dispEnd.setHours(0, 0, 0, 0);
const isFirstDay = t === dispStart.getTime();
- const isLastDay = t === dispEnd.getTime();
+ const isLastDay = t === dispEnd.getTime();
const EDGE_W = CELL_WIDTH >= 16 ? 4 : 0;
const allocInfo: AllocMouseDownInfo = {
@@ -785,27 +1054,53 @@ function renderDailyBars(
: "hover:opacity-80 z-[10]",
)}
style={{ left: i * CELL_WIDTH + 2, width: CELL_WIDTH - 4, height: segH, bottom }}
- onContextMenu={(e) => e.preventDefault()}
+ onContextMenu={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onAllocationContextMenu(
+ { allocationId: alloc.id, projectId: alloc.projectId },
+ e.clientX,
+ e.clientY,
+ );
+ }}
>
{isFirstDay && EDGE_W > 0 && (
{ e.stopPropagation(); onAllocMouseDown(e, { ...allocInfo, mode: "resize-start" }); }}
- onTouchStart={(e) => { e.stopPropagation(); onAllocTouchStart(e, { ...allocInfo, mode: "resize-start" }); }}
+ onMouseDown={(e) => {
+ e.stopPropagation();
+ onAllocMouseDown(e, { ...allocInfo, mode: "resize-start" });
+ }}
+ onTouchStart={(e) => {
+ e.stopPropagation();
+ onAllocTouchStart(e, { ...allocInfo, mode: "resize-start" });
+ }}
/>
)}
{ e.stopPropagation(); onAllocMouseDown(e, allocInfo); }}
- onTouchStart={(e) => { e.stopPropagation(); onAllocTouchStart(e, allocInfo); }}
+ onMouseDown={(e) => {
+ e.stopPropagation();
+ onAllocMouseDown(e, allocInfo);
+ }}
+ onTouchStart={(e) => {
+ e.stopPropagation();
+ onAllocTouchStart(e, allocInfo);
+ }}
/>
{isLastDay && EDGE_W > 0 && (
{ e.stopPropagation(); onAllocMouseDown(e, { ...allocInfo, mode: "resize-end" }); }}
- onTouchStart={(e) => { e.stopPropagation(); onAllocTouchStart(e, { ...allocInfo, mode: "resize-end" }); }}
+ onMouseDown={(e) => {
+ e.stopPropagation();
+ onAllocMouseDown(e, { ...allocInfo, mode: "resize-end" });
+ }}
+ onTouchStart={(e) => {
+ e.stopPropagation();
+ onAllocTouchStart(e, { ...allocInfo, mode: "resize-end" });
+ }}
/>
)}
diff --git a/apps/web/src/components/timeline/TimelineToolbar.tsx b/apps/web/src/components/timeline/TimelineToolbar.tsx
index 88e89b7..f2b51ab 100644
--- a/apps/web/src/components/timeline/TimelineToolbar.tsx
+++ b/apps/web/src/components/timeline/TimelineToolbar.tsx
@@ -3,6 +3,7 @@
import { clsx } from "clsx";
import { useRef } from "react";
import { TimelineFilter, type TimelineFilters } from "./TimelineFilter.js";
+import { TimelineQuickFilters } from "./TimelineQuickFilters.js";
interface TimelineToolbarProps {
viewMode: "resource" | "project";
@@ -42,9 +43,22 @@ export function TimelineToolbar({
onRedo,
}: TimelineToolbarProps) {
const activeFilterCount =
- filters.chapters.length + filters.eids.length + filters.projectIds.length;
+ filters.clientIds.length +
+ filters.chapters.length +
+ filters.eids.length +
+ filters.projectIds.length;
const filterAnchorRef = useRef
(null);
+ function clearQuickFilters() {
+ onFiltersChange({
+ ...filters,
+ clientIds: [],
+ chapters: [],
+ eids: [],
+ projectIds: [],
+ });
+ }
+
return (
@@ -54,6 +68,17 @@ export function TimelineToolbar({
+
+ {activeFilterCount > 0 && (
+
+ )}
+
{/* Timeline navigation */}