From 51d37fc65af8d3b1e11670c3eb0383f0be38d1e2 Mon Sep 17 00:00:00 2001 From: Chop <28534054+RChopin@users.noreply.github.com> Date: Thu, 10 Jul 2025 23:35:17 +0200 Subject: [PATCH] feat: Implement task editing functionality with validation and user assignment --- src/app/api/project-tasks/[id]/route.js | 45 ++++- src/components/ProjectTasksSection.js | 240 ++++++++++++++++++++++-- src/lib/queries/tasks.js | 113 ++++++++++- 3 files changed, 372 insertions(+), 26 deletions(-) diff --git a/src/app/api/project-tasks/[id]/route.js b/src/app/api/project-tasks/[id]/route.js index ce96dd8..3ee8cc5 100644 --- a/src/app/api/project-tasks/[id]/route.js +++ b/src/app/api/project-tasks/[id]/route.js @@ -1,13 +1,45 @@ import { updateProjectTaskStatus, deleteProjectTask, + updateProjectTask, } from "@/lib/queries/tasks"; import { NextResponse } from "next/server"; import { withUserAuth } from "@/lib/middleware/auth"; -// PATCH: Update project task status +// PUT: Update project task (general update) async function updateProjectTaskHandler(req, { params }) { try { + const { id } = await params; + const updates = await req.json(); + + // Validate that we have at least one field to update + const allowedFields = ["priority", "status", "assigned_to", "date_started"]; + const hasValidFields = Object.keys(updates).some((key) => + allowedFields.includes(key) + ); + + if (!hasValidFields) { + return NextResponse.json( + { error: "No valid fields provided for update" }, + { status: 400 } + ); + } + + updateProjectTask(id, updates, req.user?.id || null); + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error updating task:", error); + return NextResponse.json( + { error: "Failed to update project task", details: error.message }, + { status: 500 } + ); + } +} + +// PATCH: Update project task status +async function updateProjectTaskStatusHandler(req, { params }) { + try { + const { id } = await params; const { status } = await req.json(); if (!status) { @@ -17,7 +49,7 @@ async function updateProjectTaskHandler(req, { params }) { ); } - updateProjectTaskStatus(params.id, status, req.user?.id || null); + updateProjectTaskStatus(id, status, req.user?.id || null); return NextResponse.json({ success: true }); } catch (error) { console.error("Error updating task status:", error); @@ -31,16 +63,19 @@ async function updateProjectTaskHandler(req, { params }) { // DELETE: Delete a project task async function deleteProjectTaskHandler(req, { params }) { try { - deleteProjectTask(params.id); + const { id } = await params; + const result = deleteProjectTask(id); return NextResponse.json({ success: true }); } catch (error) { + console.error("Error in deleteProjectTaskHandler:", error); return NextResponse.json( - { error: "Failed to delete project task" }, + { error: "Failed to delete project task", details: error.message }, { status: 500 } ); } } // Protected routes - require authentication -export const PATCH = withUserAuth(updateProjectTaskHandler); +export const PUT = withUserAuth(updateProjectTaskHandler); +export const PATCH = withUserAuth(updateProjectTaskStatusHandler); export const DELETE = withUserAuth(deleteProjectTaskHandler); diff --git a/src/components/ProjectTasksSection.js b/src/components/ProjectTasksSection.js index d435d7f..981350f 100644 --- a/src/components/ProjectTasksSection.js +++ b/src/components/ProjectTasksSection.js @@ -17,6 +17,15 @@ export default function ProjectTasksSection({ projectId }) { 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 { @@ -49,22 +58,38 @@ export default function ProjectTasksSection({ projectId }) { } }; + // 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 modal + // Handle escape key to close modals useEffect(() => { const handleEscape = (e) => { - if (e.key === "Escape" && showAddTaskModal) { - setShowAddTaskModal(false); + if (e.key === "Escape") { + if (showEditTaskModal) { + handleCloseEditModal(); + } else if (showAddTaskModal) { + setShowAddTaskModal(false); + } } }; document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); - }, [showAddTaskModal]); + }, [showAddTaskModal, showEditTaskModal]); // Prevent body scroll when modal is open and handle map z-index useEffect(() => { - if (showAddTaskModal) { + if (showAddTaskModal || showEditTaskModal) { // Prevent body scroll document.body.style.overflow = "hidden"; @@ -111,7 +136,7 @@ export default function ProjectTasksSection({ projectId }) { nav.style.zIndex = ""; }); }; - }, [showAddTaskModal]); + }, [showAddTaskModal, showEditTaskModal]); const refetchTasks = async () => { try { const res = await fetch(`/api/project-tasks?project_id=${projectId}`); @@ -171,10 +196,11 @@ export default function ProjectTasksSection({ projectId }) { if (res.ok) { refetchTasks(); // Refresh the list } else { - alert("Failed to delete task"); + const errorData = await res.json(); + alert("Failed to delete task: " + (errorData.error || "Unknown error")); } } catch (error) { - alert("Error deleting task"); + alert("Error deleting task: " + error.message); } }; @@ -210,26 +236,66 @@ export default function ProjectTasksSection({ projectId }) { } }; - const handleDeleteNote = async (noteId, taskId) => { - if (!confirm("Are you sure you want to delete this note?")) return; + 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/task-notes?note_id=${noteId}`, { - method: "DELETE", + 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) { - // 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 })); + refetchTasks(); + handleCloseEditModal(); } else { - alert("Failed to delete note"); + alert("Failed to update task"); } } catch (error) { - alert("Error deleting note"); + alert("Error updating task"); } }; + + const handleCloseEditModal = () => { + setShowEditTaskModal(false); + setEditingTask(null); + setEditTaskForm({ + priority: "", + date_started: "", + status: "", + assigned_to: "", + }); + }; + const getPriorityVariant = (priority) => { switch (priority) { case "urgent": @@ -447,7 +513,7 @@ export default function ProjectTasksSection({ projectId }) { {task.date_started ? formatDate(task.date_started) : "Not started"} - {" "} + Notes ({taskNotes[task.id]?.length || 0}) + + + +
+ {/* Assignment */} +
+ + +
+ + {/* Priority */} +
+ + +
+ + {/* Date Started */} +
+ + + 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" + /> +
+ + {/* Status */} +
+ + +
+
+ +
+ + +
+ + + )} ); } diff --git a/src/lib/queries/tasks.js b/src/lib/queries/tasks.js index 05f185a..c9d1abf 100644 --- a/src/lib/queries/tasks.js +++ b/src/lib/queries/tasks.js @@ -192,8 +192,13 @@ export function updateProjectTaskStatus(taskId, status, userId = null) { // Delete a project task export function deleteProjectTask(taskId) { - const stmt = db.prepare("DELETE FROM project_tasks WHERE id = ?"); - return stmt.run(taskId); + // First delete all related task notes + const deleteNotesStmt = db.prepare("DELETE FROM notes WHERE task_id = ?"); + deleteNotesStmt.run(taskId); + + // Then delete the task itself + const deleteTaskStmt = db.prepare("DELETE FROM project_tasks WHERE id = ?"); + return deleteTaskStmt.run(taskId); } // Get project tasks assigned to a specific user @@ -291,3 +296,107 @@ export function getAllUsersForTaskAssignment() { ) .all(); } + +// Update project task (general update for edit modal) +export function updateProjectTask(taskId, updates, userId = null) { + // Get current task for logging + const getCurrentTask = db.prepare(` + SELECT + pt.*, + COALESCE(pt.custom_task_name, t.name) as task_name + FROM project_tasks pt + LEFT JOIN tasks t ON pt.task_template_id = t.task_id + WHERE pt.id = ? + `); + const currentTask = getCurrentTask.get(taskId); + + if (!currentTask) { + throw new Error(`Task with ID ${taskId} not found`); + } + + // Build dynamic update query + const fields = []; + const values = []; + + if (updates.priority !== undefined) { + fields.push("priority = ?"); + values.push(updates.priority); + } + + if (updates.status !== undefined) { + fields.push("status = ?"); + values.push(updates.status); + + // Handle status-specific timestamp updates + if (currentTask.status === "pending" && updates.status === "in_progress") { + fields.push("date_started = CURRENT_TIMESTAMP"); + } else if (updates.status === "completed") { + fields.push("date_completed = CURRENT_TIMESTAMP"); + } + } + + if (updates.assigned_to !== undefined) { + fields.push("assigned_to = ?"); + values.push(updates.assigned_to || null); + } + + if (updates.date_started !== undefined) { + fields.push("date_started = ?"); + values.push(updates.date_started || null); + } + + // Always update the updated_at timestamp + fields.push("updated_at = CURRENT_TIMESTAMP"); + values.push(taskId); + + const stmt = db.prepare(` + UPDATE project_tasks + SET ${fields.join(", ")} + WHERE id = ? + `); + + const result = stmt.run(...values); + + // Log the update + if (userId) { + const changes = []; + if ( + updates.priority !== undefined && + updates.priority !== currentTask.priority + ) { + changes.push( + `Priority: ${currentTask.priority || "None"} → ${ + updates.priority || "None" + }` + ); + } + if (updates.status !== undefined && updates.status !== currentTask.status) { + changes.push( + `Status: ${currentTask.status || "None"} → ${updates.status || "None"}` + ); + } + if ( + updates.assigned_to !== undefined && + updates.assigned_to !== currentTask.assigned_to + ) { + changes.push(`Assignment updated`); + } + if ( + updates.date_started !== undefined && + updates.date_started !== currentTask.date_started + ) { + changes.push( + `Date started: ${currentTask.date_started || "None"} → ${ + updates.date_started || "None" + }` + ); + } + + if (changes.length > 0) { + const logMessage = `Task updated: ${changes.join(", ")}`; + addNoteToTask(taskId, logMessage, true, userId); + } + } + + return result; +}