From df59aa36db2af7dbd63a3b7ecb18163a5ad73ff7 Mon Sep 17 00:00:00 2001 From: aishwarya117-code Date: Fri, 17 Jul 2026 21:20:47 +0530 Subject: [PATCH 1/8] feat: add student report card generator, 404 page, and forgot password - Add ReportCardView: printable student progress report card with FLN levels, assessment history, skill proficiency, certification status, and teacher signatures. Accessible from student profile and sidebar navigation. - Add NotFoundView: 404 page with navigation back to home - Add Forgot Password modal on login page with email reset flow - Wire ReportCardView into PanelViews with report_card panel routing - Add Report Card nav item for Teacher and Volunteer roles in Layout --- frontend/src/App.tsx | 2 +- frontend/src/components/Layout.tsx | 6 +- frontend/src/components/LoginView.tsx | 85 +++- frontend/src/components/NotFoundView.tsx | 49 +++ frontend/src/components/PanelViews.tsx | 22 +- frontend/src/components/ReportCardView.tsx | 438 +++++++++++++++++++++ 6 files changed, 597 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/NotFoundView.tsx create mode 100644 frontend/src/components/ReportCardView.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b2bc56d7..ee2e76ea 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -224,7 +224,7 @@ export default function App() { )} {!['workspace', 'logbook', 'tickets', 'calendar', 'settings', 'notifications'].includes(activePanel) && ( - + )} {toast && ( diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index bccc9509..186e042f 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -139,7 +139,8 @@ export const Layout: React.FC = ({ subItems: [ { name: 'Student List', view: 'student_list' }, { name: 'Student Profile', view: 'student_profile' }, - { name: 'Performance', view: 'performance' } + { name: 'Performance', view: 'performance' }, + { name: 'Report Card', view: 'report_card' } ] }); list.push({ name: 'Worksheets', view: 'worksheets', icon: ClipboardList }); @@ -164,7 +165,8 @@ export const Layout: React.FC = ({ subItems: [ { name: 'Student List', view: 'student_list' }, { name: 'Student Profile', view: 'student_profile' }, - { name: 'Performance', view: 'performance' } + { name: 'Performance', view: 'performance' }, + { name: 'Report Card', view: 'report_card' } ] }); list.push({ name: 'Worksheets', view: 'worksheets', icon: ClipboardList }); diff --git a/frontend/src/components/LoginView.tsx b/frontend/src/components/LoginView.tsx index b17401da..bd326afa 100644 --- a/frontend/src/components/LoginView.tsx +++ b/frontend/src/components/LoginView.tsx @@ -4,7 +4,7 @@ */ import React, { useState } from 'react'; -import { Eye, EyeOff, AlertCircle, ArrowLeft } from 'lucide-react'; +import { Eye, EyeOff, AlertCircle, ArrowLeft, KeyRound, CheckCircle2, X } from 'lucide-react'; import { User, UserRole } from '../types'; interface LoginViewProps { @@ -18,6 +18,10 @@ export const LoginView: React.FC = ({ onLoginSuccess, onBackToHo const [showPassword, setShowPassword] = useState(false); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); + const [showForgotPassword, setShowForgotPassword] = useState(false); + const [forgotEmail, setForgotEmail] = useState(''); + const [forgotSent, setForgotSent] = useState(false); + const [forgotLoading, setForgotLoading] = useState(false); const mockUsersList = [ { label: 'Superadmin 🌐', email: 'superadmin@fln.org', pass: 'Fln@2026' }, @@ -61,6 +65,24 @@ export const LoginView: React.FC = ({ onLoginSuccess, onBackToHo } }; + const handleForgotPassword = async (e: React.FormEvent) => { + e.preventDefault(); + if (!forgotEmail) return; + setForgotLoading(true); + try { + await fetch('/api/reset', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: forgotEmail }) + }); + setForgotSent(true); + } catch { + setForgotSent(true); + } finally { + setForgotLoading(false); + } + }; + return (
@@ -126,6 +148,11 @@ export const LoginView: React.FC = ({ onLoginSuccess, onBackToHo {showPassword ? : }
+
+ +
{/* Validation Alerts */} @@ -181,6 +208,62 @@ export const LoginView: React.FC = ({ onLoginSuccess, onBackToHo Warning: Unauthorized access to this system is strictly prohibited under the IT Act, 2000. All activities are monitored. + + {/* Forgot Password Modal */} + {showForgotPassword && ( +
+
+
+
+ +

Reset Password

+
+ +
+ + {!forgotSent ? ( +
+

+ Enter your registered email address and we will send you a password reset link. +

+ setForgotEmail(e.target.value)} + className="w-full rounded-lg border-2 border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-950 px-3.5 py-2.5 text-sm text-slate-950 dark:text-white placeholder-slate-400 focus:border-indigo-700 focus:outline-none focus:ring-1 focus:ring-indigo-700 font-medium" + placeholder="Enter your email address" + /> + +
+ ) : ( +
+
+ +
+

Reset link sent!

+

+ If an account exists with {forgotEmail}, you will receive a password reset link shortly. Check your inbox and spam folder. +

+ +
+ )} +
+
+ )} ); }; \ No newline at end of file diff --git a/frontend/src/components/NotFoundView.tsx b/frontend/src/components/NotFoundView.tsx new file mode 100644 index 00000000..192c32d7 --- /dev/null +++ b/frontend/src/components/NotFoundView.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Home, ArrowLeft } from 'lucide-react'; + +interface NotFoundViewProps { + onNavigateHome: () => void; +} + +export const NotFoundView: React.FC = ({ onNavigateHome }) => { + return ( +
+
+
+
+ 404 +
+
+ +

+ Page Not Found +

+

+ The page you are looking for does not exist or has been moved. + Please check the URL or return to the portal. +

+ +
+ + +
+ +
+ FLN Assessment Portal | NIPUN Bharat +
+
+
+ ); +}; diff --git a/frontend/src/components/PanelViews.tsx b/frontend/src/components/PanelViews.tsx index c85d316f..aad41697 100644 --- a/frontend/src/components/PanelViews.tsx +++ b/frontend/src/components/PanelViews.tsx @@ -4,11 +4,13 @@ import { Users, ShieldAlert, BookOpen, UserCheck, Calendar, ArrowRight, CheckCir import { Table, Column } from './Table'; import { MetricCard } from './Card'; import { STATE_NAMES, DISTRICT_NAMES, BLOCK_NAMES } from '../constants'; +import { ReportCardView } from './ReportCardView'; interface PanelViewsProps { activePanel: string; currentUser: User; token: string; + onSelectPanel?: (panel: string) => void; } const STUDENTS_FALLBACK: Student[] = [ @@ -184,7 +186,7 @@ function EmptyStudents({ students }: { students: Student[] }) { return ; } -export const PanelViews: React.FC = ({ activePanel, currentUser, token }) => { +export const PanelViews: React.FC = ({ activePanel, currentUser, token, onSelectPanel }) => { const [search, setSearch] = useState(''); const [stateFilter, setStateFilter] = useState('all'); const [distFilter, setDistFilter] = useState('all'); @@ -437,6 +439,10 @@ export const PanelViews: React.FC = ({ activePanel, currentUser {s.name} + {showDropdown && ( <>
setShowDropdown(false)} /> @@ -874,6 +880,20 @@ export const PanelViews: React.FC = ({ activePanel, currentUser ); } + if (panel === 'report_card') { + const s = students.find(x => x.id === sel) || students[0]; + const reports = REPORTS_MOCK.filter(r => r.studentId === s.id); + const studentSchool = schools.find(sch => sch.id === s.schoolId); + return ( + {}} + /> + ); + } + if (panel === 'diagnostic_test') { const pending = students.filter(s => s.levelHistory.length === 0); const completed = students.filter(s => s.levelHistory.length > 0); diff --git a/frontend/src/components/ReportCardView.tsx b/frontend/src/components/ReportCardView.tsx new file mode 100644 index 00000000..e457574f --- /dev/null +++ b/frontend/src/components/ReportCardView.tsx @@ -0,0 +1,438 @@ +import React from 'react'; +import { Student, EvaluationReport } from '../types'; +import { ArrowLeft, Printer, Download } from 'lucide-react'; + +interface ReportCardViewProps { + student: Student; + reports: EvaluationReport[]; + schoolName: string; + onBack: () => void; +} + +export const ReportCardView: React.FC = ({ student, reports, schoolName, onBack }) => { + const latestReport = reports.length > 0 ? reports[0] : null; + const avgScore = reports.length > 0 + ? Math.round(reports.reduce((a, r) => a + (r.score / r.totalQuestions) * 100, 0) / reports.length) + : 0; + const allConceptMastery: Record = {}; + reports.forEach(r => { + Object.entries(r.conceptMastery).forEach(([topic, mastery]) => { + if (!allConceptMastery[topic] || mastery === 'Strong') { + allConceptMastery[topic] = mastery as 'Strong' | 'Needs Practice' | 'Satisfactory'; + } + }); + }); + const certified = student.currentLevel >= 5; + const progressPct = Math.round((student.currentLevel / 59) * 100); + const scoreBand = avgScore >= 80 ? 'Strong' : avgScore >= 60 ? 'Satisfactory' : 'Needs Practice'; + + const handlePrint = () => { + const printWindow = window.open('', '_blank'); + if (!printWindow) { + alert('Please allow popups to print the report card.'); + return; + } + + const conceptRows = Object.entries(allConceptMastery).map(([topic, mastery]) => ` +
+ + + + `).join(''); + + const assessmentRows = reports.map(r => { + const pct = Math.round((r.score / r.totalQuestions) * 100); + return ` + + + + + + + `; + }).join(''); + + const levelHistoryRows = student.levelHistory.map(lh => ` + + + + + + `).join(''); + + const html = ` + + + + FLN Report Card - ${student.name} + + + + +
+
+
National Education Policy 2020
+
${schoolName || 'Government Primary School'}
+
Foundational Literacy and Numeracy Assessment
+
Student Progress Report Card
+
+ +
+
+
${student.name}
+
${student.classGroup} - ${student.section} | ID: ${student.id} | Age: ${student.age} years
+
School ID: ${student.schoolId} | Aadhaar: ${student.aadharMasked}
+
+
+ ${certified ? 'FLN Certified' : 'In Progress'} +
+
+ +
+
+
L${student.currentLevel}.${student.currentSubLevel ?? 0}
+
Current FLN Level
+
+
+
${reports.length}
+
Assessments Taken
+
+
+
${avgScore}%
+
Average Score
+
+
+
${student.streak}
+
Day Streak
+
+
+ +
+
FLN Level Progress (Max: L59)
+
+ Level ${student.currentLevel} + Target: Level ${student.targetLevel} +
+
+
+
+
+ + ${reports.length > 0 ? ` +
Assessment History
+
${topic} + + ${mastery} + +
${r.worksheetId}${r.score}/${r.totalQuestions}${pct}%${new Date(r.timestamp).toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })}
L${lh.level}.${lh.subLevel ?? 0}${lh.reason}${new Date(lh.date).toLocaleDateString('en-IN', { day: 'numeric', month: 'long', year: 'numeric' })}
+ + + + + + + + + + ${assessmentRows} + +
WorksheetScorePercentageDate
+ ` : ''} + + ${Object.keys(allConceptMastery).length > 0 ? ` +
Skill Proficiency Breakdown
+ + + + + + + + + ${conceptRows} + +
Topic / SkillMastery Level
+ ` : ''} + + ${latestReport ? ` +
Teacher Evaluation Summary
+
${latestReport.narrative}
+ ` : ''} + + ${student.levelHistory.length > 0 ? ` +
Level Progression History
+ + + + + + + + + + ${levelHistoryRows} + +
Level AchievedAssessment TypeDate
+ ` : ''} + +
Overall Performance Band
+
+
+
${scoreBand}
+
Performance Band
+
+
+
L${student.currentLevel}
+
Current Level
+
+
+
L${student.targetLevel}
+
Target Level
+
+
+ +
+
+
Class Teacher Signature
+
+
+
Principal / Headmaster
+
+
+
Parent / Guardian
+
+
+ + + + + + + + `; + + printWindow.document.open(); + printWindow.document.write(html); + printWindow.document.close(); + }; + + return ( +
+
+ +
+ + +
+
+ +
+
+
+ National Education Policy 2020 +
+

Student Progress Report Card

+

Foundational Literacy and Numeracy Assessment

+
+ +
+
+
+

{student.name}

+

+ {student.classGroup} - {student.section} | ID: {student.id} | Age: {student.age} +

+

+ School: {schoolName} ({student.schoolId}) +

+
+
+ {certified ? 'FLN Certified' : 'In Progress'} +
+
+ +
+ {[ + { value: `L${student.currentLevel}.${student.currentSubLevel ?? 0}`, label: 'Current Level', color: 'text-indigo-600 dark:text-indigo-400' }, + { value: String(reports.length), label: 'Assessments', color: 'text-slate-900 dark:text-white' }, + { value: `${avgScore}%`, label: 'Average Score', color: avgScore >= 80 ? 'text-emerald-600' : avgScore >= 60 ? 'text-amber-600' : 'text-red-600' }, + { value: `${student.streak}`, label: 'Day Streak', color: student.streak >= 3 ? 'text-emerald-600' : 'text-amber-600' }, + ].map(m => ( +
+
{m.value}
+
{m.label}
+
+ ))} +
+ +
+

FLN Level Progress (Max: L59)

+
+ Level {student.currentLevel} + Target: Level {student.targetLevel} +
+
+
+
+
{progressPct}%
+
+ + {reports.length > 0 && ( +
+

Assessment History

+
+ + + + + + + + + + + {reports.map(r => { + const pct = Math.round((r.score / r.totalQuestions) * 100); + return ( + + + + + + + ); + })} + +
WorksheetScorePercentageDate
{r.worksheetId}{r.score}/{r.totalQuestions} + = 80 ? 'bg-emerald-100 dark:bg-emerald-950 text-emerald-700 dark:text-emerald-300' : pct >= 60 ? 'bg-amber-100 dark:bg-amber-950 text-amber-700 dark:text-amber-300' : 'bg-red-100 dark:bg-red-950 text-red-700 dark:text-red-300'}`}>{pct}% + {new Date(r.timestamp).toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })}
+
+
+ )} + + {Object.keys(allConceptMastery).length > 0 && ( +
+

Skill Proficiency Breakdown

+
+ {Object.entries(allConceptMastery).map(([topic, mastery]) => ( +
+ {topic} + {mastery} +
+ ))} +
+
+ )} + + {latestReport && ( +
+

Teacher Evaluation Summary

+
+ {latestReport.narrative} +
+
+ )} + + {student.levelHistory.length > 0 && ( +
+

Level Progression History

+
+ {student.levelHistory.map((lh, i) => ( +
+
+
+ L{lh.level} +
+
+
{lh.reason}
+
Level {lh.level}.{lh.subLevel ?? 0}
+
+
+ {new Date(lh.date).toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })} +
+ ))} +
+
+ )} + +
+ {['Class Teacher', 'Principal / Headmaster', 'Parent / Guardian'].map(role => ( +
+
+ {role} +
+ ))} +
+ +
+

Confidential Student Academic Record

+

Generated by FLN Portal | NIPUN Bharat | {new Date().toLocaleDateString('en-IN', { day: 'numeric', month: 'long', year: 'numeric' })}

+
+
+
+
+ ); +}; From a83fd3dc3967dc7716beab2e47536a33fde4dc76 Mon Sep 17 00:00:00 2001 From: aishwarya117-code Date: Fri, 17 Jul 2026 22:07:13 +0530 Subject: [PATCH 2/8] feat: fix critical bugs in report card, login, and 404 features - CRITICAL: Fix forgot password calling /api/reset which wiped entire database - Fix login demo accounts to use real seeded DB emails - Fix XSS in ReportCardView print HTML via escapeHtml utility - Fix conceptMastery merge with priority-based selection - Clamp progressPct to max 100%, sort reports by timestamp - Fix ReportCardView back button to navigate to student_profile - Fix NotFoundView to use onNavigateHome instead of window.history.back - Wire NotFoundView as fallback for unknown panels in PanelViews - Guard against crash when students array is empty - Add accessibility attributes to forgot password modal - Disable demo login buttons during loading --- frontend/src/components/LoginView.tsx | 44 ++++++++++++---------- frontend/src/components/NotFoundView.tsx | 2 +- frontend/src/components/PanelViews.tsx | 10 +++-- frontend/src/components/ReportCardView.tsx | 40 ++++++++++++-------- 4 files changed, 56 insertions(+), 40 deletions(-) diff --git a/frontend/src/components/LoginView.tsx b/frontend/src/components/LoginView.tsx index bd326afa..faafb4a3 100644 --- a/frontend/src/components/LoginView.tsx +++ b/frontend/src/components/LoginView.tsx @@ -24,18 +24,14 @@ export const LoginView: React.FC = ({ onLoginSuccess, onBackToHo const [forgotLoading, setForgotLoading] = useState(false); const mockUsersList = [ - { label: 'Superadmin 🌐', email: 'superadmin@fln.org', pass: 'Fln@2026' }, - { label: 'Punjab Admin 🌾', email: 'admin.pb@fln.org', pass: 'Fln@2026' }, - { label: 'Haryana Admin 🌾', email: 'admin.hr@fln.org', pass: 'Fln@2026' }, - { label: 'UP Admin 🏛️', email: 'admin.up@fln.org', pass: 'Fln@2026' }, - { label: 'Rajasthan Admin 🏰', email: 'admin.rj@fln.org', pass: 'Fln@2026' }, - { label: 'Ludhiana Dist 🏢', email: 'district.ldh@fln.org', pass: 'Fln@2026' }, - { label: 'Ambala Dist 🏢', email: 'district.amb@fln.org', pass: 'Fln@2026' }, - { label: 'Ludhiana Block 🏫', email: 'block.ldh-01@fln.org', pass: 'Fln@2026' }, - { label: 'Punjab Principal 🎓', email: 'gps-mt-001@fln.org', pass: 'Fln@2026' }, - { label: 'Haryana Teacher 👩‍🏫', email: 'gps-amb-003.t01@fln.org', pass: 'Fln@2026' }, - { label: 'Punjab Volunteer 🤝', email: 'vol.rahul@fln.org', pass: 'Fln@2026' }, - { label: 'Haryana Volunteer 🤝', email: 'vol.hr_vipin@fln.org', pass: 'Fln@2026' } + { label: 'Superadmin', email: 'superadmin@fln.org', pass: 'Fln@2026' }, + { label: 'AP State Admin', email: 'admin.ap@fln.org', pass: 'Fln@2026' }, + { label: 'Guntur District', email: 'district.gnt@fln.org', pass: 'Fln@2026' }, + { label: 'Guntur Block', email: 'block.gnt_01@fln.org', pass: 'Fln@2026' }, + { label: 'School Principal', email: 'school.ap_gnt_gnt_01_01@fln.org', pass: 'Fln@2026' }, + { label: 'Class 2 Teacher', email: 'teacher.ap_gnt_gnt_01_01.c2@fln.org', pass: 'Fln@2026' }, + { label: 'Class 3 Teacher', email: 'teacher.ap_gnt_gnt_01_01.c3@fln.org', pass: 'Fln@2026' }, + { label: 'Volunteer', email: 'vol.ap_gnt_gnt_01_03@fln.org', pass: 'Fln@2026' }, ]; const handleLogin = async (e?: React.FormEvent, customEmail?: string, customPass?: string) => { @@ -70,16 +66,19 @@ export const LoginView: React.FC = ({ onLoginSuccess, onBackToHo if (!forgotEmail) return; setForgotLoading(true); try { - await fetch('/api/reset', { + const res = await fetch('/api/auth/forgot-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: forgotEmail }) }); - setForgotSent(true); + if (!res.ok && res.status !== 404) { + throw new Error('Request failed'); + } } catch { - setForgotSent(true); + // Silently succeed — we don't reveal whether the email exists } finally { setForgotLoading(false); + setForgotSent(true); } }; @@ -182,8 +181,9 @@ export const LoginView: React.FC = ({ onLoginSuccess, onBackToHo {mockUsersList.map(u => (
diff --git a/frontend/src/components/NotFoundView.tsx b/frontend/src/components/NotFoundView.tsx index 192c32d7..7fa57c3b 100644 --- a/frontend/src/components/NotFoundView.tsx +++ b/frontend/src/components/NotFoundView.tsx @@ -32,7 +32,7 @@ export const NotFoundView: React.FC = ({ onNavigateHome }) => Back to Home