diff --git a/src/components/admin/TimeSlotPickerModal.tsx b/src/components/admin/TimeSlotPickerModal.tsx index 68b9b4b..97456f7 100644 --- a/src/components/admin/TimeSlotPickerModal.tsx +++ b/src/components/admin/TimeSlotPickerModal.tsx @@ -1,10 +1,11 @@ import { useState, useEffect } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; -import { Clock, Calendar as CalendarIcon, Loader2 } from 'lucide-react'; -import { format, addMinutes, parse, isAfter, isBefore, startOfDay } from 'date-fns'; +import { Clock, Calendar as CalendarIcon, Loader2, ChevronLeft, ChevronRight } from 'lucide-react'; +import { format, addMinutes, parse, isAfter, isBefore, startOfDay, addDays, isToday, isPast } from 'date-fns'; import { id } from 'date-fns/locale'; import { supabase } from '@/integrations/supabase/client'; @@ -36,7 +37,7 @@ interface TimeSlotPickerModalProps { selectedDate: Date; initialStartTime?: string; initialEndTime?: string; - onSave: (startTime: string, endTime: string, totalBlocks: number, totalDuration: number) => void; + onSave: (startTime: string, endTime: string, totalBlocks: number, totalDuration: number, selectedDate: string) => void; sessionId?: string; // If editing, exclude this session from availability check } @@ -54,6 +55,9 @@ export function TimeSlotPickerModal({ const [confirmedSlots, setConfirmedSlots] = useState([]); const [loading, setLoading] = useState(true); + // Date selection state + const [currentDate, setCurrentDate] = useState(selectedDate); + // Range selection state const [selectedRange, setSelectedRange] = useState<{ start: string | null; end: string | null }>({ start: initialStartTime || null, @@ -61,11 +65,21 @@ export function TimeSlotPickerModal({ }); const [pendingSlot, setPendingSlot] = useState(null); + // Reset range when date changes + useEffect(() => { + setCurrentDate(selectedDate); + setSelectedRange({ + start: initialStartTime || null, + end: initialEndTime || null + }); + setPendingSlot(null); + }, [selectedDate, initialStartTime, initialEndTime]); + useEffect(() => { if (open) { fetchData(); } - }, [open, selectedDate]); + }, [open, currentDate]); const fetchData = async () => { setLoading(true); @@ -83,7 +97,7 @@ export function TimeSlotPickerModal({ } // Fetch confirmed sessions for availability check - const dateStr = format(selectedDate, 'yyyy-MM-dd'); + const dateStr = format(currentDate, 'yyyy-MM-dd'); const query = supabase .from('consulting_sessions') .select('session_date, start_time, end_time') @@ -103,10 +117,28 @@ export function TimeSlotPickerModal({ setLoading(false); }; + // Date navigation handlers + const handlePreviousDay = () => { + const newDate = addDays(currentDate, -1); + setCurrentDate(newDate); + }; + + const handleNextDay = () => { + const newDate = addDays(currentDate, 1); + setCurrentDate(newDate); + }; + + const handleDateChange = (e: React.ChangeEvent) => { + const newDate = parse(e.target.value, 'yyyy-MM-dd', new Date()); + if (!isNaN(newDate.getTime())) { + setCurrentDate(newDate); + } + }; + const generateTimeSlots = (): TimeSlot[] => { if (!settings || !workhours.length) return []; - const dayOfWeek = selectedDate.getDay(); + const dayOfWeek = currentDate.getDay(); const workhour = workhours.find(wh => wh.weekday === dayOfWeek); if (!workhour) { @@ -119,17 +151,28 @@ export function TimeSlotPickerModal({ const startTime = parse(workhour.start_time, 'HH:mm:ss', new Date()); const endTime = parse(workhour.end_time, 'HH:mm:ss', new Date()); - let currentTime = startTime; - while (true) { - const slotEnd = addMinutes(currentTime, slotDuration); + // For today, filter out passed time slots + const now = new Date(); + const isTodayDate = isToday(currentDate); + const currentTimeStr = isTodayDate ? format(now, 'HH:mm') : '00:00'; - if (isAfter(slotEnd, endTime) || isBefore(slotEnd, currentTime)) { + let currentSlotTime = startTime; + while (true) { + const slotEnd = addMinutes(currentSlotTime, slotDuration); + + if (isAfter(slotEnd, endTime) || isBefore(slotEnd, currentSlotTime)) { break; } - const timeString = format(currentTime, 'HH:mm'); + const timeString = format(currentSlotTime, 'HH:mm'); - // Check if this slot is available + // Skip slots that have already passed for today + if (isTodayDate && timeString < currentTimeStr) { + currentSlotTime = slotEnd; + continue; + } + + // Check if this slot is available (not booked by another session) const isAvailable = !confirmedSlots.some(slot => { const slotStart = slot.start_time.substring(0, 5); const slotEnd = slot.end_time.substring(0, 5); @@ -142,7 +185,7 @@ export function TimeSlotPickerModal({ available: isAvailable }); - currentTime = slotEnd; + currentSlotTime = slotEnd; } return slots; @@ -201,7 +244,8 @@ export function TimeSlotPickerModal({ const handleSave = () => { if (selectedRange.start && selectedRange.end) { - onSave(selectedRange.start, selectedRange.end, totalBlocks, totalDuration); + const dateStr = format(currentDate, 'yyyy-MM-dd'); + onSave(selectedRange.start, selectedRange.end, totalBlocks, totalDuration, dateStr); } }; @@ -209,9 +253,9 @@ export function TimeSlotPickerModal({ - Pilih Waktu Sesi + Pilih Jadwal Sesi - {format(selectedDate, 'd MMMM yyyy', { locale: id })} • Pilih slot waktu untuk sesi konsultasi + Pilih tanggal dan waktu untuk sesi konsultasi @@ -223,74 +267,150 @@ export function TimeSlotPickerModal({ ) : (
- {/* Info */} -
- - - Klik slot untuk memilih durasi. Setiap slot = {settings?.consulting_block_duration_minutes || 30} menit - + {/* Date Selector */} +
+
+ + Tanggal +
+ + {/* Date Navigation */} +
+ + + + + +
+ + {/* Selected Date Display */} +
+

+ {format(currentDate, 'd MMMM yyyy', { locale: id })} +

+

+ {isToday(currentDate) && 'Hari ini • '} + {timeSlots.length} slot tersedia +

+
- {/* Time Slots Grid */} -
- {timeSlots.map((slot) => { - const isSelected = selectedRange.start && selectedRange.end && - timeSlots.findIndex(s => s.start === selectedRange.start) <= - timeSlots.findIndex(s => s.start === slot.start) && - timeSlots.findIndex(s => s.start === selectedRange.end) >= - timeSlots.findIndex(s => s.start === slot.start); + {/* Divider */} +
- const isPending = pendingSlot === slot.start; + {/* Time Slots Section */} +
+
+ + Waktu +
- return ( - - ); - })} -
- - {/* Selection Summary */} - {selectedRange.start && selectedRange.end && ( -
-
-
-

Mulai

-

{selectedRange.start}

-
-
-

-

{totalBlocks} blok

-
-
-

Selesai

-

- {format(addMinutes(parse(selectedRange.end, 'HH:mm', new Date()), settings?.consulting_block_duration_minutes || 30), 'HH:mm')} -

-
+ {/* Info */} + {isToday(currentDate) && timeSlots.length === 0 ? ( +
+

+ Tidak ada slot tersedia untuk sisa hari ini. Silakan pilih tanggal lain. +

-

- Durasi: {totalDuration} menit -

-
- )} + ) : timeSlots.length === 0 ? ( +
+

+ Tidak ada jadwal kerja untuk tanggal ini. +

+
+ ) : ( + <> +
+ + + Klik slot untuk memilih durasi. Setiap slot = {settings?.consulting_block_duration_minutes || 30} menit + +
- {/* Pending Slot */} - {pendingSlot && ( -
-

- Klik lagi untuk konfirmasi slot: {pendingSlot} -

-
- )} + {/* Time Slots Grid */} +
+ {timeSlots.map((slot) => { + const isSelected = selectedRange.start && selectedRange.end && + timeSlots.findIndex(s => s.start === selectedRange.start) <= + timeSlots.findIndex(s => s.start === slot.start) && + timeSlots.findIndex(s => s.start === selectedRange.end) >= + timeSlots.findIndex(s => s.start === slot.start); + + const isPending = pendingSlot === slot.start; + + return ( + + ); + })} +
+ + {/* Selection Summary */} + {selectedRange.start && selectedRange.end && ( +
+
+
+

Mulai

+

{selectedRange.start}

+
+
+

+

{totalBlocks} blok

+
+
+

Selesai

+

+ {format(addMinutes(parse(selectedRange.end, 'HH:mm', new Date()), settings?.consulting_block_duration_minutes || 30), 'HH:mm')} +

+
+
+

+ Durasi: {totalDuration} menit +

+
+ )} + + {/* Pending Slot */} + {pendingSlot && ( +
+

+ Klik lagi untuk konfirmasi slot: {pendingSlot} +

+
+ )} + + )} +
{/* Actions */}
@@ -302,7 +422,7 @@ export function TimeSlotPickerModal({ disabled={!selectedRange.start || !selectedRange.end} className="shadow-sm" > - Simpan Waktu + Simpan Jadwal
diff --git a/src/pages/admin/AdminConsulting.tsx b/src/pages/admin/AdminConsulting.tsx index 5c988fa..c39e763 100644 --- a/src/pages/admin/AdminConsulting.tsx +++ b/src/pages/admin/AdminConsulting.tsx @@ -132,11 +132,12 @@ export default function AdminConsulting() { setDialogOpen(true); }; - const handleTimeSlotSelect = (startTime: string, endTime: string, totalBlocks: number, totalDuration: number) => { + const handleTimeSlotSelect = (startTime: string, endTime: string, totalBlocks: number, totalDuration: number, selectedDate: string) => { setEditStartTime(startTime); setEditEndTime(endTime); setEditTotalBlocks(totalBlocks); setEditTotalDuration(totalDuration); + setEditSessionDate(selectedDate); setTimeSlotPickerOpen(false); }; @@ -1089,7 +1090,7 @@ export default function AdminConsulting() { setTimeSlotPickerOpen(false)} - selectedDate={parseISO(selectedSession.session_date)} + selectedDate={parseISO(editSessionDate || selectedSession.session_date)} initialStartTime={editStartTime} initialEndTime={editEndTime} onSave={handleTimeSlotSelect}