Frontend - i18n: install tailwindcss-rtl, Cairo font, RTL-aware direction in index.css. - Language toggle: localize aria-label / menu label, persist choice, update document dir synchronously. - Sidebar: add `side` prop so the drawer pins to the right in RTL; wire up AdminLmsLayout, RoleLayout (student/teacher) and AppSidebar to pass side = i18n.dir() === 'rtl' ? 'right' : 'left'. - AdminLmsLayout: convert every nav item from hard-coded title to titleKey, translate group labels (incl. the collapsible Training), breadcrumbs, user menu (Profile / Settings / Logout), help button and toggle aria labels; replace physical mr-/right- utilities with logical me-/end-. - AI components (AiTipBanner, AiInsightsPanel, AiAlertBanner, AiSearchBar, AiAssistantDrawer): apply dir="auto" at the container level, localize titles, loading / error / empty states. - Dashboards (admin / student / teacher): wrap numeric values in <bdi>, localize dates via ar-EG, fix flex direction for KPI and assignment cards. - UI primitives (breadcrumb, calendar, carousel, dropdown-menu, menubar, context-menu, pagination, sidebar): flip chevrons in RTL via a scoped CSS rule, swap pl-/pr-/ml-/mr- for ps-/pe-/ms-/me-. - Add logical-direction helpers and bidirectional isolation classes. Locales - Expand en.ts and ar.ts with full `nav`, `sidebarGroup`, `breadcrumb`, `userMenu`, `chrome`, `ai`, and dashboard key sets; keep key parity. API client - `api-client.ts` reads the active language from localStorage/i18n and sends `Accept-Language` on every request so the backend can localize AI output. Backend (encoach_ai) - openai_service: add _LANGUAGE_NAMES, normalize_language, language-aware system prompt injection for every OpenAI call. - coach_service + controllers (coach_controller, ai_controller): thread the requested language from headers / user locale down to OpenAIService. - ai_feedback: fix latent registry error by pointing course_id at op.course instead of the non-existent encoach.course. Other - .gitignore: ignore runtime odoo logs and local caches. Made-with: Cursor
90 lines
4.8 KiB
TypeScript
90 lines
4.8 KiB
TypeScript
import { useParams, Link } from "react-router-dom";
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||
import { useBatches, useStudents, useTimetable } from "@/hooks/queries";
|
||
import { ArrowLeft, Users, Calendar, BookOpen } from "lucide-react";
|
||
|
||
export default function AdminBatchDetail() {
|
||
const { id } = useParams();
|
||
const batchId = id ? Number(id) : NaN;
|
||
const { data: batchesData, isLoading: lb } = useBatches();
|
||
const { data: studentsData, isLoading: ls } = useStudents(
|
||
Number.isFinite(batchId) ? { batch_id: batchId, size: 500 } : { size: 500 },
|
||
);
|
||
const { data: timetableData, isLoading: ltt } = useTimetable();
|
||
const batches = batchesData?.items ?? [];
|
||
const students = studentsData?.items ?? [];
|
||
const timetableSessions = Array.isArray(timetableData) ? timetableData : [];
|
||
if (lb || ls || ltt) return <div className="flex items-center justify-center min-h-[400px]"><div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" /></div>;
|
||
|
||
const batch = batches.find(b => String(b.id) === id);
|
||
|
||
if (!batch) return <div className="p-8 text-center text-muted-foreground">Batch not found.</div>;
|
||
|
||
const studentIds = Array.isArray(batch.student_ids) ? batch.student_ids : [];
|
||
const batchStudents = Number.isFinite(batchId)
|
||
? students.filter((s) => s.batch_id === batchId)
|
||
: students.filter((s) => studentIds.includes(s.id));
|
||
const coursePrefix = (batch.course_name || "").split(" ")[0];
|
||
const batchSessions = timetableSessions.filter(s =>
|
||
s.batch_name === batch.name || (coursePrefix && (s.course_name || "").includes(coursePrefix)),
|
||
);
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="flex items-center gap-3">
|
||
<Button variant="ghost" size="icon" asChild><Link to="/admin/batches"><ArrowLeft className="h-4 w-4 rtl:rotate-180" /></Link></Button>
|
||
<div>
|
||
<h1 className="text-2xl font-bold">{batch.name}</h1>
|
||
<p className="text-muted-foreground">{batch.course_name} · {batch.teacher_name}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4">
|
||
<Card><CardContent className="pt-4"><Users className="h-4 w-4 text-primary mb-1" /><p className="text-lg font-bold">{batch.student_count}/{batch.capacity}</p><p className="text-xs text-muted-foreground">Students</p></CardContent></Card>
|
||
<Card><CardContent className="pt-4"><Calendar className="h-4 w-4 text-info mb-1" /><p className="text-sm font-bold">{batch.schedule}</p><p className="text-xs text-muted-foreground">Schedule</p></CardContent></Card>
|
||
<Card><CardContent className="pt-4"><BookOpen className="h-4 w-4 text-success mb-1" /><p className="text-sm font-bold">{batch.start_date} — {batch.end_date}</p><p className="text-xs text-muted-foreground">Duration</p></CardContent></Card>
|
||
<Card><CardContent className="pt-4"><Badge variant={batch.status === "active" ? "default" : "secondary"} className="capitalize">{batch.status}</Badge><p className="text-xs text-muted-foreground mt-1">Status</p></CardContent></Card>
|
||
</div>
|
||
|
||
<Card>
|
||
<CardHeader><CardTitle className="text-lg">Enrolled Students</CardTitle></CardHeader>
|
||
<CardContent>
|
||
<Table>
|
||
<TableHeader><TableRow><TableHead>Student</TableHead><TableHead>Attendance</TableHead><TableHead>Avg Grade</TableHead><TableHead>Risk</TableHead></TableRow></TableHeader>
|
||
<TableBody>
|
||
{batchStudents.map(s => (
|
||
<TableRow key={s.id}>
|
||
<TableCell className="font-medium">{s.name}</TableCell>
|
||
<TableCell>—</TableCell>
|
||
<TableCell>—</TableCell>
|
||
<TableCell><Badge variant="secondary" className="capitalize">—</Badge></TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader><CardTitle className="text-lg">Timetable</CardTitle></CardHeader>
|
||
<CardContent className="space-y-2">
|
||
{batchSessions.length === 0 ? (
|
||
<p className="text-muted-foreground text-sm">No sessions scheduled.</p>
|
||
) : batchSessions.map(s => (
|
||
<div key={s.id} className="flex items-center justify-between p-3 rounded border">
|
||
<div>
|
||
<p className="text-sm font-medium">{s.day} {s.start_time}–{s.end_time}</p>
|
||
<p className="text-xs text-muted-foreground">{s.room}</p>
|
||
</div>
|
||
<span className="text-xs px-2 py-1 rounded text-white bg-primary">{s.course_name}</span>
|
||
</div>
|
||
))}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|