From 68c6165b322d5dc546f37b22560ce1c06aad7e2e Mon Sep 17 00:00:00 2001 From: RKWojs Date: Fri, 20 Jun 2025 23:17:18 +0200 Subject: [PATCH] feat: implement ProjectTasksList component and update ProjectTasksPage to use it feat: add date_completed column to project_tasks table in database initialization feat: update project task status to set date_completed when marking as completed --- src/app/project-tasks/page.js | 6 +- src/components/ProjectTasksList.js | 518 +++++++++++++++++++++++++++++ src/lib/init-db.js | 10 +- src/lib/queries/tasks.js | 12 +- 4 files changed, 541 insertions(+), 5 deletions(-) create mode 100644 src/components/ProjectTasksList.js diff --git a/src/app/project-tasks/page.js b/src/app/project-tasks/page.js index 77766a3..f261315 100644 --- a/src/app/project-tasks/page.js +++ b/src/app/project-tasks/page.js @@ -1,4 +1,4 @@ -import ProjectTasksDashboard from "@/components/ProjectTasksDashboard"; +import ProjectTasksList from "@/components/ProjectTasksList"; import PageContainer from "@/components/ui/PageContainer"; import PageHeader from "@/components/ui/PageHeader"; @@ -7,9 +7,9 @@ export default function ProjectTasksPage() { - + ); } diff --git a/src/components/ProjectTasksList.js b/src/components/ProjectTasksList.js new file mode 100644 index 0000000..ee3e470 --- /dev/null +++ b/src/components/ProjectTasksList.js @@ -0,0 +1,518 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card, CardHeader, CardContent } from "./ui/Card"; +import Button from "./ui/Button"; +import Badge from "./ui/Badge"; +import TaskStatusDropdownSimple from "./TaskStatusDropdownSimple"; +import SearchBar from "./ui/SearchBar"; +import { Select } from "./ui/Input"; +import Link from "next/link"; +import { + differenceInCalendarDays, + parseISO, + formatDistanceToNow, +} from "date-fns"; +import { formatDate } from "@/lib/utils"; + +export default function ProjectTasksList() { + const [allTasks, setAllTasks] = useState([]); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [groupBy, setGroupBy] = useState("none"); + + useEffect(() => { + const fetchAllTasks = async () => { + try { + const res = await fetch("/api/all-project-tasks"); + const tasks = await res.json(); + setAllTasks(tasks); + } catch (error) { + console.error("Failed to fetch project tasks:", error); + } finally { + setLoading(false); + } + }; + + fetchAllTasks(); + }, []); // 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 }; + } + try { + // For in-progress tasks, use date_started if available and valid, otherwise fall back to date_added + let referenceDate; + console.log(task.date_started) + if (task.status === "in_progress" && task.date_started && task.date_started.trim() !== "") { + // Handle the format "2025-06-20 08:40:38" + referenceDate = new Date(task.date_started); + } else { + // Handle date_added format + referenceDate = task.date_added.includes("T") + ? parseISO(task.date_added) + : new Date(task.date_added); + } + + // Check if date is valid + if (isNaN(referenceDate.getTime())) { + throw new Error("Invalid date"); + } + + const daysElapsed = differenceInCalendarDays(new Date(), referenceDate); + const maxWaitDays = task.max_wait_days || 0; + const daysRemaining = maxWaitDays - daysElapsed; + + if (task.status === "in_progress") { + if (daysRemaining < 0) { + return { type: "overdue", days: Math.abs(daysRemaining), daysRemaining: daysRemaining }; + } else { + return { type: "in_progress", days: daysRemaining, daysRemaining: daysRemaining }; + } + } + + // For pending tasks, use original logic + const daysOverdue = daysElapsed - maxWaitDays; + if (daysOverdue > 0) { + return { type: "overdue", days: daysOverdue }; + } else if (maxWaitDays - daysElapsed <= 2) { + return { type: "due_soon", days: maxWaitDays - daysElapsed }; + } else { + return { type: "pending", days: maxWaitDays - daysElapsed }; + } + } catch (error) { + console.error("Error parsing date:", task.date_added, task.date_started, error); + return { type: "pending", days: 0 }; + } + }; + // Group tasks by status + const groupTasksByStatus = () => { + const groups = { + pending: [], + in_progress: [], + completed: [], + }; + + allTasks.forEach((task) => { + const statusInfo = getTaskStatus(task); + const taskWithStatus = { ...task, statusInfo }; + + if (task.status === "completed" || task.status === "cancelled") { + groups.completed.push(taskWithStatus); + } else if (task.status === "in_progress") { + groups.in_progress.push(taskWithStatus); + } else { + groups.pending.push(taskWithStatus); + } + }); + + // Sort pending tasks by date_added (newest first) + groups.pending.sort((a, b) => { + try { + const dateA = new Date(a.date_added); + const dateB = new Date(b.date_added); + return dateB - dateA; // Newest first + } catch (error) { + return 0; + } + }); + + // Sort in_progress tasks by time left (urgent first - less time left comes first) + groups.in_progress.sort((a, b) => { + // If both have valid time remaining, sort by days remaining (ascending - urgent first) + if (!isNaN(a.statusInfo.daysRemaining) && !isNaN(b.statusInfo.daysRemaining)) { + return a.statusInfo.daysRemaining - b.statusInfo.daysRemaining; + } + // If one has invalid time, sort by date_started as fallback + try { + const dateA = a.date_started ? new Date(a.date_started) : new Date(a.date_added); + const dateB = b.date_started ? new Date(b.date_started) : new Date(b.date_added); + return dateA - dateB; // Oldest started first + } catch (error) { + return 0; + } + }); // Sort completed tasks by date_completed if available, otherwise by date_added (most recently completed first) + groups.completed.sort((a, b) => { + try { + // Try to use date_completed first + if (a.date_completed && b.date_completed) { + const dateA = new Date(a.date_completed); + const dateB = new Date(b.date_completed); + return dateB - dateA; // Most recently completed first + } + // If only one has date_completed, prioritize it + if (a.date_completed && !b.date_completed) return -1; + if (!a.date_completed && b.date_completed) return 1; + + // Fall back to date_added for both + const dateA = new Date(a.date_added); + const dateB = new Date(b.date_added); + return dateB - dateA; // Newest first + } catch (error) { + return 0; + } + }); + + return groups; + }; + + const taskGroups = groupTasksByStatus(); + // Filter tasks based on search term + const filterTasks = (tasks) => { + if (!searchTerm) return tasks; + return tasks.filter( + (task) => + task.task_name.toLowerCase().includes(searchTerm.toLowerCase()) || + task.project_name.toLowerCase().includes(searchTerm.toLowerCase()) || + task.city?.toLowerCase().includes(searchTerm.toLowerCase()) || + task.address?.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }; + + // Group tasks by task name when groupBy is set to "task_name" + const groupTasksByName = (tasks) => { + if (groupBy !== "task_name") return { "All Tasks": tasks }; + + const groups = {}; + tasks.forEach((task) => { + const taskName = task.task_name; + if (!groups[taskName]) { + groups[taskName] = []; + } + groups[taskName].push(task); + }); + + return groups; + }; + + 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) { + // Refresh tasks + const res2 = await fetch("/api/all-project-tasks"); + const tasks = await res2.json(); + setAllTasks(tasks); + } else { + alert("Failed to update task status"); + } + } catch (error) { + alert("Error updating task status"); + } + }; + + 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 getOverdueBadgeVariant = (days) => { + if (days > 7) return "danger"; + if (days > 3) return "warning"; + return "high"; + }; const TaskRow = ({ task, showTimeLeft = false }) => ( + + +
+ {task.task_name} + + {task.priority} + +
+ + + + {task.project_name} + + {task.city || 'N/A'} + {task.address || 'N/A'} + {showTimeLeft && ( + +
+ {task.statusInfo && task.statusInfo.type === "in_progress" && ( + + {!isNaN(task.statusInfo.daysRemaining) ? ( + task.statusInfo.daysRemaining > 0 + ? `${task.statusInfo.daysRemaining}d left` + : `${Math.abs(task.statusInfo.daysRemaining)}d overdue` + ) : ( + "Calculating..." + )} + + )} + {task.statusInfo && task.statusInfo.type === "overdue" && task.status === "in_progress" && ( + + {!isNaN(task.statusInfo.daysRemaining) ? `${Math.abs(task.statusInfo.daysRemaining)}d overdue` : "Overdue"} + + )} +
+ + )} + {task.status === "completed" && task.date_completed ? ( +
+
+ Completed: {(() => { + try { + const completedDate = new Date(task.date_completed); + return formatDistanceToNow(completedDate, { addSuffix: true }); + } catch (error) { + return task.date_completed; + } + })()} +
+
+ ) : task.status === "in_progress" && task.date_started ? ( +
+
+ Started: {(() => { + try { + const startedDate = new Date(task.date_started); + return formatDistanceToNow(startedDate, { addSuffix: true }); + } catch (error) { + return task.date_started; + } + })()} +
+
+ ) : ( + (() => { + try { + const addedDate = task.date_added.includes("T") + ? parseISO(task.date_added) + : new Date(task.date_added); + return formatDistanceToNow(addedDate, { addSuffix: true }); + } catch (error) { + return task.date_added; + } + })() + )} + + + {task.max_wait_days} days + + + + + + ); + const TaskTable = ({ tasks, showGrouped = false, showTimeLeft = false }) => { + const filteredTasks = filterTasks(tasks); + const groupedTasks = groupTasksByName(filteredTasks); + const colSpan = showTimeLeft ? "8" : "7"; + + return ( +
+ + + + + + + {showTimeLeft && ( + + )} + + + + + + {Object.entries(groupedTasks).map(([groupName, groupTasks]) => ( + <> + {showGrouped && groupName !== "All Tasks" && ( + + + + )} + {groupTasks.map((task) => ( + + ))} + + ))} + +
+ Task Name + + Project + + City + + Address + + Time Left + + Date Info + + Max Wait + + Actions +
+ {groupName} ({groupTasks.length} tasks) +
+ {filteredTasks.length === 0 && ( +
+

No tasks found

+
+ )} +
+ ); + }; + if (loading) { + return ( +
+
+
+
+
+
+ ); + } + + return ( +
+ {/* Summary Stats */} +
+ + +
+ {taskGroups.pending.length} +
+
Pending
+
+
+ + +
+ {taskGroups.in_progress.length} +
+
In Progress
+
+
+ + +
+ {taskGroups.completed.length} +
+
Completed
+
+
+
{/* Search and Controls */} setSearchTerm(e.target.value)} + placeholder="Search tasks, projects, city, or address..." + resultsCount={ + filterTasks(taskGroups.pending).length + + filterTasks(taskGroups.in_progress).length + + filterTasks(taskGroups.completed).length + } + resultsText="tasks" + filters={ +
+
+ + +
+
+ } + /> {/* Task Tables */} +
+ {/* Pending Tasks */} +
+
+

+ Pending Tasks + + {taskGroups.pending.length} + +

+

+ Tasks waiting to be started +

+
+ +
+ + {/* In Progress Tasks */} +
+
+

+ In Progress Tasks + + {taskGroups.in_progress.length} + +

+

+ Tasks currently being worked on - showing time left for completion +

+
+ +
+ + {/* Completed Tasks */} +
+
+

+ Completed Tasks + + {taskGroups.completed.length} + +

+

+ Recently completed and cancelled tasks +

+
+ +
+
+
+ ); +} diff --git a/src/lib/init-db.js b/src/lib/init-db.js index d29a17d..a72788b 100644 --- a/src/lib/init-db.js +++ b/src/lib/init-db.js @@ -145,7 +145,6 @@ export default function initializeDatabase() { } catch (e) { // Column already exists, ignore error } - // Migration: Add is_system column to notes table try { db.exec(` @@ -154,4 +153,13 @@ export default function initializeDatabase() { } catch (e) { // Column already exists, ignore error } + + // Migration: Add date_completed column to project_tasks table + try { + db.exec(` + ALTER TABLE project_tasks ADD COLUMN date_completed TEXT; + `); + } catch (e) { + // Column already exists, ignore error + } } diff --git a/src/lib/queries/tasks.js b/src/lib/queries/tasks.js index c98874c..437025a 100644 --- a/src/lib/queries/tasks.js +++ b/src/lib/queries/tasks.js @@ -25,6 +25,8 @@ export function getAllProjectTasks() { p.project_name, p.wp, p.plot, + p.city, + p.address, p.finish_date FROM project_tasks pt LEFT JOIN tasks t ON pt.task_template_id = t.task_id @@ -138,8 +140,16 @@ export function updateProjectTaskStatus(taskId, status) { WHERE id = ? `); result = stmt.run(status, taskId); + } else if (status === "completed") { + // Completing a task - set date_completed + stmt = db.prepare(` + UPDATE project_tasks + SET status = ?, date_completed = CURRENT_TIMESTAMP + WHERE id = ? + `); + result = stmt.run(status, taskId); } else { - // Just updating status without changing date_started + // Just updating status without changing timestamps stmt = db.prepare(` UPDATE project_tasks SET status = ?