Spaces:
Sleeping
Sleeping
| import { useEffect, useState } from 'react'; | |
| import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; | |
| import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; | |
| import { Button } from '../ui/button'; | |
| import { Badge } from '../ui/badge'; | |
| import { | |
| CheckCircle2, Calendar, Clock, User, | |
| FileText, Star, ArrowLeft, Award, Quote, XCircle, AlertTriangle | |
| } from 'lucide-react'; | |
| import { useLanguage } from '../../lib/languageContext'; | |
| import { translations } from '../../lib/translations'; | |
| import { useAuth } from '../../lib/authContext'; | |
| import { fetchSessionReview, submitSessionReview } from '../../lib/api/bookingApi'; | |
| import { getApiBaseUrl } from '../../lib/api/config'; | |
| export function SessionReport() { | |
| const { id } = useParams(); | |
| const [searchParams] = useSearchParams(); | |
| const reviewMode = searchParams.get('mode') === 'review'; | |
| const navigate = useNavigate(); | |
| const { language } = useLanguage(); | |
| const { user } = useAuth(); | |
| const t = (translations[language] as any).sessionReport || { | |
| title: 'Session Summary', | |
| subtitle: 'Great job completing your session!', | |
| duration: 'Actual Duration', | |
| notes: "Sheikh's Observations", | |
| backHome: 'Back to Dashboard', | |
| reviewSectionTitle: 'Your Review', | |
| date: 'Date', | |
| teacher: 'Teacher', | |
| completed: 'Completed', | |
| minutes: 'minutes', | |
| noNotes: 'No notes were provided for this session.', | |
| }; | |
| const isAr = language === 'ar'; | |
| const [session, setSession] = useState<any>(null); | |
| const [loading, setLoading] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| const [sessionReview, setSessionReview] = useState<any | null>(null); | |
| const [loadingReview, setLoadingReview] = useState(false); | |
| // โโ Review form state โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| const [hoveredStar, setHoveredStar] = useState(0); | |
| const [selectedStar, setSelectedStar] = useState(0); | |
| const [reviewComment, setReviewComment] = useState(''); | |
| const [submitting, setSubmitting] = useState(false); | |
| const [submitError, setSubmitError] = useState<string | null>(null); | |
| const [submitSuccess, setSubmitSuccess] = useState(false); | |
| useEffect(() => { | |
| const fetchSession = async () => { | |
| if (!id || !user) return; | |
| try { | |
| const token = localStorage.getItem('authToken'); | |
| const BASE_URL = getApiBaseUrl(); | |
| const rolePath = user.role === 'sheikh' ? 'sheikh' : 'student'; | |
| const response = await fetch(`${BASE_URL}/api/${rolePath}/sessions/${id}`, { | |
| headers: { 'Authorization': `Bearer ${token}` }, | |
| }); | |
| if (!response.ok) throw new Error('Session not found'); | |
| const data = await response.json(); | |
| const scheduledDate = data.scheduledStart | |
| ? new Date(data.scheduledStart) | |
| : data.scheduledStartDate && data.scheduledStartTime | |
| ? new Date(`${data.scheduledStartDate}T${data.scheduledStartTime}`) | |
| : undefined; | |
| setSession({ | |
| id: (data.id || data.sessionId).toString(), | |
| studentName: data.studentName || '', | |
| sheikhName: data.sheikhName || '', | |
| scheduledDate, | |
| duration: data.duration ?? 60, | |
| sessionNotes: data.sessionNotes || '', | |
| sessionStatus: data.sessionStatus || data.status || '', | |
| }); | |
| } catch (err) { | |
| console.error(err); | |
| setError('Session summary could not be loaded.'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| fetchSession(); | |
| }, [id, user]); | |
| useEffect(() => { | |
| if (!id || !session) return; | |
| setLoadingReview(true); | |
| fetchSessionReview(Number(id), user?.role) | |
| .then(setSessionReview) | |
| .catch(() => setSessionReview(null)) | |
| .finally(() => setLoadingReview(false)); | |
| }, [id, session, user]); | |
| const handleSubmitReview = async () => { | |
| if (!selectedStar || !id) return; | |
| setSubmitting(true); | |
| setSubmitError(null); | |
| try { | |
| await submitSessionReview(Number(id), { rating: selectedStar, notes: reviewComment }); | |
| setSubmitSuccess(true); | |
| // Refresh the review display | |
| const updated = await fetchSessionReview(Number(id), user?.role); | |
| setSessionReview(updated); | |
| } catch { | |
| setSubmitError(isAr ? 'ูุดู ุฅุฑุณุงู ุงูุชูููู . ุญุงูู ู ุฑุฉ ุฃุฎุฑู.' : 'Failed to submit review. Please try again.'); | |
| } finally { | |
| setSubmitting(false); | |
| } | |
| }; | |
| if (loading) { | |
| return <div className="min-h-screen flex items-center justify-center">Loading session report...</div>; | |
| } | |
| if (error || !session) { | |
| return ( | |
| <div className="min-h-screen flex items-center justify-center p-6 text-center"> | |
| <div> | |
| <XCircle className="h-12 w-12 mx-auto text-red-500 mb-4" /> | |
| <p className="text-xl font-bold">{error || 'Session not found.'}</p> | |
| <Button className="mt-4" onClick={() => navigate(-1)}>Go Back</Button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| const isMissed = session.sessionStatus === 'MISSED' || session.sessionStatus === 'CANCELLED'; | |
| const isStudent = user?.role === 'student'; | |
| const existingRate: number | null = sessionReview?.studentRate ?? null; | |
| const existingComment: string | null = sessionReview?.studentReview ?? null; | |
| const existingDate: string | null = sessionReview?.reviewedAt ?? null; | |
| const reviewerName: string | null = sessionReview?.studentName ?? null; | |
| const canLeaveReview = reviewMode && isStudent && existingRate == null && !submitSuccess; | |
| return ( | |
| <div className="min-h-screen bg-slate-50 py-8 px-4 sm:px-6 lg:px-8" dir={isAr ? 'rtl' : 'ltr'}> | |
| <div className="max-w-3xl mx-auto space-y-6"> | |
| {/* Back Button */} | |
| <Button | |
| variant="ghost" | |
| className="text-slate-500 hover:text-slate-900 -ml-2" | |
| onClick={() => navigate(user?.role === 'sheikh' ? '/sheikh/dashboard' : '/student/dashboard')} | |
| > | |
| <ArrowLeft className="mr-2 h-4 w-4" /> | |
| {t.backHome} | |
| </Button> | |
| {/* Header */} | |
| <div className="text-center space-y-4 py-6"> | |
| <div className={`inline-flex items-center justify-center p-4 rounded-full ${isMissed ? 'bg-red-100' : 'bg-emerald-100'}`}> | |
| {isMissed | |
| ? <AlertTriangle className="h-12 w-12 text-red-500" /> | |
| : <CheckCircle2 className="h-12 w-12 text-emerald-600" />} | |
| </div> | |
| <div className="space-y-2"> | |
| <h1 className="text-3xl sm:text-4xl font-bold text-slate-900 tracking-tight"> | |
| {isMissed | |
| ? (isAr ? 'ุงูุฌูุณุฉ ูู ุชูุนูุฏ' : 'Session Was Missed') | |
| : t.title} | |
| </h1> | |
| <p className="text-slate-500 text-lg"> | |
| {isMissed | |
| ? (isAr ? 'ูู ูุญุถุฑ ุงูุดูุฎ ูู ุงูููุช ุงูู ุญุฏุฏ.' : 'The sheikh did not show up in time.') | |
| : t.subtitle} | |
| </p> | |
| </div> | |
| </div> | |
| {/* Overview Cards */} | |
| <div className="grid grid-cols-2 sm:grid-cols-4 gap-3"> | |
| {/* Duration */} | |
| <Card className="col-span-1 bg-emerald-600 text-white border-0"> | |
| <CardContent className="p-4 flex flex-col items-center justify-center text-center h-full min-h-[120px]"> | |
| <Clock className="h-6 w-6 opacity-80 mb-2" /> | |
| <div className="text-3xl font-bold">{isMissed ? 0 : session.duration}</div> | |
| <div className="text-xs opacity-80 uppercase tracking-wider">{t.minutes}</div> | |
| </CardContent> | |
| </Card> | |
| {/* Teacher */} | |
| <Card className="col-span-1 bg-white border-slate-200"> | |
| <CardContent className="p-4 flex flex-col items-center justify-center text-center h-full min-h-[120px]"> | |
| <User className="h-6 w-6 text-emerald-600 mb-2" /> | |
| <div className="text-xs text-slate-500 uppercase tracking-wider mb-1">{t.teacher}</div> | |
| <div className="font-semibold text-slate-800 text-sm">{session.sheikhName}</div> | |
| </CardContent> | |
| </Card> | |
| {/* Date */} | |
| <Card className="col-span-1 bg-white border-slate-200"> | |
| <CardContent className="p-4 flex flex-col items-center justify-center text-center h-full min-h-[120px]"> | |
| <Calendar className="h-6 w-6 text-emerald-600 mb-2" /> | |
| <div className="text-xs text-slate-500 uppercase tracking-wider mb-1">{t.date}</div> | |
| <div className="font-semibold text-slate-800 text-sm"> | |
| {session.scheduledDate?.toLocaleDateString(isAr ? 'ar-EG' : 'en-US', { | |
| month: 'short', day: 'numeric', year: 'numeric', | |
| })} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| {/* Status */} | |
| <Card className="col-span-1 bg-white border-slate-200"> | |
| <CardContent className="p-4 flex flex-col items-center justify-center text-center h-full min-h-[120px]"> | |
| <Award className="h-6 w-6 text-emerald-600 mb-2" /> | |
| <div className="text-xs text-slate-500 uppercase tracking-wider mb-1">Status</div> | |
| {isMissed ? ( | |
| <Badge className="bg-red-100 text-red-700 hover:bg-red-100 border-0 text-xs"> | |
| {isAr ? 'ูุงุฆุชุฉ' : 'Missed'} | |
| </Badge> | |
| ) : ( | |
| <Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100 border-0 text-xs"> | |
| {t.completed} | |
| </Badge> | |
| )} | |
| </CardContent> | |
| </Card> | |
| </div> | |
| {/* Sheikh's Notes โ only shown for completed sessions */} | |
| {!isMissed && ( | |
| <Card className="bg-white border-slate-200 overflow-hidden"> | |
| <CardHeader className="bg-slate-800 border-b border-slate-700 pb-4"> | |
| <CardTitle className="flex items-center gap-2 text-lg text-emerald-400"> | |
| <FileText className="h-5 w-5 text-emerald-400" /> | |
| {t.notes} | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent className="p-6"> | |
| <div className="flex gap-3 items-start"> | |
| <Quote className="h-5 w-5 text-emerald-500 shrink-0 mt-0.5" /> | |
| <p className="text-slate-700 leading-relaxed text-base italic"> | |
| {session.sessionNotes || t.noNotes} | |
| </p> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| )} | |
| {/* โโ Review Section โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */} | |
| <Card className="bg-white border-slate-200 overflow-hidden"> | |
| <CardHeader className="bg-slate-900 border-b border-slate-700 pb-4"> | |
| <CardTitle className="flex items-center gap-2 text-lg text-emerald-400"> | |
| <Star className="h-5 w-5 text-emerald-400" /> | |
| {user?.role === 'sheikh' | |
| ? (isAr ? 'ุชูููู ุงูุทุงูุจ' : 'Student Review') | |
| : (isAr ? 'ุชูููู ู' : t.reviewSectionTitle)} | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent className="p-6"> | |
| {loadingReview ? ( | |
| <p className="text-slate-500 text-center py-4"> | |
| {isAr ? 'ุฌุงุฑู ุงูุชุญู ูู...' : 'Loading review...'} | |
| </p> | |
| ) : (existingRate != null || submitSuccess) ? ( | |
| /* โโ Existing / just-submitted review display โโ */ | |
| <div className="space-y-4"> | |
| {submitSuccess && ( | |
| <div className="flex items-center gap-2 text-emerald-600 bg-emerald-50 rounded-lg px-4 py-3 border border-emerald-200 mb-2"> | |
| <CheckCircle2 className="h-5 w-5 shrink-0" /> | |
| <span className="text-sm font-medium"> | |
| {isAr ? 'ุชู ุฅุฑุณุงู ุชูููู ู ุจูุฌุงุญ!' : 'Your review was submitted successfully!'} | |
| </span> | |
| </div> | |
| )} | |
| <div className="flex items-center gap-1"> | |
| {Array.from({ length: 5 }).map((_, i) => ( | |
| <Star | |
| key={i} | |
| className="h-7 w-7" | |
| style={i < (existingRate ?? selectedStar ?? 0) | |
| ? { fill: '#FBBF24', color: '#FBBF24' } | |
| : { fill: '#e2e8f0', color: '#e2e8f0' }} | |
| /> | |
| ))} | |
| </div> | |
| {(existingComment || reviewComment) && ( | |
| <div className="flex gap-3 items-start"> | |
| <Quote className="h-5 w-5 text-emerald-500 shrink-0 mt-0.5" /> | |
| <p className="text-slate-700 leading-relaxed text-base italic"> | |
| {existingComment || reviewComment} | |
| </p> | |
| </div> | |
| )} | |
| <div className="flex items-center gap-2 text-sm text-slate-400 pt-2"> | |
| {reviewerName && user?.role === 'sheikh' && <span>{reviewerName}</span>} | |
| {reviewerName && user?.role === 'sheikh' && <span>โ</span>} | |
| {existingDate && ( | |
| <span> | |
| {new Date(existingDate).toLocaleDateString(isAr ? 'ar-EG' : 'en-US', { | |
| month: 'short', day: 'numeric', year: 'numeric', | |
| })} | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| ) : canLeaveReview ? ( | |
| /* โโ Review submission form (student, no review yet) โโ */ | |
| <div className="space-y-5"> | |
| {isMissed && ( | |
| <div className="flex items-start gap-2 bg-amber-50 border border-amber-200 rounded-lg px-4 py-3 text-sm text-amber-800"> | |
| <AlertTriangle className="h-4 w-4 shrink-0 mt-0.5" /> | |
| <span> | |
| {isAr | |
| ? 'ุงูุดูุฎ ูู ูุญุถุฑ ุงูุฌูุณุฉ. ูู ููู ุชูููู ุชุฌุฑุจุชู.' | |
| : 'The sheikh missed this session. You can still leave a review of your experience.'} | |
| </span> | |
| </div> | |
| )} | |
| <div> | |
| <p className="text-sm font-medium text-slate-700 mb-3"> | |
| {isAr ? 'ููู ุชูููู ูุฐู ุงูุชุฌุฑุจุฉุ' : 'How would you rate this experience?'} | |
| </p> | |
| <div className="flex gap-1"> | |
| {Array.from({ length: 5 }).map((_, i) => { | |
| const val = i + 1; | |
| const filled = val <= (hoveredStar || selectedStar); | |
| return ( | |
| <button | |
| key={i} | |
| type="button" | |
| onMouseEnter={() => setHoveredStar(val)} | |
| onMouseLeave={() => setHoveredStar(0)} | |
| onClick={() => setSelectedStar(val)} | |
| className="focus:outline-none transition-transform hover:scale-110" | |
| > | |
| <Star | |
| className="h-9 w-9 transition-colors" | |
| style={filled | |
| ? { fill: '#FBBF24', color: '#FBBF24' } | |
| : { fill: '#e2e8f0', color: '#e2e8f0' }} | |
| /> | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| {selectedStar > 0 && ( | |
| <p className="text-xs text-slate-400 mt-1"> | |
| {['', isAr ? 'ุณูุฆ ุฌุฏุงู' : 'Very poor', isAr ? 'ุณูุฆ' : 'Poor', isAr ? 'ู ูุจูู' : 'Fair', isAr ? 'ุฌูุฏ' : 'Good', isAr ? 'ู ู ุชุงุฒ' : 'Excellent'][selectedStar]} | |
| </p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="text-sm font-medium text-slate-700 block mb-2"> | |
| {isAr ? 'ุชุนูููู (ุงุฎุชูุงุฑู)' : 'Your comment (optional)'} | |
| </label> | |
| <textarea | |
| value={reviewComment} | |
| onChange={(e) => setReviewComment(e.target.value)} | |
| rows={3} | |
| placeholder={isAr ? 'ุงูุชุจ ุชุนูููู ููุง...' : 'Write your comment here...'} | |
| className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-emerald-400 resize-none" | |
| /> | |
| </div> | |
| {submitError && ( | |
| <p className="text-sm text-red-600">{submitError}</p> | |
| )} | |
| <Button | |
| onClick={handleSubmitReview} | |
| disabled={!selectedStar || submitting} | |
| className="w-full bg-emerald-600 hover:bg-emerald-700 text-white" | |
| > | |
| {submitting | |
| ? (isAr ? 'ุฌุงุฑู ุงูุฅุฑุณุงู...' : 'Submitting...') | |
| : (isAr ? 'ุฅุฑุณุงู ุงูุชูููู ' : 'Submit Review')} | |
| </Button> | |
| </div> | |
| ) : ( | |
| /* โโ Sheikh view or student who already reviewed (no form) โโ */ | |
| <p className="text-slate-500 text-center py-4"> | |
| {user?.role === 'sheikh' | |
| ? (isAr ? 'ูู ูุชุฑู ุงูุทุงูุจ ุชูููู ุงู ุจุนุฏ' : 'No review has been left for this session yet.') | |
| : (isAr ? 'ูู ุชุชุฑู ุชูููู ุงู ุจุนุฏ' : 'You have not left a review for this session yet.')} | |
| </p> | |
| )} | |
| </CardContent> | |
| </Card> | |
| {/* Back Button */} | |
| <div className="flex justify-center pt-4 pb-8"> | |
| <Button | |
| size="lg" | |
| className="bg-slate-900 text-white hover:bg-slate-800 px-8" | |
| onClick={() => navigate(user?.role === 'sheikh' ? '/sheikh/dashboard' : '/student/dashboard')} | |
| > | |
| <ArrowLeft className="mr-2 h-5 w-5" /> | |
| {t.backHome} | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } |