Handle passed consulting sessions for admin and member
Problem: Passed consulting sessions stayed in "Dikonfirmasi" status indefinitely, showing JOIN button to members even after session ended, with no admin action buttons. Solution: 1. Admin UI (AdminConsulting.tsx): - Add isSessionPassed() helper to check if session end time has passed - Add "Sesi Terlewat" alert card at top with quick action buttons - Add "Perlu Update" stat card (orange) for passed confirmed sessions - Existing action buttons (Selesai/Batal) already work for confirmed sessions 2. Member UI (ConsultingHistory.tsx): - Move passed confirmed sessions from "Sesi Mendatang" to new "Sesi Terlewat" section - Remove JOIN button for passed sessions - Show "Menunggu konfirmasi admin" status message - Display with orange styling to indicate needs attention 3. Order Detail (OrderDetail.tsx): - Add isConsultingSessionPassed check - Show orange alert for passed paid sessions: "Sesi telah berakhir. Menunggu konfirmasi admin" - Keep green alert for upcoming paid sessions Flow: - Session ends → Still shows "confirmed" status - Admin sees orange alert → Clicks Selesai or Batal - Member sees passed session → No JOIN button → Waits for admin - Admin updates status → Session moves to completed/cancelled 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -87,6 +87,12 @@ export function ConsultingHistory({ userId }: ConsultingHistoryProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// Check if session has passed
|
||||
const isSessionPassed = (session: ConsultingSession) => {
|
||||
const sessionEndDateTime = new Date(`${session.session_date}T${session.end_time}`);
|
||||
return new Date() > sessionEndDateTime;
|
||||
};
|
||||
|
||||
const openReviewModal = (session: ConsultingSession) => {
|
||||
const dateLabel = format(new Date(session.session_date), 'd MMMM yyyy', { locale: id });
|
||||
const timeLabel = `${session.start_time.substring(0, 5)} - ${session.end_time.substring(0, 5)}`;
|
||||
@@ -128,7 +134,8 @@ export function ConsultingHistory({ userId }: ConsultingHistoryProps) {
|
||||
};
|
||||
|
||||
const doneSessions = sessions.filter(s => s.status === 'done' || s.status === 'completed');
|
||||
const upcomingSessions = sessions.filter(s => s.status === 'confirmed');
|
||||
const upcomingSessions = sessions.filter(s => s.status === 'confirmed' && !isSessionPassed(s));
|
||||
const passedSessions = sessions.filter(s => s.status === 'confirmed' && isSessionPassed(s));
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -212,6 +219,33 @@ export function ConsultingHistory({ userId }: ConsultingHistoryProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Passed confirmed sessions (waiting for admin action) */}
|
||||
{passedSessions.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-orange-600 dark:text-orange-400">Sesi Terlewat</h4>
|
||||
{passedSessions.map((session) => (
|
||||
<div key={session.id} className="flex items-center justify-between p-3 border-2 border-orange-200 bg-orange-50 dark:bg-orange-950/20">
|
||||
<div className="flex items-center gap-3">
|
||||
<Clock className="w-4 h-4 text-orange-600" />
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{format(new Date(session.session_date), 'd MMM yyyy', { locale: id })}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||
{session.start_time.substring(0, 5)} - {session.end_time.substring(0, 5)}
|
||||
{session.topic_category && ` • ${session.topic_category}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusBadge(session.status)}
|
||||
<span className="text-xs text-muted-foreground">Menunggu konfirmasi admin</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Completed sessions */}
|
||||
{doneSessions.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -323,6 +323,12 @@ export default function AdminConsulting() {
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to check if session has passed
|
||||
const isSessionPassed = (session: ConsultingSession) => {
|
||||
const sessionEndDateTime = new Date(`${session.session_date}T${session.end_time}`);
|
||||
return new Date() > sessionEndDateTime;
|
||||
};
|
||||
|
||||
const updateSessionStatus = async (sessionId: string, newStatus: string) => {
|
||||
// If cancelling and session has a calendar event, delete it first
|
||||
if (newStatus === 'cancelled') {
|
||||
@@ -389,6 +395,7 @@ export default function AdminConsulting() {
|
||||
const upcomingSessions = filteredSessions.filter(s => s.session_date >= today && (s.status === 'confirmed' || s.status === 'pending_payment'));
|
||||
const pastSessions = filteredSessions.filter(s => s.session_date < today || s.status === 'completed' || s.status === 'cancelled');
|
||||
const todaySessions = filteredSessions.filter(s => isToday(parseISO(s.session_date)) && s.status === 'confirmed');
|
||||
const passedConfirmedSessions = filteredSessions.filter(s => s.status === 'confirmed' && isSessionPassed(s));
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
@@ -401,6 +408,50 @@ export default function AdminConsulting() {
|
||||
Kelola jadwal dan link Google Meet untuk sesi konsultasi
|
||||
</p>
|
||||
|
||||
{/* Passed Confirmed Sessions Alert */}
|
||||
{passedConfirmedSessions.length > 0 && (
|
||||
<Card className="border-2 border-orange-500 bg-orange-50 dark:bg-orange-950 mb-6">
|
||||
<CardContent className="py-4">
|
||||
<h3 className="font-bold flex items-center gap-2 text-orange-900 dark:text-orange-100">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
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.
|
||||
</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>
|
||||
{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">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => updateSessionStatus(session.id, 'completed')}
|
||||
className="border-green-600 text-green-600 hover:bg-green-50"
|
||||
>
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
Selesai
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => updateSessionStatus(session.id, 'cancelled')}
|
||||
className="border-red-600 text-red-600 hover:bg-red-50"
|
||||
>
|
||||
<XCircle className="w-3 h-3 mr-1" />
|
||||
Batal
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Today's Sessions Alert */}
|
||||
{todaySessions.length > 0 && (
|
||||
<Card className="border-2 border-primary bg-primary/5 mb-6">
|
||||
@@ -451,10 +502,10 @@ export default function AdminConsulting() {
|
||||
<p className="text-sm text-muted-foreground">Perlu Link Meet</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-2 border-border">
|
||||
<Card className="border-2 border-orange-500 bg-orange-50 dark:bg-orange-950">
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{pastSessions.filter(s => s.status === 'completed').length}</div>
|
||||
<p className="text-sm text-muted-foreground">Selesai</p>
|
||||
<div className="text-2xl font-bold text-orange-900 dark:text-orange-100">{passedConfirmedSessions.length}</div>
|
||||
<p className="text-sm text-orange-700 dark:text-orange-300">Perlu Update</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -75,6 +75,13 @@ export default function OrderDetail() {
|
||||
(item: OrderItem) => item.products.type === "consulting"
|
||||
) || false;
|
||||
|
||||
// Check if consulting session has passed
|
||||
const isConsultingSessionPassed = consultingSlots.length > 0 ? (() => {
|
||||
const slot = consultingSlots[0];
|
||||
const sessionEndDateTime = new Date(`${slot.session_date}T${slot.end_time}`);
|
||||
return new Date() > sessionEndDateTime;
|
||||
})() : false;
|
||||
|
||||
// Memoized fetchOrder to avoid recreating on every render
|
||||
const fetchOrder = useCallback(async () => {
|
||||
if (!user || !id) return;
|
||||
@@ -758,12 +765,21 @@ export default function OrderDetail() {
|
||||
|
||||
{/* Status Alert */}
|
||||
{order.payment_status === "paid" ? (
|
||||
isConsultingSessionPassed ? (
|
||||
<Alert className="bg-orange-50 border-orange-200">
|
||||
<Clock className="h-4 w-4 text-orange-600" />
|
||||
<AlertDescription className="text-orange-900">
|
||||
Sesi konsultasi telah berakhir. Menunggu konfirmasi admin.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert className="bg-green-50 border-green-200">
|
||||
<Video className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Pembayaran berhasil! Silakan bergabung sesuai jadwal.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
) : order.status !== "cancelled" && order.payment_status === "pending" && !isQrExpired ? (
|
||||
<Alert className="bg-yellow-50 border-yellow-200">
|
||||
<Clock className="h-4 w-4" />
|
||||
|
||||
Reference in New Issue
Block a user