Skip to content

Commit 5dbdfc5

Browse files
committed
feat: Introduce staff tasks page with a Kanban board and a dialog for creating and editing tasks.
1 parent 700e547 commit 5dbdfc5

File tree

2 files changed

+444
-0
lines changed

2 files changed

+444
-0
lines changed

app/staff/tasks/page.tsx

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
"use client"
2+
3+
import { useEffect, useState, useCallback, useMemo } from "react"
4+
import { createClient } from "@/lib/supabase/client"
5+
import { useAuth } from "@/lib/hooks/useAuth"
6+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
7+
import { Button } from "@/components/ui/button"
8+
import {
9+
ClipboardList,
10+
Clock,
11+
CheckCircle2,
12+
Circle,
13+
AlertCircle,
14+
Calendar,
15+
ArrowRight,
16+
Pencil
17+
} from "lucide-react"
18+
import { format } from "date-fns"
19+
import { TaskDialog } from "@/components/staff/TaskDialog"
20+
21+
type Task = {
22+
id: string
23+
title: string
24+
description: string | null
25+
status: 'todo' | 'in-progress' | 'done'
26+
priority: 'low' | 'medium' | 'high'
27+
due_date: string | null
28+
created_at: string
29+
}
30+
31+
export default function MyTasksPage() {
32+
const { user, loading: authLoading } = useAuth()
33+
const [tasks, setTasks] = useState<Task[]>([])
34+
const [loading, setLoading] = useState(true)
35+
36+
// Stable supabase instance
37+
const supabase = useMemo(() => createClient(), []);
38+
39+
const fetchTasks = useCallback(async () => {
40+
if (!user) return
41+
setLoading(true)
42+
try {
43+
const { data, error } = await supabase
44+
.from("tasks")
45+
.select("*")
46+
.eq("user_id", user.id)
47+
.order("due_date", { ascending: true }) // Earliest due first
48+
49+
if (error && error.code !== '42P01') throw error // Ignore missing table for now
50+
51+
setTasks(data || [])
52+
} catch (err) {
53+
console.error("Error fetching tasks:", err)
54+
} finally {
55+
setLoading(false)
56+
}
57+
}, [user, supabase])
58+
59+
useEffect(() => {
60+
if (!user) return
61+
fetchTasks()
62+
}, [user, fetchTasks])
63+
64+
const updateStatus = async (taskId: string, newStatus: Task['status']) => {
65+
// Optimistic update
66+
setTasks(prev => prev.map(t => t.id === taskId ? { ...t, status: newStatus } : t))
67+
68+
try {
69+
const { error } = await supabase
70+
.from("tasks")
71+
.update({ status: newStatus })
72+
.eq("id", taskId)
73+
74+
if (error) throw error
75+
} catch (err) {
76+
console.error("Error updating status:", err)
77+
// Revert on error (could fetch again)
78+
fetchTasks()
79+
}
80+
}
81+
82+
if (authLoading) {
83+
return (
84+
<div className="flex items-center justify-center min-h-[60vh]">
85+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
86+
</div>
87+
)
88+
}
89+
90+
const columns: { id: Task['status'], title: string, icon: any, color: string }[] = [
91+
{ id: 'todo', title: 'To Do', icon: Circle, color: 'text-zinc-400' },
92+
{ id: 'in-progress', title: 'In Progress', icon: Clock, color: 'text-blue-400' },
93+
{ id: 'done', title: 'Done', icon: CheckCircle2, color: 'text-emerald-400' },
94+
]
95+
96+
return (
97+
<div className="max-w-7xl mx-auto space-y-8 pb-10 px-4 md:px-0 pt-6">
98+
<header className="flex flex-col md:flex-row md:items-center justify-between gap-4">
99+
<div>
100+
<h1 className="text-3xl font-bold tracking-tight text-white mb-2">
101+
My <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400">Tasks</span>
102+
</h1>
103+
<p className="text-zinc-400">
104+
Manage your assignments and track progress.
105+
</p>
106+
</div>
107+
<TaskDialog onTaskSaved={fetchTasks} />
108+
</header>
109+
110+
{/* Kanban Board */}
111+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
112+
{columns.map(col => {
113+
const colTasks = tasks.filter(t => t.status === col.id)
114+
115+
return (
116+
<div key={col.id} className="space-y-4">
117+
{/* Column Header */}
118+
<div className="flex items-center justify-between p-2 rounded-lg bg-zinc-900/50 border border-zinc-800">
119+
<div className="flex items-center gap-2 font-semibold text-zinc-200">
120+
<col.icon className={`w-5 h-5 ${col.color}`} />
121+
{col.title}
122+
<span className="text-xs bg-zinc-800 text-zinc-400 px-2 py-0.5 rounded-full">
123+
{colTasks.length}
124+
</span>
125+
</div>
126+
</div>
127+
128+
{/* Task List */}
129+
<div className="space-y-3 min-h-[200px]">
130+
{colTasks.length === 0 ? (
131+
<div className="h-full border-2 border-dashed border-zinc-800/50 rounded-xl flex items-center justify-center p-8">
132+
<p className="text-zinc-600 text-sm italic">No tasks</p>
133+
</div>
134+
) : (
135+
colTasks.map(task => (
136+
<Card key={task.id} className="bg-zinc-900/80 border-zinc-800/80 hover:border-blue-500/30 transition-all group">
137+
<CardContent className="p-4 space-y-3">
138+
{/* Priority & Due Date & Edit */}
139+
<div className="flex items-center justify-between text-xs">
140+
<div className="flex items-center gap-2">
141+
<span className={`
142+
px-2 py-0.5 rounded font-medium uppercase tracking-wider
143+
${task.priority === 'high' ? 'bg-red-500/10 text-red-500' :
144+
task.priority === 'medium' ? 'bg-orange-500/10 text-orange-400' :
145+
'bg-blue-500/10 text-blue-400'}
146+
`}>
147+
{task.priority}
148+
</span>
149+
{task.due_date && (
150+
<span className="flex items-center gap-1 text-zinc-500">
151+
<Calendar className="w-3 h-3" />
152+
{format(new Date(task.due_date), "MMM d")}
153+
</span>
154+
)}
155+
</div>
156+
157+
<TaskDialog
158+
task={task}
159+
onTaskSaved={fetchTasks}
160+
trigger={
161+
<Button size="icon" variant="ghost" className="h-6 w-6 text-zinc-600 hover:text-white hover:bg-zinc-800">
162+
<Pencil className="w-3 h-3" />
163+
</Button>
164+
}
165+
/>
166+
</div>
167+
168+
{/* Content */}
169+
<div>
170+
<h3 className="font-medium text-zinc-200 group-hover:text-blue-400 transition-colors">
171+
{task.title}
172+
</h3>
173+
{task.description && (
174+
<p className="text-sm text-zinc-500 mt-1 line-clamp-2">
175+
{task.description}
176+
</p>
177+
)}
178+
</div>
179+
180+
{/* Action Buttons (Move Status) */}
181+
<div className="pt-2 border-t border-zinc-800/50 flex justify-end">
182+
{task.status === 'todo' && (
183+
<Button
184+
variant="ghost"
185+
size="sm"
186+
className="h-7 text-xs hover:bg-blue-500/10 hover:text-blue-400"
187+
onClick={() => updateStatus(task.id, 'in-progress')}
188+
>
189+
Start Working <ArrowRight className="w-3 h-3 ml-1" />
190+
</Button>
191+
)}
192+
{task.status === 'in-progress' && (
193+
<Button
194+
variant="ghost"
195+
size="sm"
196+
className="h-7 text-xs hover:bg-emerald-500/10 hover:text-emerald-400"
197+
onClick={() => updateStatus(task.id, 'done')}
198+
>
199+
Mark Done <CheckCircle2 className="w-3 h-3 ml-1" />
200+
</Button>
201+
)}
202+
</div>
203+
</CardContent>
204+
</Card>
205+
))
206+
)}
207+
</div>
208+
</div>
209+
)
210+
})}
211+
</div>
212+
</div>
213+
)
214+
}

0 commit comments

Comments
 (0)