"use client"; import React, { useState, useEffect } from "react"; import ProjectTaskForm from "./ProjectTaskForm"; import TaskStatusDropdownSimple from "./TaskStatusDropdownSimple"; import { Card, CardHeader, CardContent } from "./ui/Card"; import Button from "./ui/Button"; import Badge from "./ui/Badge"; import { formatDate } from "@/lib/utils"; export default function ProjectTasksSection({ projectId }) { const [projectTasks, setProjectTasks] = useState([]); const [loading, setLoading] = useState(true); const [taskNotes, setTaskNotes] = useState({}); const [newNote, setNewNote] = useState({}); const [loadingNotes, setLoadingNotes] = useState({}); const [showAddTaskModal, setShowAddTaskModal] = useState(false); const [expandedDescriptions, setExpandedDescriptions] = useState({}); const [expandedNotes, setExpandedNotes] = useState({}); const [editingTask, setEditingTask] = useState(null); const [showEditTaskModal, setShowEditTaskModal] = useState(false); const [editTaskForm, setEditTaskForm] = useState({ priority: "", date_started: "", status: "", assigned_to: "", }); const [users, setUsers] = useState([]); useEffect(() => { const fetchProjectTasks = async () => { try { const res = await fetch(`/api/project-tasks?project_id=${projectId}`); const tasks = await res.json(); setProjectTasks(tasks); // Fetch notes for each task const notesPromises = tasks.map(async (task) => { try { const notesRes = await fetch(`/api/task-notes?task_id=${task.id}`); const notes = await notesRes.json(); return { taskId: task.id, notes }; } catch (error) { console.error(`Failed to fetch notes for task ${task.id}:`, error); return { taskId: task.id, notes: [] }; } }); const notesResults = await Promise.all(notesPromises); const notesMap = {}; notesResults.forEach(({ taskId, notes }) => { notesMap[taskId] = notes; }); setTaskNotes(notesMap); } catch (error) { console.error("Failed to fetch project tasks:", error); } finally { setLoading(false); } }; // Fetch users for assignment dropdown const fetchUsers = async () => { try { const res = await fetch("/api/project-tasks/users"); const usersData = await res.json(); setUsers(usersData); } catch (error) { console.error("Failed to fetch users:", error); } }; fetchProjectTasks(); fetchUsers(); }, [projectId]); // Handle escape key to close modals useEffect(() => { const handleEscape = (e) => { if (e.key === "Escape") { if (showEditTaskModal) { handleCloseEditModal(); } else if (showAddTaskModal) { setShowAddTaskModal(false); } } }; document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); }, [showAddTaskModal, showEditTaskModal]); // Prevent body scroll when modal is open and handle map z-index useEffect(() => { if (showAddTaskModal || showEditTaskModal) { // Prevent body scroll document.body.style.overflow = "hidden"; // Find and temporarily lower z-index of leaflet containers const leafletContainers = document.querySelectorAll(".leaflet-container"); leafletContainers.forEach((container) => { container.style.zIndex = "1"; }); // Also handle navigation and other potential high z-index elements const navElements = document.querySelectorAll("nav"); navElements.forEach((nav) => { nav.style.position = "relative"; nav.style.zIndex = "1"; }); } else { // Restore body scroll document.body.style.overflow = "unset"; // Restore leaflet container z-index const leafletContainers = document.querySelectorAll(".leaflet-container"); leafletContainers.forEach((container) => { container.style.zIndex = ""; }); // Restore navigation z-index const navElements = document.querySelectorAll("nav"); navElements.forEach((nav) => { nav.style.position = ""; nav.style.zIndex = ""; }); } // Cleanup function return () => { document.body.style.overflow = "unset"; const leafletContainers = document.querySelectorAll(".leaflet-container"); leafletContainers.forEach((container) => { container.style.zIndex = ""; }); const navElements = document.querySelectorAll("nav"); navElements.forEach((nav) => { nav.style.position = ""; nav.style.zIndex = ""; }); }; }, [showAddTaskModal, showEditTaskModal]); const refetchTasks = async () => { try { const res = await fetch(`/api/project-tasks?project_id=${projectId}`); const tasks = await res.json(); setProjectTasks(tasks); // Refresh notes for all tasks const notesPromises = tasks.map(async (task) => { try { const notesRes = await fetch(`/api/task-notes?task_id=${task.id}`); const notes = await notesRes.json(); return { taskId: task.id, notes }; } catch (error) { console.error(`Failed to fetch notes for task ${task.id}:`, error); return { taskId: task.id, notes: [] }; } }); const notesResults = await Promise.all(notesPromises); const notesMap = {}; notesResults.forEach(({ taskId, notes }) => { notesMap[taskId] = notes; }); setTaskNotes(notesMap); } catch (error) { console.error("Failed to fetch project tasks:", error); } }; const handleTaskAdded = () => { refetchTasks(); // Refresh the list setShowAddTaskModal(false); // Close the modal }; const handleStatusChange = async (taskId, newStatus) => { try { const res = await fetch(`/api/project-tasks/${taskId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: newStatus }), }); if (res.ok) { refetchTasks(); // Refresh the list } else { alert("Failed to update task status"); } } catch (error) { alert("Error updating task status"); } }; const handleDeleteTask = async (taskId) => { if (!confirm("Are you sure you want to delete this task?")) return; try { const res = await fetch(`/api/project-tasks/${taskId}`, { method: "DELETE", }); if (res.ok) { refetchTasks(); // Refresh the list } else { const errorData = await res.json(); alert("Failed to delete task: " + (errorData.error || "Unknown error")); } } catch (error) { alert("Error deleting task: " + error.message); } }; const handleAddNote = async (taskId) => { const noteContent = newNote[taskId]?.trim(); if (!noteContent) return; setLoadingNotes((prev) => ({ ...prev, [taskId]: true })); try { const res = await fetch("/api/task-notes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ task_id: taskId, note: noteContent, }), }); if (res.ok) { // Refresh notes for this task const notesRes = await fetch(`/api/task-notes?task_id=${taskId}`); const notes = await notesRes.json(); setTaskNotes((prev) => ({ ...prev, [taskId]: notes })); setNewNote((prev) => ({ ...prev, [taskId]: "" })); } else { alert("Failed to add note"); } } catch (error) { alert("Error adding note"); } finally { setLoadingNotes((prev) => ({ ...prev, [taskId]: false })); } }; const handleEditTask = (task) => { setEditingTask(task); // Format date for HTML input (YYYY-MM-DD) let dateStarted = ""; if (task.date_started) { const date = new Date(task.date_started); if (!isNaN(date.getTime())) { dateStarted = date.toISOString().split("T")[0]; } } const formData = { priority: task.priority || "", date_started: dateStarted, status: task.status || "", assigned_to: task.assigned_to || "", }; setEditTaskForm(formData); setShowEditTaskModal(true); }; const handleUpdateTask = async () => { if (!editingTask) return; try { const res = await fetch(`/api/project-tasks/${editingTask.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ priority: editTaskForm.priority, date_started: editTaskForm.date_started || null, status: editTaskForm.status, assigned_to: editTaskForm.assigned_to || null, }), }); if (res.ok) { refetchTasks(); handleCloseEditModal(); } else { alert("Failed to update task"); } } catch (error) { alert("Error updating task"); } }; const handleCloseEditModal = () => { setShowEditTaskModal(false); setEditingTask(null); setEditTaskForm({ priority: "", date_started: "", status: "", assigned_to: "", }); }; 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 toggleDescription = (taskId) => { setExpandedDescriptions((prev) => ({ ...prev, [taskId]: !prev[taskId], })); }; const toggleNotes = (taskId) => { setExpandedNotes((prev) => ({ ...prev, [taskId]: !prev[taskId], })); }; return (
No tasks assigned to this project yet.
Add a task above to get started.
| Task | Priority | {" "}Max Wait | Date Started | Status | Actions |
|---|---|---|---|---|---|
{task.task_name}{task.description && ( )} |
|
{task.max_wait_days} days | {" "}{task.date_started ? formatDate(task.date_started) : "Not started"} |
|
|
|
Description:
{task.description} |
|||||
Notes ({taskNotes[task.id]?.length || 0}){/* Existing Notes */} {taskNotes[task.id] && taskNotes[task.id].length > 0 && (
{taskNotes[task.id].map((note) => (
)}
{/* Add New Note */}
{note.is_system ? (
System
) : null}
{note.created_by_name && (
{note.created_by_name}
)}
{note.note} {" "}{formatDate(note.note_date, { includeTime: true, })}
setNewNote((prev) => ({
...prev,
[task.id]: e.target.value,
}))
}
className="flex-1 px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
onKeyPress={(e) => {
if (e.key === "Enter") {
handleAddNote(task.id);
}
}}
/>
|
|||||