diff --git a/next.config.ts b/next.config.ts
index db91768..b8119aa 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -6,12 +6,12 @@ const nextConfig: NextConfig = {
{
protocol: 'http',
hostname: 'localhost',
- port: '3342', //4200
+ port: '4200', //4200
},
{
protocol: 'https',
- hostname: 'venus.acatlan.unam.mx'
- }
+ hostname: 'venus.acatlan.unam.mx',
+ },
],
},
};
diff --git a/package-lock.json b/package-lock.json
index b168c61..82e1a46 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7403,6 +7403,96 @@
"peerDependencies": {
"@types/emscripten": ">=1.39.6"
}
+ },
+ "node_modules/@next/swc-darwin-arm64": {
+ "version": "15.5.12",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.12.tgz",
+ "integrity": "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "15.5.12",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.12.tgz",
+ "integrity": "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "15.5.12",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.12.tgz",
+ "integrity": "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-musl": {
+ "version": "15.5.12",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.12.tgz",
+ "integrity": "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "15.5.12",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.12.tgz",
+ "integrity": "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-x64-msvc": {
+ "version": "15.5.12",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.12.tgz",
+ "integrity": "sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
}
}
}
diff --git a/public/plantillas/carga-alumnos.xlsx b/public/plantillas/carga-alumnos.xlsx
new file mode 100644
index 0000000..8284081
Binary files /dev/null and b/public/plantillas/carga-alumnos.xlsx differ
diff --git a/public/plantillas/carga-trabajadores.xlsx b/public/plantillas/carga-trabajadores.xlsx
new file mode 100644
index 0000000..33f6470
Binary files /dev/null and b/public/plantillas/carga-trabajadores.xlsx differ
diff --git a/src/app/(admins)/user/carga_masiva_usuarios/page.tsx b/src/app/(admins)/user/carga_masiva_usuarios/page.tsx
new file mode 100644
index 0000000..bc80b3b
--- /dev/null
+++ b/src/app/(admins)/user/carga_masiva_usuarios/page.tsx
@@ -0,0 +1,224 @@
+'use client';
+
+import React, { useRef, useState } from 'react';
+import axiosInstance from '@/utils/api-config';
+import { Button, Form, Alert } from 'react-bootstrap';
+import { FaUpload, FaDownload } from 'react-icons/fa';
+
+type TipoCarga = 'alumnos' | 'trabajadores';
+
+interface ResultadoCarga {
+ insertados: number;
+ omitidos: number;
+}
+
+interface SeccionState {
+ archivo: File | null;
+ loading: boolean;
+ error: string | null;
+ success: string | null;
+ resultado: ResultadoCarga | null;
+}
+
+const initialState: SeccionState = {
+ archivo: null,
+ loading: false,
+ error: null,
+ success: null,
+ resultado: null,
+};
+
+export default function Page() {
+ const [alumnos, setAlumnos] = useState(initialState);
+ const [trabajadores, setTrabajadores] = useState(initialState);
+
+ const alumnosRef = useRef(null);
+ const trabajadoresRef = useRef(null);
+
+ const getState = (tipo: TipoCarga) =>
+ tipo === 'alumnos' ? alumnos : trabajadores;
+ const setState = (tipo: TipoCarga) =>
+ tipo === 'alumnos' ? setAlumnos : setTrabajadores;
+
+ const validarExt = (file: File | null): boolean => {
+ if (!file) return false;
+ return /\.xlsx$/i.test(file.name);
+ };
+
+ const handleArchivoChange = (tipo: TipoCarga, file: File | null) => {
+ if (file && !validarExt(file)) {
+ setState(tipo)((prev) => ({
+ ...prev,
+ archivo: null,
+ error: 'Selecciona un archivo .xlsx válido',
+ success: null,
+ }));
+ return;
+ }
+ setState(tipo)((prev) => ({
+ ...prev,
+ archivo: file,
+ error: null,
+ success: null,
+ }));
+ };
+
+ const handleCargaMasiva = async (tipo: TipoCarga) => {
+ const estado = getState(tipo);
+ if (!estado.archivo) {
+ setState(tipo)((prev) => ({
+ ...prev,
+ error: 'Selecciona un archivo xlsx',
+ }));
+ return;
+ }
+ const formData = new FormData();
+ formData.append('file', estado.archivo);
+ setState(tipo)((prev) => ({
+ ...prev,
+ loading: true,
+ error: null,
+ success: null,
+ }));
+ try {
+ const res = await axiosInstance.post(`/${tipo}/cargar`, formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
+ setState(tipo)((prev) => ({
+ ...prev,
+ archivo: null,
+ success: `${tipo === 'alumnos' ? 'Alumnos' : 'Trabajadores'} cargados exitosamente`,
+ resultado: res.data ?? null,
+ }));
+ const ref = tipo === 'alumnos' ? alumnosRef : trabajadoresRef;
+ if (ref.current) ref.current.value = '';
+ } catch (error) {
+ const err = error as { response?: { data?: { message?: string } } };
+ setState(tipo)((prev) => ({
+ ...prev,
+ error: err?.response?.data?.message || 'Error al cargar el archivo',
+ }));
+ } finally {
+ setState(tipo)((prev) => ({ ...prev, loading: false }));
+ }
+ };
+
+ const handleDescargarPlantilla = (tipo: TipoCarga) => {
+ const nombre = tipo === 'alumnos' ? 'carga-alumnos' : 'carga-trabajadores';
+ const link = document.createElement('a');
+ link.href = `/plantillas/${nombre}.xlsx`;
+ link.setAttribute('download', `${nombre}.xlsx`);
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ };
+
+ const SeccionCarga = ({
+ tipo,
+ label,
+ }: {
+ tipo: TipoCarga;
+ label: string;
+ }) => {
+ const estado = getState(tipo);
+ const inputRef = tipo === 'alumnos' ? alumnosRef : trabajadoresRef;
+
+ return (
+
+
{label}
+
+ {estado.error && (
+
setState(tipo)((p) => ({ ...p, error: null }))}
+ dismissible
+ >
+ {estado.error}
+
+ )}
+ {estado.success && (
+
+ setState(tipo)((p) => ({ ...p, success: null, resultado: null }))
+ }
+ dismissible
+ >
+ {estado.success}
+ {estado.resultado && (
+
+
+ Insertados: {estado.resultado.insertados}
+
+
+ Omitidos: {estado.resultado.omitidos}
+
+
+ )}
+
+ )}
+
+
+ Archivo Excel (.xlsx)
+ inputRef.current?.click()}
+ >
+
+
+ {estado.archivo?.name ||
+ 'Arrastra aquí tu archivo o haz click para buscar'}
+
+
+
+ handleArchivoChange(tipo, e.target.files?.[0] ?? null)
+ }
+ />
+
+
+
+ handleCargaMasiva(tipo)}
+ >
+ {estado.loading ? 'Cargando...' : 'Cargar archivo'}
+
+
+ handleDescargarPlantilla(tipo)}
+ >
+
+ Descargar plantilla
+
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
Carga masiva de usuarios
+
+ Sube un archivo Excel para registrar alumnos o trabajadores en el
+ sistema.
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(admins)/user/evento/[id_evento]/formularios/crear/page.tsx b/src/app/(admins)/user/evento/[id_evento]/formularios/crear/page.tsx
index f8fafba..3861fd1 100644
--- a/src/app/(admins)/user/evento/[id_evento]/formularios/crear/page.tsx
+++ b/src/app/(admins)/user/evento/[id_evento]/formularios/crear/page.tsx
@@ -1,7 +1,6 @@
'use client';
import { plantillasDisponibles } from '@/data/plantillas';
import { FormularioCreacion } from '@/types/create-formulario';
-import Image from 'next/image';
import React, { useState } from 'react';
import CreateFormulario from '@/containers/create-formulario';
import FormularioCardPreview from '@/components/formulario/formulario-card-preview';
@@ -193,21 +192,18 @@ export default function Page() {
-
+
{plantillasDisponibles.map((plantilla) => (
handleSeleccionarPlantilla(plantilla.id)}
>
-
))}
diff --git a/src/app/(admins)/user/evento/[id_evento]/page.tsx b/src/app/(admins)/user/evento/[id_evento]/page.tsx
index 129796c..249e487 100644
--- a/src/app/(admins)/user/evento/[id_evento]/page.tsx
+++ b/src/app/(admins)/user/evento/[id_evento]/page.tsx
@@ -27,6 +27,40 @@ export default function Page() {
{}
);
+ type TipoCarga = 'alumnos' | 'trabajadores';
+ const [tipoCarga, setTipoCarga] = useState
('alumnos');
+ const [archivoCarga, setArchivoCarga] = useState(null);
+ const [loadingCarga, setLoadingCarga] = useState(false);
+
+ const handleCargaMasiva = async () => {
+ if (!archivoCarga) {
+ toast.error('Selecciona un archivo xlsx');
+ return;
+ }
+ const formData = new FormData();
+ formData.append('file', archivoCarga);
+ formData.append('id_evento', params.id_evento);
+ setLoadingCarga(true);
+ try {
+ await axiosInstance.post(
+ `/evento/${params.id_evento}/usuarios`,
+ formData,
+ {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ }
+ );
+ toast.success(
+ `${tipoCarga === 'alumnos' ? 'Alumnos' : 'Trabajadores'} cargados exitosamente`
+ );
+ setArchivoCarga(null);
+ } catch (error) {
+ const err = error as { response?: { data?: { message?: string } } };
+ toast.error(err?.response?.data?.message || 'Error al cargar el archivo');
+ } finally {
+ setLoadingCarga(false);
+ }
+ };
+
const handleChange = (
field: keyof CreateEventoType,
value: string | Date
@@ -88,8 +122,61 @@ export default function Page() {
handleOnChange={handleEventoActualizado}
/>
+ {/* Carga masiva de alumnos o trabajadores */}
+ {tipo_usuario !== 'staff' && (
+
+
Carga masiva de participantes
+
+ Sube un archivo .xlsx para registrar los
+ participantes que podrán inscribirse a este evento. Solo los
+ usuarios incluidos en la lista tendrán acceso al registro.
+
+
+
+
+
Tipo de participante
+
+ setTipoCarga('alumnos')}
+ >
+ Alumnos
+
+ setTipoCarga('trabajadores')}
+ >
+ Trabajadores
+
+
+
+
+
+ Archivo xlsx
+ setArchivoCarga(e.target.files?.[0] ?? null)}
+ />
+
+
+
+ {loadingCarga ? 'Cargando...' : 'Cargar'}
+
+
+
+ )}
+
{/* Validacion del tipo de usuario */}
- { tipo_usuario !== "staff" && (
+ {tipo_usuario !== 'staff' && (
@@ -103,7 +190,9 @@ export default function Page() {
icon="plus"
variant="primary"
onClick={() =>
- router.push(`/user/evento/${params.id_evento}/formularios/crear`)
+ router.push(
+ `/user/evento/${params.id_evento}/formularios/crear`
+ )
}
>
Crear nuevo formulario
diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx
index 8f8b3fc..84d51ee 100644
--- a/src/components/navbar.tsx
+++ b/src/components/navbar.tsx
@@ -29,6 +29,10 @@ const Navbar: React.FC = () => {
href: '/user/eventos/carga_masiva_banner',
label: 'Carga Masiva Imagenes',
},
+ {
+ href: '/user/carga_masiva_usuarios',
+ label: 'Carga Masiva Usuarios',
+ },
],
},
{ href: '/user/usuarios', label: 'Usuarios' },
diff --git a/src/containers/create-formulario.tsx b/src/containers/create-formulario.tsx
index eb8c981..9d92dc1 100644
--- a/src/containers/create-formulario.tsx
+++ b/src/containers/create-formulario.tsx
@@ -214,6 +214,13 @@ export default function FormularioEditor({
)}
+
actualizarCampo('mensaje_correo', e.target.value)}
+ placeholder="Mensaje que se enviará al correo del participante al registrarse"
+ />
+
+
+ handleChange('mensaje_correo', e.target.value)
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
¡Felicidades!
+
+ Has completado exitosamente el formulario
+
+
+
+ {nombreEvento && (
+
+
+
+
Evento
+
+
{nombreEvento}
+
+ )}
+
+ {nombreFormulario && (
+
+
+
+
Formulario
+
+
{nombreFormulario}
+
+ )}
+
+ {response.message && (
+
+
+
+
+
+
Mensaje:
+
{response.message}
+
+
+
+
+ )}
+
+
+
+
+
+ {qrImageUrl ? (
+
+
+
+ Código QR
+
+
+
+
+
+ ) : (
+
+
+
No hay código QR disponible
+
+ )}
+
+
+
+
+
+
+
+
+ Ir al inicio
+
+
+
+
+
+ );
+}
diff --git a/src/containers/formulario/formulario-registro.tsx b/src/containers/formulario/formulario-registro.tsx
index eb4a6e6..4e6fcce 100644
--- a/src/containers/formulario/formulario-registro.tsx
+++ b/src/containers/formulario/formulario-registro.tsx
@@ -1,13 +1,10 @@
-import React, { useState, useEffect } from 'react';
-import { useGetApi } from '@/hooks/use-get-api';
-import { GetCuestionario } from '@/types/evento';
+import React from 'react';
import { SubmitResponse } from '@/types/submit';
import SimpleInput from '@/components/input';
import RadioOptionGroup, { RadioOption } from '@/components/radio-option-group';
import Button from '@/components/button';
-import axiosInstance from '@/utils/api-config';
import toast from 'react-hot-toast';
-import { getAxiosError } from '@/utils/errors-utils';
+import { useFormulario } from '@/context/formulario';
export type UsuarioDataResponse = {
cuenta: string | null;
@@ -18,19 +15,7 @@ export type UsuarioDataResponse = {
rfc?: string | null;
};
-type UsuarioData = {
- nombre: string;
- apellidos: string;
- correo: string;
- genero: 'M' | 'F';
- carrera: string;
- rfc?: string;
-};
-
-type RespuestaFormulario = Record
;
-
export default function FormularioRegistro({
- id_cuestionario,
handleSubmitFormulario,
}: {
evento: string;
@@ -39,270 +24,61 @@ export default function FormularioRegistro({
id_cuestionario: number;
handleSubmitFormulario: (data: SubmitResponse) => void;
}) {
- // Estados principales
- const [respuestas, setRespuestas] = useState({});
- const [cuentaInfo, setCuentaInfo] = useState(null);
- const [busquedaCompletada, setBusquedaCompletada] = useState(false);
- const [cuentaBuscada, setCuentaBuscada] = useState('');
- const [isSending, setIsSending] = useState(false);
-
- // Carga del cuestionario
- const { data, error } = useGetApi(
- `/cuestionario/${id_cuestionario}/formulario`
- );
+ const {
+ state,
+ preguntaComunidad,
+ preguntaCuenta,
+ esComunidadSi,
+ esExclusivo,
+ mostrarFormularioCompleto,
+ setRespuesta,
+ submitFormulario,
+ } = useFormulario();
- // Extraer todas las preguntas del cuestionario
- const preguntas = data?.cuestionario.secciones.flatMap((seccion) =>
- seccion.preguntas.map((pregunta) => pregunta.pregunta)
- );
+ const { data, loadingData, errorData, respuestas, isSending, errorBusqueda, loadingBusqueda } = state;
- // Identificar preguntas especiales
- const preguntaComunidad = preguntas?.find(
- (pregunta) =>
- pregunta.validacion === 'comunidad_alumno' ||
- pregunta.validacion === 'comunidad_trabajador'
- );
-
- const preguntaCuenta = preguntas?.find(
- (pregunta) =>
- pregunta.validacion === 'cuenta_alumno' ||
- pregunta.validacion === 'cuenta_trabajador' ||
- pregunta.validacion === 'rfc'
- );
-
- // Determinar tipo de comunidad seleccionada
- const comunidadSeleccionada = preguntaComunidad
- ? respuestas[preguntaComunidad.id_pregunta]
+ const comunidadSeleccionada = preguntaComunidad
+ ? respuestas[preguntaComunidad.id_pregunta]
: null;
-
- const esComunidadSi = comunidadSeleccionada &&
- preguntaComunidad?.opciones.find(op =>
- op.id_opcion === Number(comunidadSeleccionada) &&
- op.opcion.opcion.toLowerCase().includes('si')
- );
- // Efecto para buscar información de cuenta
- useEffect(() => {
- if (!preguntaCuenta || !esComunidadSi) {
- setCuentaInfo(null);
- setBusquedaCompletada(false);
- setCuentaBuscada('');
- return;
- }
-
- const cuenta = respuestas[preguntaCuenta.id_pregunta];
-
- if (!cuenta || typeof cuenta !== 'string') {
- setCuentaInfo(null);
- setBusquedaCompletada(false);
- setCuentaBuscada('');
- return;
- }
-
- // Determinar longitudes válidas según el tipo
- const longitudesValidas = {
- 'cuenta_alumno': 9,
- 'cuenta_trabajador': 6,
- 'rfc': 10
- };
-
- const longitudRequerida = longitudesValidas[preguntaCuenta.validacion as keyof typeof longitudesValidas];
- const puedeBuscar = cuenta.length === longitudRequerida;
-
- // Solo buscar si no se ha buscado ya esta cuenta
- if (puedeBuscar && cuentaBuscada !== cuenta) {
- setBusquedaCompletada(false);
-
- const fetchCuentaInfo = async () => {
- try {
- let endpoint: string;
-
- switch (preguntaCuenta.validacion) {
- case 'cuenta_alumno':
- endpoint = `/alumnos/${cuenta}`;
- break;
- case 'cuenta_trabajador':
- endpoint = `/trabajadores/${cuenta}`;
- break;
- case 'rfc':
- endpoint = `/trabajadores/${cuenta}`;
- break;
- default:
- throw new Error('Tipo de validación no reconocido');
- }
-
- const res = await axiosInstance.get(endpoint);
- console.log('Datos de cuenta obtenidos:', res.data);
-
- // Verificar si hay datos válidos
- const hayDatosValidos = res.data.nombre || res.data.apellidos ||
- res.data.carrera || res.data.genero;
-
- if (hayDatosValidos) {
- setCuentaInfo({
- nombre: res.data.nombre || '',
- apellidos: res.data.apellidos || '',
- correo: res.data.cuenta
- ? `${res.data.cuenta}@pcpuma.acatlan.unam.mx`
- : '',
- genero: (res.data.genero === 'M' || res.data.genero === 'F')
- ? res.data.genero
- : 'M',
- carrera: res.data.carrera || '',
- rfc: res.data.rfc || undefined,
- });
- } else {
- setCuentaInfo(null);
- }
-
- setCuentaBuscada(cuenta);
- setBusquedaCompletada(true);
-
- } catch (error) {
- console.error('Error al obtener datos de cuenta:', error);
- setCuentaInfo(null);
- setCuentaBuscada(cuenta);
- setBusquedaCompletada(true);
- }
- };
-
- fetchCuentaInfo();
- } else if (!puedeBuscar) {
- setCuentaInfo(null);
- setBusquedaCompletada(false);
- setCuentaBuscada('');
- }
- }, [respuestas, preguntaCuenta, esComunidadSi, cuentaBuscada]);
-
- // Efecto para prellenar respuestas cuando se obtiene información de cuenta
- useEffect(() => {
- if (!cuentaInfo || !data?.cuestionario.secciones) return;
-
- const nuevasRespuestas: RespuestaFormulario = {};
-
- preguntas?.forEach((pregunta) => {
- const id = pregunta.id_pregunta;
-
- switch (pregunta.validacion) {
- case 'nombre':
- nuevasRespuestas[id] = cuentaInfo.nombre;
- break;
- case 'apellidos':
- nuevasRespuestas[id] = cuentaInfo.apellidos;
- break;
- case 'correo':
- nuevasRespuestas[id] = cuentaInfo.correo;
- break;
- case 'carrera':
- nuevasRespuestas[id] = cuentaInfo.carrera;
- break;
- case 'genero':
- const generoTexto = cuentaInfo.genero === 'M' ? 'Masculino' : 'Femenino';
- const opcionGenero = pregunta.opciones?.find(
- (op) => op.opcion.opcion.toLowerCase() === generoTexto.toLowerCase()
- );
- if (opcionGenero) {
- nuevasRespuestas[id] = String(opcionGenero.id_opcion);
- }
- break;
- }
- });
-
- setRespuestas(prev => ({ ...prev, ...nuevasRespuestas }));
- }, [cuentaInfo, data?.cuestionario.secciones, preguntas]);
-
- // Función para actualizar respuestas
- const actualizarRespuesta = (idPregunta: string, valor: string) => {
- setRespuestas(prev => ({ ...prev, [idPregunta]: valor }));
- };
-
- // Función para enviar formulario
const handleOnSubmit = async () => {
- setIsSending(true);
-
if (!data?.cuestionario.id_cuestionario) {
toast.error('No se ha cargado correctamente el formulario');
- setIsSending(false);
return;
}
- // Validar preguntas obligatorias
const preguntasObligatorias = data.cuestionario.secciones.flatMap((seccion) =>
seccion.preguntas
- .filter((pregunta) => pregunta.pregunta.obligatoria)
- .map((pregunta) => pregunta.pregunta)
+ .filter((p) => p.pregunta.obligatoria)
+ .map((p) => p.pregunta)
);
const faltantes = preguntasObligatorias.filter(
- (pregunta) =>
- respuestas[pregunta.id_pregunta] === undefined ||
- respuestas[pregunta.id_pregunta] === '' ||
- respuestas[pregunta.id_pregunta] === null
+ (p) =>
+ respuestas[p.id_pregunta] === undefined ||
+ respuestas[p.id_pregunta] === '' ||
+ respuestas[p.id_pregunta] === null
);
if (faltantes.length > 0) {
toast.error('Por favor responde todas las preguntas obligatorias.');
- setIsSending(false);
return;
}
- // Buscar correo
- const preguntaCorreo = preguntas?.find(p =>
- p.validacion === 'correo' || p.validacion === 'correo_institucional'
- );
-
- let correo = preguntaCorreo
- ? String(respuestas[preguntaCorreo.id_pregunta] || '')
- : '';
-
- if (!correo) {
- const correoRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
- for (const valor of Object.values(respuestas)) {
- if (typeof valor === 'string' && correoRegex.test(valor)) {
- correo = valor;
- break;
- }
- }
- }
-
- const payload = {
- id_cuestionario: id_cuestionario,
- correo: correo,
- fecha_envio: new Date().toISOString(),
- respuestas: Object.entries(respuestas).map(([id_pregunta, valor]) => ({
- id_pregunta: Number(id_pregunta),
- valor: isNaN(Number(valor)) ? valor : Number(valor),
- })),
- };
-
try {
- const res = await axiosInstance.post(
- '/cuestionario-respondido/submit',
- payload
- );
-
- toast.success(res.data.message || '¡Formulario enviado exitosamente!');
- handleSubmitFormulario(res.data);
- } catch (error) {
- console.error('Error al enviar formulario:', getAxiosError(error));
+ const res = await submitFormulario(data.cuestionario.id_cuestionario);
+ toast.success(res.message || '¡Formulario enviado exitosamente!');
+ handleSubmitFormulario(res);
+ } catch {
toast.error('Hubo un error al enviar el formulario');
- } finally {
- setIsSending(false);
}
};
- // Condición para mostrar el resto del formulario
- const mostrarFormularioCompleto =
- !preguntaComunidad || // Sin pregunta de comunidad
- (esComunidadSi && busquedaCompletada) || // Con comunidad "Sí" y búsqueda completada
- (!esComunidadSi && comunidadSeleccionada); // Con comunidad "No"
-
- if (error) {
- console.error('Error al cargar el cuestionario:', error);
+ if (errorData) {
return Error al cargar el cuestionario
;
}
- if (!data) {
+ if (loadingData || !data) {
return Cargando...
;
}
@@ -321,15 +97,15 @@ export default function FormularioRegistro({
value: op.id_opcion,
}))}
selectedValue={comunidadSeleccionada ? Number(comunidadSeleccionada) : undefined}
- onChange={(opt) => {
- actualizarRespuesta(preguntaComunidad.id_pregunta.toString(), String(opt.value));
- }}
+ onChange={(opt) =>
+ setRespuesta(preguntaComunidad.id_pregunta.toString(), String(opt.value))
+ }
/>
)}
- {/* Pregunta de cuenta (si respondió "Sí" a comunidad) */}
- {esComunidadSi && preguntaCuenta && (
+ {/* Pregunta de cuenta (si respondió "Sí" a comunidad, o en cuestionario exclusivo) */}
+ {(esComunidadSi || esExclusivo) && preguntaCuenta && (
{preguntaCuenta.pregunta}
@@ -341,107 +117,120 @@ export default function FormularioRegistro({
preguntaCuenta.validacion === 'cuenta_alumno'
? 'Ingrese su número de cuenta (9 dígitos)'
: preguntaCuenta.validacion === 'cuenta_trabajador'
- ? 'Ingrese su número de trabajador (6 dígitos)'
- : preguntaCuenta.validacion === 'rfc'
- ? 'Ingrese su RFC sin homoclave (10 caracteres)'
- : `Ingrese su ${preguntaCuenta.pregunta}`
+ ? 'Ingrese su número de trabajador (6 dígitos)'
+ : preguntaCuenta.validacion === 'rfc'
+ ? 'Ingrese su RFC sin homoclave (10 caracteres)'
+ : `Ingrese su ${preguntaCuenta.pregunta}`
+ }
+ maxLength={
+ preguntaCuenta.validacion === 'rfc'
+ ? 10
+ : preguntaCuenta.validacion === 'cuenta_alumno'
+ ? 9
+ : 6
}
- maxLength={preguntaCuenta.validacion === 'rfc' ? 10 :
- preguntaCuenta.validacion === 'cuenta_alumno' ? 9 : 6}
value={respuestas[preguntaCuenta.id_pregunta] || ''}
- onChange={(e) => actualizarRespuesta(
- preguntaCuenta.id_pregunta.toString(),
- e.target.value
- )}
+ onChange={(e) =>
+ setRespuesta(preguntaCuenta.id_pregunta.toString(), e.target.value)
+ }
/>
+ {loadingBusqueda && (
+
+
+ Buscando...
+
+ )}
+ {errorBusqueda && (
+
+
+ {errorBusqueda}
+
+ )}
)}
{/* Resto del formulario */}
- {mostrarFormularioCompleto && data.cuestionario.secciones.map((seccion, i) => (
-
- {seccion.preguntas.map((pregunta) => {
- // No renderizar preguntas ya manejadas
- if (
- pregunta.pregunta.id_pregunta === preguntaComunidad?.id_pregunta ||
- pregunta.pregunta.id_pregunta === preguntaCuenta?.id_pregunta
- ) {
+ {mostrarFormularioCompleto &&
+ data.cuestionario.secciones.map((seccion, i) => (
+
+ {seccion.preguntas.map((pregunta) => {
+ if (
+ pregunta.pregunta.id_pregunta === preguntaComunidad?.id_pregunta ||
+ pregunta.pregunta.id_pregunta === preguntaCuenta?.id_pregunta
+ ) {
+ return null;
+ }
+
+ const valor = respuestas[pregunta.pregunta.id_pregunta] || '';
+
+ if (pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Abierta (Respuesta corta)') {
+ return (
+
+
+ {pregunta.pregunta.pregunta}
+
+
+ setRespuesta(pregunta.pregunta.id_pregunta.toString(), e.target.value)
+ }
+ />
+
+ );
+ }
+
+ if (pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Abierta (Parrafo)') {
+ return (
+
+
+ {pregunta.pregunta.pregunta}
+
+
+ setRespuesta(pregunta.pregunta.id_pregunta.toString(), e.target.value)
+ }
+ />
+
+ );
+ }
+
+ if (
+ pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Cerrada' ||
+ pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Multiple'
+ ) {
+ const opciones: RadioOption
[] = pregunta.pregunta.opciones.map((op) => ({
+ label: op.opcion.opcion,
+ value: op.id_opcion,
+ }));
+
+ return (
+
+
+ {pregunta.pregunta.pregunta}
+
+
+ setRespuesta(pregunta.pregunta.id_pregunta.toString(), String(opt.value))
+ }
+ />
+
+ );
+ }
+
return null;
- }
-
- const valor = respuestas[pregunta.pregunta.id_pregunta] || '';
-
- // Pregunta abierta corta
- if (pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Abierta (Respuesta corta)') {
- return (
-
-
- {pregunta.pregunta.pregunta}
-
- actualizarRespuesta(
- pregunta.pregunta.id_pregunta.toString(),
- e.target.value
- )}
- />
-
- );
- }
-
- // Pregunta abierta párrafo
- if (pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Abierta (Parrafo)') {
- return (
-
-
- {pregunta.pregunta.pregunta}
-
- actualizarRespuesta(
- pregunta.pregunta.id_pregunta.toString(),
- e.target.value
- )}
- />
-
- );
- }
-
- // Pregunta cerrada
- if (pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Cerrada') {
- const opciones: RadioOption[] = pregunta.pregunta.opciones.map((op) => ({
- label: op.opcion.opcion,
- value: op.id_opcion,
- }));
-
- return (
-
-
- {pregunta.pregunta.pregunta}
-
- actualizarRespuesta(
- pregunta.pregunta.id_pregunta.toString(),
- String(opt.value)
- )}
- />
-
- );
- }
-
- return null;
- })}
-
- ))}
+ })}
+
+ ))}
{/* Botón de envío */}
{mostrarFormularioCompleto && (
@@ -456,16 +245,3 @@ export default function FormularioRegistro({
);
}
-
-/*
-curl -X 'POST' \
- 'https://venus.acatlan.unam.mx/registro_test/administrador' \
- -H 'accept: application/json' \
- -H 'Content-Type: application/json' \
- -d '{
- "nombre_usuario": "staffmike",
- "correo": "staffmike@ejemplo.com",
- "password": "password",
- "id_tipo_user": 2
-}'
-*/
\ No newline at end of file
diff --git a/src/containers/pages/formulario-page.tsx b/src/containers/pages/formulario-page.tsx
index f4edc86..6824406 100644
--- a/src/containers/pages/formulario-page.tsx
+++ b/src/containers/pages/formulario-page.tsx
@@ -4,10 +4,11 @@ import { useGetApi } from '@/hooks/use-get-api';
import Image from 'next/image';
import React, { useState, useEffect } from 'react';
import FormularioRegistro from '../formulario/formulario-registro';
+import FormularioExito from '../formulario/formulario-exito';
import { GetEventoWithCuestionario } from '@/types/evento';
import { formatearRangoFechas } from '@/utils/date-utils';
import { SubmitResponse } from '@/types/submit';
-import Link from 'next/link';
+import { FormularioProvider } from '@/context/formulario';
export default function FormularioPage({
id_evento,
@@ -64,121 +65,12 @@ export default function FormularioPage({
// Si hay una respuesta exitosa, mostrar la vista de éxito
if (response && response.success) {
return (
-
-
-
-
-
-
-
-
-
-
-
- ¡Felicidades!
-
-
- Has completado exitosamente el formulario
-
-
-
-
-
-
-
Evento
-
-
- {data?.nombre_evento}
-
-
-
-
-
-
-
Formulario
-
-
- {data?.cuestionario.nombre_form}
-
-
-
- {response.message && (
-
-
-
-
-
-
Mensaje:
-
{response.message}
-
-
-
-
- )}
-
-
-
-
-
- {qrImageUrl ? (
-
-
-
- Código QR
-
-
-
-
-
- ) : (
-
-
-
- No hay código QR disponible
-
-
- )}
-
-
-
-
-
-
-
-
- Ir al inicio
-
-
-
-
-
+
);
}
@@ -353,13 +245,18 @@ export default function FormularioPage({
diff --git a/src/context/formulario/formulario-context.tsx b/src/context/formulario/formulario-context.tsx
new file mode 100644
index 0000000..3c0e6da
--- /dev/null
+++ b/src/context/formulario/formulario-context.tsx
@@ -0,0 +1,330 @@
+'use client';
+import React, {
+ createContext,
+ useContext,
+ useReducer,
+ useEffect,
+ useRef,
+ useMemo,
+ useCallback,
+ ReactNode,
+} from 'react';
+import axiosInstance from '@/utils/api-config';
+import { getAxiosError } from '@/utils/errors-utils';
+import { GetCuestionario } from '@/types/evento';
+import { UsuarioDataResponse } from '@/containers/formulario/formulario-registro';
+import {
+ formularioReducer,
+ initialState,
+ FormularioState,
+ RespuestaFormulario,
+} from './formulario-reducer';
+
+// ── Longitudes requeridas por tipo de validación ─────────────────────────────
+
+const LONGITUDES: Record
= {
+ cuenta_alumno: 9,
+ cuenta_trabajador: 6,
+ rfc: 10,
+};
+
+// ── Tipos del contexto ────────────────────────────────────────────────────────
+
+interface FormularioContextType {
+ state: FormularioState;
+
+ // Datos derivados estables
+ preguntas: ReturnType;
+ preguntaComunidad: PreguntaFlat | undefined;
+ preguntaCuenta: PreguntaFlat | undefined;
+ esComunidadSi: boolean;
+ esExclusivo: boolean;
+ mostrarFormularioCompleto: boolean;
+
+ // Acciones
+ setRespuesta: (id: string, valor: string) => void;
+ submitFormulario: (id_cuestionario: number) => Promise;
+}
+
+// Helper para aplanar preguntas
+type PreguntaFlat = GetCuestionario['cuestionario']['secciones'][0]['preguntas'][0]['pregunta'];
+
+function getPreguntasFlat(data: GetCuestionario | null): PreguntaFlat[] {
+ if (!data) return [];
+ return data.cuestionario.secciones.flatMap((s) =>
+ s.preguntas.map((p) => p.pregunta)
+ );
+}
+
+// ── Context ───────────────────────────────────────────────────────────────────
+
+const FormularioContext = createContext(undefined);
+
+interface FormularioProviderProps {
+ children: ReactNode;
+ id_evento: number;
+ id_cuestionario: number;
+}
+
+export function FormularioProvider({
+ children,
+ id_evento,
+ id_cuestionario,
+}: FormularioProviderProps) {
+ const [state, dispatch] = useReducer(formularioReducer, initialState);
+ // Rastrea si ya se limpiaron los campos para el usuarioEncontrado actual
+ const yaLimpioRef = useRef(false);
+
+ // ── Carga del cuestionario ─────────────────────────────────────────────────
+
+ useEffect(() => {
+ dispatch({ type: 'FETCH_DATA_START' });
+ axiosInstance
+ .get(`/cuestionario/${id_cuestionario}/formulario`)
+ .then((res) => dispatch({ type: 'FETCH_DATA_SUCCESS', payload: res.data }))
+ .catch((err) => {
+ const { message } = getAxiosError(err);
+ dispatch({ type: 'FETCH_DATA_ERROR', payload: message });
+ });
+ }, [id_cuestionario]);
+
+ // ── Derivados estables ─────────────────────────────────────────────────────
+
+ const preguntas = useMemo(() => getPreguntasFlat(state.data), [state.data]);
+
+ const preguntaComunidad = useMemo(
+ () =>
+ preguntas.find(
+ (p) =>
+ p.validacion === 'comunidad_alumno' ||
+ p.validacion === 'comunidad_trabajador'
+ ),
+ [preguntas]
+ );
+
+ const preguntaCuenta = useMemo(
+ () =>
+ preguntas.find(
+ (p) =>
+ p.validacion === 'cuenta_alumno' ||
+ p.validacion === 'cuenta_trabajador' ||
+ p.validacion === 'rfc'
+ ),
+ [preguntas]
+ );
+
+ const comunidadSeleccionada = preguntaComunidad
+ ? state.respuestas[preguntaComunidad.id_pregunta]
+ : null;
+
+ const esComunidadSi = useMemo(
+ () =>
+ !!(
+ comunidadSeleccionada &&
+ preguntaComunidad?.opciones.find(
+ (op) =>
+ op.id_opcion === Number(comunidadSeleccionada) &&
+ op.opcion.opcion.toLowerCase().includes('si')
+ )
+ ),
+ [comunidadSeleccionada, preguntaComunidad]
+ );
+
+ const esExclusivo = useMemo(
+ () => !preguntaComunidad && !!preguntaCuenta,
+ [preguntaComunidad, preguntaCuenta]
+ );
+
+ const mostrarFormularioCompleto =
+ (esExclusivo && state.busquedaCompletada) ||
+ (!preguntaComunidad && !esExclusivo) ||
+ (esComunidadSi && state.busquedaCompletada) ||
+ (!!(esComunidadSi === false) && !!comunidadSeleccionada);
+
+ // ── Búsqueda de cuenta ─────────────────────────────────────────────────────
+
+ useEffect(() => {
+ const debeIniciarBusqueda = preguntaCuenta && (esExclusivo || esComunidadSi);
+
+ if (!debeIniciarBusqueda) {
+ if (state.cuentaBuscada || state.busquedaCompletada || state.errorBusqueda) {
+ dispatch({ type: 'BUSQUEDA_RESET' });
+ }
+ return;
+ }
+
+ const cuenta = state.respuestas[preguntaCuenta.id_pregunta];
+ if (!cuenta || typeof cuenta !== 'string') {
+ if (state.cuentaBuscada || state.busquedaCompletada || state.errorBusqueda) {
+ dispatch({ type: 'BUSQUEDA_RESET' });
+ }
+ return;
+ }
+
+ const longitudRequerida = LONGITUDES[preguntaCuenta.validacion ?? ''];
+ if (!longitudRequerida || cuenta.length !== longitudRequerida) {
+ if (state.cuentaBuscada) dispatch({ type: 'BUSQUEDA_RESET' });
+ return;
+ }
+
+ // No re-buscar la misma cuenta
+ if (cuenta === state.cuentaBuscada) return;
+
+ let endpoint =
+ preguntaCuenta.validacion === 'cuenta_alumno'
+ ? `/alumnos/${cuenta}`
+ : `/trabajadores/${cuenta}`;
+
+ if (esExclusivo) endpoint += `?id_evento=${id_evento}`;
+
+ dispatch({ type: 'BUSQUEDA_START', payload: cuenta });
+
+ axiosInstance
+ .get(endpoint)
+ .then((res) => {
+ const d = res.data;
+ if (d.nombre || d.apellidos || d.carrera || d.genero) {
+ dispatch({ type: 'BUSQUEDA_SUCCESS', payload: d });
+ } else {
+ dispatch({ type: 'BUSQUEDA_ERROR', payload: 'No se encontraron datos para este usuario' });
+ }
+ })
+ .catch((err) => {
+ const { message } = getAxiosError(err);
+ dispatch({ type: 'BUSQUEDA_ERROR', payload: message });
+ });
+ }, [
+ state.respuestas,
+ state.cuentaBuscada,
+ state.busquedaCompletada,
+ state.errorBusqueda,
+ preguntaCuenta,
+ esComunidadSi,
+ esExclusivo,
+ id_evento,
+ ]);
+
+ // ── Autorelleno cuando llega usuarioEncontrado ─────────────────────────────
+
+ useEffect(() => {
+ if (!state.data) return;
+
+ if (!state.usuarioEncontrado) {
+ // Solo limpiar si aún no lo hicimos (evita loop infinito)
+ if (!yaLimpioRef.current) return;
+ yaLimpioRef.current = false;
+ const idsCampos = preguntas
+ .filter((p) =>
+ ['nombre', 'apellidos', 'correo', 'carrera', 'genero'].includes(p.validacion ?? '')
+ )
+ .map((p) => p.id_pregunta);
+ if (idsCampos.length) {
+ dispatch({ type: 'LIMPIAR_CAMPOS_AUTORELLENO', payload: idsCampos });
+ }
+ return;
+ }
+
+ const u = state.usuarioEncontrado;
+ const bulk: RespuestaFormulario = {};
+
+ preguntas.forEach((pregunta) => {
+ const id = pregunta.id_pregunta;
+ switch (pregunta.validacion) {
+ case 'nombre':
+ bulk[id] = u.nombre ?? '';
+ break;
+ case 'apellidos':
+ bulk[id] = u.apellidos ?? '';
+ break;
+ case 'correo':
+ bulk[id] = u.cuenta ? `${u.cuenta}@pcpuma.acatlan.unam.mx` : '';
+ break;
+ case 'carrera':
+ bulk[id] = u.carrera ?? '';
+ break;
+ case 'genero': {
+ const texto = u.genero === 'M' ? 'Masculino' : 'Femenino';
+ const opcion = pregunta.opciones?.find(
+ (op) => op.opcion.opcion.toLowerCase() === texto.toLowerCase()
+ );
+ if (opcion) bulk[id] = String(opcion.id_opcion);
+ break;
+ }
+ }
+ });
+
+ if (Object.keys(bulk).length) {
+ yaLimpioRef.current = true;
+ dispatch({ type: 'SET_RESPUESTAS_BULK', payload: bulk });
+ }
+ }, [state.usuarioEncontrado, state.data, preguntas]);
+
+ // ── Acciones expuestas ─────────────────────────────────────────────────────
+
+ const setRespuesta = useCallback((id: string, valor: string) => {
+ dispatch({ type: 'SET_RESPUESTA', payload: { id, valor } });
+ }, []);
+
+ const submitFormulario = useCallback(
+ async (id_cuestionario: number) => {
+ dispatch({ type: 'SEND_START' });
+ try {
+ const preguntaCorreo = preguntas.find(
+ (p) => p.validacion === 'correo' || p.validacion === 'correo_institucional'
+ );
+ let correo = preguntaCorreo
+ ? String(state.respuestas[preguntaCorreo.id_pregunta] ?? '')
+ : '';
+ if (!correo) {
+ const rx = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ for (const v of Object.values(state.respuestas)) {
+ if (typeof v === 'string' && rx.test(v)) { correo = v; break; }
+ }
+ }
+
+ const res = await axiosInstance.post(
+ '/cuestionario-respondido/submit',
+ {
+ id_cuestionario,
+ correo,
+ fecha_envio: new Date().toISOString(),
+ respuestas: Object.entries(state.respuestas).map(([id, valor]) => ({
+ id_pregunta: Number(id),
+ valor: isNaN(Number(valor)) ? valor : Number(valor),
+ })),
+ }
+ );
+ return res.data;
+ } finally {
+ dispatch({ type: 'SEND_END' });
+ }
+ },
+ [state.respuestas, preguntas]
+ );
+
+ // ── Valor del contexto ─────────────────────────────────────────────────────
+
+ const value: FormularioContextType = {
+ state,
+ preguntas,
+ preguntaComunidad,
+ preguntaCuenta,
+ esComunidadSi,
+ esExclusivo,
+ mostrarFormularioCompleto,
+ setRespuesta,
+ submitFormulario,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useFormulario() {
+ const ctx = useContext(FormularioContext);
+ if (!ctx) throw new Error('useFormulario debe usarse dentro de FormularioProvider');
+ return ctx;
+}
diff --git a/src/context/formulario/formulario-reducer.ts b/src/context/formulario/formulario-reducer.ts
new file mode 100644
index 0000000..a3d192e
--- /dev/null
+++ b/src/context/formulario/formulario-reducer.ts
@@ -0,0 +1,152 @@
+import { GetCuestionario } from '@/types/evento';
+import { UsuarioDataResponse } from '@/containers/formulario/formulario-registro';
+
+export type RespuestaFormulario = Record;
+
+// ── Estado ──────────────────────────────────────────────────────────────────
+
+export interface FormularioState {
+ // Datos del cuestionario cargado desde la API
+ data: GetCuestionario | null;
+ loadingData: boolean;
+ errorData: string | null;
+
+ // Respuestas del usuario
+ respuestas: RespuestaFormulario;
+
+ // Búsqueda de cuenta/rfc
+ cuentaBuscada: string;
+ busquedaCompletada: boolean;
+ loadingBusqueda: boolean;
+ errorBusqueda: string | null;
+ usuarioEncontrado: UsuarioDataResponse | null;
+
+ // Envío
+ isSending: boolean;
+}
+
+export const initialState: FormularioState = {
+ data: null,
+ loadingData: true,
+ errorData: null,
+
+ respuestas: {},
+
+ cuentaBuscada: '',
+ busquedaCompletada: false,
+ loadingBusqueda: false,
+ errorBusqueda: null,
+ usuarioEncontrado: null,
+
+ isSending: false,
+};
+
+// ── Acciones ─────────────────────────────────────────────────────────────────
+
+export type FormularioAction =
+ // Carga del cuestionario
+ | { type: 'FETCH_DATA_START' }
+ | { type: 'FETCH_DATA_SUCCESS'; payload: GetCuestionario }
+ | { type: 'FETCH_DATA_ERROR'; payload: string }
+
+ // Respuestas
+ | { type: 'SET_RESPUESTA'; payload: { id: string; valor: string } }
+ | { type: 'SET_RESPUESTAS_BULK'; payload: RespuestaFormulario }
+ | { type: 'LIMPIAR_CAMPOS_AUTORELLENO'; payload: number[] } // ids de pregunta
+
+ // Búsqueda de cuenta
+ | { type: 'BUSQUEDA_START'; payload: string } // cuenta buscada
+ | { type: 'BUSQUEDA_SUCCESS'; payload: UsuarioDataResponse }
+ | { type: 'BUSQUEDA_ERROR'; payload: string }
+ | { type: 'BUSQUEDA_RESET' }
+
+ // Envío
+ | { type: 'SEND_START' }
+ | { type: 'SEND_END' };
+
+// ── Reducer ──────────────────────────────────────────────────────────────────
+
+export function formularioReducer(
+ state: FormularioState,
+ action: FormularioAction
+): FormularioState {
+ switch (action.type) {
+ // Carga del cuestionario
+ case 'FETCH_DATA_START':
+ return { ...state, loadingData: true, errorData: null };
+
+ case 'FETCH_DATA_SUCCESS':
+ return { ...state, loadingData: false, data: action.payload };
+
+ case 'FETCH_DATA_ERROR':
+ return { ...state, loadingData: false, errorData: action.payload };
+
+ // Respuestas
+ case 'SET_RESPUESTA':
+ return {
+ ...state,
+ respuestas: { ...state.respuestas, [action.payload.id]: action.payload.valor },
+ };
+
+ case 'SET_RESPUESTAS_BULK':
+ return {
+ ...state,
+ respuestas: { ...state.respuestas, ...action.payload },
+ };
+
+ case 'LIMPIAR_CAMPOS_AUTORELLENO': {
+ const limpio = { ...state.respuestas };
+ action.payload.forEach((id) => delete limpio[id]);
+ return { ...state, respuestas: limpio };
+ }
+
+ // Búsqueda
+ case 'BUSQUEDA_START':
+ return {
+ ...state,
+ loadingBusqueda: true,
+ errorBusqueda: null,
+ busquedaCompletada: false,
+ usuarioEncontrado: null,
+ cuentaBuscada: action.payload,
+ };
+
+ case 'BUSQUEDA_SUCCESS':
+ return {
+ ...state,
+ loadingBusqueda: false,
+ busquedaCompletada: true,
+ errorBusqueda: null,
+ usuarioEncontrado: action.payload,
+ };
+
+ case 'BUSQUEDA_ERROR':
+ return {
+ ...state,
+ loadingBusqueda: false,
+ busquedaCompletada: false,
+ errorBusqueda: action.payload,
+ usuarioEncontrado: null,
+ };
+
+ case 'BUSQUEDA_RESET':
+ return {
+ ...state,
+ cuentaBuscada: '',
+ busquedaCompletada: false,
+ loadingBusqueda: false,
+ errorBusqueda: null,
+ usuarioEncontrado: null,
+ };
+
+ // Envío
+ case 'SEND_START':
+ return { ...state, isSending: true };
+
+ case 'SEND_END':
+ return { ...state, isSending: false };
+
+ default:
+ return state;
+ }
+}
diff --git a/src/context/formulario/index.ts b/src/context/formulario/index.ts
new file mode 100644
index 0000000..c9859d7
--- /dev/null
+++ b/src/context/formulario/index.ts
@@ -0,0 +1,2 @@
+export { FormularioProvider, useFormulario } from './formulario-context';
+export type { FormularioState, FormularioAction, RespuestaFormulario } from './formulario-reducer';
diff --git a/src/data/plantillas.ts b/src/data/plantillas.ts
index d0991ba..813a3b6 100644
--- a/src/data/plantillas.ts
+++ b/src/data/plantillas.ts
@@ -1,7 +1,6 @@
import { FormularioCreacion } from '@/types/create-formulario';
interface Plantilla {
- imagen: string;
nombre: string;
id: string;
datos: FormularioCreacion;
@@ -9,8 +8,7 @@ interface Plantilla {
export const plantillasDisponibles: Plantilla[] = [
{
- imagen: '/plantilla_trabajadores.png',
- nombre: 'Registro Comunidad Trabajador',
+ nombre: 'Registro General (con prellenado para Trabajadores)',
id: 'registro-comunidad-estudiantil',
datos: {
nombre_form: 'Registro',
@@ -79,8 +77,7 @@ export const plantillasDisponibles: Plantilla[] = [
},
},
{
- imagen: '/plantilla_alumnos.png',
- nombre: 'Registro Comunidad Alumnos',
+ nombre: 'Registro General (con prellenado para Alumnos)',
id: 'registro-comunidad-alumnos',
datos: {
nombre_form: 'Registro',
@@ -155,7 +152,6 @@ export const plantillasDisponibles: Plantilla[] = [
},
},
{
- imagen: '/plantilla_general.png',
nombre: 'Registro General',
id: 'registro-general',
datos: {
@@ -209,9 +205,125 @@ export const plantillasDisponibles: Plantilla[] = [
],
},
},
+ {
+ nombre: 'Formulario Exclusivo Alumnos',
+ id: 'formulario-exclusivo-alumnos',
+ datos: {
+ nombre_form: 'Formulario Exclusivo Alumnos',
+ descripcion:
+ 'Este formulario está destinado exclusivamente para alumnos. Por favor, completa todos los campos obligatorios.',
+ fecha_inicio: '2025-08-01T08:00:00',
+ fecha_fin: '2025-08-10T23:59:59',
+ id_tipo_cuestionario: 1,
+ secciones: [
+ {
+ titulo: 'Información Personal',
+ descripcion: 'Proporciona tus datos personales.',
+ preguntas: [
+ {
+ titulo: 'Número de cuenta',
+ tipo: 'Abierta (Respuesta corta)',
+ obligatoria: true,
+ validacion: 'cuenta_alumno',
+ },
+ {
+ titulo: 'Correo electrónico',
+ tipo: 'Abierta (Respuesta corta)',
+ obligatoria: true,
+ validacion: 'correo',
+ },
+ {
+ titulo: 'Nombre(s)',
+ tipo: 'Abierta (Respuesta corta)',
+ obligatoria: true,
+ validacion: 'nombre',
+ },
+ {
+ titulo: 'Apellidos',
+ tipo: 'Abierta (Respuesta corta)',
+ obligatoria: true,
+ validacion: 'apellidos',
+ },
+ {
+ titulo: 'Carrera',
+ tipo: 'Abierta (Respuesta corta)',
+ validacion: 'carrera',
+ obligatoria: true,
+ },
+ {
+ titulo: 'Género',
+ tipo: 'Cerrada',
+ obligatoria: true,
+ validacion: 'genero',
+ opciones: [
+ { valor: 'Masculino' },
+ { valor: 'Femenino' },
+ { valor: 'Prefiero no decirlo' },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ },
+ {
+ nombre: 'Formulario Exclusivo Profesores',
+ id: 'formulario-exclusivo-profesores',
+ datos: {
+ nombre_form: 'Formulario Exclusivo Profesores',
+ descripcion:
+ 'Este formulario está destinado exclusivamente para profesores. Por favor, completa todos los campos obligatorios.',
+ fecha_inicio: '2025-08-01T08:00:00',
+ fecha_fin: '2025-08-10T23:59:59',
+ id_tipo_cuestionario: 2,
+ secciones: [
+ {
+ titulo: 'Información Personal',
+ descripcion: 'Proporciona tus datos personales.',
+ preguntas: [
+ {
+ titulo: 'RFC',
+ tipo: 'Abierta (Respuesta corta)',
+ obligatoria: true,
+ validacion: 'rfc',
+ },
+ {
+ titulo: 'Correo electrónico',
+ tipo: 'Abierta (Respuesta corta)',
+ obligatoria: true,
+ validacion: 'correo',
+ },
+ {
+ titulo: 'Nombre(s)',
+ tipo: 'Abierta (Respuesta corta)',
+ obligatoria: true,
+ validacion: 'nombre',
+ },
+ {
+ titulo: 'Apellidos',
+ tipo: 'Abierta (Respuesta corta)',
+ obligatoria: true,
+ validacion: 'apellidos',
+ },
+ {
+ titulo: 'Género',
+ tipo: 'Cerrada',
+ obligatoria: true,
+ validacion: 'genero',
+ opciones: [
+ { valor: 'Masculino' },
+ { valor: 'Femenino' },
+ { valor: 'Prefiero no decirlo' },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ },
];
-/*
+/*
¡Bienvenidas y bienvenidos a la Facultad!
Inicia esta nueva etapa con un recorrido especial por nuestras instalaciones, pensado para que conozcas tu nueva casa universitaria. Te acompañaremos por aulas, laboratorios, bibliotecas, áreas comunes y servicios que estarán a tu disposición durante tu formación. Además, podrás resolver dudas y conocer a quienes serán parte de tu camino académico.
¡No faltes, esta actividad marcará el inicio de grandes experiencias!
diff --git a/src/types/create-formulario.d.ts b/src/types/create-formulario.d.ts
index b835e7e..db49066 100644
--- a/src/types/create-formulario.d.ts
+++ b/src/types/create-formulario.d.ts
@@ -29,6 +29,7 @@ export interface FormularioCreacion {
id_tipo_cuestionario: number;
id_tipo_evento?: number;
cupo_maximo?: number; // Opcional, si no se requiere un cupo máximo
+ mensaje_correo?: string;
secciones: SeccionFormulario[];
}
diff --git a/src/types/cuestionario.d.ts b/src/types/cuestionario.d.ts
index 219a761..11e13da 100644
--- a/src/types/cuestionario.d.ts
+++ b/src/types/cuestionario.d.ts
@@ -15,6 +15,7 @@ export interface GetCuestionario {
fecha_fin: string; // Formato ISO: 'YYYY-MM-DDTHH:mm:ss'
fecha_inicio: string; // Formato ISO: 'YYYY-MM-DDTHH:mm:ss'
id_tipo_cuestionario: number;
+ mensaje_correo?: string | null;
}
export interface GetCuestionarioWithEvento extends GetCuestionario {