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:
dwindown
2025-12-31 12:59:07 +07:00
parent 9e76d07cc2
commit 0be27ccf99
3 changed files with 111 additions and 10 deletions

View File

@@ -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">

View File

@@ -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>

View File

@@ -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" ? (
<Alert className="bg-green-50 border-green-200">
<Video className="h-4 w-4" />
<AlertDescription>
Pembayaran berhasil! Silakan bergabung sesuai jadwal.
</AlertDescription>
</Alert>
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" />