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' && (
+
+ )}
+
+ {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