diff --git a/CALENDAR_INTEGRATION.md b/CALENDAR_INTEGRATION.md new file mode 100644 index 0000000..3a671c3 --- /dev/null +++ b/CALENDAR_INTEGRATION.md @@ -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 && ( +
Google Meet Link
+ + {consultingSlots[0].meet_link.substring(0, 40)}... + +{order.profile?.name || '-'}
+{order.profile?.email}
+// CHANGE TO: +{session.profiles?.name || '-'}
+{session.profiles?.email}
+ +// 6. Category cell: +Tanggal: {format(parseISO(selectedSlot.date), 'd MMMM yyyy', { locale: id })}
+Waktu: {selectedSlot.start_time.substring(0, 5)} - {selectedSlot.end_time.substring(0, 5)}
+Klien: {selectedSlot.profiles?.name}
+Topik: {selectedSlot.topic_category}
+ {selectedSlot.notes &&Catatan: {selectedSlot.notes}
} +Tanggal: {format(parseISO(selectedSession.session_date), 'd MMMM yyyy', { locale: id })}
+Waktu: {selectedSession.start_time.substring(0, 5)} - {selectedSession.end_time.substring(0, 5)}
+Klien: {selectedSession.profiles?.name}
+Topik: {selectedSession.topic_category}
+ {selectedSession.notes &&Catatan: {selectedSession.notes}
} +- {format(new Date(firstSlot.date), 'd MMM yyyy', { locale: id })} -
-
-
+ {format(new Date(session.session_date), 'd MMM yyyy', { locale: id })} +
+
+
- {format(new Date(firstSlot.date), 'd MMM yyyy', { locale: id })} + {format(new Date(session.session_date), 'd MMM yyyy', { locale: id })}