import React, { useState, useRef, useEffect } from "react"; import { Repeat, Circle, X } from "lucide-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faGoogle, faMicrosoft, faApple } from "@fortawesome/free-brands-svg-icons"; import { faServer } from "@fortawesome/free-solid-svg-icons"; import { Task, KanbanStage } from "./WeeklyView"; interface GridTaskBlockProps { task: Task; date: Date; activeDate: Date; cellDuration: number; darkMode: boolean; isProtected: boolean; editingTaskId: string | null; setEditingTaskId: (id: string | null) => void; updateTask: (id: string, title: string) => void; updateTaskNotes: (id: string, notes: string) => void; updateTaskDuration: (id: string, duration: number) => void; toggleTask: (id: string) => void; deleteTask: (id: string) => void; toggleTaskRolling: (id: string) => void; setSelectedTaskForNotes: (task: Task) => void; setSelectedTaskForRecurrence: (task: Task) => void; handleDragStart: (e: React.DragEvent, task: Task) => void; handleDragEnd: (e: React.DragEvent) => void; getSlotHeight: (duration: number) => number; draggedTask: Task | null; addSubTask: (parentId: string, title: string) => void; toggleSubTask: (id: string) => void; updateSubTask: (id: string, title: string) => void; deleteSubTask: (id: string) => void; onSetEditingTaskId?: (id: string | null) => void; workingHoursStart: number; showTaskCheckboxes?: boolean; projects?: any[]; onProjectAssign?: (taskId: string, projectId: string | null) => void; kanbanStages?: KanbanStage[]; } export function GridTaskBlock({ task, date, activeDate, cellDuration, darkMode, isProtected, editingTaskId, setEditingTaskId, updateTask, updateTaskNotes, updateTaskDuration, toggleTask, deleteTask, toggleTaskRolling, setSelectedTaskForNotes, setSelectedTaskForRecurrence, handleDragStart, handleDragEnd, getSlotHeight, draggedTask, addSubTask, toggleSubTask, updateSubTask, deleteSubTask, onSetEditingTaskId, workingHoursStart, showTaskCheckboxes, projects, onProjectAssign, kanbanStages = [] }: GridTaskBlockProps) { const [isNotesOpen, setIsNotesOpen] = useState(false); const [notesValue, setNotesValue] = useState(task.markdownContent || ""); const [isSubTasksOpen, setIsSubTasksOpen] = useState(false); const [isSubTaskInputOpen, setIsSubTaskInputOpen] = useState(false); const [newSubTaskTitle, setNewSubTaskTitle] = useState(""); const [showProjectPicker, setShowProjectPicker] = useState(false); const notesRef = useRef(null); const subTaskInputRef = useRef(null); const projectPickerRef = useRef(null); // Touch: tap-to-reveal actions const [touchActive, setTouchActive] = useState(false); const blockRef = useRef(null); useEffect(() => { if (!touchActive) return; const handler = (e: Event) => { if (blockRef.current && !blockRef.current.contains(e.target as Node)) setTouchActive(false); }; document.addEventListener("touchstart", handler); document.addEventListener("mousedown", handler); return () => { document.removeEventListener("touchstart", handler); document.removeEventListener("mousedown", handler); }; }, [touchActive]); // Resize State const [isResizing, setIsResizing] = useState(false); const [resizeHeight, setResizeHeight] = useState(null); const resizeStartY = useRef(0); const resizeStartHeight = useRef(0); // Focus notes when opened useEffect(() => { if (isNotesOpen && notesRef.current) { notesRef.current.focus(); } }, [isNotesOpen]); const handleNotesBlur = () => { if (notesValue !== task.markdownContent) { updateTaskNotes(task.id, notesValue); } }; const insertMarkdown = (prefix: string, suffix: string = "") => { if (!notesRef.current) return; const start = notesRef.current.selectionStart; const end = notesRef.current.selectionEnd; const text = notesValue; const before = text.substring(0, start); const selection = text.substring(start, end); const after = text.substring(end); const newText = `${before}${prefix}${selection}${suffix}${after}`; setNotesValue(newText); setTimeout(() => { if (notesRef.current) { notesRef.current.focus(); const newCursorPos = start + prefix.length + selection.length + suffix.length; notesRef.current.setSelectionRange(newCursorPos, newCursorPos); } }, 0); }; const pixelsPerMinute = getSlotHeight(cellDuration) / cellDuration; useEffect(() => { if (!task.startTime) return; const handleResizeMove = (e: MouseEvent) => { if (!isResizing) return; const deltaY = e.clientY - resizeStartY.current; let newHeight = resizeStartHeight.current + deltaY; const minHeight = 15 * pixelsPerMinute; if (newHeight < minHeight) newHeight = minHeight; setResizeHeight(newHeight); }; const handleResizeEnd = () => { if (!isResizing) return; setIsResizing(false); document.body.style.cursor = ""; if (resizeHeight !== null) { const newDurationMins = Math.round(resizeHeight / pixelsPerMinute / 15) * 15; updateTaskDuration(task.id, newDurationMins); } setResizeHeight(null); }; if (isResizing) { window.addEventListener("mousemove", handleResizeMove); window.addEventListener("mouseup", handleResizeEnd); } return () => { window.removeEventListener("mousemove", handleResizeMove); window.removeEventListener("mouseup", handleResizeEnd); }; }, [isResizing, resizeHeight, pixelsPerMinute, task.id, updateTaskDuration, task.startTime]); // Conditional rendering should only happen after hooks if (!task.startTime) return null; const [startHour, startMinute] = task.startTime.split(":").map(Number); const startMinutes = (startHour - workingHoursStart) * 60 + startMinute; const topOffset = startMinutes * pixelsPerMinute; const duration = task.duration || 15; const baseHeight = duration * pixelsPerMinute; const currentHeight = isResizing && resizeHeight !== null ? resizeHeight : baseHeight; const onResizeStart = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); setIsResizing(true); resizeStartY.current = e.clientY; resizeStartHeight.current = baseHeight; document.body.style.cursor = "ns-resize"; }; return (
{ const stageColor = task.kanbanStage ? kanbanStages.find(s => s.id === task.kanbanStage)?.color : null; if (stageColor) return `4px solid ${stageColor}`; if (task.project?.color) return `4px solid ${task.project.color}`; return (isNotesOpen || isSubTasksOpen || isResizing) ? `1px solid ${darkMode ? "#404040" : "#e0e0e0"}` : "none"; })(), borderRadius: (isNotesOpen || isSubTasksOpen || isResizing) ? "4px" : "0", padding: (task.kanbanStage && kanbanStages.some(s => s.id === task.kanbanStage)) || task.project?.color ? "2px 4px 2px 8px" : "2px 4px", boxShadow: (isNotesOpen || isSubTasksOpen || isResizing) ? "0 1px 3px rgba(0,0,0,0.05)" : "none", display: "flex", flexDirection: "column", overflow: "visible !important", }} draggable={!editingTaskId && !isResizing} onDragStart={(e) => handleDragStart(e, task)} onDragEnd={handleDragEnd} onClick={(e) => { e.stopPropagation(); // Touch: toggle action toolbar on tap instead of toggling completion if (window.matchMedia("(pointer: coarse)").matches && editingTaskId !== task.id) { const target = e.target as HTMLElement; if (target.closest(".task-actions") || target.closest("button")) return; setTouchActive(!touchActive); return; } if (editingTaskId !== task.id) toggleTask(task.id); }} >
{editingTaskId === task.id ? (
{ e.preventDefault(); const input = e.currentTarget.elements.namedItem("title") as HTMLTextAreaElement; updateTask(task.id, input.value || ""); }} onClick={(e) => e.stopPropagation()} style={{ width: "100%", paddingRight: "20px" }} >