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 */}
+
+
+
+ 💬
+
+
+
+
+
+
+ );
+}