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:
dwindown
2025-12-25 13:46:03 +07:00
parent eea3a1f8d8
commit 711a5c5d6b
2 changed files with 51 additions and 5 deletions

View File

@@ -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>