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:
227
MIGRATION_GUIDE.md
Normal file
227
MIGRATION_GUIDE.md
Normal 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.
|
||||
Reference in New Issue
Block a user