feat: implement formulario de registro with community and account validation, including dynamic response handling and submission logic
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
'use client'
|
||||
import dynamic from 'next/dynamic';
|
||||
import Footer from '@/components/layout/footer';
|
||||
import Header from '@/components/layout/header';
|
||||
import Navbar from '@/components/navbar';
|
||||
const Navbar = dynamic(() => import('@/components/navbar'), { ssr: false });
|
||||
import React from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
@@ -10,34 +10,30 @@ import toast from 'react-hot-toast';
|
||||
const Modal = dynamic(() => import('@/components/modal'), { ssr: false });
|
||||
|
||||
export default function Page() {
|
||||
const [scannedData, setScannedData] = useState<{
|
||||
id_participante: number;
|
||||
id_cuestionario: number;
|
||||
} | null>(null);
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [enableScan, setEnableScan] = useState(true);
|
||||
const [statusMessage, setStatusMessage] = useState('');
|
||||
|
||||
const [participante, setParticipante] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [token, setToken] = useState('');
|
||||
const [tokenValid, setTokenValid] = useState(false);
|
||||
|
||||
const handleScan = async (rawValue: string) => {
|
||||
console.log('QR Escaneado:', rawValue);
|
||||
if (!enableScan) return;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(rawValue);
|
||||
if (data.id_participante && data.id_cuestionario) {
|
||||
if (rawValue) {
|
||||
try {
|
||||
const response = await axiosInstance.get(
|
||||
`/participante-evento/${data.id_participante}/${data.id_cuestionario}`
|
||||
await axiosInstance.post(
|
||||
`/participante-evento/decode-token`, {
|
||||
token: rawValue
|
||||
}
|
||||
);
|
||||
|
||||
setParticipante(response.data.participante.correo);
|
||||
setScannedData({
|
||||
id_participante: data.id_participante,
|
||||
id_cuestionario: data.id_cuestionario,
|
||||
});
|
||||
setToken(rawValue);
|
||||
setTokenValid(true);
|
||||
setShowModal(true);
|
||||
setEnableScan(false);
|
||||
} catch (err) {
|
||||
@@ -56,13 +52,17 @@ export default function Page() {
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!scannedData) return;
|
||||
if (!tokenValid) return;
|
||||
|
||||
try {
|
||||
const response = await axiosInstance.post(
|
||||
`/participante-evento/asistencia/${scannedData.id_participante}/${scannedData.id_cuestionario}`
|
||||
`/participante-evento/asistencia`, {
|
||||
token: token
|
||||
}
|
||||
);
|
||||
|
||||
setParticipante(response.data.data.participante);
|
||||
setMessage(response.data.message);
|
||||
console.log('Asistencia registrada:', response.data);
|
||||
toast.success('✅ Asistencia registrada correctamente');
|
||||
} catch (err) {
|
||||
@@ -72,7 +72,6 @@ export default function Page() {
|
||||
|
||||
setShowModal(false);
|
||||
setEnableScan(true);
|
||||
setScannedData(null);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -124,6 +123,24 @@ export default function Page() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{participante && (
|
||||
<div className="mb-4">
|
||||
<div className="alert alert-success border-0 rounded-3 d-flex align-items-center">
|
||||
<i className="bi bi-person-check-fill me-2"></i>
|
||||
<span>Ultimo participante registrado: {participante}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message && (
|
||||
<div className="mb-4">
|
||||
<div className="alert alert-info border-0 rounded-3 d-flex align-items-center">
|
||||
<i className="bi bi-info-circle-fill me-2"></i>
|
||||
<span>{message}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => setEnableScan(false)}
|
||||
variant="outline-danger"
|
||||
@@ -195,7 +212,7 @@ export default function Page() {
|
||||
onClose={() => {
|
||||
setShowModal(false);
|
||||
setEnableScan(true);
|
||||
setScannedData(null);
|
||||
setTokenValid(false);
|
||||
}}
|
||||
closeButton
|
||||
className={{
|
||||
@@ -211,31 +228,8 @@ export default function Page() {
|
||||
</div>
|
||||
|
||||
<div className="modal-body p-4">
|
||||
{scannedData ? (
|
||||
{tokenValid ? (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<div className="mb-3">
|
||||
<i
|
||||
className="bi bi-person-check-fill text-success"
|
||||
style={{ fontSize: '3rem' }}
|
||||
></i>
|
||||
</div>
|
||||
<h5 className="mb-3 fw-semibold">
|
||||
Información del participante
|
||||
</h5>
|
||||
<div className="card bg-light border-0 rounded-3">
|
||||
<div className="card-body py-3">
|
||||
<div className="d-flex align-items-center justify-content-center">
|
||||
<i className="bi bi-envelope-fill text-primary me-2"></i>
|
||||
<strong className="me-2">Correo:</strong>
|
||||
<span className="text-primary fw-medium">
|
||||
{participante}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-grid gap-2 d-md-flex justify-content-center">
|
||||
<Button
|
||||
variant="success"
|
||||
@@ -250,7 +244,8 @@ export default function Page() {
|
||||
onClick={() => {
|
||||
setShowModal(false);
|
||||
setEnableScan(true);
|
||||
setScannedData(null);
|
||||
setTokenValid(false);
|
||||
setToken('');
|
||||
}}
|
||||
className="px-4 py-2 rounded-pill"
|
||||
>
|
||||
|
||||
@@ -34,13 +34,15 @@ export default function LoginForm() {
|
||||
password: loginData.password,
|
||||
});
|
||||
|
||||
console.log(response)
|
||||
|
||||
// Guardar el token en sessionStorage
|
||||
if (response.data.token) {
|
||||
sessionStorage.setItem('token', response.data.token);
|
||||
|
||||
// Opcional: guardar información adicional del usuario si viene en la respuesta
|
||||
if (response.data.user) {
|
||||
sessionStorage.setItem('user', JSON.stringify(response.data.user));
|
||||
if (response.data.tipo_usuario) {
|
||||
sessionStorage.setItem('axxxd', JSON.stringify(response.data.tipo_usuario));
|
||||
}
|
||||
|
||||
// Redireccionar al dashboard
|
||||
|
||||
@@ -5,16 +5,25 @@ import { usePathname } from 'next/navigation';
|
||||
|
||||
const Navbar: React.FC = () => {
|
||||
const pathname = usePathname();
|
||||
const tipo_usuario = sessionStorage.getItem('axxxd')?.replace(/"/g, '') || '';
|
||||
|
||||
const navItems = [
|
||||
const navItemsAdmin = [
|
||||
{ href: '/', label: 'Inicio' },
|
||||
{ href: '/user/eventos', label: 'Eventos' },
|
||||
{ href: '/user/qr', label: 'Lector de QRs' },
|
||||
{ href: '/user/eventos/crear', label: 'Crear Evento' },
|
||||
{ href: '/user/usuarios', label: 'Usuarios' },
|
||||
{ href: '/signout', label: 'Cerrar sesión' },
|
||||
{ href: '/login', label: 'Cerrar sesión' },
|
||||
];
|
||||
|
||||
const navItemsUser = [
|
||||
{ href: '/user/eventos', label: 'Eventos' },
|
||||
{ href: '/user/qr', label: 'Lector de QRs' },
|
||||
{ href: '/login', label: 'Cerrar sesión' },
|
||||
];
|
||||
|
||||
const navItems = tipo_usuario === "administrador" ? navItemsAdmin : navItemsUser;
|
||||
|
||||
return (
|
||||
<nav className="navbar navbar-expand-lg navbar-dark bg-azul">
|
||||
<div className="container">
|
||||
|
||||
@@ -31,6 +31,8 @@ export default function EditEvento({
|
||||
handleChange,
|
||||
handleOnChange,
|
||||
}: EditEventoProps) {
|
||||
const tipo_usuario = sessionStorage.getItem('axxxd')?.replace(/"/g, '') || '';
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [nuevoBanner, setNuevoBanner] = useState<File | null>(null);
|
||||
|
||||
@@ -102,17 +104,19 @@ export default function EditEvento({
|
||||
<div className="p-4">
|
||||
{/* Banner */}
|
||||
<div className="mb-4">
|
||||
<ImageUploader
|
||||
label="Banner del evento"
|
||||
defaultBanner={
|
||||
evento.banner
|
||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`
|
||||
: undefined
|
||||
{tipo_usuario === 'administrador' && (
|
||||
<>
|
||||
<ImageUploader
|
||||
label="Banner del evento"
|
||||
defaultBanner={
|
||||
evento.banner
|
||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`
|
||||
: undefined
|
||||
}
|
||||
onChange={(file) => {
|
||||
setNuevoBanner(file);
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
<p className="text-muted mt-2 small">
|
||||
<strong>
|
||||
<i className="bi bi-info-circle-fill me-2"></i>
|
||||
@@ -129,6 +133,8 @@ export default function EditEvento({
|
||||
Formatos soportados: PNG, JPG, JPEG.
|
||||
</strong>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Detalles del evento */}
|
||||
@@ -142,6 +148,8 @@ export default function EditEvento({
|
||||
onChange={(e) =>
|
||||
handleChange('nombre_evento', e.target.value)
|
||||
}
|
||||
disabled={tipo_usuario !== 'administrador'}
|
||||
|
||||
/>
|
||||
|
||||
<div className="mb-4">
|
||||
@@ -150,6 +158,7 @@ export default function EditEvento({
|
||||
<MDEditor
|
||||
value={evento.descripcion_evento ?? ''}
|
||||
onChange={(value) => {
|
||||
if (tipo_usuario !== 'administrador') return;
|
||||
const texto = value || '';
|
||||
if (texto.length <= MAX_LENGTH) {
|
||||
handleChange('descripcion_evento', texto);
|
||||
@@ -167,6 +176,7 @@ export default function EditEvento({
|
||||
placeholder="Selecciona un tipo de evento"
|
||||
onChange={(e) => handleChange('tipo_evento', e.target.value)}
|
||||
options={tipoEventos}
|
||||
disabled={tipo_usuario !== 'administrador'}
|
||||
/>
|
||||
|
||||
<div className="row">
|
||||
@@ -197,11 +207,11 @@ export default function EditEvento({
|
||||
</div>
|
||||
|
||||
{/* Botón de guardar */}
|
||||
<div className="d-flex justify-content-end mt-4">
|
||||
{tipo_usuario === "administrador" && <div className="d-flex justify-content-end mt-4">
|
||||
<Button icon="save" disabled={loading} onClick={handleOnSave}>
|
||||
{loading ? 'Guardando...' : 'Guardar Cambios'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ export default function EditFormulario({
|
||||
evento,
|
||||
handleChange,
|
||||
}: EditFormularioProps) {
|
||||
const tipo_usuario = sessionStorage.getItem('axxxd')?.replace(/"/g, '') || '';
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [nuevoBanner, setNuevoBanner] = useState<File | null>(null);
|
||||
|
||||
@@ -213,11 +214,13 @@ export default function EditFormulario({
|
||||
</div>
|
||||
|
||||
{/* Botón de guardar */}
|
||||
<div className="d-flex justify-content-end mt-4">
|
||||
<Button icon="save" disabled={loading} onClick={handleOnSave}>
|
||||
{loading ? 'Guardando...' : 'Guardar Cambios'}
|
||||
</Button>
|
||||
</div>
|
||||
{tipo_usuario === "administrador" && (
|
||||
<div className="d-flex justify-content-end mt-4">
|
||||
<Button icon="save" disabled={loading} onClick={handleOnSave}>
|
||||
{loading ? 'Guardando...' : 'Guardar Cambios'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,591 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useGetApi } from '@/hooks/use-get-api';
|
||||
import { GetCuestionario } from '@/types/evento';
|
||||
import SimpleInput from '@/components/input';
|
||||
import RadioOptionGroup, { RadioOption } from '@/components/radio-option-group';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import PrefetchAbiertaCorta from './prefetch-abierta-corta';
|
||||
import Button from '@/components/button';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getAxiosError } from '@/utils/errors-utils';
|
||||
import { SubmitResponse } from '@/types/submit';
|
||||
|
||||
export type UsuarioDataResponse = {
|
||||
cuenta: string | null;
|
||||
nombre: string | null;
|
||||
apellidos: string | null;
|
||||
carrera: string | null;
|
||||
genero: string | null;
|
||||
rfc?: string | null; // Solo para trabajadores
|
||||
};
|
||||
|
||||
type UsuarioData = {
|
||||
nombre: string; // alumno o trabajador
|
||||
apellidos: string; // alumno o trabajador
|
||||
correo: string; // alumno o trabajador (en los alumnos se usa el id_ncuenta en trabajadores ellos deben escribirlo)
|
||||
genero: 'M' | 'F'; // alumno o trabajador
|
||||
carrera: string; // solo para alumnos
|
||||
rfc?: string; // solo para trabajadores se usa para buscar información de cuenta
|
||||
};
|
||||
|
||||
const confirmativeOption = ['Sí', 'Si', 'Si, claro', 'Claro que sí'];
|
||||
const negativeOption = ['No', 'No, gracias', 'No, de momento'];
|
||||
|
||||
type RespuestaFormulario = Record<string, string | number>; // id_pregunta: respuesta
|
||||
|
||||
export default function FormularioRegistro({
|
||||
id_cuestionario,
|
||||
handleSubmitFormulario,
|
||||
}: {
|
||||
evento: string;
|
||||
cuestionario: string;
|
||||
id_evento: number;
|
||||
id_cuestionario: number;
|
||||
handleSubmitFormulario: (data: SubmitResponse) => void;
|
||||
}) {
|
||||
// ------------------------
|
||||
// Estados
|
||||
// ------------------------
|
||||
const [isComunidad, setIsComunidad] = useState<RadioOption<number>>();
|
||||
const [respuestas, setRespuestas] = useState<RespuestaFormulario>({});
|
||||
const [cuentaInfo, setCuentaInfo] = useState<UsuarioData | null>(null);
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
// ------------------------
|
||||
// Carga del cuestionario
|
||||
// ------------------------
|
||||
const { data, error } = useGetApi<GetCuestionario>(
|
||||
`/cuestionario/${id_cuestionario}/formulario`
|
||||
);
|
||||
// ------------------------
|
||||
// Extracción de preguntas clave
|
||||
// ------------------------
|
||||
const preguntas = data?.cuestionario.secciones.flatMap((seccion) =>
|
||||
seccion.preguntas.map((pregunta) => pregunta.pregunta)
|
||||
);
|
||||
|
||||
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'
|
||||
);
|
||||
|
||||
// Solución: Agregar estado para controlar si ya se hizo la búsqueda
|
||||
const [cuentaBuscada, setCuentaBuscada] = useState<string>(''); // Nuevo estado
|
||||
|
||||
// ------------------------
|
||||
// Efecto: buscar información de cuenta
|
||||
// ------------------------
|
||||
useEffect(() => {
|
||||
const cuentaId = preguntaCuenta?.id_pregunta ?? '';
|
||||
const cuenta = respuestas[cuentaId];
|
||||
|
||||
const esCuentaAlumno = preguntaCuenta?.validacion === 'cuenta_alumno';
|
||||
const esCuentaTrabajador =
|
||||
preguntaCuenta?.validacion === 'cuenta_trabajador' ||
|
||||
preguntaCuenta?.validacion === 'rfc';
|
||||
|
||||
const puedeBuscar =
|
||||
cuenta &&
|
||||
typeof cuenta === 'string' &&
|
||||
((esCuentaAlumno && cuenta.length === 9) ||
|
||||
(esCuentaTrabajador && cuenta.length === 10));
|
||||
|
||||
const fetchCuentaInfo = async () => {
|
||||
try {
|
||||
const endpoint = esCuentaAlumno
|
||||
? `/alumnos/${cuenta}`
|
||||
: `/trabajadores/${cuenta}`;
|
||||
|
||||
const res = await axiosInstance.get<UsuarioDataResponse>(endpoint);
|
||||
|
||||
console.log('Datos de cuenta obtenidos:', res.data);
|
||||
|
||||
// Verificar si realmente 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 ? res.data.nombre : '',
|
||||
apellidos: res.data.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 ? res.data.carrera : '',
|
||||
});
|
||||
} else {
|
||||
// Si no hay datos válidos, establecer como null
|
||||
setCuentaInfo(null);
|
||||
}
|
||||
|
||||
// Marcar que ya se buscó esta cuenta
|
||||
setCuentaBuscada(cuenta as string);
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos de cuenta:', error);
|
||||
setCuentaInfo(null);
|
||||
setCuentaBuscada(cuenta as string); // También marcar en caso de error
|
||||
}
|
||||
};
|
||||
|
||||
// Solo buscar si puede buscar Y no se ha buscado ya esta cuenta
|
||||
if (puedeBuscar && cuentaBuscada !== cuenta) {
|
||||
fetchCuentaInfo();
|
||||
} else if (!puedeBuscar) {
|
||||
setCuentaInfo(null);
|
||||
setCuentaBuscada(''); // Reset cuando no se puede buscar
|
||||
}
|
||||
}, [
|
||||
respuestas,
|
||||
preguntaCuenta?.id_pregunta,
|
||||
preguntaCuenta?.validacion,
|
||||
cuentaBuscada,
|
||||
]);
|
||||
|
||||
// También resetear cuentaBuscada cuando cambie el número de cuenta
|
||||
useEffect(() => {
|
||||
const cuentaId = preguntaCuenta?.id_pregunta ?? '';
|
||||
const cuenta = respuestas[cuentaId];
|
||||
|
||||
// Si la cuenta cambió, resetear el estado de búsqueda
|
||||
if (cuentaBuscada && cuenta !== cuentaBuscada) {
|
||||
setCuentaBuscada('');
|
||||
}
|
||||
}, [respuestas, preguntaCuenta?.id_pregunta, cuentaBuscada]);
|
||||
|
||||
// ------------------------
|
||||
// Efecto: precargar respuestas si hay cuentaInfo
|
||||
// ------------------------
|
||||
useEffect(() => {
|
||||
if (!cuentaInfo) {
|
||||
if (data?.cuestionario.secciones) {
|
||||
const preguntas = data.cuestionario.secciones.flatMap((seccion) =>
|
||||
seccion.preguntas.map((pregunta) => pregunta.pregunta)
|
||||
);
|
||||
|
||||
const idsPrefetch = preguntas
|
||||
.filter((pregunta) =>
|
||||
[
|
||||
'nombre',
|
||||
'apellidos',
|
||||
'correo',
|
||||
'institucion',
|
||||
'carrera',
|
||||
'genero',
|
||||
].includes(pregunta.validacion)
|
||||
)
|
||||
.map((pregunta) => pregunta.id_pregunta);
|
||||
|
||||
setRespuestas((prev) => {
|
||||
const nuevas = { ...prev };
|
||||
idsPrefetch.forEach((id) => {
|
||||
delete nuevas[id];
|
||||
});
|
||||
return nuevas;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const nuevasRespuestas: RespuestaFormulario = {};
|
||||
|
||||
if (data?.cuestionario.secciones) {
|
||||
const preguntas = data.cuestionario.secciones.flatMap((seccion) =>
|
||||
seccion.preguntas.map((pregunta) => pregunta.pregunta)
|
||||
);
|
||||
|
||||
for (const pregunta of preguntas) {
|
||||
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 'institucion':
|
||||
nuevasRespuestas[id] = 'FES Acatlán';
|
||||
break;
|
||||
case 'carrera':
|
||||
nuevasRespuestas[id] = cuentaInfo.carrera;
|
||||
break;
|
||||
case 'genero':
|
||||
const generoTexto =
|
||||
cuentaInfo.genero === 'M' ? 'Masculino' : 'Femenino';
|
||||
const opcion = pregunta.opciones?.find(
|
||||
(op) =>
|
||||
op.opcion.opcion.toLowerCase() === generoTexto.toLowerCase()
|
||||
);
|
||||
if (opcion) {
|
||||
nuevasRespuestas[id] = String(opcion.id_opcion);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setRespuestas((prev) => ({
|
||||
...prev,
|
||||
...nuevasRespuestas,
|
||||
}));
|
||||
}
|
||||
}, [cuentaInfo, data?.cuestionario.secciones]);
|
||||
|
||||
// ------------------------
|
||||
// Funciones auxiliares
|
||||
// ------------------------
|
||||
const actualizarRespuesta = (idPregunta: string, valor: string) => {
|
||||
setRespuestas((prev) => ({ ...prev, [idPregunta]: valor }));
|
||||
};
|
||||
|
||||
const handleOnSubmit = async () => {
|
||||
setIsSending(true);
|
||||
if (!data?.cuestionario.id_cuestionario) {
|
||||
alert('No se ha cargado correctamente el formulario');
|
||||
return;
|
||||
}
|
||||
|
||||
const preguntasObligatorias =
|
||||
data?.cuestionario.secciones.flatMap((seccion) =>
|
||||
seccion.preguntas
|
||||
.filter((pregunta) => pregunta.pregunta.obligatoria)
|
||||
.map((pregunta) => pregunta.pregunta)
|
||||
) || [];
|
||||
|
||||
const faltantes = preguntasObligatorias.filter(
|
||||
(pregunta) =>
|
||||
respuestas[pregunta.id_pregunta] === undefined ||
|
||||
respuestas[pregunta.id_pregunta] === '' ||
|
||||
respuestas[pregunta.id_pregunta] === null
|
||||
);
|
||||
|
||||
const preguntaCorreo = preguntasObligatorias.find(
|
||||
(pregunta) =>
|
||||
pregunta.validacion === 'correo' ||
|
||||
pregunta.validacion === 'correo_institucional'
|
||||
);
|
||||
|
||||
if (faltantes.length > 0) {
|
||||
toast.error('Por favor responde todas las preguntas obligatorias.');
|
||||
setIsSending(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Buscar un posible correo en las respuestas si no hay cuentaInfo
|
||||
let correo =
|
||||
cuentaInfo?.correo === respuestas[preguntaCorreo?.id_pregunta ?? '']
|
||||
? cuentaInfo?.correo
|
||||
: respuestas[preguntaCorreo?.id_pregunta ?? ''];
|
||||
|
||||
if (!correo) {
|
||||
// Buscar en las respuestas una que parezca 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<SubmitResponse>(
|
||||
'/cuestionario-respondido/submit',
|
||||
payload
|
||||
);
|
||||
toast(
|
||||
() => (
|
||||
<span
|
||||
className="fs-4 pc-3 py-2"
|
||||
style={{
|
||||
maxWidth: '300px',
|
||||
wordWrap: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
}}
|
||||
>
|
||||
{res.data.message.includes('ya ha respondido') ? (
|
||||
<>
|
||||
<i className="bi bi-exclamation-circle-fill text-warning me-2"></i>
|
||||
Respuestas Enviadas Con Éxito
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="bi bi-check-circle-fill text-success me-2"></i>
|
||||
{res.data.message || '¡Formulario enviado!'}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
{
|
||||
duration: 6000,
|
||||
position: 'bottom-center',
|
||||
style: {
|
||||
maxWidth: '425px',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
handleSubmitFormulario(res.data);
|
||||
} catch (error) {
|
||||
console.log(getAxiosError(error));
|
||||
toast.error('Hubo un error al enviar el formulario', {
|
||||
duration: 5000,
|
||||
});
|
||||
router.push('/');
|
||||
}
|
||||
};
|
||||
|
||||
// ------------------------
|
||||
// Condición para mostrar el formulario
|
||||
// ------------------------
|
||||
const mostrarFormularioRestante =
|
||||
!preguntaComunidad || // Si no hay pregunta de comunidad, mostrar el formulario
|
||||
(isComunidad &&
|
||||
confirmativeOption.some((opt) =>
|
||||
isComunidad.label?.toLowerCase().includes(opt.toLowerCase())
|
||||
) &&
|
||||
!!cuentaInfo) ||
|
||||
(isComunidad &&
|
||||
negativeOption.some((opt) =>
|
||||
isComunidad.label?.toLowerCase().includes(opt.toLowerCase())
|
||||
));
|
||||
|
||||
if (error) {
|
||||
console.error('Error al cargar el cuestionario:', error);
|
||||
return <div>Error al cargar el cuestionario</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{preguntaComunidad && (
|
||||
<div className="my-4">
|
||||
<label
|
||||
className={`form-label ${
|
||||
preguntaComunidad.obligatoria ? 'required' : ''
|
||||
}`}
|
||||
>
|
||||
{preguntaComunidad.pregunta}
|
||||
</label>
|
||||
<RadioOptionGroup
|
||||
name="comunidad"
|
||||
options={preguntaComunidad.opciones.map((op) => ({
|
||||
label: op.opcion.opcion,
|
||||
value: op.id_opcion,
|
||||
}))}
|
||||
selectedValue={isComunidad?.value}
|
||||
onChange={(opt) => {
|
||||
console.log('Opción comunidad seleccionada:', opt);
|
||||
setIsComunidad(opt);
|
||||
actualizarRespuesta(
|
||||
preguntaComunidad.id_pregunta.toString(),
|
||||
String(opt.value)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isComunidad &&
|
||||
confirmativeOption.some((opt) =>
|
||||
isComunidad.label?.toLowerCase().includes(opt.toLowerCase())
|
||||
) &&
|
||||
preguntaCuenta && (
|
||||
<div className="my-4">
|
||||
<label
|
||||
className={`form-label ${
|
||||
preguntaCuenta.obligatoria ? 'required' : ''
|
||||
}`}
|
||||
>
|
||||
{preguntaCuenta.pregunta}
|
||||
</label>
|
||||
<SimpleInput
|
||||
type="text"
|
||||
name="cuenta"
|
||||
placeholder={
|
||||
preguntaCuenta.validacion === 'cuenta_alumno'
|
||||
? 'Ingrese su cuenta'
|
||||
: preguntaCuenta?.validacion === 'rfc'
|
||||
? 'Ingrese su RFC sin homoclave'
|
||||
: preguntaCuenta?.validacion === 'cuenta_trabajador'
|
||||
? 'Ingrese su numero de trabajador'
|
||||
: 'Ingrese su ' + preguntaCuenta.pregunta
|
||||
}
|
||||
maxLength={10}
|
||||
value={respuestas[preguntaCuenta.id_pregunta] || ''}
|
||||
onChange={(e) =>
|
||||
setRespuestas((prev) => ({
|
||||
...prev,
|
||||
[preguntaCuenta.id_pregunta]: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{mostrarFormularioRestante &&
|
||||
data?.cuestionario.secciones.map((seccion, i) => (
|
||||
<div key={i}>
|
||||
{seccion.preguntas.map((pregunta) => {
|
||||
// Evitar renderizar las preguntas ya manejadas
|
||||
if (
|
||||
pregunta.pregunta.id_pregunta ===
|
||||
preguntaComunidad?.id_pregunta ||
|
||||
pregunta.pregunta.id_pregunta === preguntaCuenta?.id_pregunta
|
||||
)
|
||||
return null;
|
||||
|
||||
if (
|
||||
pregunta.pregunta.tipo_pregunta.tipo_pregunta ===
|
||||
'Abierta (Respuesta corta)'
|
||||
) {
|
||||
return (
|
||||
<PrefetchAbiertaCorta
|
||||
key={pregunta.pregunta.id_pregunta}
|
||||
obligatorio={pregunta.pregunta.obligatoria}
|
||||
idPregunta={pregunta.pregunta.id_pregunta}
|
||||
enunciado={pregunta.pregunta.pregunta}
|
||||
validacion={pregunta.pregunta.validacion}
|
||||
respuestas={respuestas}
|
||||
cuentaInfo={cuentaInfo}
|
||||
isComunidad={isComunidad?.label}
|
||||
onChange={actualizarRespuesta}
|
||||
preguntaComunidad={preguntaComunidad?.validacion ?? ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
pregunta.pregunta.tipo_pregunta.tipo_pregunta ===
|
||||
'Abierta (Parrafo)'
|
||||
) {
|
||||
return (
|
||||
<div key={pregunta.pregunta.id_pregunta} className="my-4">
|
||||
<label
|
||||
className={`form-label ${
|
||||
pregunta.pregunta.obligatoria ? 'required' : ''
|
||||
}`}
|
||||
>
|
||||
{pregunta.pregunta.pregunta}
|
||||
</label>
|
||||
<SimpleInput
|
||||
type="textarea"
|
||||
name={`respuesta_${pregunta.pregunta.id_pregunta}`}
|
||||
placeholder="Ingrese una respuesta"
|
||||
value={respuestas[pregunta.pregunta.id_pregunta] || ''}
|
||||
onChange={(e) =>
|
||||
setRespuestas((prev) => ({
|
||||
...prev,
|
||||
[pregunta.pregunta.id_pregunta]: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Cerrada') {
|
||||
const opciones: RadioOption<number>[] =
|
||||
pregunta.pregunta.opciones.map((op) => ({
|
||||
label: op.opcion.opcion,
|
||||
value: op.id_opcion,
|
||||
}));
|
||||
let respuestaActual =
|
||||
respuestas[`${pregunta.pregunta.id_pregunta}`];
|
||||
|
||||
if (
|
||||
(!preguntaComunidad ||
|
||||
(confirmativeOption.includes(isComunidad?.label || '') &&
|
||||
cuentaInfo)) &&
|
||||
respuestaActual === undefined // solo si no ha respondido
|
||||
) {
|
||||
if (pregunta.pregunta.validacion === 'genero' && cuentaInfo) {
|
||||
const generoTexto =
|
||||
cuentaInfo.genero === 'M' ? 'Masculino' : 'Femenino';
|
||||
|
||||
const opcionGenero = pregunta.pregunta.opciones.find(
|
||||
(op) =>
|
||||
op.opcion.opcion.toLowerCase() ===
|
||||
generoTexto.toLowerCase()
|
||||
);
|
||||
if (opcionGenero) {
|
||||
respuestaActual = String(opcionGenero.id_opcion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={pregunta.pregunta.id_pregunta}>
|
||||
<label
|
||||
className={`form-label ${
|
||||
pregunta.pregunta.obligatoria ? 'required' : ''
|
||||
}`}
|
||||
>
|
||||
{pregunta.pregunta.pregunta}
|
||||
</label>
|
||||
<RadioOptionGroup
|
||||
name={`pregunta_${pregunta.pregunta.id_pregunta}`}
|
||||
options={opciones}
|
||||
selectedValue={
|
||||
respuestaActual ? Number(respuestaActual) : undefined
|
||||
}
|
||||
onChange={(opt) => {
|
||||
actualizarRespuesta(
|
||||
`${pregunta.pregunta.id_pregunta}`,
|
||||
String(opt.value)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={pregunta.pregunta.id_pregunta} className="my-4">
|
||||
<label
|
||||
className={`form-label ${
|
||||
pregunta.pregunta.obligatoria ? 'required' : ''
|
||||
}`}
|
||||
>
|
||||
{pregunta.pregunta.pregunta}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
className="btn btn-primary"
|
||||
onClick={handleOnSubmit}
|
||||
disabled={!mostrarFormularioRestante || isSending}
|
||||
>
|
||||
{isSending ? 'Enviando...' : 'Enviar Formulario'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useGetApi } from '@/hooks/use-get-api';
|
||||
import { GetCuestionario } from '@/types/evento';
|
||||
import { SubmitResponse } from '@/types/submit';
|
||||
import SimpleInput from '@/components/input';
|
||||
import RadioOptionGroup, { RadioOption } from '@/components/radio-option-group';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import PrefetchAbiertaCorta from './prefetch-abierta-corta';
|
||||
import Button from '@/components/button';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getAxiosError } from '@/utils/errors-utils';
|
||||
import { SubmitResponse } from '@/types/submit';
|
||||
|
||||
export type UsuarioDataResponse = {
|
||||
cuenta: string | null;
|
||||
@@ -17,22 +15,19 @@ export type UsuarioDataResponse = {
|
||||
apellidos: string | null;
|
||||
carrera: string | null;
|
||||
genero: string | null;
|
||||
rfc?: string | null; // Solo para trabajadores
|
||||
rfc?: string | null;
|
||||
};
|
||||
|
||||
type UsuarioData = {
|
||||
nombre: string; // alumno o trabajador
|
||||
apellidos: string; // alumno o trabajador
|
||||
correo: string; // alumno o trabajador (en los alumnos se usa el id_ncuenta en trabajadores ellos deben escribirlo)
|
||||
genero: 'M' | 'F'; // alumno o trabajador
|
||||
carrera: string; // solo para alumnos
|
||||
rfc?: string; // solo para trabajadores se usa para buscar información de cuenta
|
||||
nombre: string;
|
||||
apellidos: string;
|
||||
correo: string;
|
||||
genero: 'M' | 'F';
|
||||
carrera: string;
|
||||
rfc?: string;
|
||||
};
|
||||
|
||||
const confirmativeOption = ['Sí', 'Si', 'Si, claro', 'Claro que sí'];
|
||||
const negativeOption = ['No', 'No, gracias', 'No, de momento'];
|
||||
|
||||
type RespuestaFormulario = Record<string, string | number>; // id_pregunta: respuesta
|
||||
type RespuestaFormulario = Record<string, string | number>;
|
||||
|
||||
export default function FormularioRegistro({
|
||||
id_cuestionario,
|
||||
@@ -44,28 +39,24 @@ export default function FormularioRegistro({
|
||||
id_cuestionario: number;
|
||||
handleSubmitFormulario: (data: SubmitResponse) => void;
|
||||
}) {
|
||||
// ------------------------
|
||||
// Estados
|
||||
// ------------------------
|
||||
const [isComunidad, setIsComunidad] = useState<RadioOption<number>>();
|
||||
// Estados principales
|
||||
const [respuestas, setRespuestas] = useState<RespuestaFormulario>({});
|
||||
const [cuentaInfo, setCuentaInfo] = useState<UsuarioData | null>(null);
|
||||
const [busquedaCompletada, setBusquedaCompletada] = useState<boolean>(false);
|
||||
const [cuentaBuscada, setCuentaBuscada] = useState<string>('');
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
// ------------------------
|
||||
|
||||
// Carga del cuestionario
|
||||
// ------------------------
|
||||
const { data, error } = useGetApi<GetCuestionario>(
|
||||
`/cuestionario/${id_cuestionario}/formulario`
|
||||
);
|
||||
// ------------------------
|
||||
// Extracción de preguntas clave
|
||||
// ------------------------
|
||||
|
||||
// Extraer todas las preguntas del cuestionario
|
||||
const preguntas = data?.cuestionario.secciones.flatMap((seccion) =>
|
||||
seccion.preguntas.map((pregunta) => pregunta.pregunta)
|
||||
);
|
||||
|
||||
// Identificar preguntas especiales
|
||||
const preguntaComunidad = preguntas?.find(
|
||||
(pregunta) =>
|
||||
pregunta.validacion === 'comunidad_alumno' ||
|
||||
@@ -79,166 +70,168 @@ export default function FormularioRegistro({
|
||||
pregunta.validacion === 'rfc'
|
||||
);
|
||||
|
||||
// ------------------------
|
||||
// Efecto: buscar información de cuenta
|
||||
// ------------------------
|
||||
// Determinar tipo de comunidad seleccionada
|
||||
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(() => {
|
||||
const cuentaId = preguntaCuenta?.id_pregunta ?? '';
|
||||
const cuenta = respuestas[cuentaId];
|
||||
|
||||
const esCuentaAlumno = preguntaCuenta?.validacion === 'cuenta_alumno';
|
||||
const esCuentaTrabajador =
|
||||
preguntaCuenta?.validacion === 'cuenta_trabajador' ||
|
||||
preguntaCuenta?.validacion === 'rfc';
|
||||
|
||||
const puedeBuscar =
|
||||
cuenta &&
|
||||
typeof cuenta === 'string' &&
|
||||
((esCuentaAlumno && cuenta.length === 9) ||
|
||||
(esCuentaTrabajador && cuenta.length === 10));
|
||||
|
||||
const fetchCuentaInfo = async () => {
|
||||
try {
|
||||
const endpoint = esCuentaAlumno
|
||||
? `/alumnos/${cuenta}`
|
||||
: `/trabajadores/${cuenta}`;
|
||||
|
||||
const res = await axiosInstance.get<UsuarioDataResponse>(endpoint);
|
||||
|
||||
console.log('Datos de cuenta obtenidos:', res.data);
|
||||
setCuentaInfo({
|
||||
nombre: res.data.nombre ? res.data.nombre : '',
|
||||
apellidos: res.data.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 ? res.data.carrera : '',
|
||||
});
|
||||
// Usar datos fake para pruebas
|
||||
/* setCuentaInfo({
|
||||
nombre: 'Juan',
|
||||
apellidos: 'Pérez',
|
||||
correo: 'juan.perez@pcpuma.acatlan.unam.mx',
|
||||
genero: 'M',
|
||||
carrera: 'Ingeniería en Computación',
|
||||
}); */
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos de cuenta:', error);
|
||||
setCuentaInfo(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (puedeBuscar) {
|
||||
fetchCuentaInfo();
|
||||
} else {
|
||||
if (!preguntaCuenta || !esComunidadSi) {
|
||||
setCuentaInfo(null);
|
||||
}
|
||||
}, [respuestas, preguntaCuenta?.id_pregunta, preguntaCuenta?.validacion]);
|
||||
|
||||
// ------------------------
|
||||
// Efecto: precargar respuestas si hay cuentaInfo
|
||||
// ------------------------
|
||||
useEffect(() => {
|
||||
if (!cuentaInfo) {
|
||||
if (data?.cuestionario.secciones) {
|
||||
const preguntas = data.cuestionario.secciones.flatMap((seccion) =>
|
||||
seccion.preguntas.map((pregunta) => pregunta.pregunta)
|
||||
);
|
||||
|
||||
const idsPrefetch = preguntas
|
||||
.filter((pregunta) =>
|
||||
[
|
||||
'nombre',
|
||||
'apellidos',
|
||||
'correo',
|
||||
'institucion',
|
||||
'carrera',
|
||||
'genero',
|
||||
].includes(pregunta.validacion)
|
||||
)
|
||||
.map((pregunta) => pregunta.id_pregunta);
|
||||
|
||||
setRespuestas((prev) => {
|
||||
const nuevas = { ...prev };
|
||||
idsPrefetch.forEach((id) => {
|
||||
delete nuevas[id];
|
||||
});
|
||||
return nuevas;
|
||||
});
|
||||
}
|
||||
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<UsuarioDataResponse>(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 = {};
|
||||
|
||||
if (data?.cuestionario.secciones) {
|
||||
const preguntas = data.cuestionario.secciones.flatMap((seccion) =>
|
||||
seccion.preguntas.map((pregunta) => pregunta.pregunta)
|
||||
);
|
||||
|
||||
for (const pregunta of preguntas) {
|
||||
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 'institucion':
|
||||
nuevasRespuestas[id] = 'FES Acatlán';
|
||||
break;
|
||||
case 'carrera':
|
||||
nuevasRespuestas[id] = cuentaInfo.carrera;
|
||||
break;
|
||||
case 'genero':
|
||||
const generoTexto =
|
||||
cuentaInfo.genero === 'M' ? 'Masculino' : 'Femenino';
|
||||
const opcion = pregunta.opciones?.find(
|
||||
(op) =>
|
||||
op.opcion.opcion.toLowerCase() === generoTexto.toLowerCase()
|
||||
);
|
||||
if (opcion) {
|
||||
nuevasRespuestas[id] = String(opcion.id_opcion);
|
||||
}
|
||||
break;
|
||||
}
|
||||
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]);
|
||||
setRespuestas(prev => ({ ...prev, ...nuevasRespuestas }));
|
||||
}, [cuentaInfo, data?.cuestionario.secciones, preguntas]);
|
||||
|
||||
// ------------------------
|
||||
// Funciones auxiliares
|
||||
// ------------------------
|
||||
// Función para actualizar respuestas
|
||||
const actualizarRespuesta = (idPregunta: string, valor: string) => {
|
||||
setRespuestas((prev) => ({ ...prev, [idPregunta]: valor }));
|
||||
setRespuestas(prev => ({ ...prev, [idPregunta]: valor }));
|
||||
};
|
||||
|
||||
// Función para enviar formulario
|
||||
const handleOnSubmit = async () => {
|
||||
setIsSending(true);
|
||||
|
||||
if (!data?.cuestionario.id_cuestionario) {
|
||||
alert('No se ha cargado correctamente el formulario');
|
||||
toast.error('No se ha cargado correctamente el formulario');
|
||||
setIsSending(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const preguntasObligatorias =
|
||||
data?.cuestionario.secciones.flatMap((seccion) =>
|
||||
seccion.preguntas
|
||||
.filter((pregunta) => pregunta.pregunta.obligatoria)
|
||||
.map((pregunta) => pregunta.pregunta)
|
||||
) || [];
|
||||
// Validar preguntas obligatorias
|
||||
const preguntasObligatorias = data.cuestionario.secciones.flatMap((seccion) =>
|
||||
seccion.preguntas
|
||||
.filter((pregunta) => pregunta.pregunta.obligatoria)
|
||||
.map((pregunta) => pregunta.pregunta)
|
||||
);
|
||||
|
||||
const faltantes = preguntasObligatorias.filter(
|
||||
(pregunta) =>
|
||||
@@ -247,26 +240,22 @@ export default function FormularioRegistro({
|
||||
respuestas[pregunta.id_pregunta] === null
|
||||
);
|
||||
|
||||
const preguntaCorreo = preguntasObligatorias.find(
|
||||
(pregunta) =>
|
||||
pregunta.validacion === 'correo' ||
|
||||
pregunta.validacion === 'correo_institucional'
|
||||
);
|
||||
|
||||
if (faltantes.length > 0) {
|
||||
toast.error('Por favor responde todas las preguntas obligatorias.');
|
||||
setIsSending(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Buscar un posible correo en las respuestas si no hay cuentaInfo
|
||||
let correo =
|
||||
cuentaInfo?.correo === respuestas[preguntaCorreo?.id_pregunta ?? '']
|
||||
? cuentaInfo?.correo
|
||||
: respuestas[preguntaCorreo?.id_pregunta ?? ''];
|
||||
// Buscar correo
|
||||
const preguntaCorreo = preguntas?.find(p =>
|
||||
p.validacion === 'correo' || p.validacion === 'correo_institucional'
|
||||
);
|
||||
|
||||
let correo = preguntaCorreo
|
||||
? String(respuestas[preguntaCorreo.id_pregunta] || '')
|
||||
: '';
|
||||
|
||||
if (!correo) {
|
||||
// Buscar en las respuestas una que parezca correo
|
||||
const correoRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
for (const valor of Object.values(respuestas)) {
|
||||
if (typeof valor === 'string' && correoRegex.test(valor)) {
|
||||
@@ -278,7 +267,7 @@ export default function FormularioRegistro({
|
||||
|
||||
const payload = {
|
||||
id_cuestionario: id_cuestionario,
|
||||
correo: correo || '',
|
||||
correo: correo,
|
||||
fecha_envio: new Date().toISOString(),
|
||||
respuestas: Object.entries(respuestas).map(([id_pregunta, valor]) => ({
|
||||
id_pregunta: Number(id_pregunta),
|
||||
@@ -291,77 +280,38 @@ export default function FormularioRegistro({
|
||||
'/cuestionario-respondido/submit',
|
||||
payload
|
||||
);
|
||||
toast(
|
||||
() => (
|
||||
<span
|
||||
className="fs-4 pc-3 py-2"
|
||||
style={{
|
||||
maxWidth: '300px',
|
||||
wordWrap: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
}}
|
||||
>
|
||||
{res.data.message.includes('ya ha respondido') ? (
|
||||
<>
|
||||
<i className="bi bi-exclamation-circle-fill text-warning me-2"></i>
|
||||
Respuestas Enviadas Con Éxito
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="bi bi-check-circle-fill text-success me-2"></i>
|
||||
{res.data.message || '¡Formulario enviado!'}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
{
|
||||
duration: 6000,
|
||||
position: 'bottom-center',
|
||||
style: {
|
||||
maxWidth: '425px',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
toast.success(res.data.message || '¡Formulario enviado exitosamente!');
|
||||
handleSubmitFormulario(res.data);
|
||||
} catch (error) {
|
||||
console.log(getAxiosError(error));
|
||||
toast.error('Hubo un error al enviar el formulario', {
|
||||
duration: 5000,
|
||||
});
|
||||
router.push('/');
|
||||
console.error('Error al enviar formulario:', getAxiosError(error));
|
||||
toast.error('Hubo un error al enviar el formulario');
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ------------------------
|
||||
// Condición para mostrar el formulario
|
||||
// ------------------------
|
||||
const mostrarFormularioRestante =
|
||||
!preguntaComunidad || // Si no hay pregunta de comunidad, mostrar el formulario
|
||||
(isComunidad &&
|
||||
confirmativeOption.some((opt) =>
|
||||
isComunidad.label?.toLowerCase().includes(opt.toLowerCase())
|
||||
) &&
|
||||
!!cuentaInfo) ||
|
||||
(isComunidad &&
|
||||
negativeOption.some((opt) =>
|
||||
isComunidad.label?.toLowerCase().includes(opt.toLowerCase())
|
||||
));
|
||||
// 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);
|
||||
return <div>Error al cargar el cuestionario</div>;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <div>Cargando...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Pregunta de comunidad */}
|
||||
{preguntaComunidad && (
|
||||
<div className="my-4">
|
||||
<label
|
||||
className={`form-label ${
|
||||
preguntaComunidad.obligatoria ? 'required' : ''
|
||||
}`}
|
||||
>
|
||||
<label className={`form-label ${preguntaComunidad.obligatoria ? 'required' : ''}`}>
|
||||
{preguntaComunidad.pregunta}
|
||||
</label>
|
||||
<RadioOptionGroup
|
||||
@@ -370,192 +320,152 @@ export default function FormularioRegistro({
|
||||
label: op.opcion.opcion,
|
||||
value: op.id_opcion,
|
||||
}))}
|
||||
selectedValue={isComunidad?.value}
|
||||
selectedValue={comunidadSeleccionada ? Number(comunidadSeleccionada) : undefined}
|
||||
onChange={(opt) => {
|
||||
console.log('Opción comunidad seleccionada:', opt);
|
||||
setIsComunidad(opt);
|
||||
actualizarRespuesta(
|
||||
preguntaComunidad.id_pregunta.toString(),
|
||||
String(opt.value)
|
||||
);
|
||||
actualizarRespuesta(preguntaComunidad.id_pregunta.toString(), String(opt.value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isComunidad &&
|
||||
confirmativeOption.some((opt) =>
|
||||
isComunidad.label?.toLowerCase().includes(opt.toLowerCase())
|
||||
) &&
|
||||
preguntaCuenta && (
|
||||
<div className="my-4">
|
||||
<label
|
||||
className={`form-label ${
|
||||
preguntaCuenta.obligatoria ? 'required' : ''
|
||||
}`}
|
||||
>
|
||||
{preguntaCuenta.pregunta}
|
||||
</label>
|
||||
<SimpleInput
|
||||
type="text"
|
||||
name="cuenta"
|
||||
placeholder={
|
||||
preguntaCuenta.validacion === 'cuenta_alumno'
|
||||
? 'Ingrese su cuenta'
|
||||
: preguntaCuenta?.validacion === 'rfc'
|
||||
? 'Ingrese su RFC sin homoclave'
|
||||
: preguntaCuenta?.validacion === 'cuenta_trabajador'
|
||||
? 'Ingrese su numero de trabajador'
|
||||
: 'Ingrese su ' + preguntaCuenta.pregunta
|
||||
}
|
||||
maxLength={10}
|
||||
value={respuestas[preguntaCuenta.id_pregunta] || ''}
|
||||
onChange={(e) =>
|
||||
setRespuestas((prev) => ({
|
||||
...prev,
|
||||
[preguntaCuenta.id_pregunta]: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{mostrarFormularioRestante &&
|
||||
data?.cuestionario.secciones.map((seccion, i) => (
|
||||
<div key={i}>
|
||||
{seccion.preguntas.map((pregunta) => {
|
||||
// Evitar renderizar las preguntas ya manejadas
|
||||
if (
|
||||
pregunta.pregunta.id_pregunta ===
|
||||
preguntaComunidad?.id_pregunta ||
|
||||
pregunta.pregunta.id_pregunta === preguntaCuenta?.id_pregunta
|
||||
)
|
||||
return null;
|
||||
|
||||
if (
|
||||
pregunta.pregunta.tipo_pregunta.tipo_pregunta ===
|
||||
'Abierta (Respuesta corta)'
|
||||
) {
|
||||
return (
|
||||
<PrefetchAbiertaCorta
|
||||
key={pregunta.pregunta.id_pregunta}
|
||||
obligatorio={pregunta.pregunta.obligatoria}
|
||||
idPregunta={pregunta.pregunta.id_pregunta}
|
||||
enunciado={pregunta.pregunta.pregunta}
|
||||
validacion={pregunta.pregunta.validacion}
|
||||
respuestas={respuestas}
|
||||
cuentaInfo={cuentaInfo}
|
||||
isComunidad={isComunidad?.label}
|
||||
onChange={actualizarRespuesta}
|
||||
preguntaComunidad={preguntaComunidad?.validacion ?? ''}
|
||||
{/* Pregunta de cuenta (si respondió "Sí" a comunidad) */}
|
||||
{esComunidadSi && preguntaCuenta && (
|
||||
<div className="my-4">
|
||||
<label className={`form-label ${preguntaCuenta.obligatoria ? 'required' : ''}`}>
|
||||
{preguntaCuenta.pregunta}
|
||||
</label>
|
||||
<SimpleInput
|
||||
type="text"
|
||||
name="cuenta"
|
||||
placeholder={
|
||||
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}`
|
||||
}
|
||||
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
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Resto del formulario */}
|
||||
{mostrarFormularioCompleto && data.cuestionario.secciones.map((seccion, i) => (
|
||||
<div key={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
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const valor = respuestas[pregunta.pregunta.id_pregunta] || '';
|
||||
|
||||
// Pregunta abierta corta
|
||||
if (pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Abierta (Respuesta corta)') {
|
||||
return (
|
||||
<div key={pregunta.pregunta.id_pregunta} className="my-4">
|
||||
<label className={`form-label ${pregunta.pregunta.obligatoria ? 'required' : ''}`}>
|
||||
{pregunta.pregunta.pregunta}
|
||||
</label>
|
||||
<SimpleInput
|
||||
type="text"
|
||||
name={`respuesta_${pregunta.pregunta.id_pregunta}`}
|
||||
placeholder="Ingrese una respuesta"
|
||||
value={valor}
|
||||
onChange={(e) => actualizarRespuesta(
|
||||
pregunta.pregunta.id_pregunta.toString(),
|
||||
e.target.value
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
pregunta.pregunta.tipo_pregunta.tipo_pregunta ===
|
||||
'Abierta (Parrafo)'
|
||||
) {
|
||||
return (
|
||||
<div key={pregunta.pregunta.id_pregunta} className="my-4">
|
||||
<label
|
||||
className={`form-label ${
|
||||
pregunta.pregunta.obligatoria ? 'required' : ''
|
||||
}`}
|
||||
>
|
||||
{pregunta.pregunta.pregunta}
|
||||
</label>
|
||||
<SimpleInput
|
||||
type="textarea"
|
||||
name={`respuesta_${pregunta.pregunta.id_pregunta}`}
|
||||
placeholder="Ingrese una respuesta"
|
||||
value={respuestas[pregunta.pregunta.id_pregunta] || ''}
|
||||
onChange={(e) =>
|
||||
setRespuestas((prev) => ({
|
||||
...prev,
|
||||
[pregunta.pregunta.id_pregunta]: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Pregunta abierta párrafo
|
||||
if (pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Abierta (Parrafo)') {
|
||||
return (
|
||||
<div key={pregunta.pregunta.id_pregunta} className="my-4">
|
||||
<label className={`form-label ${pregunta.pregunta.obligatoria ? 'required' : ''}`}>
|
||||
{pregunta.pregunta.pregunta}
|
||||
</label>
|
||||
<SimpleInput
|
||||
type="textarea"
|
||||
name={`respuesta_${pregunta.pregunta.id_pregunta}`}
|
||||
placeholder="Ingrese una respuesta"
|
||||
value={valor}
|
||||
onChange={(e) => actualizarRespuesta(
|
||||
pregunta.pregunta.id_pregunta.toString(),
|
||||
e.target.value
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Cerrada') {
|
||||
const opciones: RadioOption<number>[] =
|
||||
pregunta.pregunta.opciones.map((op) => ({
|
||||
label: op.opcion.opcion,
|
||||
value: op.id_opcion,
|
||||
}));
|
||||
let respuestaActual =
|
||||
respuestas[`${pregunta.pregunta.id_pregunta}`];
|
||||
|
||||
if (
|
||||
(!preguntaComunidad ||
|
||||
(confirmativeOption.includes(isComunidad?.label || '') &&
|
||||
cuentaInfo)) &&
|
||||
respuestaActual === undefined // solo si no ha respondido
|
||||
) {
|
||||
if (pregunta.pregunta.validacion === 'genero' && cuentaInfo) {
|
||||
const generoTexto =
|
||||
cuentaInfo.genero === 'M' ? 'Masculino' : 'Femenino';
|
||||
|
||||
const opcionGenero = pregunta.pregunta.opciones.find(
|
||||
(op) =>
|
||||
op.opcion.opcion.toLowerCase() ===
|
||||
generoTexto.toLowerCase()
|
||||
);
|
||||
if (opcionGenero) {
|
||||
respuestaActual = String(opcionGenero.id_opcion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={pregunta.pregunta.id_pregunta}>
|
||||
<label
|
||||
className={`form-label ${
|
||||
pregunta.pregunta.obligatoria ? 'required' : ''
|
||||
}`}
|
||||
>
|
||||
{pregunta.pregunta.pregunta}
|
||||
</label>
|
||||
<RadioOptionGroup
|
||||
name={`pregunta_${pregunta.pregunta.id_pregunta}`}
|
||||
options={opciones}
|
||||
selectedValue={
|
||||
respuestaActual ? Number(respuestaActual) : undefined
|
||||
}
|
||||
onChange={(opt) => {
|
||||
actualizarRespuesta(
|
||||
`${pregunta.pregunta.id_pregunta}`,
|
||||
String(opt.value)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Pregunta cerrada
|
||||
if (pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Cerrada') {
|
||||
const opciones: RadioOption<number>[] = pregunta.pregunta.opciones.map((op) => ({
|
||||
label: op.opcion.opcion,
|
||||
value: op.id_opcion,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div key={pregunta.pregunta.id_pregunta} className="my-4">
|
||||
<label
|
||||
className={`form-label ${
|
||||
pregunta.pregunta.obligatoria ? 'required' : ''
|
||||
}`}
|
||||
>
|
||||
<label className={`form-label ${pregunta.pregunta.obligatoria ? 'required' : ''}`}>
|
||||
{pregunta.pregunta.pregunta}
|
||||
</label>
|
||||
<RadioOptionGroup
|
||||
name={`pregunta_${pregunta.pregunta.id_pregunta}`}
|
||||
options={opciones}
|
||||
selectedValue={valor ? Number(valor) : undefined}
|
||||
onChange={(opt) => actualizarRespuesta(
|
||||
pregunta.pregunta.id_pregunta.toString(),
|
||||
String(opt.value)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
className="btn btn-primary"
|
||||
onClick={handleOnSubmit}
|
||||
disabled={!mostrarFormularioRestante || isSending}
|
||||
>
|
||||
{isSending ? 'Enviando...' : 'Enviar Formulario'}
|
||||
</Button>
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Botón de envío */}
|
||||
{mostrarFormularioCompleto && (
|
||||
<Button
|
||||
className="btn btn-primary"
|
||||
onClick={handleOnSubmit}
|
||||
disabled={isSending}
|
||||
>
|
||||
{isSending ? 'Enviando...' : 'Enviar Formulario'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
}'
|
||||
*/
|
||||
Reference in New Issue
Block a user