- Created ProjectStatusDropdownSimple component for managing project statuses with a simple dropdown interface. - Updated ProjectTasksDashboard and ProjectTasksSection to use the new ProjectStatusDropdownSimple component. - Refactored TaskStatusDropdown to simplify its structure and added debugging features. - Introduced TaskStatusDropdownDebug for testing purposes with enhanced logging and debugging UI. - Added TaskStatusDropdownSimple for task statuses, mirroring the functionality of the project status dropdown. - Created comprehensive HTML test files for dropdown functionality validation. - Added a batch script to clear Next.js cache and start the development server.
412 lines
12 KiB
JavaScript
412 lines
12 KiB
JavaScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import Link from "next/link";
|
|
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
|
|
import Button from "@/components/ui/Button";
|
|
import Badge from "@/components/ui/Badge";
|
|
import TaskStatusDropdownSimple from "@/components/TaskStatusDropdownSimple";
|
|
import { Input } from "@/components/ui/Input";
|
|
import { formatDistanceToNow, parseISO } from "date-fns";
|
|
import PageContainer from "@/components/ui/PageContainer";
|
|
import PageHeader from "@/components/ui/PageHeader";
|
|
import SearchBar from "@/components/ui/SearchBar";
|
|
import FilterBar from "@/components/ui/FilterBar";
|
|
import { LoadingState } from "@/components/ui/States";
|
|
|
|
export default function ProjectTasksPage() {
|
|
const [allTasks, setAllTasks] = useState([]);
|
|
const [filteredTasks, setFilteredTasks] = useState([]);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [statusFilter, setStatusFilter] = useState("all");
|
|
const [priorityFilter, setPriorityFilter] = useState("all");
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const fetchAllTasks = async () => {
|
|
try {
|
|
const res = await fetch("/api/all-project-tasks");
|
|
const tasks = await res.json();
|
|
setAllTasks(tasks);
|
|
setFilteredTasks(tasks);
|
|
} catch (error) {
|
|
console.error("Failed to fetch all project tasks:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchAllTasks();
|
|
}, []);
|
|
|
|
// Filter tasks based on search term and filters
|
|
useEffect(() => {
|
|
let filtered = allTasks;
|
|
|
|
// Apply search filter
|
|
if (searchTerm.trim()) {
|
|
const searchLower = searchTerm.toLowerCase();
|
|
filtered = filtered.filter((task) => {
|
|
return (
|
|
task.task_name?.toLowerCase().includes(searchLower) ||
|
|
task.project_name?.toLowerCase().includes(searchLower) ||
|
|
task.wp?.toLowerCase().includes(searchLower) ||
|
|
task.plot?.toLowerCase().includes(searchLower)
|
|
);
|
|
});
|
|
}
|
|
|
|
// Apply status filter
|
|
if (statusFilter !== "all") {
|
|
filtered = filtered.filter((task) => task.status === statusFilter);
|
|
}
|
|
|
|
// Apply priority filter
|
|
if (priorityFilter !== "all") {
|
|
filtered = filtered.filter((task) => task.priority === priorityFilter);
|
|
}
|
|
|
|
setFilteredTasks(filtered);
|
|
}, [searchTerm, statusFilter, priorityFilter, allTasks]);
|
|
|
|
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) {
|
|
// Update the task in the local state
|
|
setAllTasks((prevTasks) =>
|
|
prevTasks.map((task) =>
|
|
task.id === taskId ? { ...task, status: newStatus } : task
|
|
)
|
|
);
|
|
} 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) {
|
|
// Remove the task from local state
|
|
setAllTasks((prevTasks) =>
|
|
prevTasks.filter((task) => task.id !== taskId)
|
|
);
|
|
} else {
|
|
alert("Failed to delete task");
|
|
}
|
|
} catch (error) {
|
|
alert("Error deleting task");
|
|
}
|
|
};
|
|
|
|
const getPriorityVariant = (priority) => {
|
|
switch (priority) {
|
|
case "high":
|
|
return "danger";
|
|
case "normal":
|
|
return "secondary";
|
|
case "low":
|
|
return "success";
|
|
default:
|
|
return "secondary";
|
|
}
|
|
};
|
|
|
|
const statusCounts = {
|
|
all: allTasks.length,
|
|
pending: allTasks.filter((task) => task.status === "pending").length,
|
|
in_progress: allTasks.filter((task) => task.status === "in_progress")
|
|
.length,
|
|
completed: allTasks.filter((task) => task.status === "completed").length,
|
|
};
|
|
if (loading) {
|
|
return (
|
|
<PageContainer>
|
|
<PageHeader
|
|
title="Project Tasks"
|
|
description="Monitor and manage tasks across all projects"
|
|
/>
|
|
<LoadingState message="Loading tasks..." />
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
const filterOptions = [
|
|
{
|
|
label: "Status",
|
|
value: statusFilter,
|
|
onChange: (e) => setStatusFilter(e.target.value),
|
|
options: [
|
|
{ value: "all", label: "All" },
|
|
{ value: "pending", label: "Pending" },
|
|
{ value: "in_progress", label: "In Progress" },
|
|
{ value: "completed", label: "Completed" },
|
|
],
|
|
},
|
|
{
|
|
label: "Priority",
|
|
value: priorityFilter,
|
|
onChange: (e) => setPriorityFilter(e.target.value),
|
|
options: [
|
|
{ value: "all", label: "All" },
|
|
{ value: "high", label: "High" },
|
|
{ value: "normal", label: "Normal" },
|
|
{ value: "low", label: "Low" },
|
|
],
|
|
},
|
|
];
|
|
|
|
return (
|
|
<PageContainer>
|
|
<PageHeader
|
|
title="Project Tasks"
|
|
description="Monitor and manage tasks across all projects"
|
|
/>
|
|
<SearchBar
|
|
searchTerm={searchTerm}
|
|
onSearchChange={(e) => setSearchTerm(e.target.value)}
|
|
placeholder="Search tasks by name, project, WP, or plot..."
|
|
resultsCount={filteredTasks.length}
|
|
resultsText="tasks"
|
|
/>{" "}
|
|
<FilterBar filters={filterOptions} className="mb-6" />
|
|
{/* Stats Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center">
|
|
<div className="p-2 bg-blue-100 rounded-lg">
|
|
<svg
|
|
className="w-6 h-6 text-blue-600"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-gray-600">Total Tasks</p>
|
|
<p className="text-2xl font-bold text-gray-900">
|
|
{statusCounts.all}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center">
|
|
<div className="p-2 bg-gray-100 rounded-lg">
|
|
<svg
|
|
className="w-6 h-6 text-gray-600"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-gray-600">Pending</p>
|
|
<p className="text-2xl font-bold text-gray-900">
|
|
{statusCounts.pending}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center">
|
|
<div className="p-2 bg-yellow-100 rounded-lg">
|
|
<svg
|
|
className="w-6 h-6 text-yellow-600"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M13 10V3L4 14h7v7l9-11h-7z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-gray-600">In Progress</p>
|
|
<p className="text-2xl font-bold text-gray-900">
|
|
{statusCounts.in_progress}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center">
|
|
<div className="p-2 bg-green-100 rounded-lg">
|
|
<svg
|
|
className="w-6 h-6 text-green-600"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M5 13l4 4L19 7"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-gray-600">Completed</p>
|
|
<p className="text-2xl font-bold text-gray-900">
|
|
{statusCounts.completed}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>{" "}
|
|
</div>
|
|
{/* Tasks List */}
|
|
{filteredTasks.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="text-center py-12">
|
|
<div className="text-gray-400 mb-4">
|
|
<svg
|
|
className="w-16 h-16 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>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
|
No tasks found
|
|
</h3>
|
|
<p className="text-gray-500 mb-6">
|
|
{searchTerm || statusFilter !== "all" || priorityFilter !== "all"
|
|
? "Try adjusting your filters to see more tasks"
|
|
: "No tasks have been created yet"}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{filteredTasks.map((task) => (
|
|
<Card key={task.id} className="hover:shadow-md transition-shadow">
|
|
<CardContent className="p-6">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
{" "}
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h3 className="text-lg font-semibold text-gray-900">
|
|
{task.task_name}
|
|
</h3>{" "}
|
|
<TaskStatusDropdownSimple
|
|
task={task}
|
|
size="sm"
|
|
onStatusChange={handleStatusChange}
|
|
/>
|
|
<Badge
|
|
variant={getPriorityVariant(task.priority)}
|
|
size="sm"
|
|
>
|
|
{task.priority}
|
|
</Badge>
|
|
{task.task_type === "template" && (
|
|
<Badge variant="primary" size="sm">
|
|
Template
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
|
<div>
|
|
<p className="text-sm text-gray-600">Project</p>
|
|
<p className="font-medium text-gray-900">
|
|
{task.project_name}
|
|
</p>
|
|
</div>
|
|
{task.wp && (
|
|
<div>
|
|
<p className="text-sm text-gray-600">WP</p>
|
|
<p className="font-medium text-gray-900">{task.wp}</p>
|
|
</div>
|
|
)}
|
|
{task.plot && (
|
|
<div>
|
|
<p className="text-sm text-gray-600">Plot</p>
|
|
<p className="font-medium text-gray-900">
|
|
{task.plot}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-4 text-sm text-gray-500">
|
|
<span>
|
|
Added{" "}
|
|
{formatDistanceToNow(parseISO(task.date_added), {
|
|
addSuffix: true,
|
|
})}
|
|
</span>
|
|
{task.max_wait_days > 0 && (
|
|
<span>Max wait: {task.max_wait_days} days</span>
|
|
)}
|
|
</div>
|
|
</div>{" "}
|
|
<div className="flex items-center space-x-2 ml-6">
|
|
<Link href={`/projects/${task.project_id}`}>
|
|
<Button variant="outline" size="sm">
|
|
View Project
|
|
</Button>
|
|
</Link>
|
|
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
onClick={() => handleDeleteTask(task.id)}
|
|
>
|
|
Delete
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}{" "}
|
|
</div>
|
|
)}
|
|
</PageContainer>
|
|
);
|
|
}
|