Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions app/(dashboard)/tasks/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,61 @@ export async function getAllTasks() {
}
}

// Get filtered tasks with search and filter options
export async function getFilteredTasks(filters: {
search?: string;
status?: string[];
priority?: string[];
assigneeId?: number;
}) {
try {
const where: {
OR?: Array<{ name: { contains: string; mode: "insensitive" } } | { description: { contains: string; mode: "insensitive" } }>;
status?: { in: string[] };
priority?: { in: string[] };
assigneeId?: number;
} = {};

// Add search filter (case-insensitive search on name and description)
if (filters.search) {
where.OR = [
{ name: { contains: filters.search, mode: "insensitive" } },
{ description: { contains: filters.search, mode: "insensitive" } },
];
}

// Add status filter
if (filters.status && filters.status.length > 0) {
where.status = { in: filters.status };
}

// Add priority filter
if (filters.priority && filters.priority.length > 0) {
where.priority = { in: filters.priority };
}

// Add assignee filter
if (filters.assigneeId) {
where.assigneeId = filters.assigneeId;
}

const tasks = await prisma.task.findMany({
where,
include: {
assignee: { select: { id: true, name: true, email: true } },
creator: { select: { id: true, name: true, email: true } },
},
orderBy: [
{ createdAt: "desc" },
{ id: "desc" }
],
});
return { tasks, error: null };
} catch {
return { tasks: [], error: "Failed to fetch filtered tasks." };
}
}

// Delete a task by ID
export async function deleteTask(taskId: number) {
try {
Expand Down
4 changes: 2 additions & 2 deletions app/(dashboard)/tasks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Suspense } from "react"
import { Button } from "@/components/ui/button"
import { Plus } from "lucide-react"
import Link from "next/link"
import { TaskList } from "@/components/task-list"
import { TasksPageClient } from "@/components/tasks-page-with-filters"
import { poppins } from "@/lib/fonts"

import { getAllTasks } from "@/app/(dashboard)/tasks/actions"
Expand Down Expand Up @@ -30,7 +30,7 @@ export default async function TasksPage() {
</div>

<Suspense fallback={<div>Loading tasks...</div>}>
<TaskList initialTasks={tasks || []} />
<TasksPageClient initialTasks={tasks || []} />
</Suspense>
</div>
)
Expand Down
174 changes: 174 additions & 0 deletions components/task-filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
"use client"

import { useState, useEffect } from "react"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Search, X } from "lucide-react"
import { getAllUsers } from "@/app/login/actions"
import type { User } from "@/app/generated/prisma/client"

interface TaskFiltersProps {
onFilterChange: (filters: {
search: string;
status: string[];
priority: string[];
assigneeId: number | undefined;
}) => void;
}

const STATUS_OPTIONS = [
{ value: "todo", label: "Todo" },
{ value: "in_progress", label: "In Progress" },
{ value: "review", label: "Review" },
{ value: "done", label: "Done" },
]

const PRIORITY_OPTIONS = [
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High" },
]

export function TaskFilters({ onFilterChange }: TaskFiltersProps) {
const [search, setSearch] = useState("")
const [selectedStatuses, setSelectedStatuses] = useState<string[]>([])
const [selectedPriorities, setSelectedPriorities] = useState<string[]>([])
const [selectedAssignee, setSelectedAssignee] = useState<number | undefined>(undefined)
const [users, setUsers] = useState<Pick<User, "id" | "name">[]>([])

useEffect(() => {
// Fetch users for assignee filter
getAllUsers()
.then(setUsers)
.catch((error) => {
console.error("Failed to load users:", error)
setUsers([])
})
}, [])

useEffect(() => {
// Notify parent component when filters change
// Note: onFilterChange should be wrapped in useCallback in parent component
onFilterChange({
search,
status: selectedStatuses,
priority: selectedPriorities,
assigneeId: selectedAssignee,
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [search, selectedStatuses, selectedPriorities, selectedAssignee])

const toggleStatus = (status: string) => {
setSelectedStatuses(prev =>
prev.includes(status)
? prev.filter(s => s !== status)
: [...prev, status]
)
}

const togglePriority = (priority: string) => {
setSelectedPriorities(prev =>
prev.includes(priority)
? prev.filter(p => p !== priority)
: [...prev, priority]
)
}

const clearFilters = () => {
setSearch("")
setSelectedStatuses([])
setSelectedPriorities([])
setSelectedAssignee(undefined)
}

const hasActiveFilters = search || selectedStatuses.length > 0 || selectedPriorities.length > 0 || selectedAssignee

return (
<div className="space-y-4 p-4 border rounded-lg bg-card">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">Filters</h3>
{hasActiveFilters && (
<Button variant="ghost" size="sm" onClick={clearFilters}>
<X className="mr-2 h-4 w-4" />
Clear All
</Button>
)}
</div>

<div className="space-y-4">
{/* Search Input */}
<div className="space-y-2">
<Label htmlFor="search">Search</Label>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="search"
placeholder="Search tasks by title or description..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10"
/>
</div>
</div>

{/* Status Filter */}
<div className="space-y-2">
<Label>Status</Label>
<div className="flex flex-wrap gap-2">
{STATUS_OPTIONS.map((status) => (
<Badge
key={status.value}
variant={selectedStatuses.includes(status.value) ? "default" : "outline"}
className="cursor-pointer hover:bg-primary/80"
onClick={() => toggleStatus(status.value)}
>
{status.label}
</Badge>
))}
</div>
</div>

{/* Priority Filter */}
<div className="space-y-2">
<Label>Priority</Label>
<div className="flex flex-wrap gap-2">
{PRIORITY_OPTIONS.map((priority) => (
<Badge
key={priority.value}
variant={selectedPriorities.includes(priority.value) ? "default" : "outline"}
className="cursor-pointer hover:bg-primary/80"
onClick={() => togglePriority(priority.value)}
>
{priority.label}
</Badge>
))}
</div>
</div>

{/* Assignee Filter */}
<div className="space-y-2">
<Label htmlFor="assignee">Assignee</Label>
<Select
value={selectedAssignee?.toString() || "all"}
onValueChange={(value) => setSelectedAssignee(value === "all" ? undefined : parseInt(value))}
>
<SelectTrigger>
<SelectValue placeholder="All assignees" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All assignees</SelectItem>
{users.map((user) => (
<SelectItem key={user.id} value={user.id.toString()}>
{user.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
)
}
62 changes: 62 additions & 0 deletions components/tasks-page-with-filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"use client"

import { useState, useCallback } from "react"
import { TaskList } from "@/components/task-list"
import { TaskFilters } from "@/components/task-filters"
import { getFilteredTasks } from "@/app/(dashboard)/tasks/actions"
import type { Task as PrismaTask, User } from "@/app/generated/prisma/client"

type TaskWithProfile = PrismaTask & {
assignee?: Pick<User, "name"> | null;
}

interface TasksPageClientProps {
initialTasks: TaskWithProfile[]
}

export function TasksPageClient({ initialTasks }: TasksPageClientProps) {
const [tasks, setTasks] = useState<TaskWithProfile[]>(initialTasks)
const [isLoading, setIsLoading] = useState(false)

const handleFilterChange = useCallback(async (filters: {
search: string;
status: string[];
priority: string[];
assigneeId: number | undefined;
}) => {
setIsLoading(true)
try {
const { tasks: filteredTasks, error } = await getFilteredTasks({
search: filters.search || undefined,
status: filters.status.length > 0 ? filters.status : undefined,
priority: filters.priority.length > 0 ? filters.priority : undefined,
assigneeId: filters.assigneeId,
})

if (!error && filteredTasks) {
setTasks(filteredTasks)
}
} catch (err) {
console.error("Error filtering tasks:", err)
} finally {
setIsLoading(false)
}
}, [])

return (
<div className="grid gap-4 md:grid-cols-[300px_1fr]">
<div>
<TaskFilters onFilterChange={handleFilterChange} />
</div>
<div>
{isLoading ? (
<div className="flex items-center justify-center p-8">
<p className="text-muted-foreground">Loading tasks...</p>
</div>
) : (
<TaskList initialTasks={tasks} />
)}
</div>
</div>
)
}
Loading