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">
|
||||
<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>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { formatIDR } from '@/lib/format';
|
||||
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';
|
||||
|
||||
interface ConsultingSession {
|
||||
@@ -64,9 +64,12 @@ export default function AdminConsulting() {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [creatingMeet, setCreatingMeet] = useState(false);
|
||||
const [deletingMeet, setDeletingMeet] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('upcoming');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filterStatus, setFilterStatus] = useState<string>('all');
|
||||
const [editStartTime, setEditStartTime] = useState('');
|
||||
const [editEndTime, setEditEndTime] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading) {
|
||||
@@ -112,45 +115,78 @@ export default function AdminConsulting() {
|
||||
const openMeetDialog = (session: ConsultingSession) => {
|
||||
setSelectedSession(session);
|
||||
setMeetLink(session.meet_link || '');
|
||||
setEditStartTime(session.start_time.substring(0, 5));
|
||||
setEditEndTime(session.end_time.substring(0, 5));
|
||||
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 () => {
|
||||
if (!selectedSession) return;
|
||||
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
|
||||
.from('consulting_sessions')
|
||||
.update({ meet_link: meetLink })
|
||||
.update(updateData)
|
||||
.eq('id', selectedSession.id);
|
||||
|
||||
if (error) {
|
||||
toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
||||
} else {
|
||||
toast({ title: 'Berhasil', description: 'Link Google Meet disimpan' });
|
||||
toast({ title: 'Berhasil', description: 'Perubahan disimpan' });
|
||||
setDialogOpen(false);
|
||||
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);
|
||||
};
|
||||
@@ -757,50 +793,113 @@ export default function AdminConsulting() {
|
||||
}}>
|
||||
<DialogContent className="max-w-md border-2 border-border">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Link Google Meet</DialogTitle>
|
||||
<DialogTitle>Edit Sesi Konsultasi</DialogTitle>
|
||||
<DialogDescription>
|
||||
Tambahkan atau edit link Google Meet untuk sesi ini
|
||||
Edit waktu, link Google Meet, dan kelola event kalender
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
{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>}
|
||||
{selectedSession.calendar_event_id && (
|
||||
<p className="text-xs text-green-600"><strong>✓ Event Kalender:</strong> Terhubung</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Time Editing */}
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
value={meetLink}
|
||||
onChange={(e) => setMeetLink(e.target.value)}
|
||||
placeholder="https://meet.google.com/xxx-xxxx-xxx"
|
||||
className="border-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>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={createMeetLink}
|
||||
variant="outline"
|
||||
className="flex-1 border-2"
|
||||
disabled={creatingMeet}
|
||||
>
|
||||
{creatingMeet ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Membuat...
|
||||
</>
|
||||
) : (
|
||||
'Buat Link Meet'
|
||||
)}
|
||||
</Button>
|
||||
<Button onClick={saveMeetLink} disabled={saving} className="flex-1 shadow-sm">
|
||||
{saving ? 'Menyimpan...' : 'Simpan'}
|
||||
</Button>
|
||||
{/* Meet Link */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Link Google Meet</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={meetLink}
|
||||
onChange={(e) => setMeetLink(e.target.value)}
|
||||
placeholder="https://meet.google.com/xxx-xxxx-xxx"
|
||||
className="border-2 flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="space-y-2">
|
||||
{/* Create/Regenerate Meet Link */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={createMeetLink}
|
||||
variant="outline"
|
||||
className="flex-1 border-2"
|
||||
disabled={creatingMeet}
|
||||
>
|
||||
{creatingMeet ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Membuat...
|
||||
</>
|
||||
) : meetLink ? (
|
||||
'Regenerate Link'
|
||||
) : (
|
||||
'Buat Link Meet'
|
||||
)}
|
||||
</Button>
|
||||
<Button onClick={saveMeetLink} disabled={saving} className="flex-1 shadow-sm">
|
||||
{saving ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||
</Button>
|
||||
</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 && (
|
||||
|
||||
Reference in New Issue
Block a user