From a0e86b3873b4b6ca314c33c0c3b798b333a3783b Mon Sep 17 00:00:00 2001 From: jorgemike Date: Sun, 6 Apr 2025 16:04:56 -0600 Subject: [PATCH] auto complete alumnos --- .../formulario/[id_formulario]/layout.tsx | 10 +- src/app/(public)/page.tsx | 37 ++- .../registro/[id_formulario]/page.tsx | 306 ------------------ src/app/(public)/registro/[slug]/page.tsx | 80 +++++ src/containers/formulario.tsx | 299 +++++++++++++++++ src/data/plantillas.ts | 2 +- src/utils/helpers.ts | 57 ++++ 7 files changed, 468 insertions(+), 323 deletions(-) delete mode 100644 src/app/(public)/registro/[id_formulario]/page.tsx create mode 100644 src/app/(public)/registro/[slug]/page.tsx create mode 100644 src/containers/formulario.tsx create mode 100644 src/utils/helpers.ts diff --git a/src/app/(admins)/administrador/formulario/[id_formulario]/layout.tsx b/src/app/(admins)/administrador/formulario/[id_formulario]/layout.tsx index 37562b8..4b2545e 100644 --- a/src/app/(admins)/administrador/formulario/[id_formulario]/layout.tsx +++ b/src/app/(admins)/administrador/formulario/[id_formulario]/layout.tsx @@ -1,19 +1,23 @@ +'use client'; import Link from 'next/link'; +import { useParams } from 'next/navigation'; import React from 'react'; export default function Layout({ children }: { children: React.ReactNode }) { + const params = useParams<{ id_formulario: string }>(); + return (
{children}
diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx index 6157155..16d4128 100644 --- a/src/app/(public)/page.tsx +++ b/src/app/(public)/page.tsx @@ -36,6 +36,17 @@ export default async function Page() { return
Error al cargar los eventos
; } + function slugify(text: string): string { + return text + .toString() + .normalize('NFD') // Quita tildes + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, '-') // Reemplaza cualquier cosa que no sea alfanumérica por guiones + .replace(/^-+|-+$/g, ''); // Quita guiones al inicio o final + } + return ( <>
@@ -44,19 +55,19 @@ export default async function Page() {
- {eventos.map((cuestionario) => ( - - ))} - {eventos.length === 0 && ( -

- No hay formularios disponibles. -

- )} + {eventos.map((cuestionario) => { + const slug = slugify(cuestionario.nombre_form); // Asumiendo que el nombre del evento está en `nombre` + const url = `/registro/${slug}-${cuestionario.id_cuestionario}`; + + return ( + + ); + })}
diff --git a/src/app/(public)/registro/[id_formulario]/page.tsx b/src/app/(public)/registro/[id_formulario]/page.tsx deleted file mode 100644 index 2592fea..0000000 --- a/src/app/(public)/registro/[id_formulario]/page.tsx +++ /dev/null @@ -1,306 +0,0 @@ -'use client'; -import Button from '@/components/button'; -import CheckboxOptionGroup from '@/components/checkbox-option-group'; -import Input from '@/components/input'; -import RadioOptionGroup from '@/components/radio-option-group'; -import { CuestionarioResponse } from '@/types/responder-formulario'; -import axiosInstance from '@/utils/api-config'; -import { validarRespuesta } from '@/utils/validador'; -import Image from 'next/image'; -import { useParams, useRouter } from 'next/navigation'; -import React, { useEffect, useState } from 'react'; -import toast from 'react-hot-toast'; - -export default function Page() { - const router = useRouter(); - const params = useParams<{ id_formulario: string }>(); - const id_formulario = params?.id_formulario; - const [cuestionario, setCuestionario] = useState( - null - ); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [respuestas, setRespuestas] = useState< - Record> - >({}); - const [isSubmitting, setIsSubmitting] = useState(false); // New state for submission - - useEffect(() => { - const fetchData = async () => { - try { - setLoading(true); - const { data } = await axiosInstance.get( - `/cuestionario/${id_formulario}/formulario` - ); - if (data) { - console.log(data); - setCuestionario(data); - } else { - setError('Error fetching data'); - } - } catch (error) { - console.error('Error fetching data:', error); - setError('Error de conexión'); - } finally { - setLoading(false); - } - }; - - if (id_formulario) fetchData(); - }, [id_formulario]); - - const handleInputChange = (id: number, value: string) => { - setRespuestas((prev) => ({ ...prev, [id]: value })); - }; - - const handleRadioChange = (id: number, opcion: { value: number }) => { - setRespuestas((prev) => ({ ...prev, [id]: opcion.value })); - }; - - const handleCheckboxChange = (id: number, opcion: { value: number }) => { - setRespuestas((prev) => { - const actuales: number[] = Array.isArray(prev[id]) - ? (prev[id] as number[]) - : []; - const existe = actuales.includes(opcion.value); - const nuevos = existe - ? actuales.filter((v: number) => v !== opcion.value) - : [...actuales, opcion.value]; - return { ...prev, [id]: nuevos }; - }); - }; - - const formatearRespuestas = async () => { - if (!validarRespuestasObligatorias()) return; - - const respuestasArray = Object.entries(respuestas).flatMap( - ([id, valor]) => { - if (Array.isArray(valor)) { - return valor.map((v) => ({ - id_pregunta: Number(id), - valor: v, - })); - } - return [{ id_pregunta: Number(id), valor }]; - } - ); - - const correoEntry = respuestasArray.find( - (r) => - typeof r.valor === 'string' && - /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(r.valor) - ); - - const resultado = { - id_formulario: Number(id_formulario), - correo: correoEntry?.valor || 'correo@no-encontrado.com', - respuestas: respuestasArray, - fecha_envio: new Date().toISOString(), - }; - - setIsSubmitting(true); // Set submitting state to true - toast - .promise( - axiosInstance.post('/cuestionario-respondido/submit', resultado), - { - loading: 'Enviando formulario...', - success: 'Formulario enviado con éxito!', - error: 'Error al enviar el formulario.', - } - ) - .then(() => { - router.push('/'); - }) - .catch((error) => { - console.error('Error al enviar el formulario:', error); - }) - .finally(() => { - setIsSubmitting(false); // Reset submitting state - }); - }; - - const validarRespuestasObligatorias = (): boolean => { - const faltantes: number[] = []; - const errores: { id: number; mensaje: string }[] = []; - - cuestionario?.cuestionario.secciones.forEach((seccion) => { - seccion.preguntas.forEach(({ pregunta }) => { - const id = pregunta.id_pregunta; - const valor = respuestas[id]; - const tipo = pregunta.tipo_pregunta.tipo_pregunta; - const validacion = pregunta.validacion; - - const respondida = - (tipo === 'Abierta' && - typeof valor === 'string' && - valor.trim() !== '') || - (tipo === 'Cerrada' && valor !== undefined) || - (tipo === 'Multiple' && Array.isArray(valor) && valor.length > 0); - - if (!respondida) { - if (pregunta.obligatoria) { - faltantes.push(id); - } - return; // no hay nada más que validar si está vacía - } - - if (tipo === 'Abierta' && validacion) { - const resultado = validarRespuesta(valor as string, validacion); - if (!resultado.valido) { - errores.push({ id, mensaje: resultado.mensaje }); - } - } - }); - }); - - if (faltantes.length > 0) { - toast.error('Responde todas las preguntas obligatorias antes de enviar.'); - return false; - } - - if (errores.length > 0) { - toast.error( - `Corrige las respuestas con error de formato:\n${errores - .map((e) => `• ${e.mensaje}`) - .join('\n')}` - ); - return false; - } - - return true; - }; - - if (loading) return

Cargando...

; - if (error) return

{error}

; - if (!cuestionario) return

No hay datos

; - - return ( -
- -
- Ejemplo de banner -
-
-

{cuestionario.cuestionario.nombre_form}

-

{cuestionario.cuestionario.descripcion}

- - {cuestionario.cuestionario.secciones.map((seccion, index) => ( -
-

{seccion.seccion.titulo}

-

{seccion.seccion.descripcion}

- - {seccion.preguntas.map((p) => { - const pregunta = p.pregunta; - const id = pregunta.id_pregunta; - const tipo = pregunta.tipo_pregunta.tipo_pregunta; - - if (tipo === 'Abierta') { - return ( - handleInputChange(id, e.target.value)} - required={pregunta.obligatoria} - placeholder={ - pregunta.validacion === 'correo' - ? 'ejemplo@correo.com' - : pregunta.validacion === 'cuenta_alumno' - ? '123456789' - : undefined - } - type={ - pregunta.validacion === 'correo' - ? 'email' - : pregunta.validacion === 'cuenta_alumno' - ? 'number' - : 'text' - } - /> - ); - } - - if (tipo === 'Cerrada' || tipo === 'Multiple') { - const opciones = (pregunta.opciones ?? []).map((o) => ({ - label: o.opcion.opcion, - value: o.opcion.id_opcion, - })); - - if (tipo === 'Multiple') { - return ( -
-

{pregunta.pregunta}

- { - if (typeof opcion.value === 'number') { - handleCheckboxChange(id, { value: opcion.value }); - } else { - console.error('Invalid value type:', opcion.value); - } - }} - /> -
- ); - } - - return ( -
-

{pregunta.pregunta}

- { - if (typeof opcion.value === 'number') { - handleRadioChange(id, { value: opcion.value }); - } else { - console.error('Invalid value type:', opcion.value); - } - }} - disabled={false} - /> -
- ); - } - - return null; - })} -
- ))} - - -
-
- ); -} diff --git a/src/app/(public)/registro/[slug]/page.tsx b/src/app/(public)/registro/[slug]/page.tsx new file mode 100644 index 0000000..7d7d660 --- /dev/null +++ b/src/app/(public)/registro/[slug]/page.tsx @@ -0,0 +1,80 @@ +'use client'; +import Button from '@/components/button'; +import Formulario from '@/containers/formulario'; +import { CuestionarioResponse } from '@/types/responder-formulario'; +import axiosInstance from '@/utils/api-config'; +import Image from 'next/image'; +import { useParams, useRouter } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; + +export default function Page() { + const router = useRouter(); + const params = useParams<{ slug: string }>(); + const slug = params?.slug; + + const id_formulario = slug?.split('-').pop(); // Toma el último fragmento como ID + const [cuestionario, setCuestionario] = useState( + null + ); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + const { data } = await axiosInstance.get( + `/cuestionario/${id_formulario}/formulario` + ); + if (data) { + console.log(data); + setCuestionario(data); + } else { + setError('Error fetching data'); + } + } catch (error) { + console.error('Error fetching data:', error); + setError('Error de conexión'); + } finally { + setLoading(false); + } + }; + + if (id_formulario) fetchData(); + }, [id_formulario]); + + if (loading) return

Cargando...

; + if (error) return

{error}

; + if (!cuestionario) return

No hay datos

; + + return ( +
+ +
+ Ejemplo de banner +
+
+

{cuestionario.cuestionario.nombre_form}

+

{cuestionario.cuestionario.descripcion}

+ + +
+
+ ); +} diff --git a/src/containers/formulario.tsx b/src/containers/formulario.tsx new file mode 100644 index 0000000..00fdc75 --- /dev/null +++ b/src/containers/formulario.tsx @@ -0,0 +1,299 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { CuestionarioSeccion } from '@/types/responder-formulario'; +import RadioOptionGroup, { RadioOption } from '@/components/radio-option-group'; +import Input from '@/components/input'; +import Button from '@/components/button'; +import { validarRespuesta } from '@/utils/validador'; +import toast from 'react-hot-toast'; +import axiosInstance from '@/utils/api-config'; +import { useRouter } from 'next/navigation'; + +export default function Formulario({ + id_formulario, + secciones, +}: { + id_formulario: number; + secciones: CuestionarioSeccion[]; +}) { + const router = useRouter(); + const [esDeFES, setEsDeFES] = useState(null); + const [cuenta, setCuenta] = useState(''); + const [datosAuto, setDatosAuto] = useState(null); + const [respuestas, setRespuestas] = useState>({}); + const [isSubmitting, setIsSubmitting] = useState(false); // New state for submission + + const actualizarRespuesta = (idPregunta: string, valor: string) => { + setRespuestas((prev) => ({ ...prev, [idPregunta]: valor })); + }; + + useEffect(() => { + if (esDeFES && cuenta.length === 9) { + fetch(`/datos/alumnos/${cuenta}`) + .then((res) => res.json()) + .then((data) => setDatosAuto(data)) + .catch((err) => { + console.error('Error al obtener datos del alumno:', err); + setDatosAuto(null); + }); + } + }, [cuenta, esDeFES]); + + // Encuentra preguntas por texto o validación + const todasPreguntas = secciones.flatMap((s) => + s.preguntas.map((p) => p.pregunta) + ); + const preguntaFES = todasPreguntas.find( + (p) => p.pregunta === '¿Eres parte de la comunidad de la FES Acatlán?' + ); + const preguntaCuenta = todasPreguntas.find( + (p) => p.validacion === 'cuenta_alumno' + ); + const preguntasRestantes = todasPreguntas.filter( + (p) => + ![preguntaFES?.id_pregunta, preguntaCuenta?.id_pregunta].includes( + p.id_pregunta + ) + ); + const respuestaFES = respuestas[`pregunta_${preguntaFES?.id_pregunta}`]; + + const formatearRespuestas = async () => { + if (!validarRespuestasObligatorias()) return; + + const respuestasArray = Object.entries(respuestas).flatMap( + ([key, valor]) => { + const idNumerico = Number(key.replace('pregunta_', '')); + const pregunta = secciones + .flatMap((s) => s.preguntas) + .find((p) => p.pregunta.id_pregunta === idNumerico); + + const tipo = pregunta?.pregunta.tipo_pregunta.tipo_pregunta; + + // Si es múltiple, convertir cada valor individual si corresponde + if (Array.isArray(valor)) { + return valor.map((v) => ({ + id_pregunta: idNumerico, + valor: tipo === 'Cerrada' || tipo === 'Multiple' ? Number(v) : v, + })); + } + + return [ + { + id_pregunta: idNumerico, + valor: tipo === 'Cerrada' ? Number(valor) : valor, + }, + ]; + } + ); + + const correoEntry = respuestasArray.find( + (r) => + typeof r.valor === 'string' && + /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(r.valor) + ); + + const resultado = { + id_formulario: Number(id_formulario), + correo: correoEntry?.valor || 'correo@no-encontrado.com', + respuestas: respuestasArray, + fecha_envio: new Date().toISOString(), + }; + + console.log('Resultado a enviar:', resultado); + + setIsSubmitting(true); + toast + .promise( + axiosInstance.post('/cuestionario-respondido/submit', resultado), + { + loading: 'Enviando formulario...', + success: 'Formulario enviado con éxito!', + error: 'Error al enviar el formulario.', + } + ) + .then(() => router.push('/')) + .finally(() => setIsSubmitting(false)); + }; + + const validarRespuestasObligatorias = (): boolean => { + const faltantes: number[] = []; + const errores: { id: number; mensaje: string }[] = []; + console.log('Validando respuestas:', respuestas); + + secciones.forEach((seccion) => { + seccion.preguntas.forEach(({ pregunta }) => { + const id = pregunta.id_pregunta; + const valor = respuestas[`pregunta_${id}`]; + const tipo = pregunta.tipo_pregunta.tipo_pregunta; + const validacion = pregunta.validacion; + + const respondida = + (tipo === 'Abierta' && + typeof valor === 'string' && + valor.trim() !== '') || + (tipo === 'Cerrada' && valor !== undefined) || + (tipo === 'Multiple' && Array.isArray(valor) && valor.length > 0); + + if (!respondida && pregunta.obligatoria) { + faltantes.push(id); + } + + if (tipo === 'Abierta' && validacion && typeof valor === 'string') { + const resultado = validarRespuesta(valor, validacion); + if (!resultado.valido) { + errores.push({ id, mensaje: resultado.mensaje }); + } + } + }); + }); + if (faltantes.length > 0) { + console.log('Faltan preguntas obligatorias:', faltantes); + toast.error('Responde todas las preguntas obligatorias antes de enviar.'); + return false; + } + + if (errores.length > 0) { + toast.error( + `Corrige las respuestas con error de formato:\n${errores + .map((e) => `• ${e.mensaje}`) + .join('\n')}` + ); + return false; + } + + return true; + }; + + return ( +
+ {/* Pregunta inicial */} + {preguntaFES && ( +
+ + ({ + label: op.opcion.opcion, + value: op.id_opcion, + }))} + selectedValue={respuestaFES ? Number(respuestaFES) : undefined} + onChange={(opt) => { + const idSeleccionado = opt.value; + const opcionSeleccionada = preguntaFES.opciones.find( + (o) => o.id_opcion === idSeleccionado + ); + + const valorTexto = opcionSeleccionada?.opcion.opcion ?? ''; + setEsDeFES(valorTexto === 'Si'); // Se actualiza esDeFES con el texto + setCuenta(''); + setDatosAuto(null); + + actualizarRespuesta( + `pregunta_${preguntaFES.id_pregunta}`, + String(idSeleccionado) + ); + }} + /> +
+ )} + + {/* Si dijo que sí, pedir número de cuenta */} + {esDeFES && preguntaCuenta && ( + { + const value = e.target.value; + setCuenta(value); + actualizarRespuesta( + `pregunta_${preguntaCuenta.id_pregunta}`, + value + ); // ✅ Guarda la respuesta + }} + /> + )} + + {/* Si ya tiene datos o dijo que no es de la FES, mostrar el resto */} + {(esDeFES === false || (esDeFES && datosAuto)) && + preguntasRestantes.map((p) => { + if (p.tipo_pregunta.tipo_pregunta === 'Abierta') { + let defaultValue = ''; + let disabled = false; + + if (esDeFES && datosAuto) { + if (p.validacion === 'nombre') { + defaultValue = p.pregunta.toLowerCase().includes('apellido') + ? datosAuto.apellidos + : datosAuto.nombre; + disabled = true; + } + if (p.validacion === 'correo') { + defaultValue = datosAuto.correo; + disabled = true; + } + } + + return ( + + actualizarRespuesta( + `pregunta_${p.id_pregunta}`, + e.target.value + ) + } + /> + ); + } + + if (p.tipo_pregunta.tipo_pregunta === 'Cerrada') { + const opciones: RadioOption[] = p.opciones.map( + (op) => ({ + label: op.opcion.opcion, + value: op.id_opcion, + }) + ); + + const respuestaActual = respuestas[`pregunta_${p.id_pregunta}`]; + + return ( +
+ + + actualizarRespuesta( + `pregunta_${p.id_pregunta}`, + String(opt.value) + ) + } + /> +
+ ); + } + + return null; + })} + + {/* Botón de enviar */} + + + ); +} diff --git a/src/data/plantillas.ts b/src/data/plantillas.ts index 1a0d771..dcbb65f 100644 --- a/src/data/plantillas.ts +++ b/src/data/plantillas.ts @@ -42,7 +42,7 @@ export const plantillasDisponibles: Plantilla[] = [ { titulo: 'Numero de cuenta', tipo: 'Abierta', - obligatoria: true, + obligatoria: false, limite: 250, validacion: 'cuenta_alumno', }, diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts new file mode 100644 index 0000000..b541f63 --- /dev/null +++ b/src/utils/helpers.ts @@ -0,0 +1,57 @@ +import { CuestionarioResponse } from "@/types/responder-formulario"; +import toast from "react-hot-toast"; +import { validarRespuesta } from "./validador"; + +export const validarRespuestasObligatorias = ( + cuestionario: CuestionarioResponse | null, + respuestas: Record> + ): boolean => { + const faltantes: number[] = []; + const errores: { id: number; mensaje: string }[] = []; + + cuestionario?.cuestionario.secciones.forEach((seccion) => { + seccion.preguntas.forEach(({ pregunta }) => { + const id = pregunta.id_pregunta; + const valor = respuestas[id]; + const tipo = pregunta.tipo_pregunta.tipo_pregunta; + const validacion = pregunta.validacion; + + const respondida = + (tipo === 'Abierta' && + typeof valor === 'string' && + valor.trim() !== '') || + (tipo === 'Cerrada' && valor !== undefined) || + (tipo === 'Multiple' && Array.isArray(valor) && valor.length > 0); + + if (!respondida) { + if (pregunta.obligatoria) { + faltantes.push(id); + } + return; // no hay nada más que validar si está vacía + } + + if (tipo === 'Abierta' && validacion) { + const resultado = validarRespuesta(valor as string, validacion); + if (!resultado.valido) { + errores.push({ id, mensaje: resultado.mensaje }); + } + } + }); + }); + + if (faltantes.length > 0) { + toast.error('Responde todas las preguntas obligatorias antes de enviar.'); + return false; + } + + if (errores.length > 0) { + toast.error( + `Corrige las respuestas con error de formato:\n${errores + .map((e) => `• ${e.mensaje}`) + .join('\n')}` + ); + return false; + } + + return true; + }; \ No newline at end of file