Merge pull request 'develop' (#32) from develop into master
Reviewed-on: #32
This commit was merged in pull request #32.
This commit is contained in:
+3
-3
@@ -6,12 +6,12 @@ const nextConfig: NextConfig = {
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
port: '3342', //4200
|
||||
port: '4200', //4200
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'venus.acatlan.unam.mx'
|
||||
}
|
||||
hostname: 'venus.acatlan.unam.mx',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
Generated
+90
@@ -7403,6 +7403,96 @@
|
||||
"peerDependencies": {
|
||||
"@types/emscripten": ">=1.39.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.12.tgz",
|
||||
"integrity": "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.12.tgz",
|
||||
"integrity": "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.12.tgz",
|
||||
"integrity": "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.12.tgz",
|
||||
"integrity": "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.12.tgz",
|
||||
"integrity": "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.12.tgz",
|
||||
"integrity": "sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,224 @@
|
||||
'use client';
|
||||
|
||||
import React, { useRef, useState } from 'react';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import { Button, Form, Alert } from 'react-bootstrap';
|
||||
import { FaUpload, FaDownload } from 'react-icons/fa';
|
||||
|
||||
type TipoCarga = 'alumnos' | 'trabajadores';
|
||||
|
||||
interface ResultadoCarga {
|
||||
insertados: number;
|
||||
omitidos: number;
|
||||
}
|
||||
|
||||
interface SeccionState {
|
||||
archivo: File | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
success: string | null;
|
||||
resultado: ResultadoCarga | null;
|
||||
}
|
||||
|
||||
const initialState: SeccionState = {
|
||||
archivo: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
success: null,
|
||||
resultado: null,
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const [alumnos, setAlumnos] = useState<SeccionState>(initialState);
|
||||
const [trabajadores, setTrabajadores] = useState<SeccionState>(initialState);
|
||||
|
||||
const alumnosRef = useRef<HTMLInputElement>(null);
|
||||
const trabajadoresRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const getState = (tipo: TipoCarga) =>
|
||||
tipo === 'alumnos' ? alumnos : trabajadores;
|
||||
const setState = (tipo: TipoCarga) =>
|
||||
tipo === 'alumnos' ? setAlumnos : setTrabajadores;
|
||||
|
||||
const validarExt = (file: File | null): boolean => {
|
||||
if (!file) return false;
|
||||
return /\.xlsx$/i.test(file.name);
|
||||
};
|
||||
|
||||
const handleArchivoChange = (tipo: TipoCarga, file: File | null) => {
|
||||
if (file && !validarExt(file)) {
|
||||
setState(tipo)((prev) => ({
|
||||
...prev,
|
||||
archivo: null,
|
||||
error: 'Selecciona un archivo .xlsx válido',
|
||||
success: null,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
setState(tipo)((prev) => ({
|
||||
...prev,
|
||||
archivo: file,
|
||||
error: null,
|
||||
success: null,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleCargaMasiva = async (tipo: TipoCarga) => {
|
||||
const estado = getState(tipo);
|
||||
if (!estado.archivo) {
|
||||
setState(tipo)((prev) => ({
|
||||
...prev,
|
||||
error: 'Selecciona un archivo xlsx',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', estado.archivo);
|
||||
setState(tipo)((prev) => ({
|
||||
...prev,
|
||||
loading: true,
|
||||
error: null,
|
||||
success: null,
|
||||
}));
|
||||
try {
|
||||
const res = await axiosInstance.post(`/${tipo}/cargar`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
setState(tipo)((prev) => ({
|
||||
...prev,
|
||||
archivo: null,
|
||||
success: `${tipo === 'alumnos' ? 'Alumnos' : 'Trabajadores'} cargados exitosamente`,
|
||||
resultado: res.data ?? null,
|
||||
}));
|
||||
const ref = tipo === 'alumnos' ? alumnosRef : trabajadoresRef;
|
||||
if (ref.current) ref.current.value = '';
|
||||
} catch (error) {
|
||||
const err = error as { response?: { data?: { message?: string } } };
|
||||
setState(tipo)((prev) => ({
|
||||
...prev,
|
||||
error: err?.response?.data?.message || 'Error al cargar el archivo',
|
||||
}));
|
||||
} finally {
|
||||
setState(tipo)((prev) => ({ ...prev, loading: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDescargarPlantilla = (tipo: TipoCarga) => {
|
||||
const nombre = tipo === 'alumnos' ? 'carga-alumnos' : 'carga-trabajadores';
|
||||
const link = document.createElement('a');
|
||||
link.href = `/plantillas/${nombre}.xlsx`;
|
||||
link.setAttribute('download', `${nombre}.xlsx`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
};
|
||||
|
||||
const SeccionCarga = ({
|
||||
tipo,
|
||||
label,
|
||||
}: {
|
||||
tipo: TipoCarga;
|
||||
label: string;
|
||||
}) => {
|
||||
const estado = getState(tipo);
|
||||
const inputRef = tipo === 'alumnos' ? alumnosRef : trabajadoresRef;
|
||||
|
||||
return (
|
||||
<div className="p-4 border rounded bg-light mb-4">
|
||||
<h2 className="mb-3">{label}</h2>
|
||||
|
||||
{estado.error && (
|
||||
<Alert
|
||||
variant="danger"
|
||||
onClose={() => setState(tipo)((p) => ({ ...p, error: null }))}
|
||||
dismissible
|
||||
>
|
||||
{estado.error}
|
||||
</Alert>
|
||||
)}
|
||||
{estado.success && (
|
||||
<Alert
|
||||
variant="success"
|
||||
onClose={() =>
|
||||
setState(tipo)((p) => ({ ...p, success: null, resultado: null }))
|
||||
}
|
||||
dismissible
|
||||
>
|
||||
<p className="mb-1">{estado.success}</p>
|
||||
{estado.resultado && (
|
||||
<ul className="mb-0">
|
||||
<li>
|
||||
Insertados: <strong>{estado.resultado.insertados}</strong>
|
||||
</li>
|
||||
<li>
|
||||
Omitidos: <strong>{estado.resultado.omitidos}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Archivo Excel (.xlsx)</Form.Label>
|
||||
<div
|
||||
className="border p-4 text-center rounded"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
<FaUpload size={36} className="mb-2 text-secondary" />
|
||||
<p className="mb-0">
|
||||
{estado.archivo?.name ||
|
||||
'Arrastra aquí tu archivo o haz click para buscar'}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept=".xlsx"
|
||||
style={{ display: 'none' }}
|
||||
onChange={(e) =>
|
||||
handleArchivoChange(tipo, e.target.files?.[0] ?? null)
|
||||
}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<div className="d-flex gap-2 flex-wrap">
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={estado.loading || !estado.archivo}
|
||||
onClick={() => handleCargaMasiva(tipo)}
|
||||
>
|
||||
{estado.loading ? 'Cargando...' : 'Cargar archivo'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline-success"
|
||||
onClick={() => handleDescargarPlantilla(tipo)}
|
||||
>
|
||||
<FaDownload className="me-2" />
|
||||
Descargar plantilla
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container-fluid my-4">
|
||||
<div className="row mb-4">
|
||||
<div className="col-12">
|
||||
<div className="p-4 border rounded bg-light">
|
||||
<h1 className="mb-1">Carga masiva de usuarios</h1>
|
||||
<p className="mb-0 text-muted">
|
||||
Sube un archivo Excel para registrar alumnos o trabajadores en el
|
||||
sistema.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SeccionCarga tipo="alumnos" label="Carga de alumnos" />
|
||||
<SeccionCarga tipo="trabajadores" label="Carga de trabajadores" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
import { plantillasDisponibles } from '@/data/plantillas';
|
||||
import { FormularioCreacion } from '@/types/create-formulario';
|
||||
import Image from 'next/image';
|
||||
import React, { useState } from 'react';
|
||||
import CreateFormulario from '@/containers/create-formulario';
|
||||
import FormularioCardPreview from '@/components/formulario/formulario-card-preview';
|
||||
@@ -193,21 +192,18 @@ export default function Page() {
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div className="d-flex gap-4 flex-wrap">
|
||||
<div className="d-flex gap-2 flex-wrap">
|
||||
{plantillasDisponibles.map((plantilla) => (
|
||||
<div
|
||||
className="mb-3 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
key={plantilla.id}
|
||||
onClick={() => handleSeleccionarPlantilla(plantilla.id)}
|
||||
>
|
||||
<div className="text-center">
|
||||
<Image
|
||||
src={plantilla.imagen}
|
||||
width={200}
|
||||
height={200}
|
||||
alt={`Plantilla de formulario ${plantilla.nombre}`}
|
||||
className="img-fluid rounded shadow-sm"
|
||||
/>
|
||||
<div
|
||||
className="d-flex align-items-center justify-content-center rounded shadow-sm border text-center fw-semibold bg-azul text-white"
|
||||
style={{ padding: '1rem' }}
|
||||
>
|
||||
{plantilla.nombre}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -27,6 +27,40 @@ export default function Page() {
|
||||
{}
|
||||
);
|
||||
|
||||
type TipoCarga = 'alumnos' | 'trabajadores';
|
||||
const [tipoCarga, setTipoCarga] = useState<TipoCarga>('alumnos');
|
||||
const [archivoCarga, setArchivoCarga] = useState<File | null>(null);
|
||||
const [loadingCarga, setLoadingCarga] = useState(false);
|
||||
|
||||
const handleCargaMasiva = async () => {
|
||||
if (!archivoCarga) {
|
||||
toast.error('Selecciona un archivo xlsx');
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', archivoCarga);
|
||||
formData.append('id_evento', params.id_evento);
|
||||
setLoadingCarga(true);
|
||||
try {
|
||||
await axiosInstance.post(
|
||||
`/evento/${params.id_evento}/usuarios`,
|
||||
formData,
|
||||
{
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
}
|
||||
);
|
||||
toast.success(
|
||||
`${tipoCarga === 'alumnos' ? 'Alumnos' : 'Trabajadores'} cargados exitosamente`
|
||||
);
|
||||
setArchivoCarga(null);
|
||||
} catch (error) {
|
||||
const err = error as { response?: { data?: { message?: string } } };
|
||||
toast.error(err?.response?.data?.message || 'Error al cargar el archivo');
|
||||
} finally {
|
||||
setLoadingCarga(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (
|
||||
field: keyof CreateEventoType,
|
||||
value: string | Date
|
||||
@@ -88,8 +122,61 @@ export default function Page() {
|
||||
handleOnChange={handleEventoActualizado}
|
||||
/>
|
||||
|
||||
{/* Carga masiva de alumnos o trabajadores */}
|
||||
{tipo_usuario !== 'staff' && (
|
||||
<div className="p-4 border rounded bg-light mb-4">
|
||||
<h2 className="mb-1">Carga masiva de participantes</h2>
|
||||
<p className="text-muted mb-3">
|
||||
Sube un archivo <strong>.xlsx</strong> para registrar los
|
||||
participantes que podrán inscribirse a este evento. Solo los
|
||||
usuarios incluidos en la lista tendrán acceso al registro.
|
||||
</p>
|
||||
|
||||
<div className="d-flex gap-3 align-items-end flex-wrap">
|
||||
<div>
|
||||
<label className="form-label mb-1">Tipo de participante</label>
|
||||
<div className="d-flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn ${tipoCarga === 'alumnos' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||
onClick={() => setTipoCarga('alumnos')}
|
||||
>
|
||||
<i className="bi bi-mortarboard me-2"></i>Alumnos
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn ${tipoCarga === 'trabajadores' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||
onClick={() => setTipoCarga('trabajadores')}
|
||||
>
|
||||
<i className="bi bi-person-badge me-2"></i>Trabajadores
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow-1">
|
||||
<label className="form-label mb-1">Archivo xlsx</label>
|
||||
<input
|
||||
type="file"
|
||||
accept=".xlsx"
|
||||
className="form-control"
|
||||
onChange={(e) => setArchivoCarga(e.target.files?.[0] ?? null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
icon="upload"
|
||||
variant="success"
|
||||
onClick={handleCargaMasiva}
|
||||
disabled={loadingCarga || !archivoCarga}
|
||||
>
|
||||
{loadingCarga ? 'Cargando...' : 'Cargar'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Validacion del tipo de usuario */}
|
||||
{ tipo_usuario !== "staff" && (
|
||||
{tipo_usuario !== 'staff' && (
|
||||
<div className="p-4 border rounded bg-light">
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
@@ -103,7 +190,9 @@ export default function Page() {
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
onClick={() =>
|
||||
router.push(`/user/evento/${params.id_evento}/formularios/crear`)
|
||||
router.push(
|
||||
`/user/evento/${params.id_evento}/formularios/crear`
|
||||
)
|
||||
}
|
||||
>
|
||||
Crear nuevo formulario
|
||||
|
||||
@@ -29,6 +29,10 @@ const Navbar: React.FC = () => {
|
||||
href: '/user/eventos/carga_masiva_banner',
|
||||
label: 'Carga Masiva Imagenes',
|
||||
},
|
||||
{
|
||||
href: '/user/carga_masiva_usuarios',
|
||||
label: 'Carga Masiva Usuarios',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ href: '/user/usuarios', label: 'Usuarios' },
|
||||
|
||||
@@ -214,6 +214,13 @@ export default function FormularioEditor({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SimpleInput
|
||||
label="Mensaje de correo"
|
||||
value={formulario.mensaje_correo || ''}
|
||||
onChange={(e) => actualizarCampo('mensaje_correo', e.target.value)}
|
||||
placeholder="Mensaje que se enviará al correo del participante al registrarse"
|
||||
/>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<Select
|
||||
|
||||
@@ -55,6 +55,7 @@ export default function EditFormulario({
|
||||
fecha_inicio: cuestionario.fecha_inicio,
|
||||
fecha_fin: cuestionario.fecha_fin,
|
||||
cupo_maximo: Number(cuestionario.cupo_maximo) || null,
|
||||
mensaje_correo: cuestionario.mensaje_correo || null,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -147,6 +148,14 @@ export default function EditFormulario({
|
||||
}
|
||||
/>
|
||||
|
||||
<SimpleInput
|
||||
label="Mensaje de correo"
|
||||
value={cuestionario.mensaje_correo ?? ''}
|
||||
onChange={(e) =>
|
||||
handleChange('mensaje_correo', e.target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<SimpleInput
|
||||
label="Cupo máximo (Si no se requiere, dejar en blanco)"
|
||||
type="number"
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
'use client';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { SubmitResponse } from '@/types/submit';
|
||||
|
||||
interface Props {
|
||||
response: SubmitResponse;
|
||||
qrImageUrl: string | null;
|
||||
nombreEvento?: string;
|
||||
nombreFormulario?: string;
|
||||
}
|
||||
|
||||
export default function FormularioExito({
|
||||
response,
|
||||
qrImageUrl,
|
||||
nombreEvento,
|
||||
nombreFormulario,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="container my-5">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10 col-xl-8">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-body">
|
||||
<div className="row g-4">
|
||||
<div className="col-md-6">
|
||||
<div className="h-100 d-flex flex-column justify-content-center">
|
||||
<div className="text-center mb-4">
|
||||
<div className="mb-3">
|
||||
<div
|
||||
className="d-inline-flex align-items-center justify-content-center bg-success rounded-circle"
|
||||
style={{ width: '60px', height: '60px' }}
|
||||
>
|
||||
<i
|
||||
className="bi bi-check-lg text-white"
|
||||
style={{ fontSize: '2rem' }}
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="fw-bold text-success mb-2">¡Felicidades!</h2>
|
||||
<p className="text-muted mb-0">
|
||||
Has completado exitosamente el formulario
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{nombreEvento && (
|
||||
<div className="mb-3">
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<i
|
||||
className="bi bi-calendar-event text-primary me-3"
|
||||
style={{ fontSize: '1.3rem' }}
|
||||
></i>
|
||||
<h6 className="mb-0 text-primary">Evento</h6>
|
||||
</div>
|
||||
<p className="mb-0 fs-6 fw-semibold ms-5">{nombreEvento}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nombreFormulario && (
|
||||
<div className="mb-3">
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<i
|
||||
className="bi bi-file-text text-info me-3"
|
||||
style={{ fontSize: '1.3rem' }}
|
||||
></i>
|
||||
<h6 className="mb-0 text-info">Formulario</h6>
|
||||
</div>
|
||||
<p className="mb-0 fs-6 fw-semibold ms-5">{nombreFormulario}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{response.message && (
|
||||
<div className="mb-3">
|
||||
<div className="alert alert-info" role="alert">
|
||||
<div className="d-flex align-items-start">
|
||||
<i className="bi bi-info-circle me-2 mt-1"></i>
|
||||
<div>
|
||||
<strong>Mensaje:</strong>
|
||||
<p className="mb-0 mt-1">{response.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="h-100 d-flex flex-column justify-content-center align-items-center">
|
||||
{qrImageUrl ? (
|
||||
<div className="text-center">
|
||||
<h5 className="mb-3 text-secondary">
|
||||
<i className="bi bi-qr-code me-2"></i>
|
||||
Código QR
|
||||
</h5>
|
||||
<div className="p-3 bg-white rounded shadow-sm">
|
||||
<Image
|
||||
src={qrImageUrl}
|
||||
alt="Código QR"
|
||||
width={250}
|
||||
height={250}
|
||||
className="rounded"
|
||||
style={{ maxWidth: '100%', height: 'auto' }}
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-muted">
|
||||
<i
|
||||
className="bi bi-qr-code-scan"
|
||||
style={{ fontSize: '4rem' }}
|
||||
></i>
|
||||
<p className="mt-3 mb-0">No hay código QR disponible</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-4">
|
||||
<Link href="/" className="btn btn-primary btn-lg me-3">
|
||||
Ir al inicio
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useGetApi } from '@/hooks/use-get-api';
|
||||
import { GetCuestionario } from '@/types/evento';
|
||||
import React from 'react';
|
||||
import { SubmitResponse } from '@/types/submit';
|
||||
import SimpleInput from '@/components/input';
|
||||
import RadioOptionGroup, { RadioOption } from '@/components/radio-option-group';
|
||||
import Button from '@/components/button';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import toast from 'react-hot-toast';
|
||||
import { getAxiosError } from '@/utils/errors-utils';
|
||||
import { useFormulario } from '@/context/formulario';
|
||||
|
||||
export type UsuarioDataResponse = {
|
||||
cuenta: string | null;
|
||||
@@ -18,19 +15,7 @@ export type UsuarioDataResponse = {
|
||||
rfc?: string | null;
|
||||
};
|
||||
|
||||
type UsuarioData = {
|
||||
nombre: string;
|
||||
apellidos: string;
|
||||
correo: string;
|
||||
genero: 'M' | 'F';
|
||||
carrera: string;
|
||||
rfc?: string;
|
||||
};
|
||||
|
||||
type RespuestaFormulario = Record<string, string | number>;
|
||||
|
||||
export default function FormularioRegistro({
|
||||
id_cuestionario,
|
||||
handleSubmitFormulario,
|
||||
}: {
|
||||
evento: string;
|
||||
@@ -39,270 +24,61 @@ export default function FormularioRegistro({
|
||||
id_cuestionario: number;
|
||||
handleSubmitFormulario: (data: SubmitResponse) => void;
|
||||
}) {
|
||||
// Estados principales
|
||||
const [respuestas, setRespuestas] = useState<RespuestaFormulario>({});
|
||||
const [cuentaInfo, setCuentaInfo] = useState<UsuarioData | null>(null);
|
||||
const [busquedaCompletada, setBusquedaCompletada] = useState<boolean>(false);
|
||||
const [cuentaBuscada, setCuentaBuscada] = useState<string>('');
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
|
||||
// Carga del cuestionario
|
||||
const { data, error } = useGetApi<GetCuestionario>(
|
||||
`/cuestionario/${id_cuestionario}/formulario`
|
||||
);
|
||||
const {
|
||||
state,
|
||||
preguntaComunidad,
|
||||
preguntaCuenta,
|
||||
esComunidadSi,
|
||||
esExclusivo,
|
||||
mostrarFormularioCompleto,
|
||||
setRespuesta,
|
||||
submitFormulario,
|
||||
} = useFormulario();
|
||||
|
||||
// Extraer todas las preguntas del cuestionario
|
||||
const preguntas = data?.cuestionario.secciones.flatMap((seccion) =>
|
||||
seccion.preguntas.map((pregunta) => pregunta.pregunta)
|
||||
);
|
||||
const { data, loadingData, errorData, respuestas, isSending, errorBusqueda, loadingBusqueda } = state;
|
||||
|
||||
// Identificar preguntas especiales
|
||||
const preguntaComunidad = preguntas?.find(
|
||||
(pregunta) =>
|
||||
pregunta.validacion === 'comunidad_alumno' ||
|
||||
pregunta.validacion === 'comunidad_trabajador'
|
||||
);
|
||||
|
||||
const preguntaCuenta = preguntas?.find(
|
||||
(pregunta) =>
|
||||
pregunta.validacion === 'cuenta_alumno' ||
|
||||
pregunta.validacion === 'cuenta_trabajador' ||
|
||||
pregunta.validacion === 'rfc'
|
||||
);
|
||||
|
||||
// Determinar tipo de comunidad seleccionada
|
||||
const comunidadSeleccionada = preguntaComunidad
|
||||
? respuestas[preguntaComunidad.id_pregunta]
|
||||
const comunidadSeleccionada = preguntaComunidad
|
||||
? respuestas[preguntaComunidad.id_pregunta]
|
||||
: null;
|
||||
|
||||
const esComunidadSi = comunidadSeleccionada &&
|
||||
preguntaComunidad?.opciones.find(op =>
|
||||
op.id_opcion === Number(comunidadSeleccionada) &&
|
||||
op.opcion.opcion.toLowerCase().includes('si')
|
||||
);
|
||||
|
||||
// Efecto para buscar información de cuenta
|
||||
useEffect(() => {
|
||||
if (!preguntaCuenta || !esComunidadSi) {
|
||||
setCuentaInfo(null);
|
||||
setBusquedaCompletada(false);
|
||||
setCuentaBuscada('');
|
||||
return;
|
||||
}
|
||||
|
||||
const cuenta = respuestas[preguntaCuenta.id_pregunta];
|
||||
|
||||
if (!cuenta || typeof cuenta !== 'string') {
|
||||
setCuentaInfo(null);
|
||||
setBusquedaCompletada(false);
|
||||
setCuentaBuscada('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Determinar longitudes válidas según el tipo
|
||||
const longitudesValidas = {
|
||||
'cuenta_alumno': 9,
|
||||
'cuenta_trabajador': 6,
|
||||
'rfc': 10
|
||||
};
|
||||
|
||||
const longitudRequerida = longitudesValidas[preguntaCuenta.validacion as keyof typeof longitudesValidas];
|
||||
const puedeBuscar = cuenta.length === longitudRequerida;
|
||||
|
||||
// Solo buscar si no se ha buscado ya esta cuenta
|
||||
if (puedeBuscar && cuentaBuscada !== cuenta) {
|
||||
setBusquedaCompletada(false);
|
||||
|
||||
const fetchCuentaInfo = async () => {
|
||||
try {
|
||||
let endpoint: string;
|
||||
|
||||
switch (preguntaCuenta.validacion) {
|
||||
case 'cuenta_alumno':
|
||||
endpoint = `/alumnos/${cuenta}`;
|
||||
break;
|
||||
case 'cuenta_trabajador':
|
||||
endpoint = `/trabajadores/${cuenta}`;
|
||||
break;
|
||||
case 'rfc':
|
||||
endpoint = `/trabajadores/${cuenta}`;
|
||||
break;
|
||||
default:
|
||||
throw new Error('Tipo de validación no reconocido');
|
||||
}
|
||||
|
||||
const res = await axiosInstance.get<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 = {};
|
||||
|
||||
preguntas?.forEach((pregunta) => {
|
||||
const id = pregunta.id_pregunta;
|
||||
|
||||
switch (pregunta.validacion) {
|
||||
case 'nombre':
|
||||
nuevasRespuestas[id] = cuentaInfo.nombre;
|
||||
break;
|
||||
case 'apellidos':
|
||||
nuevasRespuestas[id] = cuentaInfo.apellidos;
|
||||
break;
|
||||
case 'correo':
|
||||
nuevasRespuestas[id] = cuentaInfo.correo;
|
||||
break;
|
||||
case 'carrera':
|
||||
nuevasRespuestas[id] = cuentaInfo.carrera;
|
||||
break;
|
||||
case 'genero':
|
||||
const generoTexto = cuentaInfo.genero === 'M' ? 'Masculino' : 'Femenino';
|
||||
const opcionGenero = pregunta.opciones?.find(
|
||||
(op) => op.opcion.opcion.toLowerCase() === generoTexto.toLowerCase()
|
||||
);
|
||||
if (opcionGenero) {
|
||||
nuevasRespuestas[id] = String(opcionGenero.id_opcion);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
setRespuestas(prev => ({ ...prev, ...nuevasRespuestas }));
|
||||
}, [cuentaInfo, data?.cuestionario.secciones, preguntas]);
|
||||
|
||||
// Función para actualizar respuestas
|
||||
const actualizarRespuesta = (idPregunta: string, valor: string) => {
|
||||
setRespuestas(prev => ({ ...prev, [idPregunta]: valor }));
|
||||
};
|
||||
|
||||
// Función para enviar formulario
|
||||
const handleOnSubmit = async () => {
|
||||
setIsSending(true);
|
||||
|
||||
if (!data?.cuestionario.id_cuestionario) {
|
||||
toast.error('No se ha cargado correctamente el formulario');
|
||||
setIsSending(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar preguntas obligatorias
|
||||
const preguntasObligatorias = data.cuestionario.secciones.flatMap((seccion) =>
|
||||
seccion.preguntas
|
||||
.filter((pregunta) => pregunta.pregunta.obligatoria)
|
||||
.map((pregunta) => pregunta.pregunta)
|
||||
.filter((p) => p.pregunta.obligatoria)
|
||||
.map((p) => p.pregunta)
|
||||
);
|
||||
|
||||
const faltantes = preguntasObligatorias.filter(
|
||||
(pregunta) =>
|
||||
respuestas[pregunta.id_pregunta] === undefined ||
|
||||
respuestas[pregunta.id_pregunta] === '' ||
|
||||
respuestas[pregunta.id_pregunta] === null
|
||||
(p) =>
|
||||
respuestas[p.id_pregunta] === undefined ||
|
||||
respuestas[p.id_pregunta] === '' ||
|
||||
respuestas[p.id_pregunta] === null
|
||||
);
|
||||
|
||||
if (faltantes.length > 0) {
|
||||
toast.error('Por favor responde todas las preguntas obligatorias.');
|
||||
setIsSending(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Buscar correo
|
||||
const preguntaCorreo = preguntas?.find(p =>
|
||||
p.validacion === 'correo' || p.validacion === 'correo_institucional'
|
||||
);
|
||||
|
||||
let correo = preguntaCorreo
|
||||
? String(respuestas[preguntaCorreo.id_pregunta] || '')
|
||||
: '';
|
||||
|
||||
if (!correo) {
|
||||
const correoRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
for (const valor of Object.values(respuestas)) {
|
||||
if (typeof valor === 'string' && correoRegex.test(valor)) {
|
||||
correo = valor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
id_cuestionario: id_cuestionario,
|
||||
correo: correo,
|
||||
fecha_envio: new Date().toISOString(),
|
||||
respuestas: Object.entries(respuestas).map(([id_pregunta, valor]) => ({
|
||||
id_pregunta: Number(id_pregunta),
|
||||
valor: isNaN(Number(valor)) ? valor : Number(valor),
|
||||
})),
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await axiosInstance.post<SubmitResponse>(
|
||||
'/cuestionario-respondido/submit',
|
||||
payload
|
||||
);
|
||||
|
||||
toast.success(res.data.message || '¡Formulario enviado exitosamente!');
|
||||
handleSubmitFormulario(res.data);
|
||||
} catch (error) {
|
||||
console.error('Error al enviar formulario:', getAxiosError(error));
|
||||
const res = await submitFormulario(data.cuestionario.id_cuestionario);
|
||||
toast.success(res.message || '¡Formulario enviado exitosamente!');
|
||||
handleSubmitFormulario(res);
|
||||
} catch {
|
||||
toast.error('Hubo un error al enviar el formulario');
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Condición para mostrar el resto del formulario
|
||||
const mostrarFormularioCompleto =
|
||||
!preguntaComunidad || // Sin pregunta de comunidad
|
||||
(esComunidadSi && busquedaCompletada) || // Con comunidad "Sí" y búsqueda completada
|
||||
(!esComunidadSi && comunidadSeleccionada); // Con comunidad "No"
|
||||
|
||||
if (error) {
|
||||
console.error('Error al cargar el cuestionario:', error);
|
||||
if (errorData) {
|
||||
return <div>Error al cargar el cuestionario</div>;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
if (loadingData || !data) {
|
||||
return <div>Cargando...</div>;
|
||||
}
|
||||
|
||||
@@ -321,15 +97,15 @@ export default function FormularioRegistro({
|
||||
value: op.id_opcion,
|
||||
}))}
|
||||
selectedValue={comunidadSeleccionada ? Number(comunidadSeleccionada) : undefined}
|
||||
onChange={(opt) => {
|
||||
actualizarRespuesta(preguntaComunidad.id_pregunta.toString(), String(opt.value));
|
||||
}}
|
||||
onChange={(opt) =>
|
||||
setRespuesta(preguntaComunidad.id_pregunta.toString(), String(opt.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pregunta de cuenta (si respondió "Sí" a comunidad) */}
|
||||
{esComunidadSi && preguntaCuenta && (
|
||||
{/* Pregunta de cuenta (si respondió "Sí" a comunidad, o en cuestionario exclusivo) */}
|
||||
{(esComunidadSi || esExclusivo) && preguntaCuenta && (
|
||||
<div className="my-4">
|
||||
<label className={`form-label ${preguntaCuenta.obligatoria ? 'required' : ''}`}>
|
||||
{preguntaCuenta.pregunta}
|
||||
@@ -341,107 +117,120 @@ export default function FormularioRegistro({
|
||||
preguntaCuenta.validacion === 'cuenta_alumno'
|
||||
? 'Ingrese su número de cuenta (9 dígitos)'
|
||||
: preguntaCuenta.validacion === 'cuenta_trabajador'
|
||||
? 'Ingrese su número de trabajador (6 dígitos)'
|
||||
: preguntaCuenta.validacion === 'rfc'
|
||||
? 'Ingrese su RFC sin homoclave (10 caracteres)'
|
||||
: `Ingrese su ${preguntaCuenta.pregunta}`
|
||||
? 'Ingrese su número de trabajador (6 dígitos)'
|
||||
: preguntaCuenta.validacion === 'rfc'
|
||||
? 'Ingrese su RFC sin homoclave (10 caracteres)'
|
||||
: `Ingrese su ${preguntaCuenta.pregunta}`
|
||||
}
|
||||
maxLength={
|
||||
preguntaCuenta.validacion === 'rfc'
|
||||
? 10
|
||||
: preguntaCuenta.validacion === 'cuenta_alumno'
|
||||
? 9
|
||||
: 6
|
||||
}
|
||||
maxLength={preguntaCuenta.validacion === 'rfc' ? 10 :
|
||||
preguntaCuenta.validacion === 'cuenta_alumno' ? 9 : 6}
|
||||
value={respuestas[preguntaCuenta.id_pregunta] || ''}
|
||||
onChange={(e) => actualizarRespuesta(
|
||||
preguntaCuenta.id_pregunta.toString(),
|
||||
e.target.value
|
||||
)}
|
||||
onChange={(e) =>
|
||||
setRespuesta(preguntaCuenta.id_pregunta.toString(), e.target.value)
|
||||
}
|
||||
/>
|
||||
{loadingBusqueda && (
|
||||
<div className="text-muted small mt-1">
|
||||
<span className="spinner-border spinner-border-sm me-1" />
|
||||
Buscando...
|
||||
</div>
|
||||
)}
|
||||
{errorBusqueda && (
|
||||
<div className="alert alert-danger py-2 mt-2" role="alert">
|
||||
<i className="bi bi-exclamation-circle me-2"></i>
|
||||
{errorBusqueda}
|
||||
</div>
|
||||
)}
|
||||
</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
|
||||
) {
|
||||
{mostrarFormularioCompleto &&
|
||||
data.cuestionario.secciones.map((seccion, i) => (
|
||||
<div key={i}>
|
||||
{seccion.preguntas.map((pregunta) => {
|
||||
if (
|
||||
pregunta.pregunta.id_pregunta === preguntaComunidad?.id_pregunta ||
|
||||
pregunta.pregunta.id_pregunta === preguntaCuenta?.id_pregunta
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const valor = respuestas[pregunta.pregunta.id_pregunta] || '';
|
||||
|
||||
if (pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Abierta (Respuesta corta)') {
|
||||
return (
|
||||
<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) =>
|
||||
setRespuesta(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={valor}
|
||||
onChange={(e) =>
|
||||
setRespuesta(pregunta.pregunta.id_pregunta.toString(), e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Cerrada' ||
|
||||
pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Multiple'
|
||||
) {
|
||||
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' : ''}`}>
|
||||
{pregunta.pregunta.pregunta}
|
||||
</label>
|
||||
<RadioOptionGroup
|
||||
name={`pregunta_${pregunta.pregunta.id_pregunta}`}
|
||||
options={opciones}
|
||||
selectedValue={valor ? Number(valor) : undefined}
|
||||
onChange={(opt) =>
|
||||
setRespuesta(pregunta.pregunta.id_pregunta.toString(), String(opt.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
|
||||
// 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' : ''}`}>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Botón de envío */}
|
||||
{mostrarFormularioCompleto && (
|
||||
@@ -456,16 +245,3 @@ export default function FormularioRegistro({
|
||||
</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
|
||||
}'
|
||||
*/
|
||||
@@ -4,10 +4,11 @@ import { useGetApi } from '@/hooks/use-get-api';
|
||||
import Image from 'next/image';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import FormularioRegistro from '../formulario/formulario-registro';
|
||||
import FormularioExito from '../formulario/formulario-exito';
|
||||
import { GetEventoWithCuestionario } from '@/types/evento';
|
||||
import { formatearRangoFechas } from '@/utils/date-utils';
|
||||
import { SubmitResponse } from '@/types/submit';
|
||||
import Link from 'next/link';
|
||||
import { FormularioProvider } from '@/context/formulario';
|
||||
|
||||
export default function FormularioPage({
|
||||
id_evento,
|
||||
@@ -64,121 +65,12 @@ export default function FormularioPage({
|
||||
// Si hay una respuesta exitosa, mostrar la vista de éxito
|
||||
if (response && response.success) {
|
||||
return (
|
||||
<div className="container my-5">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10 col-xl-8">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-body">
|
||||
<div className="row g-4">
|
||||
<div className="col-md-6">
|
||||
<div className="h-100 d-flex flex-column justify-content-center">
|
||||
<div className="text-center mb-4">
|
||||
<div className="mb-3">
|
||||
<div
|
||||
className="d-inline-flex align-items-center justify-content-center bg-success rounded-circle"
|
||||
style={{ width: '60px', height: '60px' }}
|
||||
>
|
||||
<i
|
||||
className="bi bi-check-lg text-white"
|
||||
style={{ fontSize: '2rem' }}
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="fw-bold text-success mb-2">
|
||||
¡Felicidades!
|
||||
</h2>
|
||||
<p className="text-muted mb-0">
|
||||
Has completado exitosamente el formulario
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<i
|
||||
className="bi bi-calendar-event text-primary me-3"
|
||||
style={{ fontSize: '1.3rem' }}
|
||||
></i>
|
||||
<h6 className="mb-0 text-primary">Evento</h6>
|
||||
</div>
|
||||
<p className="mb-0 fs-6 fw-semibold ms-5">
|
||||
{data?.nombre_evento}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<i
|
||||
className="bi bi-file-text text-info me-3"
|
||||
style={{ fontSize: '1.3rem' }}
|
||||
></i>
|
||||
<h6 className="mb-0 text-info">Formulario</h6>
|
||||
</div>
|
||||
<p className="mb-0 fs-6 fw-semibold ms-5">
|
||||
{data?.cuestionario.nombre_form}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{response.message && (
|
||||
<div className="mb-3">
|
||||
<div className="alert alert-info" role="alert">
|
||||
<div className="d-flex align-items-start">
|
||||
<i className="bi bi-info-circle me-2 mt-1"></i>
|
||||
<div>
|
||||
<strong>Mensaje:</strong>
|
||||
<p className="mb-0 mt-1">{response.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="h-100 d-flex flex-column justify-content-center align-items-center">
|
||||
{qrImageUrl ? (
|
||||
<div className="text-center">
|
||||
<h5 className="mb-3 text-secondary">
|
||||
<i className="bi bi-qr-code me-2"></i>
|
||||
Código QR
|
||||
</h5>
|
||||
<div className="p-3 bg-white rounded shadow-sm">
|
||||
<Image
|
||||
src={qrImageUrl}
|
||||
alt="Código QR"
|
||||
width={250}
|
||||
height={250}
|
||||
className="rounded"
|
||||
style={{ maxWidth: '100%', height: 'auto' }}
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-muted">
|
||||
<i
|
||||
className="bi bi-qr-code-scan"
|
||||
style={{ fontSize: '4rem' }}
|
||||
></i>
|
||||
<p className="mt-3 mb-0">
|
||||
No hay código QR disponible
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-4">
|
||||
<Link href="/" className="btn btn-primary btn-lg me-3">
|
||||
Ir al inicio
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FormularioExito
|
||||
response={response}
|
||||
qrImageUrl={qrImageUrl}
|
||||
nombreEvento={data?.nombre_evento}
|
||||
nombreFormulario={data?.cuestionario.nombre_form}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -353,13 +245,18 @@ export default function FormularioPage({
|
||||
<div className="col-lg-7">
|
||||
<div className="ps-lg-4">
|
||||
{data && (
|
||||
<FormularioRegistro
|
||||
evento={data.nombre_evento}
|
||||
cuestionario={data?.cuestionario.nombre_form}
|
||||
id_evento={data?.id_evento}
|
||||
id_cuestionario={data?.cuestionario.id_cuestionario}
|
||||
handleSubmitFormulario={onSubmitFormulario}
|
||||
/>
|
||||
<FormularioProvider
|
||||
id_evento={data.id_evento}
|
||||
id_cuestionario={data.cuestionario.id_cuestionario}
|
||||
>
|
||||
<FormularioRegistro
|
||||
evento={data.nombre_evento}
|
||||
cuestionario={data.cuestionario.nombre_form}
|
||||
id_evento={data.id_evento}
|
||||
id_cuestionario={data.cuestionario.id_cuestionario}
|
||||
handleSubmitFormulario={onSubmitFormulario}
|
||||
/>
|
||||
</FormularioProvider>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
'use client';
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useReducer,
|
||||
useEffect,
|
||||
useRef,
|
||||
useMemo,
|
||||
useCallback,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import { getAxiosError } from '@/utils/errors-utils';
|
||||
import { GetCuestionario } from '@/types/evento';
|
||||
import { UsuarioDataResponse } from '@/containers/formulario/formulario-registro';
|
||||
import {
|
||||
formularioReducer,
|
||||
initialState,
|
||||
FormularioState,
|
||||
RespuestaFormulario,
|
||||
} from './formulario-reducer';
|
||||
|
||||
// ── Longitudes requeridas por tipo de validación ─────────────────────────────
|
||||
|
||||
const LONGITUDES: Record<string, number> = {
|
||||
cuenta_alumno: 9,
|
||||
cuenta_trabajador: 6,
|
||||
rfc: 10,
|
||||
};
|
||||
|
||||
// ── Tipos del contexto ────────────────────────────────────────────────────────
|
||||
|
||||
interface FormularioContextType {
|
||||
state: FormularioState;
|
||||
|
||||
// Datos derivados estables
|
||||
preguntas: ReturnType<typeof getPreguntasFlat>;
|
||||
preguntaComunidad: PreguntaFlat | undefined;
|
||||
preguntaCuenta: PreguntaFlat | undefined;
|
||||
esComunidadSi: boolean;
|
||||
esExclusivo: boolean;
|
||||
mostrarFormularioCompleto: boolean;
|
||||
|
||||
// Acciones
|
||||
setRespuesta: (id: string, valor: string) => void;
|
||||
submitFormulario: (id_cuestionario: number) => Promise<import('@/types/submit').SubmitResponse>;
|
||||
}
|
||||
|
||||
// Helper para aplanar preguntas
|
||||
type PreguntaFlat = GetCuestionario['cuestionario']['secciones'][0]['preguntas'][0]['pregunta'];
|
||||
|
||||
function getPreguntasFlat(data: GetCuestionario | null): PreguntaFlat[] {
|
||||
if (!data) return [];
|
||||
return data.cuestionario.secciones.flatMap((s) =>
|
||||
s.preguntas.map((p) => p.pregunta)
|
||||
);
|
||||
}
|
||||
|
||||
// ── Context ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const FormularioContext = createContext<FormularioContextType | undefined>(undefined);
|
||||
|
||||
interface FormularioProviderProps {
|
||||
children: ReactNode;
|
||||
id_evento: number;
|
||||
id_cuestionario: number;
|
||||
}
|
||||
|
||||
export function FormularioProvider({
|
||||
children,
|
||||
id_evento,
|
||||
id_cuestionario,
|
||||
}: FormularioProviderProps) {
|
||||
const [state, dispatch] = useReducer(formularioReducer, initialState);
|
||||
// Rastrea si ya se limpiaron los campos para el usuarioEncontrado actual
|
||||
const yaLimpioRef = useRef(false);
|
||||
|
||||
// ── Carga del cuestionario ─────────────────────────────────────────────────
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'FETCH_DATA_START' });
|
||||
axiosInstance
|
||||
.get<GetCuestionario>(`/cuestionario/${id_cuestionario}/formulario`)
|
||||
.then((res) => dispatch({ type: 'FETCH_DATA_SUCCESS', payload: res.data }))
|
||||
.catch((err) => {
|
||||
const { message } = getAxiosError(err);
|
||||
dispatch({ type: 'FETCH_DATA_ERROR', payload: message });
|
||||
});
|
||||
}, [id_cuestionario]);
|
||||
|
||||
// ── Derivados estables ─────────────────────────────────────────────────────
|
||||
|
||||
const preguntas = useMemo(() => getPreguntasFlat(state.data), [state.data]);
|
||||
|
||||
const preguntaComunidad = useMemo(
|
||||
() =>
|
||||
preguntas.find(
|
||||
(p) =>
|
||||
p.validacion === 'comunidad_alumno' ||
|
||||
p.validacion === 'comunidad_trabajador'
|
||||
),
|
||||
[preguntas]
|
||||
);
|
||||
|
||||
const preguntaCuenta = useMemo(
|
||||
() =>
|
||||
preguntas.find(
|
||||
(p) =>
|
||||
p.validacion === 'cuenta_alumno' ||
|
||||
p.validacion === 'cuenta_trabajador' ||
|
||||
p.validacion === 'rfc'
|
||||
),
|
||||
[preguntas]
|
||||
);
|
||||
|
||||
const comunidadSeleccionada = preguntaComunidad
|
||||
? state.respuestas[preguntaComunidad.id_pregunta]
|
||||
: null;
|
||||
|
||||
const esComunidadSi = useMemo(
|
||||
() =>
|
||||
!!(
|
||||
comunidadSeleccionada &&
|
||||
preguntaComunidad?.opciones.find(
|
||||
(op) =>
|
||||
op.id_opcion === Number(comunidadSeleccionada) &&
|
||||
op.opcion.opcion.toLowerCase().includes('si')
|
||||
)
|
||||
),
|
||||
[comunidadSeleccionada, preguntaComunidad]
|
||||
);
|
||||
|
||||
const esExclusivo = useMemo(
|
||||
() => !preguntaComunidad && !!preguntaCuenta,
|
||||
[preguntaComunidad, preguntaCuenta]
|
||||
);
|
||||
|
||||
const mostrarFormularioCompleto =
|
||||
(esExclusivo && state.busquedaCompletada) ||
|
||||
(!preguntaComunidad && !esExclusivo) ||
|
||||
(esComunidadSi && state.busquedaCompletada) ||
|
||||
(!!(esComunidadSi === false) && !!comunidadSeleccionada);
|
||||
|
||||
// ── Búsqueda de cuenta ─────────────────────────────────────────────────────
|
||||
|
||||
useEffect(() => {
|
||||
const debeIniciarBusqueda = preguntaCuenta && (esExclusivo || esComunidadSi);
|
||||
|
||||
if (!debeIniciarBusqueda) {
|
||||
if (state.cuentaBuscada || state.busquedaCompletada || state.errorBusqueda) {
|
||||
dispatch({ type: 'BUSQUEDA_RESET' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const cuenta = state.respuestas[preguntaCuenta.id_pregunta];
|
||||
if (!cuenta || typeof cuenta !== 'string') {
|
||||
if (state.cuentaBuscada || state.busquedaCompletada || state.errorBusqueda) {
|
||||
dispatch({ type: 'BUSQUEDA_RESET' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const longitudRequerida = LONGITUDES[preguntaCuenta.validacion ?? ''];
|
||||
if (!longitudRequerida || cuenta.length !== longitudRequerida) {
|
||||
if (state.cuentaBuscada) dispatch({ type: 'BUSQUEDA_RESET' });
|
||||
return;
|
||||
}
|
||||
|
||||
// No re-buscar la misma cuenta
|
||||
if (cuenta === state.cuentaBuscada) return;
|
||||
|
||||
let endpoint =
|
||||
preguntaCuenta.validacion === 'cuenta_alumno'
|
||||
? `/alumnos/${cuenta}`
|
||||
: `/trabajadores/${cuenta}`;
|
||||
|
||||
if (esExclusivo) endpoint += `?id_evento=${id_evento}`;
|
||||
|
||||
dispatch({ type: 'BUSQUEDA_START', payload: cuenta });
|
||||
|
||||
axiosInstance
|
||||
.get<UsuarioDataResponse>(endpoint)
|
||||
.then((res) => {
|
||||
const d = res.data;
|
||||
if (d.nombre || d.apellidos || d.carrera || d.genero) {
|
||||
dispatch({ type: 'BUSQUEDA_SUCCESS', payload: d });
|
||||
} else {
|
||||
dispatch({ type: 'BUSQUEDA_ERROR', payload: 'No se encontraron datos para este usuario' });
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
const { message } = getAxiosError(err);
|
||||
dispatch({ type: 'BUSQUEDA_ERROR', payload: message });
|
||||
});
|
||||
}, [
|
||||
state.respuestas,
|
||||
state.cuentaBuscada,
|
||||
state.busquedaCompletada,
|
||||
state.errorBusqueda,
|
||||
preguntaCuenta,
|
||||
esComunidadSi,
|
||||
esExclusivo,
|
||||
id_evento,
|
||||
]);
|
||||
|
||||
// ── Autorelleno cuando llega usuarioEncontrado ─────────────────────────────
|
||||
|
||||
useEffect(() => {
|
||||
if (!state.data) return;
|
||||
|
||||
if (!state.usuarioEncontrado) {
|
||||
// Solo limpiar si aún no lo hicimos (evita loop infinito)
|
||||
if (!yaLimpioRef.current) return;
|
||||
yaLimpioRef.current = false;
|
||||
const idsCampos = preguntas
|
||||
.filter((p) =>
|
||||
['nombre', 'apellidos', 'correo', 'carrera', 'genero'].includes(p.validacion ?? '')
|
||||
)
|
||||
.map((p) => p.id_pregunta);
|
||||
if (idsCampos.length) {
|
||||
dispatch({ type: 'LIMPIAR_CAMPOS_AUTORELLENO', payload: idsCampos });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const u = state.usuarioEncontrado;
|
||||
const bulk: RespuestaFormulario = {};
|
||||
|
||||
preguntas.forEach((pregunta) => {
|
||||
const id = pregunta.id_pregunta;
|
||||
switch (pregunta.validacion) {
|
||||
case 'nombre':
|
||||
bulk[id] = u.nombre ?? '';
|
||||
break;
|
||||
case 'apellidos':
|
||||
bulk[id] = u.apellidos ?? '';
|
||||
break;
|
||||
case 'correo':
|
||||
bulk[id] = u.cuenta ? `${u.cuenta}@pcpuma.acatlan.unam.mx` : '';
|
||||
break;
|
||||
case 'carrera':
|
||||
bulk[id] = u.carrera ?? '';
|
||||
break;
|
||||
case 'genero': {
|
||||
const texto = u.genero === 'M' ? 'Masculino' : 'Femenino';
|
||||
const opcion = pregunta.opciones?.find(
|
||||
(op) => op.opcion.opcion.toLowerCase() === texto.toLowerCase()
|
||||
);
|
||||
if (opcion) bulk[id] = String(opcion.id_opcion);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(bulk).length) {
|
||||
yaLimpioRef.current = true;
|
||||
dispatch({ type: 'SET_RESPUESTAS_BULK', payload: bulk });
|
||||
}
|
||||
}, [state.usuarioEncontrado, state.data, preguntas]);
|
||||
|
||||
// ── Acciones expuestas ─────────────────────────────────────────────────────
|
||||
|
||||
const setRespuesta = useCallback((id: string, valor: string) => {
|
||||
dispatch({ type: 'SET_RESPUESTA', payload: { id, valor } });
|
||||
}, []);
|
||||
|
||||
const submitFormulario = useCallback(
|
||||
async (id_cuestionario: number) => {
|
||||
dispatch({ type: 'SEND_START' });
|
||||
try {
|
||||
const preguntaCorreo = preguntas.find(
|
||||
(p) => p.validacion === 'correo' || p.validacion === 'correo_institucional'
|
||||
);
|
||||
let correo = preguntaCorreo
|
||||
? String(state.respuestas[preguntaCorreo.id_pregunta] ?? '')
|
||||
: '';
|
||||
if (!correo) {
|
||||
const rx = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
for (const v of Object.values(state.respuestas)) {
|
||||
if (typeof v === 'string' && rx.test(v)) { correo = v; break; }
|
||||
}
|
||||
}
|
||||
|
||||
const res = await axiosInstance.post<import('@/types/submit').SubmitResponse>(
|
||||
'/cuestionario-respondido/submit',
|
||||
{
|
||||
id_cuestionario,
|
||||
correo,
|
||||
fecha_envio: new Date().toISOString(),
|
||||
respuestas: Object.entries(state.respuestas).map(([id, valor]) => ({
|
||||
id_pregunta: Number(id),
|
||||
valor: isNaN(Number(valor)) ? valor : Number(valor),
|
||||
})),
|
||||
}
|
||||
);
|
||||
return res.data;
|
||||
} finally {
|
||||
dispatch({ type: 'SEND_END' });
|
||||
}
|
||||
},
|
||||
[state.respuestas, preguntas]
|
||||
);
|
||||
|
||||
// ── Valor del contexto ─────────────────────────────────────────────────────
|
||||
|
||||
const value: FormularioContextType = {
|
||||
state,
|
||||
preguntas,
|
||||
preguntaComunidad,
|
||||
preguntaCuenta,
|
||||
esComunidadSi,
|
||||
esExclusivo,
|
||||
mostrarFormularioCompleto,
|
||||
setRespuesta,
|
||||
submitFormulario,
|
||||
};
|
||||
|
||||
return (
|
||||
<FormularioContext.Provider value={value}>
|
||||
{children}
|
||||
</FormularioContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useFormulario() {
|
||||
const ctx = useContext(FormularioContext);
|
||||
if (!ctx) throw new Error('useFormulario debe usarse dentro de FormularioProvider');
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { GetCuestionario } from '@/types/evento';
|
||||
import { UsuarioDataResponse } from '@/containers/formulario/formulario-registro';
|
||||
|
||||
export type RespuestaFormulario = Record<string, string | number>;
|
||||
|
||||
// ── Estado ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface FormularioState {
|
||||
// Datos del cuestionario cargado desde la API
|
||||
data: GetCuestionario | null;
|
||||
loadingData: boolean;
|
||||
errorData: string | null;
|
||||
|
||||
// Respuestas del usuario
|
||||
respuestas: RespuestaFormulario;
|
||||
|
||||
// Búsqueda de cuenta/rfc
|
||||
cuentaBuscada: string;
|
||||
busquedaCompletada: boolean;
|
||||
loadingBusqueda: boolean;
|
||||
errorBusqueda: string | null;
|
||||
usuarioEncontrado: UsuarioDataResponse | null;
|
||||
|
||||
// Envío
|
||||
isSending: boolean;
|
||||
}
|
||||
|
||||
export const initialState: FormularioState = {
|
||||
data: null,
|
||||
loadingData: true,
|
||||
errorData: null,
|
||||
|
||||
respuestas: {},
|
||||
|
||||
cuentaBuscada: '',
|
||||
busquedaCompletada: false,
|
||||
loadingBusqueda: false,
|
||||
errorBusqueda: null,
|
||||
usuarioEncontrado: null,
|
||||
|
||||
isSending: false,
|
||||
};
|
||||
|
||||
// ── Acciones ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export type FormularioAction =
|
||||
// Carga del cuestionario
|
||||
| { type: 'FETCH_DATA_START' }
|
||||
| { type: 'FETCH_DATA_SUCCESS'; payload: GetCuestionario }
|
||||
| { type: 'FETCH_DATA_ERROR'; payload: string }
|
||||
|
||||
// Respuestas
|
||||
| { type: 'SET_RESPUESTA'; payload: { id: string; valor: string } }
|
||||
| { type: 'SET_RESPUESTAS_BULK'; payload: RespuestaFormulario }
|
||||
| { type: 'LIMPIAR_CAMPOS_AUTORELLENO'; payload: number[] } // ids de pregunta
|
||||
|
||||
// Búsqueda de cuenta
|
||||
| { type: 'BUSQUEDA_START'; payload: string } // cuenta buscada
|
||||
| { type: 'BUSQUEDA_SUCCESS'; payload: UsuarioDataResponse }
|
||||
| { type: 'BUSQUEDA_ERROR'; payload: string }
|
||||
| { type: 'BUSQUEDA_RESET' }
|
||||
|
||||
// Envío
|
||||
| { type: 'SEND_START' }
|
||||
| { type: 'SEND_END' };
|
||||
|
||||
// ── Reducer ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export function formularioReducer(
|
||||
state: FormularioState,
|
||||
action: FormularioAction
|
||||
): FormularioState {
|
||||
switch (action.type) {
|
||||
// Carga del cuestionario
|
||||
case 'FETCH_DATA_START':
|
||||
return { ...state, loadingData: true, errorData: null };
|
||||
|
||||
case 'FETCH_DATA_SUCCESS':
|
||||
return { ...state, loadingData: false, data: action.payload };
|
||||
|
||||
case 'FETCH_DATA_ERROR':
|
||||
return { ...state, loadingData: false, errorData: action.payload };
|
||||
|
||||
// Respuestas
|
||||
case 'SET_RESPUESTA':
|
||||
return {
|
||||
...state,
|
||||
respuestas: { ...state.respuestas, [action.payload.id]: action.payload.valor },
|
||||
};
|
||||
|
||||
case 'SET_RESPUESTAS_BULK':
|
||||
return {
|
||||
...state,
|
||||
respuestas: { ...state.respuestas, ...action.payload },
|
||||
};
|
||||
|
||||
case 'LIMPIAR_CAMPOS_AUTORELLENO': {
|
||||
const limpio = { ...state.respuestas };
|
||||
action.payload.forEach((id) => delete limpio[id]);
|
||||
return { ...state, respuestas: limpio };
|
||||
}
|
||||
|
||||
// Búsqueda
|
||||
case 'BUSQUEDA_START':
|
||||
return {
|
||||
...state,
|
||||
loadingBusqueda: true,
|
||||
errorBusqueda: null,
|
||||
busquedaCompletada: false,
|
||||
usuarioEncontrado: null,
|
||||
cuentaBuscada: action.payload,
|
||||
};
|
||||
|
||||
case 'BUSQUEDA_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
loadingBusqueda: false,
|
||||
busquedaCompletada: true,
|
||||
errorBusqueda: null,
|
||||
usuarioEncontrado: action.payload,
|
||||
};
|
||||
|
||||
case 'BUSQUEDA_ERROR':
|
||||
return {
|
||||
...state,
|
||||
loadingBusqueda: false,
|
||||
busquedaCompletada: false,
|
||||
errorBusqueda: action.payload,
|
||||
usuarioEncontrado: null,
|
||||
};
|
||||
|
||||
case 'BUSQUEDA_RESET':
|
||||
return {
|
||||
...state,
|
||||
cuentaBuscada: '',
|
||||
busquedaCompletada: false,
|
||||
loadingBusqueda: false,
|
||||
errorBusqueda: null,
|
||||
usuarioEncontrado: null,
|
||||
};
|
||||
|
||||
// Envío
|
||||
case 'SEND_START':
|
||||
return { ...state, isSending: true };
|
||||
|
||||
case 'SEND_END':
|
||||
return { ...state, isSending: false };
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { FormularioProvider, useFormulario } from './formulario-context';
|
||||
export type { FormularioState, FormularioAction, RespuestaFormulario } from './formulario-reducer';
|
||||
+119
-7
@@ -1,7 +1,6 @@
|
||||
import { FormularioCreacion } from '@/types/create-formulario';
|
||||
|
||||
interface Plantilla {
|
||||
imagen: string;
|
||||
nombre: string;
|
||||
id: string;
|
||||
datos: FormularioCreacion;
|
||||
@@ -9,8 +8,7 @@ interface Plantilla {
|
||||
|
||||
export const plantillasDisponibles: Plantilla[] = [
|
||||
{
|
||||
imagen: '/plantilla_trabajadores.png',
|
||||
nombre: 'Registro Comunidad Trabajador',
|
||||
nombre: 'Registro General (con prellenado para Trabajadores)',
|
||||
id: 'registro-comunidad-estudiantil',
|
||||
datos: {
|
||||
nombre_form: 'Registro',
|
||||
@@ -79,8 +77,7 @@ export const plantillasDisponibles: Plantilla[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
imagen: '/plantilla_alumnos.png',
|
||||
nombre: 'Registro Comunidad Alumnos',
|
||||
nombre: 'Registro General (con prellenado para Alumnos)',
|
||||
id: 'registro-comunidad-alumnos',
|
||||
datos: {
|
||||
nombre_form: 'Registro',
|
||||
@@ -155,7 +152,6 @@ export const plantillasDisponibles: Plantilla[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
imagen: '/plantilla_general.png',
|
||||
nombre: 'Registro General',
|
||||
id: 'registro-general',
|
||||
datos: {
|
||||
@@ -209,9 +205,125 @@ export const plantillasDisponibles: Plantilla[] = [
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
nombre: 'Formulario Exclusivo Alumnos',
|
||||
id: 'formulario-exclusivo-alumnos',
|
||||
datos: {
|
||||
nombre_form: 'Formulario Exclusivo Alumnos',
|
||||
descripcion:
|
||||
'Este formulario está destinado exclusivamente para alumnos. Por favor, completa todos los campos obligatorios.',
|
||||
fecha_inicio: '2025-08-01T08:00:00',
|
||||
fecha_fin: '2025-08-10T23:59:59',
|
||||
id_tipo_cuestionario: 1,
|
||||
secciones: [
|
||||
{
|
||||
titulo: 'Información Personal',
|
||||
descripcion: 'Proporciona tus datos personales.',
|
||||
preguntas: [
|
||||
{
|
||||
titulo: 'Número de cuenta',
|
||||
tipo: 'Abierta (Respuesta corta)',
|
||||
obligatoria: true,
|
||||
validacion: 'cuenta_alumno',
|
||||
},
|
||||
{
|
||||
titulo: 'Correo electrónico',
|
||||
tipo: 'Abierta (Respuesta corta)',
|
||||
obligatoria: true,
|
||||
validacion: 'correo',
|
||||
},
|
||||
{
|
||||
titulo: 'Nombre(s)',
|
||||
tipo: 'Abierta (Respuesta corta)',
|
||||
obligatoria: true,
|
||||
validacion: 'nombre',
|
||||
},
|
||||
{
|
||||
titulo: 'Apellidos',
|
||||
tipo: 'Abierta (Respuesta corta)',
|
||||
obligatoria: true,
|
||||
validacion: 'apellidos',
|
||||
},
|
||||
{
|
||||
titulo: 'Carrera',
|
||||
tipo: 'Abierta (Respuesta corta)',
|
||||
validacion: 'carrera',
|
||||
obligatoria: true,
|
||||
},
|
||||
{
|
||||
titulo: 'Género',
|
||||
tipo: 'Cerrada',
|
||||
obligatoria: true,
|
||||
validacion: 'genero',
|
||||
opciones: [
|
||||
{ valor: 'Masculino' },
|
||||
{ valor: 'Femenino' },
|
||||
{ valor: 'Prefiero no decirlo' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
nombre: 'Formulario Exclusivo Profesores',
|
||||
id: 'formulario-exclusivo-profesores',
|
||||
datos: {
|
||||
nombre_form: 'Formulario Exclusivo Profesores',
|
||||
descripcion:
|
||||
'Este formulario está destinado exclusivamente para profesores. Por favor, completa todos los campos obligatorios.',
|
||||
fecha_inicio: '2025-08-01T08:00:00',
|
||||
fecha_fin: '2025-08-10T23:59:59',
|
||||
id_tipo_cuestionario: 2,
|
||||
secciones: [
|
||||
{
|
||||
titulo: 'Información Personal',
|
||||
descripcion: 'Proporciona tus datos personales.',
|
||||
preguntas: [
|
||||
{
|
||||
titulo: 'RFC',
|
||||
tipo: 'Abierta (Respuesta corta)',
|
||||
obligatoria: true,
|
||||
validacion: 'rfc',
|
||||
},
|
||||
{
|
||||
titulo: 'Correo electrónico',
|
||||
tipo: 'Abierta (Respuesta corta)',
|
||||
obligatoria: true,
|
||||
validacion: 'correo',
|
||||
},
|
||||
{
|
||||
titulo: 'Nombre(s)',
|
||||
tipo: 'Abierta (Respuesta corta)',
|
||||
obligatoria: true,
|
||||
validacion: 'nombre',
|
||||
},
|
||||
{
|
||||
titulo: 'Apellidos',
|
||||
tipo: 'Abierta (Respuesta corta)',
|
||||
obligatoria: true,
|
||||
validacion: 'apellidos',
|
||||
},
|
||||
{
|
||||
titulo: 'Género',
|
||||
tipo: 'Cerrada',
|
||||
obligatoria: true,
|
||||
validacion: 'genero',
|
||||
opciones: [
|
||||
{ valor: 'Masculino' },
|
||||
{ valor: 'Femenino' },
|
||||
{ valor: 'Prefiero no decirlo' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/*
|
||||
/*
|
||||
¡Bienvenidas y bienvenidos a la Facultad!
|
||||
Inicia esta nueva etapa con un recorrido especial por nuestras instalaciones, pensado para que conozcas tu nueva casa universitaria. Te acompañaremos por aulas, laboratorios, bibliotecas, áreas comunes y servicios que estarán a tu disposición durante tu formación. Además, podrás resolver dudas y conocer a quienes serán parte de tu camino académico.
|
||||
¡No faltes, esta actividad marcará el inicio de grandes experiencias!
|
||||
|
||||
Vendored
+1
@@ -29,6 +29,7 @@ export interface FormularioCreacion {
|
||||
id_tipo_cuestionario: number;
|
||||
id_tipo_evento?: number;
|
||||
cupo_maximo?: number; // Opcional, si no se requiere un cupo máximo
|
||||
mensaje_correo?: string;
|
||||
secciones: SeccionFormulario[];
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
@@ -15,6 +15,7 @@ export interface GetCuestionario {
|
||||
fecha_fin: string; // Formato ISO: 'YYYY-MM-DDTHH:mm:ss'
|
||||
fecha_inicio: string; // Formato ISO: 'YYYY-MM-DDTHH:mm:ss'
|
||||
id_tipo_cuestionario: number;
|
||||
mensaje_correo?: string | null;
|
||||
}
|
||||
|
||||
export interface GetCuestionarioWithEvento extends GetCuestionario {
|
||||
|
||||
Reference in New Issue
Block a user