From 9b6307eabe48a698efe1417ad27d7489c3203821 Mon Sep 17 00:00:00 2001 From: Chop Date: Thu, 17 Jul 2025 22:49:12 +0200 Subject: [PATCH] feat: Add TaskCommentsModal for viewing and managing task comments --- src/components/ProjectTasksList.js | 54 +++- src/components/TaskCommentsModal.js | 369 ++++++++++++++++++++++++++++ 2 files changed, 417 insertions(+), 6 deletions(-) create mode 100644 src/components/TaskCommentsModal.js diff --git a/src/components/ProjectTasksList.js b/src/components/ProjectTasksList.js index 83166a8..6c22fbf 100644 --- a/src/components/ProjectTasksList.js +++ b/src/components/ProjectTasksList.js @@ -5,6 +5,7 @@ import { Card, CardHeader, CardContent } from "./ui/Card"; import Button from "./ui/Button"; import Badge from "./ui/Badge"; import TaskStatusDropdownSimple from "./TaskStatusDropdownSimple"; +import TaskCommentsModal from "./TaskCommentsModal"; import SearchBar from "./ui/SearchBar"; import { Select } from "./ui/Input"; import Link from "next/link"; @@ -20,6 +21,8 @@ export default function ProjectTasksList() { const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); const [groupBy, setGroupBy] = useState("none"); + const [selectedTask, setSelectedTask] = useState(null); + const [showCommentsModal, setShowCommentsModal] = useState(false); useEffect(() => { const fetchAllTasks = async () => { @@ -35,7 +38,19 @@ export default function ProjectTasksList() { }; fetchAllTasks(); - }, []); // Calculate task status based on date_added and max_wait_days + }, []); + + // Handle escape key to close modal + useEffect(() => { + const handleEscape = (e) => { + if (e.key === "Escape" && showCommentsModal) { + handleCloseComments(); + } + }; + + document.addEventListener("keydown", handleEscape); + return () => document.removeEventListener("keydown", handleEscape); + }, [showCommentsModal]); // Calculate task status based on date_added and max_wait_days const getTaskStatus = (task) => { if (task.status === "completed" || task.status === "cancelled") { return { type: "completed", days: 0 }; @@ -230,6 +245,16 @@ export default function ProjectTasksList() { } }; + const handleShowComments = (task) => { + setSelectedTask(task); + setShowCommentsModal(true); + }; + + const handleCloseComments = () => { + setShowCommentsModal(false); + setSelectedTask(null); + }; + const getPriorityVariant = (priority) => { switch (priority) { case "urgent": @@ -364,11 +389,21 @@ export default function ProjectTasksList() { )} - +
+ + +
); @@ -582,6 +617,13 @@ export default function ProjectTasksList() { /> + + {/* Comments Modal */} + ); } diff --git a/src/components/TaskCommentsModal.js b/src/components/TaskCommentsModal.js new file mode 100644 index 0000000..274bd5f --- /dev/null +++ b/src/components/TaskCommentsModal.js @@ -0,0 +1,369 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import Button from "./ui/Button"; +import Badge from "./ui/Badge"; +import { formatDate } from "@/lib/utils"; +import { formatDistanceToNow, parseISO } from "date-fns"; + +export default function TaskCommentsModal({ task, isOpen, onClose }) { + const [notes, setNotes] = useState([]); + const [loading, setLoading] = useState(true); + const [newNote, setNewNote] = useState(""); + const [loadingAdd, setLoadingAdd] = useState(false); + + useEffect(() => { + if (isOpen && task) { + fetchNotes(); + } + }, [isOpen, task]); + + const fetchNotes = async () => { + if (!task?.id) return; + + try { + setLoading(true); + const res = await fetch(`/api/task-notes?task_id=${task.id}`); + const data = await res.json(); + setNotes(data); + } catch (error) { + console.error("Failed to fetch notes:", error); + } finally { + setLoading(false); + } + }; + + const handleAddNote = async () => { + if (!newNote.trim() || !task?.id) return; + + try { + setLoadingAdd(true); + const res = await fetch("/api/task-notes", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + task_id: task.id, + note: newNote.trim(), + }), + }); + + if (res.ok) { + setNewNote(""); + await fetchNotes(); // Refresh notes + } else { + alert("Failed to add note"); + } + } catch (error) { + alert("Error adding note"); + } finally { + setLoadingAdd(false); + } + }; + + const handleDeleteNote = async (noteId) => { + if (!confirm("Are you sure you want to delete this note?")) return; + + try { + const res = await fetch("/api/task-notes", { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ note_id: noteId }), + }); + + if (res.ok) { + await fetchNotes(); // Refresh notes + } else { + alert("Failed to delete note"); + } + } catch (error) { + alert("Error deleting note"); + } + }; + + const handleKeyDown = (e) => { + if (e.key === "Escape") { + onClose(); + } else if (e.key === "Enter" && e.ctrlKey) { + handleAddNote(); + } + }; + + const getPriorityVariant = (priority) => { + switch (priority) { + case "urgent": + return "urgent"; + case "high": + return "high"; + case "normal": + return "normal"; + case "low": + return "low"; + default: + return "default"; + } + }; + + const getStatusVariant = (status) => { + switch (status) { + case "completed": + case "cancelled": + return "success"; + case "in_progress": + return "secondary"; + case "pending": + return "primary"; + default: + return "default"; + } + }; + + const formatTaskDate = (dateString, label) => { + if (!dateString) return null; + try { + const date = dateString.includes("T") ? parseISO(dateString) : new Date(dateString); + return { + label, + relative: formatDistanceToNow(date, { addSuffix: true }), + absolute: formatDate(date, { includeTime: true }) + }; + } catch (error) { + return { label, relative: dateString, absolute: dateString }; + } + }; + + if (!isOpen) return null; + + return ( +
e.target === e.currentTarget && onClose()} + > +
+ {/* Header */} +
+
+
+
+

+ {task?.task_name} +

+ + {task?.priority} + + + {task?.status?.replace('_', ' ')} + +
+

+ Project: {task?.project_name} +

+
+ +
+ + {/* Task Details Grid */} +
+ {/* Location Information */} + {(task?.city || task?.address) && ( +
+

Location

+ {task?.city && ( +

{task.city}

+ )} + {task?.address && ( +

{task.address}

+ )} +
+ )} + + {/* Assignment Information */} +
+

Assignment

+ {task?.assigned_to_name ? ( +
+

{task.assigned_to_name}

+

{task.assigned_to_email}

+
+ ) : ( +

Unassigned

+ )} +
+ + {/* Task Timing */} +
+

Timing

+ {task?.max_wait_days && ( +

Max wait: {task.max_wait_days} days

+ )} + {(() => { + if (task?.status === "completed" && task?.date_completed) { + const dateInfo = formatTaskDate(task.date_completed, "Completed"); + return ( +
+

{dateInfo.relative}

+

{dateInfo.absolute}

+
+ ); + } else if (task?.status === "in_progress" && task?.date_started) { + const dateInfo = formatTaskDate(task.date_started, "Started"); + return ( +
+

Started {dateInfo.relative}

+

{dateInfo.absolute}

+
+ ); + } else if (task?.date_added) { + const dateInfo = formatTaskDate(task.date_added, "Created"); + return ( +
+

Created {dateInfo.relative}

+

{dateInfo.absolute}

+
+ ); + } + return null; + })()} +
+ + {/* Task Description */} + {task?.description && ( +
+

Description

+

{task.description}

+
+ )} +
+
+ + {/* Content */} +
+ {loading ? ( +
+
+
+
+
+
+
+ ) : ( +
+
+
+ Comments & Activity +
+ + {notes.length} + +
+ + {notes.length === 0 ? ( +
+
+ 💬 +
+

No comments yet

+

Be the first to add a comment!

+
+ ) : ( +
+ {notes.map((note) => ( +
+
+
+ {note.is_system ? ( + + 🤖 + System + + ) : ( + + 👤 + {note.created_by_name || 'User'} + + )} + + {formatDate(note.note_date, { + includeTime: true, + })} + +
+

+ {note.note} +

+
+ {!note.is_system && ( + + )} +
+ ))} +
+ )} +
+ )} +
+ + {/* Footer - Add new comment */} +
+
+
+ 💬 + +
+