- 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>
445 lines
12 KiB
Markdown
445 lines
12 KiB
Markdown
# 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_slots` queries (lines 317-334, 355-373)
|
|
- Now updates `consulting_sessions` table instead
|
|
- Stores both `meet_link` AND `calendar_event_id` in the session
|
|
- Much simpler - just update one row per session
|
|
|
|
**Before:**
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// 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`
|
|
|
|
```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:**
|
|
1. Takes a `session_id` as input
|
|
2. Retrieves the session's `calendar_event_id`
|
|
3. Uses Google Calendar API to DELETE the event
|
|
4. Clears the `calendar_event_id` from the database
|
|
|
|
**API Usage:**
|
|
```typescript
|
|
await supabase.functions.invoke('delete-calendar-event', {
|
|
body: { session_id: 'session-uuid-here' }
|
|
});
|
|
```
|
|
|
|
**Google Calendar API Call:**
|
|
```http
|
|
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_id` to `ConsultingSession` interface
|
|
- Updated `updateSessionStatus()` to call `delete-calendar-event` before cancelling
|
|
- Calendar events are automatically deleted when admin cancels a session
|
|
|
|
**Code:**
|
|
```typescript
|
|
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:**
|
|
|
|
```typescript
|
|
// 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):
|
|
```tsx
|
|
{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):
|
|
```tsx
|
|
{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:
|
|
|
|
```json
|
|
{
|
|
"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 in `calendar_event_id`)
|
|
- `conferenceData.entryPoints[0].uri` - Meet link (stored in `meet_link`)
|
|
|
|
---
|
|
|
|
## Testing Checklist
|
|
|
|
### ✅ Test Event Creation
|
|
- [ ] Book a consulting session
|
|
- [ ] Verify Google Calendar event is created
|
|
- [ ] Verify `meet_link` is saved to `consulting_sessions`
|
|
- [ ] Verify `calendar_event_id` is saved to `consulting_sessions`
|
|
|
|
### ✅ Test Event Deletion
|
|
- [ ] Cancel a session in admin panel
|
|
- [ ] Verify Google Calendar event is deleted
|
|
- [ ] Verify `calendar_event_id` is 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:
|
|
|
|
```bash
|
|
# Connect to your Supabase database
|
|
psql -h db.xxx.supabase.co -U postgres -d postgres
|
|
|
|
# Or use Supabase Dashboard:
|
|
# SQL Editor → Paste and Run
|
|
```
|
|
|
|
```sql
|
|
-- 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
|
|
|
|
```bash
|
|
# 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_id` in database
|
|
|
|
### Option 2: Batch Delete
|
|
If multiple sessions are cancelled (e.g., order refund):
|
|
- Get all `calendar_event_id`s for the order
|
|
- Delete all events in batch
|
|
- Clear all `calendar_event_id`s
|
|
|
|
### 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:**
|
|
1. Does the session have `calendar_event_id`?
|
|
```sql
|
|
SELECT id, calendar_event_id FROM consulting_sessions WHERE id = 'session-uuid';
|
|
```
|
|
2. Are the OAuth credentials valid?
|
|
```sql
|
|
SELECT google_oauth_config FROM platform_settings;
|
|
```
|
|
3. Check the edge function logs:
|
|
```bash
|
|
supabase functions logs delete-calendar-event
|
|
```
|
|
|
|
### Issue: "Token exchange failed"
|
|
**Solution:** Refresh OAuth credentials in settings
|
|
- Go to: Admin → Settings → Integrations
|
|
- Update `google_oauth_config` with new `refresh_token`
|
|
|
|
### Issue: Event already deleted (410 Gone)
|
|
**This is normal!** The function handles this gracefully and continues.
|
|
|
|
---
|
|
|
|
## Files Modified
|
|
|
|
1. ✅ `supabase/functions/create-google-meet-event/index.ts` - Use consulting_sessions, store calendar_event_id
|
|
2. ✅ `supabase/migrations/20241228_add_calendar_event_id.sql` - Add calendar_event_id column
|
|
3. ✅ `supabase/functions/delete-calendar-event/index.ts` - NEW: Delete calendar events
|
|
4. ✅ `src/pages/admin/AdminConsulting.tsx` - Auto-delete on cancel, add calendar_event_id to interface
|
|
5. ✅ `src/pages/member/OrderDetail.tsx` - Add "Tambah ke Kalender" button
|
|
6. ✅ `src/components/reviews/ConsultingHistory.tsx` - Add "Tambah ke Kalender" button
|
|
|
|
---
|
|
|
|
**All set!** 🎉
|
|
Your consulting sessions now have full calendar lifecycle management.
|