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

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_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:

// 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:

  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:

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_id to ConsultingSession interface
  • Updated updateSessionStatus() to call delete-calendar-event before 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 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:

# 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_id in 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:

  1. Does the session have calendar_event_id?
    SELECT id, calendar_event_id FROM consulting_sessions WHERE id = 'session-uuid';
    
  2. Are the OAuth credentials valid?
    SELECT google_oauth_config FROM platform_settings;
    
  3. 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_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.