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 (
+
+ {value === index && {children}}
+
+ );
+}
+
+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 (
+
+ {value === index && {children}}
+
+ );
+}
+
+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