From a9afebdda5d785fac7a1e06b75b231be56888c4c Mon Sep 17 00:00:00 2001 From: Chop <28534054+RChopin@users.noreply.github.com> Date: Tue, 3 Jun 2025 00:14:17 +0200 Subject: [PATCH] feat: Add ProjectTasksPage and ProjectTasksDashboard components with task categorization and filtering --- src/app/project-tasks/page.js | 15 + src/components/ProjectTasksDashboard.js | 519 ++++++++++++++++++++++++ src/components/ui/Navigation.js | 2 +- 3 files changed, 535 insertions(+), 1 deletion(-) create mode 100644 src/app/project-tasks/page.js create mode 100644 src/components/ProjectTasksDashboard.js diff --git a/src/app/project-tasks/page.js b/src/app/project-tasks/page.js new file mode 100644 index 0000000..77766a3 --- /dev/null +++ b/src/app/project-tasks/page.js @@ -0,0 +1,15 @@ +import ProjectTasksDashboard from "@/components/ProjectTasksDashboard"; +import PageContainer from "@/components/ui/PageContainer"; +import PageHeader from "@/components/ui/PageHeader"; + +export default function ProjectTasksPage() { + return ( + + + + + ); +} diff --git a/src/components/ProjectTasksDashboard.js b/src/components/ProjectTasksDashboard.js new file mode 100644 index 0000000..5106284 --- /dev/null +++ b/src/components/ProjectTasksDashboard.js @@ -0,0 +1,519 @@ +"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 SearchBar from "./ui/SearchBar"; +import { Select } from "./ui/Input"; +import Link from "next/link"; +import { + differenceInCalendarDays, + parseISO, + formatDistanceToNow, +} from "date-fns"; + +export default function ProjectTasksDashboard() { + const [allTasks, setAllTasks] = useState([]); + const [loading, setLoading] = useState(true); + const [filter, setFilter] = useState("all"); + const [searchTerm, setSearchTerm] = useState(""); + + 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 { + // Handle different date formats + const addedDate = task.date_added.includes("T") + ? parseISO(task.date_added) + : new Date(task.date_added + "T00:00:00"); + + const daysElapsed = differenceInCalendarDays(new Date(), addedDate); + const maxWaitDays = task.max_wait_days || 0; + 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, error); + return { type: "pending", days: 0 }; + } + }; + + // Categorize tasks + const categorizeTasks = () => { + const now = new Date(); + const categories = { + overdue: [], + due_soon: [], + pending: [], + in_progress: [], + recent_completed: [], + }; + allTasks.forEach((task) => { + const taskStatus = getTaskStatus(task); + try { + const addedDate = task.date_added.includes("T") + ? parseISO(task.date_added) + : new Date(task.date_added + "T00:00:00"); + const daysAgo = differenceInCalendarDays(now, addedDate); // First check if task is overdue (regardless of status) + if ( + taskStatus.type === "overdue" && + task.status !== "completed" && + task.status !== "cancelled" + ) { + categories.overdue.push({ ...task, statusInfo: taskStatus }); + } + // Then check if it's due soon (regardless of status) + else if ( + taskStatus.type === "due_soon" && + task.status !== "completed" && + task.status !== "cancelled" + ) { + categories.due_soon.push({ ...task, statusInfo: taskStatus }); + } + // Then categorize by actual status + else if (task.status === "pending") { + categories.pending.push({ ...task, statusInfo: taskStatus }); + } else if (task.status === "in_progress") { + categories.in_progress.push({ ...task, statusInfo: taskStatus }); + } else if (task.status === "completed" || task.status === "cancelled") { + // Show all completed/cancelled tasks (most recent activity) + categories.recent_completed.push({ ...task, statusInfo: taskStatus }); + } + } catch (error) { + console.error("Error processing task:", task, error); + // Still add to appropriate category if there's an error + if (task.status === "pending") { + categories.pending.push({ + ...task, + statusInfo: { type: "pending", days: 0 }, + }); + } else if (task.status === "in_progress") { + categories.in_progress.push({ + ...task, + statusInfo: { type: "pending", days: 0 }, + }); + } + } + }); // Sort each category + categories.overdue.sort((a, b) => b.statusInfo.days - a.statusInfo.days); + categories.due_soon.sort((a, b) => a.statusInfo.days - b.statusInfo.days); + categories.pending.sort((a, b) => { + try { + const dateA = a.date_added.includes("T") + ? parseISO(a.date_added) + : new Date(a.date_added + "T00:00:00"); + const dateB = b.date_added.includes("T") + ? parseISO(b.date_added) + : new Date(b.date_added + "T00:00:00"); + return dateB - dateA; + } catch (error) { + return 0; + } + }); + categories.in_progress.sort((a, b) => { + try { + const dateA = a.date_added.includes("T") + ? parseISO(a.date_added) + : new Date(a.date_added + "T00:00:00"); + const dateB = b.date_added.includes("T") + ? parseISO(b.date_added) + : new Date(b.date_added + "T00:00:00"); + return dateB - dateA; + } catch (error) { + return 0; + } + }); + categories.recent_completed.sort((a, b) => { + try { + const dateA = a.date_added.includes("T") + ? parseISO(a.date_added) + : new Date(a.date_added + "T00:00:00"); + const dateB = b.date_added.includes("T") + ? parseISO(b.date_added) + : new Date(b.date_added + "T00:00:00"); + return dateB - dateA; + } catch (error) { + return 0; + } + }); + + return categories; + }; + + const categorizedTasks = categorizeTasks(); + + // Filter tasks based on search and filter + 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.wp.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }; + const getVisibleTasks = () => { + switch (filter) { + case "overdue": + return filterTasks(categorizedTasks.overdue); + case "due_soon": + return filterTasks(categorizedTasks.due_soon); + case "pending": + return filterTasks(categorizedTasks.pending); + case "in_progress": + return filterTasks(categorizedTasks.in_progress); + case "completed": + return filterTasks(categorizedTasks.recent_completed); + default: + return { + overdue: filterTasks(categorizedTasks.overdue), + due_soon: filterTasks(categorizedTasks.due_soon), + pending: filterTasks(categorizedTasks.pending), + in_progress: filterTasks(categorizedTasks.in_progress), + recent_completed: filterTasks(categorizedTasks.recent_completed), + }; + } + }; + + const visibleTasks = getVisibleTasks(); + + 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 getStatusBadgeVariant = (status) => { + switch (status) { + case "completed": + return "success"; + case "in_progress": + return "primary"; + case "pending": + return "warning"; + case "cancelled": + return "danger"; + default: + return "default"; + } + }; + + const getOverdueBadgeVariant = (days) => { + if (days > 7) return "danger"; + if (days > 3) return "warning"; + return "high"; + }; + + const TaskCard = ({ task, showStatusBadge = false }) => ( +
+
+
+
+

+ {task.task_name} +

+ + {task.priority} + + {showStatusBadge && ( + + {task.status.replace("_", " ")} + + )} +
+
+ + {task.project_name} + + WP: {task.wp} + Plot: {task.plot} +
{" "} +
+ + Added:{" "} + {(() => { + try { + const addedDate = task.date_added.includes("T") + ? parseISO(task.date_added) + : new Date(task.date_added + "T00:00:00"); + return formatDistanceToNow(addedDate, { addSuffix: true }); + } catch (error) { + return task.date_added; + } + })()} + + Max wait: {task.max_wait_days} days + Type: {task.task_type} +
+
+
+ {task.statusInfo && task.statusInfo.type === "overdue" && ( + + {task.statusInfo.days} days overdue + + )} + {task.statusInfo && task.statusInfo.type === "due_soon" && ( + + Due in {task.statusInfo.days} days + + )} + {(task.status === "pending" || task.status === "in_progress") && ( + + )} +
+
+
+ ); + + const SectionCard = ({ title, tasks, variant = "default", count }) => ( + + +
+

{title}

+ + {count || tasks.length} {tasks.length === 1 ? "task" : "tasks"} + +
+
+ + {tasks.length === 0 ? ( +
+

No tasks in this category

+
+ ) : ( + tasks.map((task) => ( + + )) + )} +
+
+ ); + const filterOptions = [ + { value: "all", label: "All Categories" }, + { value: "overdue", label: "Overdue" }, + { value: "due_soon", label: "Due Soon" }, + { value: "pending", label: "Pending" }, + { value: "in_progress", label: "In Progress" }, + { value: "completed", label: "Recent Activity" }, + ]; + + if (loading) { + return ( +
+
+
+
+ {[1, 2, 3, 4].map((i) => ( +
+ ))} +
+
+
+ ); + } + + return ( +
+ {" "} + {/* Summary Stats */} +
+ + +
+ {categorizedTasks.overdue.length} +
+
Overdue
+
+
+ + +
+ {categorizedTasks.due_soon.length} +
+
Due Soon
+
+
+ + +
+ {categorizedTasks.pending.length} +
+
Pending
+
+
+ + +
+ {categorizedTasks.in_progress.length} +
+
In Progress
+
+
+ + +
+ {categorizedTasks.recent_completed.length} +
+
Recent Activity
+
+
+
+ {/* Search and Filters */} + setSearchTerm(e.target.value)} + placeholder="Search tasks, projects, or WP..." + resultsCount={ + filter === "all" + ? allTasks.length + : Array.isArray(visibleTasks) + ? visibleTasks.length + : 0 + } + resultsText="tasks" + filters={ +
+ + +
+ } + />{" "} + {/* Task Sections */} + {filter === "all" ? ( +
+ + + + + +
+ ) : ( +
+ f.value === filter)?.label || "Tasks" + } + tasks={Array.isArray(visibleTasks) ? visibleTasks : []} + variant={ + filter === "overdue" + ? "danger" + : filter === "due_soon" + ? "warning" + : filter === "pending" + ? "primary" + : filter === "in_progress" + ? "secondary" + : "success" + } + /> +
+ )} +
+ ); +} diff --git a/src/components/ui/Navigation.js b/src/components/ui/Navigation.js index a075997..0020eea 100644 --- a/src/components/ui/Navigation.js +++ b/src/components/ui/Navigation.js @@ -17,7 +17,7 @@ const Navigation = () => { { href: "/", label: "Dashboard" }, { href: "/projects", label: "Projects" }, { href: "/tasks/templates", label: "Task Templates" }, - { href: "/tasks", label: "Project Tasks" }, + { href: "/project-tasks", label: "Project Tasks" }, { href: "/contracts", label: "Contracts" }, ];