diff --git a/apps/web/src/components/gantt/GanttChart.test.tsx b/apps/web/src/components/gantt/GanttChart.test.tsx index 2ed6f9b..8e25088 100644 --- a/apps/web/src/components/gantt/GanttChart.test.tsx +++ b/apps/web/src/components/gantt/GanttChart.test.tsx @@ -91,7 +91,7 @@ describe("GanttChart", () => { render(); const taskRow = screen.getAllByText("Completed Task")[0].closest("[role='row']"); - expect(taskRow).toHaveClass(/completed/i); + expect(taskRow?.className).toMatch(/Completed/i); }); it("should visually distinguish in-progress tasks", () => { @@ -106,7 +106,7 @@ describe("GanttChart", () => { render(); const taskRow = screen.getAllByText("Active Task")[0].closest("[role='row']"); - expect(taskRow).toHaveClass(/in-progress/i); + expect(taskRow?.className).toMatch(/InProgress/i); }); }); diff --git a/apps/web/src/components/gantt/GanttChart.tsx b/apps/web/src/components/gantt/GanttChart.tsx index 63b4363..8539ba7 100644 --- a/apps/web/src/components/gantt/GanttChart.tsx +++ b/apps/web/src/components/gantt/GanttChart.tsx @@ -3,7 +3,8 @@ import type { GanttTask, GanttChartProps, TimelineRange, GanttBarPosition } from "./types"; import { TaskStatus } from "@mosaic/shared"; import { formatDate, isPastTarget, isApproachingTarget } from "@/lib/utils/date-format"; -import { useMemo } from "react"; +import { useMemo, useCallback } from "react"; +import styles from "./gantt.module.css"; /** * Represents a dependency line between two tasks @@ -111,11 +112,11 @@ function getStatusClass(status: TaskStatus): string { function getRowStatusClass(status: TaskStatus): string { switch (status) { case TaskStatus.COMPLETED: - return "gantt-row-completed"; + return styles.rowCompleted; case TaskStatus.IN_PROGRESS: - return "gantt-row-in-progress"; + return styles.rowInProgress; case TaskStatus.PAUSED: - return "gantt-row-paused"; + return styles.rowPaused; default: return ""; } @@ -232,20 +233,26 @@ export function GanttChart({ [showDependencies, sortedTasks, timelineRange] ); - const handleTaskClick = (task: GanttTask) => (): void => { - if (onTaskClick) { - onTaskClick(task); - } - }; - - const handleKeyDown = (task: GanttTask) => (e: React.KeyboardEvent): void => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); + const handleTaskClick = useCallback( + (task: GanttTask) => (): void => { if (onTaskClick) { onTaskClick(task); } - } - }; + }, + [onTaskClick] + ); + + const handleKeyDown = useCallback( + (task: GanttTask) => (e: React.KeyboardEvent): void => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + if (onTaskClick) { + onTaskClick(task); + } + } + }, + [onTaskClick] + ); return (
- - {/* CSS for status classes */} - ); } diff --git a/apps/web/src/components/gantt/gantt.module.css b/apps/web/src/components/gantt/gantt.module.css new file mode 100644 index 0000000..a81d090 --- /dev/null +++ b/apps/web/src/components/gantt/gantt.module.css @@ -0,0 +1,12 @@ +/* Gantt Chart Status Row Styles */ +.rowCompleted { + background-color: #f0fdf4; /* green-50 */ +} + +.rowInProgress { + background-color: #eff6ff; /* blue-50 */ +} + +.rowPaused { + background-color: #fefce8; /* yellow-50 */ +} diff --git a/apps/web/src/components/gantt/types.ts b/apps/web/src/components/gantt/types.ts index b4151b1..ef5ef3b 100644 --- a/apps/web/src/components/gantt/types.ts +++ b/apps/web/src/components/gantt/types.ts @@ -58,31 +58,49 @@ export interface GanttChartProps { showDependencies?: boolean; } +/** + * Type guard to check if a value is a valid date string + */ +function isDateString(value: unknown): value is string { + return typeof value === 'string' && !isNaN(Date.parse(value)); +} + +/** + * Type guard to check if a value is an array of strings + */ +function isStringArray(value: unknown): value is string[] { + return Array.isArray(value) && value.every((item) => typeof item === 'string'); +} + /** * Helper to convert a base Task to GanttTask * Uses createdAt as startDate if not in metadata, dueDate as endDate */ export function toGanttTask(task: Task): GanttTask | null { // For Gantt chart, we need both start and end dates - const startDate = - (task.metadata?.startDate as string | undefined) - ? new Date(task.metadata.startDate as string) - : task.createdAt; - - const endDate = task.dueDate || new Date(); + const metadataStartDate = task.metadata?.startDate; + const startDate = isDateString(metadataStartDate) + ? new Date(metadataStartDate) + : task.createdAt; + + const endDate = task.dueDate ?? new Date(); // Validate dates if (!startDate || !endDate) { return null; } + // Extract dependencies with type guard + const metadataDependencies = task.metadata?.dependencies; + const dependencies = isStringArray(metadataDependencies) + ? metadataDependencies + : undefined; + return { ...task, startDate, endDate, - dependencies: Array.isArray(task.metadata?.dependencies) - ? (task.metadata.dependencies as string[]) - : undefined, + dependencies, isMilestone: task.metadata?.isMilestone === true, }; }