Quran_Tech_Server / F_Pro /src /components /sheikh /SheikhSessionRequests.tsx
aboalaa147's picture
Initial deployment
eb6a2f9
Raw
History Blame Contribute Delete
20.5 kB
// 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>
);
}