feat: Refactor carousel component and add new event card
- Updated ClientCarousel to accept props for dynamic image rendering. - Changed CarouselProps interface to export for better type usage. - Enhanced Carousel component to handle default images and improved styling. - Introduced EventoCard component for displaying event details with dynamic data. - Added MarkdownRenderer for rendering markdown content in EventoCard. - Removed old Formulario component and replaced it with FormularioRegistro for better structure. - Implemented prefetching logic in FormularioRegistro to auto-fill user data based on account info. - Created PrefetchAbiertaCorta component for handling short answer questions with pre-filled data. - Added useGetApi hook for simplified API data fetching. - Updated plantillas data to include validation types for new form fields. - Introduced new types for event and questionnaire handling in TypeScript. - Enhanced styles for better UI/UX, including carousel controls and required field indicators.
This commit is contained in:
+10
-2
@@ -1,7 +1,15 @@
|
||||
import type { NextConfig } from "next";
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
port: '4200',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
Generated
+1175
-4
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-markdown": "^10.1.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 790 KiB |
@@ -7,8 +7,13 @@ import CreateEvento from '@/containers/create-evento';
|
||||
import { plantillasDisponibles } from '@/data/plantillas';
|
||||
import Image from 'next/image';
|
||||
import CreateFormulario from '@/containers/create-formulario';
|
||||
import Button from '@/components/button';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function Page() {
|
||||
const [eventoBanner, setEventoBanner] = useState<File | null>(null);
|
||||
const [evento, setEvento] = useState<CreateEventoType>({
|
||||
tipo_evento: '',
|
||||
nombre_evento: '',
|
||||
@@ -16,6 +21,7 @@ export default function Page() {
|
||||
fecha_inicio: new Date(),
|
||||
fecha_fin: new Date(),
|
||||
});
|
||||
const router = useRouter();
|
||||
|
||||
const [datosFormulario, setDatosFormulario] =
|
||||
useState<FormularioCreacion | null>(null);
|
||||
@@ -31,7 +37,7 @@ export default function Page() {
|
||||
};
|
||||
|
||||
const handleBannerChange = (file: File | null) => {
|
||||
console.log('Archivo seleccionado:', file);
|
||||
setEventoBanner(file);
|
||||
};
|
||||
|
||||
const handleSeleccionarPlantilla = (plantillaId: string) => {
|
||||
@@ -44,6 +50,42 @@ export default function Page() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCrearEventoYFormulario = async () => {
|
||||
try {
|
||||
const createEvento = await axiosInstance.post('/evento', evento);
|
||||
|
||||
if (eventoBanner) {
|
||||
const formData = new FormData();
|
||||
formData.append('banner', eventoBanner);
|
||||
const bannercreated = await axiosInstance.post(
|
||||
`/evento/${createEvento.data.id_evento}/banner`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
console.log('Banner creado:', bannercreated.data);
|
||||
}
|
||||
const createFormulario = await axiosInstance.post('/cuestionario', {
|
||||
id_evento: createEvento.data.id_evento,
|
||||
...datosFormulario,
|
||||
});
|
||||
if (createFormulario.data.id_formulario) {
|
||||
console.log('Formulario creado:', createFormulario.data);
|
||||
}
|
||||
|
||||
router.push(`/administrador`)
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
'Error al crear el evento y el formulario. Por favor, inténtalo de nuevo.'
|
||||
);
|
||||
console.error('Error al crear evento y formulario:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="my-4">
|
||||
@@ -108,6 +150,10 @@ export default function Page() {
|
||||
onChange={(nuevo) => setDatosFormulario({ ...nuevo })}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button className="mb-4" onClick={handleCrearEventoYFormulario}>
|
||||
Crear Evento y Formulario
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
import MarkdownRenderer from '@/components/markdown-render';
|
||||
import FormularioRegistro from '@/containers/formulario/formulario-registro';
|
||||
import { useGetApi } from '@/hooks/use-get-api';
|
||||
import { GetEventoWithCuestionario } from '@/types/evento';
|
||||
import Image from 'next/image';
|
||||
import { useParams } from 'next/navigation';
|
||||
import React from 'react';
|
||||
|
||||
type Params = {
|
||||
nombre_evento: string;
|
||||
id_evento: string;
|
||||
id_cuestionario: string;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const params = useParams<Params>();
|
||||
const { data, error } = useGetApi<GetEventoWithCuestionario>(
|
||||
`/evento/${params.id_evento}/cuestionario/${params.id_cuestionario}`
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <div>Error al cargar el cuestionario: {error.message}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container my-5">
|
||||
<div className="text-center mb-4">
|
||||
{data?.banner && (
|
||||
<Image
|
||||
src={`${process.env.NEXT_PUBLIC_API_URL}/banners/${data.banner}`}
|
||||
alt={data?.nombre_evento || ''}
|
||||
width={800}
|
||||
height={400}
|
||||
className="img-fluid rounded"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<h1>{data?.nombre_evento}</h1>
|
||||
{data?.descripcion_evento && (
|
||||
<MarkdownRenderer markdown={data?.descripcion_evento} />
|
||||
)}
|
||||
{data && (
|
||||
<FormularioRegistro id_cuestionario={data?.cuestionario.id_cuestionario} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+26
-77
@@ -1,89 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import ClientCarousel from '@/client-components/client-carousel';
|
||||
import FormularioCard from '@/components/formulario-card';
|
||||
import { GetCuestionario } from '@/types/cuestionario';
|
||||
import { useEffect, useState } from 'react';
|
||||
import EventoCard from '@/components/evento-card';
|
||||
import { useGetApi } from '@/hooks/use-get-api';
|
||||
import { GetEventoWithCuestionarios } from '@/types/evento';
|
||||
import React from 'react';
|
||||
|
||||
export default function Page() {
|
||||
const [eventos, setEventos] = useState<GetCuestionario[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function getEventos() {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/cuestionario`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Error ${res.status}: ${res.statusText}`);
|
||||
}
|
||||
|
||||
const data: GetCuestionario[] = await res.json();
|
||||
setEventos(data);
|
||||
} catch (err) {
|
||||
console.error('Error en getEventos:', err);
|
||||
setError('No se pudieron cargar los cuestionarios.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
getEventos();
|
||||
}, []);
|
||||
|
||||
function slugify(text: string): string {
|
||||
return text
|
||||
.toString()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
if (loading)
|
||||
return <div className="text-center">Cargando cuestionarios...</div>;
|
||||
if (error) return <div>{error}</div>;
|
||||
const { loading, data } = useGetApi<GetEventoWithCuestionarios[]>(
|
||||
'/evento/activos/cuestionarios'
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="mx-auto mb-5 mt-3 fade-in-down-bounce">
|
||||
<ClientCarousel />
|
||||
<ClientCarousel
|
||||
images={data?.map((item) => ({
|
||||
src: item.banner
|
||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${item.banner}`
|
||||
: '/default-banner.jpg',
|
||||
alt: item.nombre_evento,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div className="cards-1 mt-5">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
{eventos.map((cuestionario, index) => {
|
||||
const slug = slugify(cuestionario.nombre_form);
|
||||
const url = `/registro/${slug}-${cuestionario.id_cuestionario}`;
|
||||
const fadeClass = `delay-${(index % 5) + 1}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`col-md-6 col-lg-4 my-4 fade-in-up-bounce ${fadeClass}`}
|
||||
key={cuestionario.id_cuestionario}
|
||||
>
|
||||
<FormularioCard
|
||||
cuestionario={cuestionario}
|
||||
link={url}
|
||||
button_message="Registrarse"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{loading && <div>Cargando...</div>}
|
||||
{data && (
|
||||
<div className="row">
|
||||
{data.map((item, key) => (
|
||||
<div className="col-6 col-md-4" key={key}>
|
||||
<EventoCard evento={item} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
'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<CuestionarioResponse | null>(
|
||||
null
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await axiosInstance.get<CuestionarioResponse>(
|
||||
`/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 <p>Cargando...</p>;
|
||||
if (error) return <p className="text-danger">{error}</p>;
|
||||
if (!cuestionario) return <p>No hay datos</p>;
|
||||
|
||||
return (
|
||||
<div className="container py-4">
|
||||
<Button icon="arrow-left" onClick={() => router.back()}>
|
||||
Volver
|
||||
</Button>
|
||||
<div className="text-center mb-4 mt-2">
|
||||
<Image
|
||||
src={'/feriasex.png'}
|
||||
width={1000}
|
||||
height={300}
|
||||
alt="Ejemplo de banner"
|
||||
className="rounded-4 shadow-sm img-fluid"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center',
|
||||
height: 300,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-lg-5 mx-lg-5">
|
||||
<h2>{cuestionario.cuestionario.nombre_form}</h2>
|
||||
<p>{cuestionario.cuestionario.descripcion}</p>
|
||||
|
||||
<Formulario
|
||||
id_formulario={cuestionario.cuestionario.id_cuestionario}
|
||||
secciones={cuestionario.cuestionario.secciones}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
import { CarouselProps } from '@/components/carousel';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
const Carousel = dynamic(() => import('@/components/carousel'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function ClientCarousel() {
|
||||
return <Carousel />;
|
||||
export default function ClientCarousel(props: CarouselProps) {
|
||||
return <Carousel {...props} />;
|
||||
}
|
||||
|
||||
+13
-13
@@ -1,7 +1,7 @@
|
||||
import Image from 'next/image';
|
||||
import React from 'react';
|
||||
|
||||
interface CarouselProps {
|
||||
export interface CarouselProps {
|
||||
id?: string;
|
||||
images?: {
|
||||
src: string;
|
||||
@@ -9,15 +9,9 @@ interface CarouselProps {
|
||||
}[];
|
||||
}
|
||||
|
||||
export default function Carousel({
|
||||
id = 'carouselExample',
|
||||
images = [
|
||||
{
|
||||
src: '/feriasex.png',
|
||||
alt: 'Imagen de ejemplo',
|
||||
},
|
||||
],
|
||||
}: CarouselProps) {
|
||||
export default function Carousel({ id, images }: CarouselProps) {
|
||||
console.log('Carousel images:', images);
|
||||
|
||||
if (!images || images.length === 0) {
|
||||
return <div>No hay imágenes para mostrar</div>;
|
||||
}
|
||||
@@ -31,11 +25,17 @@ export default function Carousel({
|
||||
className={`carousel-item ${index === 0 ? 'active' : ''}`}
|
||||
>
|
||||
<Image
|
||||
src={img.src}
|
||||
width={1600}
|
||||
height={535}
|
||||
src={img.src ? img.src : '/default-banner.png'}
|
||||
width={1200}
|
||||
height={400}
|
||||
alt="Ejemplo de banner"
|
||||
className="d-block w-100 rounded-3 img-fluid"
|
||||
style={{
|
||||
height: '400px',
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center',
|
||||
maxHeight: '400px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { GetEventoWithCuestionarios } from '@/types/evento';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import React, { useState } from 'react';
|
||||
import MarkdownRenderer from './markdown-render';
|
||||
|
||||
export default function EventoCard({
|
||||
evento,
|
||||
}: {
|
||||
evento: GetEventoWithCuestionarios;
|
||||
}) {
|
||||
const [verMas, setVerMas] = useState(false);
|
||||
const descripcion = evento.descripcion_evento;
|
||||
const limite = 100;
|
||||
|
||||
const descripcionRecortada =
|
||||
descripcion.length > limite && !verMas
|
||||
? `${descripcion.slice(0, limite)}...`
|
||||
: descripcion;
|
||||
|
||||
return (
|
||||
<div className="card card-blog d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<div className="card-image scale-hover-1">
|
||||
<Link href={`/evento/${evento.id_evento}`}>
|
||||
<Image
|
||||
width={400}
|
||||
height={300}
|
||||
className="img-fluid"
|
||||
src={`${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`}
|
||||
alt="Banner formulario"
|
||||
/>
|
||||
<div className="card-caption">{evento.nombre_evento}</div>
|
||||
</Link>
|
||||
<div className="ripple-cont"></div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="card-description mb-0">
|
||||
<MarkdownRenderer markdown={descripcionRecortada} />
|
||||
{descripcion.length > limite && (
|
||||
<button
|
||||
onClick={() => setVerMas(!verMas)}
|
||||
className="btn btn-link btn-sm p-0 ms-1 align-baseline"
|
||||
style={{ fontSize: '0.875rem' }}
|
||||
>
|
||||
{verMas ? 'Ver menos' : 'Ver más'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{evento.cuestionarios.length > 0 &&
|
||||
evento.cuestionarios.map((cuestionario) => (
|
||||
<div key={cuestionario.id_cuestionario} className="">
|
||||
<Link
|
||||
href={`/evento/${evento.nombre_evento.split(' ').join('_')}/${evento.id_evento}/${cuestionario.id_cuestionario}`}
|
||||
className="btn mt-2 w-100 btn-outline-primary"
|
||||
>
|
||||
{cuestionario.nombre_form}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
type MarkdownRendererProps = {
|
||||
markdown: string;
|
||||
};
|
||||
|
||||
const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ markdown }) => (
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
p: ({ ...props }) => <p style={{ margin: 0 }} {...props} />,
|
||||
}}
|
||||
>
|
||||
{markdown}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
|
||||
export default MarkdownRenderer;
|
||||
@@ -1,416 +0,0 @@
|
||||
'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';
|
||||
import { getAxiosError } from '@/utils/errors-utils';
|
||||
|
||||
interface GetAlumnoResponse {
|
||||
id_ncuenta: number;
|
||||
nombre: string;
|
||||
apellidos: string;
|
||||
carrera: string;
|
||||
genero: 'M' | 'F';
|
||||
}
|
||||
|
||||
export default function Formulario({
|
||||
id_formulario,
|
||||
secciones,
|
||||
}: {
|
||||
id_formulario: number;
|
||||
secciones: CuestionarioSeccion[];
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [esDeFES, setEsDeFES] = useState<boolean | null>(null);
|
||||
const [cuenta, setCuenta] = useState('');
|
||||
const [datosAuto, setDatosAuto] = useState<null | {
|
||||
nombre: string;
|
||||
apellidos: string;
|
||||
correo: string;
|
||||
genero: 'M' | 'F';
|
||||
carrera: string;
|
||||
}>(null);
|
||||
const [respuestas, setRespuestas] = useState<Record<string, string>>({});
|
||||
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) {
|
||||
(async () => {
|
||||
try {
|
||||
setDatosAuto(null);
|
||||
const { data } = await axiosInstance.get<GetAlumnoResponse>(
|
||||
`/alumnos/${cuenta}`
|
||||
);
|
||||
console.log('Datos del alumno:', data);
|
||||
if (data) {
|
||||
setDatosAuto({
|
||||
nombre: data.nombre.toString(),
|
||||
apellidos: data.apellidos.toString(),
|
||||
correo: data.id_ncuenta + '@pcpuma.acatlan.unam.mx',
|
||||
genero: data.genero,
|
||||
carrera: data.carrera,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = getAxiosError(err);
|
||||
toast.error(msg.message);
|
||||
setDatosAuto(null);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [cuenta, esDeFES]);
|
||||
|
||||
useEffect(() => {
|
||||
if (esDeFES && datosAuto) {
|
||||
const nuevasRespuestas: Record<string, string> = {};
|
||||
|
||||
secciones.forEach((seccion) => {
|
||||
seccion.preguntas.forEach(({ pregunta }) => {
|
||||
const id = pregunta.id_pregunta;
|
||||
|
||||
if (pregunta.validacion === 'nombre') {
|
||||
nuevasRespuestas[`pregunta_${id}`] = pregunta.pregunta
|
||||
.toLowerCase()
|
||||
.includes('apellido')
|
||||
? datosAuto.apellidos.toString()
|
||||
: datosAuto.nombre.toString();
|
||||
}
|
||||
|
||||
if (pregunta.validacion === 'correo') {
|
||||
nuevasRespuestas[`pregunta_${id}`] = datosAuto.correo;
|
||||
}
|
||||
|
||||
if (pregunta.validacion === 'cuenta_alumno') {
|
||||
nuevasRespuestas[`pregunta_${id}`] = cuenta;
|
||||
}
|
||||
|
||||
if (
|
||||
pregunta.pregunta
|
||||
.toLowerCase()
|
||||
.includes('institución de procedencia')
|
||||
) {
|
||||
nuevasRespuestas[`pregunta_${id}`] = 'FES Acatlán';
|
||||
}
|
||||
|
||||
if (pregunta.pregunta.toLowerCase().includes('carrera')) {
|
||||
nuevasRespuestas[`pregunta_${id}`] = datosAuto.carrera;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setRespuestas((prev) => ({
|
||||
...prev,
|
||||
...nuevasRespuestas,
|
||||
}));
|
||||
}
|
||||
}, [datosAuto, esDeFES, cuenta, secciones]);
|
||||
|
||||
// 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 }[] = [];
|
||||
|
||||
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;
|
||||
|
||||
console.log('Validando pregunta:', pregunta.pregunta, ' ---------- ');
|
||||
console.table({
|
||||
id,
|
||||
valor,
|
||||
tipo,
|
||||
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 (
|
||||
<form className="container py-4">
|
||||
{/* Pregunta inicial */}
|
||||
{preguntaFES && (
|
||||
<div className="mb-4">
|
||||
<label className="form-label">{preguntaFES.pregunta}</label>
|
||||
<RadioOptionGroup
|
||||
name="comunidad_fes"
|
||||
options={preguntaFES.opciones.map((op) => ({
|
||||
label: op.opcion.opcion,
|
||||
value: op.id_opcion,
|
||||
}))}
|
||||
selectedValue={respuestaFES ? Number(respuestaFES) : undefined}
|
||||
onChange={(opt) => {
|
||||
const id = opt.value;
|
||||
const value = opt?.label ?? '';
|
||||
|
||||
setEsDeFES(value === 'Si'); // Se actualiza esDeFES con el texto
|
||||
setCuenta('');
|
||||
setDatosAuto(null);
|
||||
|
||||
actualizarRespuesta(
|
||||
`pregunta_${preguntaFES.id_pregunta}`,
|
||||
String(id)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Si dijo que sí, pedir número de cuenta */}
|
||||
{esDeFES && preguntaCuenta && (
|
||||
<Input
|
||||
key={preguntaCuenta.id_pregunta}
|
||||
label={preguntaCuenta.pregunta}
|
||||
name={`pregunta_${preguntaCuenta.id_pregunta}`}
|
||||
value={cuenta}
|
||||
maxLength={10}
|
||||
onChange={(e) => {
|
||||
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;
|
||||
}
|
||||
if (p.validacion === 'cuenta_alumno') {
|
||||
defaultValue = cuenta;
|
||||
disabled = true;
|
||||
}
|
||||
if (
|
||||
p.pregunta.toLowerCase().includes('institución de procedencia')
|
||||
) {
|
||||
defaultValue = 'FES Acatlán';
|
||||
disabled = true;
|
||||
}
|
||||
if (p.pregunta.toLowerCase().includes('carrera')) {
|
||||
defaultValue = datosAuto.carrera;
|
||||
disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key={p.id_pregunta}>
|
||||
<Input
|
||||
label={p.pregunta}
|
||||
name={`pregunta_${p.id_pregunta}`}
|
||||
defaultValue={defaultValue}
|
||||
disabled={disabled}
|
||||
onChange={(e) => {
|
||||
console.log('Respuesta:', e.target.value);
|
||||
actualizarRespuesta(
|
||||
`pregunta_${p.id_pregunta}`,
|
||||
e.target.value
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{p.validacion === 'correo' && (
|
||||
<div className="alert alert-info">
|
||||
Este es el correo donde enviaremos la validación de registro{' '}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (p.tipo_pregunta.tipo_pregunta === 'Cerrada') {
|
||||
const opciones: RadioOption<number>[] = p.opciones.map((op) => ({
|
||||
label: op.opcion.opcion,
|
||||
value: op.id_opcion,
|
||||
}));
|
||||
|
||||
let respuestaActual = respuestas[`pregunta_${p.id_pregunta}`];
|
||||
|
||||
// Si es de FES y hay datos automáticos, intentar preseleccionar género
|
||||
if (
|
||||
esDeFES &&
|
||||
datosAuto &&
|
||||
!respuestaActual &&
|
||||
p.pregunta.toLowerCase().includes('género')
|
||||
) {
|
||||
const generoTexto =
|
||||
datosAuto.genero === 'M' ? 'Masculino' : 'Femenino';
|
||||
const opcionGenero = p.opciones.find(
|
||||
(op) =>
|
||||
op.opcion.opcion.toLowerCase() === generoTexto.toLowerCase()
|
||||
);
|
||||
if (opcionGenero) {
|
||||
respuestaActual = String(opcionGenero.id_opcion);
|
||||
actualizarRespuesta(
|
||||
`pregunta_${p.id_pregunta}`,
|
||||
String(respuestaActual)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={p.id_pregunta}>
|
||||
<label className="form-label">{p.pregunta}</label>
|
||||
<RadioOptionGroup
|
||||
name={`pregunta_${p.id_pregunta}`}
|
||||
options={opciones}
|
||||
selectedValue={
|
||||
respuestaActual ? Number(respuestaActual) : undefined
|
||||
}
|
||||
onChange={(opt) =>
|
||||
actualizarRespuesta(
|
||||
`pregunta_${p.id_pregunta}`,
|
||||
String(opt.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
|
||||
{/* Botón de enviar */}
|
||||
<Button onClick={formatearRespuestas} disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Enviando...' : 'Enviar'}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import MarkdownRenderer from '@/components/markdown-render';
|
||||
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';
|
||||
|
||||
type UsuarioData = {
|
||||
nombre: string;
|
||||
apellidos: string;
|
||||
correo: string;
|
||||
genero: 'M' | 'F';
|
||||
carrera: 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
|
||||
|
||||
export default function FormularioRegistro({
|
||||
id_cuestionario,
|
||||
}: {
|
||||
id_cuestionario: number;
|
||||
}) {
|
||||
// ------------------------
|
||||
// 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'
|
||||
);
|
||||
|
||||
// ------------------------
|
||||
// Efecto: buscar información de cuenta
|
||||
// ------------------------
|
||||
useEffect(() => {
|
||||
const cuenta = respuestas[preguntaCuenta?.id_pregunta ?? ''];
|
||||
|
||||
const esCuentaAlumno = preguntaCuenta?.validacion === 'cuenta_alumno';
|
||||
const esCuentaTrabajador =
|
||||
preguntaCuenta?.validacion === 'cuenta_trabajador';
|
||||
|
||||
const puedeBuscar =
|
||||
cuenta &&
|
||||
typeof cuenta === 'string' &&
|
||||
((esCuentaAlumno && cuenta.length === 9) || esCuentaTrabajador);
|
||||
|
||||
const fetchCuentaInfo = async () => {
|
||||
try {
|
||||
const endpoint = esCuentaAlumno
|
||||
? `/alumnos/${cuenta}`
|
||||
: `/trabajadores/${cuenta}`;
|
||||
|
||||
console.log('Buscando información de cuenta:', endpoint);
|
||||
|
||||
// Usar datos fake para pruebas
|
||||
setCuentaInfo({
|
||||
nombre: 'Juan',
|
||||
apellidos: 'Pérez',
|
||||
correo: '421010301@pcpuma.acatlan.unam.mx',
|
||||
genero: 'M',
|
||||
carrera: 'Ingeniería',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos de cuenta:', error);
|
||||
setCuentaInfo(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (puedeBuscar) {
|
||||
fetchCuentaInfo();
|
||||
} else {
|
||||
setCuentaInfo(null);
|
||||
}
|
||||
}, [respuestas[preguntaCuenta?.id_pregunta ?? '']]);
|
||||
|
||||
// ------------------------
|
||||
// Efecto: precargar respuestas si hay cuentaInfo
|
||||
// ------------------------
|
||||
useEffect(() => {
|
||||
if (!cuentaInfo) {
|
||||
if (preguntas) {
|
||||
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 (preguntas) {
|
||||
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]);
|
||||
|
||||
// ------------------------
|
||||
// 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
|
||||
);
|
||||
|
||||
if (faltantes.length > 0) {
|
||||
toast.error('Por favor responde todas las preguntas obligatorias.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Buscar un posible correo en las respuestas si no hay cuentaInfo
|
||||
let correo = cuentaInfo?.correo;
|
||||
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),
|
||||
})),
|
||||
};
|
||||
|
||||
console.log('Payload para enviar:', payload);
|
||||
|
||||
try {
|
||||
const res = await axiosInstance.post(
|
||||
'/cuestionario-respondido/submit',
|
||||
payload
|
||||
);
|
||||
toast.success(res.data.message || 'Formulario enviado correctamente');
|
||||
} catch (error) {
|
||||
console.log(getAxiosError(error));
|
||||
toast.error('Hubo un error al enviar el formulario');
|
||||
} finally {
|
||||
// Redirigir al usuario después de enviar el formulario
|
||||
router.push('/');
|
||||
}
|
||||
};
|
||||
|
||||
// ------------------------
|
||||
// Condición para mostrar el formulario
|
||||
// ------------------------
|
||||
const mostrarFormularioRestante =
|
||||
(isComunidad &&
|
||||
confirmativeOption.includes(isComunidad.label) &&
|
||||
!!cuentaInfo) ||
|
||||
(isComunidad && negativeOption.includes(isComunidad.label));
|
||||
|
||||
if (error) {
|
||||
console.error('Error al cargar el cuestionario:', error);
|
||||
return <div>Error al cargar el cuestionario</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<hr />
|
||||
<h2 className="text-xl font-bold">{data?.cuestionario.nombre_form}</h2>
|
||||
{data?.cuestionario.descripcion && (
|
||||
<MarkdownRenderer markdown={data.cuestionario.descripcion} />
|
||||
)}
|
||||
|
||||
{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.includes(isComunidad?.label) &&
|
||||
preguntaCuenta && (
|
||||
<div className="my-4">
|
||||
<label
|
||||
className={`form-label ${
|
||||
preguntaCuenta.obligatoria ? 'required' : ''
|
||||
}`}
|
||||
>
|
||||
{preguntaCuenta.pregunta}
|
||||
</label>
|
||||
<SimpleInput
|
||||
type="text"
|
||||
name="cuenta"
|
||||
placeholder="Ingrese su cuenta"
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
confirmativeOption.includes(isComunidad?.label) &&
|
||||
cuentaInfo &&
|
||||
respuestaActual === undefined // solo si no ha respondido
|
||||
) {
|
||||
if (pregunta.pregunta.validacion === 'genero') {
|
||||
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 ? 'Enviando...' : 'Enviar Formulario'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import SimpleInput from '@/components/input';
|
||||
|
||||
type Props = {
|
||||
idPregunta: number;
|
||||
enunciado: string;
|
||||
obligatorio?: boolean;
|
||||
validacion?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
respuestas: Record<string, any>;
|
||||
cuentaInfo: {
|
||||
nombre: string;
|
||||
apellidos: string;
|
||||
correo: string;
|
||||
genero: 'M' | 'F';
|
||||
carrera: string;
|
||||
} | null;
|
||||
isComunidad: string | undefined;
|
||||
onChange: (id: string, valor: string) => void;
|
||||
};
|
||||
|
||||
const confirmativeOption = ['Sí', 'Si', 'Si, claro', 'Claro que sí'];
|
||||
|
||||
export default function PrefetchAbiertaCorta({
|
||||
idPregunta,
|
||||
enunciado,
|
||||
validacion,
|
||||
respuestas,
|
||||
cuentaInfo,
|
||||
isComunidad,
|
||||
obligatorio,
|
||||
onChange,
|
||||
}: Props) {
|
||||
let defaultValue = '';
|
||||
let disabled = false;
|
||||
|
||||
if (confirmativeOption.includes(isComunidad ?? '') && cuentaInfo) {
|
||||
if (validacion === 'nombre') {
|
||||
defaultValue = cuentaInfo.nombre;
|
||||
disabled = true;
|
||||
}
|
||||
if (validacion === 'apellidos') {
|
||||
defaultValue = cuentaInfo.apellidos;
|
||||
disabled = true;
|
||||
}
|
||||
if (validacion === 'correo') {
|
||||
defaultValue = cuentaInfo.correo;
|
||||
disabled = true;
|
||||
}
|
||||
if (validacion === 'institucion') {
|
||||
defaultValue = 'FES Acatlán';
|
||||
disabled = true;
|
||||
}
|
||||
if (validacion === 'carrera') {
|
||||
defaultValue = cuentaInfo.carrera;
|
||||
disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-4">
|
||||
<label className={`form-label ${obligatorio ? 'required' : ''}`}>
|
||||
{enunciado}
|
||||
</label>
|
||||
<SimpleInput
|
||||
type="text"
|
||||
name={`respuesta_${idPregunta}`}
|
||||
placeholder="Ingrese una respuesta"
|
||||
value={respuestas[idPregunta] ?? defaultValue}
|
||||
disabled={disabled}
|
||||
onChange={(e) => onChange(idPregunta.toString(), e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -49,16 +49,19 @@ export const plantillasDisponibles: Plantilla[] = [
|
||||
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' },
|
||||
@@ -111,16 +114,19 @@ export const plantillasDisponibles: Plantilla[] = [
|
||||
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' },
|
||||
@@ -130,11 +136,13 @@ export const plantillasDisponibles: Plantilla[] = [
|
||||
{
|
||||
titulo: 'Institución de procedencia',
|
||||
tipo: 'Abierta (Respuesta corta)',
|
||||
validacion: 'institucion',
|
||||
obligatoria: true,
|
||||
},
|
||||
{
|
||||
titulo: 'Carrera',
|
||||
tipo: 'Abierta (Respuesta corta)',
|
||||
validacion: 'carrera',
|
||||
obligatoria: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import { getAxiosError } from '@/utils/errors-utils';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const useGetApi = <T,>(url: string, params?: Record<string, any>) => {
|
||||
const [data, setData] = useState<T>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<ErrorState | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await axiosInstance.get<T>(url, { params });
|
||||
setData(res.data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const message = getAxiosError(err);
|
||||
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [url, params]);
|
||||
|
||||
return { data, setData, loading, error };
|
||||
};
|
||||
Vendored
+27
-2
@@ -30,6 +30,12 @@ $theme-colors: map-merge($theme-colors, $custom-colors);
|
||||
@import 'animation';
|
||||
@import './global.scss';
|
||||
|
||||
.carousel-control-prev-icon,
|
||||
.carousel-control-next-icon {
|
||||
filter: invert(100%) sepia(100%) saturate(0%) hue-rotate(0deg)
|
||||
brightness(200%);
|
||||
}
|
||||
|
||||
//Add additional custom code here
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
@@ -73,6 +79,7 @@ ul.list-unstyled li {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: -1.5rem 1rem 0;
|
||||
cursor: pointer;
|
||||
@extend .rounded;
|
||||
@extend .shadow;
|
||||
|
||||
@@ -83,7 +90,7 @@ ul.list-unstyled li {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 80%; // ajusta según el tamaño deseado
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.4), transparent);
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.5), transparent);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -98,13 +105,20 @@ ul.list-unstyled li {
|
||||
|
||||
.card-caption {
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
bottom: 5px;
|
||||
left: 15px;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
text-shadow: 0 2px 5px rgba(33, 33, 33, 0.5);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
> p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
@@ -139,3 +153,14 @@ ul.list-unstyled li {
|
||||
input {
|
||||
--bs-body-bg: #fff;
|
||||
}
|
||||
|
||||
.required {
|
||||
position: relative;
|
||||
&::after {
|
||||
content: '*';
|
||||
position: absolute;
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
Vendored
+5
-1
@@ -4,11 +4,15 @@ export type TiposValidacion =
|
||||
| 'telefono'
|
||||
| 'nombre'
|
||||
| 'entero'
|
||||
| 'apellidos'
|
||||
| 'decimal'
|
||||
| 'comunidad_alumno'
|
||||
| 'cuenta_alumno'
|
||||
| 'comunidad_trabajador'
|
||||
| 'cuenta_trabajador';
|
||||
| 'cuenta_trabajador'
|
||||
| 'genero'
|
||||
| 'carrera'
|
||||
| 'institucion'
|
||||
|
||||
export type TipoPregunta =
|
||||
| 'Abierta (Respuesta corta)'
|
||||
|
||||
Vendored
+85
@@ -0,0 +1,85 @@
|
||||
import { TipoPregunta, TiposValidacion } from "./create-formulario";
|
||||
|
||||
export interface GetEvento {
|
||||
id_evento: number;
|
||||
nombre_evento: string;
|
||||
descripcion_evento: string;
|
||||
tipo_evento: string;
|
||||
fecha_inicio: string; // formato ISO 8601
|
||||
fecha_fin: string; // formato ISO 8601
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
asistencias: any | null;
|
||||
banner: string | null;
|
||||
}
|
||||
|
||||
export interface Cuestionario {
|
||||
id_cuestionario: number;
|
||||
nombre_form: string;
|
||||
banner: string | null;
|
||||
contador_secciones: number;
|
||||
descripcion: string;
|
||||
editable: boolean;
|
||||
fecha_inicio: string; // formato ISO 8601
|
||||
fecha_fin: string; // formato ISO 8601
|
||||
id_tipo_cuestionario: number;
|
||||
id_evento: number;
|
||||
tipoCuestionario: {
|
||||
id_tipo_cuestionario: number;
|
||||
tipo_cuestionario: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GetCuestionario {
|
||||
tipo_cuestionario: {
|
||||
id_tipo_cuestionario: number;
|
||||
tipo_cuestionario: string;
|
||||
};
|
||||
evento: GetEvento;
|
||||
cuestionario: CuestionarioWithSecciones;
|
||||
}
|
||||
|
||||
export interface CuestionarioWithSecciones extends Cuestionario {
|
||||
secciones: {
|
||||
id_cuestionario_seccion: number;
|
||||
posicion: number;
|
||||
seccion: {
|
||||
id_seccion: number;
|
||||
titulo: string;
|
||||
contador_pregunta: number;
|
||||
descripcion: string;
|
||||
};
|
||||
preguntas: {
|
||||
id_seccion_pregunta: number;
|
||||
posicion: number;
|
||||
pregunta: {
|
||||
id_pregunta: number;
|
||||
pregunta: string;
|
||||
contador_opcion: number;
|
||||
obligatoria: boolean;
|
||||
id_tipo_pregunta: number;
|
||||
validacion: TiposValidacion;
|
||||
tipo_pregunta: {
|
||||
id_tipo: number;
|
||||
tipo_pregunta: TipoPregunta;
|
||||
};
|
||||
opciones: {
|
||||
id_pregunta_opcion: number;
|
||||
posicion: number;
|
||||
id_opcion: number;
|
||||
opcion: {
|
||||
id_opcion: number;
|
||||
opcion: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface GetEventoWithCuestionarios extends GetEvento {
|
||||
cuestionarios: Cuestionario[];
|
||||
}
|
||||
|
||||
export interface GetEventoWithCuestionario extends GetEvento {
|
||||
cuestionario: Cuestionario;
|
||||
}
|
||||
Vendored
+2
-1
@@ -7,8 +7,9 @@ interface User {
|
||||
carrera?: string;
|
||||
}
|
||||
|
||||
|
||||
interface ErrorState {
|
||||
status: boolean;
|
||||
message: string;
|
||||
code: number;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user