Refactor Project Tasks and Task Templates pages with new UI components
- Introduced PageContainer and PageHeader components for consistent layout. - Added SearchBar and FilterBar components for improved task filtering and searching. - Implemented LoadingState component for better loading indication. - Updated ProjectTasksPage to utilize new components and enhance user experience. - Refactored TaskTemplatesPage to use PageContainer and PageHeader for better structure. - Created FilterBar component to manage filter options dynamically. - Added SearchBar component for searching tasks with clear functionality. - Introduced States component for loading and error states.
This commit is contained in:
@@ -7,6 +7,11 @@ import Button from "@/components/ui/Button";
|
||||
import Badge from "@/components/ui/Badge";
|
||||
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([]);
|
||||
@@ -153,335 +158,293 @@ export default function ProjectTasksPage() {
|
||||
.length,
|
||||
completed: allTasks.filter((task) => task.status === "completed").length,
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-6xl mx-auto p-6">
|
||||
<div className="text-center py-12">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<p className="mt-4 text-gray-600">Loading tasks...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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 (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-6xl mx-auto p-6">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Project Tasks</h1>
|
||||
<p className="text-gray-600 mt-1">
|
||||
Monitor and manage tasks across all projects
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Filters */}
|
||||
<Card className="mb-6">
|
||||
<CardContent className="p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Search Tasks
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search by task name, project name, WP, or plot..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
<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"
|
||||
>
|
||||
<option value="all">All Statuses</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="in_progress">In Progress</option>
|
||||
<option value="completed">Completed</option>
|
||||
</select>
|
||||
<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>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Priority
|
||||
</label>
|
||||
<select
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={priorityFilter}
|
||||
onChange={(e) => setPriorityFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">All Priorities</option>
|
||||
<option value="high">High</option>
|
||||
<option value="normal">Normal</option>
|
||||
<option value="low">Low</option>
|
||||
</select>
|
||||
<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>
|
||||
|
||||
{/* Tasks List */}
|
||||
{filteredTasks.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="text-center py-12">
|
||||
<div className="text-gray-400 mb-4">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="p-2 bg-gray-100 rounded-lg">
|
||||
<svg
|
||||
className="w-16 h-16 mx-auto"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
className="w-6 h-6 text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<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"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</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>
|
||||
<Badge
|
||||
variant={getStatusVariant(task.status)}
|
||||
size="sm"
|
||||
>
|
||||
{getStatusDisplayName(task.status)}
|
||||
<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>
|
||||
<Badge variant={getStatusVariant(task.status)} size="sm">
|
||||
{getStatusDisplayName(task.status)}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant={getPriorityVariant(task.priority)}
|
||||
size="sm"
|
||||
>
|
||||
{task.priority}
|
||||
</Badge>
|
||||
{task.task_type === "template" && (
|
||||
<Badge variant="primary" size="sm">
|
||||
Template
|
||||
</Badge>
|
||||
<Badge
|
||||
variant={getPriorityVariant(task.priority)}
|
||||
size="sm"
|
||||
>
|
||||
{task.priority}
|
||||
</Badge>
|
||||
{task.task_type === "template" && (
|
||||
<Badge variant="primary" size="sm">
|
||||
Template
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<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">Project</p>
|
||||
<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.project_name}
|
||||
{task.plot}
|
||||
</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">
|
||||
{task.status !== "completed" && (
|
||||
<select
|
||||
className="text-sm px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={task.status}
|
||||
onChange={(e) =>
|
||||
handleStatusChange(task.id, e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="in_progress">In Progress</option>
|
||||
<option value="completed">Completed</option>
|
||||
</select>
|
||||
<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>
|
||||
)}
|
||||
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 ml-6">
|
||||
{task.status !== "completed" && (
|
||||
<select
|
||||
className="text-sm px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={task.status}
|
||||
onChange={(e) =>
|
||||
handleStatusChange(task.id, e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="in_progress">In Progress</option>
|
||||
<option value="completed">Completed</option>
|
||||
</select>
|
||||
)}
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user