Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
7d6b44ebbc Autosave: 20260618-050816 2026-06-18 05:08:13 +00:00
11 changed files with 606 additions and 18 deletions

View File

@ -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';

View File

@ -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'

View File

@ -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 (
<CardBox className="h-full" isHoverable>
<div className="flex h-full flex-col">
<div className="mb-4 flex items-start justify-between gap-4">
<div>
<h2 className="text-xl font-bold text-gray-900 dark:text-white">{title}</h2>
<p className="mt-2 text-sm leading-relaxed text-gray-500 dark:text-gray-400">
{description}
</p>
</div>
<BaseIcon
path={icon}
size={30}
className="rounded-full bg-blue-50 p-3 text-blue-600 dark:bg-dark-800 dark:text-pavitra-blue"
w="w-14"
h="h-14"
/>
</div>
{note && <p className="mb-4 text-xs font-medium text-gray-400 dark:text-gray-500">{note}</p>}
<div className="mt-auto">
<BaseButton href={href} label={cta} color={color} className="w-full" />
</div>
</div>
</CardBox>
)
}

View File

@ -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'

View File

@ -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',

View File

@ -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 (
<>
<Head>
<title>{getPageTitle('Portal Admin')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiShieldAccount} title="Portal Admin" main>
{''}
</SectionTitleLineWithButton>
<CardBox className="mb-6">
<div className="grid gap-6 lg:grid-cols-[1.5fr_1fr] lg:items-center">
<div>
<p className="mb-2 text-sm font-semibold uppercase tracking-wide text-blue-600 dark:text-pavitra-blue">
Administración rápida
</p>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Crear examen, crear sesión y revisar resultados.
</h1>
<p className="mt-3 max-w-3xl text-gray-500 dark:text-gray-400">
Este portal deja lo esencial en un solo lugar para que el Admin no tenga que buscar en
todas las tablas del sistema.
</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row lg:flex-col">
<BaseButton href="/exams/exams-new" label="Crear examen" color="info" className="w-full" />
<BaseButton
href="/exam_sessions/exam_sessions-new"
label="Crear sesión"
color="success"
className="w-full"
/>
<BaseButton
href="/report_cards/report_cards-list"
label="Resultados"
color="warning"
className="w-full"
/>
</div>
</div>
</CardBox>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{adminActions.map((action) => (
<PortalActionCard key={action.href} {...action} />
))}
</div>
</SectionMain>
</>
)
}
AdminPortal.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
}
export default AdminPortal

View File

@ -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}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your NWEA Examenes app!"/>
<CardBoxComponentTitle title="Bienvenido a NWEA Exámenes"/>
<div className="space-y-3">
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center text-gray-500'>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
<p className='text-center text-gray-500'>Elige tu portal para entrar más rápido al flujo correcto.</p>
<p className='text-center text-gray-500'>Admin y maestros pueden crear exámenes y sesiones. Estudiantes entran con código. Padres revisan resultados.</p>
</div>
<BaseButtons>
<BaseButtons type='justify-center' className='mt-6'>
<BaseButton
href='/admin-portal'
label='Portal Admin'
color='info'
className='w-full sm:w-auto'
/>
<BaseButton
href='/teacher-portal'
label='Portal Maestros'
color='success'
className='w-full sm:w-auto'
/>
<BaseButton
href='/student-portal'
label='Portal Estudiantes'
color='warning'
className='w-full sm:w-auto'
/>
<BaseButton
href='/parent-portal'
label='Portal Padres'
color='info'
outline
className='w-full sm:w-auto'
/>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
color='whiteDark'
className='w-full sm:w-auto'
/>
</BaseButtons>
</CardBox>
</div>

View File

@ -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 (
<>
<Head>
<title>{getPageTitle('Portal Padres')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiAccountGroup} title="Portal Padres" main>
{''}
</SectionTitleLineWithButton>
<CardBox className="mb-6">
<div className="grid gap-6 lg:grid-cols-[1.5fr_1fr] lg:items-center">
<div>
<p className="mb-2 text-sm font-semibold uppercase tracking-wide text-blue-600 dark:text-pavitra-blue">
Familias y tutores
</p>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Resultados claros para padres.
</h1>
<p className="mt-3 max-w-3xl text-gray-500 dark:text-gray-400">
Este portal está pensado para que las familias entren, revisen resultados y entiendan el
progreso del estudiante sin navegar por todo el sistema.
</p>
</div>
<BaseButton href="/login" label="Entrar con mi cuenta" icon={mdiLogin} color="info" className="w-full" />
</div>
</CardBox>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{parentActions.map((action) => (
<PortalActionCard key={`${action.href}-${action.title}`} {...action} />
))}
</div>
</SectionMain>
</>
)
}
ParentPortal.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
}
export default ParentPortal

View File

@ -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';

View File

@ -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<StudentPortalStage>('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<HTMLFormElement>) => {
event.preventDefault()
if (!canContinue) return
setStage('confirm')
}
return (
<>
<Head>
<title>{getPageTitle('Portal Estudiantes')}</title>
</Head>
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-emerald-50 dark:from-dark-900 dark:via-dark-800 dark:to-dark-900">
<header className="border-b border-gray-200 bg-white/80 backdrop-blur dark:border-dark-700 dark:bg-dark-900/80">
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
<Link href="/" className="flex items-center gap-2 font-bold text-gray-900 dark:text-white">
<BaseIcon path={mdiAccountSchool} size={24} className="text-blue-600 dark:text-pavitra-blue" />
NWEA Estudiantes
</Link>
<div className="flex gap-2">
<BaseButton href="/" label="Inicio" icon={mdiHome} color="whiteDark" small />
<BaseButton href="/login" label="Login" icon={mdiLogin} color="info" small />
</div>
</div>
</header>
<SectionMain>
<div className="mx-auto grid max-w-6xl gap-6 lg:grid-cols-[1fr_1.15fr] lg:items-center">
<div>
<p className="mb-3 inline-flex rounded-full bg-blue-100 px-3 py-1 text-sm font-semibold text-blue-700 dark:bg-dark-800 dark:text-pavitra-blue">
Entrada rápida con código de sesión
</p>
<h1 className="text-4xl font-black leading-tight text-gray-900 dark:text-white md:text-5xl">
Portal para estudiantes
</h1>
<p className="mt-4 text-lg leading-relaxed text-gray-600 dark:text-gray-300">
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 y espera a que empiece el examen.
</p>
<div className="mt-6 grid gap-3 text-sm text-gray-500 dark:text-gray-400 sm:grid-cols-3">
<div className="rounded-lg bg-white/80 p-4 dark:bg-dark-900/80">
<strong className="block text-gray-900 dark:text-white">1. Código</strong>
Usa 3 dígitos.
</div>
<div className="rounded-lg bg-white/80 p-4 dark:bg-dark-900/80">
<strong className="block text-gray-900 dark:text-white">2. Confirmar</strong>
Verifica tu identidad.
</div>
<div className="rounded-lg bg-white/80 p-4 dark:bg-dark-900/80">
<strong className="block text-gray-900 dark:text-white">3. Esperar</strong>
El proctor inicia la prueba.
</div>
</div>
</div>
<CardBox>
{stage === 'entry' && (
<form onSubmit={handleSubmit} className="space-y-5">
<div className="text-center">
<BaseIcon
path={mdiClipboardText}
size={36}
className="mx-auto mb-3 rounded-full bg-blue-50 p-4 text-blue-600 dark:bg-dark-800 dark:text-pavitra-blue"
w="w-20"
h="h-20"
/>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Entrar al examen</h2>
<p className="mt-2 text-gray-500 dark:text-gray-400">
Completa estos datos exactamente como te los dio tu escuela.
</p>
</div>
<div>
<label className="mb-1 block text-sm font-semibold text-gray-700 dark:text-gray-200">
Código de sesión
</label>
<input
value={form.sessionCode}
onChange={(event) => 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"
/>
</div>
<div>
<label className="mb-1 block text-sm font-semibold text-gray-700 dark:text-gray-200">
Nombre completo
</label>
<input
value={form.studentName}
onChange={(event) => 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"
/>
</div>
<div>
<label className="mb-1 block text-sm font-semibold text-gray-700 dark:text-gray-200">
ID de estudiante
</label>
<input
value={form.studentId}
onChange={(event) => 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"
/>
</div>
<BaseButton
type="submit"
label="Continuar"
color="info"
className="w-full"
disabled={!canContinue}
/>
</form>
)}
{stage === 'confirm' && (
<div className="space-y-5 text-center">
<BaseIcon
path={mdiCheckCircle}
size={42}
className="mx-auto rounded-full bg-emerald-50 p-4 text-emerald-600 dark:bg-dark-800"
w="w-20"
h="h-20"
/>
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">¿Eres ?</h2>
<p className="mt-2 text-gray-500 dark:text-gray-400">
Confirma que la información está correcta antes de entrar a la sala de espera.
</p>
</div>
<div className="rounded-lg bg-gray-50 p-4 text-left dark:bg-dark-800">
<p>
<strong>Código:</strong> {form.sessionCode}
</p>
<p>
<strong>Nombre:</strong> {form.studentName}
</p>
<p>
<strong>ID:</strong> {form.studentId}
</p>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<BaseButton label="Cambiar datos" color="whiteDark" onClick={() => setStage('entry')} />
<BaseButton label="Sí, soy yo" color="success" onClick={() => setStage('waiting')} />
</div>
</div>
)}
{stage === 'waiting' && (
<div className="space-y-5 text-center">
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-blue-50 text-blue-600 dark:bg-dark-800 dark:text-pavitra-blue">
<span className="h-10 w-10 animate-pulse rounded-full bg-current opacity-40" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Sala de espera</h2>
<p className="mt-2 text-gray-500 dark:text-gray-400">
Listo, {form.studentName}. Espera a que tu maestro o proctor inicie el examen.
</p>
</div>
<div className="rounded-lg bg-blue-50 p-4 text-sm text-blue-700 dark:bg-dark-800 dark:text-pavitra-blue">
Código de sesión: <strong>{form.sessionCode}</strong>
</div>
<BaseButton label="Volver al inicio" color="whiteDark" onClick={() => setStage('entry')} />
</div>
)}
</CardBox>
</div>
</SectionMain>
</div>
</>
)
}
StudentPortal.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>
}
export default StudentPortal

View File

@ -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 (
<>
<Head>
<title>{getPageTitle('Portal Maestros')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiHumanMaleBoard} title="Portal Maestros" main>
{''}
</SectionTitleLineWithButton>
<CardBox className="mb-6">
<p className="mb-2 text-sm font-semibold uppercase tracking-wide text-blue-600 dark:text-pavitra-blue">
Flujo para maestros
</p>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Exámenes, sesiones, estudiantes y resultados en un solo lugar.
</h1>
<p className="mt-3 max-w-4xl text-gray-500 dark:text-gray-400">
Usa este portal como panel rápido para preparar pruebas, abrir sesiones y revisar el progreso
de tus estudiantes.
</p>
</CardBox>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
{teacherActions.map((action) => (
<PortalActionCard key={action.href} {...action} />
))}
</div>
</SectionMain>
</>
)
}
TeacherPortal.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
}
export default TeacherPortal