Add reschedule functionality for consulting sessions
Problem: Admins need to reschedule sessions when members can't make it, either before or after the scheduled time. Previously had to edit manually without clear indication of rescheduling vs simple edits. Solution: 1. Add "Reschedule" button (blue Calendar icon) for confirmed sessions: - In desktop table action buttons - In mobile card layout - In passed sessions alert card 2. Enhanced session editing with reschedule mode: - openMeetDialog(session, rescheduleMode = true/false) - Tracks isRescheduling state to show appropriate UI - Dynamic dialog title: "Reschedule Sesi" vs "Edit Sesi" - Dynamic description based on mode 3. Enhanced saveMeetLink function: - Detects date and time changes separately - Updates session_date when date changed - Recalculates duration when time changes - Updates consulting_time_slots for new schedule - Updates calendar event if exists - Shows success message: "Berhasil Reschedule" with new date/time 4. Session info display improvements: - Show current time in session info card - Better context for rescheduling decisions Reschedule use cases: - Member can't make it BEFORE session → Admin clicks Reschedule, picks new slot - Member misses session, tells admin AFTER → Admin clicks Reschedule in passed alert - Emergency reschedule → Quick date/time change with calendar auto-update Calendar integration: - Existing calendar events automatically updated/moved to new time - Time slots properly released (old) and booked (new) UI placement: - Passed sessions alert: First button (blue) for quick reschedule access - Upcoming table: Between Edit and Complete buttons - Mobile: Between Link and Complete buttons 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -71,9 +71,12 @@ export default function AdminConsulting() {
|
||||
const [filterStatus, setFilterStatus] = useState<string>('all');
|
||||
const [editStartTime, setEditStartTime] = useState('');
|
||||
const [editEndTime, setEditEndTime] = useState('');
|
||||
const [editSessionDate, setEditSessionDate] = useState('');
|
||||
const [timeSlotPickerOpen, setTimeSlotPickerOpen] = useState(false);
|
||||
const [editTotalBlocks, setEditTotalBlocks] = useState(0);
|
||||
const [editTotalDuration, setEditTotalDuration] = useState(0);
|
||||
const [isRescheduling, setIsRescheduling] = useState(false);
|
||||
const [notifyMember, setNotifyMember] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading) {
|
||||
@@ -116,13 +119,16 @@ export default function AdminConsulting() {
|
||||
if (data) setSettings(data);
|
||||
};
|
||||
|
||||
const openMeetDialog = (session: ConsultingSession) => {
|
||||
const openMeetDialog = (session: ConsultingSession, rescheduleMode: boolean = false) => {
|
||||
setSelectedSession(session);
|
||||
setMeetLink(session.meet_link || '');
|
||||
setEditStartTime(session.start_time.substring(0, 5));
|
||||
setEditEndTime(session.end_time.substring(0, 5));
|
||||
setEditSessionDate(session.session_date);
|
||||
setEditTotalBlocks(session.total_blocks || 1);
|
||||
setEditTotalDuration(session.total_duration_minutes || 30);
|
||||
setIsRescheduling(rescheduleMode);
|
||||
setNotifyMember(rescheduleMode); // Auto-enable notification when rescheduling
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
@@ -177,14 +183,17 @@ export default function AdminConsulting() {
|
||||
meet_link: meetLink || null
|
||||
};
|
||||
|
||||
// Check if time changed
|
||||
// Check if date or time changed
|
||||
const newStartTime = editStartTime + ':00';
|
||||
const newEndTime = editEndTime + ':00';
|
||||
const dateChanged = editSessionDate !== selectedSession.session_date;
|
||||
const timeChanged = newStartTime !== selectedSession.start_time || newEndTime !== selectedSession.end_time;
|
||||
const scheduleChanged = dateChanged || timeChanged;
|
||||
|
||||
if (timeChanged) {
|
||||
if (scheduleChanged) {
|
||||
updateData.start_time = newStartTime;
|
||||
updateData.end_time = newEndTime;
|
||||
updateData.session_date = editSessionDate;
|
||||
|
||||
// Recalculate duration
|
||||
const start = parse(newStartTime, 'HH:mm', new Date());
|
||||
@@ -209,7 +218,7 @@ export default function AdminConsulting() {
|
||||
|
||||
newSlots.push({
|
||||
session_id: selectedSession.id,
|
||||
slot_date: selectedSession.session_date,
|
||||
slot_date: editSessionDate,
|
||||
start_time: format(currentSlotStart, 'HH:mm:ss'),
|
||||
end_time: format(slotEnd, 'HH:mm:ss'),
|
||||
is_available: false,
|
||||
@@ -229,7 +238,7 @@ export default function AdminConsulting() {
|
||||
await supabase.functions.invoke('update-calendar-event', {
|
||||
body: {
|
||||
session_id: selectedSession.id,
|
||||
date: selectedSession.session_date,
|
||||
date: editSessionDate,
|
||||
start_time: newStartTime,
|
||||
end_time: newEndTime
|
||||
}
|
||||
@@ -250,7 +259,18 @@ export default function AdminConsulting() {
|
||||
if (error) {
|
||||
toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
||||
} else {
|
||||
toast({ title: 'Berhasil', description: 'Perubahan disimpan' });
|
||||
// Show different success message based on what changed
|
||||
if (isRescheduling && scheduleChanged) {
|
||||
toast({
|
||||
title: 'Berhasil Reschedule',
|
||||
description: `Jadwal diubah ke ${format(parseISO(editSessionDate), 'd MMM yyyy', { locale: id })} pukul ${editStartTime}-${editEndTime}`
|
||||
});
|
||||
} else if (scheduleChanged) {
|
||||
toast({ title: 'Berhasil', description: 'Waktu sesi diperbarui' });
|
||||
} else {
|
||||
toast({ title: 'Berhasil', description: 'Perubahan disimpan' });
|
||||
}
|
||||
|
||||
setDialogOpen(false);
|
||||
fetchSessions();
|
||||
}
|
||||
@@ -417,20 +437,31 @@ export default function AdminConsulting() {
|
||||
Sesi Terlewat ({passedConfirmedSessions.length})
|
||||
</h3>
|
||||
<p className="text-sm text-orange-800 dark:text-orange-200 mt-1">
|
||||
Sesi berikut telah berakhir namun masih berstatus "Dikonfirmasi". Silakan update statusnya.
|
||||
Sesi berikut telah berakhir namun masih berstatus "Dikonfirmasi". Reschedule atau update statusnya.
|
||||
</p>
|
||||
<div className="mt-3 space-y-2">
|
||||
{passedConfirmedSessions.map((session) => (
|
||||
<div key={session.id} className="flex items-center justify-between text-sm p-2 bg-white dark:bg-gray-900 rounded border border-orange-200 dark:border-orange-800">
|
||||
<span>
|
||||
<span className="flex-1 mr-2">
|
||||
{format(parseISO(session.session_date), 'd MMM yyyy', { locale: id })} • {session.start_time.substring(0, 5)} - {session.end_time.substring(0, 5)} • {session.profiles?.name || 'N/A'} ({session.topic_category})
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<div className="flex gap-1 shrink-0">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => openMeetDialog(session, true)}
|
||||
className="border-blue-600 text-blue-600 hover:bg-blue-50"
|
||||
title="Reschedule session"
|
||||
>
|
||||
<Calendar className="w-3 h-3 mr-1" />
|
||||
Reschedule
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => updateSessionStatus(session.id, 'completed')}
|
||||
className="border-green-600 text-green-600 hover:bg-green-50"
|
||||
title="Mark as completed"
|
||||
>
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
Selesai
|
||||
@@ -440,6 +471,7 @@ export default function AdminConsulting() {
|
||||
variant="outline"
|
||||
onClick={() => updateSessionStatus(session.id, 'cancelled')}
|
||||
className="border-red-600 text-red-600 hover:bg-red-50"
|
||||
title="Cancel session"
|
||||
>
|
||||
<XCircle className="w-3 h-3 mr-1" />
|
||||
Batal
|
||||
@@ -681,6 +713,15 @@ export default function AdminConsulting() {
|
||||
<LinkIcon className="w-4 h-4 mr-1" />
|
||||
{session.meet_link ? 'Edit' : 'Link'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => openMeetDialog(session, true)}
|
||||
className="border-2 text-blue-600"
|
||||
>
|
||||
<Calendar className="w-4 h-4 mr-1" />
|
||||
Reschedule
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -777,6 +818,15 @@ export default function AdminConsulting() {
|
||||
<LinkIcon className="w-3 h-3 mr-1" />
|
||||
{session.meet_link ? 'Edit' : 'Link'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => openMeetDialog(session, true)}
|
||||
className="flex-1 border-2 text-blue-600 text-xs"
|
||||
>
|
||||
<Calendar className="w-3 h-3 mr-1" />
|
||||
Reschedule
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -915,15 +965,19 @@ export default function AdminConsulting() {
|
||||
}}>
|
||||
<DialogContent className="max-w-md border-2 border-border">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Sesi Konsultasi</DialogTitle>
|
||||
<DialogTitle>{isRescheduling ? 'Reschedule Sesi Konsultasi' : 'Edit Sesi Konsultasi'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Edit waktu, link Google Meet, dan kelola event kalender
|
||||
{isRescheduling
|
||||
? 'Pilih tanggal dan waktu baru untuk sesi konsultasi'
|
||||
: '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>}
|
||||
|
||||
Reference in New Issue
Block a user