feat: add LAB and Assignment marks fetching with lazy loading and system protection

This commit is contained in:
Shanmuga Krishnan S M
2026-08-19 14:58:59 +05:30
parent b712b1e2f1
commit bbb2cf7d30
2 changed files with 563 additions and 302 deletions

View File

@@ -6,7 +6,7 @@ import {
Sparkles, KeyRound, ArrowRight, LogOut, Sparkles, KeyRound, ArrowRight, LogOut,
AlertCircle, User, HelpCircle, Compass, ChevronRight, ExternalLink, AlertCircle, User, HelpCircle, Compass, ChevronRight, ExternalLink,
Award, CheckCircle2, AlertTriangle, FileText, Calculator, TrendingUp, Award, CheckCircle2, AlertTriangle, FileText, Calculator, TrendingUp,
Layers, BookOpen, Check, Mail, Phone, RefreshCw, Edit2, Save, FlaskConical, Search, RotateCw Layers, BookOpen, Check, Mail, Phone, RefreshCw, Edit2, Save, FlaskConical
} from 'lucide-react'; } from 'lucide-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
@@ -21,11 +21,15 @@ import {
fetchImsTimetable, fetchImsTimetable,
fetchImsAttendance, fetchImsAttendance,
fetchImsCatMarks, fetchImsCatMarks,
fetchImsAssignmentMarks,
fetchImsLabMarks,
fetchImsSemesterResults, fetchImsSemesterResults,
type StudentProfile, type StudentProfile,
type WeeklySchedule, type WeeklySchedule,
type AttendanceReport, type AttendanceReport,
type CatMarksReport, type CatMarksReport,
type AssignmentMarkItem,
type LabMarkItem,
type SemesterResult type SemesterResult
} from '@/services/imsService'; } from '@/services/imsService';
@@ -35,15 +39,17 @@ export default function StudentDashboard() {
// Navigation State // Navigation State
const [activeTab, setActiveTab] = useState<'timetable' | 'attendance' | 'marks' | 'grades' | 'faculties'>('timetable'); const [activeTab, setActiveTab] = useState<'timetable' | 'attendance' | 'marks' | 'grades' | 'faculties'>('timetable');
const [selectedDayFilter, setSelectedDayFilter] = useState<'all' | 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday'>('all'); const [selectedDay, setSelectedDay] = useState<'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday'>('monday');
const [ttSearchQuery, setTtSearchQuery] = useState('');
const [selectedSemFilter, setSelectedSemFilter] = useState<number>(0); const [selectedSemFilter, setSelectedSemFilter] = useState<number>(0);
const [marksSubTab, setMarksSubTab] = useState<'cat' | 'assignment' | 'lab'>('cat');
// Loaded Data States // Loaded Data States
const [profile, setProfile] = useState<StudentProfile | null>(session?.profile || null); const [profile, setProfile] = useState<StudentProfile | null>(session?.profile || null);
const [timetable, setTimetable] = useState<WeeklySchedule | null>(null); const [timetable, setTimetable] = useState<WeeklySchedule | null>(null);
const [attendance, setAttendance] = useState<AttendanceReport | null>(null); const [attendance, setAttendance] = useState<AttendanceReport | null>(null);
const [catMarks, setCatMarks] = useState<CatMarksReport | null>(null); const [catMarks, setCatMarks] = useState<CatMarksReport | null>(null);
const [assignmentMarks, setAssignmentMarks] = useState<AssignmentMarkItem[] | null>(null);
const [labMarks, setLabMarks] = useState<LabMarkItem[] | null>(null);
const [gradesData, setGradesData] = useState<{ results: SemesterResult[]; cgpa: number; totalCredits: number; activeArrears: number } | null>(null); const [gradesData, setGradesData] = useState<{ results: SemesterResult[]; cgpa: number; totalCredits: number; activeArrears: number } | null>(null);
// Loading States // Loading States
@@ -68,6 +74,13 @@ export default function StudentDashboard() {
} }
}, [session?.token]); }, [session?.token]);
// Set default day to today (Mon-Fri)
useEffect(() => {
const dayNames: (keyof WeeklySchedule)[] = ['monday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'friday'];
const today = dayNames[new Date().getDay()] || 'monday';
setSelectedDay(today);
}, []);
const loadInitialData = async (token: string, force = false) => { const loadInitialData = async (token: string, force = false) => {
setIsInitialLoading(true); setIsInitialLoading(true);
setError(null); setError(null);
@@ -76,6 +89,18 @@ export default function StudentDashboard() {
const p = await fetchImsProfile(token, reg, force); const p = await fetchImsProfile(token, reg, force);
setProfile(p); setProfile(p);
const tt = await fetchImsTimetable(token, p.registerNumber, force); const tt = await fetchImsTimetable(token, p.registerNumber, force);
setTimetable(tt);
const dayNames: (keyof WeeklySchedule)[] = ['monday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'friday'];
const today = dayNames[new Date().getDay()] || 'monday';
if (tt && tt[today] && tt[today].length > 0) {
setSelectedDay(today);
} else if (tt) {
const firstActive = (['monday', 'tuesday', 'wednesday', 'thursday', 'friday'] as const).find(
d => tt[d] && tt[d].length > 0
);
if (firstActive) setSelectedDay(firstActive);
}
} catch (err: any) { } catch (err: any) {
console.error('Error loading initial IMS data:', err); console.error('Error loading initial IMS data:', err);
setError(err.message || 'Failed to load timetable.'); setError(err.message || 'Failed to load timetable.');
@@ -100,13 +125,19 @@ export default function StudentDashboard() {
}; };
const ensureCatMarksLoaded = async (force = false) => { const ensureCatMarksLoaded = async (force = false) => {
if ((catMarks && !force) || !session?.token || !profile) return; if ((catMarks && assignmentMarks && labMarks && !force) || !session?.token || !profile) return;
setIsTabLoading(true); setIsTabLoading(true);
try { try {
const data = await fetchImsCatMarks(session.token, profile.registerNumber, force); const [cat, assign, lab] = await Promise.all([
setCatMarks(data); fetchImsCatMarks(session.token, profile.registerNumber, force),
fetchImsAssignmentMarks(),
fetchImsLabMarks(),
]);
setCatMarks(cat);
setAssignmentMarks(assign);
setLabMarks(lab);
} catch (err) { } catch (err) {
console.warn('CAT marks load notice:', err); console.warn('Marks load notice:', err);
} finally { } finally {
setIsTabLoading(false); setIsTabLoading(false);
} }
@@ -354,6 +385,7 @@ export default function StudentDashboard() {
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// VIEW 2: AUTHENTICATED STUDENT DASHBOARD // VIEW 2: AUTHENTICATED STUDENT DASHBOARD
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
const currentPeriods = (timetable && timetable[selectedDay]) || [];
const filteredGrades = gradesData?.results ? ( const filteredGrades = gradesData?.results ? (
selectedSemFilter === 0 selectedSemFilter === 0
? gradesData.results ? gradesData.results
@@ -712,197 +744,109 @@ export default function StudentDashboard() {
{/* ─── 1. TIMETABLE TAB ─── */} {/* ─── 1. TIMETABLE TAB ─── */}
{activeTab === 'timetable' && ( {activeTab === 'timetable' && (
<div className="space-y-6"> <div className="space-y-6">
{/* Header Title & Subtitle */} {/* Day Switcher */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-white p-6 rounded-3xl border border-[#E5E7EB] shadow-sm">
<div>
<h2 className="text-2xl font-extrabold text-[#1E293B] tracking-tight">Class Time Table</h2>
<p className="text-xs font-semibold text-[#64748B] mt-1">
Weekly class schedule for Class : <span className="text-[#1E293B] font-bold">{profile?.department || 'B.Tech. AI & DS'} / {profile?.semester || 5} / E</span>
</p>
</div>
<div className="flex items-center gap-2">
<span className="px-3.5 py-1.5 rounded-full text-xs font-bold bg-emerald-50 text-emerald-700 border border-emerald-200 flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
CLASS Class : {profile?.department || 'B.Tech. AI & DS'} / {profile?.semester || 5} / E
</span>
</div>
</div>
{/* Toolbar: Day Filter Pills, Search Bar, Refresh */}
<div className="flex flex-wrap items-center justify-between gap-3 bg-white p-4 rounded-2xl border border-[#E5E7EB] shadow-sm"> <div className="flex flex-wrap items-center justify-between gap-3 bg-white p-4 rounded-2xl border border-[#E5E7EB] shadow-sm">
{/* Day Filter Pills */} <div className="flex items-center gap-2 overflow-x-auto py-1">
<div className="flex items-center gap-1.5 overflow-x-auto py-1"> {(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'] as const).map((dayKey) => {
{[ const count = timetable?.[dayKey]?.length || 0;
{ key: 'all', label: 'All Days' }, const isSelected = selectedDay === dayKey;
{ key: 'monday', label: 'Mon' },
{ key: 'tuesday', label: 'Tue' },
{ key: 'wednesday', label: 'Wed' },
{ key: 'thursday', label: 'Thu' },
{ key: 'friday', label: 'Fri' },
{ key: 'saturday', label: 'Sat' },
].map((day) => {
const isSelected = selectedDayFilter === day.key;
const currentDayName = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'][new Date().getDay()];
const isToday = day.key === currentDayName;
return ( return (
<button <button
key={day.key} key={dayKey}
onClick={() => setSelectedDayFilter(day.key as any)} onClick={() => setSelectedDay(dayKey)}
className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-xs font-bold transition-all cursor-pointer ${ className={`flex items-center gap-2 px-3.5 py-1.5 rounded-xl text-xs font-bold transition-all cursor-pointer capitalize ${
isSelected isSelected
? 'bg-gradient-to-r from-[#FF6B00] to-[#F97316] text-white shadow-md shadow-orange-500/20' ? 'bg-gradient-to-r from-[#FF6B00] to-[#F97316] text-white shadow-md shadow-orange-500/20'
: 'bg-slate-100 hover:bg-slate-200 text-[#64748B]' : 'bg-slate-100 hover:bg-slate-200 text-[#64748B]'
}`} }`}
> >
<span>{day.label}</span> <span>{dayKey}</span>
{isToday && ( <span className={`px-1.5 py-0.2 rounded-md text-[10px] font-mono ${isSelected ? 'bg-white/20 text-white' : 'bg-white text-[#64748B] border border-[#E5E7EB]'}`}>
<span className={`w-1.5 h-1.5 rounded-full ${isSelected ? 'bg-white' : 'bg-[#F97316]'}`} /> {count}
)} </span>
</button> </button>
); );
})} })}
</div> </div>
{/* Right Toolbar Controls */} <span className="text-xs text-emerald-600 font-bold flex items-center gap-1.5">
<div className="flex items-center gap-3 w-full sm:w-auto"> <span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
<div className="relative flex-1 sm:flex-initial"> Live IMS Timetable
<Search className="w-4 h-4 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" /> </span>
<input
type="text"
value={ttSearchQuery}
onChange={(e) => setTtSearchQuery(e.target.value)}
placeholder="Search subject or faculty"
className="w-full sm:w-64 pl-9 pr-3 py-1.5 rounded-xl text-xs bg-slate-50 border border-[#E5E7EB] focus:outline-none focus:border-[#F97316] text-gray-800 placeholder-gray-400 font-medium"
/>
</div> </div>
<button
onClick={() => session?.token && loadInitialData(session.token, true)}
className="flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-xs font-bold bg-slate-100 hover:bg-slate-200 text-[#64748B] transition-all cursor-pointer"
>
<RotateCw className="w-3.5 h-3.5" />
<span>Refresh</span>
</button>
</div>
</div>
{/* Timetable Grid Matrix */}
{isInitialLoading ? ( {isInitialLoading ? (
<div className="flex flex-col items-center justify-center py-20 gap-3 bg-white rounded-3xl border border-[#E5E7EB]"> <div className="flex flex-col items-center justify-center py-20 gap-3 bg-white rounded-3xl border border-[#E5E7EB]">
<div className="w-8 h-8 border-3 border-[#F97316] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-3 border-[#F97316] border-t-transparent rounded-full animate-spin" />
<p className="text-xs text-[#64748B]">Fetching timetable from RIT IMS...</p> <p className="text-xs text-[#64748B]">Fetching timetable from RIT IMS...</p>
</div> </div>
) : ( ) : currentPeriods.length === 0 ? (
<div className="overflow-x-auto rounded-3xl border border-[#E5E7EB] bg-white shadow-sm"> <div className="bg-white border border-[#E5E7EB] rounded-3xl p-12 text-center space-y-3">
<table className="w-full text-left border-collapse min-w-[900px]"> <div className="w-12 h-12 rounded-2xl bg-orange-50 text-[#F97316] flex items-center justify-center mx-auto">
<thead> <Calendar className="w-6 h-6" />
<tr className="bg-slate-50 border-b border-[#E5E7EB]">
<th className="py-4 px-4 text-center text-xs font-bold text-gray-500 uppercase tracking-wider w-36 border-r border-[#E5E7EB]">
Period
</th>
{(['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'] as const)
.filter(d => selectedDayFilter === 'all' || selectedDayFilter === d)
.map((dayKey) => {
const currentDayName = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'][new Date().getDay()];
const isToday = dayKey === currentDayName;
return (
<th key={dayKey} className="py-4 px-4 text-center text-xs font-extrabold text-gray-800 capitalize border-r border-[#E5E7EB] last:border-r-0 min-w-[160px]">
<div className="flex items-center justify-center gap-1.5">
<span>{dayKey}</span>
{isToday && (
<span className="px-1.5 py-0.5 rounded text-[9px] font-bold bg-emerald-500 text-white uppercase tracking-wider">
TODAY
</span>
)}
</div> </div>
</th> <h3 className="text-base font-bold text-[#1E293B]">
); No classes scheduled for {selectedDay.toUpperCase()}
})} </h3>
</tr> <p className="text-xs text-[#64748B] max-w-sm mx-auto">
</thead> Select another day of the week above to view your timetable periods and staff.
</p>
<tbody className="divide-y divide-[#E5E7EB]"> </div>
{[1, 2, 3, 4, 5, 6, 7].map((pNum) => { ) : (
const timeSlots = [ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
'08:45 - 09:40', {currentPeriods.map((period) => (
'09:40 - 10:35', <motion.div
'10:50 - 11:45', key={period.periodNumber}
'11:45 - 12:40', initial={{ opacity: 0, y: 10 }}
'01:30 - 02:25', animate={{ opacity: 1, y: 0 }}
'02:25 - 03:20', className="bg-white rounded-2xl p-5 border border-[#E5E7EB] hover:border-slate-300 shadow-sm transition-all"
'03:20 - 04:15',
];
const slotTime = timeSlots[pNum - 1] || '';
const activeDays = (['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'] as const)
.filter(d => selectedDayFilter === 'all' || selectedDayFilter === d);
return (
<tr key={pNum} className="hover:bg-slate-50/50 transition-colors">
{/* Period Column */}
<td className="py-4 px-3 text-center border-r border-[#E5E7EB] bg-slate-50/30 align-middle">
<div className="font-extrabold text-xs text-emerald-600">Period {pNum}</div>
<div className="text-[10px] font-medium text-gray-400 mt-0.5">{slotTime}</div>
</td>
{/* Days Columns */}
{activeDays.map((dayKey) => {
const daySched = timetable?.[dayKey];
const rawPeriods = daySched?.[pNum] || [];
const periods = rawPeriods.filter((p) => {
if (!ttSearchQuery) return true;
const q = ttSearchQuery.toLowerCase();
return (
p.subjectName.toLowerCase().includes(q) ||
p.subjectCode.toLowerCase().includes(q) ||
p.staffName.toLowerCase().includes(q)
);
});
return (
<td key={dayKey} className="p-2.5 border-r border-[#E5E7EB] last:border-r-0 align-top min-w-[170px]">
{periods.length > 0 ? (
<div className="space-y-2">
{periods.map((p, idx) => (
<div
key={idx}
className="bg-[#F8FAFC] hover:bg-slate-100 p-3 rounded-2xl border border-[#E5E7EB] shadow-xs transition-all space-y-1"
> >
<div className="font-bold text-xs text-gray-900 leading-tight"> {/* Header */}
{p.subjectName} <div className="flex items-center justify-between gap-2 mb-3">
</div> <div className="flex items-center gap-2">
<div className="flex items-center gap-1.5 flex-wrap"> <span className="w-6 h-6 rounded-lg bg-slate-100 text-[#1E293B] font-bold text-xs flex items-center justify-center">
<span className={`inline-block text-[10px] font-bold px-1.5 py-0.5 rounded-md ${ P{period.periodNumber}
p.type === 'LAB' </span>
? 'bg-purple-50 text-purple-700 border border-purple-200' <span className="text-xs font-semibold text-[#64748B] flex items-center gap-1">
: 'bg-emerald-50 text-emerald-700 border border-emerald-200' <Clock className="w-3 h-3 text-[#94A3B8]" />
}`}> {period.timeSlot}
{p.subjectCode || p.type}
</span> </span>
</div> </div>
{p.staffName && (
<div className="text-[11px] font-medium text-gray-500 truncate"> <span
{p.staffName} className={`px-2.5 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-wider flex items-center gap-1 ${
period.type === 'LAB'
? 'bg-purple-100 text-purple-700 border border-purple-300'
: 'bg-blue-100 text-blue-700 border border-blue-300'
}`}
>
{period.type === 'LAB' ? <FlaskConical className="w-3 h-3 text-purple-600" /> : <BookOpen className="w-3 h-3 text-blue-600" />}
{period.type}
</span>
</div> </div>
{/* Subject Details */}
<div className="space-y-1.5 mb-4">
<div className="flex items-center gap-2">
<span className="text-[11px] font-mono font-bold text-[#F97316]">
{period.subjectCode}
</span>
</div>
<h3 className="text-base font-extrabold text-[#1E293B] line-clamp-1" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
{period.subjectName}
</h3>
</div>
{/* Faculty Footer */}
<div className="pt-3 border-t border-[#E5E7EB] flex items-center justify-between text-xs text-[#475569]">
<span className="truncate pr-2 font-medium">{period.staffName || 'Faculty Instructor'}</span>
{period.staffCode && (
<span className="font-mono text-[10px] text-[#94A3B8]">{period.staffCode}</span>
)} )}
</div> </div>
</motion.div>
))} ))}
</div> </div>
) : (
<div className="flex items-center justify-center h-full min-h-[64px] text-gray-300 font-bold text-sm select-none">
—
</div>
)}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</div>
)} )}
</div> </div>
)} )}
@@ -980,16 +924,55 @@ export default function StudentDashboard() {
</div> </div>
)} )}
{/* ─── 3. CAT MARKS TAB (LAZY LOADED) ─── */} {/* ─── 3. INTERNAL MARKS TAB (CAT, ASSIGNMENT & LAB MARKS) ─── */}
{activeTab === 'marks' && ( {activeTab === 'marks' && (
<div className="space-y-6"> <div className="space-y-6">
{/* Sub-Tab Selector */}
<div className="flex items-center gap-2 bg-white p-2 rounded-2xl border border-[#E5E7EB] shadow-sm w-fit">
<button
onClick={() => setMarksSubTab('cat')}
className={`px-4 py-2 rounded-xl text-xs font-bold transition-all cursor-pointer ${
marksSubTab === 'cat'
? 'bg-[#F97316] text-white shadow-sm'
: 'text-[#64748B] hover:bg-slate-100'
}`}
>
CAT Marks
</button>
<button
onClick={() => setMarksSubTab('assignment')}
className={`px-4 py-2 rounded-xl text-xs font-bold transition-all cursor-pointer ${
marksSubTab === 'assignment'
? 'bg-[#F97316] text-white shadow-sm'
: 'text-[#64748B] hover:bg-slate-100'
}`}
>
Assignment Marks
</button>
<button
onClick={() => setMarksSubTab('lab')}
className={`px-4 py-2 rounded-xl text-xs font-bold transition-all cursor-pointer ${
marksSubTab === 'lab'
? 'bg-[#F97316] text-white shadow-sm'
: 'text-[#64748B] hover:bg-slate-100'
}`}
>
LAB Marks
</button>
</div>
{isTabLoading && !catMarks ? ( {isTabLoading && !catMarks ? (
<div className="flex flex-col items-center justify-center py-20 gap-3 bg-white rounded-3xl border border-[#E5E7EB]"> <div className="flex flex-col items-center justify-center py-20 gap-3 bg-white rounded-3xl border border-[#E5E7EB]">
<div className="w-8 h-8 border-3 border-[#F97316] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-3 border-[#F97316] border-t-transparent rounded-full animate-spin" />
<p className="text-xs text-[#64748B]">Fetching CAT marks on demand...</p> <p className="text-xs text-[#64748B]">Fetching internal marks on demand...</p>
</div> </div>
) : catMarks ? ( ) : (
<div className="bg-white border border-[#E5E7EB] rounded-3xl p-6 shadow-sm"> <div className="bg-white border border-[#E5E7EB] rounded-3xl p-6 shadow-sm">
{/* SUB TAB 1: CAT MARKS */}
{marksSubTab === 'cat' && catMarks && (
<div>
<div className="mb-6"> <div className="mb-6">
<h3 className="text-lg font-extrabold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}> <h3 className="text-lg font-extrabold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
Continuous Assessment Test (CAT) Marks Continuous Assessment Test (CAT) Marks
@@ -1026,7 +1009,103 @@ export default function StudentDashboard() {
</table> </table>
</div> </div>
</div> </div>
) : null} )}
{/* SUB TAB 2: ASSIGNMENT MARKS */}
{marksSubTab === 'assignment' && (
<div>
<div className="mb-6">
<h3 className="text-lg font-extrabold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
Assignment Marks
</h3>
<p className="text-xs text-[#64748B]">Continuous assignment submissions (10 Marks per assignment)</p>
</div>
{assignmentMarks && assignmentMarks.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-left text-xs">
<thead>
<tr className="border-b border-[#E5E7EB] text-[#94A3B8] uppercase font-bold">
<th className="py-3 px-4">Subject Code</th>
<th className="py-3 px-4">Subject Title</th>
<th className="py-3 px-4">Handling Faculty</th>
<th className="py-3 px-3 text-center">Assgn 1</th>
<th className="py-3 px-3 text-center">Assgn 2</th>
<th className="py-3 px-3 text-center">Assgn 3</th>
<th className="py-3 px-3 text-center">Assgn 4</th>
<th className="py-3 px-3 text-center">Assgn 5</th>
<th className="py-3 px-4 text-center">Total (50)</th>
</tr>
</thead>
<tbody className="divide-y divide-[#E5E7EB]">
{assignmentMarks.map((item, idx) => (
<tr key={idx} className="hover:bg-slate-50 transition-colors">
<td className="py-3.5 px-4 font-mono font-bold text-[#F97316]">{item.subjectCode}</td>
<td className="py-3.5 px-4 font-semibold text-[#1E293B]">{item.subjectName}</td>
<td className="py-3.5 px-4 font-medium text-[#475569]">{item.facultyName || 'Faculty In-Charge'}</td>
<td className="py-3.5 px-3 text-center font-bold text-[#1E293B]">{item.a1}</td>
<td className="py-3.5 px-3 text-center font-bold text-[#1E293B]">{item.a2}</td>
<td className="py-3.5 px-3 text-center font-bold text-[#1E293B]">{item.a3}</td>
<td className="py-3.5 px-3 text-center font-bold text-[#1E293B]">{item.a4}</td>
<td className="py-3.5 px-3 text-center font-bold text-[#1E293B]">{item.a5}</td>
<td className="py-3.5 px-4 text-center font-bold text-emerald-600">{item.total}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="py-12 text-center text-xs text-[#64748B]">
No Assignment Marks posted by faculty yet for this semester.
</div>
)}
</div>
)}
{/* SUB TAB 3: LAB MARKS */}
{marksSubTab === 'lab' && (
<div>
<div className="mb-6">
<h3 className="text-lg font-extrabold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>
Laboratory Practical Marks
</h3>
<p className="text-xs text-[#64748B]">Practical course evaluations & lab experiment marks</p>
</div>
{labMarks && labMarks.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-left text-xs">
<thead>
<tr className="border-b border-[#E5E7EB] text-[#94A3B8] uppercase font-bold">
<th className="py-3 px-4">Subject Code</th>
<th className="py-3 px-4">Subject Title</th>
<th className="py-3 px-4">Handling Faculty</th>
<th className="py-3 px-4 text-center">Marks</th>
<th className="py-3 px-4 text-center">Total</th>
</tr>
</thead>
<tbody className="divide-y divide-[#E5E7EB]">
{labMarks.map((item, idx) => (
<tr key={idx} className="hover:bg-slate-50 transition-colors">
<td className="py-3.5 px-4 font-mono font-bold text-[#F97316]">{item.subjectCode}</td>
<td className="py-3.5 px-4 font-semibold text-[#1E293B]">{item.subjectName}</td>
<td className="py-3.5 px-4 font-medium text-[#475569]">{item.facultyName || 'Faculty In-Charge'}</td>
<td className="py-3.5 px-4 text-center font-bold text-[#1E293B]">{item.marks}</td>
<td className="py-3.5 px-4 text-center font-bold text-emerald-600">{item.total}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="py-12 text-center text-xs text-[#64748B]">
No Lab Marks posted by faculty yet for this semester.
</div>
)}
</div>
)}
</div>
)}
</div> </div>
)} )}

View File

@@ -28,15 +28,12 @@ export interface TimetablePeriod {
type: 'THEORY' | 'LAB'; type: 'THEORY' | 'LAB';
} }
export type DaySchedule = Record<number, TimetablePeriod[]>;
export interface WeeklySchedule { export interface WeeklySchedule {
monday: DaySchedule; monday: TimetablePeriod[];
tuesday: DaySchedule; tuesday: TimetablePeriod[];
wednesday: DaySchedule; wednesday: TimetablePeriod[];
thursday: DaySchedule; thursday: TimetablePeriod[];
friday: DaySchedule; friday: TimetablePeriod[];
saturday: DaySchedule;
} }
export interface SubjectAttendance { export interface SubjectAttendance {
@@ -70,6 +67,26 @@ export interface CatMarksReport {
subjects: SubjectCatMark[]; subjects: SubjectCatMark[];
} }
export interface AssignmentMarkItem {
subjectName: string;
subjectCode: string;
facultyName: string;
a1: string;
a2: string;
a3: string;
a4: string;
a5: string;
total: string;
}
export interface LabMarkItem {
subjectName: string;
subjectCode: string;
facultyName: string;
marks: string;
total: string;
}
export interface SemesterGrade { export interface SemesterGrade {
code: string; code: string;
name: string; name: string;
@@ -238,17 +255,8 @@ export function parseDepartmentCode(rawDept: string = ''): string {
} }
export function generateCurriculumSchedule(deptCode: string, semester: number): WeeklySchedule { export function generateCurriculumSchedule(deptCode: string, semester: number): WeeklySchedule {
const createDay = (list: TimetablePeriod[]): DaySchedule => {
const day: DaySchedule = { 1: [], 2: [], 3: [], 4: [], 5: [], 6: [], 7: [] };
list.forEach(p => {
if (!day[p.periodNumber]) day[p.periodNumber] = [];
day[p.periodNumber].push(p);
});
return day;
};
return { return {
monday: createDay([ monday: [
{ periodNumber: 1, timeSlot: '08:45 AM - 09:40 AM', startTime: '08:45', endTime: '09:40', subjectCode: 'AD23511', subjectName: 'Deep Learning', staffName: 'ARTHI A. (CS87)', type: 'THEORY' }, { periodNumber: 1, timeSlot: '08:45 AM - 09:40 AM', startTime: '08:45', endTime: '09:40', subjectCode: 'AD23511', subjectName: 'Deep Learning', staffName: 'ARTHI A. (CS87)', type: 'THEORY' },
{ periodNumber: 2, timeSlot: '09:40 AM - 10:35 AM', startTime: '09:40', endTime: '10:35', subjectCode: 'AD23V12', subjectName: 'Big Data Analytics', staffName: 'KAVITHA V (CS253)', type: 'THEORY' }, { periodNumber: 2, timeSlot: '09:40 AM - 10:35 AM', startTime: '09:40', endTime: '10:35', subjectCode: 'AD23V12', subjectName: 'Big Data Analytics', staffName: 'KAVITHA V (CS253)', type: 'THEORY' },
{ periodNumber: 3, timeSlot: '10:50 AM - 11:45 AM', startTime: '10:50', endTime: '11:45', subjectCode: 'CS23511', subjectName: 'Computer Networks', staffName: 'SELVAKUMARAN S. (CS91)', type: 'THEORY' }, { periodNumber: 3, timeSlot: '10:50 AM - 11:45 AM', startTime: '10:50', endTime: '11:45', subjectCode: 'CS23511', subjectName: 'Computer Networks', staffName: 'SELVAKUMARAN S. (CS91)', type: 'THEORY' },
@@ -256,16 +264,17 @@ export function generateCurriculumSchedule(deptCode: string, semester: number):
{ periodNumber: 5, timeSlot: '01:30 PM - 02:25 PM', startTime: '13:30', endTime: '14:25', subjectCode: 'CB23531', subjectName: 'Business Analytics', staffName: 'FOUZIA SULTHANA K. (CS84)', type: 'THEORY' }, { periodNumber: 5, timeSlot: '01:30 PM - 02:25 PM', startTime: '13:30', endTime: '14:25', subjectCode: 'CB23531', subjectName: 'Business Analytics', staffName: 'FOUZIA SULTHANA K. (CS84)', type: 'THEORY' },
{ periodNumber: 6, timeSlot: '02:25 PM - 03:20 PM', startTime: '14:25', endTime: '15:20', subjectCode: 'AD23532', subjectName: 'Data Exploration & Visualization', staffName: 'DR.VIDHYA M (CS256)', type: 'THEORY' }, { periodNumber: 6, timeSlot: '02:25 PM - 03:20 PM', startTime: '14:25', endTime: '15:20', subjectCode: 'AD23532', subjectName: 'Data Exploration & Visualization', staffName: 'DR.VIDHYA M (CS256)', type: 'THEORY' },
{ periodNumber: 7, timeSlot: '03:20 PM - 04:15 PM', startTime: '15:20', endTime: '16:15', subjectCode: 'MENT', subjectName: 'Mentoring & Library', staffName: 'ARTHI A. (CS87)', type: 'THEORY' }, { periodNumber: 7, timeSlot: '03:20 PM - 04:15 PM', startTime: '15:20', endTime: '16:15', subjectCode: 'MENT', subjectName: 'Mentoring & Library', staffName: 'ARTHI A. (CS87)', type: 'THEORY' },
]), ],
tuesday: createDay([ tuesday: [
{ periodNumber: 1, timeSlot: '08:45 AM - 09:40 AM', startTime: '08:45', endTime: '09:40', subjectCode: 'AD23V15', subjectName: 'Image and Video Analytics', staffName: 'JOEL J (CS226)', type: 'THEORY' }, { periodNumber: 1, timeSlot: '08:45 AM - 09:40 AM', startTime: '08:45', endTime: '09:40', subjectCode: 'AD23V15', subjectName: 'Image and Video Analytics', staffName: 'JOEL J (CS226)', type: 'THEORY' },
{ periodNumber: 2, timeSlot: '09:40 AM - 10:35 AM', startTime: '09:40', endTime: '10:35', subjectCode: 'AD23532', subjectName: 'Data Exploration & Visualization', staffName: 'DR.VIDHYA M (CS256)', type: 'THEORY' }, { periodNumber: 2, timeSlot: '09:40 AM - 10:35 AM', startTime: '09:40', endTime: '10:35', subjectCode: 'AD23532', subjectName: 'Data Exploration & Visualization', staffName: 'DR.VIDHYA M (CS256)', type: 'THEORY' },
{ periodNumber: 3, timeSlot: '10:50 AM - 11:45 AM', startTime: '10:50', endTime: '11:45', subjectCode: 'AD23511', subjectName: 'Deep Learning', staffName: 'ARTHI A. (CS87)', type: 'THEORY' }, { periodNumber: 3, timeSlot: '10:50 AM - 11:45 AM', startTime: '10:50', endTime: '11:45', subjectCode: 'AD23511', subjectName: 'Deep Learning', staffName: 'ARTHI A. (CS87)', type: 'THEORY' },
{ periodNumber: 4, timeSlot: '11:45 AM - 12:40 PM', startTime: '11:45', endTime: '12:40', subjectCode: 'CS23521', subjectName: 'Computer Networks Laboratory', staffName: 'SELVAKUMARAN S. (CS91)', type: 'LAB' }, { periodNumber: 4, timeSlot: '11:45 AM - 12:40 PM', startTime: '11:45', endTime: '12:40', subjectCode: 'CS23521', subjectName: 'Computer Networks Laboratory', staffName: 'SELVAKUMARAN S. (CS91)', type: 'LAB' },
{ periodNumber: 5, timeSlot: '01:30 PM - 02:25 PM', startTime: '13:30', endTime: '14:25', subjectCode: 'CS23521', subjectName: 'Computer Networks Laboratory', staffName: 'SELVAKUMARAN S. (CS91)', type: 'LAB' }, { periodNumber: 5, timeSlot: '01:30 PM - 02:25 PM', startTime: '13:30', endTime: '14:25', subjectCode: 'CS23521', subjectName: 'Computer Networks Laboratory', staffName: 'SELVAKUMARAN S. (CS91)', type: 'LAB' },
{ periodNumber: 6, timeSlot: '02:25 PM - 03:20 PM', startTime: '14:25', endTime: '15:20', subjectCode: 'CB23531', subjectName: 'Business Analytics', staffName: 'FOUZIA SULTHANA K. (CS84)', type: 'THEORY' }, { periodNumber: 6, timeSlot: '02:25 PM - 03:20 PM', startTime: '14:25', endTime: '15:20', subjectCode: 'CB23531', subjectName: 'Business Analytics', staffName: 'FOUZIA SULTHANA K. (CS84)', type: 'THEORY' },
]), { periodNumber: 7, timeSlot: '03:20 PM - 04:15 PM', startTime: '15:20', endTime: '16:15', subjectCode: 'MENT', subjectName: 'Mentoring & Guidance', staffName: 'FOUZIA SULTHANA K. (CS84)', type: 'THEORY' },
wednesday: createDay([ ],
wednesday: [
{ periodNumber: 1, timeSlot: '08:45 AM - 09:40 AM', startTime: '08:45', endTime: '09:40', subjectCode: 'AD23521', subjectName: 'Deep Learning Laboratory', staffName: 'ARTHI A. (CS87)', type: 'LAB' }, { periodNumber: 1, timeSlot: '08:45 AM - 09:40 AM', startTime: '08:45', endTime: '09:40', subjectCode: 'AD23521', subjectName: 'Deep Learning Laboratory', staffName: 'ARTHI A. (CS87)', type: 'LAB' },
{ periodNumber: 2, timeSlot: '09:40 AM - 10:35 AM', startTime: '09:40', endTime: '10:35', subjectCode: 'AD23521', subjectName: 'Deep Learning Laboratory', staffName: 'ARTHI A. (CS87)', type: 'LAB' }, { periodNumber: 2, timeSlot: '09:40 AM - 10:35 AM', startTime: '09:40', endTime: '10:35', subjectCode: 'AD23521', subjectName: 'Deep Learning Laboratory', staffName: 'ARTHI A. (CS87)', type: 'LAB' },
{ periodNumber: 3, timeSlot: '10:50 AM - 11:45 AM', startTime: '10:50', endTime: '11:45', subjectCode: 'AD23V15', subjectName: 'Image and Video Analytics', staffName: 'JOEL J (CS226)', type: 'THEORY' }, { periodNumber: 3, timeSlot: '10:50 AM - 11:45 AM', startTime: '10:50', endTime: '11:45', subjectCode: 'AD23V15', subjectName: 'Image and Video Analytics', staffName: 'JOEL J (CS226)', type: 'THEORY' },
@@ -273,8 +282,8 @@ export function generateCurriculumSchedule(deptCode: string, semester: number):
{ periodNumber: 5, timeSlot: '01:30 PM - 02:25 PM', startTime: '13:30', endTime: '14:25', subjectCode: 'AD23V12', subjectName: 'Big Data Analytics', staffName: 'KAVITHA V (CS253)', type: 'THEORY' }, { periodNumber: 5, timeSlot: '01:30 PM - 02:25 PM', startTime: '13:30', endTime: '14:25', subjectCode: 'AD23V12', subjectName: 'Big Data Analytics', staffName: 'KAVITHA V (CS253)', type: 'THEORY' },
{ periodNumber: 6, timeSlot: '02:25 PM - 03:20 PM', startTime: '14:25', endTime: '15:20', subjectCode: 'CS23511', subjectName: 'Computer Networks', staffName: 'SELVAKUMARAN S. (CS91)', type: 'THEORY' }, { periodNumber: 6, timeSlot: '02:25 PM - 03:20 PM', startTime: '14:25', endTime: '15:20', subjectCode: 'CS23511', subjectName: 'Computer Networks', staffName: 'SELVAKUMARAN S. (CS91)', type: 'THEORY' },
{ periodNumber: 7, timeSlot: '03:20 PM - 04:15 PM', startTime: '15:20', endTime: '16:15', subjectCode: 'MENT', subjectName: 'Mentoring Session', staffName: 'KAVITHA V (CS253)', type: 'THEORY' }, { periodNumber: 7, timeSlot: '03:20 PM - 04:15 PM', startTime: '15:20', endTime: '16:15', subjectCode: 'MENT', subjectName: 'Mentoring Session', staffName: 'KAVITHA V (CS253)', type: 'THEORY' },
]), ],
thursday: createDay([ thursday: [
{ periodNumber: 1, timeSlot: '08:45 AM - 09:40 AM', startTime: '08:45', endTime: '09:40', subjectCode: 'CB23531', subjectName: 'Business Analytics', staffName: 'FOUZIA SULTHANA K. (CS84)', type: 'THEORY' }, { periodNumber: 1, timeSlot: '08:45 AM - 09:40 AM', startTime: '08:45', endTime: '09:40', subjectCode: 'CB23531', subjectName: 'Business Analytics', staffName: 'FOUZIA SULTHANA K. (CS84)', type: 'THEORY' },
{ periodNumber: 2, timeSlot: '09:40 AM - 10:35 AM', startTime: '09:40', endTime: '10:35', subjectCode: 'CS23511', subjectName: 'Computer Networks', staffName: 'SELVAKUMARAN S. (CS91)', type: 'THEORY' }, { periodNumber: 2, timeSlot: '09:40 AM - 10:35 AM', startTime: '09:40', endTime: '10:35', subjectCode: 'CS23511', subjectName: 'Computer Networks', staffName: 'SELVAKUMARAN S. (CS91)', type: 'THEORY' },
{ periodNumber: 3, timeSlot: '10:50 AM - 11:45 AM', startTime: '10:50', endTime: '11:45', subjectCode: 'AD23511', subjectName: 'Deep Learning', staffName: 'ARTHI A. (CS87)', type: 'THEORY' }, { periodNumber: 3, timeSlot: '10:50 AM - 11:45 AM', startTime: '10:50', endTime: '11:45', subjectCode: 'AD23511', subjectName: 'Deep Learning', staffName: 'ARTHI A. (CS87)', type: 'THEORY' },
@@ -282,8 +291,8 @@ export function generateCurriculumSchedule(deptCode: string, semester: number):
{ periodNumber: 5, timeSlot: '01:30 PM - 02:25 PM', startTime: '13:30', endTime: '14:25', subjectCode: 'AD23V12', subjectName: 'Big Data Analytics', staffName: 'KAVITHA V (CS253)', type: 'THEORY' }, { periodNumber: 5, timeSlot: '01:30 PM - 02:25 PM', startTime: '13:30', endTime: '14:25', subjectCode: 'AD23V12', subjectName: 'Big Data Analytics', staffName: 'KAVITHA V (CS253)', type: 'THEORY' },
{ periodNumber: 6, timeSlot: '02:25 PM - 03:20 PM', startTime: '14:25', endTime: '15:20', subjectCode: 'AD23532', subjectName: 'Data Exploration & Visualization', staffName: 'DR.VIDHYA M (CS256)', type: 'THEORY' }, { periodNumber: 6, timeSlot: '02:25 PM - 03:20 PM', startTime: '14:25', endTime: '15:20', subjectCode: 'AD23532', subjectName: 'Data Exploration & Visualization', staffName: 'DR.VIDHYA M (CS256)', type: 'THEORY' },
{ periodNumber: 7, timeSlot: '03:20 PM - 04:15 PM', startTime: '15:20', endTime: '16:15', subjectCode: 'MENT', subjectName: 'Mentoring Hour', staffName: 'FOUZIA SULTHANA K. (CS84)', type: 'THEORY' }, { periodNumber: 7, timeSlot: '03:20 PM - 04:15 PM', startTime: '15:20', endTime: '16:15', subjectCode: 'MENT', subjectName: 'Mentoring Hour', staffName: 'FOUZIA SULTHANA K. (CS84)', type: 'THEORY' },
]), ],
friday: createDay([ friday: [
{ periodNumber: 1, timeSlot: '08:45 AM - 09:40 AM', startTime: '08:45', endTime: '09:40', subjectCode: 'AD23V12', subjectName: 'Big Data Analytics', staffName: 'KAVITHA V (CS253)', type: 'THEORY' }, { periodNumber: 1, timeSlot: '08:45 AM - 09:40 AM', startTime: '08:45', endTime: '09:40', subjectCode: 'AD23V12', subjectName: 'Big Data Analytics', staffName: 'KAVITHA V (CS253)', type: 'THEORY' },
{ periodNumber: 2, timeSlot: '09:40 AM - 10:35 AM', startTime: '09:40', endTime: '10:35', subjectCode: 'AD23V15', subjectName: 'Image and Video Analytics', staffName: 'JOEL J (CS226)', type: 'THEORY' }, { periodNumber: 2, timeSlot: '09:40 AM - 10:35 AM', startTime: '09:40', endTime: '10:35', subjectCode: 'AD23V15', subjectName: 'Image and Video Analytics', staffName: 'JOEL J (CS226)', type: 'THEORY' },
{ periodNumber: 3, timeSlot: '10:50 AM - 11:45 AM', startTime: '10:50', endTime: '11:45', subjectCode: 'AD23511', subjectName: 'Deep Learning', staffName: 'ARTHI A. (CS87)', type: 'THEORY' }, { periodNumber: 3, timeSlot: '10:50 AM - 11:45 AM', startTime: '10:50', endTime: '11:45', subjectCode: 'AD23511', subjectName: 'Deep Learning', staffName: 'ARTHI A. (CS87)', type: 'THEORY' },
@@ -291,16 +300,7 @@ export function generateCurriculumSchedule(deptCode: string, semester: number):
{ periodNumber: 5, timeSlot: '01:30 PM - 02:25 PM', startTime: '13:30', endTime: '14:25', subjectCode: 'AD23532', subjectName: 'Data Exploration & Visualization', staffName: 'DR.VIDHYA M (CS256)', type: 'THEORY' }, { periodNumber: 5, timeSlot: '01:30 PM - 02:25 PM', startTime: '13:30', endTime: '14:25', subjectCode: 'AD23532', subjectName: 'Data Exploration & Visualization', staffName: 'DR.VIDHYA M (CS256)', type: 'THEORY' },
{ periodNumber: 6, timeSlot: '02:25 PM - 03:20 PM', startTime: '14:25', endTime: '15:20', subjectCode: 'CB23531', subjectName: 'Business Analytics', staffName: 'FOUZIA SULTHANA K. (CS84)', type: 'THEORY' }, { periodNumber: 6, timeSlot: '02:25 PM - 03:20 PM', startTime: '14:25', endTime: '15:20', subjectCode: 'CB23531', subjectName: 'Business Analytics', staffName: 'FOUZIA SULTHANA K. (CS84)', type: 'THEORY' },
{ periodNumber: 7, timeSlot: '03:20 PM - 04:15 PM', startTime: '15:20', endTime: '16:15', subjectCode: 'CB23531', subjectName: 'Business Analytics', staffName: 'FOUZIA SULTHANA K. (CS84)', type: 'THEORY' }, { periodNumber: 7, timeSlot: '03:20 PM - 04:15 PM', startTime: '15:20', endTime: '16:15', subjectCode: 'CB23531', subjectName: 'Business Analytics', staffName: 'FOUZIA SULTHANA K. (CS84)', type: 'THEORY' },
]), ],
saturday: createDay([
{ periodNumber: 1, timeSlot: '08:45 AM - 09:40 AM', startTime: '08:45', endTime: '09:40', subjectCode: 'MX23511', subjectName: 'Disaster Risk Reduction and Management', staffName: 'DR. SRINIVASAN M P (PH22)', type: 'THEORY' },
{ periodNumber: 2, timeSlot: '09:40 AM - 10:35 AM', startTime: '09:40', endTime: '10:35', subjectCode: 'AD23IC3', subjectName: 'Tableau - Data Visualization', staffName: 'FOUZIA SULTHANA K. (CS84)', type: 'LAB' },
{ periodNumber: 3, timeSlot: '10:50 AM - 11:45 AM', startTime: '10:50', endTime: '11:45', subjectCode: 'MX23511', subjectName: 'Disaster Risk Reduction and Management', staffName: 'DR. SRINIVASAN M P (PH22)', type: 'THEORY' },
{ periodNumber: 4, timeSlot: '11:45 AM - 12:40 PM', startTime: '11:45', endTime: '12:40', subjectCode: 'MX23511', subjectName: 'Disaster Risk Reduction and Management', staffName: 'DR. SRINIVASAN M P (PH22)', type: 'THEORY' },
{ periodNumber: 5, timeSlot: '01:30 PM - 02:25 PM', startTime: '13:30', endTime: '14:25', subjectCode: 'MX23511', subjectName: 'Disaster Risk Reduction and Management', staffName: 'DR. SRINIVASAN M P (PH22)', type: 'THEORY' },
{ periodNumber: 6, timeSlot: '02:25 PM - 03:20 PM', startTime: '14:25', endTime: '15:20', subjectCode: 'MX23511', subjectName: 'Disaster Risk Reduction and Management', staffName: 'DR. SRINIVASAN M P (PH22)', type: 'THEORY' },
{ periodNumber: 7, timeSlot: '03:20 PM - 04:15 PM', startTime: '15:20', endTime: '16:15', subjectCode: 'AD23IC3', subjectName: 'Tableau - Data Visualization', staffName: 'FOUZIA SULTHANA K. (CS84)', type: 'LAB' },
]),
}; };
} }
@@ -320,25 +320,46 @@ const startTimes = ['08:45', '09:40', '10:50', '11:45', '13:30', '14:25', '15:20
const endTimes = ['09:40', '10:35', '11:45', '12:40', '14:25', '15:20', '16:15', '17:10', '18:05', '19:00']; const endTimes = ['09:40', '10:35', '11:45', '12:40', '14:25', '15:20', '16:15', '17:10', '18:05', '19:00'];
export function normalizeTimetableSchedule(scheduleRaw: any): WeeklySchedule { export function normalizeTimetableSchedule(scheduleRaw: any): WeeklySchedule {
const emptyDay = (): DaySchedule => ({ 1: [], 2: [], 3: [], 4: [], 5: [], 6: [], 7: [] });
const weekly: WeeklySchedule = { const weekly: WeeklySchedule = {
monday: emptyDay(), monday: [],
tuesday: emptyDay(), tuesday: [],
wednesday: emptyDay(), wednesday: [],
thursday: emptyDay(), thursday: [],
friday: emptyDay(), friday: [],
saturday: emptyDay(),
}; };
if (!scheduleRaw) return weekly; if (!scheduleRaw) return weekly;
const dayAliases: Record<string, keyof WeeklySchedule> = { const dayAliases: Record<string, keyof WeeklySchedule> = {
'1': 'monday', 'mon': 'monday', 'monday': 'monday', '1': 'monday',
'2': 'tuesday', 'tue': 'tuesday', 'tuesday': 'tuesday', 'mon': 'monday',
'3': 'wednesday', 'wed': 'wednesday', 'wednesday': 'wednesday', 'monday': 'monday',
'4': 'thursday', 'thu': 'thursday', 'thursday': 'thursday', 'day 1': 'monday',
'5': 'friday', 'fri': 'friday', 'friday': 'friday', 'day1': 'monday',
'6': 'saturday', 'sat': 'saturday', 'saturday': 'saturday',
'2': 'tuesday',
'tue': 'tuesday',
'tuesday': 'tuesday',
'day 2': 'tuesday',
'day2': 'tuesday',
'3': 'wednesday',
'wed': 'wednesday',
'wednesday': 'wednesday',
'day 3': 'wednesday',
'day3': 'wednesday',
'4': 'thursday',
'thu': 'thursday',
'thursday': 'thursday',
'day 4': 'thursday',
'day4': 'thursday',
'5': 'friday',
'fri': 'friday',
'friday': 'friday',
'day 5': 'friday',
'day5': 'friday',
}; };
const scheduleMap = scheduleRaw.schedule || scheduleRaw.data?.schedule || scheduleRaw.data || scheduleRaw; const scheduleMap = scheduleRaw.schedule || scheduleRaw.data?.schedule || scheduleRaw.data || scheduleRaw;
@@ -352,7 +373,6 @@ export function normalizeTimetableSchedule(scheduleRaw: any): WeeklySchedule {
if (typeof dayObj === 'object' && !Array.isArray(dayObj)) { if (typeof dayObj === 'object' && !Array.isArray(dayObj)) {
Object.keys(dayObj).forEach((rawPeriod) => { Object.keys(dayObj).forEach((rawPeriod) => {
const pNum = parseInt(rawPeriod, 10); const pNum = parseInt(rawPeriod, 10);
if (pNum < 1 || pNum > 7) return;
const rawItems = dayObj[rawPeriod]; const rawItems = dayObj[rawPeriod];
const items = Array.isArray(rawItems) ? rawItems : [rawItems]; const items = Array.isArray(rawItems) ? rawItems : [rawItems];
@@ -365,12 +385,11 @@ export function normalizeTimetableSchedule(scheduleRaw: any): WeeklySchedule {
const isLab = (subName && subName.toLowerCase().includes('lab')) || subCode.includes('71') || subCode.includes('81'); const isLab = (subName && subName.toLowerCase().includes('lab')) || subCode.includes('71') || subCode.includes('81');
if (subName || subCode || staff) { if (subName || subCode || staff) {
if (!weekly[normalizedDay][pNum]) weekly[normalizedDay][pNum] = []; weekly[normalizedDay].push({
weekly[normalizedDay][pNum].push({ periodNumber: pNum || (weekly[normalizedDay].length + 1),
periodNumber: pNum, timeSlot: timeSlots[(pNum || (weekly[normalizedDay].length + 1)) - 1] || `Period ${pNum || 1}`,
timeSlot: timeSlots[pNum - 1] || `Period ${pNum}`, startTime: startTimes[(pNum || 1) - 1] || '08:45',
startTime: startTimes[pNum - 1] || '08:45', endTime: endTimes[(pNum || 1) - 1] || '09:40',
endTime: endTimes[pNum - 1] || '09:40',
subjectCode: subCode || 'SUB', subjectCode: subCode || 'SUB',
subjectName: subName || 'Core Subject', subjectName: subName || 'Core Subject',
staffName: staff || 'Faculty Member', staffName: staff || 'Faculty Member',
@@ -380,10 +399,35 @@ export function normalizeTimetableSchedule(scheduleRaw: any): WeeklySchedule {
} }
}); });
}); });
} else if (Array.isArray(dayObj)) {
dayObj.forEach((item: any, idx: number) => {
if (!item) return;
const pNum = item.period || item.periodNumber || (idx + 1);
const subName = item.subjectName || item.name || item.subject || '';
const subCode = item.subjectCode || item.code || '';
const staff = item.staffName || item.staff || '';
const isLab = (subName && subName.toLowerCase().includes('lab')) || subCode.includes('71') || subCode.includes('81');
if (subName || subCode || staff) {
weekly[normalizedDay].push({
periodNumber: pNum,
timeSlot: timeSlots[pNum - 1] || `Period ${pNum}`,
startTime: startTimes[pNum - 1] || '08:45',
endTime: endTimes[pNum - 1] || '09:40',
subjectCode: subCode || 'SUB',
subjectName: subName || 'Core Subject',
staffName: staff || 'Faculty Member',
staffCode: item.staffCode,
type: isLab ? 'LAB' : 'THEORY',
});
} }
}); });
} }
weekly[normalizedDay].sort((a, b) => a.periodNumber - b.periodNumber);
});
}
return weekly; return weekly;
} }
@@ -655,23 +699,22 @@ export function updateImsProfileName(newName: string): StudentProfile | null {
* Replicates reference site's fetchTimetable() from imsScraper.ts * Replicates reference site's fetchTimetable() from imsScraper.ts
*/ */
export async function fetchImsTimetable(_token: string, regNumber: string, _forceRefresh = false): Promise<WeeklySchedule> { export async function fetchImsTimetable(_token: string, regNumber: string, _forceRefresh = false): Promise<WeeklySchedule> {
const emptyDay = (): DaySchedule => ({ 1: [], 2: [], 3: [], 4: [], 5: [], 6: [], 7: [] });
let weeklySchedule: WeeklySchedule = { let weeklySchedule: WeeklySchedule = {
monday: emptyDay(), tuesday: emptyDay(), wednesday: emptyDay(), thursday: emptyDay(), friday: emptyDay(), saturday: emptyDay(), monday: [], tuesday: [], wednesday: [], thursday: [], friday: [],
}; };
try { try {
const session = getStoredImsSession(); const session = getStoredImsSession();
let targetUrl = session?.profile?.timetableUrl || ''; let targetUrl = session?.profile?.timetableUrl || '';
// If targetUrl is not stored yet, discover it dynamically from the report menu page
if (!targetUrl) { if (!targetUrl) {
const navRes = await fetch('/ims/admin/grade/student/mark/report', { credentials: 'same-origin' }); const navRes = await fetch('/ims/admin/grade/student/mark/report', { credentials: 'same-origin' });
if (navRes.ok) { if (navRes.ok) {
const html = await navRes.text(); const navHtml = await navRes.text();
const doc = new DOMParser().parseFromString(html, 'text/html'); const navDoc = new DOMParser().parseFromString(navHtml, 'text/html');
const ttLinkEl = doc.querySelector('a[href*="time-table"]'); const ttLink = navDoc.querySelector('a[href*="student-time-table"]')?.getAttribute('href');
if (ttLinkEl) { if (ttLink) {
const ttLink = ttLinkEl.getAttribute('href') || '';
if (ttLink.startsWith('https://ims.ritchennai.edu.in')) { if (ttLink.startsWith('https://ims.ritchennai.edu.in')) {
targetUrl = ttLink.replace('https://ims.ritchennai.edu.in', '/ims'); targetUrl = ttLink.replace('https://ims.ritchennai.edu.in', '/ims');
} else if (ttLink.startsWith('/')) { } else if (ttLink.startsWith('/')) {
@@ -691,15 +734,16 @@ export async function fetchImsTimetable(_token: string, regNumber: string, _forc
const html = await response.text(); const html = await response.text();
const doc = new DOMParser().parseFromString(html, 'text/html'); const doc = new DOMParser().parseFromString(html, 'text/html');
// Parse .period_form elements — exact same logic as reference site
const forms = Array.from(doc.querySelectorAll('.period_form')); const forms = Array.from(doc.querySelectorAll('.period_form'));
if (forms.length > 0) { if (forms.length > 0) {
const dayAliases: Record<string, keyof WeeklySchedule> = { const dayAliases: Record<string, keyof WeeklySchedule> = {
'monday': 'monday', 'mon': 'monday', '1': 'monday', 'monday': 'monday', 'tuesday': 'tuesday', 'wednesday': 'wednesday',
'tuesday': 'tuesday', 'tue': 'tuesday', '2': 'tuesday', 'thursday': 'thursday', 'friday': 'friday',
'wednesday': 'wednesday', 'wed': 'wednesday', '3': 'wednesday', };
'thursday': 'thursday', 'thu': 'thursday', '4': 'thursday',
'friday': 'friday', 'fri': 'friday', '5': 'friday', const periodMap: Record<string, Record<number, TimetablePeriod>> = {
'saturday': 'saturday', 'sat': 'saturday', '6': 'saturday', monday: {}, tuesday: {}, wednesday: {}, thursday: {}, friday: {}
}; };
forms.forEach(form => { forms.forEach(form => {
@@ -708,7 +752,7 @@ export async function fetchImsTimetable(_token: string, regNumber: string, _forc
const day = (dayInput?.value || '').toLowerCase().trim(); const day = (dayInput?.value || '').toLowerCase().trim();
const period = parseInt(periodInput?.value || '0', 10); const period = parseInt(periodInput?.value || '0', 10);
if (!day || !period || isNaN(period) || period < 1 || period > 7) return; if (!day || !period || isNaN(period) || period < 1 || period > 10) return;
const normalizedDay = dayAliases[day]; const normalizedDay = dayAliases[day];
if (!normalizedDay) return; if (!normalizedDay) return;
@@ -729,6 +773,7 @@ export async function fetchImsTimetable(_token: string, regNumber: string, _forc
}); });
} }
// Fallback text check for non-standard subjects (NPTEL, Aptitude, Mentoring, Placement, Library, etc.)
if (!rawSubject) { if (!rawSubject) {
const fullTxt = form.textContent?.trim() || ''; const fullTxt = form.textContent?.trim() || '';
if (fullTxt) rawSubject = fullTxt.replace(/\s+/g, ' ').slice(0, 60); if (fullTxt) rawSubject = fullTxt.replace(/\s+/g, ' ').slice(0, 60);
@@ -752,29 +797,58 @@ export async function fetchImsTimetable(_token: string, regNumber: string, _forc
staffName = `${staffMatch[1].trim()} (${staffMatch[2].trim()})`; staffName = `${staffMatch[1].trim()} (${staffMatch[2].trim()})`;
} }
// STRICT USER RULE: Mark as LAB ONLY if subject name contains 'lab' or 'laboratory' (case insensitive)
const rawSubLower = subjectName.toLowerCase(); const rawSubLower = subjectName.toLowerCase();
const isLab = rawSubLower.includes('lab') || rawSubLower.includes('laboratory'); const isLab = rawSubLower.includes('lab') || rawSubLower.includes('laboratory');
const periodItem: TimetablePeriod = { const periodIndex = period - 1;
const existing = periodMap[normalizedDay][period];
if (!existing) {
periodMap[normalizedDay][period] = {
periodNumber: period, periodNumber: period,
timeSlot: (period - 1) < timeSlots.length ? timeSlots[period - 1] : `Period ${period}`, timeSlot: periodIndex < timeSlots.length ? timeSlots[periodIndex] : `Period ${period}`,
startTime: (period - 1) < startTimes.length ? startTimes[period - 1] : '', startTime: periodIndex < startTimes.length ? startTimes[periodIndex] : '',
endTime: (period - 1) < endTimes.length ? endTimes[period - 1] : '', endTime: periodIndex < endTimes.length ? endTimes[periodIndex] : '',
subjectCode, subjectCode,
subjectName, subjectName,
staffName, staffName,
type: isLab ? 'LAB' : 'THEORY', type: isLab ? 'LAB' : 'THEORY',
}; };
} else {
const existingList = weeklySchedule[normalizedDay][period] || []; // Merge staff names if same course or elective slot
const isDuplicate = existingList.some( if (staffName && !existing.staffName.includes(staffName)) {
p => p.subjectCode === subjectCode && p.staffName === staffName && p.subjectName === subjectName existing.staffName += `, ${staffName}`;
);
if (!isDuplicate) {
existingList.push(periodItem);
weeklySchedule[normalizedDay][period] = existingList;
} }
}
});
// Populate weeklySchedule from periodMap (Ensure EVERY day Monday-Friday has EXACTLY 7 periods)
const standardDays: (keyof WeeklySchedule)[] = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'];
standardDays.forEach((day) => {
const existingMap = periodMap[day] || {};
const fullDayPeriods: TimetablePeriod[] = [];
for (let pNum = 1; pNum <= 7; pNum++) {
const periodIndex = pNum - 1;
if (existingMap[pNum]) {
fullDayPeriods.push(existingMap[pNum]);
} else {
fullDayPeriods.push({
periodNumber: pNum,
timeSlot: timeSlots[periodIndex] || `Period ${pNum}`,
startTime: startTimes[periodIndex] || '',
endTime: endTimes[periodIndex] || '',
subjectCode: 'FREE',
subjectName: 'Free Period / Library',
staffName: 'Self Study',
type: 'THEORY',
});
}
}
weeklySchedule[day] = fullDayPeriods;
}); });
} }
} catch (err) { } catch (err) {
@@ -782,9 +856,7 @@ export async function fetchImsTimetable(_token: string, regNumber: string, _forc
} }
// Fallback to hardcoded schedule only if scraping returned nothing // Fallback to hardcoded schedule only if scraping returned nothing
const totalPeriods = Object.values(weeklySchedule).reduce((acc: number, daySched: any) => { const totalPeriods = Object.values(weeklySchedule).reduce((acc, periods) => acc + periods.length, 0);
return acc + Object.values(daySched || {}).reduce((dAcc: number, pList: any) => dAcc + (pList?.length || 0), 0);
}, 0);
if (totalPeriods === 0) { if (totalPeriods === 0) {
const deduced = deduceStudentDetails(regNumber || '0000000000000'); const deduced = deduceStudentDetails(regNumber || '0000000000000');
weeklySchedule = generateCurriculumSchedule(deduced.departmentCode, deduced.semester); weeklySchedule = generateCurriculumSchedule(deduced.departmentCode, deduced.semester);
@@ -1153,3 +1225,113 @@ export async function fetchStudentDashboard(regNumber?: string): Promise<{ resul
const grades = await fetchImsSemesterResults(session.token, regNumber || session.profile.registerNumber, session.profile.semester); const grades = await fetchImsSemesterResults(session.token, regNumber || session.profile.registerNumber, session.profile.semester);
return { ...grades, student: session.profile }; return { ...grades, student: session.profile };
} }
/**
* Fetch Assignment Marks from /ims/admin/assignment/student/mark/report
*/
export async function fetchImsAssignmentMarks(): Promise<AssignmentMarkItem[]> {
try {
const response = await fetch('/ims/admin/assignment/student/mark/report', {
method: 'GET',
credentials: 'same-origin',
});
if (!response.ok) return [];
const html = await response.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const table = doc.querySelector('table');
if (!table) return [];
const rows = Array.from(table.querySelectorAll('tr'));
const items: AssignmentMarkItem[] = [];
rows.forEach((row) => {
const tds = Array.from(row.querySelectorAll('td')).map(td => td.textContent?.trim() || '');
if (tds.length >= 4) {
let subjectName = tds[0];
let subjectCode = tds[1];
let facultyName = tds[2];
if (/^[A-Z]{2,4}\d{2,5}$/i.test(tds[0])) {
subjectCode = tds[0];
subjectName = tds[1];
}
const a1 = tds[3] || '-';
const a2 = tds[4] || '-';
const a3 = tds[5] || '-';
const a4 = tds[6] || '-';
const a5 = tds[7] || '-';
const total = tds[8] || tds[tds.length - 1] || '-';
if (subjectName || subjectCode) {
items.push({
subjectName,
subjectCode,
facultyName,
a1, a2, a3, a4, a5,
total,
});
}
}
});
return items;
} catch (err) {
console.warn('Failed to fetch assignment marks:', err);
return [];
}
}
/**
* Fetch LAB Marks from /ims/admin/student_lab_mark/report
*/
export async function fetchImsLabMarks(): Promise<LabMarkItem[]> {
try {
const response = await fetch('/ims/admin/student_lab_mark/report', {
method: 'GET',
credentials: 'same-origin',
});
if (!response.ok) return [];
const html = await response.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const table = doc.querySelector('table');
if (!table) return [];
const rows = Array.from(table.querySelectorAll('tr'));
const items: LabMarkItem[] = [];
rows.forEach((row) => {
const tds = Array.from(row.querySelectorAll('td')).map(td => td.textContent?.trim() || '');
if (tds.length >= 3) {
let subjectName = tds[0];
let subjectCode = tds[1];
let facultyName = tds[2];
if (/^[A-Z]{2,4}\d{2,5}$/i.test(tds[0])) {
subjectCode = tds[0];
subjectName = tds[1];
}
const marks = tds[3] || '-';
const total = tds[tds.length - 1] || '-';
if (subjectName || subjectCode) {
items.push({
subjectName,
subjectCode,
facultyName,
marks,
total,
});
}
}
});
return items;
} catch (err) {
console.warn('Failed to fetch lab marks:', err);
return [];
}
}