Spaces:
Sleeping
Sleeping
| // src/components/sheikh/SheikhSessionRequests.tsx | |
| import { useState, useEffect, useRef } from 'react'; | |
| import { useNavigate } from 'react-router-dom'; | |
| import { toast, Toaster } from 'sonner'; // إضافة import sonner | |
| import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../ui/card'; | |
| import { Button } from '../ui/button'; | |
| import { Badge } from '../ui/badge'; | |
| import { | |
| Table, | |
| TableBody, | |
| TableCell, | |
| TableHead, | |
| TableHeader, | |
| TableRow, | |
| } from '../ui/table'; | |
| import { | |
| Dialog, | |
| DialogContent, | |
| DialogDescription, | |
| DialogFooter, | |
| DialogHeader, | |
| DialogTitle, | |
| } from '../ui/dialog'; | |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; | |
| import { | |
| CheckCircle2, | |
| XCircle, | |
| Clock, | |
| Calendar, | |
| User, | |
| DollarSign, | |
| MessageSquare, | |
| Loader2, | |
| Eye, | |
| RefreshCw, | |
| Video, | |
| CreditCard, | |
| AlertCircle, | |
| } from 'lucide-react'; | |
| import { Alert, AlertDescription } from '../ui/alert'; | |
| import { Separator } from '../ui/separator'; | |
| import { Textarea } from '../ui/textarea'; | |
| import { useLanguage } from '../../lib/languageContext'; | |
| import { translations } from '../../lib/translations'; | |
| // استيراد API | |
| import { | |
| fetchSheikhSessions, | |
| acceptSessionRequest, | |
| rejectSessionRequest, | |
| SessionDto, | |
| SessionStatus, | |
| formatTimeTo12Hour, | |
| isAuthenticated, | |
| } from '../../lib/api/bookingApi'; | |
| export function SheikhSessionRequests() { | |
| const { language } = useLanguage(); | |
| const t = translations[language].sheikhSessions || { | |
| title: 'Session Requests', | |
| pendingRequests: 'Pending Requests', | |
| acceptedSessions: 'Accepted Sessions', | |
| completedSessions: 'Completed Sessions', | |
| rejectedSessions: 'Rejected Sessions', | |
| noPending: 'No pending requests', | |
| noSessions: 'No sessions found', | |
| accept: 'Accept', | |
| reject: 'Reject', | |
| confirmAccept: 'Confirm Acceptance', | |
| confirmReject: 'Confirm Rejection', | |
| acceptMessage: 'Are you sure you want to accept this session request?', | |
| rejectMessage: 'Are you sure you want to reject this session request?', | |
| reasonRequired: 'Please provide a reason for rejection', | |
| enterRejectionReason: 'Enter reason for rejection...', | |
| acceptSuccess: 'Session accepted successfully!', | |
| rejectSuccess: 'Session rejected successfully!', | |
| student: 'Student', | |
| dateTime: 'Date & Time', | |
| amount: 'Amount', | |
| notes: 'Notes', | |
| status: 'Status', | |
| actions: 'Actions', | |
| refresh: 'Refresh', | |
| scheduleMeeting: 'Schedule Meeting', | |
| meetingScheduled: 'Meeting Scheduled', | |
| paymentStatus: 'Payment Status', | |
| paid: 'Paid', | |
| pending: 'Pending', | |
| }; | |
| const navigate = useNavigate(); | |
| const [sessions, setSessions] = useState<SessionDto[]>([]); | |
| const [loading, setLoading] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| const [selectedSession, setSelectedSession] = useState<SessionDto | null>(null); | |
| const [acceptDialogOpen, setAcceptDialogOpen] = useState(false); | |
| const [rejectDialogOpen, setRejectDialogOpen] = useState(false); | |
| const [rejectionReason, setRejectionReason] = useState(''); | |
| const [submitting, setSubmitting] = useState(false); | |
| const [activeTab, setActiveTab] = useState<SessionStatus>('REQUESTED'); | |
| // التحقق من المصادقة | |
| useEffect(() => { | |
| if (!isAuthenticated()) { | |
| navigate('/auth/signin'); | |
| } | |
| }, [navigate]); | |
| const hasLoadedOnce = useRef(false); | |
| // جلب الجلسات | |
| const loadSessions = async () => { | |
| if (!hasLoadedOnce.current) { | |
| setLoading(true); | |
| } | |
| setError(null); | |
| try { | |
| const data = await fetchSheikhSessions(); | |
| setSessions(data || []); | |
| } catch (err) { | |
| console.error('Failed to load sessions:', err); | |
| setError(language === 'ar' | |
| ? 'فشل تحميل الجلسات. يرجى المحاولة مرة أخرى.' | |
| : 'Failed to load sessions. Please try again.'); | |
| } finally { | |
| setLoading(false); | |
| hasLoadedOnce.current = true; | |
| } | |
| }; | |
| useEffect(() => { | |
| loadSessions(); | |
| }, []); | |
| // تصفية الجلسات حسب الحالة | |
| const filteredSessions = sessions.filter(s => s.sessionStatus === activeTab); | |
| // الجلسات المعلقة (REQUESTED) - الأهم | |
| const pendingSessions = sessions.filter(s => s.sessionStatus === 'REQUESTED'); | |
| // قبول الجلسة | |
| const handleAccept = async () => { | |
| if (!selectedSession) return; | |
| const sessionId = selectedSession.sessionId; | |
| // إغلاق الديالوج فوراً وتغيير التبويب لتوفير تجربة مستخدم فورية (0ms) | |
| setAcceptDialogOpen(false); | |
| setActiveTab('TOPAY'); | |
| // تحديث الحالة محلياً بشكل فوري | |
| setSessions(prev => | |
| prev.map(s => | |
| s.sessionId === sessionId | |
| ? { ...s, sessionStatus: 'TOPAY' } | |
| : s | |
| ) | |
| ); | |
| const promise = acceptSessionRequest(sessionId); | |
| toast.promise(promise, { | |
| loading: language === 'ar' ? 'جاري قبول الجلسة...' : 'Accepting session...', | |
| success: t.acceptSuccess, | |
| error: language === 'ar' ? 'فشل قبول الجلسة.' : 'Failed to accept session.', | |
| }); | |
| setSubmitting(true); | |
| try { | |
| const result = await promise; | |
| if (!result) throw new Error('Failed to accept'); | |
| } catch (err) { | |
| console.error('Failed to accept session:', err); | |
| // إعادة تحميل الجلسات في حال الفشل للتراجع عن التغيير المتفائل | |
| loadSessions(); | |
| } finally { | |
| setSubmitting(false); | |
| setSelectedSession(null); | |
| } | |
| }; | |
| // رفض الجلسة | |
| const handleReject = async () => { | |
| if (!selectedSession) return; | |
| const sessionId = selectedSession.sessionId; | |
| // إغلاق الديالوج فوراً | |
| setRejectDialogOpen(false); | |
| // تحديث الحالة محلياً بشكل فوري | |
| setSessions(prev => | |
| prev.map(s => | |
| s.sessionId === sessionId | |
| ? { ...s, sessionStatus: 'CANCELLED' } | |
| : s | |
| ) | |
| ); | |
| const promise = rejectSessionRequest(sessionId); | |
| toast.promise(promise, { | |
| loading: language === 'ar' ? 'جاري رفض الجلسة...' : 'Rejecting session...', | |
| success: t.rejectSuccess, | |
| error: language === 'ar' ? 'فشل رفض الجلسة.' : 'Failed to reject session.', | |
| }); | |
| setSubmitting(true); | |
| try { | |
| const result = await promise; | |
| if (!result) throw new Error('Failed to reject'); | |
| } catch (err) { | |
| console.error('Failed to reject session:', err); | |
| // إعادة تحميل الجلسات للتراجع عن التغيير المتفائل | |
| loadSessions(); | |
| } finally { | |
| setSubmitting(false); | |
| setSelectedSession(null); | |
| setRejectionReason(''); | |
| } | |
| }; | |
| // الحصول على لون وشكل الحالة | |
| const getStatusBadge = (status: SessionStatus) => { | |
| switch (status) { | |
| case 'REQUESTED': | |
| return <Badge className="bg-yellow-500 hover:bg-yellow-600"> | |
| <Clock className="h-3 w-3 mr-1" /> | |
| {language === 'ar' ? 'في انتظار الموافقة' : 'Pending'} | |
| </Badge>; | |
| case 'SCHEDULED': | |
| return <Badge className="bg-blue-500 hover:bg-blue-600"> | |
| <Calendar className="h-3 w-3 mr-1" /> | |
| {language === 'ar' ? 'مجدولة' : 'Scheduled'} | |
| </Badge>; | |
| case 'TOPAY': | |
| return <Badge className="bg-orange-500 hover:bg-orange-600"> | |
| <CreditCard className="h-3 w-3 mr-1" /> | |
| {language === 'ar' ? 'في انتظار الدفع' : 'To Pay'} | |
| </Badge>; | |
| case 'ON_GOING': | |
| return <Badge className="bg-green-500 hover:bg-green-600"> | |
| <Video className="h-3 w-3 mr-1" /> | |
| {language === 'ar' ? 'جارية الآن' : 'On Going'} | |
| </Badge>; | |
| case 'COMPLETED': | |
| return <Badge className="bg-emerald-600 hover:bg-emerald-700"> | |
| <CheckCircle2 className="h-3 w-3 mr-1" /> | |
| {language === 'ar' ? 'مكتملة' : 'Completed'} | |
| </Badge>; | |
| case 'CANCELLED': | |
| return <Badge className="bg-red-500 hover:bg-red-600"> | |
| <XCircle className="h-3 w-3 mr-1" /> | |
| {language === 'ar' ? 'ملغية' : 'Cancelled'} | |
| </Badge>; | |
| case 'MISSED': | |
| return <Badge className="bg-gray-500 hover:bg-gray-600"> | |
| <AlertCircle className="h-3 w-3 mr-1" /> | |
| {language === 'ar' ? 'فاتت' : 'Missed'} | |
| </Badge>; | |
| default: | |
| return <Badge variant="outline">{status}</Badge>; | |
| } | |
| }; | |
| // تنسيق التاريخ والوقت | |
| const formatDateTime = (dateTimeStr: string) => { | |
| const date = new Date(dateTimeStr); | |
| return { | |
| date: date.toLocaleDateString(language === 'ar' ? 'ar-EG' : 'en-US'), | |
| time: formatTimeTo12Hour(date.toLocaleTimeString()), | |
| full: `${date.toLocaleDateString(language === 'ar' ? 'ar-EG' : 'en-US')} - ${formatTimeTo12Hour(date.toLocaleTimeString())}`, | |
| }; | |
| }; | |
| // جدول الجلسات | |
| const SessionsTable = ({ sessionsList, showActions = true }: { sessionsList: SessionDto[], showActions?: boolean }) => { | |
| if (sessionsList.length === 0) { | |
| return ( | |
| <div className="text-center py-12 text-muted-foreground"> | |
| {activeTab === 'REQUESTED' ? t.noPending : t.noSessions} | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="overflow-x-auto"> | |
| <Table> | |
| <TableHeader> | |
| <TableRow> | |
| <TableHead>{t.student}</TableHead> | |
| <TableHead>{t.dateTime}</TableHead> | |
| <TableHead>{t.amount}</TableHead> | |
| <TableHead>{t.paymentStatus}</TableHead> | |
| <TableHead>{t.status}</TableHead> | |
| {showActions && <TableHead className="text-right">{t.actions}</TableHead>} | |
| </TableRow> | |
| </TableHeader> | |
| <TableBody> | |
| {sessionsList.map((session) => { | |
| const dateTime = formatDateTime(session.scheduledStart); | |
| return ( | |
| <TableRow key={session.sessionId}> | |
| <TableCell className="font-medium">{session.studentName}</TableCell> | |
| <TableCell>{dateTime.full}</TableCell> | |
| <TableCell>${session.price}</TableCell> | |
| <TableCell> | |
| <Badge variant={session.paymentStatus === 'PAID' ? 'default' : 'secondary'} className={session.paymentStatus === 'PAID' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}> | |
| {session.paymentStatus === 'PAID' ? t.paid : t.pending} | |
| </Badge> | |
| </TableCell> | |
| <TableCell>{getStatusBadge(session.sessionStatus)}</TableCell> | |
| {showActions && session.sessionStatus === 'REQUESTED' && ( | |
| <TableCell className="text-right"> | |
| <div className="flex gap-2 justify-end"> | |
| <Button | |
| size="sm" | |
| variant="outline" | |
| className="border-green-500 text-green-600 hover:bg-green-50" | |
| onClick={() => { | |
| setSelectedSession(session); | |
| setAcceptDialogOpen(true); | |
| }} | |
| > | |
| <CheckCircle2 className="h-4 w-4 mr-1" /> | |
| {t.accept} | |
| </Button> | |
| <Button | |
| size="sm" | |
| variant="outline" | |
| className="border-red-500 text-red-600 hover:bg-red-50" | |
| onClick={() => { | |
| setSelectedSession(session); | |
| setRejectDialogOpen(true); | |
| }} | |
| > | |
| <XCircle className="h-4 w-4 mr-1" /> | |
| {t.reject} | |
| </Button> | |
| </div> | |
| </TableCell> | |
| )} | |
| {showActions && session.sessionStatus === 'TOPAY' && ( | |
| <TableCell className="text-right"> | |
| <Button | |
| size="sm" | |
| variant="outline" | |
| onClick={() => navigate(`/sheikh/session/${session.sessionId}/meeting`)} | |
| > | |
| <Video className="h-4 w-4 mr-1" /> | |
| {t.scheduleMeeting} | |
| </Button> | |
| </TableCell> | |
| )} | |
| </TableRow> | |
| ); | |
| })} | |
| </TableBody> | |
| </Table> | |
| </div> | |
| ); | |
| }; | |
| return ( | |
| <div className="container mx-auto p-6 max-w-7xl" dir={language === 'ar' ? 'rtl' : 'ltr'}> | |
| {/* إضافة Toaster component */} | |
| <Toaster richColors position="top-center" /> | |
| {/* Header */} | |
| <div className="flex justify-between items-center mb-6"> | |
| <div> | |
| <h1 className="text-2xl font-bold">{t.title}</h1> | |
| <p className="text-muted-foreground"> | |
| {language === 'ar' | |
| ? `لديك ${pendingSessions.length} طلبات جلسات في انتظار الموافقة` | |
| : `You have ${pendingSessions.length} pending session requests`} | |
| </p> | |
| </div> | |
| <Button variant="outline" onClick={loadSessions} disabled={loading}> | |
| <RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} /> | |
| {t.refresh} | |
| </Button> | |
| </div> | |
| {/* Error Message */} | |
| {error && ( | |
| <Alert variant="destructive" className="mb-6"> | |
| <AlertCircle className="h-4 w-4" /> | |
| <AlertDescription>{error}</AlertDescription> | |
| </Alert> | |
| )} | |
| {/* Tabs */} | |
| <Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as SessionStatus)}> | |
| <TabsList className="grid w-full grid-cols-4"> | |
| <TabsTrigger value="REQUESTED"> | |
| {language === 'ar' ? 'قيد الانتظار' : 'Pending'} | |
| {pendingSessions.length > 0 && ` (${pendingSessions.length})`} | |
| </TabsTrigger> | |
| <TabsTrigger value="TOPAY">{language === 'ar' ? 'في انتظار الدفع' : 'To Pay'}</TabsTrigger> | |
| <TabsTrigger value="COMPLETED">{language === 'ar' ? 'مكتملة' : 'Completed'}</TabsTrigger> | |
| <TabsTrigger value="CANCELLED">{language === 'ar' ? 'ملغية' : 'Cancelled'}</TabsTrigger> | |
| </TabsList> | |
| <TabsContent value="REQUESTED" className="mt-6"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>{t.pendingRequests}</CardTitle> | |
| <CardDescription> | |
| {language === 'ar' | |
| ? 'قم بقبول أو رفض طلبات الجلسات من الطلاب' | |
| : 'Accept or reject session requests from students'} | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| <SessionsTable sessionsList={filteredSessions} showActions={true} /> | |
| </CardContent> | |
| </Card> | |
| </TabsContent> | |
| <TabsContent value="TOPAY" className="mt-6"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>{t.acceptedSessions}</CardTitle> | |
| <CardDescription> | |
| {language === 'ar' | |
| ? 'الجلسات التي تم قبولها وفي انتظار الدفع' | |
| : 'Sessions that have been accepted and awaiting payment'} | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| <SessionsTable sessionsList={filteredSessions} showActions={true} /> | |
| </CardContent> | |
| </Card> | |
| </TabsContent> | |
| <TabsContent value="COMPLETED" className="mt-6"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>{t.completedSessions}</CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <SessionsTable sessionsList={filteredSessions} showActions={false} /> | |
| </CardContent> | |
| </Card> | |
| </TabsContent> | |
| <TabsContent value="CANCELLED" className="mt-6"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>{t.rejectedSessions}</CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <SessionsTable sessionsList={filteredSessions} showActions={false} /> | |
| </CardContent> | |
| </Card> | |
| </TabsContent> | |
| </Tabs> | |
| {/* Accept Dialog */} | |
| <Dialog open={acceptDialogOpen} onOpenChange={setAcceptDialogOpen}> | |
| <DialogContent> | |
| <DialogHeader> | |
| <DialogTitle className="flex items-center gap-2"> | |
| <CheckCircle2 className="h-5 w-5 text-green-600" /> | |
| {t.confirmAccept} | |
| </DialogTitle> | |
| <DialogDescription> | |
| {t.acceptMessage} | |
| </DialogDescription> | |
| </DialogHeader> | |
| {selectedSession && ( | |
| <div className="space-y-3 py-4"> | |
| <div className="bg-gray-50 rounded-lg p-3 text-sm"> | |
| <div className="flex justify-between mb-2"> | |
| <span className="text-gray-500">{t.student}:</span> | |
| <span className="font-medium">{selectedSession.studentName}</span> | |
| </div> | |
| <div className="flex justify-between mb-2"> | |
| <span className="text-gray-500">{t.dateTime}:</span> | |
| <span className="font-medium">{formatDateTime(selectedSession.scheduledStart).full}</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-500">{t.amount}:</span> | |
| <span className="font-bold text-green-600">${selectedSession.price}</span> | |
| </div> | |
| {selectedSession.sessionDescription && ( | |
| <div className="mt-3 pt-2 border-t"> | |
| <span className="text-gray-500 text-xs">{t.notes}:</span> | |
| <p className="text-xs mt-1">{selectedSession.sessionDescription}</p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| <DialogFooter> | |
| <Button variant="outline" onClick={() => setAcceptDialogOpen(false)}> | |
| {language === 'ar' ? 'إلغاء' : 'Cancel'} | |
| </Button> | |
| <Button onClick={handleAccept} disabled={submitting} className="bg-green-600 hover:bg-green-700"> | |
| {submitting ? ( | |
| <> | |
| <Loader2 className="h-4 w-4 mr-2 animate-spin" /> | |
| {language === 'ar' ? 'جاري القبول...' : 'Accepting...'} | |
| </> | |
| ) : ( | |
| t.accept | |
| )} | |
| </Button> | |
| </DialogFooter> | |
| </DialogContent> | |
| </Dialog> | |
| {/* Reject Dialog */} | |
| <Dialog open={rejectDialogOpen} onOpenChange={setRejectDialogOpen}> | |
| <DialogContent> | |
| <DialogHeader> | |
| <DialogTitle className="flex items-center gap-2"> | |
| <XCircle className="h-5 w-5 text-red-600" /> | |
| {t.confirmReject} | |
| </DialogTitle> | |
| <DialogDescription> | |
| {t.rejectMessage} | |
| </DialogDescription> | |
| </DialogHeader> | |
| <div className="space-y-4 py-4"> | |
| <div className="space-y-2"> | |
| <label className="text-sm font-medium">{t.reasonRequired}</label> | |
| <Textarea | |
| placeholder={t.enterRejectionReason} | |
| value={rejectionReason} | |
| onChange={(e) => setRejectionReason(e.target.value)} | |
| rows={3} | |
| /> | |
| </div> | |
| </div> | |
| <DialogFooter> | |
| <Button variant="outline" onClick={() => setRejectDialogOpen(false)}> | |
| {language === 'ar' ? 'إلغاء' : 'Cancel'} | |
| </Button> | |
| <Button onClick={handleReject} disabled={submitting} variant="destructive"> | |
| {submitting ? ( | |
| <> | |
| <Loader2 className="h-4 w-4 mr-2 animate-spin" /> | |
| {language === 'ar' ? 'جاري الرفض...' : 'Rejecting...'} | |
| </> | |
| ) : ( | |
| t.reject | |
| )} | |
| </Button> | |
| </DialogFooter> | |
| </DialogContent> | |
| </Dialog> | |
| </div> | |
| ); | |
| } |