Add calendar event lifecycle management and "Add to Calendar" feature

- Migrate consulting_slots to consulting_sessions structure
- Add calendar_event_id to track Google Calendar events
- Create delete-calendar-event edge function for auto-cleanup
- Add "Tambah ke Kalender" button for members (OrderDetail, ConsultingHistory)
- Update create-google-meet-event to store calendar event ID
- Update handle-order-paid to use consulting_sessions table
- Remove deprecated create-meet-link function
- Add comprehensive documentation (CALENDAR_INTEGRATION.md, MIGRATION_GUIDE.md)

🤖 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-28 13:54:16 +07:00
parent 952bb209cf
commit 5ab4e6b974
11 changed files with 1303 additions and 554 deletions

View File

@@ -31,8 +31,8 @@ interface Workhour {
end_time: string;
}
interface ConfirmedSlot {
date: string;
interface ConfirmedSession {
session_date: string;
start_time: string;
end_time: string;
}
@@ -107,9 +107,9 @@ export default function ConsultingBooking() {
const fetchConfirmedSlots = async (date: Date) => {
const dateStr = format(date, 'yyyy-MM-dd');
const { data } = await supabase
.from('consulting_slots')
.select('date, start_time, end_time')
.eq('date', dateStr)
.from('consulting_sessions')
.select('session_date, start_time, end_time')
.eq('session_date', dateStr)
.in('status', ['pending_payment', 'confirmed']);
if (data) setConfirmedSlots(data);
@@ -331,26 +331,55 @@ export default function ConsultingBooking() {
if (orderError) throw orderError;
// Create consulting slots
const slotsToInsert = getSlotsInRange.map(slotStart => {
// Create consulting session and time slots
const firstSlotStart = getSlotsInRange[0];
const lastSlotEnd = format(
addMinutes(parse(getSlotsInRange[getSlotsInRange.length - 1], 'HH:mm', new Date()), settings.consulting_block_duration_minutes),
'HH:mm'
);
// Calculate session duration in minutes
const sessionDurationMinutes = totalBlocks * settings.consulting_block_duration_minutes;
// Create the session record (ONE row per booking)
const { data: session, error: sessionError } = await supabase
.from('consulting_sessions')
.insert({
user_id: user.id,
order_id: order.id,
session_date: format(selectedDate, 'yyyy-MM-dd'),
start_time: firstSlotStart + ':00',
end_time: lastSlotEnd + ':00',
total_duration_minutes: sessionDurationMinutes,
topic_category: selectedCategory,
notes: notes,
status: 'pending_payment',
total_blocks: totalBlocks,
total_price: totalPrice,
})
.select()
.single();
if (sessionError) throw sessionError;
// Create time slots for availability tracking (MULTIPLE rows per booking)
const timeSlotsToInsert = getSlotsInRange.map(slotStart => {
const slotEnd = format(
addMinutes(parse(slotStart, 'HH:mm', new Date()), settings.consulting_block_duration_minutes),
'HH:mm'
);
return {
user_id: user.id,
order_id: order.id,
date: format(selectedDate, 'yyyy-MM-dd'),
session_id: session.id,
slot_date: format(selectedDate, 'yyyy-MM-dd'),
start_time: slotStart + ':00',
end_time: slotEnd + ':00',
status: 'pending_payment',
topic_category: selectedCategory,
notes: notes,
is_available: false,
booked_at: new Date().toISOString(),
};
});
const { error: slotsError } = await supabase.from('consulting_slots').insert(slotsToInsert);
if (slotsError) throw slotsError;
const { error: timeSlotsError } = await supabase.from('consulting_time_slots').insert(timeSlotsToInsert);
if (timeSlotsError) throw timeSlotsError;
// Call edge function to create payment with QR code
const { data: paymentData, error: paymentError } = await supabase.functions.invoke('create-payment', {