From 42d6bd98e2b806f7e8a73eba76dd46a35d8a4dc6 Mon Sep 17 00:00:00 2001 From: dwindown Date: Sat, 27 Dec 2025 01:34:40 +0700 Subject: [PATCH] Fix calendar timezone and group consulting slots by order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/pages/admin/AdminConsulting.tsx | 514 ++++++++++-------- .../create-google-meet-event/index.ts | 5 +- supabase/functions/handle-order-paid/index.ts | 72 +-- 3 files changed, 337 insertions(+), 254 deletions(-) diff --git a/src/pages/admin/AdminConsulting.tsx b/src/pages/admin/AdminConsulting.tsx index 01d8216..cdb6e4e 100644 --- a/src/pages/admin/AdminConsulting.tsx +++ b/src/pages/admin/AdminConsulting.tsx @@ -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 { integration_n8n_base_url?: 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(); + + 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 upcomingSlots = slots.filter(s => s.date >= today && (s.status === 'confirmed' || s.status === 'pending_payment')); - const pastSlots = slots.filter(s => s.date < today || s.status === 'completed' || s.status === 'cancelled'); - const todaySlots = slots.filter(s => isToday(parseISO(s.date)) && s.status === 'confirmed'); + const upcomingOrders = groupedOrders.filter(o => o.firstDate >= today && o.slots.some(s => s.status === 'confirmed' || s.status === 'pending_payment')); + const pastOrders = groupedOrders.filter(o => o.firstDate < today || o.slots.every(s => s.status === 'completed' || s.status === 'cancelled')); + const todayOrders = groupedOrders.filter(o => isToday(parseISO(o.firstDate)) && o.slots.some(s => s.status === 'confirmed')); return ( @@ -262,30 +294,34 @@ export default function AdminConsulting() {

{/* Today's Sessions Alert */} - {todaySlots.length > 0 && ( + {todayOrders.length > 0 && (

- Sesi Hari Ini ({todaySlots.length}) + Sesi Hari Ini ({todayOrders.length})

- {todaySlots.map(slot => ( -
- - {slot.start_time.substring(0, 5)} - {slot.profiles?.name || 'N/A'} ({slot.topic_category}) - - {slot.meet_link ? ( - - Join - - ) : ( - - )} -
- ))} + {todayOrders.map((order) => { + const firstSlot = order.slots[0]; + const lastSlot = order.slots[order.slots.length - 1]; + return ( +
+ + {firstSlot.start_time.substring(0, 5)} - {lastSlot.end_time.substring(0, 5)} • {order.profile?.name || 'N/A'} ({firstSlot.topic_category}) + + {order.meetLink ? ( + + Join + + ) : ( + + )} +
+ ); + })}
@@ -295,25 +331,25 @@ export default function AdminConsulting() {
-
{todaySlots.length}
+
{todayOrders.length}

Hari Ini

-
{upcomingSlots.filter(s => s.status === 'confirmed').length}
+
{upcomingOrders.filter(o => o.slots.some(s => s.status === 'confirmed')).length}

Dikonfirmasi

-
{upcomingSlots.filter(s => !s.meet_link && s.status === 'confirmed').length}
+
{upcomingOrders.filter(o => !o.meetLink && o.slots.some(s => s.status === 'confirmed')).length}

Perlu Link Meet

-
{pastSlots.filter(s => s.status === 'completed').length}
+
{pastOrders.filter(o => o.slots.every(s => s.status === 'completed')).length}

Selesai

@@ -322,8 +358,8 @@ export default function AdminConsulting() { {/* Tabs */} - Mendatang ({upcomingSlots.length}) - Riwayat ({pastSlots.length}) + Mendatang ({upcomingOrders.length}) + Riwayat ({pastOrders.length}) @@ -344,81 +380,91 @@ export default function AdminConsulting() { - {upcomingSlots.map((slot) => ( - - -
- {format(parseISO(slot.date), 'd MMM yyyy', { locale: id })} - {isToday(parseISO(slot.date)) && Hari Ini} - {isTomorrow(parseISO(slot.date)) && Besok} -
-
- - {slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)} - - -
-

{slot.profiles?.name || '-'}

-

{slot.profiles?.email}

-
-
- - {slot.topic_category} - - - - {statusLabels[slot.status]?.label || slot.status} - - - - {slot.meet_link ? ( - - - Buka - - ) : ( - - - )} - - - {slot.status === 'confirmed' && ( - <> - - - - - )} - -
- ))} - {upcomingSlots.length === 0 && ( + + Buka + + ) : ( + - + )} + + + {firstSlot.status === 'confirmed' && ( + <> + + + + + )} + + + ); + })} + {upcomingOrders.length === 0 && ( Tidak ada jadwal mendatang @@ -433,90 +479,98 @@ export default function AdminConsulting() { {/* Mobile Card Layout */}
- {upcomingSlots.map((slot) => ( -
-
-
-
-
-

- {format(parseISO(slot.date), 'd MMM yyyy', { locale: id })} -

- - {statusLabels[slot.status]?.label || slot.status} - -
-

- {slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)} -

+ {upcomingOrders.map((order) => { + const firstSlot = order.slots[0]; + const lastSlot = order.slots[order.slots.length - 1]; + const sessionCount = order.slots.length; + return ( +
+
+
+
+
+

+ {format(parseISO(firstSlot.date), 'd MMM yyyy', { locale: id })} +

+ + {statusLabels[firstSlot.status]?.label || firstSlot.status} + +
+

+ {firstSlot.start_time.substring(0, 5)} - {lastSlot.end_time.substring(0, 5)} + {sessionCount > 1 && ( + ({sessionCount} sesi) + )} +

+
+
+
+
+ Klien: +
+

{order.profile?.name || '-'}

-
-
- Klien: -
-

{slot.profiles?.name || '-'}

-
-
-
- Kategori: - {slot.topic_category} -
- {slot.meet_link && ( -
- Meet: - - - Buka - -
- )} +
+ Kategori: + {firstSlot.topic_category}
- {slot.status === 'confirmed' && ( - )}
+ {firstSlot.status === 'confirmed' && ( +
+ + + +
+ )}
- ))} - {upcomingSlots.length === 0 && ( -
- Tidak ada jadwal mendatang -
- )} +
+ ); + })} + {upcomingOrders.length === 0 && ( +
+ Tidak ada jadwal mendatang
+ )} +
@@ -535,20 +589,32 @@ export default function AdminConsulting() { - {pastSlots.slice(0, 20).map((slot) => ( - - {format(parseISO(slot.date), 'd MMM yyyy', { locale: id })} - {slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)} - {slot.profiles?.name || '-'} - {slot.topic_category} - - - {statusLabels[slot.status]?.label || slot.status} - - - - ))} - {pastSlots.length === 0 && ( + {pastOrders.slice(0, 20).map((order) => { + const firstSlot = order.slots[0]; + const lastSlot = order.slots[order.slots.length - 1]; + const sessionCount = order.slots.length; + return ( + + {format(parseISO(firstSlot.date), 'd MMM yyyy', { locale: id })} + +
+
{firstSlot.start_time.substring(0, 5)} - {lastSlot.end_time.substring(0, 5)}
+ {sessionCount > 1 && ( +
{sessionCount} sesi
+ )} +
+
+ {order.profile?.name || '-'} + {firstSlot.topic_category} + + + {statusLabels[firstSlot.status]?.label || firstSlot.status} + + +
+ ); + })} + {pastOrders.length === 0 && ( Belum ada riwayat konsultasi @@ -563,40 +629,48 @@ export default function AdminConsulting() { {/* Mobile Card Layout */}
- {pastSlots.slice(0, 20).map((slot) => ( -
-
-
-
-

- {format(parseISO(slot.date), 'd MMM yyyy', { locale: id })} -

-

- {slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)} -

-
- - {statusLabels[slot.status]?.label || slot.status} - + {pastOrders.slice(0, 20).map((order) => { + const firstSlot = order.slots[0]; + const lastSlot = order.slots[order.slots.length - 1]; + const sessionCount = order.slots.length; + return ( +
+
+
+
+

+ {format(parseISO(firstSlot.date), 'd MMM yyyy', { locale: id })} +

+

+ {firstSlot.start_time.substring(0, 5)} - {lastSlot.end_time.substring(0, 5)} + {sessionCount > 1 && ( + ({sessionCount} sesi) + )} +

-
-
- Klien: - {slot.profiles?.name || '-'} -
-
- Kategori: - {slot.topic_category} -
+ + {statusLabels[firstSlot.status]?.label || firstSlot.status} + +
+
+
+ Klien: + {order.profile?.name || '-'} +
+
+ Kategori: + {firstSlot.topic_category}
- ))} - {pastSlots.length === 0 && ( -
- Belum ada riwayat konsultasi -
- )} +
+ ); + })} + {pastOrders.length === 0 && ( +
+ Belum ada riwayat konsultasi +
+ )}
diff --git a/supabase/functions/create-google-meet-event/index.ts b/supabase/functions/create-google-meet-event/index.ts index ebc12a9..85a7757 100644 --- a/supabase/functions/create-google-meet-event/index.ts +++ b/supabase/functions/create-google-meet-event/index.ts @@ -202,8 +202,9 @@ serve(async (req: Request): Promise => { console.log("Got access token"); // Build event data - const startDate = new Date(`${body.date}T${body.start_time}`); - const endDate = new Date(`${body.date}T${body.end_time}`); + // Include +07:00 timezone offset to ensure times are treated as Asia/Jakarta 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 = { summary: `Konsultasi: ${body.topic} - ${body.client_name}`, diff --git a/supabase/functions/handle-order-paid/index.ts b/supabase/functions/handle-order-paid/index.ts index 0044588..16a89f4 100644 --- a/supabase/functions/handle-order-paid/index.ts +++ b/supabase/functions/handle-order-paid/index.ts @@ -113,42 +113,50 @@ serve(async (req: Request): Promise => { } if (consultingSlots && consultingSlots.length > 0) { - for (const slot of consultingSlots) { - try { - console.log("[HANDLE-PAID] Creating Google Meet for slot:", slot.id); + try { + console.log("[HANDLE-PAID] Creating Google Meet for order:", order_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( - `${supabaseUrl}/functions/v1/create-google-meet-event`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${Deno.env.get("SUPABASE_ANON_KEY")}`, - }, - body: JSON.stringify({ - slot_id: slot.id, - date: slot.date, - start_time: slot.start_time, - end_time: slot.end_time, - client_name: userName, - client_email: userEmail, - topic: topic, - }), - } - ); - - if (meetResponse.ok) { - const meetData = await meetResponse.json(); - if (meetData.success) { - console.log("[HANDLE-PAID] Meet created:", meetData.meet_link); - } + const meetResponse = await fetch( + `${supabaseUrl}/functions/v1/create-google-meet-event`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${Deno.env.get("SUPABASE_ANON_KEY")}`, + }, + body: JSON.stringify({ + slot_id: firstSlot.id, // Use first slot ID + date: firstSlot.date, + start_time: firstSlot.start_time, + end_time: lastSlot.end_time, // Use last slot's end time for continuous block + client_name: userName, + client_email: userEmail, + 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 (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 } }