Files
encoach_frontend_new_v2/src/pages/admin/AdminTimetable.tsx
Yamen Ahmad 11a7265460 feat(v3): restructure project + add complete frontend
- Restructure: move backend from new_project/ to backend/
- Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks)
- Add docs/ with SRS specs, user stories, and workflow documentation
- Update .gitignore for new directory layout

Workflows implemented:
  WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration,
  WF4 General English Exam, WF5 Course Generation,
  WF6 Entity Student Onboarding, AI Course Generation,
  Adaptive Learning Engine UI, White-Label Branding, Score Release

Made-with: Cursor
2026-04-10 17:26:42 +04:00

147 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { useTimetable, useCourses } from "@/hooks/queries";
import { lmsService } from "@/services/lms.service";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useToast } from "@/hooks/use-toast";
import { Plus, Trash2 } from "lucide-react";
const days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"] as const;
const hours = ["08:00", "09:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00", "16:00", "17:00"];
const SESSION_COLOR = "hsl(192, 82%, 32%)";
export default function AdminTimetable() {
const { data: rawSessions, isLoading } = useTimetable();
const timetableSessions = Array.isArray(rawSessions) ? rawSessions : [];
const [createOpen, setCreateOpen] = useState(false);
const [selectedSession, setSelectedSession] = useState<(typeof timetableSessions)[number] | null>(null);
const [form, setForm] = useState({ course_id: "", day: "Monday" as string, start_time: "08:00", end_time: "09:00", room: "" });
const { toast } = useToast();
const qc = useQueryClient();
const { data: coursesData } = useCourses({ size: 200 });
const courses = coursesData?.items ?? [];
const createMutation = useMutation({
mutationFn: (data: Record<string, unknown>) => lmsService.createTimetableSession(data as never),
onSuccess: () => { qc.invalidateQueries({ queryKey: ["lms", "timetable"] }); setCreateOpen(false); toast({ title: "Session created" }); },
onError: (err: Error) => toast({ title: "Error", description: err.message, variant: "destructive" }),
});
const deleteMutation = useMutation({
mutationFn: (id: number) => lmsService.deleteTimetableSession(id),
onSuccess: () => { qc.invalidateQueries({ queryKey: ["lms", "timetable"] }); setSelectedSession(null); toast({ title: "Session deleted" }); },
onError: (err: Error) => toast({ title: "Error", description: err.message, variant: "destructive" }),
});
if (isLoading) 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>;
function isStart(day: string, hour: string) {
return timetableSessions.find(s => s.day === day && s.start_time === hour);
}
function getSessionAt(day: string, hour: string) {
return timetableSessions.find(s => s.day === day && s.start_time <= hour && s.end_time > hour);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div><h1 className="text-2xl font-bold">Master Timetable</h1><p className="text-muted-foreground">View and manage all scheduled sessions.</p></div>
<Button onClick={() => setCreateOpen(true)}><Plus className="mr-2 h-4 w-4" />Add Session</Button>
</div>
<Card>
<CardContent className="pt-6 overflow-x-auto">
<div className="min-w-[700px]">
<div className="grid grid-cols-6 gap-1">
<div className="text-xs font-medium text-muted-foreground p-2" />
{days.map(d => <div key={d} className="text-xs font-semibold text-center p-2 bg-muted rounded">{d}</div>)}
</div>
{hours.map(hour => (
<div key={hour} className="grid grid-cols-6 gap-1 min-h-[48px]">
<div className="text-xs text-muted-foreground p-2 flex items-start">{hour}</div>
{days.map(day => {
const session = isStart(day, hour);
const occupied = getSessionAt(day, hour);
if (session) {
return (
<div key={day} className="rounded-md p-2 text-xs text-white cursor-pointer hover:opacity-90" style={{ backgroundColor: SESSION_COLOR }} onClick={() => setSelectedSession(session)}>
<p className="font-semibold">{session.course_name}</p>
<p className="opacity-80">{session.batch_name}</p>
<p className="opacity-70">{session.teacher_name}</p>
<p className="opacity-70">{session.room}</p>
</div>
);
}
if (occupied) return <div key={day} />;
return <div key={day} className="border border-dashed border-border/50 rounded-md" />;
})}
</div>
))}
</div>
</CardContent>
</Card>
<Dialog open={!!selectedSession} onOpenChange={(v) => { if (!v) setSelectedSession(null); }}>
<DialogContent>
<DialogHeader><DialogTitle>Session Details</DialogTitle></DialogHeader>
{selectedSession && (
<div className="space-y-2 text-sm">
<p><span className="font-medium">Course:</span> {selectedSession.course_name}</p>
<p><span className="font-medium">Batch:</span> {selectedSession.batch_name}</p>
<p><span className="font-medium">Teacher:</span> {selectedSession.teacher_name}</p>
<p><span className="font-medium">Day:</span> {selectedSession.day}</p>
<p><span className="font-medium">Time:</span> {selectedSession.start_time} {selectedSession.end_time}</p>
<p><span className="font-medium">Room:</span> {selectedSession.room}</p>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setSelectedSession(null)}>Close</Button>
<Button variant="destructive" disabled={deleteMutation.isPending} onClick={() => { if (!selectedSession || !window.confirm("Delete this session?")) return; deleteMutation.mutate(selectedSession.id); }}>
<Trash2 className="mr-2 h-4 w-4" /> Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
<DialogContent>
<DialogHeader><DialogTitle>Add Timetable Session</DialogTitle></DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>Course *</Label>
<Select value={form.course_id || "__none__"} onValueChange={v => setForm(f => ({ ...f, course_id: v === "__none__" ? "" : v }))}>
<SelectTrigger><SelectValue placeholder="Select course" /></SelectTrigger>
<SelectContent>
<SelectItem value="__none__"> Select </SelectItem>
{courses.map(c => <SelectItem key={c.id} value={String(c.id)}>{c.title}</SelectItem>)}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Day</Label>
<Select value={form.day} onValueChange={v => setForm(f => ({ ...f, day: v }))}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>{days.map(d => <SelectItem key={d} value={d}>{d}</SelectItem>)}</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2"><Label>Start Time</Label><Input type="time" value={form.start_time} onChange={e => setForm(f => ({ ...f, start_time: e.target.value }))} /></div>
<div className="space-y-2"><Label>End Time</Label><Input type="time" value={form.end_time} onChange={e => setForm(f => ({ ...f, end_time: e.target.value }))} /></div>
</div>
<div className="space-y-2"><Label>Room</Label><Input value={form.room} onChange={e => setForm(f => ({ ...f, room: e.target.value }))} placeholder="e.g. Room A101" /></div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setCreateOpen(false)}>Cancel</Button>
<Button disabled={createMutation.isPending || !form.course_id} onClick={() => createMutation.mutate({ course_id: Number(form.course_id), day: form.day, start_time: form.start_time, end_time: form.end_time, room: form.room })}>Create</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}