"use client"; import { useState, useEffect, useCallback } from "react"; import { Plus, X, Save, ArrowLeft, User } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; import { fetchProjectTasks, createTask, updateTask, fetchEmployeesForAssign, type TaskRow, } from "@/lib/supabase/task-actions"; import PageSkeleton from "./PageSkeleton"; const COLUMNS = [ { key: "todo", label: "To do", color: "bg-gray-100" }, { key: "in_progress", label: "In progress", color: "bg-blue-100" }, { key: "done", label: "Done", color: "bg-green-100" }, ] as const; const PRIORITY_COLORS: Record = { low: "bg-gray-100 text-gray-600 border-gray-200", medium: "bg-blue-50 text-blue-700 border-blue-200", high: "bg-amber-50 text-amber-700 border-amber-200", urgent: "bg-red-50 text-red-700 border-red-200", }; const AVATAR_COLORS = [ "#1D9E75", "#534AB7", "#E07A5F", "#3D84A8", "#F2CC8F", "#81B29A", "#E07A5F", "#6C63FF", ]; function getInitials(first: string, last: string): string { return `${(first?.[0] || "")}${(last?.[0] || "")}`.toUpperCase(); } function getAvatarColor(name: string): string { let hash = 0; for (let i = 0; i < name.length; i++) { hash = name.charCodeAt(i) + ((hash << 5) - hash); } return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length]; } function formatDate(d: string | null): string { if (!d) return ""; return new Date(d).toLocaleDateString("en-AU", { day: "numeric", month: "short", }); } function isOverdue(task: TaskRow): boolean { if (!task.due_date || task.status === "done") return false; return new Date(task.due_date) < new Date(new Date().toDateString()); } interface EmployeeOption { id: string; first_name: string; last_name: string; email: string | null; } export default function TasksProjectPage({ projectId, onBack, }: { projectId: string; onBack: () => void; }) { const [tasks, setTasks] = useState([]); const [employees, setEmployees] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Inline add form state const [addColumn, setAddColumn] = useState(null); const [addTitle, setAddTitle] = useState(""); const [addDesc, setAddDesc] = useState(""); const [addAssignee, setAddAssignee] = useState(""); const [addDueDate, setAddDueDate] = useState(""); const [addPriority, setAddPriority] = useState("medium"); const [addSaving, setAddSaving] = useState(false); // Slide-over panel const [editTask, setEditTask] = useState(null); const [editTitle, setEditTitle] = useState(""); const [editDesc, setEditDesc] = useState(""); const [editAssignee, setEditAssignee] = useState(""); const [editDueDate, setEditDueDate] = useState(""); const [editPriority, setEditPriority] = useState(""); const [editStatus, setEditStatus] = useState(""); const [editSaving, setEditSaving] = useState(false); useEffect(() => { let cancelled = false; Promise.all([fetchProjectTasks(projectId), fetchEmployeesForAssign()]) .then(([t, e]) => { if (!cancelled) { setTasks(t); setEmployees(e); } }) .catch((err) => { console.error("[TasksProject] Failed to load:", err); setError(err.message || "Failed to load tasks"); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [projectId]); const openAddForm = useCallback((col: string) => { setAddColumn(col); setAddTitle(""); setAddDesc(""); setAddAssignee(""); setAddDueDate(""); setAddPriority("medium"); }, []); const closeAddForm = useCallback(() => { setAddColumn(null); }, []); const handleAddTask = async () => { if (!addTitle.trim() || !addColumn) return; setAddSaving(true); setError(null); const result = await createTask({ project_id: projectId, title: addTitle.trim(), description: addDesc.trim() || undefined, assigned_to: addAssignee || undefined, due_date: addDueDate || undefined, priority: addPriority, status: addColumn, }); setAddSaving(false); if (!result.success) { setError(result.error || "Failed to create task"); return; } closeAddForm(); const updated = await fetchProjectTasks(projectId); setTasks(updated); }; const openEditPanel = useCallback((task: TaskRow) => { setEditTask(task); setEditTitle(task.title); setEditDesc(task.description || ""); setEditAssignee(task.assigned_to || ""); setEditDueDate(task.due_date || ""); setEditPriority(task.priority); setEditStatus(task.status); }, []); const closeEditPanel = useCallback(() => { setEditTask(null); }, []); const handleSaveEdit = async () => { if (!editTask) return; setEditSaving(true); setError(null); const result = await updateTask(editTask.id, { title: editTitle.trim(), description: editDesc.trim() || undefined, assigned_to: editAssignee || undefined, due_date: editDueDate || undefined, priority: editPriority, status: editStatus, }); setEditSaving(false); if (!result.success) { setError(result.error || "Failed to update task"); return; } const updated = await fetchProjectTasks(projectId); setTasks(updated); closeEditPanel(); }; if (loading) return ; return (
{error && (
{error}
)} {/* Header */}

Project Tasks

Kanban board

{/* Kanban Columns */}
{COLUMNS.map((col) => { const colTasks = tasks.filter((t) => t.status === col.key); const isAdding = addColumn === col.key; return (
{/* Column header */}

{col.label}

{colTasks.length}
{!isAdding && ( )}
{/* Inline add form */} {isAdding && ( setAddTitle(e.target.value)} placeholder="Task title" className="w-full border border-gray-200 rounded px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#1D9E75]/20 focus:border-[#1D9E75]" autoFocus />