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