From bbb2cf7d30b84048c389f6a16cd54a5da60af6ad Mon Sep 17 00:00:00 2001 From: Shanmuga Krishnan S M Date: Wed, 19 Aug 2026 14:58:59 +0530 Subject: [PATCH] feat: add LAB and Assignment marks fetching with lazy loading and system protection --- .../StudentDashboard/StudentDashboard.tsx | 501 ++++++++++-------- src/services/imsService.ts | 364 +++++++++---- 2 files changed, 563 insertions(+), 302 deletions(-) diff --git a/src/pages/StudentDashboard/StudentDashboard.tsx b/src/pages/StudentDashboard/StudentDashboard.tsx index eaae7e9..4bfe030 100644 --- a/src/pages/StudentDashboard/StudentDashboard.tsx +++ b/src/pages/StudentDashboard/StudentDashboard.tsx @@ -6,7 +6,7 @@ import { Sparkles, KeyRound, ArrowRight, LogOut, AlertCircle, User, HelpCircle, Compass, ChevronRight, ExternalLink, 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'; import { Link } from 'react-router-dom'; import { useAuth } from '@/context/AuthContext'; @@ -21,11 +21,15 @@ import { fetchImsTimetable, fetchImsAttendance, fetchImsCatMarks, + fetchImsAssignmentMarks, + fetchImsLabMarks, fetchImsSemesterResults, type StudentProfile, type WeeklySchedule, type AttendanceReport, type CatMarksReport, + type AssignmentMarkItem, + type LabMarkItem, type SemesterResult } from '@/services/imsService'; @@ -35,15 +39,17 @@ export default function StudentDashboard() { // Navigation State const [activeTab, setActiveTab] = useState<'timetable' | 'attendance' | 'marks' | 'grades' | 'faculties'>('timetable'); - const [selectedDayFilter, setSelectedDayFilter] = useState<'all' | 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday'>('all'); - const [ttSearchQuery, setTtSearchQuery] = useState(''); + const [selectedDay, setSelectedDay] = useState<'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday'>('monday'); const [selectedSemFilter, setSelectedSemFilter] = useState(0); + const [marksSubTab, setMarksSubTab] = useState<'cat' | 'assignment' | 'lab'>('cat'); // Loaded Data States const [profile, setProfile] = useState(session?.profile || null); const [timetable, setTimetable] = useState(null); const [attendance, setAttendance] = useState(null); const [catMarks, setCatMarks] = useState(null); + const [assignmentMarks, setAssignmentMarks] = useState(null); + const [labMarks, setLabMarks] = useState(null); const [gradesData, setGradesData] = useState<{ results: SemesterResult[]; cgpa: number; totalCredits: number; activeArrears: number } | null>(null); // Loading States @@ -68,6 +74,13 @@ export default function StudentDashboard() { } }, [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) => { setIsInitialLoading(true); setError(null); @@ -76,6 +89,18 @@ export default function StudentDashboard() { const p = await fetchImsProfile(token, reg, force); setProfile(p); 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) { console.error('Error loading initial IMS data:', err); setError(err.message || 'Failed to load timetable.'); @@ -100,13 +125,19 @@ export default function StudentDashboard() { }; const ensureCatMarksLoaded = async (force = false) => { - if ((catMarks && !force) || !session?.token || !profile) return; + if ((catMarks && assignmentMarks && labMarks && !force) || !session?.token || !profile) return; setIsTabLoading(true); try { - const data = await fetchImsCatMarks(session.token, profile.registerNumber, force); - setCatMarks(data); + const [cat, assign, lab] = await Promise.all([ + fetchImsCatMarks(session.token, profile.registerNumber, force), + fetchImsAssignmentMarks(), + fetchImsLabMarks(), + ]); + setCatMarks(cat); + setAssignmentMarks(assign); + setLabMarks(lab); } catch (err) { - console.warn('CAT marks load notice:', err); + console.warn('Marks load notice:', err); } finally { setIsTabLoading(false); } @@ -354,6 +385,7 @@ export default function StudentDashboard() { // ───────────────────────────────────────────────────────────── // VIEW 2: AUTHENTICATED STUDENT DASHBOARD // ───────────────────────────────────────────────────────────── + const currentPeriods = (timetable && timetable[selectedDay]) || []; const filteredGrades = gradesData?.results ? ( selectedSemFilter === 0 ? gradesData.results @@ -712,196 +744,108 @@ export default function StudentDashboard() { {/* ─── 1. TIMETABLE TAB ─── */} {activeTab === 'timetable' && (
- {/* Header Title & Subtitle */} -
-
-

Class Time Table

-

- Weekly class schedule for Class : {profile?.department || 'B.Tech. AI & DS'} / {profile?.semester || 5} / E -

-
- -
- - - CLASS Class : {profile?.department || 'B.Tech. AI & DS'} / {profile?.semester || 5} / E - -
-
- - {/* Toolbar: Day Filter Pills, Search Bar, Refresh */} + {/* Day Switcher */}
- {/* Day Filter Pills */} -
- {[ - { key: 'all', label: 'All Days' }, - { 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; +
+ {(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'] as const).map((dayKey) => { + const count = timetable?.[dayKey]?.length || 0; + const isSelected = selectedDay === dayKey; return ( ); })}
- {/* Right Toolbar Controls */} -
-
- - 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" - /> -
- - -
+ + + Live IMS Timetable +
- {/* Timetable Grid Matrix */} {isInitialLoading ? (

Fetching timetable from RIT IMS...

+ ) : currentPeriods.length === 0 ? ( +
+
+ +
+

+ No classes scheduled for {selectedDay.toUpperCase()} +

+

+ Select another day of the week above to view your timetable periods and staff. +

+
) : ( -
- - - - - {(['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 ( - - ); - })} - - +
+ {currentPeriods.map((period) => ( + + {/* Header */} +
+
+ + P{period.periodNumber} + + + + {period.timeSlot} + +
-
- {[1, 2, 3, 4, 5, 6, 7].map((pNum) => { - const timeSlots = [ - '08:45 - 09:40', - '09:40 - 10:35', - '10:50 - 11:45', - '11:45 - 12:40', - '01:30 - 02:25', - '02:25 - 03:20', - '03:20 - 04:15', - ]; - const slotTime = timeSlots[pNum - 1] || ''; + + {period.type === 'LAB' ? : } + {period.type} + + - const activeDays = (['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'] as const) - .filter(d => selectedDayFilter === 'all' || selectedDayFilter === d); + {/* Subject Details */} +
+
+ + {period.subjectCode} + +
+

+ {period.subjectName} +

+
- return ( - - {/* Period Column */} - - - {/* 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 ( - - ); - })} - - ); - })} - -
- Period - -
- {dayKey} - {isToday && ( - - TODAY - - )} -
-
-
Period {pNum}
-
{slotTime}
-
- {periods.length > 0 ? ( -
- {periods.map((p, idx) => ( -
-
- {p.subjectName} -
-
- - {p.subjectCode || p.type} - -
- {p.staffName && ( -
- {p.staffName} -
- )} -
- ))} -
- ) : ( -
- — -
- )} -
+ {/* Faculty Footer */} +
+ {period.staffName || 'Faculty Instructor'} + {period.staffCode && ( + {period.staffCode} + )} +
+ + ))}
)}
@@ -980,53 +924,188 @@ export default function StudentDashboard() {
)} - {/* ─── 3. CAT MARKS TAB (LAZY LOADED) ─── */} + {/* ─── 3. INTERNAL MARKS TAB (CAT, ASSIGNMENT & LAB MARKS) ─── */} {activeTab === 'marks' && (
+ {/* Sub-Tab Selector */} +
+ + + + + +
+ {isTabLoading && !catMarks ? (
-

Fetching CAT marks on demand...

+

Fetching internal marks on demand...

- ) : catMarks ? ( + ) : (
-
-

- Continuous Assessment Test (CAT) Marks -

-

Internal examination scores synchronized with college records

-
+ {/* SUB TAB 1: CAT MARKS */} + {marksSubTab === 'cat' && catMarks && ( +
+
+

+ Continuous Assessment Test (CAT) Marks +

+

Internal examination scores synchronized with college records

+
-
- - - - - - - - - - - - - - {catMarks.subjects.map((sub) => ( - - - - - - - - - - ))} - -
Subject CodeSubject TitleHandling FacultyCO 1CO 2TotalWeightage
{sub.code}{sub.name}{sub.faculty || 'Faculty In-Charge'}{sub.co1 ?? '-'}{sub.co2 ?? '-'}{sub.total ?? '-'}{sub.weightage ?? '-'}
-
+
+ + + + + + + + + + + + + + {catMarks.subjects.map((sub) => ( + + + + + + + + + + ))} + +
Subject CodeSubject TitleHandling FacultyCO 1CO 2TotalWeightage
{sub.code}{sub.name}{sub.faculty || 'Faculty In-Charge'}{sub.co1 ?? '-'}{sub.co2 ?? '-'}{sub.total ?? '-'}{sub.weightage ?? '-'}
+
+
+ )} + + {/* SUB TAB 2: ASSIGNMENT MARKS */} + {marksSubTab === 'assignment' && ( +
+
+

+ Assignment Marks +

+

Continuous assignment submissions (10 Marks per assignment)

+
+ + {assignmentMarks && assignmentMarks.length > 0 ? ( +
+ + + + + + + + + + + + + + + + {assignmentMarks.map((item, idx) => ( + + + + + + + + + + + + ))} + +
Subject CodeSubject TitleHandling FacultyAssgn 1Assgn 2Assgn 3Assgn 4Assgn 5Total (50)
{item.subjectCode}{item.subjectName}{item.facultyName || 'Faculty In-Charge'}{item.a1}{item.a2}{item.a3}{item.a4}{item.a5}{item.total}
+
+ ) : ( +
+ No Assignment Marks posted by faculty yet for this semester. +
+ )} +
+ )} + + {/* SUB TAB 3: LAB MARKS */} + {marksSubTab === 'lab' && ( +
+
+

+ Laboratory Practical Marks +

+

Practical course evaluations & lab experiment marks

+
+ + {labMarks && labMarks.length > 0 ? ( +
+ + + + + + + + + + + + {labMarks.map((item, idx) => ( + + + + + + + + ))} + +
Subject CodeSubject TitleHandling FacultyMarksTotal
{item.subjectCode}{item.subjectName}{item.facultyName || 'Faculty In-Charge'}{item.marks}{item.total}
+
+ ) : ( +
+ No Lab Marks posted by faculty yet for this semester. +
+ )} +
+ )}
- ) : null} + )}
)} diff --git a/src/services/imsService.ts b/src/services/imsService.ts index c92d2b8..9fff934 100644 --- a/src/services/imsService.ts +++ b/src/services/imsService.ts @@ -28,15 +28,12 @@ export interface TimetablePeriod { type: 'THEORY' | 'LAB'; } -export type DaySchedule = Record; - export interface WeeklySchedule { - monday: DaySchedule; - tuesday: DaySchedule; - wednesday: DaySchedule; - thursday: DaySchedule; - friday: DaySchedule; - saturday: DaySchedule; + monday: TimetablePeriod[]; + tuesday: TimetablePeriod[]; + wednesday: TimetablePeriod[]; + thursday: TimetablePeriod[]; + friday: TimetablePeriod[]; } export interface SubjectAttendance { @@ -70,6 +67,26 @@ export interface CatMarksReport { 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 { code: string; name: string; @@ -238,17 +255,8 @@ export function parseDepartmentCode(rawDept: string = ''): string { } 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 { - 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: 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' }, @@ -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: 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' }, - ]), - 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: 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: 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: 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' }, - ]), - wednesday: createDay([ + { 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: [ { 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: 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: 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' }, - ]), - 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: 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' }, @@ -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: 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' }, - ]), - 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: 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' }, @@ -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: 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' }, - ]), - 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']; export function normalizeTimetableSchedule(scheduleRaw: any): WeeklySchedule { - const emptyDay = (): DaySchedule => ({ 1: [], 2: [], 3: [], 4: [], 5: [], 6: [], 7: [] }); const weekly: WeeklySchedule = { - monday: emptyDay(), - tuesday: emptyDay(), - wednesday: emptyDay(), - thursday: emptyDay(), - friday: emptyDay(), - saturday: emptyDay(), + monday: [], + tuesday: [], + wednesday: [], + thursday: [], + friday: [], }; if (!scheduleRaw) return weekly; const dayAliases: Record = { - '1': 'monday', 'mon': 'monday', 'monday': 'monday', - '2': 'tuesday', 'tue': 'tuesday', 'tuesday': 'tuesday', - '3': 'wednesday', 'wed': 'wednesday', 'wednesday': 'wednesday', - '4': 'thursday', 'thu': 'thursday', 'thursday': 'thursday', - '5': 'friday', 'fri': 'friday', 'friday': 'friday', - '6': 'saturday', 'sat': 'saturday', 'saturday': 'saturday', + '1': 'monday', + 'mon': 'monday', + 'monday': 'monday', + 'day 1': 'monday', + 'day1': 'monday', + + '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; @@ -352,7 +373,6 @@ export function normalizeTimetableSchedule(scheduleRaw: any): WeeklySchedule { if (typeof dayObj === 'object' && !Array.isArray(dayObj)) { Object.keys(dayObj).forEach((rawPeriod) => { const pNum = parseInt(rawPeriod, 10); - if (pNum < 1 || pNum > 7) return; const rawItems = dayObj[rawPeriod]; 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'); if (subName || subCode || staff) { - if (!weekly[normalizedDay][pNum]) weekly[normalizedDay][pNum] = []; - weekly[normalizedDay][pNum].push({ - periodNumber: pNum, - timeSlot: timeSlots[pNum - 1] || `Period ${pNum}`, - startTime: startTimes[pNum - 1] || '08:45', - endTime: endTimes[pNum - 1] || '09:40', + weekly[normalizedDay].push({ + periodNumber: pNum || (weekly[normalizedDay].length + 1), + timeSlot: timeSlots[(pNum || (weekly[normalizedDay].length + 1)) - 1] || `Period ${pNum || 1}`, + startTime: startTimes[(pNum || 1) - 1] || '08:45', + endTime: endTimes[(pNum || 1) - 1] || '09:40', subjectCode: subCode || 'SUB', subjectName: subName || 'Core Subject', staffName: staff || 'Faculty Member', @@ -380,7 +399,32 @@ 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); }); } @@ -655,23 +699,22 @@ export function updateImsProfileName(newName: string): StudentProfile | null { * Replicates reference site's fetchTimetable() from imsScraper.ts */ export async function fetchImsTimetable(_token: string, regNumber: string, _forceRefresh = false): Promise { - const emptyDay = (): DaySchedule => ({ 1: [], 2: [], 3: [], 4: [], 5: [], 6: [], 7: [] }); let weeklySchedule: WeeklySchedule = { - monday: emptyDay(), tuesday: emptyDay(), wednesday: emptyDay(), thursday: emptyDay(), friday: emptyDay(), saturday: emptyDay(), + monday: [], tuesday: [], wednesday: [], thursday: [], friday: [], }; try { const session = getStoredImsSession(); let targetUrl = session?.profile?.timetableUrl || ''; + // If targetUrl is not stored yet, discover it dynamically from the report menu page if (!targetUrl) { const navRes = await fetch('/ims/admin/grade/student/mark/report', { credentials: 'same-origin' }); if (navRes.ok) { - const html = await navRes.text(); - const doc = new DOMParser().parseFromString(html, 'text/html'); - const ttLinkEl = doc.querySelector('a[href*="time-table"]'); - if (ttLinkEl) { - const ttLink = ttLinkEl.getAttribute('href') || ''; + const navHtml = await navRes.text(); + const navDoc = new DOMParser().parseFromString(navHtml, 'text/html'); + const ttLink = navDoc.querySelector('a[href*="student-time-table"]')?.getAttribute('href'); + if (ttLink) { if (ttLink.startsWith('https://ims.ritchennai.edu.in')) { targetUrl = ttLink.replace('https://ims.ritchennai.edu.in', '/ims'); } else if (ttLink.startsWith('/')) { @@ -691,15 +734,16 @@ export async function fetchImsTimetable(_token: string, regNumber: string, _forc const html = await response.text(); 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')); if (forms.length > 0) { const dayAliases: Record = { - 'monday': 'monday', 'mon': 'monday', '1': 'monday', - 'tuesday': 'tuesday', 'tue': 'tuesday', '2': 'tuesday', - 'wednesday': 'wednesday', 'wed': 'wednesday', '3': 'wednesday', - 'thursday': 'thursday', 'thu': 'thursday', '4': 'thursday', - 'friday': 'friday', 'fri': 'friday', '5': 'friday', - 'saturday': 'saturday', 'sat': 'saturday', '6': 'saturday', + 'monday': 'monday', 'tuesday': 'tuesday', 'wednesday': 'wednesday', + 'thursday': 'thursday', 'friday': 'friday', + }; + + const periodMap: Record> = { + monday: {}, tuesday: {}, wednesday: {}, thursday: {}, friday: {} }; forms.forEach(form => { @@ -708,7 +752,7 @@ export async function fetchImsTimetable(_token: string, regNumber: string, _forc const day = (dayInput?.value || '').toLowerCase().trim(); 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]; 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) { const fullTxt = form.textContent?.trim() || ''; if (fullTxt) rawSubject = fullTxt.replace(/\s+/g, ' ').slice(0, 60); @@ -752,39 +797,66 @@ export async function fetchImsTimetable(_token: string, regNumber: string, _forc 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 isLab = rawSubLower.includes('lab') || rawSubLower.includes('laboratory'); - const periodItem: TimetablePeriod = { - periodNumber: period, - timeSlot: (period - 1) < timeSlots.length ? timeSlots[period - 1] : `Period ${period}`, - startTime: (period - 1) < startTimes.length ? startTimes[period - 1] : '', - endTime: (period - 1) < endTimes.length ? endTimes[period - 1] : '', - subjectCode, - subjectName, - staffName, - type: isLab ? 'LAB' : 'THEORY', - }; + const periodIndex = period - 1; + const existing = periodMap[normalizedDay][period]; - const existingList = weeklySchedule[normalizedDay][period] || []; - const isDuplicate = existingList.some( - p => p.subjectCode === subjectCode && p.staffName === staffName && p.subjectName === subjectName - ); - - if (!isDuplicate) { - existingList.push(periodItem); - weeklySchedule[normalizedDay][period] = existingList; + if (!existing) { + periodMap[normalizedDay][period] = { + periodNumber: period, + timeSlot: periodIndex < timeSlots.length ? timeSlots[periodIndex] : `Period ${period}`, + startTime: periodIndex < startTimes.length ? startTimes[periodIndex] : '', + endTime: periodIndex < endTimes.length ? endTimes[periodIndex] : '', + subjectCode, + subjectName, + staffName, + type: isLab ? 'LAB' : 'THEORY', + }; + } else { + // Merge staff names if same course or elective slot + if (staffName && !existing.staffName.includes(staffName)) { + existing.staffName += `, ${staffName}`; + } } }); + + // 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) { console.warn('Direct timetable scrape failed:', err); } // Fallback to hardcoded schedule only if scraping returned nothing - const totalPeriods = Object.values(weeklySchedule).reduce((acc: number, daySched: any) => { - return acc + Object.values(daySched || {}).reduce((dAcc: number, pList: any) => dAcc + (pList?.length || 0), 0); - }, 0); + const totalPeriods = Object.values(weeklySchedule).reduce((acc, periods) => acc + periods.length, 0); if (totalPeriods === 0) { const deduced = deduceStudentDetails(regNumber || '0000000000000'); 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); return { ...grades, student: session.profile }; } + +/** + * Fetch Assignment Marks from /ims/admin/assignment/student/mark/report + */ +export async function fetchImsAssignmentMarks(): Promise { + 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 { + 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 []; + } +}