diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx index b2615f2..dd5c272 100644 --- a/frontend/src/components/AsideMenuLayer.tsx +++ b/frontend/src/components/AsideMenuLayer.tsx @@ -3,10 +3,9 @@ import { mdiLogout, mdiClose } from '@mdi/js' import BaseIcon from './BaseIcon' import AsideMenuList from './AsideMenuList' import { MenuAsideItem } from '../interfaces' -import { useAppSelector } from '../stores/hooks' +import { useAppDispatch, useAppSelector } from '../stores/hooks' import Link from 'next/link'; -import { useAppDispatch } from '../stores/hooks'; import { createAsyncThunk } from '@reduxjs/toolkit'; import axios from 'axios'; diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 72935e6..fcbd9b9 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, { useEffect, useRef, useState } from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' diff --git a/frontend/src/components/PortalActionCard.tsx b/frontend/src/components/PortalActionCard.tsx new file mode 100644 index 0000000..fdd57d6 --- /dev/null +++ b/frontend/src/components/PortalActionCard.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import type { ColorButtonKey } from '../interfaces' +import BaseButton from './BaseButton' +import BaseIcon from './BaseIcon' +import CardBox from './CardBox' + +type Props = { + icon: string + title: string + description: string + href: string + cta: string + color?: ColorButtonKey + note?: string +} + +export default function PortalActionCard({ + icon, + title, + description, + href, + cta, + color = 'info', + note, +}: Props) { + return ( + +
+
+
+

{title}

+

+ {description} +

+
+ +
+ + {note &&

{note}

} + +
+ +
+
+
+ ) +} diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..73d8391 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' +import React, { ReactNode, useEffect, useState } from 'react' import jwt from 'jsonwebtoken'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 5380949..d0388a3 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -8,6 +8,27 @@ const menuAside: MenuAsideItem[] = [ label: 'Dashboard', }, + { + href: '/admin-portal', + icon: icon.mdiShieldAccount, + label: 'Portal Admin', + }, + { + href: '/teacher-portal', + icon: icon.mdiHumanMaleBoard, + label: 'Portal Maestros', + }, + { + href: '/student-portal', + icon: icon.mdiAccountSchool, + label: 'Portal Estudiantes', + }, + { + href: '/parent-portal', + icon: icon.mdiAccountGroup, + label: 'Portal Padres', + }, + { href: '/users/users-list', label: 'Users', diff --git a/frontend/src/pages/admin-portal.tsx b/frontend/src/pages/admin-portal.tsx new file mode 100644 index 0000000..ee93592 --- /dev/null +++ b/frontend/src/pages/admin-portal.tsx @@ -0,0 +1,101 @@ +import { + mdiCalendarClock, + mdiChartBox, + mdiClipboardText, + mdiShieldAccount, +} from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import BaseButton from '../components/BaseButton' +import CardBox from '../components/CardBox' +import PortalActionCard from '../components/PortalActionCard' +import SectionMain from '../components/SectionMain' +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton' +import { getPageTitle } from '../config' +import LayoutAuthenticated from '../layouts/Authenticated' + +const adminActions = [ + { + title: 'Crear examen', + description: 'Empieza un examen nuevo con título, descripción, tiempo límite, puntaje máximo y estado.', + href: '/exams/exams-new', + cta: 'Crear examen', + icon: mdiClipboardText, + color: 'info' as const, + }, + { + title: 'Crear sesión', + description: 'Abre una sesión para que estudiantes entren con el código indicado por el maestro o proctor.', + href: '/exam_sessions/exam_sessions-new', + cta: 'Crear sesión', + icon: mdiCalendarClock, + color: 'success' as const, + }, + { + title: 'Resultados', + description: 'Revisa reportes, calificaciones y resultados generados para estudiantes y familias.', + href: '/report_cards/report_cards-list', + cta: 'Ver resultados', + icon: mdiChartBox, + color: 'warning' as const, + }, +] + +const AdminPortal = () => { + return ( + <> + + {getPageTitle('Portal Admin')} + + + + {''} + + + +
+
+

+ Administración rápida +

+

+ Crear examen, crear sesión y revisar resultados. +

+

+ Este portal deja lo esencial en un solo lugar para que el Admin no tenga que buscar en + todas las tablas del sistema. +

+
+
+ + + +
+
+
+ +
+ {adminActions.map((action) => ( + + ))} +
+
+ + ) +} + +AdminPortal.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default AdminPortal diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 06cfc9d..7984195 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -7,7 +7,6 @@ import BaseButton from '../components/BaseButton'; import CardBox from '../components/CardBox'; import SectionFullScreen from '../components/SectionFullScreen'; import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; import BaseButtons from '../components/BaseButtons'; import { getPageTitle } from '../config'; import { useAppSelector } from '../stores/hooks'; @@ -128,22 +127,45 @@ export default function Starter() { : null}
- +
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

+

Elige tu portal para entrar más rápido al flujo correcto.

+

Admin y maestros pueden crear exámenes y sesiones. Estudiantes entran con código. Padres revisan resultados.

- + + + + + -
diff --git a/frontend/src/pages/parent-portal.tsx b/frontend/src/pages/parent-portal.tsx new file mode 100644 index 0000000..bfc25f5 --- /dev/null +++ b/frontend/src/pages/parent-portal.tsx @@ -0,0 +1,82 @@ +import { mdiAccountGroup, mdiChartBox, mdiFileChart, mdiLogin, mdiAccountCircle } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import BaseButton from '../components/BaseButton' +import CardBox from '../components/CardBox' +import PortalActionCard from '../components/PortalActionCard' +import SectionMain from '../components/SectionMain' +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton' +import { getPageTitle } from '../config' +import LayoutAuthenticated from '../layouts/Authenticated' + +const parentActions = [ + { + title: 'Resultados del estudiante', + description: 'Consulta reportes y calificaciones disponibles para tu estudiante.', + href: '/report_cards/report_cards-list', + cta: 'Ver resultados', + icon: mdiChartBox, + color: 'info' as const, + }, + { + title: 'Reporte detallado', + description: 'Entra a las tarjetas de reporte para revisar progreso y evidencia del examen.', + href: '/report_cards/report_cards-list', + cta: 'Abrir reportes', + icon: mdiFileChart, + color: 'success' as const, + }, + { + title: 'Mi perfil', + description: 'Revisa tu cuenta de padre/madre o tutor y mantén tus datos actualizados.', + href: '/profile', + cta: 'Ver perfil', + icon: mdiAccountCircle, + color: 'warning' as const, + }, +] + +const ParentPortal = () => { + return ( + <> + + {getPageTitle('Portal Padres')} + + + + {''} + + + +
+
+

+ Familias y tutores +

+

+ Resultados claros para padres. +

+

+ Este portal está pensado para que las familias entren, revisen resultados y entiendan el + progreso del estudiante sin navegar por todo el sistema. +

+
+ +
+
+ +
+ {parentActions.map((action) => ( + + ))} +
+
+ + ) +} + +ParentPortal.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default ParentPortal diff --git a/frontend/src/pages/search.tsx b/frontend/src/pages/search.tsx index a46cc96..72ed039 100644 --- a/frontend/src/pages/search.tsx +++ b/frontend/src/pages/search.tsx @@ -1,9 +1,7 @@ import React, { ReactElement, useEffect, useState } from 'react'; import Head from 'next/head'; import 'react-datepicker/dist/react-datepicker.css'; -import { useAppDispatch } from '../stores/hooks'; - -import { useAppSelector } from '../stores/hooks'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; import { useRouter } from 'next/router'; import LayoutAuthenticated from '../layouts/Authenticated'; diff --git a/frontend/src/pages/student-portal.tsx b/frontend/src/pages/student-portal.tsx new file mode 100644 index 0000000..6d4419b --- /dev/null +++ b/frontend/src/pages/student-portal.tsx @@ -0,0 +1,215 @@ +import { mdiAccountSchool, mdiCheckCircle, mdiClipboardText, mdiHome, mdiLogin } from '@mdi/js' +import Head from 'next/head' +import Link from 'next/link' +import React, { ReactElement } from 'react' +import BaseButton from '../components/BaseButton' +import CardBox from '../components/CardBox' +import BaseIcon from '../components/BaseIcon' +import SectionMain from '../components/SectionMain' +import { getPageTitle } from '../config' +import LayoutGuest from '../layouts/Guest' + +type StudentPortalStage = 'entry' | 'confirm' | 'waiting' + +const StudentPortal = () => { + const [stage, setStage] = React.useState('entry') + const [form, setForm] = React.useState({ + sessionCode: '', + studentName: '', + studentId: '', + }) + + const canContinue = Boolean( + form.sessionCode.length === 3 && form.studentName.trim() && form.studentId.trim(), + ) + + const updateField = (field: keyof typeof form, value: string) => { + setForm((current) => ({ ...current, [field]: value })) + } + + const handleCodeChange = (value: string) => { + updateField('sessionCode', value.replace(/\D/g, '').slice(0, 3)) + } + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault() + if (!canContinue) return + setStage('confirm') + } + + return ( + <> + + {getPageTitle('Portal Estudiantes')} + + +
+
+
+ + + NWEA Estudiantes + +
+ + +
+
+
+ + +
+
+

+ Entrada rápida con código de sesión +

+

+ Portal para estudiantes +

+

+ Escribe el código de 3 dígitos que te dio tu maestro, tu nombre y tu ID de estudiante. + Después confirma que eres tú y espera a que empiece el examen. +

+
+
+ 1. Código + Usa 3 dígitos. +
+
+ 2. Confirmar + Verifica tu identidad. +
+
+ 3. Esperar + El proctor inicia la prueba. +
+
+
+ + + {stage === 'entry' && ( +
+
+ +

Entrar al examen

+

+ Completa estos datos exactamente como te los dio tu escuela. +

+
+ +
+ + handleCodeChange(event.target.value)} + inputMode="numeric" + placeholder="123" + className="h-14 w-full rounded border border-gray-300 px-4 text-center text-3xl font-black tracking-[0.5em] focus:border-blue-600 focus:outline-none focus:ring focus:ring-blue-200 dark:border-dark-700 dark:bg-dark-800 dark:text-white" + /> +
+ +
+ + updateField('studentName', event.target.value)} + placeholder="Ejemplo: Ana García" + className="h-12 w-full rounded border border-gray-300 px-4 focus:border-blue-600 focus:outline-none focus:ring focus:ring-blue-200 dark:border-dark-700 dark:bg-dark-800 dark:text-white" + /> +
+ +
+ + updateField('studentId', event.target.value)} + placeholder="ID escolar" + className="h-12 w-full rounded border border-gray-300 px-4 focus:border-blue-600 focus:outline-none focus:ring focus:ring-blue-200 dark:border-dark-700 dark:bg-dark-800 dark:text-white" + /> +
+ + + + )} + + {stage === 'confirm' && ( +
+ +
+

¿Eres tú?

+

+ Confirma que la información está correcta antes de entrar a la sala de espera. +

+
+
+

+ Código: {form.sessionCode} +

+

+ Nombre: {form.studentName} +

+

+ ID: {form.studentId} +

+
+
+ setStage('entry')} /> + setStage('waiting')} /> +
+
+ )} + + {stage === 'waiting' && ( +
+
+ +
+
+

Sala de espera

+

+ Listo, {form.studentName}. Espera a que tu maestro o proctor inicie el examen. +

+
+
+ Código de sesión: {form.sessionCode} +
+ setStage('entry')} /> +
+ )} +
+
+
+
+ + ) +} + +StudentPortal.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default StudentPortal diff --git a/frontend/src/pages/teacher-portal.tsx b/frontend/src/pages/teacher-portal.tsx new file mode 100644 index 0000000..457e109 --- /dev/null +++ b/frontend/src/pages/teacher-portal.tsx @@ -0,0 +1,99 @@ +import { + mdiAccountSchool, + mdiCalendarClock, + mdiChartBox, + mdiClipboardText, + mdiFileDocumentEdit, + mdiHumanMaleBoard, +} from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import CardBox from '../components/CardBox' +import PortalActionCard from '../components/PortalActionCard' +import SectionMain from '../components/SectionMain' +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton' +import { getPageTitle } from '../config' +import LayoutAuthenticated from '../layouts/Authenticated' + +const teacherActions = [ + { + title: 'Mis exámenes', + description: 'Crea, revisa o edita exámenes antes de publicarlos para tus estudiantes.', + href: '/exams/exams-list', + cta: 'Ver exámenes', + icon: mdiClipboardText, + color: 'info' as const, + }, + { + title: 'Sesiones de examen', + description: 'Crea sesiones, comparte códigos y acompaña a los estudiantes durante el examen.', + href: '/exam_sessions/exam_sessions-list', + cta: 'Ver sesiones', + icon: mdiCalendarClock, + color: 'success' as const, + }, + { + title: 'Estudiantes', + description: 'Administra estudiantes, IDs y datos necesarios para entrar a una sesión.', + href: '/students/students-list', + cta: 'Ver estudiantes', + icon: mdiAccountSchool, + color: 'warning' as const, + }, + { + title: 'Calificar respuestas', + description: 'Revisa respuestas abiertas y deja listas las calificaciones finales.', + href: '/attempt_answers/attempt_answers-list', + cta: 'Calificar', + icon: mdiFileDocumentEdit, + color: 'info' as const, + }, + { + title: 'Resultados', + description: 'Consulta reportes por estudiante para compartir avances con la escuela o familias.', + href: '/report_cards/report_cards-list', + cta: 'Ver resultados', + icon: mdiChartBox, + color: 'success' as const, + }, +] + +const TeacherPortal = () => { + return ( + <> + + {getPageTitle('Portal Maestros')} + + + + {''} + + + +

+ Flujo para maestros +

+

+ Exámenes, sesiones, estudiantes y resultados en un solo lugar. +

+

+ Usa este portal como panel rápido para preparar pruebas, abrir sesiones y revisar el progreso + de tus estudiantes. +

+
+ +
+ {teacherActions.map((action) => ( + + ))} +
+
+ + ) +} + +TeacherPortal.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default TeacherPortal