Files
meet-hub/CALENDAR_INTEGRATION.md
dwindown 5ab4e6b974 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>
2025-12-28 13:54:16 +07:00

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.