From 3c269741e0b75d93da279c04ae6465d4edb6e6f3 Mon Sep 17 00:00:00 2001 From: Akhila sai Date: Fri, 31 Jul 2026 16:52:27 +0530 Subject: [PATCH 1/3] add printable weekly student certificates and top-five achievement certificates with student details --- frontend/src/components/PanelViews.tsx | 402 ++++++++++++++++++++++++- frontend/src/index.css | 109 ++++++- 2 files changed, 497 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/PanelViews.tsx b/frontend/src/components/PanelViews.tsx index 3c271f5e5..686414077 100644 --- a/frontend/src/components/PanelViews.tsx +++ b/frontend/src/components/PanelViews.tsx @@ -1,7 +1,35 @@ -import { apiFetch } from '../services/apiClient'; -import React, { useState, useEffect } from 'react'; +import { apiFetch, withBase } from '../services/apiClient'; +import React, { useState, useEffect, useRef } from 'react'; import { User, UserRole, Student, ClassGroup, School, EvaluationReport, LogEntry, Ticket } from '../types'; -import { Users, ShieldAlert, BookOpen, Calendar, ArrowRight, CheckCircle2, XCircle, SlidersHorizontal, Layers, Award, MapPin, School as SchoolIcon, BarChart3, FileText, ClipboardList, Building2, GraduationCap, BookMarked, Globe, Settings, Database, RefreshCw, Search, ChevronDown } from 'lucide-react'; +import { + Users, + ShieldAlert, + BookOpen, + UserCheck, + Calendar, + ArrowRight, + CheckCircle2, + XCircle, + SlidersHorizontal, + Layers, + Award, + MapPin, + School as SchoolIcon, + BarChart3, + FileText, + ClipboardList, + Building2, + GraduationCap, + BookMarked, + Globe, + Settings, + Database, + RefreshCw, + Search, + ChevronDown, + Download, + Printer, +} from 'lucide-react'; import { Table, Column } from './Table'; import { MetricCard } from './Card'; import { STATE_NAMES, DISTRICT_NAMES, BLOCK_NAMES } from '../constants'; @@ -22,6 +50,96 @@ const STUDENTS_FALLBACK: Student[] = [ { id: 's7', name: 'Simran Kaur', age: 6, classGroup: 'Class 1', section: 'A', schoolId: 'gps-mt-001', currentLevel: 4, currentSubLevel: 0, targetLevel: 8, aadharMasked: 'XXXX-XXXX-6789', levelHistory: [], streak: 0 }, ]; +type RankedStudent = { + student: Student; + rank: number; + issuedAt?: string; +}; + +const ordinal = (rank: number) => { + const endings = ['th', 'st', 'nd', 'rd']; + const remainder = rank % 100; + return `${rank}${endings[(remainder - 20) % 10] || endings[remainder] || endings[0]}`; +}; + +const printCertificate = async (certificate?: HTMLDivElement) => { + await document.fonts?.ready; + const originalParent = certificate?.parentNode; + const originalNextSibling = certificate?.nextSibling; + + // Normal certificates live below the student-profile header and tabs. Move + // only that print target to the body while the dialog is open so none of its + // screen-layout ancestors can contribute a top offset to the printed page. + if (certificate) document.body.appendChild(certificate); + + const cleanup = () => { + document.body.classList.remove('printing-certificate'); + document.body.classList.remove('printing-normal-certificate'); + if (certificate && originalParent) { + originalParent.insertBefore(certificate, originalNextSibling ?? null); + } + }; + document.body.classList.add('printing-certificate'); + if (certificate) document.body.classList.add('printing-normal-certificate'); + window.addEventListener('afterprint', cleanup, { once: true }); + window.print(); +}; + +function RankCertificate({ ranked, teacherName, allStudents, certificateRef }: { ranked: RankedStudent; teacherName: string; allStudents: Student[]; certificateRef?: React.RefObject }) { + const issuedAt = ranked.issuedAt ? new Date(ranked.issuedAt) : new Date(); + const issuedDate = issuedAt.toLocaleDateString('en-IN', { + day: 'numeric', month: 'long', year: 'numeric', + }); + const weekOfMonth = Math.ceil(issuedAt.getDate() / 7); + const month = issuedAt.toLocaleDateString('en-IN', { month: 'long' }); + const classmates = allStudents + .filter(student => student.classGroup === ranked.student.classGroup && student.section === ranked.student.section) + .sort((a, b) => b.currentLevel - a.currentLevel || b.streak - a.streak || a.name.localeCompare(b.name)); + const classRank = classmates.findIndex(student => student.id === ranked.student.id) + 1; + + return ( +
+
+
+ ); +} + const REPORTS_MOCK: EvaluationReport[] = [ { id: 'r1', studentId: 's1', worksheetId: 'ws1', score: 8, totalQuestions: 10, conceptMastery: { 'Number Sense': 'Strong', 'Addition': 'Satisfactory', 'Subtraction': 'Needs Practice' }, narrative: 'Shows good number sense but needs practice with borrowing in subtraction.', recommendedLevel: 12, timestamp: '2026-03-15T10:00:00Z' }, { id: 'r2', studentId: 's2', worksheetId: 'ws2', score: 5, totalQuestions: 10, conceptMastery: { 'Number Sense': 'Satisfactory', 'Shapes': 'Needs Practice', 'Patterns': 'Needs Practice' }, narrative: 'Struggling with pattern recognition. Recommend additional tracing and matching exercises.', recommendedLevel: 8, recommendedSubLevel: 1, timestamp: '2026-02-20T11:30:00Z' }, @@ -192,7 +310,10 @@ export const PanelViews: React.FC = ({ activePanel, currentUser const [blockFilter, setBlockFilter] = useState('all'); const [expandedReportId, setExpandedReportId] = useState(null); const [sel, setSel] = useState(''); - const [profileTab, setProfileTab] = useState<'overview' | 'academic' | 'personal' | 'activity'>('overview'); + const [profileTab, setProfileTab] = useState< + 'overview' | 'academic' | 'personal' | 'activity' | 'certificate' + >('overview'); + const [editingProfile, setEditingProfile] = useState(false); const [profileDraft, setProfileDraft] = useState>({}); const [savingProfile, setSavingProfile] = useState(false); @@ -203,6 +324,9 @@ export const PanelViews: React.FC = ({ activePanel, currentUser const [expandedDist, setExpandedDist] = useState(null); const [userRoleFilter, setUserRoleFilter] = useState('superadmin'); const [userSearch, setUserSearch] = useState(''); + const [rankCertificate, setRankCertificate] = useState(null); + const certificatePreviewRef = useRef(null); + const rankCertificatePreviewRef = useRef(null); const [apiStudents, setApiStudents] = useState([]); const [apiSchools, setApiSchools] = useState([]); @@ -234,6 +358,10 @@ export const PanelViews: React.FC = ({ activePanel, currentUser const panel = activePanel; + useEffect(() => { + if (panel === 'certificates') setProfileTab('certificate'); + }, [panel]); + const handleDownloadPDF = (student: Student, r: EvaluationReport, examResponses: any[]) => { const printWindow = window.open('', '_blank'); if (!printWindow) { @@ -364,6 +492,185 @@ export const PanelViews: React.FC = ({ activePanel, currentUser printWindow.document.close(); }; + const legacyDownloadCertificate = async (student: Student) => { + // Certificates are deliberately available only in the teacher workspace. The + // values below are the selected student returned by the authenticated roster + // API and the authenticated teacher's name, rather than form input. + if (currentUser.role !== UserRole.TEACHER) return; + + // This must happen synchronously within the click event. Opening it after + // awaiting the API request makes modern browsers block it as a popup. + const printWindow = window.open('', '_blank'); + if (!printWindow) { + alert('Please allow popups to download the certificate as a PDF.'); + return; + } + printWindow.document.write('Preparing certificate…

Preparing your PDF certificate…

'); + + const certificate: { + student: Pick; + teacherName: string; + issuedOn: string; + } = { + // `student` is the selected record from the authenticated /api/students + // roster; `currentUser` is the authenticated teacher session. + student: { + name: student.name, + classGroup: student.classGroup, + section: student.section, + currentLevel: student.currentLevel, + currentSubLevel: student.currentSubLevel, + streak: student.streak, + }, + teacherName: currentUser.name, + issuedOn: new Date().toISOString(), + }; + + const escapeHtml = (value: string) => value.replace(/[&<>'"]/g, char => ({ + '&': '&', '<': '<', '>': '>', "'": ''', '"': '"', + }[char] || char)); + const issueDate = new Date(certificate.issuedOn).toLocaleDateString('en-IN', { + day: 'numeric', month: 'long', year: 'numeric', + }); + const isClassOne = /^class\s*1(?:\D|$)/i.test(certificate.student.classGroup.trim()); + const isClassTwo = /^class\s*2(?:\D|$)/i.test(certificate.student.classGroup.trim()); + + printWindow.document.open(); + printWindow.document.write(` + + + + Certificate - ${escapeHtml(certificate.student.name)} + + + + + +
+
${isClassOne + ? '
β˜€FANTASTIC!
Brilliant!
πŸ†
β˜… ✦ β˜…
' + : isClassTwo + ? '
β˜…
β˜…
β˜…
β˜…
β˜…
' + : '
β˜…
✦
✢
✹
'} +
+
β˜…
+
FLN Portal Β· Foundational Literacy & Numeracy
+

Certificate of

+

Excellence

+

This certificate is proudly presented to

+
${escapeHtml(certificate.student.name)}
+

for their dedicated learning journey and progress in foundational literacy and numeracy.

+
+
Class${escapeHtml(`${certificate.student.classGroup} - ${certificate.student.section}`)}
+
Current LevelL${certificate.student.currentLevel}.${certificate.student.currentSubLevel ?? 0}
+
Learning Streak${certificate.student.streak} days
+
+
+
${issueDate}Date issued
+
${escapeHtml(certificate.teacherName)}Teacher signature
+
+
+
Issued digitally by FLN Portal
+
+ - - - `; - - printWindow.document.open(); - printWindow.document.write(htmlContent); - printWindow.document.close(); - }; - - const legacyDownloadCertificate = async (student: Student) => { - // Certificates are deliberately available only in the teacher workspace. The - // values below are the selected student returned by the authenticated roster - // API and the authenticated teacher's name, rather than form input. - if (currentUser.role !== UserRole.TEACHER) return; - - // This must happen synchronously within the click event. Opening it after - // awaiting the API request makes modern browsers block it as a popup. - const printWindow = window.open('', '_blank'); - if (!printWindow) { - alert('Please allow popups to download the certificate as a PDF.'); - return; - } - printWindow.document.write('Preparing certificate…

Preparing your PDF certificate…

'); - - const certificate: { - student: Pick; - teacherName: string; - issuedOn: string; - } = { - // `student` is the selected record from the authenticated /api/students - // roster; `currentUser` is the authenticated teacher session. - student: { - name: student.name, - classGroup: student.classGroup, - section: student.section, - currentLevel: student.currentLevel, - currentSubLevel: student.currentSubLevel, - streak: student.streak, - }, - teacherName: currentUser.name, - issuedOn: new Date().toISOString(), - }; - - const escapeHtml = (value: string) => value.replace(/[&<>'"]/g, char => ({ - '&': '&', '<': '<', '>': '>', "'": ''', '"': '"', - }[char] || char)); - const issueDate = new Date(certificate.issuedOn).toLocaleDateString('en-IN', { - day: 'numeric', month: 'long', year: 'numeric', - }); - const isClassOne = /^class\s*1(?:\D|$)/i.test(certificate.student.classGroup.trim()); - const isClassTwo = /^class\s*2(?:\D|$)/i.test(certificate.student.classGroup.trim()); - - printWindow.document.open(); - printWindow.document.write(` - - - - Certificate - ${escapeHtml(certificate.student.name)} - - - - - -
-
${isClassOne - ? '
β˜€FANTASTIC!
Brilliant!
πŸ†
β˜… ✦ β˜…
' - : isClassTwo - ? '
β˜…
β˜…
β˜…
β˜…
β˜…
' - : '
β˜…
✦
✢
✹
'} -
-
β˜…
-
FLN Portal Β· Foundational Literacy & Numeracy
-

Certificate of

-

Excellence

-

This certificate is proudly presented to

-
${escapeHtml(certificate.student.name)}
-

for their dedicated learning journey and progress in foundational literacy and numeracy.

-
-
Class${escapeHtml(`${certificate.student.classGroup} - ${certificate.student.section}`)}
-
Current LevelL${certificate.student.currentLevel}.${certificate.student.currentSubLevel ?? 0}
-
Learning Streak${certificate.student.streak} days
-
-
-
${issueDate}Date issued
-
${escapeHtml(certificate.teacherName)}Teacher signature
-
-
-
Issued digitally by FLN Portal
-
-