import type { Route } from "./+types/projectPage"; import { Button } from "@/components/ui/button"; import { ArrowLeft, Plus, SquarePen } from "lucide-react"; import { Link, useRevalidator } from "react-router"; import { db } from "@lib/db"; import { projects, columns, tasks, type Task, type Column } from "@lib/db/schema"; import { eq, asc, desc } from "drizzle-orm"; import Layout from "@/components/layout"; import { TaskDialog } from "@/components/task/TaskDialog"; import { ColumnDialog } from "@/components/column/ColumnDialog"; import { ProjectDialog } from "@/components/project/ProjectDialog"; import { useEffect, useState } from "react"; import { projectPageAction } from "./projectPageAction"; import { getCurrentUser } from "@lib/auth-utils"; import { canUserEditProject, canUserViewProject } from "@lib/auth"; export function meta({ loaderData }: Route.MetaArgs) { return [ { title: `${loaderData.project.name} - FramSpor` }, { name: "description", content: `Manage tasks for ${loaderData.project.name}` } ]; } export async function loader({ params, request }: Route.LoaderArgs) { const user = await getCurrentUser(request); const projectId = params.id; // Check if user can view this project const canView = await canUserViewProject(user?.id || "", projectId); if (!canView) { throw new Response("You do not have permission to view this project", { status: 403 }); } // Fetch the project const projectResult = await db .select() .from(projects) .where(eq(projects.id, projectId)) .limit(1); if (projectResult.length === 0) { throw new Response("Project not found", { status: 404 }); } const project = projectResult[0]; // Check if user can edit this project const canEdit = await canUserEditProject(user?.id || "", projectId); // Fetch columns for this project const projectColumns = await db .select() .from(columns) .where(eq(columns.projectId, projectId)) .orderBy(asc(columns.position)); // Fetch tasks for each column const columnsWithTasks = await Promise.all( projectColumns.map(async (column) => { const columnTasks = await db .select() .from(tasks) .where(eq(tasks.columnId, column.id)) .orderBy(desc(tasks.priority), asc(tasks.dueDate)); return { ...column, tasks: columnTasks.sort((a, b) => { if (a.dueDate === null && b.dueDate === null) return 0; if (a.dueDate === null) return 1; if (b.dueDate === null) return -1; return a.dueDate.getTime() - b.dueDate.getTime(); }) }; }) ); return { project, columns: columnsWithTasks, user, canEdit }; } interface ColumnWithTasks extends Column { tasks: Task[]; } export const action = projectPageAction; export default function ProjectBoard({ loaderData }: Route.ComponentProps) { const { project, columns: initialColumns, user, canEdit } = loaderData; const [columns, setColumns] = useState(initialColumns); const [isTaskDialogOpen, setIsTaskDialogOpen] = useState(false); const [isColumnDialogOpen, setIsColumnDialogOpen] = useState(false); const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false); const [selectedColumnId, setSelectedColumnId] = useState(null); const [editingTask, setEditingTask] = useState(null); const [editingColumn, setEditingColumn] = useState(null); const revalidator = useRevalidator(); useEffect(() => { setColumns(loaderData.columns); }, [loaderData, loaderData.columns, loaderData.project]); const handleAddTask = (columnId?: string) => { setSelectedColumnId(columnId || null); setEditingTask(null); setIsTaskDialogOpen(true); }; const handleEditTask = (task: Task) => { setEditingTask(task); setSelectedColumnId(null); setIsTaskDialogOpen(true); }; const handleAddColumn = () => { setEditingColumn(null); setIsColumnDialogOpen(true); }; const handleEditColumn = (column: ColumnWithTasks) => { setEditingColumn(column); setIsColumnDialogOpen(true); }; const handleEditProject = () => { setIsProjectDialogOpen(true); }; const handleColumnSubmit = async (data: { name: string }) => { const formData = new FormData(); if (editingColumn) { formData.append("intent", "updateColumn"); formData.append("columnId", editingColumn.id); } else { formData.append("intent", "createColumn"); } formData.append("name", data.name); const response = await fetch(`/project/${project.id}`, { method: "POST", body: formData }); revalidator.revalidate(); }; const handleDeleteColumn = async (columnId: string) => { const formData = new FormData(); formData.append("intent", "deleteColumn"); formData.append("columnId", columnId); const response = await fetch(`/project/${project.id}`, { method: "POST", body: formData }); if (response.ok) { // Refresh the data revalidator.revalidate(); } }; const handleProjectSubmit = async (data: { name: string; description: string; isPublic: boolean; }) => { const formData = new FormData(); formData.append("intent", "updateProject"); formData.append("name", data.name); formData.append("description", data.description); formData.append("isPublic", data.isPublic ? "true" : "false"); const response = await fetch(`/project/${project.id}`, { method: "POST", body: formData }); if (response.ok) { // Refresh the data revalidator.revalidate(); } }; const handleDeleteProject = async () => { const formData = new FormData(); formData.append("intent", "deleteProject"); const response = await fetch(`/project/${project.id}`, { method: "POST", body: formData }); if (response.ok) { // Redirect to home page window.location.href = "/"; } }; const handleTaskSubmit = async (data: { title: string; description: string; columnId: string; priority: "low" | "medium" | "high"; dueDate?: Date; }) => { const formData = new FormData(); if (editingTask) { formData.append("intent", "updateTask"); formData.append("taskId", editingTask.id); } else { formData.append("intent", "createTask"); } formData.append("title", data.title); formData.append("description", data.description); formData.append("columnId", selectedColumnId || data.columnId); formData.append("priority", data.priority); if (data.dueDate) { formData.append("dueDate", data.dueDate.toISOString()); } const response = await fetch(`/project/${project.id}`, { method: "POST", body: formData }); revalidator.revalidate(); }; const handleTaskDelete = async () => { const formData = new FormData(); formData.append("intent", "deleteTask"); formData.append("taskId", editingTask.id); const response = await fetch(`/project/${project.id}`, { method: "POST", body: formData }); revalidator.revalidate(); }; return ( handleDeleteColumn(editingColumn.id) : undefined} initialData={editingColumn} isEditing={!!editingColumn} columns={columns.length} /> {/* Header */}

{project.name}

{project.description || "No description."} {!canEdit && (View Only)}

{user && ( )} {canEdit && ( <> )}
{/* Kanban Board */}
{columns.map((column) => (

{column.name}

{canEdit && ( )}
{column.tasks.map((task) => (
handleEditTask(task)} >

{task.title}

{task.description && (

{task.description}

)}
{task.priority && ( {task.priority} )} {task.dueDate && ( {new Date(task.dueDate).toLocaleDateString()} )}
))} {canEdit && ( )}
))} {canEdit && (
)}
); }