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:
dwindown
2025-12-28 13:54:16 +07:00
parent 952bb209cf
commit 5ab4e6b974
11 changed files with 1303 additions and 554 deletions

227
MIGRATION_GUIDE.md Normal file
View File

@@ -0,0 +1,227 @@
# Consulting Slots Migration - Code Updates Summary
## ✅ Completed Files
### 1. src/pages/ConsultingBooking.tsx ✅
- Updated interface: `ConfirmedSlot``ConfirmedSession` with `session_date` field
- Updated `fetchConfirmedSlots()` to query `consulting_sessions` table
- Updated slot creation logic to:
- Create ONE `consulting_sessions` row with session-level data
- Create MULTIPLE `consulting_time_slots` rows for each 45-min block
- Conflict checking logic already compatible (uses `start_time`/`end_time` fields)
### 2. supabase/functions/create-meet-link/index.ts ✅
- Changed update query from `consulting_slots` to `consulting_sessions`
- Updates meet_link once per session instead of once per slot
## ⏳ In Progress
### 3. src/pages/admin/AdminConsulting.tsx (PARTIAL)
**Updated:**
- Interface: `ConsultingSlot``ConsultingSession`
- State: `slots``sessions`, `selectedSlot``selectedSession`
- `fetchSessions()` - now queries `consulting_sessions` with profiles join
- `openMeetDialog()` - uses session parameter
- `saveMeetLink()` - updates `consulting_sessions` table
- `createMeetLink()` - uses session fields (`session_date`, etc.)
- `updateSessionStatus()` - renamed from `updateSlotStatus()`
- Filtering logic - simplified (no grouping needed)
- Stats sections - use `sessions` arrays
- Today's Sessions Alert - uses `todaySessions` array
**Still Needs Manual Update:**
Replace all remaining references in the table rendering sections (lines ~428-end):
```typescript
// FIND AND REPLACE THESE PATTERNS:
// 1. Tabs list:
<TabsTrigger value="upcoming">Mendatang ({upcomingOrders.length})</TabsTrigger>
<TabsTrigger value="past">Riwayat ({pastOrders.length})</TabsTrigger>
// CHANGE TO:
<TabsTrigger value="upcoming">Mendatang ({upcomingSessions.length})</TabsTrigger>
<TabsTrigger value="past">Riwayat ({pastSessions.length})</TabsTrigger>
// 2. Desktop table - upcoming:
{upcomingOrders.map((order) => {
const firstSlot = order.slots[0];
const lastSlot = order.slots[order.slots.length - 1];
const sessionCount = order.slots.length;
return (
<TableRow key={order.orderId || 'no-order'}>
// CHANGE TO:
{upcomingSessions.map((session) => {
return (
<TableRow key={session.id}>
// 3. Date cell:
{format(parseISO(firstSlot.date), 'd MMM yyyy', { locale: id })}
{isToday(parseISO(firstSlot.date)) && <Badge className="ml-2 bg-primary">Hari Ini</Badge>}
{isTomorrow(parseISO(firstSlot.date)) && <Badge className="ml-2 bg-accent">Besok</Badge>}
// CHANGE TO:
{format(parseISO(session.session_date), 'd MMM yyyy', { locale: id })}
{isToday(parseISO(session.session_date)) && <Badge className="ml-2 bg-primary">Hari Ini</Badge>}
{isTomorrow(parseISO(session.session_date)) && <Badge className="ml-2 bg-accent">Besok</Badge>}
// 4. Time cell:
<div>{firstSlot.start_time.substring(0, 5)} - {lastSlot.end_time.substring(0, 5)}</div>
{sessionCount > 1 && (
<div className="text-xs text-muted-foreground">{sessionCount} sesi</div>
)}
// CHANGE TO:
<div>{session.start_time.substring(0, 5)} - {session.end_time.substring(0, 5)}</div>
{session.total_blocks > 1 && (
<div className="text-xs text-muted-foreground">{session.total_blocks} blok</div>
)}
// 5. Client cell:
<p className="font-medium">{order.profile?.name || '-'}</p>
<p className="text-sm text-muted-foreground">{order.profile?.email}</p>
// CHANGE TO:
<p className="font-medium">{session.profiles?.name || '-'}</p>
<p className="text-sm text-muted-foreground">{session.profiles?.email}</p>
// 6. Category cell:
<Badge variant="outline">{firstSlot.topic_category}</Badge>
// CHANGE TO:
<Badge variant="outline">{session.topic_category}</Badge>
// 7. Status cell:
<Badge variant={statusLabels[firstSlot.status]?.variant || 'secondary'}>
{statusLabels[firstSlot.status]?.label || firstSlot.status}
</Badge>
// CHANGE TO:
<Badge variant={statusLabels[session.status]?.variant || 'secondary'}>
{statusLabels[session.status]?.label || session.status}
</Badge>
// 8. Meet link cell:
{order.meetLink ? (
<a href={order.meetLink} ...>
// CHANGE TO:
{session.meet_link ? (
<a href={session.meet_link} ...>
// 9. Action buttons:
onClick={() => openMeetDialog(firstSlot)}
onClick={() => updateSlotStatus(firstSlot.id, 'completed')}
onClick={() => updateSlotStatus(firstSlot.id, 'cancelled')}
// CHANGE TO:
onClick={() => openMeetDialog(session)}
onClick={() => updateSessionStatus(session.id, 'completed')}
onClick={() => updateSessionStatus(session.id, 'cancelled')}
// 10. Empty state:
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
Tidak ada jadwal mendatang
</TableCell>
// CHANGE TO (same colSpan):
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
Tidak ada jadwal mendatang
</TableCell>
// 11. Mobile card layout - same pattern as desktop:
{upcomingOrders.map((order) => {
const firstSlot = order.slots[0];
// CHANGE TO:
{upcomingSessions.map((session) => {
// Then replace all:
// order.orderId → session.id
// order.slots[0] / firstSlot → session
// order.slots[order.slots.length - 1] / lastSlot → session
// order.profile → session.profiles
// order.meetLink → session.meet_link
// sessionCount → session.total_blocks
// 12. Past sessions tab - same pattern:
{pastOrders.slice(0, 20).map((order) => {
// CHANGE TO:
{pastSessions.slice(0, 20).map((session) => {
// 13. Dialog - selectedSlot references:
{selectedSlot && (
<div className="p-3 bg-muted rounded-lg text-sm space-y-1">
<p><strong>Tanggal:</strong> {format(parseISO(selectedSlot.date), 'd MMMM yyyy', { locale: id })}</p>
<p><strong>Waktu:</strong> {selectedSlot.start_time.substring(0, 5)} - {selectedSlot.end_time.substring(0, 5)}</p>
<p><strong>Klien:</strong> {selectedSlot.profiles?.name}</p>
<p><strong>Topik:</strong> {selectedSlot.topic_category}</p>
{selectedSlot.notes && <p><strong>Catatan:</strong> {selectedSlot.notes}</p>}
</div>
)}
// CHANGE TO:
{selectedSession && (
<div className="p-3 bg-muted rounded-lg text-sm space-y-1">
<p><strong>Tanggal:</strong> {format(parseISO(selectedSession.session_date), 'd MMMM yyyy', { locale: id })}</p>
<p><strong>Waktu:</strong> {selectedSession.start_time.substring(0, 5)} - {selectedSession.end_time.substring(0, 5)}</p>
<p><strong>Klien:</strong> {selectedSession.profiles?.name}</p>
<p><strong>Topik:</strong> {selectedSession.topic_category}</p>
{selectedSession.notes && <p><strong>Catatan:</strong> {selectedSession.notes}</p>}
</div>
)}
```
## 📋 Remaining Files to Update
### 4. src/components/reviews/ConsultingHistory.tsx
**Changes needed:**
- Change query from `consulting_slots` to `consulting_sessions`
- Remove grouping logic (no longer needed)
- Update interface to use `ConsultingSession` with fields:
- `session_date` (instead of `date`)
- `total_duration_minutes`
- `total_blocks`
- `total_price`
- Update all field references in rendering
### 5. src/pages/member/OrderDetail.tsx
**Changes needed:**
- Find consulting_slots query and change to consulting_sessions
- Update join to include session data
- Update field names in rendering (date → session_date, etc.)
### 6. supabase/functions/handle-order-paid/index.ts
**Changes needed:**
- Change status update from `consulting_slots` to `consulting_sessions`
- Update logic to set `status = 'confirmed'` for session
---
## Quick Reference: Field Name Changes
| Old (consulting_slots) | New (consulting_sessions) |
|------------------------|---------------------------|
| `date` | `session_date` |
| `slots` array | Single `session` object |
| `slots[0]` / `firstSlot` | `session` |
| `slots[length-1]` / `lastSlot` | `session` |
| `order_id` (for grouping) | `id` (session ID) |
| `meet_link` (per slot) | `meet_link` (per session) |
| Row count × 45min | `total_duration_minutes` |
| Row count | `total_blocks` |
---
## Testing Checklist
After migration:
- [ ] Test booking flow - creates session + time slots
- [ ] Test availability checking - uses sessions table
- [ ] Test meet link creation - updates session
- [ ] Test admin consulting page - displays sessions
- [ ] Test user consulting history - displays sessions
- [ ] Test order detail - shows consulting session info
- [ ] Test payment confirmation - updates session status
---
## Rollback Plan (if needed)
If issues arise:
1. Restore old table: `ALTER TABLE consulting_slots RENAME TO consulting_slots_backup;`
2. Create view: `CREATE VIEW consulting_slots AS SELECT ... FROM consulting_sessions JOIN consulting_time_slots;`
3. Revert code changes from git
---
**Note:** All SQL tables should already be created. This document covers code changes only.