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:
444
CALENDAR_INTEGRATION.md
Normal file
444
CALENDAR_INTEGRATION.md
Normal file
@@ -0,0 +1,444 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user