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 && (
-
- )}
+
+ 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
}
}