Fix booking summary end time and enhance calendar event management
**Issue 1: Fix end time display in booking summary**
- Now correctly shows start_time + slot_duration instead of just start_time
- Example: 09:30 → 10:00 for 1 slot (30 mins)
**Issue 2: Confirm create-google-meet-event uses consulting_sessions**
- Verified: Function already updates consulting_sessions table
- The data shown is from OLD consulting_slots table (needs migration)
**Issue 3: Delete calendar events when order is deleted**
- Enhanced delete-order function to delete calendar events before removing order
- Calls delete-calendar-event for each session with calendar_event_id
**Issue 4: Admin can now edit session time and manage calendar events**
- Added time editing inputs (start/end time) in admin dialog
- Added "Delete Link & Calendar Event" button to remove meet link
- Shows calendar event connection status (✓ Event Kalender: Terhubung)
- "Regenerate Link" button creates new meet link + calendar event
- Recalculates session duration when time changes
**Issue 5: Enhanced calendar event description**
- Now includes: Kategori, Client email, Catatan, Session ID
- Format: "Kategori: {topic}\n\nClient: {email}\n\nCatatan: {notes}\n\nSession ID: {id}"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -650,7 +650,13 @@ export default function ConsultingBooking() {
|
|||||||
|
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-xs text-muted-foreground">Selesai</p>
|
<p className="text-xs text-muted-foreground">Selesai</p>
|
||||||
<p className="font-bold text-lg">{selectedRange.end}</p>
|
<p className="font-bold text-lg">
|
||||||
|
{(() => {
|
||||||
|
const start = parse(selectedRange.end, 'HH:mm', new Date());
|
||||||
|
const end = addMinutes(start, settings?.consulting_block_duration_minutes || 30);
|
||||||
|
return format(end, 'HH:mm');
|
||||||
|
})()}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|||||||
import { toast } from '@/hooks/use-toast';
|
import { toast } from '@/hooks/use-toast';
|
||||||
import { formatIDR } from '@/lib/format';
|
import { formatIDR } from '@/lib/format';
|
||||||
import { Video, Calendar, Clock, User, Link as LinkIcon, ExternalLink, CheckCircle, XCircle, Loader2, Search, X } from 'lucide-react';
|
import { Video, Calendar, Clock, User, Link as LinkIcon, ExternalLink, CheckCircle, XCircle, Loader2, Search, X } from 'lucide-react';
|
||||||
import { format, parseISO, isToday, isTomorrow, isPast } from 'date-fns';
|
import { format, parseISO, isToday, isTomorrow, isPast, parse, differenceInMinutes } from 'date-fns';
|
||||||
import { id } from 'date-fns/locale';
|
import { id } from 'date-fns/locale';
|
||||||
|
|
||||||
interface ConsultingSession {
|
interface ConsultingSession {
|
||||||
@@ -64,9 +64,12 @@ export default function AdminConsulting() {
|
|||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [creatingMeet, setCreatingMeet] = useState(false);
|
const [creatingMeet, setCreatingMeet] = useState(false);
|
||||||
|
const [deletingMeet, setDeletingMeet] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState('upcoming');
|
const [activeTab, setActiveTab] = useState('upcoming');
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [filterStatus, setFilterStatus] = useState<string>('all');
|
const [filterStatus, setFilterStatus] = useState<string>('all');
|
||||||
|
const [editStartTime, setEditStartTime] = useState('');
|
||||||
|
const [editEndTime, setEditEndTime] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authLoading) {
|
if (!authLoading) {
|
||||||
@@ -112,45 +115,78 @@ export default function AdminConsulting() {
|
|||||||
const openMeetDialog = (session: ConsultingSession) => {
|
const openMeetDialog = (session: ConsultingSession) => {
|
||||||
setSelectedSession(session);
|
setSelectedSession(session);
|
||||||
setMeetLink(session.meet_link || '');
|
setMeetLink(session.meet_link || '');
|
||||||
|
setEditStartTime(session.start_time.substring(0, 5));
|
||||||
|
setEditEndTime(session.end_time.substring(0, 5));
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteMeetLink = async () => {
|
||||||
|
if (!selectedSession) return;
|
||||||
|
|
||||||
|
// Delete calendar event if exists
|
||||||
|
if (selectedSession.calendar_event_id) {
|
||||||
|
setDeletingMeet(true);
|
||||||
|
try {
|
||||||
|
await supabase.functions.invoke('delete-calendar-event', {
|
||||||
|
body: { session_id: selectedSession.id }
|
||||||
|
});
|
||||||
|
toast({ title: 'Berhasil', description: 'Event kalender dihapus' });
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Failed to delete calendar event:', err);
|
||||||
|
toast({ title: 'Warning', description: 'Gagal menghapus event kalender', variant: 'destructive' });
|
||||||
|
}
|
||||||
|
setDeletingMeet(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear meet link from database
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('consulting_sessions')
|
||||||
|
.update({ meet_link: null })
|
||||||
|
.eq('id', selectedSession.id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
||||||
|
} else {
|
||||||
|
toast({ title: 'Berhasil', description: 'Link Google Meet dihapus' });
|
||||||
|
setMeetLink('');
|
||||||
|
fetchSessions();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const saveMeetLink = async () => {
|
const saveMeetLink = async () => {
|
||||||
if (!selectedSession) return;
|
if (!selectedSession) return;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
|
||||||
|
// Prepare update data
|
||||||
|
const updateData: any = {
|
||||||
|
meet_link: meetLink || null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if time changed
|
||||||
|
const newStartTime = editStartTime + ':00';
|
||||||
|
const newEndTime = editEndTime + ':00';
|
||||||
|
if (newStartTime !== selectedSession.start_time || newEndTime !== selectedSession.end_time) {
|
||||||
|
updateData.start_time = newStartTime;
|
||||||
|
updateData.end_time = newEndTime;
|
||||||
|
|
||||||
|
// Recalculate duration
|
||||||
|
const start = parse(newStartTime, 'HH:mm', new Date());
|
||||||
|
const end = parse(newEndTime, 'HH:mm', new Date());
|
||||||
|
const durationMinutes = differenceInMinutes(end, start);
|
||||||
|
updateData.total_duration_minutes = durationMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('consulting_sessions')
|
.from('consulting_sessions')
|
||||||
.update({ meet_link: meetLink })
|
.update(updateData)
|
||||||
.eq('id', selectedSession.id);
|
.eq('id', selectedSession.id);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
||||||
} else {
|
} else {
|
||||||
toast({ title: 'Berhasil', description: 'Link Google Meet disimpan' });
|
toast({ title: 'Berhasil', description: 'Perubahan disimpan' });
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
fetchSessions();
|
fetchSessions();
|
||||||
|
|
||||||
// Send notification to client with meet link
|
|
||||||
if (meetLink && selectedSession.profiles?.email) {
|
|
||||||
try {
|
|
||||||
await supabase.functions.invoke('send-notification', {
|
|
||||||
body: {
|
|
||||||
template_key: 'consulting_scheduled',
|
|
||||||
recipient_email: selectedSession.profiles.email,
|
|
||||||
recipient_name: selectedSession.profiles.name,
|
|
||||||
variables: {
|
|
||||||
consultation_date: format(parseISO(selectedSession.session_date), 'd MMMM yyyy', { locale: id }),
|
|
||||||
consultation_time: `${selectedSession.start_time.substring(0, 5)} - ${selectedSession.end_time.substring(0, 5)}`,
|
|
||||||
meet_link: meetLink,
|
|
||||||
topic_category: selectedSession.topic_category,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.log('Notification skipped:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
};
|
};
|
||||||
@@ -757,31 +793,69 @@ export default function AdminConsulting() {
|
|||||||
}}>
|
}}>
|
||||||
<DialogContent className="max-w-md border-2 border-border">
|
<DialogContent className="max-w-md border-2 border-border">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Link Google Meet</DialogTitle>
|
<DialogTitle>Edit Sesi Konsultasi</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Tambahkan atau edit link Google Meet untuk sesi ini
|
Edit waktu, link Google Meet, dan kelola event kalender
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
{selectedSession && (
|
{selectedSession && (
|
||||||
<div className="p-3 bg-muted rounded-lg text-sm space-y-1">
|
<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>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>Klien:</strong> {selectedSession.profiles?.name}</p>
|
||||||
<p><strong>Topik:</strong> {selectedSession.topic_category}</p>
|
<p><strong>Topik:</strong> {selectedSession.topic_category}</p>
|
||||||
{selectedSession.notes && <p><strong>Catatan:</strong> {selectedSession.notes}</p>}
|
{selectedSession.notes && <p><strong>Catatan:</strong> {selectedSession.notes}</p>}
|
||||||
|
{selectedSession.calendar_event_id && (
|
||||||
|
<p className="text-xs text-green-600"><strong>✓ Event Kalender:</strong> Terhubung</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Time Editing */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Waktu Sesi</label>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
type="time"
|
||||||
|
value={editStartTime}
|
||||||
|
onChange={(e) => setEditStartTime(e.target.value)}
|
||||||
|
className="border-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-muted-foreground">→</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
type="time"
|
||||||
|
value={editEndTime}
|
||||||
|
onChange={(e) => setEditEndTime(e.target.value)}
|
||||||
|
className="border-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Durasi: {editStartTime && editEndTime ?
|
||||||
|
`${differenceInMinutes(parse(editEndTime, 'HH:mm', new Date()), parse(editStartTime, 'HH:mm', new Date()))} menit` :
|
||||||
|
'-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Meet Link */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Link Google Meet</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
value={meetLink}
|
value={meetLink}
|
||||||
onChange={(e) => setMeetLink(e.target.value)}
|
onChange={(e) => setMeetLink(e.target.value)}
|
||||||
placeholder="https://meet.google.com/xxx-xxxx-xxx"
|
placeholder="https://meet.google.com/xxx-xxxx-xxx"
|
||||||
className="border-2"
|
className="border-2 flex-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{/* Create/Regenerate Meet Link */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={createMeetLink}
|
onClick={createMeetLink}
|
||||||
@@ -794,15 +868,40 @@ export default function AdminConsulting() {
|
|||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
Membuat...
|
Membuat...
|
||||||
</>
|
</>
|
||||||
|
) : meetLink ? (
|
||||||
|
'Regenerate Link'
|
||||||
) : (
|
) : (
|
||||||
'Buat Link Meet'
|
'Buat Link Meet'
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={saveMeetLink} disabled={saving} className="flex-1 shadow-sm">
|
<Button onClick={saveMeetLink} disabled={saving} className="flex-1 shadow-sm">
|
||||||
{saving ? 'Menyimpan...' : 'Simpan'}
|
{saving ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Delete Meet Link (only shown if link exists) */}
|
||||||
|
{meetLink && (
|
||||||
|
<Button
|
||||||
|
onClick={deleteMeetLink}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full border-2 text-destructive hover:text-destructive"
|
||||||
|
disabled={deletingMeet}
|
||||||
|
>
|
||||||
|
{deletingMeet ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Menghapus...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<XCircle className="w-4 h-4 mr-2" />
|
||||||
|
Hapus Link & Event Kalender
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{!settings.integration_n8n_base_url && (
|
{!settings.integration_n8n_base_url && (
|
||||||
<p className="text-xs text-muted-foreground text-center">
|
<p className="text-xs text-muted-foreground text-center">
|
||||||
Tip: Konfigurasi webhook di Pengaturan → Integrasi untuk pembuatan otomatis
|
Tip: Konfigurasi webhook di Pengaturan → Integrasi untuk pembuatan otomatis
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ serve(async (req: Request): Promise<Response> => {
|
|||||||
|
|
||||||
const eventData = {
|
const eventData = {
|
||||||
summary: `Konsultasi: ${body.topic} - ${body.client_name}`,
|
summary: `Konsultasi: ${body.topic} - ${body.client_name}`,
|
||||||
description: `Client: ${body.client_email}\n\nNotes: ${body.notes || '-'}\n\nSlot ID: ${body.slot_id}`,
|
description: `Kategori: ${body.topic}\n\nClient: ${body.client_email}\n\nCatatan: ${body.notes || '-'}\n\nSession ID: ${body.slot_id}`,
|
||||||
start: {
|
start: {
|
||||||
dateTime: startDate.toISOString(),
|
dateTime: startDate.toISOString(),
|
||||||
timeZone: "Asia/Jakarta",
|
timeZone: "Asia/Jakarta",
|
||||||
|
|||||||
@@ -32,6 +32,31 @@ serve(async (req: Request): Promise<Response> => {
|
|||||||
const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
||||||
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
||||||
|
|
||||||
|
// Get consulting sessions for this order to delete calendar events
|
||||||
|
const { data: sessions } = await supabase
|
||||||
|
.from("consulting_sessions")
|
||||||
|
.select("id, calendar_event_id")
|
||||||
|
.eq("order_id", order_id);
|
||||||
|
|
||||||
|
if (sessions && sessions.length > 0) {
|
||||||
|
console.log("[DELETE-ORDER] Found consulting sessions:", sessions.length);
|
||||||
|
|
||||||
|
// Delete calendar events for each session
|
||||||
|
for (const session of sessions) {
|
||||||
|
if (session.calendar_event_id) {
|
||||||
|
try {
|
||||||
|
await supabase.functions.invoke('delete-calendar-event', {
|
||||||
|
body: { session_id: session.id }
|
||||||
|
});
|
||||||
|
console.log("[DELETE-ORDER] Deleted calendar event for session:", session.id);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[DELETE-ORDER] Failed to delete calendar event:", err);
|
||||||
|
// Continue with order deletion even if calendar deletion fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Call the database function to delete the order
|
// Call the database function to delete the order
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.rpc("delete_order", { order_uuid: order_id });
|
.rpc("delete_order", { order_uuid: order_id });
|
||||||
|
|||||||
Reference in New Issue
Block a user