diff --git a/app/(app)/dashboard/KanbanCard.tsx b/app/(app)/dashboard/KanbanCard.tsx new file mode 100644 index 0000000..de35e56 --- /dev/null +++ b/app/(app)/dashboard/KanbanCard.tsx @@ -0,0 +1,64 @@ +/** + * @file dashboard/KanbanCard.tsx + * @description Client component rendering a simplified kanban task card displaying title, priority, description, and metadata. + */ + +"use client"; + +import { KanbanCardProps } from "@/types/tasks"; + +/** + * Renders a task card component with a basic layout for title, priority, description, due date, and creator info. + * + * @param {KanbanCardProps} props - The component props containing the task object. + * @returns {JSX.Element} The rendered kanban card component. + */ +export default function KanbanCard({ task }: KanbanCardProps) { + const assignees = task.assignees || []; + + return ( +
+ {/* Title & Priority */} +
+ {task.title} + {task.priority && ( + {task.priority} + )} +
+ + {/* Description */} + {task.description && ( +

{task.description}

+ )} + + {/* Footer / Meta & People */} +
+
+ {task.dueDate && ( + {new Date(task.dueDate).toLocaleDateString()} + )} +
+ +
+ {task.creator && ( + + {task.creator.substring(0, 2).toUpperCase()} + + )} + {assignees.map((assignee, index) => ( + + {assignee.substring(0, 2).toUpperCase()} + + ))} +
+
+
+ ); +} diff --git a/app/(app)/dashboard/KanbanColumn.tsx b/app/(app)/dashboard/KanbanColumn.tsx new file mode 100644 index 0000000..eccdeb2 --- /dev/null +++ b/app/(app)/dashboard/KanbanColumn.tsx @@ -0,0 +1,52 @@ +/** + * @file dashboard/KanbanColumn.tsx + * @description Client component rendering a single column in the kanban board, containing a status header, count, task list, and fallback empty states. + */ + +"use client"; + +import { Plus } from "lucide-react"; +import KanbanCard from "./KanbanCard"; +import { KanbanColumnProps } from "@/types/tasks"; + +/** + * Renders a kanban column with an indicator color, title, task count, quick-add trigger, + * and a list of rendered task cards or an empty placeholder. + * + * @param {KanbanColumnProps} props - The component props defining column metadata and task lists. + * @returns {JSX.Element} The rendered kanban column component. + */ +export default function KanbanColumn(props: KanbanColumnProps) { + return ( +
+ {/* Header */} +
+
+ +

{props.title}

+ {props.count} +
+ +
+ + {/* Task List */} +
+ {props.tasks.length === 0 ? ( +
+ No tasks +
+ ) : ( + props.tasks.map((task) => ( +
+ +
+ )) + )} +
+
+ ); +} diff --git a/app/(app)/dashboard/page.tsx b/app/(app)/dashboard/page.tsx index 647ab1e..1763efa 100644 --- a/app/(app)/dashboard/page.tsx +++ b/app/(app)/dashboard/page.tsx @@ -1,6 +1,105 @@ /** - * Dashboard + * @file dashboard/page.tsx + * @description Server component rendering the main dashboard page, handling authentication, fetching user-related tasks and assignees, and displaying them across kanban columns. */ -export default function Dashboard() { - return
Dashboard
; + +import { db } from "@/db"; +import { tasksTable, taskAssigneesTable, usersTable } from "@/db/schema"; +import { eq, inArray, or } from "drizzle-orm"; +import { auth } from "@/auth"; +import { redirect } from "next/navigation"; +import KanbanColumn from "./KanbanColumn"; +import { KANBAN_COLUMNS, Task } from "@/types/tasks"; + +/** + * Renders the dashboard page layout with user session validation, task queries, assignee mapping, + * and distributes the tasks into respective kanban columns. + * + * @async + * @returns {Promise} The rendered dashboard page component. + */ +export default async function Dashboard() { + const session = await auth(); + if (!session?.user?.id) { + redirect("/login"); + } + + const currentUserId = session.user.id; + + // Determine Assigned Task IDs + const assignedTaskRows = await db + .select({ taskId: taskAssigneesTable.taskId }) + .from(taskAssigneesTable) + .where(eq(taskAssigneesTable.userId, currentUserId)); + + const assignedTaskIds = assignedTaskRows.map((r) => r.taskId); + + // Load tasks, including their creators (created by the user OR assigned) + const taskWhereClause = + assignedTaskIds.length > 0 + ? or( + eq(tasksTable.userId, currentUserId), + inArray(tasksTable.id, assignedTaskIds), + ) + : eq(tasksTable.userId, currentUserId); + + const rawTasksWithCreator = await db + .select({ + task: tasksTable, + creatorEmail: usersTable.email, + }) + .from(tasksTable) + .innerJoin(usersTable, eq(tasksTable.userId, usersTable.id)) + .where(taskWhereClause); + + // Load all assignees for the loaded tasks in a batch + const allTaskIds = rawTasksWithCreator.map((item) => item.task.id); + + const assigneesData = + allTaskIds.length > 0 + ? await db + .select({ + taskId: taskAssigneesTable.taskId, + email: usersTable.email, + }) + .from(taskAssigneesTable) + .innerJoin(usersTable, eq(taskAssigneesTable.userId, usersTable.id)) + .where(inArray(taskAssigneesTable.taskId, allTaskIds)) + : []; + + // Map for high-performance mapping (taskId -> array of emails) + const assigneesMap = new Map(); + for (const row of assigneesData) { + const existing = assigneesMap.get(row.taskId) || []; + assigneesMap.set(row.taskId, [...existing, row.email]); + } + + // Preparing Tasks + const tasks: Task[] = rawTasksWithCreator.map(({ task, creatorEmail }) => ({ + ...task, + creator: creatorEmail, + assignees: assigneesMap.get(task.id) || [], + tags: [], + commentsCount: 0, + })); + + return ( +
+
+ {KANBAN_COLUMNS.map((col) => { + const columnTasks = tasks.filter((t) => t.status === col.id); + return ( + + ); + })} +
+
+ ); } diff --git a/types/tasks.ts b/types/tasks.ts new file mode 100644 index 0000000..2bcb239 --- /dev/null +++ b/types/tasks.ts @@ -0,0 +1,49 @@ +/** + * @file types/tasks.ts + * @description Type definitions, interfaces, and UI configuration mappings for task management and kanban views. + */ + +import { taskStatusEnum, type Task as DbTask } from "@/db/schema"; + +// ========================================== +// Types +// ========================================== + +export type TaskStatus = (typeof taskStatusEnum.enumValues)[number]; + +// ========================================== +// Interfaces +// ========================================== + +export interface Task extends Omit { + dueDate?: Date | null; + assignees?: string[]; + creator?: string; +} + +export interface KanbanColumnConfig { + id: TaskStatus; + title: string; + color: string; +} + +export interface KanbanColumnProps extends KanbanColumnConfig { + count: number; + tasks: Task[]; +} + +export interface KanbanCardProps { + task: Task; + onStatusChange?: (taskId: string, newStatus: TaskStatus) => void; +} + +// ========================================== +// UI Configurations +// ========================================== + +export const KANBAN_COLUMNS = [ + { id: "todo", title: "To-do", color: "bg-zinc-400" }, + { id: "in_progress", title: "In Progress", color: "bg-indigo-500" }, + { id: "await_feedback", title: "Await Feedback", color: "bg-amber-500" }, + { id: "done", title: "Done", color: "bg-emerald-500" }, +] as const satisfies readonly KanbanColumnConfig[];