Task Management

Building and managing tasks in ShipKit with progress tracking, assignments, due dates, and prioritization

Task Management

ShipKit provides a comprehensive task management system for tracking work items, managing assignments, monitoring progress, and handling priorities. This guide covers setup, usage, and best practices.

Setup

Layout Configuration

// src/app/(app)/(task)/layout.tsx
import { TaskNav } from "@/components/task/nav"
import { TaskHeader } from "@/components/task/header"
import { auth } from "@/lib/auth/auth"
import { redirect } from "next/navigation"

export default async function TaskLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const session = await auth()

  if (!session?.user) {
    redirect("/auth/signin?callbackUrl=/tasks")
  }

  return (
    <div className="flex min-h-screen flex-col">
      <TaskHeader user={session.user} />
      <div className="flex flex-1">
        <TaskNav />
        <main className="flex-1 p-6">
          {children}
        </main>
      </div>
    </div>
  )
}

Navigation Component

// src/components/task/nav.tsx
import { cn } from "@/lib/utils"
import { usePathname } from "next/navigation"
import Link from "next/link"

const navItems = [
  {
    title: "All Tasks",
    href: "/tasks",
    icon: "ListTodo",
  },
  {
    title: "My Tasks",
    href: "/tasks/my",
    icon: "User",
  },
  {
    title: "Calendar",
    href: "/tasks/calendar",
    icon: "Calendar",
  },
  {
    title: "Reports",
    href: "/tasks/reports",
    icon: "BarChart",
  },
  {
    title: "Settings",
    href: "/tasks/settings",
    icon: "Settings",
  },
]

export function TaskNav() {
  const pathname = usePathname()

  return (
    <nav className="w-64 border-r bg-background p-6">
      <div className="space-y-4">
        {navItems.map((item) => (
          <Link
            key={item.href}
            href={item.href}
            className={cn(
              "flex items-center space-x-3 rounded-lg px-3 py-2",
              "hover:bg-accent hover:text-accent-foreground",
              pathname === item.href && "bg-accent text-accent-foreground"
            )}
          >
            <item.icon className="h-5 w-5" />
            <span>{item.title}</span>
          </Link>
        ))}
      </div>
    </nav>
  )
}

Features

Task List

// src/app/(app)/(task)/page.tsx
import { Suspense } from "react"
import { TaskList } from "@/components/task/task-list"
import { TaskFilters } from "@/components/task/task-filters"
import { CreateTask } from "@/components/task/create-task"
import { Loading } from "@/components/ui/loading"

export default function TasksPage() {
  return (
    <div className="space-y-8">
      <div className="flex items-center justify-between">
        <h1 className="text-3xl font-bold">Tasks</h1>
        <CreateTask />
      </div>

      <div className="grid gap-6 lg:grid-cols-[300px,1fr]">
        <TaskFilters />
        <Suspense fallback={<Loading />}>
          <TaskList />
        </Suspense>
      </div>
    </div>
  )
}

Task List Component

// src/components/task/task-list.tsx
import { fetchTasks } from "@/lib/task/tasks"
import { DataTable } from "@/components/ui/data-table"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { formatDate } from "@/lib/utils"

const columns = [
  {
    accessorKey: "title",
    header: "Title",
  },
  {
    accessorKey: "status",
    header: "Status",
    cell: ({ row }) => (
      <Badge variant={getStatusVariant(row.original.status)}>
        {row.original.status}
      </Badge>
    ),
  },
  {
    accessorKey: "assignee",
    header: "Assignee",
    cell: ({ row }) => row.original.assignee?.name || "Unassigned",
  },
  {
    accessorKey: "dueDate",
    header: "Due Date",
    cell: ({ row }) => formatDate(row.original.dueDate),
  },
  {
    accessorKey: "priority",
    header: "Priority",
    cell: ({ row }) => (
      <Badge variant={getPriorityVariant(row.original.priority)}>
        {row.original.priority}
      </Badge>
    ),
  },
  {
    id: "actions",
    cell: ({ row }) => (
      <div className="flex gap-2">
        <Button
          variant="outline"
          size="sm"
          onClick={() => handleEdit(row.original)}
        >
          Edit
        </Button>
        <Button
          variant="destructive"
          size="sm"
          onClick={() => handleDelete(row.original)}
        >
          Delete
        </Button>
      </div>
    ),
  },
]

export async function TaskList() {
  const tasks = await fetchTasks()

  return (
    <DataTable
      columns={columns}
      data={tasks}
      searchKey="title"
      pagination
    />
  )
}

Create Task Dialog

// src/components/task/create-task.tsx
"use client"

import { useState } from "react"
import { Dialog } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Select } from "@/components/ui/select"
import { DatePicker } from "@/components/ui/date-picker"
import { createTask } from "@/lib/task/tasks"

export function CreateTask() {
  const [open, setOpen] = useState(false)
  const [task, setTask] = useState({
    title: "",
    description: "",
    status: "TODO",
    priority: "MEDIUM",
    dueDate: null,
    assigneeId: null,
  })

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault()

    try {
      await createTask(task)
      setOpen(false)
      setTask({
        title: "",
        description: "",
        status: "TODO",
        priority: "MEDIUM",
        dueDate: null,
        assigneeId: null,
      })
    } catch (error) {
      // Handle error
    }
  }

  return (
    <>
      <Button onClick={() => setOpen(true)}>
        Create Task
      </Button>

      <Dialog open={open} onOpenChange={setOpen}>
        <div className="p-6">
          <h2 className="text-xl font-semibold">Create Task</h2>

          <form onSubmit={handleSubmit} className="mt-4 space-y-4">
            <div>
              <label>Title</label>
              <Input
                value={task.title}
                onChange={(e) =>
                  setTask((prev) => ({ ...prev, title: e.target.value }))
                }
              />
            </div>

            <div>
              <label>Description</label>
              <Textarea
                value={task.description}
                onChange={(e) =>
                  setTask((prev) => ({
                    ...prev,
                    description: e.target.value,
                  }))
                }
              />
            </div>

            <div className="grid gap-4 sm:grid-cols-2">
              <div>
                <label>Status</label>
                <Select
                  value={task.status}
                  onValueChange={(value) =>
                    setTask((prev) => ({ ...prev, status: value }))
                  }
                >
                  <SelectItem value="TODO">To Do</SelectItem>
                  <SelectItem value="IN_PROGRESS">In Progress</SelectItem>
                  <SelectItem value="DONE">Done</SelectItem>
                </Select>
              </div>

              <div>
                <label>Priority</label>
                <Select
                  value={task.priority}
                  onValueChange={(value) =>
                    setTask((prev) => ({ ...prev, priority: value }))
                  }
                >
                  <SelectItem value="LOW">Low</SelectItem>
                  <SelectItem value="MEDIUM">Medium</SelectItem>
                  <SelectItem value="HIGH">High</SelectItem>
                </Select>
              </div>
            </div>

            <div>
              <label>Due Date</label>
              <DatePicker
                selected={task.dueDate}
                onChange={(date) =>
                  setTask((prev) => ({ ...prev, dueDate: date }))
                }
              />
            </div>

            <div className="flex justify-end gap-2">
              <Button
                type="button"
                variant="outline"
                onClick={() => setOpen(false)}
              >
                Cancel
              </Button>
              <Button type="submit">Create</Button>
            </div>
          </form>
        </div>
      </Dialog>
    </>
  )
}

Task Calendar

// src/app/(app)/(task)/calendar/page.tsx
import { TaskCalendar } from "@/components/task/task-calendar"
import { TaskCalendarFilters } from "@/components/task/task-calendar-filters"

export default function CalendarPage() {
  return (
    <div className="space-y-8">
      <h1 className="text-3xl font-bold">Task Calendar</h1>

      <TaskCalendarFilters />

      <TaskCalendar />
    </div>
  )
}

Task Reports

// src/app/(app)/(task)/reports/page.tsx
import { TaskStats } from "@/components/task/task-stats"
import { TaskChart } from "@/components/task/task-chart"
import { TaskTrends } from "@/components/task/task-trends"

export default function ReportsPage() {
  return (
    <div className="space-y-8">
      <h1 className="text-3xl font-bold">Task Reports</h1>

      <TaskStats />

      <div className="grid gap-6 lg:grid-cols-2">
        <TaskChart />
        <TaskTrends />
      </div>
    </div>
  )
}

Data Model

// src/lib/task/types.ts
export interface Task {
  id: string
  title: string
  description: string
  status: "TODO" | "IN_PROGRESS" | "DONE"
  priority: "LOW" | "MEDIUM" | "HIGH"
  dueDate: Date | null
  assigneeId: string | null
  createdAt: Date
  updatedAt: Date
}

export interface TaskStats {
  total: number
  completed: number
  overdue: number
  unassigned: number
}

export interface TaskTrend {
  date: Date
  completed: number
  created: number
}

Server Actions

// src/server/actions/tasks.ts
"use server"

import { db } from "@/lib/db"
import { revalidatePath } from "next/cache"
import { type Task } from "@/lib/task/types"

export async function createTask(data: Omit<Task, "id" | "createdAt" | "updatedAt">) {
  const task = await db.task.create({
    data: {
      ...data,
      createdAt: new Date(),
      updatedAt: new Date(),
    },
  })

  revalidatePath("/tasks")
  return task
}

export async function updateTask(id: string, data: Partial<Task>) {
  const task = await db.task.update({
    where: { id },
    data: {
      ...data,
      updatedAt: new Date(),
    },
  })

  revalidatePath("/tasks")
  return task
}

export async function deleteTask(id: string) {
  await db.task.delete({
    where: { id },
  })

  revalidatePath("/tasks")
}

Error Handling

// src/components/task/error-boundary.tsx
"use client"

import { useEffect } from "react"
import { Button } from "@/components/ui/button"
import { toast } from "@/components/ui/toast"

interface ErrorBoundaryProps {
  error: Error
  reset: () => void
}

export default function ErrorBoundary({
  error,
  reset,
}: ErrorBoundaryProps) {
  useEffect(() => {
    // Log task errors
    console.error("Task Error:", error)
  }, [error])

  return (
    <div className="flex min-h-[400px] flex-col items-center justify-center">
      <h2 className="text-2xl font-bold">Task Error</h2>
      <p className="mt-2 text-muted-foreground">
        {error.message}
      </p>
      <Button
        className="mt-4"
        onClick={() => {
          reset()
          toast({
            title: "Reset",
            description: "The task view has been reset.",
          })
        }}
      >
        Try Again
      </Button>
    </div>
  )
}

Testing

// src/tests/task.test.tsx
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { TaskList } from "@/components/task/task-list"
import { CreateTask } from "@/components/task/create-task"

describe("Task Management", () => {
  it("renders task list", async () => {
    render(await TaskList())

    expect(screen.getByText("Tasks")).toBeInTheDocument()
  })

  it("creates new task", async () => {
    const user = userEvent.setup()
    render(<CreateTask />)

    await user.click(screen.getByText("Create Task"))
    await user.type(screen.getByLabelText("Title"), "New Task")
    await user.click(screen.getByText("Create"))

    // Add assertions for task creation
  })
})

Performance Optimization

  1. Data Loading

    • Implement pagination for task lists
    • Use React Suspense for loading states
    • Cache frequently accessed task data
  2. Component Optimization

    • Memoize expensive computations
    • Use virtualization for long lists
    • Implement infinite scrolling
  3. State Management

    • Use server actions for data mutations
    • Implement optimistic updates
    • Handle concurrent modifications

Notes

  • Tasks are protected by authentication
  • All task changes are tracked in history
  • Task assignments trigger notifications
  • Calendar view supports drag-and-drop
  • Reports update in real-time
  • Error boundaries catch task errors
  • Testing covers critical task functions