Fix calendar timezone and group consulting slots by order

Calendar Timezone Fix:
- Add +07:00 timezone offset to date strings in create-google-meet-event
- Fixes 13:00 appearing as 20:00 in Google Calendar
- Now treats times as Asia/Jakarta time explicitly

Single Calendar Event per Order:
- handle-order-paid now creates ONE event for all slots in an order
- Uses first slot's start time and last slot's end time
- Updates all slots with the same meet_link
- Prevents duplicate calendar events for multi-slot orders

Admin Consulting Page Improvements:
- Group consulting slots by order_id
- Display as single row with continuous time range (start-end)
- Show session count when multiple slots (e.g., "2 sesi")
- Consistent with member-facing ConsultingHistory component
- Updated both desktop table and mobile card layouts
- Updated both upcoming and past tabs

🤖 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-27 01:34:40 +07:00
parent 3f0acca658
commit 42d6bd98e2
3 changed files with 337 additions and 254 deletions

View File

@@ -36,6 +36,17 @@ interface ConsultingSlot {
}; };
} }
interface GroupedOrder {
orderId: string | null;
slots: ConsultingSlot[];
firstDate: string;
meetLink: string | null;
profile: {
name: string;
email: string;
} | null;
}
interface PlatformSettings { interface PlatformSettings {
integration_n8n_base_url?: string; integration_n8n_base_url?: string;
integration_google_calendar_id?: string; integration_google_calendar_id?: string;
@@ -245,10 +256,31 @@ export default function AdminConsulting() {
); );
} }
// Group slots by order_id
const groupedOrders: GroupedOrder[] = (() => {
const groups = new Map<string | null, ConsultingSlot[]>();
slots.forEach(slot => {
const orderId = slot.order_id || 'no-order';
if (!groups.has(orderId)) {
groups.set(orderId, []);
}
groups.get(orderId)!.push(slot);
});
return Array.from(groups.entries()).map(([orderId, slots]) => ({
orderId: orderId === 'no-order' ? null : orderId,
slots,
firstDate: slots[0].date,
meetLink: slots[0].meet_link,
profile: slots[0].profiles || null,
})).sort((a, b) => new Date(a.firstDate).getTime() - new Date(b.firstDate).getTime());
})();
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];
const upcomingSlots = slots.filter(s => s.date >= today && (s.status === 'confirmed' || s.status === 'pending_payment')); const upcomingOrders = groupedOrders.filter(o => o.firstDate >= today && o.slots.some(s => s.status === 'confirmed' || s.status === 'pending_payment'));
const pastSlots = slots.filter(s => s.date < today || s.status === 'completed' || s.status === 'cancelled'); const pastOrders = groupedOrders.filter(o => o.firstDate < today || o.slots.every(s => s.status === 'completed' || s.status === 'cancelled'));
const todaySlots = slots.filter(s => isToday(parseISO(s.date)) && s.status === 'confirmed'); const todayOrders = groupedOrders.filter(o => isToday(parseISO(o.firstDate)) && o.slots.some(s => s.status === 'confirmed'));
return ( return (
<AppLayout> <AppLayout>
@@ -262,30 +294,34 @@ export default function AdminConsulting() {
</p> </p>
{/* Today's Sessions Alert */} {/* Today's Sessions Alert */}
{todaySlots.length > 0 && ( {todayOrders.length > 0 && (
<Card className="border-2 border-primary bg-primary/5 mb-6"> <Card className="border-2 border-primary bg-primary/5 mb-6">
<CardContent className="py-4"> <CardContent className="py-4">
<h3 className="font-bold flex items-center gap-2"> <h3 className="font-bold flex items-center gap-2">
<Calendar className="w-5 h-5" /> <Calendar className="w-5 h-5" />
Sesi Hari Ini ({todaySlots.length}) Sesi Hari Ini ({todayOrders.length})
</h3> </h3>
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
{todaySlots.map(slot => ( {todayOrders.map((order) => {
<div key={slot.id} className="flex items-center justify-between text-sm"> const firstSlot = order.slots[0];
<span> const lastSlot = order.slots[order.slots.length - 1];
{slot.start_time.substring(0, 5)} - {slot.profiles?.name || 'N/A'} ({slot.topic_category}) return (
</span> <div key={order.orderId || 'no-order'} className="flex items-center justify-between text-sm">
{slot.meet_link ? ( <span>
<a href={slot.meet_link} target="_blank" rel="noopener noreferrer" className="text-primary underline flex items-center gap-1"> {firstSlot.start_time.substring(0, 5)} - {lastSlot.end_time.substring(0, 5)} {order.profile?.name || 'N/A'} ({firstSlot.topic_category})
<ExternalLink className="w-3 h-3" /> Join </span>
</a> {order.meetLink ? (
) : ( <a href={order.meetLink} target="_blank" rel="noopener noreferrer" className="text-primary underline flex items-center gap-1">
<Button size="sm" variant="outline" onClick={() => openMeetDialog(slot)}> <ExternalLink className="w-3 h-3" /> Join
Tambah Link </a>
</Button> ) : (
)} <Button size="sm" variant="outline" onClick={() => openMeetDialog(firstSlot)}>
</div> Tambah Link
))} </Button>
)}
</div>
);
})}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -295,25 +331,25 @@ export default function AdminConsulting() {
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<Card className="border-2 border-border"> <Card className="border-2 border-border">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="text-2xl font-bold">{todaySlots.length}</div> <div className="text-2xl font-bold">{todayOrders.length}</div>
<p className="text-sm text-muted-foreground">Hari Ini</p> <p className="text-sm text-muted-foreground">Hari Ini</p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="border-2 border-border"> <Card className="border-2 border-border">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="text-2xl font-bold">{upcomingSlots.filter(s => s.status === 'confirmed').length}</div> <div className="text-2xl font-bold">{upcomingOrders.filter(o => o.slots.some(s => s.status === 'confirmed')).length}</div>
<p className="text-sm text-muted-foreground">Dikonfirmasi</p> <p className="text-sm text-muted-foreground">Dikonfirmasi</p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="border-2 border-border"> <Card className="border-2 border-border">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="text-2xl font-bold">{upcomingSlots.filter(s => !s.meet_link && s.status === 'confirmed').length}</div> <div className="text-2xl font-bold">{upcomingOrders.filter(o => !o.meetLink && o.slots.some(s => s.status === 'confirmed')).length}</div>
<p className="text-sm text-muted-foreground">Perlu Link Meet</p> <p className="text-sm text-muted-foreground">Perlu Link Meet</p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="border-2 border-border"> <Card className="border-2 border-border">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="text-2xl font-bold">{pastSlots.filter(s => s.status === 'completed').length}</div> <div className="text-2xl font-bold">{pastOrders.filter(o => o.slots.every(s => s.status === 'completed')).length}</div>
<p className="text-sm text-muted-foreground">Selesai</p> <p className="text-sm text-muted-foreground">Selesai</p>
</CardContent> </CardContent>
</Card> </Card>
@@ -322,8 +358,8 @@ export default function AdminConsulting() {
{/* Tabs */} {/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab}> <Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="mb-4"> <TabsList className="mb-4">
<TabsTrigger value="upcoming">Mendatang ({upcomingSlots.length})</TabsTrigger> <TabsTrigger value="upcoming">Mendatang ({upcomingOrders.length})</TabsTrigger>
<TabsTrigger value="past">Riwayat ({pastSlots.length})</TabsTrigger> <TabsTrigger value="past">Riwayat ({pastOrders.length})</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="upcoming"> <TabsContent value="upcoming">
@@ -344,81 +380,91 @@ export default function AdminConsulting() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{upcomingSlots.map((slot) => ( {upcomingOrders.map((order) => {
<TableRow key={slot.id}> const firstSlot = order.slots[0];
<TableCell className="font-medium"> const lastSlot = order.slots[order.slots.length - 1];
<div> const sessionCount = order.slots.length;
{format(parseISO(slot.date), 'd MMM yyyy', { locale: id })} return (
{isToday(parseISO(slot.date)) && <Badge className="ml-2 bg-primary">Hari Ini</Badge>} <TableRow key={order.orderId || 'no-order'}>
{isTomorrow(parseISO(slot.date)) && <Badge className="ml-2 bg-accent">Besok</Badge>} <TableCell className="font-medium">
</div> <div>
</TableCell> {format(parseISO(firstSlot.date), 'd MMM yyyy', { locale: id })}
<TableCell> {isToday(parseISO(firstSlot.date)) && <Badge className="ml-2 bg-primary">Hari Ini</Badge>}
{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)} {isTomorrow(parseISO(firstSlot.date)) && <Badge className="ml-2 bg-accent">Besok</Badge>}
</TableCell> </div>
<TableCell> </TableCell>
<div> <TableCell>
<p className="font-medium">{slot.profiles?.name || '-'}</p> <div>
<p className="text-sm text-muted-foreground">{slot.profiles?.email}</p> <div>{firstSlot.start_time.substring(0, 5)} - {lastSlot.end_time.substring(0, 5)}</div>
</div> {sessionCount > 1 && (
</TableCell> <div className="text-xs text-muted-foreground">{sessionCount} sesi</div>
<TableCell> )}
<Badge variant="outline">{slot.topic_category}</Badge> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge variant={statusLabels[slot.status]?.variant || 'secondary'}> <div>
{statusLabels[slot.status]?.label || slot.status} <p className="font-medium">{order.profile?.name || '-'}</p>
</Badge> <p className="text-sm text-muted-foreground">{order.profile?.email}</p>
</TableCell> </div>
<TableCell> </TableCell>
{slot.meet_link ? ( <TableCell>
<a <Badge variant="outline">{firstSlot.topic_category}</Badge>
href={slot.meet_link} </TableCell>
target="_blank" <TableCell>
rel="noopener noreferrer" <Badge variant={statusLabels[firstSlot.status]?.variant || 'secondary'}>
className="text-primary hover:underline flex items-center gap-1" {statusLabels[firstSlot.status]?.label || firstSlot.status}
> </Badge>
<ExternalLink className="w-4 h-4" /> </TableCell>
Buka <TableCell>
</a> {order.meetLink ? (
) : ( <a
<span className="text-muted-foreground">-</span> href={order.meetLink}
)} target="_blank"
</TableCell> rel="noopener noreferrer"
<TableCell className="text-right space-x-2"> className="text-primary hover:underline flex items-center gap-1"
{slot.status === 'confirmed' && (
<>
<Button
variant="outline"
size="sm"
onClick={() => openMeetDialog(slot)}
className="border-2"
> >
<LinkIcon className="w-4 h-4 mr-1" /> <ExternalLink className="w-4 h-4" />
{slot.meet_link ? 'Edit' : 'Link'} Buka
</Button> </a>
<Button ) : (
variant="outline" <span className="text-muted-foreground">-</span>
size="sm" )}
onClick={() => updateSlotStatus(slot.id, 'completed')} </TableCell>
className="border-2 text-green-600" <TableCell className="text-right space-x-2">
> {firstSlot.status === 'confirmed' && (
<CheckCircle className="w-4 h-4" /> <>
</Button> <Button
<Button variant="outline"
variant="outline" size="sm"
size="sm" onClick={() => openMeetDialog(firstSlot)}
onClick={() => updateSlotStatus(slot.id, 'cancelled')} className="border-2"
className="border-2 text-destructive" >
> <LinkIcon className="w-4 h-4 mr-1" />
<XCircle className="w-4 h-4" /> {order.meetLink ? 'Edit' : 'Link'}
</Button> </Button>
</> <Button
)} variant="outline"
</TableCell> size="sm"
</TableRow> onClick={() => updateSlotStatus(firstSlot.id, 'completed')}
))} className="border-2 text-green-600"
{upcomingSlots.length === 0 && ( >
<CheckCircle className="w-4 h-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => updateSlotStatus(firstSlot.id, 'cancelled')}
className="border-2 text-destructive"
>
<XCircle className="w-4 h-4" />
</Button>
</>
)}
</TableCell>
</TableRow>
);
})}
{upcomingOrders.length === 0 && (
<TableRow> <TableRow>
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground"> <TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
Tidak ada jadwal mendatang Tidak ada jadwal mendatang
@@ -433,90 +479,98 @@ export default function AdminConsulting() {
{/* Mobile Card Layout */} {/* Mobile Card Layout */}
<div className="md:hidden space-y-3"> <div className="md:hidden space-y-3">
{upcomingSlots.map((slot) => ( {upcomingOrders.map((order) => {
<div key={slot.id} className="border-2 border-border rounded-lg p-4 space-y-3 bg-card shadow-sm"> const firstSlot = order.slots[0];
<div> const lastSlot = order.slots[order.slots.length - 1];
<div className="flex items-start justify-between gap-2"> const sessionCount = order.slots.length;
<div className="flex-1 min-w-0"> return (
<div className="flex items-center gap-2 flex-wrap mb-1"> <div key={order.orderId || 'no-order'} className="border-2 border-border rounded-lg p-4 space-y-3 bg-card shadow-sm">
<h3 className="font-semibold text-sm"> <div>
{format(parseISO(slot.date), 'd MMM yyyy', { locale: id })} <div className="flex items-start justify-between gap-2">
</h3> <div className="flex-1 min-w-0">
<Badge variant={statusLabels[slot.status]?.variant || 'secondary'}> <div className="flex items-center gap-2 flex-wrap mb-1">
{statusLabels[slot.status]?.label || slot.status} <h3 className="font-semibold text-sm">
</Badge> {format(parseISO(firstSlot.date), 'd MMM yyyy', { locale: id })}
</div> </h3>
<p className="text-sm text-muted-foreground"> <Badge variant={statusLabels[firstSlot.status]?.variant || 'secondary'}>
{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)} {statusLabels[firstSlot.status]?.label || firstSlot.status}
</p> </Badge>
</div>
<p className="text-sm text-muted-foreground">
{firstSlot.start_time.substring(0, 5)} - {lastSlot.end_time.substring(0, 5)}
{sessionCount > 1 && (
<span className="ml-2 text-xs">({sessionCount} sesi)</span>
)}
</p>
</div>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Klien:</span>
<div className="text-right">
<p className="text-sm font-medium">{order.profile?.name || '-'}</p>
</div> </div>
</div> </div>
<div className="space-y-1"> <div className="flex items-center justify-between">
<div className="flex items-center justify-between"> <span className="text-sm text-muted-foreground">Kategori:</span>
<span className="text-sm text-muted-foreground">Klien:</span> <Badge variant="outline" className="text-xs">{firstSlot.topic_category}</Badge>
<div className="text-right">
<p className="text-sm font-medium">{slot.profiles?.name || '-'}</p>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Kategori:</span>
<Badge variant="outline" className="text-xs">{slot.topic_category}</Badge>
</div>
{slot.meet_link && (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Meet:</span>
<a
href={slot.meet_link}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline text-sm flex items-center gap-1"
>
<ExternalLink className="w-3 h-3" />
Buka
</a>
</div>
)}
</div> </div>
{slot.status === 'confirmed' && ( {order.meetLink && (
<div className="flex gap-2 pt-2 border-t border-border"> <div className="flex items-center justify-between">
<Button <span className="text-sm text-muted-foreground">Meet:</span>
variant="outline" <a
size="sm" href={order.meetLink}
onClick={() => openMeetDialog(slot)} target="_blank"
className="flex-1 border-2 text-xs" rel="noopener noreferrer"
className="text-primary hover:underline text-sm flex items-center gap-1"
> >
<LinkIcon className="w-3 h-3 mr-1" /> <ExternalLink className="w-3 h-3" />
{slot.meet_link ? 'Edit' : 'Link'} Buka
</Button> </a>
<Button
variant="outline"
size="sm"
onClick={() => updateSlotStatus(slot.id, 'completed')}
className="flex-1 border-2 text-green-600 text-xs"
>
<CheckCircle className="w-3 h-3 mr-1" />
Selesai
</Button>
<Button
variant="outline"
size="sm"
onClick={() => updateSlotStatus(slot.id, 'cancelled')}
className="flex-1 border-2 text-destructive text-xs"
>
<XCircle className="w-3 h-3 mr-1" />
Batal
</Button>
</div> </div>
)} )}
</div> </div>
{firstSlot.status === 'confirmed' && (
<div className="flex gap-2 pt-2 border-t border-border">
<Button
variant="outline"
size="sm"
onClick={() => openMeetDialog(firstSlot)}
className="flex-1 border-2 text-xs"
>
<LinkIcon className="w-3 h-3 mr-1" />
{order.meetLink ? 'Edit' : 'Link'}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => updateSlotStatus(firstSlot.id, 'completed')}
className="flex-1 border-2 text-green-600 text-xs"
>
<CheckCircle className="w-3 h-3 mr-1" />
Selesai
</Button>
<Button
variant="outline"
size="sm"
onClick={() => updateSlotStatus(firstSlot.id, 'cancelled')}
className="flex-1 border-2 text-destructive text-xs"
>
<XCircle className="w-3 h-3 mr-1" />
Batal
</Button>
</div>
)}
</div> </div>
))} </div>
{upcomingSlots.length === 0 && ( );
<div className="text-center py-8 text-muted-foreground"> })}
Tidak ada jadwal mendatang {upcomingOrders.length === 0 && (
</div> <div className="text-center py-8 text-muted-foreground">
)} Tidak ada jadwal mendatang
</div> </div>
)}
</div>
</TabsContent> </TabsContent>
<TabsContent value="past"> <TabsContent value="past">
@@ -535,20 +589,32 @@ export default function AdminConsulting() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{pastSlots.slice(0, 20).map((slot) => ( {pastOrders.slice(0, 20).map((order) => {
<TableRow key={slot.id}> const firstSlot = order.slots[0];
<TableCell>{format(parseISO(slot.date), 'd MMM yyyy', { locale: id })}</TableCell> const lastSlot = order.slots[order.slots.length - 1];
<TableCell>{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)}</TableCell> const sessionCount = order.slots.length;
<TableCell>{slot.profiles?.name || '-'}</TableCell> return (
<TableCell><Badge variant="outline">{slot.topic_category}</Badge></TableCell> <TableRow key={order.orderId || 'no-order'}>
<TableCell> <TableCell>{format(parseISO(firstSlot.date), 'd MMM yyyy', { locale: id })}</TableCell>
<Badge variant={statusLabels[slot.status]?.variant || 'secondary'}> <TableCell>
{statusLabels[slot.status]?.label || slot.status} <div>
</Badge> <div>{firstSlot.start_time.substring(0, 5)} - {lastSlot.end_time.substring(0, 5)}</div>
</TableCell> {sessionCount > 1 && (
</TableRow> <div className="text-xs text-muted-foreground">{sessionCount} sesi</div>
))} )}
{pastSlots.length === 0 && ( </div>
</TableCell>
<TableCell>{order.profile?.name || '-'}</TableCell>
<TableCell><Badge variant="outline">{firstSlot.topic_category}</Badge></TableCell>
<TableCell>
<Badge variant={statusLabels[firstSlot.status]?.variant || 'secondary'}>
{statusLabels[firstSlot.status]?.label || firstSlot.status}
</Badge>
</TableCell>
</TableRow>
);
})}
{pastOrders.length === 0 && (
<TableRow> <TableRow>
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground"> <TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
Belum ada riwayat konsultasi Belum ada riwayat konsultasi
@@ -563,40 +629,48 @@ export default function AdminConsulting() {
{/* Mobile Card Layout */} {/* Mobile Card Layout */}
<div className="md:hidden space-y-3"> <div className="md:hidden space-y-3">
{pastSlots.slice(0, 20).map((slot) => ( {pastOrders.slice(0, 20).map((order) => {
<div key={slot.id} className="border-2 border-border rounded-lg p-4 space-y-3 bg-card shadow-sm"> const firstSlot = order.slots[0];
<div> const lastSlot = order.slots[order.slots.length - 1];
<div className="flex items-start justify-between gap-2"> const sessionCount = order.slots.length;
<div className="flex-1 min-w-0"> return (
<h3 className="font-semibold text-sm"> <div key={order.orderId || 'no-order'} className="border-2 border-border rounded-lg p-4 space-y-3 bg-card shadow-sm">
{format(parseISO(slot.date), 'd MMM yyyy', { locale: id })} <div>
</h3> <div className="flex items-start justify-between gap-2">
<p className="text-sm text-muted-foreground"> <div className="flex-1 min-w-0">
{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)} <h3 className="font-semibold text-sm">
</p> {format(parseISO(firstSlot.date), 'd MMM yyyy', { locale: id })}
</div> </h3>
<Badge variant={statusLabels[slot.status]?.variant || 'secondary'}> <p className="text-sm text-muted-foreground">
{statusLabels[slot.status]?.label || slot.status} {firstSlot.start_time.substring(0, 5)} - {lastSlot.end_time.substring(0, 5)}
</Badge> {sessionCount > 1 && (
<span className="ml-2 text-xs">({sessionCount} sesi)</span>
)}
</p>
</div> </div>
<div className="space-y-1"> <Badge variant={statusLabels[firstSlot.status]?.variant || 'secondary'}>
<div className="flex items-center justify-between"> {statusLabels[firstSlot.status]?.label || firstSlot.status}
<span className="text-sm text-muted-foreground">Klien:</span> </Badge>
<span className="text-sm">{slot.profiles?.name || '-'}</span> </div>
</div> <div className="space-y-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Kategori:</span> <span className="text-sm text-muted-foreground">Klien:</span>
<Badge variant="outline" className="text-xs">{slot.topic_category}</Badge> <span className="text-sm">{order.profile?.name || '-'}</span>
</div> </div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Kategori:</span>
<Badge variant="outline" className="text-xs">{firstSlot.topic_category}</Badge>
</div> </div>
</div> </div>
</div> </div>
))} </div>
{pastSlots.length === 0 && ( );
<div className="text-center py-8 text-muted-foreground"> })}
Belum ada riwayat konsultasi {pastOrders.length === 0 && (
</div> <div className="text-center py-8 text-muted-foreground">
)} Belum ada riwayat konsultasi
</div>
)}
</div> </div>
</TabsContent> </TabsContent>
</Tabs> </Tabs>

View File

@@ -202,8 +202,9 @@ serve(async (req: Request): Promise<Response> => {
console.log("Got access token"); console.log("Got access token");
// Build event data // Build event data
const startDate = new Date(`${body.date}T${body.start_time}`); // Include +07:00 timezone offset to ensure times are treated as Asia/Jakarta time
const endDate = new Date(`${body.date}T${body.end_time}`); const startDate = new Date(`${body.date}T${body.start_time}+07:00`);
const endDate = new Date(`${body.date}T${body.end_time}+07:00`);
const eventData = { const eventData = {
summary: `Konsultasi: ${body.topic} - ${body.client_name}`, summary: `Konsultasi: ${body.topic} - ${body.client_name}`,

View File

@@ -113,42 +113,50 @@ serve(async (req: Request): Promise<Response> => {
} }
if (consultingSlots && consultingSlots.length > 0) { if (consultingSlots && consultingSlots.length > 0) {
for (const slot of consultingSlots) { try {
try { console.log("[HANDLE-PAID] Creating Google Meet for order:", order_id);
console.log("[HANDLE-PAID] Creating Google Meet for slot:", slot.id);
const topic = "Konsultasi 1-on-1"; // Group slots by order - use first slot's start time and last slot's end time
const firstSlot = consultingSlots[0];
const lastSlot = consultingSlots[consultingSlots.length - 1];
const topic = "Konsultasi 1-on-1";
const meetResponse = await fetch( const meetResponse = await fetch(
`${supabaseUrl}/functions/v1/create-google-meet-event`, `${supabaseUrl}/functions/v1/create-google-meet-event`,
{ {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `Bearer ${Deno.env.get("SUPABASE_ANON_KEY")}`, "Authorization": `Bearer ${Deno.env.get("SUPABASE_ANON_KEY")}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
slot_id: slot.id, slot_id: firstSlot.id, // Use first slot ID
date: slot.date, date: firstSlot.date,
start_time: slot.start_time, start_time: firstSlot.start_time,
end_time: slot.end_time, end_time: lastSlot.end_time, // Use last slot's end time for continuous block
client_name: userName, client_name: userName,
client_email: userEmail, client_email: userEmail,
topic: topic, topic: topic,
}), notes: `${consultingSlots.length} sesi: ${consultingSlots.map(s => s.start_time.substring(0, 5)).join(', ')}`,
} }),
); }
);
if (meetResponse.ok) {
const meetData = await meetResponse.json(); if (meetResponse.ok) {
if (meetData.success) { const meetData = await meetResponse.json();
console.log("[HANDLE-PAID] Meet created:", meetData.meet_link); if (meetData.success) {
} console.log("[HANDLE-PAID] Meet created:", meetData.meet_link);
// Update all slots with the same meet link
await supabase
.from("consulting_slots")
.update({ meet_link: meetData.meet_link })
.eq("order_id", order_id);
} }
} catch (error) {
console.error("[HANDLE-PAID] Meet creation failed:", error);
// Don't fail the entire process
} }
} catch (error) {
console.error("[HANDLE-PAID] Meet creation failed:", error);
// Don't fail the entire process
} }
} }