- 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>
12 KiB
Calendar Event Management - Complete Implementation
Summary
✅ Google Calendar integration is now fully bidirectional:
- ✅ Creates events when sessions are booked
- ✅ Stores Google Calendar event ID for tracking
- ✅ Deletes events when sessions are cancelled
- ✅ Members can add events to their own calendar with one click
What Was Fixed
1. ✅ create-google-meet-event Updated to Use consulting_sessions
File: supabase/functions/create-google-meet-event/index.ts
Changes:
- Removed old
consulting_slotsqueries (lines 317-334, 355-373) - Now updates
consulting_sessionstable instead - Stores both
meet_linkANDcalendar_event_idin the session - Much simpler - just update one row per session
Before:
// Had to check order_id and update multiple slots
const { data: slotData } = await supabase
.from("consulting_slots")
.select("order_id")
.eq("id", body.slot_id)
.single();
if (slotData?.order_id) {
await supabase
.from("consulting_slots")
.update({ meet_link: meetLink })
.eq("order_id", slotData.order_id);
}
After:
// Just update the session directly
await supabase
.from("consulting_sessions")
.update({
meet_link: meetLink,
calendar_event_id: eventDataResult.id // ← NEW: Store event ID!
})
.eq("id", body.slot_id);
2. ✅ Database Migration - Add calendar_event_id Column
File: supabase/migrations/20241228_add_calendar_event_id.sql
-- Add column to store Google Calendar event ID
ALTER TABLE consulting_sessions
ADD COLUMN calendar_event_id TEXT;
-- Index for faster lookups
CREATE INDEX idx_consulting_sessions_calendar_event
ON consulting_sessions(calendar_event_id);
COMMENT ON COLUMN consulting_sessions.calendar_event_id
IS 'Google Calendar event ID - used to delete events when sessions are cancelled/refunded';
What this does:
- Stores the Google Calendar event ID for each consulting session
- Allows us to delete the event later when session is cancelled/refunded
- No more orphaned calendar events!
3. ✅ New Edge Function: delete-calendar-event
File: supabase/functions/delete-calendar-event/index.ts
What it does:
- Takes a
session_idas input - Retrieves the session's
calendar_event_id - Uses Google Calendar API to DELETE the event
- Clears the
calendar_event_idfrom the database
API Usage:
await supabase.functions.invoke('delete-calendar-event', {
body: { session_id: 'session-uuid-here' }
});
Google Calendar API Call:
DELETE https://www.googleapis.com/calendar/v3/calendars/{calendarId}/events/{eventId}
Authorization: Bearer {access_token}
Error Handling:
- If event already deleted (410 Gone): Logs and continues
- If calendar not configured: Returns success (graceful degradation)
- If deletion fails: Logs error but doesn't block the operation
4. ✅ Admin Panel Integration - Auto-Delete on Cancel
File: src/pages/admin/AdminConsulting.tsx
Changes:
- Added
calendar_event_idtoConsultingSessioninterface - Updated
updateSessionStatus()to calldelete-calendar-eventbefore cancelling - Calendar events are automatically deleted when admin cancels a session
Code:
const updateSessionStatus = async (sessionId: string, newStatus: string) => {
// If cancelling and session has a calendar event, delete it first
if (newStatus === 'cancelled') {
const session = sessions.find(s => s.id === sessionId);
if (session?.calendar_event_id) {
try {
await supabase.functions.invoke('delete-calendar-event', {
body: { session_id: sessionId }
});
} catch (err) {
console.log('Failed to delete calendar event:', err);
// Continue with status update even if calendar deletion fails
}
}
}
// Update session status
const { error } = await supabase
.from('consulting_sessions')
.update({ status: newStatus })
.eq('id', sessionId);
if (!error) {
toast({ title: 'Berhasil', description: `Status diubah ke ${statusLabels[newStatus]?.label || newStatus}` });
fetchSessions();
}
};
5. ✅ "Add to Calendar" Button for Members
Files: src/pages/member/OrderDetail.tsx, src/components/reviews/ConsultingHistory.tsx
What it does:
- Allows members to add consulting sessions to their own Google Calendar
- Uses Google Calendar's public URL format (no OAuth required)
- One-click addition with event details pre-filled
How it works:
// Generate Google Calendar link
const generateCalendarLink = (session: ConsultingSession) => {
if (!session.meet_link) return null;
const startDate = new Date(`${session.session_date}T${session.start_time}`);
const endDate = new Date(`${session.session_date}T${session.end_time}`);
// Format dates for Google Calendar (YYYYMMDDTHHmmssZ)
const formatDate = (date: Date) => {
return date.toISOString().replace(/-|:|\.\d\d\d/g, '');
};
const params = new URLSearchParams({
action: 'TEMPLATE',
text: `Konsultasi: ${session.topic_category || 'Sesi Konsultasi'}`,
dates: `${formatDate(startDate)}/${formatDate(endDate)}`,
details: `Link Meet: ${session.meet_link}`,
location: session.meet_link,
});
return `https://www.google.com/calendar/render?${params.toString()}`;
};
UI Implementation:
OrderDetail.tsx (after meet link):
{consultingSlots[0]?.meet_link && (
<div className="space-y-2">
<div>
<p className="text-muted-foreground text-sm">Google Meet Link</p>
<a href={consultingSlots[0].meet_link} target="_blank">
{consultingSlots[0].meet_link.substring(0, 40)}...
</a>
</div>
<Button asChild variant="outline" size="sm" className="w-full border-2">
<a href={generateCalendarLink(consultingSlots[0]) || '#'} target="_blank">
<Download className="w-4 h-4 mr-2" />
Tambah ke Kalender
</a>
</Button>
</div>
)}
ConsultingHistory.tsx (upcoming sessions):
{session.meet_link && (
<>
<Button asChild size="sm" variant="outline" className="border-2">
<a href={session.meet_link} target="_blank">Join</a>
</Button>
<Button asChild size="sm" variant="outline" className="border-2">
<a href={generateCalendarLink(session) || '#'} target="_blank" title="Tambah ke Kalender">
<Download className="w-4 h-4" />
</a>
</Button>
</>
)}
Google Calendar URL Format:
https://www.google.com/calendar/render?action=TEMPLATE&text=Title&dates=StartDate/EndDate&details=Description&location=Location
Benefits:
- ✅ No OAuth required for users
- ✅ Works with any calendar app that supports Google Calendar links
- ✅ Pre-fills all event details (title, time, description, location)
- ✅ Opens in user's default calendar app
- ✅ One-click addition
Event Flow
Booking Flow (Create)
User books consulting
↓
ConsultingBooking.tsx creates session in DB
↓
handle-order-paid edge function triggered
↓
Calls create-google-meet-event
↓
Creates event in Google Calendar
↓
Returns meet_link + event_id
↓
Updates consulting_sessions:
- meet_link = "https://meet.google.com/xxx-xxx"
- calendar_event_id = "event_id_from_google"
Cancellation Flow (Delete)
Admin cancels session in AdminConsulting.tsx
↓
Calls delete-calendar-event edge function
↓
Retrieves calendar_event_id from consulting_sessions
↓
Calls Google Calendar API to DELETE event
↓
Clears calendar_event_id from database
↓
Updates session status to 'cancelled'
Google Calendar API Response
When an event is created, Google returns:
{
"id": "a1b2c3d4e5f6g7h8i9j0", // ← Calendar event ID
"status": "confirmed",
"htmlLink": "https://www.google.com/calendar/event?eid=a1b2c3d4...",
"created": "2024-12-28T10:00:00.000Z",
"updated": "2024-12-28T10:00:00.000Z",
"summary": "Konsultasi: Career Guidance - John Doe",
"description": "Client: john@example.com\n\nNotes: ...\n\nSlot ID: uuid-here",
"start": {
"dateTime": "2025-01-15T09:00:00+07:00",
"timeZone": "Asia/Jakarta"
},
"end": {
"dateTime": "2025-01-15T12:00:00+07:00",
"timeZone": "Asia/Jakarta"
},
"conferenceData": {
"entryPoints": [
{
"entryPointType": "video",
"uri": "https://meet.google.com/abc-defg-hij", // ← Meet link
"label": "meet.google.com"
}
]
}
}
Important fields:
id- Event ID (stored incalendar_event_id)conferenceData.entryPoints[0].uri- Meet link (stored inmeet_link)
Testing Checklist
✅ Test Event Creation
- Book a consulting session
- Verify Google Calendar event is created
- Verify
meet_linkis saved toconsulting_sessions - Verify
calendar_event_idis saved toconsulting_sessions
✅ Test Event Deletion
- Cancel a session in admin panel
- Verify Google Calendar event is deleted
- Verify
calendar_event_idis cleared from database - Verify session status is set to 'cancelled'
✅ Test Edge Cases
- Cancel session without calendar event (should not fail)
- Cancel session when Google Calendar not configured (should not fail)
- Delete already-deleted event (410 Gone - should handle gracefully)
SQL Migration Steps
Run this migration to add the calendar_event_id column:
# Connect to your Supabase database
psql -h db.xxx.supabase.co -U postgres -d postgres
# Or use Supabase Dashboard:
# SQL Editor → Paste and Run
-- Add calendar_event_id column
ALTER TABLE consulting_sessions
ADD COLUMN calendar_event_id TEXT;
-- Create index
CREATE INDEX idx_consulting_sessions_calendar_event
ON consulting_sessions(calendar_event_id);
-- Verify
SELECT
id,
session_date,
start_time,
end_time,
meet_link,
calendar_event_id
FROM consulting_sessions;
Deploy Edge Functions
# Deploy the updated create-google-meet-event function
supabase functions deploy create-google-meet-event
# Deploy the new delete-calendar-event function
supabase functions deploy delete-calendar-event
Or use the Supabase Dashboard:
- Edge Functions → Select function → Deploy
Future Enhancements
Option 1: Auto-reschedule
If session date/time changes:
- Delete old event
- Create new event with updated time
- Update
calendar_event_idin database
Option 2: Batch Delete
If multiple sessions are cancelled (e.g., order refund):
- Get all
calendar_event_ids for the order - Delete all events in batch
- Clear all
calendar_event_ids
Option 3: Event Sync
Periodic sync to ensure database and calendar are in sync:
- Check all upcoming sessions
- Verify events exist in Google Calendar
- Recreate if missing (with warning)
Troubleshooting
Issue: Event not deleted when session cancelled
Check:
- Does the session have
calendar_event_id?SELECT id, calendar_event_id FROM consulting_sessions WHERE id = 'session-uuid'; - Are the OAuth credentials valid?
SELECT google_oauth_config FROM platform_settings; - Check the edge function logs:
supabase functions logs delete-calendar-event
Issue: "Token exchange failed"
Solution: Refresh OAuth credentials in settings
- Go to: Admin → Settings → Integrations
- Update
google_oauth_configwith newrefresh_token
Issue: Event already deleted (410 Gone)
This is normal! The function handles this gracefully and continues.
Files Modified
- ✅
supabase/functions/create-google-meet-event/index.ts- Use consulting_sessions, store calendar_event_id - ✅
supabase/migrations/20241228_add_calendar_event_id.sql- Add calendar_event_id column - ✅
supabase/functions/delete-calendar-event/index.ts- NEW: Delete calendar events - ✅
src/pages/admin/AdminConsulting.tsx- Auto-delete on cancel, add calendar_event_id to interface - ✅
src/pages/member/OrderDetail.tsx- Add "Tambah ke Kalender" button - ✅
src/components/reviews/ConsultingHistory.tsx- Add "Tambah ke Kalender" button
All set! 🎉 Your consulting sessions now have full calendar lifecycle management.