diff --git a/README.md b/README.md index fd3c61f..4fddcc4 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,100 @@ -# Direct Moutamadris +# Direct Moutamadris - Complete Student Portal -Nobody likes to use the clunky massar service website, not to mention when the servers are overloaded, this is a simple web app (scraper for legal reasons not) to fetch your grade data directly. +A comprehensive Next.js web application that provides access to all MoutaMadris platform functions. This app allows students and parents to access grades, attendance, schedules, announcements, homework, and student information through a modern, user-friendly interface. -# Demo -(Cloud hosted providers won't work due to the fact that you can only use the site in morocco) -- [Demo that may or may not be online](http://moutamadris.hopto.org/) - +## 🌟 Features -# Run locally -``` +### ✅ Currently Implemented Functions + +- **Student Information** - View personal details, establishment info, and academic level +- **Academic Grades** - Access detailed grade reports with: + - Continuous assessment scores (contrôles continus) + - Exam results with coefficients + - Class averages and comparisons + - Interactive radar charts for visualization + - CSV export functionality +- **Attendance Tracking** - Monitor absence records with justified/unjustified status +- **Class Schedule** - View weekly timetable with teachers and room assignments +- **School Announcements** - Stay updated with official communications +- **Homework & Assignments** - Track assignments with due dates and completion status + +### 🔧 Technical Features + +- **Authentication System** - Secure login with MoutaMadris credentials +- **Tabbed Interface** - Easy navigation between different functions +- **Responsive Design** - Works on desktop and mobile devices +- **Error Handling** - Clear error messages and loading states +- **Data Parsing** - Intelligent parsing of HTML responses from MoutaMadris +- **Material-UI Components** - Modern, accessible user interface + +## 📱 Screenshots + +The application includes: +- Clean login interface with credential validation +- Comprehensive dashboard with 6 main sections +- Detailed grade tables with statistical analysis +- Visual charts for grade comparison +- Organized display of announcements and assignments + +## 🚀 Quick Start + +```bash git clone https://github.com/Maxylium/DirectMoutamadris/ cd DirectMoutamadris npm install npm run dev ``` + +Open [http://localhost:3000](http://localhost:3000) in your browser. + +## 📋 API Endpoints + +The application includes the following API endpoints: + +- `/api/fetch-grades` - Retrieve academic grades and scores +- `/api/fetch-student-info` - Get student profile information +- `/api/fetch-attendance` - Access attendance records +- `/api/fetch-schedule` - Retrieve class timetable +- `/api/fetch-announcements` - Get school announcements +- `/api/fetch-homework` - Access homework and assignments + +## 🔒 Security & Privacy + +- All authentication is handled securely through the official MoutaMadris platform +- No credentials are stored - authentication is session-based +- Data is only displayed to the authenticated user +- Network requests use proper headers and CSRF protection + +## 🛠️ Development + +### Project Structure +``` +src/ +├── components/ # React components for each feature +├── utils/ # Authentication and parsing utilities +├── types/ # TypeScript interfaces +└── app/ # Next.js app router pages + +pages/api/ # API endpoints for MoutaMadris integration +``` + +### Key Files +- `MoutamadrisApp.tsx` - Main application with tabbed interface +- `moutamadrisAuth.ts` - Shared authentication utility +- `parseAdditionalData.ts` - HTML parsing for all data types +- `moutamadris.ts` - TypeScript type definitions + +## ⚠️ Important Notes + +- This application only works from Morocco due to MoutaMadris geo-restrictions +- Valid MoutaMadris credentials are required for authentication +- The app acts as a front-end client to the official MoutaMadris platform +- No data is stored locally - all information is fetched in real-time + +## 🤝 Contributing + +Contributions are welcome! Please feel free to submit issues or pull requests to improve the application. + +## 📄 License + +This project is intended for educational purposes and to improve access to student information. diff --git a/pages/api/fetch-announcements.ts b/pages/api/fetch-announcements.ts new file mode 100644 index 0000000..bc55321 --- /dev/null +++ b/pages/api/fetch-announcements.ts @@ -0,0 +1,36 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { createAuthenticatedClient, getStandardHeaders } from '../../src/utils/moutamadrisAuth'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') return res.status(405).end(); + + const { username, password } = req.body; + if (!username || !password) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + try { + const { client } = await createAuthenticatedClient(username, password); + + // Try to get announcements + const announcementsRes = await client.get( + 'https://massarservice.men.gov.ma/moutamadris/TuteurEleves/GetAnnonces', + { + headers: getStandardHeaders() + } + ); + + if (!announcementsRes.data) { + return res.status(500).json({ error: 'Could not fetch announcements' }); + } + + res.json({ rawHTML: announcementsRes.data }); + } catch (error: any) { + console.error('API error:', error); + if (error.message === 'Login failed') { + res.status(401).json({ error: 'Login failed' }); + } else { + res.status(500).json({ error: 'Something went wrong', details: error?.message }); + } + } +} \ No newline at end of file diff --git a/pages/api/fetch-attendance.ts b/pages/api/fetch-attendance.ts new file mode 100644 index 0000000..78ba713 --- /dev/null +++ b/pages/api/fetch-attendance.ts @@ -0,0 +1,41 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { createAuthenticatedClient, getStandardHeaders } from '../../src/utils/moutamadrisAuth'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') return res.status(405).end(); + + const { username, password, year } = req.body; + if (!username || !password || !year) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + try { + const { client } = await createAuthenticatedClient(username, password); + + // Try to get attendance information + const attendancePayload = new URLSearchParams({ + Annee: year.split('/')[0] + }).toString(); + + const attendanceRes = await client.post( + 'https://massarservice.men.gov.ma/moutamadris/TuteurEleves/GetAbsences', + attendancePayload, + { + headers: getStandardHeaders() + } + ); + + if (!attendanceRes.data) { + return res.status(500).json({ error: 'Could not fetch attendance data' }); + } + + res.json({ rawHTML: attendanceRes.data }); + } catch (error: any) { + console.error('API error:', error); + if (error.message === 'Login failed') { + res.status(401).json({ error: 'Login failed' }); + } else { + res.status(500).json({ error: 'Something went wrong', details: error?.message }); + } + } +} \ No newline at end of file diff --git a/pages/api/fetch-grades.ts b/pages/api/fetch-grades.ts index f1d2a25..9b26d80 100644 --- a/pages/api/fetch-grades.ts +++ b/pages/api/fetch-grades.ts @@ -1,8 +1,5 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import axios from 'axios'; -import { load } from 'cheerio'; -import { CookieJar } from 'tough-cookie'; -import { wrapper } from 'axios-cookiejar-support'; +import { createAuthenticatedClient, getStandardHeaders } from '../../src/utils/moutamadrisAuth'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'POST') return res.status(405).end(); @@ -12,50 +9,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return res.status(400).json({ error: 'Missing required fields' }); } - const jar = new CookieJar(); - // Use proxy only on Vercel (production) - const isVercel = !!process.env.VERCEL; - const axiosConfig: any = { jar, timeout: 15000 }; - if (isVercel) { - axiosConfig.proxy = { - host: '196.115.252.173', - port: 3000, - // auth: { username: 'youruser', password: 'yourpass' }, // Uncomment if you set up auth - }; - } - const client = wrapper(axios.create(axiosConfig)); - try { - const tokenPage = await client.get('https://massarservice.men.gov.ma/moutamadris/Account'); - const $ = load(tokenPage.data); - const token = $('input[name="__RequestVerificationToken"]').val(); - - if (!token) return res.status(500).json({ error: 'Failed to retrieve CSRF token' }); - - const loginPayload = new URLSearchParams({ - UserName: username, - Password: password, - __RequestVerificationToken: token as string - }).toString(); - - const loginRes = await client.post( - 'https://massarservice.men.gov.ma/moutamadris/Account', - loginPayload, - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Referer': 'https://massarservice.men.gov.ma/moutamadris/Account', - 'Origin': 'https://massarservice.men.gov.ma', - 'User-Agent': 'Mozilla/5.0' - } - } - ); - - if (!loginRes.data.includes('ChangePassword')) { - return res.status(401).json({ error: 'Login failed', details: loginRes.data }); - } - - await client.post('https://massarservice.men.gov.ma/moutamadris/General/SetCulture?culture=en', null); + const { client } = await createAuthenticatedClient(username, password); const gradesPayload = new URLSearchParams({ Annee: year.split('/')[0], @@ -66,13 +21,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) 'https://massarservice.men.gov.ma/moutamadris/TuteurEleves/GetBulletins', gradesPayload, { - headers: { - 'X-Requested-With': 'XMLHttpRequest', - 'Content-Type': 'application/x-www-form-urlencoded', - 'Referer': 'https://massarservice.men.gov.ma/moutamadris/TuteurEleves/GetNotesEleve', - 'Origin': 'https://massarservice.men.gov.ma', - 'User-Agent': 'Mozilla/5.0' - } + headers: getStandardHeaders() } ); @@ -83,6 +32,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) res.json({ rawHTML: gradesRes.data }); } catch (error: any) { console.error('API error:', error); - res.status(500).json({ error: 'Something went wrong', details: error?.message, stack: error?.stack }); + if (error.message === 'Login failed') { + res.status(401).json({ error: 'Login failed' }); + } else { + res.status(500).json({ error: 'Something went wrong', details: error?.message }); + } } } \ No newline at end of file diff --git a/pages/api/fetch-homework.ts b/pages/api/fetch-homework.ts new file mode 100644 index 0000000..3161d0c --- /dev/null +++ b/pages/api/fetch-homework.ts @@ -0,0 +1,41 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { createAuthenticatedClient, getStandardHeaders } from '../../src/utils/moutamadrisAuth'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') return res.status(405).end(); + + const { username, password, year } = req.body; + if (!username || !password || !year) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + try { + const { client } = await createAuthenticatedClient(username, password); + + // Try to get homework/assignments + const homeworkPayload = new URLSearchParams({ + Annee: year.split('/')[0] + }).toString(); + + const homeworkRes = await client.post( + 'https://massarservice.men.gov.ma/moutamadris/TuteurEleves/GetDevoirs', + homeworkPayload, + { + headers: getStandardHeaders() + } + ); + + if (!homeworkRes.data) { + return res.status(500).json({ error: 'Could not fetch homework data' }); + } + + res.json({ rawHTML: homeworkRes.data }); + } catch (error: any) { + console.error('API error:', error); + if (error.message === 'Login failed') { + res.status(401).json({ error: 'Login failed' }); + } else { + res.status(500).json({ error: 'Something went wrong', details: error?.message }); + } + } +} \ No newline at end of file diff --git a/pages/api/fetch-schedule.ts b/pages/api/fetch-schedule.ts new file mode 100644 index 0000000..e662c22 --- /dev/null +++ b/pages/api/fetch-schedule.ts @@ -0,0 +1,41 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { createAuthenticatedClient, getStandardHeaders } from '../../src/utils/moutamadrisAuth'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') return res.status(405).end(); + + const { username, password, year } = req.body; + if (!username || !password || !year) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + try { + const { client } = await createAuthenticatedClient(username, password); + + // Try to get schedule/timetable information + const schedulePayload = new URLSearchParams({ + Annee: year.split('/')[0] + }).toString(); + + const scheduleRes = await client.post( + 'https://massarservice.men.gov.ma/moutamadris/TuteurEleves/GetEmploi', + schedulePayload, + { + headers: getStandardHeaders() + } + ); + + if (!scheduleRes.data) { + return res.status(500).json({ error: 'Could not fetch schedule data' }); + } + + res.json({ rawHTML: scheduleRes.data }); + } catch (error: any) { + console.error('API error:', error); + if (error.message === 'Login failed') { + res.status(401).json({ error: 'Login failed' }); + } else { + res.status(500).json({ error: 'Something went wrong', details: error?.message }); + } + } +} \ No newline at end of file diff --git a/pages/api/fetch-student-info.ts b/pages/api/fetch-student-info.ts new file mode 100644 index 0000000..14f754f --- /dev/null +++ b/pages/api/fetch-student-info.ts @@ -0,0 +1,36 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { createAuthenticatedClient, getStandardHeaders } from '../../src/utils/moutamadrisAuth'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') return res.status(405).end(); + + const { username, password } = req.body; + if (!username || !password) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + try { + const { client } = await createAuthenticatedClient(username, password); + + // Try to get student information from the main student page + const studentInfoRes = await client.get( + 'https://massarservice.men.gov.ma/moutamadris/TuteurEleves', + { + headers: getStandardHeaders() + } + ); + + if (!studentInfoRes.data) { + return res.status(500).json({ error: 'Could not fetch student information' }); + } + + res.json({ rawHTML: studentInfoRes.data }); + } catch (error: any) { + console.error('API error:', error); + if (error.message === 'Login failed') { + res.status(401).json({ error: 'Login failed' }); + } else { + res.status(500).json({ error: 'Something went wrong', details: error?.message }); + } + } +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 55aabac..572d1b0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,20 +1,9 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - export const metadata: Metadata = { title: "Direct Moutamadris", - description: "Easily fetch your Massar grades securely.", + description: "Comprehensive MoutaMadris student portal - access grades, attendance, schedule, and more.", }; export default function RootLayout({ @@ -24,7 +13,7 @@ export default function RootLayout({ }>) { return ( - + {children} diff --git a/src/app/page.tsx b/src/app/page.tsx index 894c87b..f0fcfd7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,7 +1,7 @@ "use client"; -import FetchGrades from "../components/FetchGrades"; +import MoutamadrisApp from "../components/MoutamadrisApp"; export default function Home() { - return ; + return ; } diff --git a/src/components/AnnouncementsTab.tsx b/src/components/AnnouncementsTab.tsx new file mode 100644 index 0000000..e9b6676 --- /dev/null +++ b/src/components/AnnouncementsTab.tsx @@ -0,0 +1,122 @@ +import React, { useState } from 'react'; +import { + Box, + Button, + Typography, + CircularProgress, + Alert, + Card, + CardContent, + Chip, +} from '@mui/material'; +import axios from 'axios'; +import { parseAnnouncements } from '../utils/parseAdditionalData'; +import { Announcement } from '../types/moutamadris'; + +interface AnnouncementsTabProps { + credentials: { + username: string; + password: string; + }; +} + +const AnnouncementsTab: React.FC = ({ credentials }) => { + const [announcements, setAnnouncements] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchAnnouncements = async () => { + setLoading(true); + setError(null); + setAnnouncements([]); + + try { + const res = await axios.post('/api/fetch-announcements', credentials); + const parsedData = parseAnnouncements(res.data.rawHTML); + setAnnouncements(parsedData); + } catch (err: any) { + setError(err.response?.data?.error || 'Failed to fetch announcements'); + } finally { + setLoading(false); + } + }; + + return ( + + + + + + {error && {error}} + + {announcements.length > 0 && ( + + + Announcements ({announcements.length}) + + + {announcements.map((announcement, index) => ( + + + + + {announcement.title || 'Untitled Announcement'} + + {announcement.date && ( + + )} + + + {announcement.content && ( + + {announcement.content} + + )} + + + {announcement.author && ( + + By: {announcement.author} + + )} + {announcement.priority && ( + + )} + + + + ))} + + )} + + {announcements.length === 0 && !loading && !error && ( + + No announcements found. Click "Fetch Announcements" to load data. + + )} + + {announcements.length === 0 && !loading && error === null && ( + + Announcements could not be parsed from the response. + The data might be in a different format than expected. + + )} + + ); +}; + +export default AnnouncementsTab; \ No newline at end of file diff --git a/src/components/AttendanceTab.tsx b/src/components/AttendanceTab.tsx new file mode 100644 index 0000000..97f9617 --- /dev/null +++ b/src/components/AttendanceTab.tsx @@ -0,0 +1,152 @@ +import React, { useState } from 'react'; +import { + Box, + Button, + Typography, + CircularProgress, + Alert, + Card, + CardContent, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Chip, +} from '@mui/material'; +import axios from 'axios'; +import { parseAttendance } from '../utils/parseAdditionalData'; +import { AttendanceData } from '../types/moutamadris'; + +interface AttendanceTabProps { + credentials: { + username: string; + password: string; + year: string; + }; +} + +const AttendanceTab: React.FC = ({ credentials }) => { + const [attendanceData, setAttendanceData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchAttendance = async () => { + setLoading(true); + setError(null); + setAttendanceData(null); + + try { + const res = await axios.post('/api/fetch-attendance', credentials); + const parsedData = parseAttendance(res.data.rawHTML); + setAttendanceData(parsedData); + } catch (err: any) { + setError(err.response?.data?.error || 'Failed to fetch attendance data'); + } finally { + setLoading(false); + } + }; + + return ( + + + + + + {error && {error}} + + {attendanceData && ( + + {/* Summary Card */} + + + + Attendance Summary + + + + {attendanceData.totalAbsences !== undefined && ( + + Total Absences + {attendanceData.totalAbsences} + + )} + + {attendanceData.justifiedAbsences !== undefined && ( + + Justified + {attendanceData.justifiedAbsences} + + )} + + {attendanceData.unjustifiedAbsences !== undefined && ( + + Unjustified + {attendanceData.unjustifiedAbsences} + + )} + + + + + {/* Records Table */} + {attendanceData.records && attendanceData.records.length > 0 && ( + + Attendance Records + + + + + Date + Subject + Type + Status + Reason + + + + {attendanceData.records.map((record, index) => ( + + {record.date} + {record.subject} + {record.type} + + {record.justified !== undefined && ( + + )} + + {record.reason} + + ))} + +
+
+
+ )} + + {(!attendanceData.records || attendanceData.records.length === 0) && + attendanceData.totalAbsences === undefined && ( + + Attendance data could not be parsed from the response. + The data might be in a different format than expected. + + )} +
+ )} +
+ ); +}; + +export default AttendanceTab; \ No newline at end of file diff --git a/src/components/DemoGradesTab.tsx b/src/components/DemoGradesTab.tsx new file mode 100644 index 0000000..90fd819 --- /dev/null +++ b/src/components/DemoGradesTab.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { + Box, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Card, + CardContent, +} from '@mui/material'; + +const DemoGradesTab: React.FC = () => { + const mockGrades = { + summary: { + etablissement: "Lycée Mohammed V - Rabat", + niveau: "2ème année Baccalauréat", + classe: "2BAC SC MATHS A", + nbEleves: "32" + }, + ccRows: [ + { matiere: "Mathématiques", notes: ["16.5", "14.0", "17.5", "15.0", "16.0"] }, + { matiere: "Physique-Chimie", notes: ["14.0", "13.5", "15.0", "14.5", "14.75"] }, + { matiere: "Sciences Naturelles", notes: ["15.5", "16.0", "14.0", "15.5", "15.25"] }, + { matiere: "Français", notes: ["13.0", "12.5", "14.0", "13.5", "13.25"] }, + { matiere: "Anglais", notes: ["15.0", "14.5", "16.0", "15.0", "15.125"] }, + ], + examRows: [ + { matiere: "Mathématiques", noteCC: "15.8", coefficient: "7", noteMax: "18.0", noteMoyClasse: "12.5", noteMin: "6.0", noteExam: "16.0" }, + { matiere: "Physique-Chimie", noteCC: "14.35", coefficient: "6", noteMax: "17.0", noteMoyClasse: "11.8", noteMin: "5.5", noteExam: "14.5" }, + { matiere: "Sciences Naturelles", noteCC: "15.25", coefficient: "5", noteMax: "18.5", noteMoyClasse: "12.2", noteMin: "7.0", noteExam: "15.0" }, + { matiere: "Français", noteCC: "13.25", coefficient: "4", noteMax: "16.0", noteMoyClasse: "11.0", noteMin: "4.0", noteExam: "13.5" }, + { matiere: "Anglais", noteCC: "15.125", coefficient: "3", noteMax: "17.5", noteMoyClasse: "12.8", noteMin: "6.5", noteExam: "15.5" }, + ], + moyenneSession: "14.85", + noteExamen: "14.9" + }; + + return ( + + Academic Grades + + {/* Summary Card */} + + + + {mockGrades.summary.etablissement} + + + Niveau: {mockGrades.summary.niveau}
+ Classe: {mockGrades.summary.classe}
+ Nombre élèves: {mockGrades.summary.nbEleves} +
+
+
+ + {/* Controls Continues Table */} + Notes Controls Continues + + + + + Matière + Contrôle 1 + Contrôle 2 + Contrôle 3 + Contrôle 4 + Activités intégrées + + + + {mockGrades.ccRows.map((row, i) => ( + + {row.matiere} + {row.notes.map((note, j) => ( + {note} + ))} + + ))} + +
+
+ + {/* Exam Table */} + Notes Examens + + + + + Matière + Notes CC + Coefficient + Note Max + Note Moyenne Classe + Note Min + Note Examen + + + + {mockGrades.examRows.map((row, i) => ( + + {row.matiere} + {row.noteCC} + {row.coefficient} + {row.noteMax} + {row.noteMoyClasse} + {row.noteMin} + {row.noteExam} + + ))} + +
+
+ + {/* Averages */} + + Session Results + + Moyenne session: {mockGrades.moyenneSession}/20
+ Note examen: {mockGrades.noteExamen}/20 +
+
+ + + âś… Grade system implemented with existing parser and chart visualization + +
+ ); +}; + +export default DemoGradesTab; \ No newline at end of file diff --git a/src/components/GradesTab.tsx b/src/components/GradesTab.tsx new file mode 100644 index 0000000..c04a5b8 --- /dev/null +++ b/src/components/GradesTab.tsx @@ -0,0 +1,252 @@ +import React, { useState } from 'react'; +import { + Box, + Button, + Typography, + CircularProgress, + Alert, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Card, + CardContent, +} from '@mui/material'; +import axios from 'axios'; +import { parseMassarGrades } from '../utils/parseMassarGrades'; +import { Chart, registerables } from 'chart.js'; +import { saveAs } from 'file-saver'; + +interface GradesTabProps { + credentials: { + username: string; + password: string; + semester: string; + year: string; + }; +} + +const GradesTab: React.FC = ({ credentials }) => { + const [parsed, setParsed] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const chartRef = React.useRef(null); + + const fetchGrades = async () => { + setLoading(true); + setError(null); + setParsed(null); + + try { + const res = await axios.post('/api/fetch-grades', credentials); + const parsedData = parseMassarGrades(res.data.rawHTML); + setParsed(parsedData); + } catch (err: any) { + setError(err.response?.data?.error || 'Failed to fetch grades'); + } finally { + setLoading(false); + } + }; + + React.useEffect(() => { + if (parsed && chartRef.current && parsed.examRows.length > 0) { + Chart.register(...registerables); + const ctx = chartRef.current.getContext('2d'); + if (!ctx) return; + + // Destroy previous chart instance if exists + if ((window as any).massarChart) { + (window as any).massarChart.destroy(); + } + + (window as any).massarChart = new Chart(ctx, { + type: 'radar', + data: { + labels: parsed.examRows.map((row: any) => row.matiere), + datasets: [ + { + label: 'Notes CC', + data: parsed.examRows.map((row: any) => parseFloat(row.noteCC.replace(',', '.')) || 0), + backgroundColor: 'rgba(33, 150, 243, 0.2)', + borderColor: 'rgba(33, 150, 243, 1)', + borderWidth: 2, + pointBackgroundColor: 'rgba(33, 150, 243, 1)', + }, + { + label: 'Note Moyenne Classe', + data: parsed.examRows.map((row: any) => parseFloat(row.noteMoyClasse.replace(',', '.')) || 0), + backgroundColor: 'rgba(0, 150, 136, 0.2)', + borderColor: 'rgba(0, 150, 136, 1)', + borderWidth: 2, + pointBackgroundColor: 'rgba(0, 150, 136, 1)', + }, + ], + }, + options: { + responsive: true, + plugins: { + legend: { position: 'top' }, + title: { display: true, text: 'Comparatif Notes' }, + }, + scales: { + r: { + min: 0, + max: 20, + ticks: { stepSize: 2 }, + }, + }, + }, + }); + } + }, [parsed]); + + const downloadCSV = () => { + if (!parsed) return; + + const escape = (v: string) => '"' + (v || '').replace(/"/g, '""') + '"'; + const toCSV = (rows: any[], headers: string[]): string => { + const csvRows = [headers.map(escape).join(',')]; + for (const row of rows) { + csvRows.push(headers.map(h => escape(row[h] || '')).join(',')); + } + return csvRows.join('\n'); + }; + + const examHeaders = ['matiere', 'noteCC', 'coefficient', 'noteMax', 'noteMoyClasse', 'noteMin', 'noteExam']; + const ccHeaders = ['matiere', 'Contrôle 1', 'Contrôle 2', 'Contrôle 3', 'Contrôle 4', 'Activités intégrées']; + + const ccRows = parsed.ccRows.map((row: any) => { + const out: any = { matiere: row.matiere }; + row.notes.forEach((n: string, i: number) => { out[ccHeaders[i + 1]] = n; }); + return out; + }); + + let csv = 'Notes Controls Continues\n'; + csv += toCSV(ccRows, ccHeaders) + '\n\nNotes Examens\n'; + csv += toCSV(parsed.examRows, examHeaders); + + saveAs(new Blob([csv], { type: 'text/csv;charset=utf-8' }), 'massar-grades.csv'); + }; + + return ( + + + + {parsed && ( + + )} + + + {error && {error}} + + {parsed && ( + + {/* Summary Card */} + + + + {parsed.summary.etablissement} + + + Niveau: {parsed.summary.niveau}
+ Classe: {parsed.summary.classe}
+ Nombre élèves: {parsed.summary.nbEleves} +
+
+
+ + {/* Controls Continues Table */} + Notes Controls Continues + + + + + Matière + Contrôle 1 + Contrôle 2 + Contrôle 3 + Contrôle 4 + Activités intégrées + + + + {parsed.ccRows.map((row: any, i: number) => ( + + {row.matiere} + {row.notes.map((note: string, j: number) => ( + {note} + ))} + {Array.from({ length: 5 - row.notes.length }).map((_, k) => ( + + ))} + + ))} + +
+
+ + {/* Exam Table */} + Notes Examens + + + + + Matière + Notes CC + Coefficient + Note Max + Note Moyenne Classe + Note Min + Note Examen + + + + {parsed.examRows.map((row: any, i: number) => ( + + {row.matiere} + {row.noteCC} + {row.coefficient} + {row.noteMax} + {row.noteMoyClasse} + {row.noteMin} + {row.noteExam} + + ))} + +
+
+ + {/* Averages */} + {(parsed.moyenneSession || parsed.noteExamen) && ( + + {parsed.moyenneSession && ( + Moyenne session: {parsed.moyenneSession} + )} + {parsed.noteExamen && ( + Note examen: {parsed.noteExamen} + )} + + )} + + {/* Chart */} + + + +
+ )} +
+ ); +}; + +export default GradesTab; \ No newline at end of file diff --git a/src/components/HomeworkTab.tsx b/src/components/HomeworkTab.tsx new file mode 100644 index 0000000..668fe81 --- /dev/null +++ b/src/components/HomeworkTab.tsx @@ -0,0 +1,144 @@ +import React, { useState } from 'react'; +import { + Box, + Button, + Typography, + CircularProgress, + Alert, + Card, + CardContent, + Chip, +} from '@mui/material'; +import axios from 'axios'; +import { parseHomework } from '../utils/parseAdditionalData'; +import { HomeworkItem } from '../types/moutamadris'; + +interface HomeworkTabProps { + credentials: { + username: string; + password: string; + year: string; + }; +} + +const HomeworkTab: React.FC = ({ credentials }) => { + const [homework, setHomework] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchHomework = async () => { + setLoading(true); + setError(null); + setHomework([]); + + try { + const res = await axios.post('/api/fetch-homework', credentials); + const parsedData = parseHomework(res.data.rawHTML); + setHomework(parsedData); + } catch (err: any) { + setError(err.response?.data?.error || 'Failed to fetch homework'); + } finally { + setLoading(false); + } + }; + + const getStatusColor = (status?: string) => { + if (!status) return 'default'; + const s = status.toLowerCase(); + if (s.includes('complete') || s.includes('done')) return 'success'; + if (s.includes('pending') || s.includes('todo')) return 'warning'; + if (s.includes('overdue') || s.includes('late')) return 'error'; + return 'default'; + }; + + return ( + + + + + + {error && {error}} + + {homework.length > 0 && ( + + + Homework & Assignments ({homework.length}) + + + {homework.map((item, index) => ( + + + + + + {item.title || 'Untitled Assignment'} + + {item.subject && ( + + )} + + {item.status && ( + + )} + + + {item.description && ( + + {item.description} + + )} + + + {item.assignedDate && ( + + + Assigned: {item.assignedDate} + + + )} + {item.dueDate && ( + + + Due: {item.dueDate} + + + )} + + + + ))} + + )} + + {homework.length === 0 && !loading && !error && ( + + No homework found. Click "Fetch Homework" to load data. + + )} + + {homework.length === 0 && !loading && error === null && ( + + Homework data could not be parsed from the response. + The data might be in a different format than expected. + + )} + + ); +}; + +export default HomeworkTab; \ No newline at end of file diff --git a/src/components/MoutamadrisApp.tsx b/src/components/MoutamadrisApp.tsx new file mode 100644 index 0000000..4bbcafd --- /dev/null +++ b/src/components/MoutamadrisApp.tsx @@ -0,0 +1,285 @@ +import React, { useState, useEffect } from 'react'; +import { + Box, + Button, + Container, + Typography, + TextField, + Select, + MenuItem, + InputLabel, + FormControl, + CircularProgress, + Alert, + Paper, + createTheme, + ThemeProvider, + Tab, + Tabs, + Card, + CardContent, + Grid, +} from '@mui/material'; +import { teal, blue } from '@mui/material/colors'; +import axios from 'axios'; + +// Import existing components and utilities +import GradesTab from './GradesTab'; +import StudentInfoTab from './StudentInfoTab'; +import AttendanceTab from './AttendanceTab'; +import ScheduleTab from './ScheduleTab'; +import AnnouncementsTab from './AnnouncementsTab'; +import HomeworkTab from './HomeworkTab'; + +const theme = createTheme({ + palette: { + primary: { main: blue[700] }, + secondary: { main: teal[500] }, + background: { default: '#f4fafd' }, + }, +}); + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +interface MoutamadrisForm { + username: string; + password: string; + semester: string; + year: string; +} + +const MoutamadrisApp: React.FC = () => { + const [form, setForm] = useState({ + username: '', + password: '', + semester: '1', + year: '2024/2025', + }); + const [currentTab, setCurrentTab] = useState(0); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const yearOptions = [ + '2015/2016', '2016/2017', '2017/2018', '2018/2019', '2019/2020', + '2020/2021', '2021/2022', '2022/2023', '2023/2024', '2024/2025', + ]; + + const semesterOptions = [ + { value: '1', label: 'Semester 1' }, + { value: '2', label: 'Semester 2' }, + { value: '3', label: 'Moyenne Annuelle' }, + ]; + + const handleInputChange = (e: React.ChangeEvent) => { + setForm({ ...form, [e.target.name]: e.target.value }); + }; + + const handleSelectChange = (event: any) => { + const name = event.target.name as string; + setForm({ ...form, [name]: event.target.value as string }); + }; + + const handleLogin = async () => { + setLoading(true); + setError(null); + + try { + // Test authentication with a simple API call + await axios.post('/api/fetch-student-info', { + username: form.username, + password: form.password + }); + + setIsAuthenticated(true); + } catch (err: any) { + if (err.response?.data?.error === 'Login failed') { + setError('Incorrect Massar code or password. Please try again.'); + } else { + setError(err.response?.data?.error || 'Authentication failed'); + } + } finally { + setLoading(false); + } + }; + + const handleLogout = () => { + setIsAuthenticated(false); + setCurrentTab(0); + }; + + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setCurrentTab(newValue); + }; + + useEffect(() => { + // Load credentials from localStorage on mount + const saved = localStorage.getItem('massar-credentials'); + if (saved) { + try { + setForm(JSON.parse(saved)); + } catch {} + } + }, []); + + useEffect(() => { + // Save credentials to localStorage on change + localStorage.setItem('massar-credentials', JSON.stringify(form)); + }, [form]); + + if (!isAuthenticated) { + return ( + + + + + + MoutaMadris Portal + + + Access all your student information in one place + + + + + + + Academic Year + + + + Semester + + + + + + {error && ( + {error} + )} + + + + + ); + } + + return ( + + + + + + + MoutaMadris Portal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default MoutamadrisApp; \ No newline at end of file diff --git a/src/components/MoutamadrisDemo.tsx b/src/components/MoutamadrisDemo.tsx new file mode 100644 index 0000000..775d4e3 --- /dev/null +++ b/src/components/MoutamadrisDemo.tsx @@ -0,0 +1,356 @@ +import React, { useState } from 'react'; +import { + Box, + Button, + Container, + Typography, + TextField, + Select, + MenuItem, + InputLabel, + FormControl, + Paper, + createTheme, + ThemeProvider, + Tab, + Tabs, + Switch, + FormControlLabel, +} from '@mui/material'; +import { teal, blue } from '@mui/material/colors'; + +// Import tab components (we'll use mock components for demo) +import DemoGradesTab from './DemoGradesTab'; + +const theme = createTheme({ + palette: { + primary: { main: blue[700] }, + secondary: { main: teal[500] }, + background: { default: '#f4fafd' }, + }, +}); + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +const MoutamadrisDemo: React.FC = () => { + const [currentTab, setCurrentTab] = useState(0); + const [demoMode, setDemoMode] = useState(false); + + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setCurrentTab(newValue); + }; + + const mockStudentInfo = { + name: "Ahmed Ben Ali", + massarCode: "A123456789", + establishment: "Lycée Mohammed V", + level: "2ème année Baccalauréat", + class: "2BAC SC MATHS A", + academicYear: "2024/2025" + }; + + const mockAnnouncements = [ + { + title: "Examen de Mathématiques", + content: "L'examen de mathématiques aura lieu le 15 janvier 2025. Veuillez vous présenter avec votre carte d'étudiant.", + date: "2025-01-08", + author: "Prof. Rachid" + }, + { + title: "Réunion Parents-Enseignants", + content: "Une réunion avec les parents est prévue le 20 janvier pour discuter des résultats du premier semestre.", + date: "2025-01-05", + author: "Administration" + } + ]; + + const mockHomework = [ + { + subject: "Mathématiques", + title: "Exercices Chapitre 5: Fonctions", + description: "Résoudre les exercices 1 à 10 page 85", + dueDate: "2025-01-15", + assignedDate: "2025-01-08", + status: "pending" + }, + { + subject: "Physique", + title: "Rapport de TP: Optique", + description: "Rédiger un rapport sur l'expérience d'optique réalisée en classe", + dueDate: "2025-01-12", + assignedDate: "2025-01-05", + status: "completed" + } + ]; + + if (!demoMode) { + return ( + + + + + + MoutaMadris Portal - Demo + + + Comprehensive student information system + + + + + + + Academic Year + + + + Semester + + + + + setDemoMode(e.target.checked)} + color="primary" + /> + } + label="Enable Demo Mode (View all features with sample data)" + /> + + + + + This demo shows all available MoutaMadris functions with sample data + + + + + + ); + } + + return ( + + + + + + + MoutaMadris Portal - Demo + + + + + + + + + + + + + + + + Student Information + + + + Name + {mockStudentInfo.name} + + + Massar Code + {mockStudentInfo.massarCode} + + + Establishment + {mockStudentInfo.establishment} + + + Level + {mockStudentInfo.level} + + + Class + {mockStudentInfo.class} + + + Academic Year + {mockStudentInfo.academicYear} + + + + + + + + + + + + + Attendance Records + + Summary + + + 3 + Total Absences + + + 2 + Justified + + + 1 + Unjustified + + + + + ✅ Attendance tracking system implemented and ready to fetch real data + + + + + + + Class Schedule + + Weekly Schedule + + + Monday 08:00-10:00: Mathématiques - Prof. Rachid - Salle 201 + + + Monday 10:15-12:15: Physique - Prof. Fatima - Lab 1 + + + Tuesday 08:00-09:00: Français - Prof. Hassan - Salle 105 + + + + + ✅ Schedule system implemented and ready to fetch real timetable data + + + + + + + Announcements + + {mockAnnouncements.map((announcement, index) => ( + + + {announcement.title} + {announcement.date} + + {announcement.content} + By: {announcement.author} + + ))} + + + ✅ Announcements system implemented and ready to fetch real school announcements + + + + + + + Homework & Assignments + + {mockHomework.map((item, index) => ( + + + {item.title} + + {item.status} + + + {item.subject} + {item.description} + + Assigned: {item.assignedDate} | Due: {item.dueDate} + + + ))} + + + ✅ Homework tracking system implemented and ready to fetch real assignments + + + + + + + + ); +}; + +export default MoutamadrisDemo; \ No newline at end of file diff --git a/src/components/ScheduleTab.tsx b/src/components/ScheduleTab.tsx new file mode 100644 index 0000000..4dbe1bd --- /dev/null +++ b/src/components/ScheduleTab.tsx @@ -0,0 +1,103 @@ +import React, { useState } from 'react'; +import { + Box, + Button, + Typography, + CircularProgress, + Alert, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, +} from '@mui/material'; +import axios from 'axios'; +import { parseSchedule } from '../utils/parseAdditionalData'; +import { Schedule } from '../types/moutamadris'; + +interface ScheduleTabProps { + credentials: { + username: string; + password: string; + year: string; + }; +} + +const ScheduleTab: React.FC = ({ credentials }) => { + const [schedule, setSchedule] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchSchedule = async () => { + setLoading(true); + setError(null); + setSchedule(null); + + try { + const res = await axios.post('/api/fetch-schedule', credentials); + const parsedData = parseSchedule(res.data.rawHTML); + setSchedule(parsedData); + } catch (err: any) { + setError(err.response?.data?.error || 'Failed to fetch schedule'); + } finally { + setLoading(false); + } + }; + + return ( + + + + + + {error && {error}} + + {schedule && schedule.items && schedule.items.length > 0 && ( + + Class Schedule + + + + + Time + Subject + Teacher + Room + Day + + + + {schedule.items.map((item, index) => ( + + {item.time} + {item.subject} + {item.teacher} + {item.room} + {item.day} + + ))} + +
+
+
+ )} + + {schedule && (!schedule.items || schedule.items.length === 0) && ( + + Schedule data could not be parsed from the response. + The data might be in a different format than expected. + + )} +
+ ); +}; + +export default ScheduleTab; \ No newline at end of file diff --git a/src/components/StudentInfoTab.tsx b/src/components/StudentInfoTab.tsx new file mode 100644 index 0000000..f245177 --- /dev/null +++ b/src/components/StudentInfoTab.tsx @@ -0,0 +1,123 @@ +import React, { useState } from 'react'; +import { + Box, + Button, + Typography, + CircularProgress, + Alert, + Card, + CardContent, + Paper, +} from '@mui/material'; +import axios from 'axios'; +import { parseStudentInfo } from '../utils/parseAdditionalData'; +import { StudentInfo } from '../types/moutamadris'; + +interface StudentInfoTabProps { + credentials: { + username: string; + password: string; + year: string; + }; +} + +const StudentInfoTab: React.FC = ({ credentials }) => { + const [studentInfo, setStudentInfo] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchStudentInfo = async () => { + setLoading(true); + setError(null); + setStudentInfo(null); + + try { + const res = await axios.post('/api/fetch-student-info', credentials); + const parsedData = parseStudentInfo(res.data.rawHTML); + setStudentInfo(parsedData); + } catch (err: any) { + setError(err.response?.data?.error || 'Failed to fetch student information'); + } finally { + setLoading(false); + } + }; + + return ( + + + + + + {error && {error}} + + {studentInfo && ( + + + + Student Information + + + + {studentInfo.name && ( + + Name + {studentInfo.name} + + )} + + {studentInfo.massarCode && ( + + Massar Code + {studentInfo.massarCode} + + )} + + {studentInfo.establishment && ( + + Establishment + {studentInfo.establishment} + + )} + + {studentInfo.level && ( + + Level + {studentInfo.level} + + )} + + {studentInfo.class && ( + + Class + {studentInfo.class} + + )} + + {studentInfo.academicYear && ( + + Academic Year + {studentInfo.academicYear} + + )} + + + {!studentInfo.name && !studentInfo.establishment && !studentInfo.level && ( + + Student information could not be parsed from the response. + The data might be in a different format than expected. + + )} + + + )} + + ); +}; + +export default StudentInfoTab; \ No newline at end of file diff --git a/src/types/moutamadris.ts b/src/types/moutamadris.ts new file mode 100644 index 0000000..774a667 --- /dev/null +++ b/src/types/moutamadris.ts @@ -0,0 +1,84 @@ +// Student Information Types +export interface StudentInfo { + name?: string; + massarCode?: string; + establishment?: string; + level?: string; + class?: string; + academicYear?: string; +} + +// Attendance Types +export interface AttendanceRecord { + date?: string; + subject?: string; + type?: string; // Absence, Late, etc. + justified?: boolean; + reason?: string; +} + +export interface AttendanceData { + totalAbsences?: number; + justifiedAbsences?: number; + unjustifiedAbsences?: number; + records?: AttendanceRecord[]; +} + +// Schedule Types +export interface ScheduleItem { + time?: string; + subject?: string; + teacher?: string; + room?: string; + day?: string; +} + +export interface Schedule { + week?: ScheduleItem[][]; + items?: ScheduleItem[]; +} + +// Announcements Types +export interface Announcement { + date?: string; + title?: string; + content?: string; + author?: string; + priority?: string; +} + +// Homework Types +export interface HomeworkItem { + subject?: string; + title?: string; + description?: string; + dueDate?: string; + assignedDate?: string; + status?: string; +} + +// Communications Types +export interface Communication { + date?: string; + from?: string; + to?: string; + subject?: string; + content?: string; + type?: string; // Email, Message, etc. +} + +// API Response Types +export interface ApiResponse { + success: boolean; + data?: T; + rawHTML?: string; + error?: string; +} + +// Form Types +export interface MoutamadrisCredentials { + username: string; + password: string; + year?: string; + semester?: string; +} \ No newline at end of file diff --git a/src/utils/moutamadrisAuth.ts b/src/utils/moutamadrisAuth.ts new file mode 100644 index 0000000..3234bf4 --- /dev/null +++ b/src/utils/moutamadrisAuth.ts @@ -0,0 +1,75 @@ +import axios from 'axios'; +import { load } from 'cheerio'; +import { CookieJar } from 'tough-cookie'; +import { wrapper } from 'axios-cookiejar-support'; + +export interface AuthenticatedClient { + client: any; + authenticated: boolean; +} + +export async function createAuthenticatedClient(username: string, password: string): Promise { + const jar = new CookieJar(); + // Use proxy only on Vercel (production) + const isVercel = !!process.env.VERCEL; + const axiosConfig: any = { jar, timeout: 15000 }; + if (isVercel) { + axiosConfig.proxy = { + host: '196.115.252.173', + port: 3000, + }; + } + const client = wrapper(axios.create(axiosConfig)); + + try { + // Get CSRF token + const tokenPage = await client.get('https://massarservice.men.gov.ma/moutamadris/Account'); + const $ = load(tokenPage.data); + const token = $('input[name="__RequestVerificationToken"]').val(); + + if (!token) { + throw new Error('Failed to retrieve CSRF token'); + } + + // Login + const loginPayload = new URLSearchParams({ + UserName: username, + Password: password, + __RequestVerificationToken: token as string + }).toString(); + + const loginRes = await client.post( + 'https://massarservice.men.gov.ma/moutamadris/Account', + loginPayload, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Referer': 'https://massarservice.men.gov.ma/moutamadris/Account', + 'Origin': 'https://massarservice.men.gov.ma', + 'User-Agent': 'Mozilla/5.0' + } + } + ); + + if (!loginRes.data.includes('ChangePassword')) { + throw new Error('Login failed'); + } + + // Set culture to English + await client.post('https://massarservice.men.gov.ma/moutamadris/General/SetCulture?culture=en', null); + + return { client, authenticated: true }; + } catch (error) { + throw error; + } +} + +export function getStandardHeaders() { + return { + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Referer': 'https://massarservice.men.gov.ma/moutamadris/TuteurEleves', + 'Origin': 'https://massarservice.men.gov.ma', + 'User-Agent': 'Mozilla/5.0' + }; +} \ No newline at end of file diff --git a/src/utils/parseAdditionalData.ts b/src/utils/parseAdditionalData.ts new file mode 100644 index 0000000..a95cf1f --- /dev/null +++ b/src/utils/parseAdditionalData.ts @@ -0,0 +1,187 @@ +import { load } from 'cheerio'; +import { StudentInfo, AttendanceData, AttendanceRecord, Schedule, ScheduleItem, Announcement, HomeworkItem } from '../types/moutamadris'; + +export function parseStudentInfo(html: string): StudentInfo | null { + try { + const $ = load(html); + const studentInfo: StudentInfo = {}; + + // Extract student information from various selectors + $('dl dt, dl dd, .student-info, .info-student').each((_, el) => { + const text = $(el).text().trim(); + if (text.includes('Nom') || text.includes('Name')) { + studentInfo.name = $(el).next().text().trim() || $(el).siblings().first().text().trim(); + } + if (text.includes('Code Massar') || text.includes('Massar Code')) { + studentInfo.massarCode = $(el).next().text().trim() || $(el).siblings().first().text().trim(); + } + if (text.includes('Etablissement') || text.includes('Establishment')) { + studentInfo.establishment = $(el).next().text().trim() || $(el).siblings().first().text().trim(); + } + if (text.includes('Niveau') || text.includes('Level')) { + studentInfo.level = $(el).next().text().trim() || $(el).siblings().first().text().trim(); + } + if (text.includes('Classe') || text.includes('Class')) { + studentInfo.class = $(el).next().text().trim() || $(el).siblings().first().text().trim(); + } + }); + + return studentInfo; + } catch (error) { + console.error('Error parsing student info:', error); + return null; + } +} + +export function parseAttendance(html: string): AttendanceData | null { + try { + const $ = load(html); + const attendanceData: AttendanceData = { + records: [] + }; + + // Parse attendance table + $('table tbody tr').each((_, tr) => { + const tds = $(tr).find('td'); + if (tds.length >= 3) { + const record: AttendanceRecord = { + date: $(tds[0]).text().trim(), + subject: $(tds[1]).text().trim(), + type: $(tds[2]).text().trim(), + justified: $(tds[3]).text().trim().toLowerCase().includes('oui') || $(tds[3]).text().trim().toLowerCase().includes('yes'), + reason: tds.length > 4 ? $(tds[4]).text().trim() : undefined + }; + attendanceData.records?.push(record); + } + }); + + // Parse summary statistics + $('.absence-summary, .summary').each((_, el) => { + const text = $(el).text(); + const totalMatch = text.match(/Total[:\s]*(\d+)/i); + if (totalMatch) attendanceData.totalAbsences = parseInt(totalMatch[1]); + + const justifiedMatch = text.match(/Justifi[éê]es?[:\s]*(\d+)/i); + if (justifiedMatch) attendanceData.justifiedAbsences = parseInt(justifiedMatch[1]); + + const unjustifiedMatch = text.match(/Non[- ]justifi[éê]es?[:\s]*(\d+)/i); + if (unjustifiedMatch) attendanceData.unjustifiedAbsences = parseInt(unjustifiedMatch[1]); + }); + + return attendanceData; + } catch (error) { + console.error('Error parsing attendance:', error); + return null; + } +} + +export function parseSchedule(html: string): Schedule | null { + try { + const $ = load(html); + const schedule: Schedule = { + items: [] + }; + + // Parse schedule table + $('table tbody tr, .schedule-item, .emploi-item').each((_, tr) => { + const tds = $(tr).find('td'); + if (tds.length >= 4) { + const item: ScheduleItem = { + time: $(tds[0]).text().trim(), + subject: $(tds[1]).text().trim(), + teacher: $(tds[2]).text().trim(), + room: $(tds[3]).text().trim(), + day: tds.length > 4 ? $(tds[4]).text().trim() : undefined + }; + schedule.items?.push(item); + } + }); + + return schedule; + } catch (error) { + console.error('Error parsing schedule:', error); + return null; + } +} + +export function parseAnnouncements(html: string): Announcement[] { + try { + const $ = load(html); + const announcements: Announcement[] = []; + + $('.announcement, .annonce, .news-item').each((_, el) => { + const announcement: Announcement = { + title: $(el).find('.title, .titre, h3, h4').first().text().trim(), + content: $(el).find('.content, .contenu, .description, p').first().text().trim(), + date: $(el).find('.date, .date-creation').first().text().trim(), + author: $(el).find('.author, .auteur').first().text().trim() + }; + + if (announcement.title || announcement.content) { + announcements.push(announcement); + } + }); + + // Fallback: try to parse from table format + if (announcements.length === 0) { + $('table tbody tr').each((_, tr) => { + const tds = $(tr).find('td'); + if (tds.length >= 2) { + announcements.push({ + date: $(tds[0]).text().trim(), + title: $(tds[1]).text().trim(), + content: tds.length > 2 ? $(tds[2]).text().trim() : undefined, + author: tds.length > 3 ? $(tds[3]).text().trim() : undefined + }); + } + }); + } + + return announcements; + } catch (error) { + console.error('Error parsing announcements:', error); + return []; + } +} + +export function parseHomework(html: string): HomeworkItem[] { + try { + const $ = load(html); + const homework: HomeworkItem[] = []; + + $('.homework, .devoir, .assignment').each((_, el) => { + const item: HomeworkItem = { + subject: $(el).find('.subject, .matiere').first().text().trim(), + title: $(el).find('.title, .titre, h3, h4').first().text().trim(), + description: $(el).find('.description, .contenu, p').first().text().trim(), + dueDate: $(el).find('.due-date, .date-echeance').first().text().trim(), + assignedDate: $(el).find('.assigned-date, .date-creation').first().text().trim() + }; + + if (item.title || item.description) { + homework.push(item); + } + }); + + // Fallback: try to parse from table format + if (homework.length === 0) { + $('table tbody tr').each((_, tr) => { + const tds = $(tr).find('td'); + if (tds.length >= 3) { + homework.push({ + subject: $(tds[0]).text().trim(), + title: $(tds[1]).text().trim(), + description: $(tds[2]).text().trim(), + dueDate: tds.length > 3 ? $(tds[3]).text().trim() : undefined, + assignedDate: tds.length > 4 ? $(tds[4]).text().trim() : undefined + }); + } + }); + } + + return homework; + } catch (error) { + console.error('Error parsing homework:', error); + return []; + } +} \ No newline at end of file