Add webinar calendar integration and consulting slot blocking
- Fix webinar duration field reference in Calendar (duration_minutes) - Calculate and display webinar end times in calendar view - Fetch webinars for selected date in consulting booking - Block consulting slots that overlap with webinar times - Show warning when webinars are scheduled on selected date - Properly handle webinar time range conflicts This prevents booking conflicts when users try to schedule consulting sessions during webinar times. Example: Webinar 20:15-22:15 blocks consulting slots 20:00-22:30 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,13 @@ interface ConfirmedSlot {
|
||||
end_time: string;
|
||||
}
|
||||
|
||||
interface Webinar {
|
||||
id: string;
|
||||
title: string;
|
||||
event_start: string;
|
||||
duration_minutes: number | null;
|
||||
}
|
||||
|
||||
interface TimeSlot {
|
||||
start: string;
|
||||
end: string;
|
||||
@@ -54,6 +61,7 @@ export default function ConsultingBooking() {
|
||||
const [settings, setSettings] = useState<ConsultingSettings | null>(null);
|
||||
const [workhours, setWorkhours] = useState<Workhour[]>([]);
|
||||
const [confirmedSlots, setConfirmedSlots] = useState<ConfirmedSlot[]>([]);
|
||||
const [webinars, setWebinars] = useState<Webinar[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [profile, setProfile] = useState<Profile | null>(null);
|
||||
|
||||
@@ -78,6 +86,7 @@ export default function ConsultingBooking() {
|
||||
useEffect(() => {
|
||||
if (selectedDate) {
|
||||
fetchConfirmedSlots(selectedDate);
|
||||
fetchWebinars(selectedDate);
|
||||
}
|
||||
}, [selectedDate]);
|
||||
|
||||
@@ -101,10 +110,22 @@ export default function ConsultingBooking() {
|
||||
.select('date, start_time, end_time')
|
||||
.eq('date', dateStr)
|
||||
.in('status', ['pending_payment', 'confirmed']);
|
||||
|
||||
|
||||
if (data) setConfirmedSlots(data);
|
||||
};
|
||||
|
||||
const fetchWebinars = async (date: Date) => {
|
||||
const dateStr = format(date, 'yyyy-MM-dd');
|
||||
const { data } = await supabase
|
||||
.from('products')
|
||||
.select('id, title, event_start, duration_minutes')
|
||||
.eq('type', 'webinar')
|
||||
.eq('is_active', true)
|
||||
.like('event_start', `${dateStr}%`);
|
||||
|
||||
if (data) setWebinars(data);
|
||||
};
|
||||
|
||||
const categories = useMemo(() => {
|
||||
if (!settings?.consulting_categories) return [];
|
||||
return settings.consulting_categories.split(',').map(c => c.trim()).filter(Boolean);
|
||||
@@ -131,20 +152,36 @@ export default function ConsultingBooking() {
|
||||
const slotStart = format(current, 'HH:mm');
|
||||
const slotEnd = format(addMinutes(current, duration), 'HH:mm');
|
||||
|
||||
// Check if slot conflicts with confirmed/pending slots
|
||||
// Check if slot conflicts with confirmed/pending consulting slots
|
||||
const isConflict = confirmedSlots.some(cs => {
|
||||
const csStart = cs.start_time.substring(0, 5);
|
||||
const csEnd = cs.end_time.substring(0, 5);
|
||||
return !(slotEnd <= csStart || slotStart >= csEnd);
|
||||
});
|
||||
|
||||
// Check if slot conflicts with webinars
|
||||
const webinarConflict = webinars.some(w => {
|
||||
const webinarStart = new Date(w.event_start);
|
||||
const webinarDurationMs = (w.duration_minutes || 60) * 60 * 1000;
|
||||
const webinarEnd = new Date(webinarStart.getTime() + webinarDurationMs);
|
||||
|
||||
const slotStartTime = new Date(selectedDate);
|
||||
slotStartTime.setHours(parseInt(slotStart.split(':')[0]), parseInt(slotStart.split(':')[1]), 0);
|
||||
|
||||
const slotEndTime = new Date(selectedDate);
|
||||
slotEndTime.setHours(parseInt(slotEnd.split(':')[0]), parseInt(slotEnd.split(':')[1]), 0);
|
||||
|
||||
// Block if slot overlaps with webinar time
|
||||
return slotStartTime < webinarEnd && slotEndTime > webinarStart;
|
||||
});
|
||||
|
||||
// Check if slot is in the past for today
|
||||
const isPassed = isToday && isBefore(current, now);
|
||||
|
||||
slots.push({
|
||||
start: slotStart,
|
||||
end: slotEnd,
|
||||
available: !isConflict && !isPassed,
|
||||
available: !isConflict && !webinarConflict && !isPassed,
|
||||
});
|
||||
|
||||
current = addMinutes(current, duration);
|
||||
@@ -152,7 +189,7 @@ export default function ConsultingBooking() {
|
||||
}
|
||||
|
||||
return slots;
|
||||
}, [selectedDate, workhours, confirmedSlots, settings]);
|
||||
}, [selectedDate, workhours, confirmedSlots, webinars, settings]);
|
||||
|
||||
// Helper: Get all slots between start and end (inclusive)
|
||||
const getSlotsInRange = useMemo(() => {
|
||||
@@ -414,6 +451,11 @@ export default function ConsultingBooking() {
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Klik slot awal dan akhir untuk memilih rentang waktu. {settings.consulting_block_duration_minutes} menit per blok.
|
||||
{webinars.length > 0 && (
|
||||
<span className="block mt-1 text-amber-600 dark:text-amber-400">
|
||||
⚠️ {webinars.length} webinar terjadwal - beberapa slot mungkin tidak tersedia
|
||||
</span>
|
||||
)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
Reference in New Issue
Block a user