Skip to content

Commit bdcd3d5

Browse files
authored
Merge pull request #397 from codeunia-dev/feat/staff-admin-attendance-routing
feat: Implement staff schedule page and add scripts for managing and debugging shifts and leaves
2 parents b6a4e38 + 700e547 commit bdcd3d5

File tree

1 file changed

+229
-0
lines changed

1 file changed

+229
-0
lines changed

app/staff/schedule/page.tsx

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
"use client"
2+
3+
import { useEffect, useState } 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+
Calendar,
10+
ChevronLeft,
11+
ChevronRight,
12+
Clock,
13+
CalendarDays,
14+
Info
15+
} from "lucide-react"
16+
import {
17+
format,
18+
startOfWeek,
19+
endOfWeek,
20+
eachDayOfInterval,
21+
addWeeks,
22+
subWeeks,
23+
isSameDay,
24+
isToday,
25+
parseISO,
26+
isSameMonth
27+
} from "date-fns"
28+
29+
type Shift = {
30+
id: string
31+
title: string
32+
start_time: string
33+
end_time: string
34+
type: 'Regular' | 'Overtime' | 'OnCall'
35+
}
36+
37+
type Leave = {
38+
id: string
39+
start_date: string
40+
end_date: string
41+
leave_type: string
42+
status: string
43+
}
44+
45+
export default function SchedulePage() {
46+
const { user, loading: authLoading } = useAuth()
47+
const [currentDate, setCurrentDate] = useState(new Date())
48+
const [shifts, setShifts] = useState<Shift[]>([])
49+
const [leaves, setLeaves] = useState<Leave[]>([])
50+
const [loading, setLoading] = useState(true)
51+
const supabase = createClient()
52+
53+
useEffect(() => {
54+
if (!user) return
55+
56+
const fetchSchedule = async () => {
57+
setLoading(true)
58+
try {
59+
// Determine date range for current view (give buffer +1/-1 week)
60+
const start = startOfWeek(subWeeks(currentDate, 1), { weekStartsOn: 1 })
61+
const end = endOfWeek(addWeeks(currentDate, 1), { weekStartsOn: 1 })
62+
63+
// Fetch Shifts
64+
const { data: shiftsData, error: shiftsError } = await supabase
65+
.from("shifts")
66+
.select("*")
67+
.eq("user_id", user.id)
68+
.gte("start_time", start.toISOString())
69+
.lte("end_time", end.toISOString())
70+
71+
if (shiftsError && shiftsError.code !== '42P01') throw shiftsError // Ignore table not found for dev
72+
73+
// Fetch Leaves
74+
const { data: leavesData, error: leavesError } = await supabase
75+
.from("leave_requests")
76+
.select("*")
77+
.eq("user_id", user.id)
78+
.neq("status", "rejected")
79+
.gte("end_date", start.toISOString()) // Overlap check simplified
80+
81+
if (leavesError) throw leavesError
82+
83+
setShifts(shiftsData || [])
84+
setLeaves(leavesData || [])
85+
86+
} catch (err) {
87+
console.error("Error fetching schedule:", err)
88+
} finally {
89+
setLoading(false)
90+
}
91+
}
92+
93+
fetchSchedule()
94+
}, [user, currentDate, supabase])
95+
96+
const nextWeek = () => setCurrentDate(addWeeks(currentDate, 1))
97+
const prevWeek = () => setCurrentDate(subWeeks(currentDate, 1))
98+
const resetToToday = () => setCurrentDate(new Date())
99+
100+
const weekStart = startOfWeek(currentDate, { weekStartsOn: 1 }) // Monday
101+
const weekEnd = endOfWeek(currentDate, { weekStartsOn: 1 })
102+
const days = eachDayOfInterval({ start: weekStart, end: weekEnd })
103+
104+
if (authLoading) {
105+
return (
106+
<div className="flex items-center justify-center min-h-[60vh]">
107+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
108+
</div>
109+
)
110+
}
111+
112+
return (
113+
<div className="max-w-7xl mx-auto space-y-8 pb-10 px-4 md:px-0 pt-6">
114+
<header className="flex flex-col md:flex-row md:items-center justify-between gap-4">
115+
<div>
116+
<h1 className="text-3xl font-bold tracking-tight text-white mb-2">
117+
My <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400">Schedule</span>
118+
</h1>
119+
<p className="text-zinc-400">
120+
View your upcoming shifts and time off.
121+
</p>
122+
</div>
123+
<div className="flex items-center gap-2 bg-zinc-900/50 p-1 rounded-lg border border-zinc-800">
124+
<Button variant="ghost" size="icon" onClick={prevWeek} className="h-8 w-8 text-zinc-400 hover:text-white">
125+
<ChevronLeft className="h-4 w-4" />
126+
</Button>
127+
<Button variant="ghost" onClick={resetToToday} className="h-8 text-sm font-medium text-zinc-300 hover:text-white px-3">
128+
{format(currentDate, "MMMM yyyy")}
129+
</Button>
130+
<Button variant="ghost" size="icon" onClick={nextWeek} className="h-8 w-8 text-zinc-400 hover:text-white">
131+
<ChevronRight className="h-4 w-4" />
132+
</Button>
133+
</div>
134+
</header>
135+
136+
{/* Weekly Calendar Grid */}
137+
<div className="grid grid-cols-1 md:grid-cols-7 gap-4">
138+
{days.map((day, i) => {
139+
const isTodayDate = isToday(day)
140+
const isCurrentMonth = isSameMonth(day, currentDate)
141+
142+
// Find shifts for this day
143+
const dayShifts = shifts.filter(s => isSameDay(parseISO(s.start_time), day))
144+
145+
// Find leaves for this day
146+
const dayLeaves = leaves.filter(l => {
147+
const start = parseISO(l.start_date)
148+
const end = parseISO(l.end_date)
149+
// Simple check: is day between start and end (inclusive)
150+
// Compare just dates to avoid time issues
151+
const target = format(day, 'yyyy-MM-dd')
152+
return target >= l.start_date && target <= l.end_date
153+
})
154+
155+
return (
156+
<Card
157+
key={i}
158+
className={`
159+
border-zinc-800 backdrop-blur-sm overflow-hidden min-h-[200px] flex flex-col
160+
${isTodayDate ? 'bg-zinc-800/40 border-blue-500/30 ring-1 ring-blue-500/20' : 'bg-zinc-900/50'}
161+
${!isCurrentMonth ? 'opacity-50' : ''}
162+
`}
163+
>
164+
<CardHeader className="p-3 border-b border-zinc-800/50">
165+
<div className="flex items-baseline justify-between">
166+
<span className="text-sm font-medium text-zinc-400 uppercase">{format(day, "EEE")}</span>
167+
<span className={`text-lg font-bold ${isTodayDate ? 'text-blue-400' : 'text-zinc-200'}`}>
168+
{format(day, "d")}
169+
</span>
170+
</div>
171+
</CardHeader>
172+
<CardContent className="p-2 flex-1 space-y-2">
173+
{/* Shifts */}
174+
{dayShifts.map(shift => (
175+
<div key={shift.id} className="bg-blue-600/10 border border-blue-600/20 p-2 rounded text-xs">
176+
<div className="font-semibold text-blue-400 mb-0.5">{shift.title}</div>
177+
<div className="flex items-center gap-1 text-zinc-400">
178+
<Clock className="w-3 h-3" />
179+
{format(parseISO(shift.start_time), "HH:mm")} - {format(parseISO(shift.end_time), "HH:mm")}
180+
</div>
181+
</div>
182+
))}
183+
184+
{/* Leaves */}
185+
{dayLeaves.map(leave => (
186+
<div key={leave.id} className="bg-yellow-600/10 border border-yellow-600/20 p-2 rounded text-xs">
187+
<div className="font-semibold text-yellow-400 mb-0.5">On Leave</div>
188+
<div className="text-zinc-400">{leave.leave_type}</div>
189+
</div>
190+
))}
191+
192+
{/* Empty State (if weekday and no activity) */}
193+
{dayShifts.length === 0 && dayLeaves.length === 0 && day.getDay() !== 0 && (
194+
<div className="h-full flex items-center justify-center p-4">
195+
<span className="text-zinc-700 text-xs text-center"></span>
196+
</div>
197+
)}
198+
199+
{/* Sunday indicator */}
200+
{day.getDay() === 0 && dayShifts.length === 0 && (
201+
<div className="h-full flex flex-col items-center justify-center p-2 opacity-50">
202+
<CalendarDays className="w-6 h-6 text-zinc-700 mb-1" />
203+
<span className="text-zinc-600 text-xs font-medium">Weekly Off</span>
204+
</div>
205+
)}
206+
</CardContent>
207+
</Card>
208+
)
209+
})}
210+
</div>
211+
212+
{/* Legend / Info */}
213+
<div className="flex gap-6 justify-center md:justify-start text-xs text-zinc-500">
214+
<div className="flex items-center gap-2">
215+
<div className="w-3 h-3 rounded bg-blue-500/20 border border-blue-500/30"></div>
216+
<span>Scheduled Shift</span>
217+
</div>
218+
<div className="flex items-center gap-2">
219+
<div className="w-3 h-3 rounded bg-yellow-500/20 border border-yellow-500/30"></div>
220+
<span>On Leave</span>
221+
</div>
222+
<div className="flex items-center gap-2">
223+
<div className="w-3 h-3 rounded bg-zinc-800 border-zinc-700"></div>
224+
<span>Weekly Off</span>
225+
</div>
226+
</div>
227+
</div>
228+
)
229+
}

0 commit comments

Comments
 (0)