843 lines
26 KiB
JavaScript
843 lines
26 KiB
JavaScript
"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 (
|
||
<div className="space-y-6">
|
||
<div className="flex items-center justify-between">
|
||
<h2 className="text-xl font-semibold text-gray-900">Project Tasks</h2>
|
||
<div className="flex items-center gap-3">
|
||
<Badge variant="default" className="text-sm">
|
||
{projectTasks.length} {projectTasks.length === 1 ? "task" : "tasks"}
|
||
</Badge>
|
||
<Button
|
||
variant="primary"
|
||
size="sm"
|
||
onClick={() => setShowAddTaskModal(true)}
|
||
>
|
||
<svg
|
||
className="w-4 h-4 mr-2"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M12 4v16m8-8H4"
|
||
/>
|
||
</svg>
|
||
Add Task
|
||
</Button>
|
||
</div>
|
||
</div>{" "}
|
||
{/* Add Task Modal */}
|
||
{showAddTaskModal && (
|
||
<div
|
||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center"
|
||
style={{ zIndex: 99999 }}
|
||
onClick={(e) => {
|
||
if (e.target === e.currentTarget) {
|
||
setShowAddTaskModal(false);
|
||
}
|
||
}}
|
||
>
|
||
<div
|
||
className="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto"
|
||
style={{ zIndex: 100000 }}
|
||
>
|
||
<div className="flex items-center justify-between p-6 border-b">
|
||
<h3 className="text-lg font-semibold text-gray-900">
|
||
Add New Task
|
||
</h3>
|
||
<button
|
||
onClick={() => setShowAddTaskModal(false)}
|
||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||
>
|
||
<svg
|
||
className="w-6 h-6"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M6 18L18 6M6 6l12 12"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div className="p-6">
|
||
<ProjectTaskForm
|
||
projectId={projectId}
|
||
onTaskAdded={handleTaskAdded}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{/* Current Tasks */}
|
||
<Card>
|
||
<CardHeader>
|
||
<h3 className="text-lg font-medium text-gray-900">Current Tasks</h3>
|
||
</CardHeader>
|
||
<CardContent className="p-0">
|
||
{loading ? (
|
||
<div className="p-6 text-center">
|
||
<div className="animate-pulse">
|
||
<div className="h-4 bg-gray-200 rounded w-1/4 mx-auto mb-2"></div>
|
||
<div className="h-3 bg-gray-200 rounded w-1/6 mx-auto"></div>
|
||
</div>
|
||
</div>
|
||
) : projectTasks.length === 0 ? (
|
||
<div className="p-6 text-center">
|
||
<div className="text-gray-400 mb-2">
|
||
<svg
|
||
className="w-12 h-12 mx-auto"
|
||
fill="currentColor"
|
||
viewBox="0 0 20 20"
|
||
>
|
||
<path
|
||
fillRule="evenodd"
|
||
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V8zm0 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2z"
|
||
clipRule="evenodd"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<p className="text-gray-500 text-sm">
|
||
No tasks assigned to this project yet.
|
||
</p>
|
||
<p className="text-gray-400 text-xs mt-1">
|
||
Add a task above to get started.
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full">
|
||
<thead className="bg-gray-50 border-b border-gray-200">
|
||
<tr>
|
||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
Task
|
||
</th>
|
||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
Priority
|
||
</th>{" "}
|
||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
Max Wait
|
||
</th>
|
||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
Date Started
|
||
</th>
|
||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||
Status
|
||
</th>
|
||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-48">
|
||
Actions
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="bg-white divide-y divide-gray-200">
|
||
{projectTasks.map((task) => (
|
||
<React.Fragment key={task.id}>
|
||
{/* Main task row */}
|
||
<tr className="hover:bg-gray-50 transition-colors">
|
||
<td className="px-4 py-4">
|
||
<div className="flex items-center gap-2">
|
||
<h4 className="text-sm font-medium text-gray-900">
|
||
{task.task_name}
|
||
</h4>
|
||
{task.description && (
|
||
<button
|
||
onClick={() => toggleDescription(task.id)}
|
||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||
title="Toggle description"
|
||
>
|
||
<svg
|
||
className={`w-4 h-4 transform transition-transform ${
|
||
expandedDescriptions[task.id]
|
||
? "rotate-180"
|
||
: ""
|
||
}`}
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M19 9l-7 7-7-7"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
)}
|
||
</div>
|
||
</td>
|
||
<td className="px-4 py-4">
|
||
<Badge
|
||
variant={getPriorityVariant(task.priority)}
|
||
size="sm"
|
||
>
|
||
{task.priority}
|
||
</Badge>
|
||
</td>
|
||
<td className="px-4 py-4 text-sm text-gray-600">
|
||
{task.max_wait_days} days
|
||
</td>{" "}
|
||
<td className="px-4 py-4 text-sm text-gray-600">
|
||
{task.date_started
|
||
? formatDate(task.date_started)
|
||
: "Not started"}
|
||
</td>
|
||
<td className="px-4 py-4">
|
||
<TaskStatusDropdownSimple
|
||
task={task}
|
||
size="sm"
|
||
onStatusChange={handleStatusChange}
|
||
/>
|
||
</td>
|
||
<td className="px-4 py-4">
|
||
<div className="flex items-center gap-1.5">
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => toggleNotes(task.id)}
|
||
className="text-xs px-2 py-1 text-blue-600 hover:text-blue-800 hover:bg-blue-50 border-0"
|
||
title={`${taskNotes[task.id]?.length || 0} notes`}
|
||
>
|
||
<svg
|
||
className="w-3 h-3 mr-1"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||
/>
|
||
</svg>
|
||
{taskNotes[task.id]?.length || 0}
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleEditTask(task)}
|
||
className="text-xs px-2 py-1 min-w-[60px]"
|
||
>
|
||
<svg
|
||
className="w-3 h-3 mr-1"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||
/>
|
||
</svg>
|
||
Edit
|
||
</Button>
|
||
<Button
|
||
variant="danger"
|
||
size="sm"
|
||
onClick={() => handleDeleteTask(task.id)}
|
||
className="text-xs px-2 py-1 min-w-[60px]"
|
||
>
|
||
<svg
|
||
className="w-3 h-3 mr-1"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||
/>
|
||
</svg>
|
||
Delete
|
||
</Button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{/* Description row (expandable) */}
|
||
{task.description && expandedDescriptions[task.id] && (
|
||
<tr className="bg-blue-50">
|
||
<td colSpan="6" className="px-4 py-3">
|
||
<div className="text-sm text-gray-700">
|
||
<span className="font-medium text-gray-900">
|
||
Description:
|
||
</span>
|
||
<p className="mt-1">{task.description}</p>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
)}{" "}
|
||
{/* Notes row (expandable) */}
|
||
{expandedNotes[task.id] && (
|
||
<tr className="bg-gray-50">
|
||
<td colSpan="6" className="px-4 py-4">
|
||
<div className="space-y-3">
|
||
<h5 className="text-sm font-medium text-gray-900">
|
||
Notes ({taskNotes[task.id]?.length || 0})
|
||
</h5>
|
||
|
||
{/* Existing Notes */}
|
||
{taskNotes[task.id] &&
|
||
taskNotes[task.id].length > 0 && (
|
||
<div className="space-y-2">
|
||
{taskNotes[task.id].map((note) => (
|
||
<div
|
||
key={note.note_id}
|
||
className={`p-3 rounded border flex justify-between items-start ${
|
||
note.is_system
|
||
? "bg-blue-50 border-blue-200"
|
||
: "bg-white border-gray-200"
|
||
}`}
|
||
>
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-2 mb-1">
|
||
{note.is_system ? (
|
||
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded-full font-medium">
|
||
System
|
||
</span>
|
||
) : null}
|
||
{note.created_by_name && (
|
||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded-full font-medium">
|
||
{note.created_by_name}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<p className="text-sm text-gray-800">
|
||
{note.note}
|
||
</p>{" "}
|
||
<p className="text-xs text-gray-500 mt-1">
|
||
{formatDate(note.note_date, {
|
||
includeTime: true,
|
||
})}
|
||
</p>
|
||
</div>
|
||
{!note.is_system && (
|
||
<button
|
||
onClick={() =>
|
||
handleDeleteNote(
|
||
note.note_id,
|
||
task.id
|
||
)
|
||
}
|
||
className="ml-2 text-red-500 hover:text-red-700 text-xs font-bold"
|
||
title="Delete note"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Add New Note */}
|
||
<div className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
placeholder="Add a note..."
|
||
value={newNote[task.id] || ""}
|
||
onChange={(e) =>
|
||
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);
|
||
}
|
||
}}
|
||
/>
|
||
<Button
|
||
size="sm"
|
||
variant="primary"
|
||
onClick={() => handleAddNote(task.id)}
|
||
disabled={
|
||
loadingNotes[task.id] ||
|
||
!newNote[task.id]?.trim()
|
||
}
|
||
>
|
||
{loadingNotes[task.id] ? "Adding..." : "Add"}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</React.Fragment>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
{/* Edit Task Modal */}
|
||
{showEditTaskModal && (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]">
|
||
<div className="bg-white rounded-lg p-6 w-full max-w-md mx-4">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h3 className="text-lg font-semibold text-gray-900">
|
||
Edit Task:{" "}
|
||
{editingTask?.custom_task_name || editingTask?.task_name}
|
||
</h3>
|
||
<button
|
||
onClick={handleCloseEditModal}
|
||
className="text-gray-400 hover:text-gray-600"
|
||
>
|
||
<svg
|
||
className="w-5 h-5"
|
||
fill="currentColor"
|
||
viewBox="0 0 20 20"
|
||
>
|
||
<path
|
||
fillRule="evenodd"
|
||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||
clipRule="evenodd"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
{/* Assignment */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Assigned To
|
||
</label>
|
||
<select
|
||
value={editTaskForm.assigned_to}
|
||
onChange={(e) =>
|
||
setEditTaskForm((prev) => ({
|
||
...prev,
|
||
assigned_to: e.target.value,
|
||
}))
|
||
}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
>
|
||
<option value="">Unassigned</option>
|
||
{users.map((user) => (
|
||
<option key={user.id} value={user.id}>
|
||
{user.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
{/* Priority */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Priority
|
||
</label>
|
||
<select
|
||
value={editTaskForm.priority}
|
||
onChange={(e) =>
|
||
setEditTaskForm((prev) => ({
|
||
...prev,
|
||
priority: e.target.value,
|
||
}))
|
||
}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
>
|
||
<option value="">Select Priority</option>
|
||
<option value="low">Low</option>
|
||
<option value="normal">Normal</option>
|
||
<option value="high">High</option>
|
||
<option value="urgent">Urgent</option>
|
||
</select>
|
||
</div>
|
||
|
||
{/* Date Started */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Date Started
|
||
</label>
|
||
<input
|
||
type="date"
|
||
value={editTaskForm.date_started}
|
||
onChange={(e) =>
|
||
setEditTaskForm((prev) => ({
|
||
...prev,
|
||
date_started: e.target.value,
|
||
}))
|
||
}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
/>
|
||
</div>
|
||
|
||
{/* Status */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Status
|
||
</label>
|
||
<select
|
||
value={editTaskForm.status}
|
||
onChange={(e) =>
|
||
setEditTaskForm((prev) => ({
|
||
...prev,
|
||
status: e.target.value,
|
||
}))
|
||
}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
>
|
||
<option value="">Select Status</option>
|
||
<option value="pending">Pending</option>
|
||
<option value="in_progress">In Progress</option>
|
||
<option value="completed">Completed</option>
|
||
<option value="cancelled">Cancelled</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-end gap-3 mt-6">
|
||
<Button variant="outline" onClick={handleCloseEditModal}>
|
||
Cancel
|
||
</Button>
|
||
<Button variant="primary" onClick={handleUpdateTask}>
|
||
Update Task
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|