diff --git a/app/staff/tasks/page.tsx b/app/staff/tasks/page.tsx new file mode 100644 index 00000000..a93d07d5 --- /dev/null +++ b/app/staff/tasks/page.tsx @@ -0,0 +1,214 @@ +"use client" + +import { useEffect, useState, useCallback, useMemo } from "react" +import { createClient } from "@/lib/supabase/client" +import { useAuth } from "@/lib/hooks/useAuth" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { + ClipboardList, + Clock, + CheckCircle2, + Circle, + AlertCircle, + Calendar, + ArrowRight, + Pencil +} from "lucide-react" +import { format } from "date-fns" +import { TaskDialog } from "@/components/staff/TaskDialog" + +type Task = { + id: string + title: string + description: string | null + status: 'todo' | 'in-progress' | 'done' + priority: 'low' | 'medium' | 'high' + due_date: string | null + created_at: string +} + +export default function MyTasksPage() { + const { user, loading: authLoading } = useAuth() + const [tasks, setTasks] = useState([]) + const [loading, setLoading] = useState(true) + + // Stable supabase instance + const supabase = useMemo(() => createClient(), []); + + const fetchTasks = useCallback(async () => { + if (!user) return + setLoading(true) + try { + const { data, error } = await supabase + .from("tasks") + .select("*") + .eq("user_id", user.id) + .order("due_date", { ascending: true }) // Earliest due first + + if (error && error.code !== '42P01') throw error // Ignore missing table for now + + setTasks(data || []) + } catch (err) { + console.error("Error fetching tasks:", err) + } finally { + setLoading(false) + } + }, [user, supabase]) + + useEffect(() => { + if (!user) return + fetchTasks() + }, [user, fetchTasks]) + + const updateStatus = async (taskId: string, newStatus: Task['status']) => { + // Optimistic update + setTasks(prev => prev.map(t => t.id === taskId ? { ...t, status: newStatus } : t)) + + try { + const { error } = await supabase + .from("tasks") + .update({ status: newStatus }) + .eq("id", taskId) + + if (error) throw error + } catch (err) { + console.error("Error updating status:", err) + // Revert on error (could fetch again) + fetchTasks() + } + } + + if (authLoading) { + return ( +
+
+
+ ) + } + + const columns: { id: Task['status'], title: string, icon: any, color: string }[] = [ + { id: 'todo', title: 'To Do', icon: Circle, color: 'text-zinc-400' }, + { id: 'in-progress', title: 'In Progress', icon: Clock, color: 'text-blue-400' }, + { id: 'done', title: 'Done', icon: CheckCircle2, color: 'text-emerald-400' }, + ] + + return ( +
+
+
+

+ My Tasks +

+

+ Manage your assignments and track progress. +

+
+ +
+ + {/* Kanban Board */} +
+ {columns.map(col => { + const colTasks = tasks.filter(t => t.status === col.id) + + return ( +
+ {/* Column Header */} +
+
+ + {col.title} + + {colTasks.length} + +
+
+ + {/* Task List */} +
+ {colTasks.length === 0 ? ( +
+

No tasks

+
+ ) : ( + colTasks.map(task => ( + + + {/* Priority & Due Date & Edit */} +
+
+ + {task.priority} + + {task.due_date && ( + + + {format(new Date(task.due_date), "MMM d")} + + )} +
+ + + + + } + /> +
+ + {/* Content */} +
+

+ {task.title} +

+ {task.description && ( +

+ {task.description} +

+ )} +
+ + {/* Action Buttons (Move Status) */} +
+ {task.status === 'todo' && ( + + )} + {task.status === 'in-progress' && ( + + )} +
+
+
+ )) + )} +
+
+ ) + })} +
+
+ ) +} diff --git a/components/staff/TaskDialog.tsx b/components/staff/TaskDialog.tsx new file mode 100644 index 00000000..547962a3 --- /dev/null +++ b/components/staff/TaskDialog.tsx @@ -0,0 +1,230 @@ +"use client" + +import { useState, useEffect } from "react" +import { createClient } from "@/lib/supabase/client" +import { useAuth } from "@/lib/hooks/useAuth" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Label } from "@/components/ui/label" +import { toast } from "sonner" +import { Plus, Loader2, Pencil } from "lucide-react" + +type Task = { + id: string + title: string + description: string | null + status: 'todo' | 'in-progress' | 'done' + priority: 'low' | 'medium' | 'high' + due_date: string | null +} + +interface TaskDialogProps { + task?: Task // If provided, we are in Edit mode + trigger?: React.ReactNode + onTaskSaved: () => void +} + +export function TaskDialog({ task, trigger, onTaskSaved }: TaskDialogProps) { + const { user } = useAuth() + const [open, setOpen] = useState(false) + const [loading, setLoading] = useState(false) + const supabase = createClient() + + const [formData, setFormData] = useState({ + title: "", + description: "", + priority: "medium" as "low" | "medium" | "high", + due_date: "", + }) + + // Initialize form if in Edit mode + useEffect(() => { + if (task && open) { + setFormData({ + title: task.title, + description: task.description || "", + priority: task.priority, + due_date: task.due_date ? task.due_date.split('T')[0] : "", + }) + } + }, [task, open]) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!user) return + + if (!formData.title.trim()) { + toast.error("Please enter a task title") + return + } + + setLoading(true) + try { + let isoDate = null; + if (formData.due_date) { + const dateObj = new Date(formData.due_date); + if (!isNaN(dateObj.getTime())) { + isoDate = dateObj.toISOString(); + } + } + + if (task?.id) { + // UPDATE Mode + const { error } = await supabase + .from("tasks") + .update({ + title: formData.title.trim(), + description: formData.description.trim() || null, + priority: formData.priority, + due_date: isoDate, + }) + .eq("id", task.id) + + if (error) throw error + toast.success("Task updated!") + } else { + // CREATE Mode + const { error } = await supabase + .from("tasks") + .insert({ + user_id: user.id, + title: formData.title.trim(), + description: formData.description.trim() || null, + priority: formData.priority, + status: "todo", + due_date: isoDate, + }) + + if (error) throw error + toast.success("Task created!") + } + + setOpen(false) + onTaskSaved() + + // Clear form only on create + if (!task) { + setFormData({ + title: "", + description: "", + priority: "medium", + due_date: "", + }) + } + } catch (err: any) { + console.error("Error saving task:", err) + toast.error(err.message || "Failed to save task") + } finally { + setLoading(false) + } + } + + return ( + + + {trigger || ( + + )} + + + + + {task ? "Edit Task" : "Create New Task"} + + +
+
+ + setFormData({ ...formData, title: e.target.value })} + /> +
+ +
+ +