Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c86f5d223 | |||
| eaa955a995 | |||
| ad983c6ae8 | |||
| ac0671bb1a | |||
| 8c2a6ecb6f | |||
| 2f50d4f2fb | |||
| dcc474a065 | |||
| fe8ec5f591 | |||
| d3e1441f87 | |||
| f7d7bcfae6 | |||
| c0f4f31e18 | |||
| 3eba0c1348 | |||
| 5a88e93e04 | |||
| 6f89b993e1 | |||
| 9068cef62d | |||
| b0f744ef14 | |||
| 4ff66e81c7 | |||
| 3123c9911a | |||
| fee9d92ed7 | |||
| 8f2ee27711 | |||
| 509ff4255a | |||
| 4c300a8e20 | |||
| 534ba64e0c | |||
| dcb66ce2c1 | |||
| dc161cf269 | |||
| 978f6c8a69 |
@@ -0,0 +1,85 @@
|
|||||||
|
name: Despliegue Automatizado Universal
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
get-context:
|
||||||
|
runs-on: host
|
||||||
|
outputs:
|
||||||
|
node_version: ${{ steps.setup.outputs.version }}
|
||||||
|
target_app: ${{ steps.setup.outputs.app }}
|
||||||
|
steps:
|
||||||
|
- name: Resolver Aplicación desde apps.conf
|
||||||
|
id: setup
|
||||||
|
run: |
|
||||||
|
# 1. Obtener el nombre del repositorio actual en Gitea (ej: front-Censo)
|
||||||
|
REPO_NAME="${{ github.event.repository.name }}"
|
||||||
|
|
||||||
|
# 2. Determinar si buscamos un entorno -dev (develop) o producción (master)
|
||||||
|
if [ "${{ github.ref_name }}" = "develop" ]; then
|
||||||
|
SUFFIX="-dev"
|
||||||
|
else
|
||||||
|
SUFFIX=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Buscando qué sección de apps.conf tiene REPO=$REPO_NAME y termina en '$SUFFIX'..."
|
||||||
|
|
||||||
|
# 3. Buscar en /etc/apps.conf el bloque que contenga REPO=nombre_del_repo
|
||||||
|
TARGET_APP=$(awk -v repo="REPO=$REPO_NAME" -v suf="$SUFFIX" '
|
||||||
|
/^\[/ { gsub(/[\[\]]/, "", $0); current_box=$0; next }
|
||||||
|
$0 == repo {
|
||||||
|
if ((suf == "-dev" && current_box ~ /-dev$/) || (suf == "" && current_box !~ /-dev$/)) {
|
||||||
|
print current_box;
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' /etc/apps.conf)
|
||||||
|
|
||||||
|
if [ -z "$TARGET_APP" ]; then
|
||||||
|
echo "ERROR: No se encontró ninguna sección en /etc/apps.conf para el repositorio $REPO_NAME con el sufijo correcto."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Leer la versión de Node del bloque encontrado
|
||||||
|
VERSION=$(awk -v app="$TARGET_APP" '
|
||||||
|
$0=="["app"]" { found=1; next }
|
||||||
|
/^\[/ && found { exit }
|
||||||
|
found && /^NODE=/ { split($0, a, "="); print a[2]; exit }
|
||||||
|
' /etc/apps.conf)
|
||||||
|
|
||||||
|
MAJOR_VERSION=$(echo "$VERSION" | tr -d '[:space:]' | grep -oE '[0-9]+' | head -n1)
|
||||||
|
|
||||||
|
echo "-> ID de Aplicación encontrado: $TARGET_APP"
|
||||||
|
echo "-> Versión Mayor de Node resuelta: $MAJOR_VERSION"
|
||||||
|
|
||||||
|
echo "app=${TARGET_APP}" >> $GITHUB_OUTPUT
|
||||||
|
echo "version=${MAJOR_VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: get-context
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Configurar Node.js dinámicamente
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ needs.get-context.outputs.node_version }}
|
||||||
|
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: [get-context, build]
|
||||||
|
runs-on: host
|
||||||
|
steps:
|
||||||
|
- name: Ejecutar Despliegue en Servidor Físico
|
||||||
|
run: |
|
||||||
|
TARGET_APP="${{ needs.get-context.outputs.target_app }}"
|
||||||
|
BRANCH_NAME="${{ github.ref_name }}"
|
||||||
|
|
||||||
|
sudo -u deploy /home/deploy/scripts/deploy-front.sh "$TARGET_APP" "$BRANCH_NAME"
|
||||||
+3
-3
@@ -6,12 +6,12 @@ const nextConfig: NextConfig = {
|
|||||||
{
|
{
|
||||||
protocol: 'http',
|
protocol: 'http',
|
||||||
hostname: 'localhost',
|
hostname: 'localhost',
|
||||||
port: '4200',
|
port: '4200', //4200
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
hostname: 'venus.acatlan.unam.mx'
|
hostname: 'venus.acatlan.unam.mx',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+399
-821
File diff suppressed because it is too large
Load Diff
+4
-2
@@ -14,13 +14,15 @@
|
|||||||
"@uiw/react-md-editor": "^4.0.7",
|
"@uiw/react-md-editor": "^4.0.7",
|
||||||
"@yudiel/react-qr-scanner": "^2.2.1",
|
"@yudiel/react-qr-scanner": "^2.2.1",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.8",
|
||||||
"bootstrap-icons": "^1.11.3",
|
"bootstrap-icons": "^1.11.3",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"next": "15.3.3",
|
"next": "^15.5.12",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
"react-bootstrap": "^2.10.10",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
|
|||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,10 @@ import { useParams } from 'next/navigation';
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useEvento } from '@/context/evento';
|
import { useEvento } from '@/context/evento';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import Button from '@/components/button';
|
||||||
|
|
||||||
|
const Modal = dynamic(() => import('@/components/modal'), { ssr: false });
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
id_evento: string;
|
id_evento: string;
|
||||||
@@ -27,6 +31,7 @@ export default function Page() {
|
|||||||
const [loadingAsistencia, setLoadingAsistencia] = React.useState<
|
const [loadingAsistencia, setLoadingAsistencia] = React.useState<
|
||||||
Record<number, boolean>
|
Record<number, boolean>
|
||||||
>({});
|
>({});
|
||||||
|
const [contadorModal, setContadorModal] = React.useState<number | null>(null);
|
||||||
|
|
||||||
const { data: participantes, setData } = useGetApi<ParticipacionEvento[]>(
|
const { data: participantes, setData } = useGetApi<ParticipacionEvento[]>(
|
||||||
`/participante-evento/evento/${params.id_cuestionario}`
|
`/participante-evento/evento/${params.id_cuestionario}`
|
||||||
@@ -45,12 +50,15 @@ export default function Page() {
|
|||||||
) => {
|
) => {
|
||||||
setLoadingAsistencia((prev) => ({ ...prev, [id_participante]: true }));
|
setLoadingAsistencia((prev) => ({ ...prev, [id_participante]: true }));
|
||||||
try {
|
try {
|
||||||
await axiosInstance.post(
|
const response = await axiosInstance.post(
|
||||||
`/participante-evento/asistencia/${id_participante}/${id_evento}`
|
`/participante-evento/asistencia/${id_participante}/${id_evento}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (response.data?.contador != null) {
|
||||||
|
setContadorModal(response.data.contador);
|
||||||
|
}
|
||||||
|
|
||||||
toast.success('Asistencia confirmada');
|
toast.success('Asistencia confirmada');
|
||||||
// Actualiza lista
|
|
||||||
const { data } = await axiosInstance.get<ParticipacionEvento[]>(
|
const { data } = await axiosInstance.get<ParticipacionEvento[]>(
|
||||||
`/participante-evento/evento/${id_evento}`
|
`/participante-evento/evento/${id_evento}`
|
||||||
);
|
);
|
||||||
@@ -188,6 +196,33 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<Modal
|
||||||
|
isVisible={contadorModal !== null}
|
||||||
|
size="sm"
|
||||||
|
onClose={() => setContadorModal(null)}
|
||||||
|
closeButton
|
||||||
|
className={{ content: 'border-0 rounded-3', body: 'text-center' }}
|
||||||
|
>
|
||||||
|
<div className="modal-header bg-primary text-white border-0 rounded-top-3">
|
||||||
|
<h5 className="modal-title mb-0 fw-bold">
|
||||||
|
<i className="bi bi-hash me-2"></i>
|
||||||
|
Contador
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body p-4">
|
||||||
|
<p className="display-4 fw-bold text-primary mb-1">{contadorModal}</p>
|
||||||
|
<p className="text-muted mb-4">
|
||||||
|
Este es el numero de estampillas a entregar al usuario
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => setContadorModal(null)}
|
||||||
|
className="px-4 py-2 rounded-pill"
|
||||||
|
>
|
||||||
|
Cerrar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { plantillasDisponibles } from '@/data/plantillas';
|
import { plantillasDisponibles } from '@/data/plantillas';
|
||||||
import { FormularioCreacion } from '@/types/create-formulario';
|
import { FormularioCreacion } from '@/types/create-formulario';
|
||||||
import Image from 'next/image';
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import CreateFormulario from '@/containers/create-formulario';
|
import CreateFormulario from '@/containers/create-formulario';
|
||||||
import FormularioCardPreview from '@/components/formulario/formulario-card-preview';
|
import FormularioCardPreview from '@/components/formulario/formulario-card-preview';
|
||||||
@@ -193,21 +192,18 @@ export default function Page() {
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="d-flex gap-4 flex-wrap">
|
<div className="d-flex gap-2 flex-wrap">
|
||||||
{plantillasDisponibles.map((plantilla) => (
|
{plantillasDisponibles.map((plantilla) => (
|
||||||
<div
|
<div
|
||||||
className="mb-3 cursor-pointer"
|
className="cursor-pointer"
|
||||||
key={plantilla.id}
|
key={plantilla.id}
|
||||||
onClick={() => handleSeleccionarPlantilla(plantilla.id)}
|
onClick={() => handleSeleccionarPlantilla(plantilla.id)}
|
||||||
>
|
>
|
||||||
<div className="text-center">
|
<div
|
||||||
<Image
|
className="d-flex align-items-center justify-content-center rounded shadow-sm border text-center fw-semibold bg-azul text-white"
|
||||||
src={plantilla.imagen}
|
style={{ padding: '1rem' }}
|
||||||
width={200}
|
>
|
||||||
height={200}
|
{plantilla.nombre}
|
||||||
alt={`Plantilla de formulario ${plantilla.nombre}`}
|
|
||||||
className="img-fluid rounded shadow-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -21,10 +21,46 @@ export default function Page() {
|
|||||||
const params = useParams<Params>();
|
const params = useParams<Params>();
|
||||||
const { evento, loading, error, updateEvento, setEventoData } = useEvento();
|
const { evento, loading, error, updateEvento, setEventoData } = useEvento();
|
||||||
|
|
||||||
|
const tipo_usuario = sessionStorage.getItem('axxxd')?.replace(/"/g, '') || '';
|
||||||
|
|
||||||
const [loadingStates, setLoadingStates] = useState<Record<number, boolean>>(
|
const [loadingStates, setLoadingStates] = useState<Record<number, boolean>>(
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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 = (
|
const handleChange = (
|
||||||
field: keyof CreateEventoType,
|
field: keyof CreateEventoType,
|
||||||
value: string | Date
|
value: string | Date
|
||||||
@@ -86,26 +122,84 @@ export default function Page() {
|
|||||||
handleOnChange={handleEventoActualizado}
|
handleOnChange={handleEventoActualizado}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="p-4 border rounded bg-light">
|
{/* Carga masiva de alumnos o trabajadores */}
|
||||||
<div className="d-flex justify-content-between align-items-center">
|
{tipo_usuario !== 'staff' && (
|
||||||
<div>
|
<div className="p-4 border rounded bg-light mb-4">
|
||||||
<h2 className="mb-0">Formularios del evento</h2>
|
<h2 className="mb-1">Carga masiva de participantes</h2>
|
||||||
<p className="mb-0 text-muted">
|
<p className="text-muted mb-3">
|
||||||
Aquí puedes ver y gestionar los formularios asociados a este
|
Sube un archivo <strong>.xlsx</strong> para registrar los
|
||||||
evento.
|
participantes que podrán inscribirse a este evento. Solo los
|
||||||
</p>
|
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>
|
||||||
<Button
|
|
||||||
icon="plus"
|
|
||||||
variant="primary"
|
|
||||||
onClick={() =>
|
|
||||||
router.push(`/user/evento/${params.id_evento}/formularios/crear`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Crear nuevo formulario
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{/* Validacion del tipo de usuario */}
|
||||||
|
{tipo_usuario !== 'staff' && (
|
||||||
|
<div className="p-4 border rounded bg-light">
|
||||||
|
<div className="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-0">Formularios del evento</h2>
|
||||||
|
<p className="mb-0 text-muted">
|
||||||
|
Aquí puedes ver y gestionar los formularios asociados a este
|
||||||
|
evento.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
icon="plus"
|
||||||
|
variant="primary"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/user/evento/${params.id_evento}/formularios/crear`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Crear nuevo formulario
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{evento?.cuestionarios && evento.cuestionarios.length > 0 && (
|
{evento?.cuestionarios && evento.cuestionarios.length > 0 && (
|
||||||
<div className="row mt-3">
|
<div className="row mt-3">
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
'use client'
|
||||||
|
import axiosInstance from "@/utils/api-config";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Button, Form, ProgressBar } from "react-bootstrap";
|
||||||
|
import { FaUpload } from "react-icons/fa";
|
||||||
|
|
||||||
|
export default function CargaMasiva() {
|
||||||
|
const [xlsx, setXlsx] = useState<File | null>(null);
|
||||||
|
const [progress, setProgress] = useState<number>(0);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const validarExt = (file: File | null) => {
|
||||||
|
if (!file) return;
|
||||||
|
const extPermitidas = /(.xlsx)$/i;
|
||||||
|
if (!extPermitidas.exec(file.name)) {
|
||||||
|
console.error("Asegúrate de ingresar un archivo .xlsx");
|
||||||
|
setXlsx(null);
|
||||||
|
} else {
|
||||||
|
setXlsx(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0] || null;
|
||||||
|
validarExt(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enviarCargaMasiva = async () => {
|
||||||
|
if (!xlsx) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", xlsx);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setProgress(0);
|
||||||
|
|
||||||
|
const res = await axiosInstance.post(
|
||||||
|
"/cuestionario/carga-masiva",
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
|
onUploadProgress: (event) => {
|
||||||
|
if (event.total) {
|
||||||
|
const porcentaje = Math.round(
|
||||||
|
(event.loaded * 100) / event.total
|
||||||
|
);
|
||||||
|
setProgress(porcentaje);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
//Falta hacer validacion
|
||||||
|
|
||||||
|
console.log("Carga masiva exitosa", res.data);
|
||||||
|
setTimeout(() => {
|
||||||
|
setProgress(100);
|
||||||
|
setLoading(false);
|
||||||
|
}, 500);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Carga masiva fallida", error);
|
||||||
|
setLoading(false);
|
||||||
|
setProgress(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container-fluid my-4">
|
||||||
|
<div className="row mb-4">
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="p-4 border rounded bg-light">
|
||||||
|
<h2 className="mb-2">Carga Masiva de Eventos</h2>
|
||||||
|
<p className="mb-0 text-muted">
|
||||||
|
Sube tu archivo excel para poder crear los eventos.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form.Group className="mb-3">
|
||||||
|
<Form.Label>Archivo Excel</Form.Label>
|
||||||
|
<div
|
||||||
|
className="border p-4 text-center rounded"
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
onClick={() => document.getElementById("xlsxInput")?.click()}
|
||||||
|
>
|
||||||
|
<FaUpload size={40} className="mb-2" />
|
||||||
|
<p className="mb-1">
|
||||||
|
{xlsx?.name ||
|
||||||
|
"Arrastra aquí tu archivo o da click aquí para buscar"}
|
||||||
|
</p>
|
||||||
|
<p className="is-size-6">Tamaño máximo 20MB</p>
|
||||||
|
<p className="is-size-7">
|
||||||
|
Si al momento de elegir un archivo este no se selecciona,
|
||||||
|
haga click en cancelar en la ventana emergente e intente de
|
||||||
|
nuevo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="xlsxInput"
|
||||||
|
type="file"
|
||||||
|
accept=".xlsx"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<div className="d-flex gap-2 mt-3">
|
||||||
|
<Button
|
||||||
|
variant="outline-primary"
|
||||||
|
disabled={!xlsx || loading}
|
||||||
|
onClick={enviarCargaMasiva}
|
||||||
|
>
|
||||||
|
{loading ? "Subiendo..." : "Enviar archivo"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Barra de progreso */}
|
||||||
|
{loading && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<ProgressBar
|
||||||
|
now={progress}
|
||||||
|
label={`${progress}%`}
|
||||||
|
animated
|
||||||
|
striped
|
||||||
|
variant="info"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Button, Image } from "react-bootstrap";
|
||||||
|
import { FaUpload } from "react-icons/fa";
|
||||||
|
import axiosInstance from "@/utils/api-config";
|
||||||
|
|
||||||
|
//Para poder subir las imagenes
|
||||||
|
interface UploadedImage {
|
||||||
|
originalName: string;
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BannerUpload() {
|
||||||
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [uploaded, setUploaded] = useState<UploadedImage[]>([]);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.files) {
|
||||||
|
setFiles(Array.from(e.target.files));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = async () => {
|
||||||
|
if (files.length === 0) {
|
||||||
|
alert("Selecciona al menos un archivo");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
files.forEach((file) => {
|
||||||
|
formData.append("banners", file);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setProgress(0);
|
||||||
|
|
||||||
|
const res = await axiosInstance.post("/cuestionario/banners/bulk", formData, {
|
||||||
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
if (progressEvent.total) {
|
||||||
|
setProgress(
|
||||||
|
Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
//Falta hacer validacion
|
||||||
|
|
||||||
|
setUploaded(res.data);
|
||||||
|
alert("Banners subidos correctamente ✅");
|
||||||
|
setFiles([]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert(error || "Error al subir archivos");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container-fluid my-4">
|
||||||
|
<div className="row mb-4">
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="p-4 border rounded bg-light">
|
||||||
|
<h2 className="mb-2">Subir Banners</h2>
|
||||||
|
<p className="mb-0 text-muted">
|
||||||
|
Arrastra o selecciona imágenes para subir tus banners (jpg, png, webp).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Área Drag & Drop */}
|
||||||
|
<div
|
||||||
|
className="border p-4 text-center rounded"
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
onClick={() => document.getElementById("bannerInput")?.click()}
|
||||||
|
>
|
||||||
|
<FaUpload size={40} className="mb-2 text-muted" />
|
||||||
|
<p className="mb-1">
|
||||||
|
Arrastra aquí tus imágenes o haz clic para buscar
|
||||||
|
</p>
|
||||||
|
<p className="is-size-6">Tamaño máximo 20MB</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="bannerInput"
|
||||||
|
type="file"
|
||||||
|
accept=".jpg,.jpeg,.png,.webp"
|
||||||
|
multiple
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Previsualización */}
|
||||||
|
{files.length > 0 && (
|
||||||
|
<div className="d-flex flex-wrap gap-3 mt-4">
|
||||||
|
{files.map((file, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="border rounded overflow-hidden shadow-sm"
|
||||||
|
style={{ width: "350px", height: "350px" }}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={URL.createObjectURL(file)}
|
||||||
|
alt={file.name}
|
||||||
|
className="img-fluid h-75 w-100 object-fit-cover"
|
||||||
|
/>
|
||||||
|
<p className="small text-center bg-light p-2 m-0">{file.name}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Botón */}
|
||||||
|
<div className="d-flex gap-2 mt-3">
|
||||||
|
<Button
|
||||||
|
variant="outline-primary"
|
||||||
|
disabled={loading}
|
||||||
|
onClick={handleUpload}
|
||||||
|
>
|
||||||
|
{loading ? "Subiendo..." : "Subir Banners"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Barra de progreso */}
|
||||||
|
{loading && (
|
||||||
|
<div className="progress mt-3" style={{ height: "8px" }}>
|
||||||
|
<div
|
||||||
|
className="progress-bar progress-bar-striped bg-primary"
|
||||||
|
role="progressbar"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Archivos subidos */}
|
||||||
|
{uploaded.length > 0 && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<h5 className="mb-2 text-muted">Archivos subidos:</h5>
|
||||||
|
<ul className="list-group">
|
||||||
|
{uploaded.map((file, i) => (
|
||||||
|
<li key={i} className="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
<span>{file.originalName}</span>
|
||||||
|
<a href={`/${file.path}`} target="_blank" className="text-primary">
|
||||||
|
Ver
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
'use client'
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
import Footer from '@/components/layout/footer';
|
import Footer from '@/components/layout/footer';
|
||||||
import Header from '@/components/layout/header';
|
import Header from '@/components/layout/header';
|
||||||
import Navbar from '@/components/navbar';
|
const Navbar = dynamic(() => import('@/components/navbar'), { ssr: false });
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
|||||||
@@ -10,61 +10,76 @@ import toast from 'react-hot-toast';
|
|||||||
const Modal = dynamic(() => import('@/components/modal'), { ssr: false });
|
const Modal = dynamic(() => import('@/components/modal'), { ssr: false });
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [scannedData, setScannedData] = useState<{
|
|
||||||
id_participante: number;
|
|
||||||
id_cuestionario: number;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [enableScan, setEnableScan] = useState(true);
|
const [enableScan, setEnableScan] = useState(true);
|
||||||
const [statusMessage, setStatusMessage] = useState('');
|
const [statusMessage, setStatusMessage] = useState('');
|
||||||
|
|
||||||
const [participante, setParticipante] = useState('');
|
const [participante, setParticipante] = useState('');
|
||||||
|
const [contador, setContador] = useState(0);
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
const [token, setToken] = useState('');
|
||||||
|
const [tokenValid, setTokenValid] = useState(false);
|
||||||
|
|
||||||
const handleScan = async (rawValue: string) => {
|
const handleScan = async (rawValue: string) => {
|
||||||
console.log('QR Escaneado:', rawValue);
|
console.log('QR Escaneado:', rawValue);
|
||||||
if (!enableScan) return;
|
if (!enableScan) return;
|
||||||
|
|
||||||
try {
|
if (!rawValue) {
|
||||||
const data = JSON.parse(rawValue);
|
setStatusMessage('⚠️ El QR no contiene los campos requeridos');
|
||||||
if (data.id_participante && data.id_cuestionario) {
|
return;
|
||||||
try {
|
}
|
||||||
const response = await axiosInstance.get(
|
|
||||||
`/participante-evento/${data.id_participante}/${data.id_cuestionario}`
|
|
||||||
);
|
|
||||||
|
|
||||||
setParticipante(response.data.participante.correo);
|
try {
|
||||||
setScannedData({
|
const response = await axiosInstance.post(
|
||||||
id_participante: data.id_participante,
|
'/participante-evento/decode-token',
|
||||||
id_cuestionario: data.id_cuestionario,
|
{ token: rawValue }
|
||||||
});
|
);
|
||||||
setShowModal(true);
|
|
||||||
setEnableScan(false);
|
if (response.data) {
|
||||||
} catch (err) {
|
// Token válido
|
||||||
console.warn('Participante no registrado en el cuestionario:', err);
|
setToken(rawValue);
|
||||||
setStatusMessage(
|
setTokenValid(true);
|
||||||
'❌ El participante no está registrado en este evento.'
|
setShowModal(true);
|
||||||
);
|
setEnableScan(false);
|
||||||
}
|
setContador(response.data.contador);
|
||||||
} else {
|
} else {
|
||||||
setStatusMessage('⚠️ El QR no contiene los campos requeridos');
|
// Token caducado o cualquier otro error
|
||||||
|
setStatusMessage(`❌ Error el código qr ya caduco`);
|
||||||
|
setTokenValid(false);
|
||||||
|
setShowModal(true); // mostramos modal con el mensaje de error
|
||||||
|
setEnableScan(false);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('QR malformado:', err);
|
console.error('Error al decodificar el QR:', err);
|
||||||
setStatusMessage('❌ Error al leer el QR: formato inválido');
|
setStatusMessage('❌ Error al leer el QR');
|
||||||
|
setTokenValid(false);
|
||||||
|
setShowModal(true);
|
||||||
|
setEnableScan(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
if (!scannedData) return;
|
if (!tokenValid) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.post(
|
const response = await axiosInstance.post(
|
||||||
`/participante-evento/asistencia/${scannedData.id_participante}/${scannedData.id_cuestionario}`
|
`/participante-evento/asistencia`,
|
||||||
|
{
|
||||||
|
token: token,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('Asistencia registrada:', response.data);
|
//Validacion
|
||||||
toast.success('✅ Asistencia registrada correctamente');
|
if (response.data.success === false) {
|
||||||
|
console.error('El usuario ya se registro');
|
||||||
|
setMessage(response.data.message);
|
||||||
|
toast.error('❌ Error el usaurio ya se registro previamente.');
|
||||||
|
} else {
|
||||||
|
setParticipante(response.data.data.participante);
|
||||||
|
setMessage(response.data.message);
|
||||||
|
console.log('Asistencia registrada:', response.data);
|
||||||
|
toast.success('✅ Asistencia registrada correctamente');
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error en la petición:', err);
|
console.error('Error en la petición:', err);
|
||||||
setStatusMessage('❌ Error de red al registrar asistencia');
|
setStatusMessage('❌ Error de red al registrar asistencia');
|
||||||
@@ -72,7 +87,6 @@ export default function Page() {
|
|||||||
|
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
setEnableScan(true);
|
setEnableScan(true);
|
||||||
setScannedData(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -124,6 +138,26 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{participante && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="alert alert-success border-0 rounded-3 d-flex align-items-center">
|
||||||
|
<i className="bi bi-person-check-fill me-2"></i>
|
||||||
|
<span>
|
||||||
|
Ultimo participante registrado: {participante}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{message && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="alert alert-info border-0 rounded-3 d-flex align-items-center">
|
||||||
|
<i className="bi bi-info-circle-fill me-2"></i>
|
||||||
|
<span>{message}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setEnableScan(false)}
|
onClick={() => setEnableScan(false)}
|
||||||
variant="outline-danger"
|
variant="outline-danger"
|
||||||
@@ -164,8 +198,8 @@ export default function Page() {
|
|||||||
statusMessage.includes('❌')
|
statusMessage.includes('❌')
|
||||||
? 'alert-danger'
|
? 'alert-danger'
|
||||||
: statusMessage.includes('⚠️')
|
: statusMessage.includes('⚠️')
|
||||||
? 'alert-warning'
|
? 'alert-warning'
|
||||||
: 'alert-success'
|
: 'alert-success'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="me-2">
|
<div className="me-2">
|
||||||
@@ -195,7 +229,7 @@ export default function Page() {
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
setEnableScan(true);
|
setEnableScan(true);
|
||||||
setScannedData(null);
|
setTokenValid(false);
|
||||||
}}
|
}}
|
||||||
closeButton
|
closeButton
|
||||||
className={{
|
className={{
|
||||||
@@ -211,31 +245,12 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="modal-body p-4">
|
<div className="modal-body p-4">
|
||||||
{scannedData ? (
|
{tokenValid ? (
|
||||||
|
// Modal de confirmación de asistencia
|
||||||
<>
|
<>
|
||||||
<div className="mb-4">
|
<p className="text-muted mb-3">
|
||||||
<div className="mb-3">
|
Contador del usuario: <strong>{contador}</strong>
|
||||||
<i
|
</p>
|
||||||
className="bi bi-person-check-fill text-success"
|
|
||||||
style={{ fontSize: '3rem' }}
|
|
||||||
></i>
|
|
||||||
</div>
|
|
||||||
<h5 className="mb-3 fw-semibold">
|
|
||||||
Información del participante
|
|
||||||
</h5>
|
|
||||||
<div className="card bg-light border-0 rounded-3">
|
|
||||||
<div className="card-body py-3">
|
|
||||||
<div className="d-flex align-items-center justify-content-center">
|
|
||||||
<i className="bi bi-envelope-fill text-primary me-2"></i>
|
|
||||||
<strong className="me-2">Correo:</strong>
|
|
||||||
<span className="text-primary fw-medium">
|
|
||||||
{participante}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="d-grid gap-2 d-md-flex justify-content-center">
|
<div className="d-grid gap-2 d-md-flex justify-content-center">
|
||||||
<Button
|
<Button
|
||||||
variant="success"
|
variant="success"
|
||||||
@@ -250,7 +265,8 @@ export default function Page() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
setEnableScan(true);
|
setEnableScan(true);
|
||||||
setScannedData(null);
|
setTokenValid(false);
|
||||||
|
setToken('');
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 rounded-pill"
|
className="px-4 py-2 rounded-pill"
|
||||||
>
|
>
|
||||||
@@ -260,6 +276,7 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
// Modal de error
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<i
|
<i
|
||||||
@@ -267,7 +284,20 @@ export default function Page() {
|
|||||||
style={{ fontSize: '2.5rem' }}
|
style={{ fontSize: '2.5rem' }}
|
||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<h6 className="text-danger mb-0">QR inválido o malformado</h6>
|
<h6 className="text-danger mb-0">{statusMessage}</h6>
|
||||||
|
<div className="mt-3">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowModal(false);
|
||||||
|
setEnableScan(true);
|
||||||
|
setStatusMessage('');
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 rounded-pill"
|
||||||
|
>
|
||||||
|
Cerrar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,158 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import Button from '@/components/button';
|
import Button from '@/components/button';
|
||||||
import Table, { Header } from '@/components/table';
|
import Table, { Header } from '@/components/table';
|
||||||
|
const Modal = dynamic(() => import('@/components/modal'), { ssr: false });
|
||||||
import { useGetApi } from '@/hooks/use-get-api';
|
import { useGetApi } from '@/hooks/use-get-api';
|
||||||
import { Administrador } from '@/types/administradores';
|
import { Administrador } from '@/types/administradores';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import axiosInstance from '@/utils/api-config';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { data } = useGetApi<Administrador[]>('/administrador');
|
const { data, setData } = useGetApi<Administrador[]>('/administrador');
|
||||||
|
|
||||||
|
// Estado del modal
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
|
const [modalMode, setModalMode] = useState<'add' | 'edit'>('add');
|
||||||
|
const [selectedUser, setSelectedUser] = useState<Administrador | null>(null);
|
||||||
|
|
||||||
|
// Estado del modal de eliminación
|
||||||
|
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
|
||||||
|
const [userToDelete, setUserToDelete] = useState<Administrador | null>(null);
|
||||||
|
|
||||||
|
// Estado del formulario
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
nombre_usuario: '',
|
||||||
|
correo: '',
|
||||||
|
password: '',
|
||||||
|
id_tipo_user: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handlers del modal
|
||||||
|
const handleOpenAddModal = () => {
|
||||||
|
setModalMode('add');
|
||||||
|
setSelectedUser(null);
|
||||||
|
setFormData({
|
||||||
|
nombre_usuario: '',
|
||||||
|
correo: '',
|
||||||
|
password: '',
|
||||||
|
id_tipo_user: '',
|
||||||
|
});
|
||||||
|
setIsModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenEditModal = (user: Administrador) => {
|
||||||
|
setModalMode('edit');
|
||||||
|
setSelectedUser(user);
|
||||||
|
setFormData({
|
||||||
|
nombre_usuario: user.nombre_usuario,
|
||||||
|
correo: user.correo,
|
||||||
|
password: '',
|
||||||
|
id_tipo_user: user.tipoUser.id.toString(),
|
||||||
|
});
|
||||||
|
setIsModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setIsModalVisible(false);
|
||||||
|
setSelectedUser(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handlers del modal de eliminación
|
||||||
|
const handleOpenDeleteModal = (user: Administrador) => {
|
||||||
|
setUserToDelete(user);
|
||||||
|
setIsDeleteModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseDeleteModal = () => {
|
||||||
|
setIsDeleteModalVisible(false);
|
||||||
|
setUserToDelete(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDelete = async () => {
|
||||||
|
// Aquí iría la lógica para eliminar el usuario
|
||||||
|
try {
|
||||||
|
if (userToDelete) {
|
||||||
|
// Lógica para eliminar el usuario
|
||||||
|
const response = await axiosInstance.delete(
|
||||||
|
`/administrador/${userToDelete.id_administrador}`
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Usuario eliminado:', response.data);
|
||||||
|
// Actualizar la lista de usuarios en el estado
|
||||||
|
if (data) {
|
||||||
|
const updatedData = data.filter(
|
||||||
|
(user) => user.id_administrador !== userToDelete.id_administrador
|
||||||
|
);
|
||||||
|
setData(updatedData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al eliminar el usuario:', error);
|
||||||
|
}
|
||||||
|
handleCloseDeleteModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handler para envío del formulario
|
||||||
|
const handleSubmitForm = async () => {
|
||||||
|
try {
|
||||||
|
if (modalMode === 'add') {
|
||||||
|
// Crear nuevo usuario
|
||||||
|
const payload = {
|
||||||
|
nombre_usuario: formData.nombre_usuario,
|
||||||
|
correo: formData.correo,
|
||||||
|
password: formData.password,
|
||||||
|
id_tipo_user: parseInt(formData.id_tipo_user),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axiosInstance.post('/administrador', payload);
|
||||||
|
console.log('Usuario creado:', response.data);
|
||||||
|
|
||||||
|
// Actualizar la lista de usuarios
|
||||||
|
if (data) {
|
||||||
|
setData([
|
||||||
|
...data,
|
||||||
|
{
|
||||||
|
correo: formData.correo,
|
||||||
|
id_administrador: response.data.id_administrador,
|
||||||
|
nombre_usuario: formData.nombre_usuario,
|
||||||
|
tipoUser: {
|
||||||
|
id: parseInt(formData.id_tipo_user),
|
||||||
|
tipo: formData.id_tipo_user === '1' ? 'Administrador' : 'Staff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else if (modalMode === 'edit' && selectedUser) {
|
||||||
|
// Editar usuario existente
|
||||||
|
const payload = {
|
||||||
|
nombre_usuario: formData.nombre_usuario,
|
||||||
|
correo: formData.correo,
|
||||||
|
id_tipo_user: parseInt(formData.id_tipo_user),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axiosInstance.put(
|
||||||
|
`/administrador/${selectedUser.id_administrador}`,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
console.log('Usuario actualizado:', response.data);
|
||||||
|
|
||||||
|
// Actualizar la lista de usuarios
|
||||||
|
if (data) {
|
||||||
|
const updatedData = data.map((user) =>
|
||||||
|
user.id_administrador === selectedUser.id_administrador
|
||||||
|
? { ...user, ...response.data }
|
||||||
|
: user
|
||||||
|
);
|
||||||
|
setData(updatedData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseModal();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al guardar el usuario:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const headers: Header<Administrador>[] = [
|
const headers: Header<Administrador>[] = [
|
||||||
{ key: 'id_administrador', label: 'ID' },
|
{ key: 'id_administrador', label: 'ID' },
|
||||||
@@ -19,12 +165,16 @@ export default function Page() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'tipoUser',
|
key: 'tipoUser',
|
||||||
render: () => (
|
render: (_, row) => (
|
||||||
<div className="d-flex gap-2">
|
<div className="d-flex gap-2">
|
||||||
<Button className="w-100" disabled>
|
<Button className="w-100" onClick={() => handleOpenEditModal(row)}>
|
||||||
Editar
|
Editar
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="danger" className="w-100" disabled>
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
className="w-100"
|
||||||
|
onClick={() => handleOpenDeleteModal(row)}
|
||||||
|
>
|
||||||
Eliminar
|
Eliminar
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,9 +187,166 @@ export default function Page() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="d-flex justify-content-between align-items-center my-3">
|
<div className="d-flex justify-content-between align-items-center my-3">
|
||||||
<h1>Administradores</h1>
|
<h1>Administradores</h1>
|
||||||
<Button variant="primary">Agregar Administrador</Button>
|
<Button variant="primary" onClick={handleOpenAddModal}>
|
||||||
|
Agregar Administrador
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{data && <Table headers={headers} data={data} />}
|
{data && <Table headers={headers} data={data} />}
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isVisible={isModalVisible}
|
||||||
|
onClose={handleCloseModal}
|
||||||
|
size="lg"
|
||||||
|
closeButton={true}
|
||||||
|
>
|
||||||
|
<div className="p-3">
|
||||||
|
<h3 className="mb-4">
|
||||||
|
{modalMode === 'add'
|
||||||
|
? 'Agregar Nuevo Administrador'
|
||||||
|
: 'Editar Administrador'}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{modalMode === 'edit' && selectedUser && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<p>
|
||||||
|
<strong>ID:</strong> {selectedUser.id_administrador}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Usuario actual:</strong> {selectedUser.nombre_usuario}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Correo actual:</strong> {selectedUser.correo}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Tipo:</strong> {selectedUser.tipoUser.tipo}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="nombre_usuario" className="form-label">
|
||||||
|
Nombre de Usuario
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="nombre_usuario"
|
||||||
|
value={formData.nombre_usuario}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
nombre_usuario: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="Ingrese el nombre de usuario"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="correo" className="form-label">
|
||||||
|
Correo Electrónico
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="form-control"
|
||||||
|
id="correo"
|
||||||
|
value={formData.correo}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
correo: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="Ingrese el correo electrónico"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{modalMode === 'add' && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="password" className="form-label">
|
||||||
|
Contraseña
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-control"
|
||||||
|
id="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
password: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="Ingrese la contraseña"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="tipoUser" className="form-label">
|
||||||
|
Tipo de Usuario
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
id="tipoUser"
|
||||||
|
value={formData.id_tipo_user}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
id_tipo_user: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">Seleccione un tipo</option>
|
||||||
|
<option value="1">Administrador</option>
|
||||||
|
<option value="2">Staff</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="d-flex gap-2 justify-content-end">
|
||||||
|
<Button variant="secondary" onClick={handleCloseModal}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={handleSubmitForm}>
|
||||||
|
{modalMode === 'add' ? 'Crear Administrador' : 'Guardar Cambios'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Modal de confirmación de eliminación */}
|
||||||
|
<Modal
|
||||||
|
isVisible={isDeleteModalVisible}
|
||||||
|
onClose={handleCloseDeleteModal}
|
||||||
|
size="md"
|
||||||
|
closeButton={true}
|
||||||
|
>
|
||||||
|
<div className="p-4 text-center">
|
||||||
|
<div className="mb-4">
|
||||||
|
<i className="bi bi-exclamation-triangle text-warning display-1"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 className="mb-3">¿Está seguro de eliminar al usuario?</h4>
|
||||||
|
|
||||||
|
{userToDelete && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="fs-5 fw-bold text-danger">
|
||||||
|
{userToDelete.nombre_usuario}
|
||||||
|
</p>
|
||||||
|
<p className="text-muted">Esta acción no se puede deshacer</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="d-flex gap-3 justify-content-center">
|
||||||
|
<Button variant="secondary" onClick={handleCloseDeleteModal}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button variant="danger" onClick={handleConfirmDelete}>
|
||||||
|
Sí, Eliminar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,13 +34,15 @@ export default function LoginForm() {
|
|||||||
password: loginData.password,
|
password: loginData.password,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(response)
|
||||||
|
|
||||||
// Guardar el token en sessionStorage
|
// Guardar el token en sessionStorage
|
||||||
if (response.data.token) {
|
if (response.data.token) {
|
||||||
sessionStorage.setItem('token', response.data.token);
|
sessionStorage.setItem('token', response.data.token);
|
||||||
|
|
||||||
// Opcional: guardar información adicional del usuario si viene en la respuesta
|
// Opcional: guardar información adicional del usuario si viene en la respuesta
|
||||||
if (response.data.user) {
|
if (response.data.tipo_usuario) {
|
||||||
sessionStorage.setItem('user', JSON.stringify(response.data.user));
|
sessionStorage.setItem('axxxd', JSON.stringify(response.data.tipo_usuario));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redireccionar al dashboard
|
// Redireccionar al dashboard
|
||||||
|
|||||||
@@ -60,6 +60,30 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/*
|
||||||
|
{!loading && data && data.length > 0 && (
|
||||||
|
<div className="row">
|
||||||
|
{data.flatMap((evento) =>
|
||||||
|
evento.cuestionarios
|
||||||
|
// Filtramos solo los cuestionarios con id_cuestionario >= 148
|
||||||
|
.filter((cuestionario) => !cuestionario.id_cuestionario > 10 && !cuestionario.id_cuestionario < 148)
|
||||||
|
// Luego mapeamos para renderizarlos
|
||||||
|
.map((cuestionario, index) => (
|
||||||
|
<div
|
||||||
|
className={`col-md-6 col-lg-4 my-3 fade-in-up-bounce delay-${(index % 5) + 1}`}
|
||||||
|
key={`${evento.id_evento}-${cuestionario.id_cuestionario}`}
|
||||||
|
>
|
||||||
|
<FormularioCardUser
|
||||||
|
evento={evento}
|
||||||
|
formulario={cuestionario}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
*/}
|
||||||
|
|
||||||
{!loading && data && data.length > 0 && (
|
{!loading && data && data.length > 0 && (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
{data.flatMap((evento) =>
|
{data.flatMap((evento) =>
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ export default function FormularioCardUser({
|
|||||||
const imgSrc = formulario.banner
|
const imgSrc = formulario.banner
|
||||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${formulario.banner}`
|
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${formulario.banner}`
|
||||||
: evento.banner
|
: evento.banner
|
||||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`
|
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`
|
||||||
: `/default-banner.png`;
|
: `/default-banner.png`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -143,7 +143,7 @@ export default function FormularioCardUser({
|
|||||||
<div className="row mb-3">
|
<div className="row mb-3">
|
||||||
{/* Columna de horario - solo visible si es el mismo día */}
|
{/* Columna de horario - solo visible si es el mismo día */}
|
||||||
{esElMismoDia && (
|
{esElMismoDia && (
|
||||||
<div className="col-6">
|
<div className="col-7">
|
||||||
<div
|
<div
|
||||||
className={`py-2 px-3 rounded ${
|
className={`py-2 px-3 rounded ${
|
||||||
sinCupos ? 'bg-primary-subtle' : 'bg-light'
|
sinCupos ? 'bg-primary-subtle' : 'bg-light'
|
||||||
@@ -154,7 +154,7 @@ export default function FormularioCardUser({
|
|||||||
<div>
|
<div>
|
||||||
<small className="text-muted d-block">Horario</small>
|
<small className="text-muted d-block">Horario</small>
|
||||||
<span className={`fw-semibold text-azul`}>
|
<span className={`fw-semibold text-azul`}>
|
||||||
{horarioFormateado}
|
{horarioFormateado} horas.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,7 +162,7 @@ export default function FormularioCardUser({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Columna de cupos - siempre visible */}
|
{/* Columna de cupos - siempre visible */}
|
||||||
<div className={esElMismoDia ? 'col-6' : 'col-12'}>
|
<div className={esElMismoDia ? 'col-4' : 'col-12'}>
|
||||||
<div
|
<div
|
||||||
className={`py-2 px-3 rounded ${
|
className={`py-2 px-3 rounded ${
|
||||||
sinCupos ? 'bg-primary-subtle' : 'bg-light'
|
sinCupos ? 'bg-primary-subtle' : 'bg-light'
|
||||||
@@ -171,7 +171,7 @@ export default function FormularioCardUser({
|
|||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<i className={`bi bi-people-fill me-2 text-primary`}></i>
|
<i className={`bi bi-people-fill me-2 text-primary`}></i>
|
||||||
<div>
|
<div>
|
||||||
<small className="text-muted d-block">Cupos</small>
|
<small className="text-muted d-block">Registro</small>
|
||||||
<span className={`fw-semibold text-primary`}>
|
<span className={`fw-semibold text-primary`}>
|
||||||
{formulario.cupo_maximo !== null
|
{formulario.cupo_maximo !== null
|
||||||
? `${formulario.cupos_usados} / ${formulario.cupo_maximo}`
|
? `${formulario.cupos_usados} / ${formulario.cupo_maximo}`
|
||||||
|
|||||||
+69
-13
@@ -3,18 +3,51 @@ import React from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
type NavChild = {
|
||||||
|
href: string; // siempre string
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NavItem =
|
||||||
|
| { href: string; label: string } // items normales
|
||||||
|
| { label: string; children: NavChild[] }; // items con submenu
|
||||||
|
|
||||||
const Navbar: React.FC = () => {
|
const Navbar: React.FC = () => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const tipo_usuario = sessionStorage.getItem('axxxd')?.replace(/"/g, '') || '';
|
||||||
|
|
||||||
const navItems = [
|
const navItemsAdmin: NavItem[] = [
|
||||||
{ href: '/', label: 'Inicio' },
|
{ href: '/', label: 'Inicio' },
|
||||||
{ href: '/user/eventos', label: 'Eventos' },
|
{ href: '/user/eventos', label: 'Eventos' },
|
||||||
{ href: '/user/qr', label: 'Lector de QRs' },
|
{ href: '/user/qr', label: 'Lector de QRs' },
|
||||||
{ href: '/user/eventos/crear', label: 'Crear Evento' },
|
{ href: '/user/eventos/crear', label: 'Crear Evento' },
|
||||||
|
{
|
||||||
|
label: 'Carga Masiva',
|
||||||
|
children: [
|
||||||
|
{ href: '/user/eventos/carga_masiva', label: 'Carga Masiva Eventos' },
|
||||||
|
{
|
||||||
|
href: '/user/eventos/carga_masiva_banner',
|
||||||
|
label: 'Carga Masiva Imagenes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/user/carga_masiva_usuarios',
|
||||||
|
label: 'Carga Masiva Usuarios',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{ href: '/user/usuarios', label: 'Usuarios' },
|
{ href: '/user/usuarios', label: 'Usuarios' },
|
||||||
{ href: '/signout', label: 'Cerrar sesión' },
|
{ href: '/login', label: 'Cerrar sesión' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const navItemsUser: NavItem[] = [
|
||||||
|
{ href: '/user/eventos', label: 'Eventos' },
|
||||||
|
{ href: '/user/qr', label: 'Lector de QRs' },
|
||||||
|
{ href: '/login', label: 'Cerrar sesión' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const navItems =
|
||||||
|
tipo_usuario === 'administrador' ? navItemsAdmin : navItemsUser;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="navbar navbar-expand-lg navbar-dark bg-azul">
|
<nav className="navbar navbar-expand-lg navbar-dark bg-azul">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
@@ -33,7 +66,7 @@ const Navbar: React.FC = () => {
|
|||||||
id="offcanvasNavbar"
|
id="offcanvasNavbar"
|
||||||
aria-labelledby="offcanvasNavbarLabel"
|
aria-labelledby="offcanvasNavbarLabel"
|
||||||
>
|
>
|
||||||
<div className="offcanvas-header">
|
<div className="offcanvas-header bg-azul text-white">
|
||||||
<h5 className="offcanvas-title" id="offcanvasNavbarLabel">
|
<h5 className="offcanvas-title" id="offcanvasNavbarLabel">
|
||||||
Menu
|
Menu
|
||||||
</h5>
|
</h5>
|
||||||
@@ -44,7 +77,7 @@ const Navbar: React.FC = () => {
|
|||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
<div className="offcanvas-body align-items-center justify-content-between">
|
<div className="offcanvas-body align-items-center justify-content-between bg-azul">
|
||||||
<h2 className="h5 mb-0 text-white">
|
<h2 className="h5 mb-0 text-white">
|
||||||
Sistema de Registro de Eventos
|
Sistema de Registro de Eventos
|
||||||
</h2>
|
</h2>
|
||||||
@@ -52,17 +85,40 @@ const Navbar: React.FC = () => {
|
|||||||
{navItems.map((item, index) => (
|
{navItems.map((item, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className={`nav-item`}
|
className="nav-item"
|
||||||
data-bs-dismiss="offcanvas"
|
data-bs-dismiss="offcanvas"
|
||||||
>
|
>
|
||||||
<Link
|
{'href' in item ? (
|
||||||
className={`nav-link ${
|
<Link
|
||||||
pathname === item.href ? 'text-white' : ''
|
className={`nav-link ${
|
||||||
}`}
|
pathname === item.href ? 'text-white' : ''
|
||||||
href={item.href}
|
}`}
|
||||||
>
|
href={item.href}
|
||||||
{item.label}
|
>
|
||||||
</Link>
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div className="nav-item dropdown">
|
||||||
|
<a
|
||||||
|
className="nav-link dropdown-toggle"
|
||||||
|
href="#"
|
||||||
|
role="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
<ul className="dropdown-menu">
|
||||||
|
{item.children.map((child: NavChild, idx: number) => (
|
||||||
|
<li key={idx}>
|
||||||
|
<Link className="dropdown-item" href={child.href}>
|
||||||
|
{child.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -214,6 +214,13 @@ export default function FormularioEditor({
|
|||||||
</div>
|
</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="row">
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export default function EditEvento({
|
|||||||
handleChange,
|
handleChange,
|
||||||
handleOnChange,
|
handleOnChange,
|
||||||
}: EditEventoProps) {
|
}: EditEventoProps) {
|
||||||
|
const tipo_usuario = sessionStorage.getItem('axxxd')?.replace(/"/g, '') || '';
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [nuevoBanner, setNuevoBanner] = useState<File | null>(null);
|
const [nuevoBanner, setNuevoBanner] = useState<File | null>(null);
|
||||||
|
|
||||||
@@ -102,17 +104,19 @@ export default function EditEvento({
|
|||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{/* Banner */}
|
{/* Banner */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<ImageUploader
|
{tipo_usuario === 'administrador' && (
|
||||||
label="Banner del evento"
|
<>
|
||||||
defaultBanner={
|
<ImageUploader
|
||||||
evento.banner
|
label="Banner del evento"
|
||||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`
|
defaultBanner={
|
||||||
: undefined
|
evento.banner
|
||||||
|
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
onChange={(file) => {
|
onChange={(file) => {
|
||||||
setNuevoBanner(file);
|
setNuevoBanner(file);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="text-muted mt-2 small">
|
<p className="text-muted mt-2 small">
|
||||||
<strong>
|
<strong>
|
||||||
<i className="bi bi-info-circle-fill me-2"></i>
|
<i className="bi bi-info-circle-fill me-2"></i>
|
||||||
@@ -129,6 +133,8 @@ export default function EditEvento({
|
|||||||
Formatos soportados: PNG, JPG, JPEG.
|
Formatos soportados: PNG, JPG, JPEG.
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Detalles del evento */}
|
{/* Detalles del evento */}
|
||||||
@@ -142,6 +148,8 @@ export default function EditEvento({
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleChange('nombre_evento', e.target.value)
|
handleChange('nombre_evento', e.target.value)
|
||||||
}
|
}
|
||||||
|
disabled={tipo_usuario !== 'administrador'}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
@@ -150,6 +158,7 @@ export default function EditEvento({
|
|||||||
<MDEditor
|
<MDEditor
|
||||||
value={evento.descripcion_evento ?? ''}
|
value={evento.descripcion_evento ?? ''}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
|
if (tipo_usuario !== 'administrador') return;
|
||||||
const texto = value || '';
|
const texto = value || '';
|
||||||
if (texto.length <= MAX_LENGTH) {
|
if (texto.length <= MAX_LENGTH) {
|
||||||
handleChange('descripcion_evento', texto);
|
handleChange('descripcion_evento', texto);
|
||||||
@@ -167,6 +176,7 @@ export default function EditEvento({
|
|||||||
placeholder="Selecciona un tipo de evento"
|
placeholder="Selecciona un tipo de evento"
|
||||||
onChange={(e) => handleChange('tipo_evento', e.target.value)}
|
onChange={(e) => handleChange('tipo_evento', e.target.value)}
|
||||||
options={tipoEventos}
|
options={tipoEventos}
|
||||||
|
disabled={tipo_usuario !== 'administrador'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
@@ -197,11 +207,11 @@ export default function EditEvento({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Botón de guardar */}
|
{/* Botón de guardar */}
|
||||||
<div className="d-flex justify-content-end mt-4">
|
{tipo_usuario === "administrador" && <div className="d-flex justify-content-end mt-4">
|
||||||
<Button icon="save" disabled={loading} onClick={handleOnSave}>
|
<Button icon="save" disabled={loading} onClick={handleOnSave}>
|
||||||
{loading ? 'Guardando...' : 'Guardar Cambios'}
|
{loading ? 'Guardando...' : 'Guardar Cambios'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export default function EditFormulario({
|
|||||||
evento,
|
evento,
|
||||||
handleChange,
|
handleChange,
|
||||||
}: EditFormularioProps) {
|
}: EditFormularioProps) {
|
||||||
|
const tipo_usuario = sessionStorage.getItem('axxxd')?.replace(/"/g, '') || '';
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [nuevoBanner, setNuevoBanner] = useState<File | null>(null);
|
const [nuevoBanner, setNuevoBanner] = useState<File | null>(null);
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ export default function EditFormulario({
|
|||||||
fecha_inicio: cuestionario.fecha_inicio,
|
fecha_inicio: cuestionario.fecha_inicio,
|
||||||
fecha_fin: cuestionario.fecha_fin,
|
fecha_fin: cuestionario.fecha_fin,
|
||||||
cupo_maximo: Number(cuestionario.cupo_maximo) || null,
|
cupo_maximo: Number(cuestionario.cupo_maximo) || null,
|
||||||
|
mensaje_correo: cuestionario.mensaje_correo || null,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -146,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
|
<SimpleInput
|
||||||
label="Cupo máximo (Si no se requiere, dejar en blanco)"
|
label="Cupo máximo (Si no se requiere, dejar en blanco)"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -213,11 +223,13 @@ export default function EditFormulario({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Botón de guardar */}
|
{/* Botón de guardar */}
|
||||||
<div className="d-flex justify-content-end mt-4">
|
{tipo_usuario === "administrador" && (
|
||||||
<Button icon="save" disabled={loading} onClick={handleOnSave}>
|
<div className="d-flex justify-content-end mt-4">
|
||||||
{loading ? 'Guardando...' : 'Guardar Cambios'}
|
<Button icon="save" disabled={loading} onClick={handleOnSave}>
|
||||||
</Button>
|
{loading ? 'Guardando...' : 'Guardar Cambios'}
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,591 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useGetApi } from '@/hooks/use-get-api';
|
||||||
|
import { GetCuestionario } from '@/types/evento';
|
||||||
|
import SimpleInput from '@/components/input';
|
||||||
|
import RadioOptionGroup, { RadioOption } from '@/components/radio-option-group';
|
||||||
|
import axiosInstance from '@/utils/api-config';
|
||||||
|
import PrefetchAbiertaCorta from './prefetch-abierta-corta';
|
||||||
|
import Button from '@/components/button';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { getAxiosError } from '@/utils/errors-utils';
|
||||||
|
import { SubmitResponse } from '@/types/submit';
|
||||||
|
|
||||||
|
export type UsuarioDataResponse = {
|
||||||
|
cuenta: string | null;
|
||||||
|
nombre: string | null;
|
||||||
|
apellidos: string | null;
|
||||||
|
carrera: string | null;
|
||||||
|
genero: string | null;
|
||||||
|
rfc?: string | null; // Solo para trabajadores
|
||||||
|
};
|
||||||
|
|
||||||
|
type UsuarioData = {
|
||||||
|
nombre: string; // alumno o trabajador
|
||||||
|
apellidos: string; // alumno o trabajador
|
||||||
|
correo: string; // alumno o trabajador (en los alumnos se usa el id_ncuenta en trabajadores ellos deben escribirlo)
|
||||||
|
genero: 'M' | 'F'; // alumno o trabajador
|
||||||
|
carrera: string; // solo para alumnos
|
||||||
|
rfc?: string; // solo para trabajadores se usa para buscar información de cuenta
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmativeOption = ['Sí', 'Si', 'Si, claro', 'Claro que sí'];
|
||||||
|
const negativeOption = ['No', 'No, gracias', 'No, de momento'];
|
||||||
|
|
||||||
|
type RespuestaFormulario = Record<string, string | number>; // id_pregunta: respuesta
|
||||||
|
|
||||||
|
export default function FormularioRegistro({
|
||||||
|
id_cuestionario,
|
||||||
|
handleSubmitFormulario,
|
||||||
|
}: {
|
||||||
|
evento: string;
|
||||||
|
cuestionario: string;
|
||||||
|
id_evento: number;
|
||||||
|
id_cuestionario: number;
|
||||||
|
handleSubmitFormulario: (data: SubmitResponse) => void;
|
||||||
|
}) {
|
||||||
|
// ------------------------
|
||||||
|
// Estados
|
||||||
|
// ------------------------
|
||||||
|
const [isComunidad, setIsComunidad] = useState<RadioOption<number>>();
|
||||||
|
const [respuestas, setRespuestas] = useState<RespuestaFormulario>({});
|
||||||
|
const [cuentaInfo, setCuentaInfo] = useState<UsuarioData | null>(null);
|
||||||
|
const [isSending, setIsSending] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// ------------------------
|
||||||
|
// Carga del cuestionario
|
||||||
|
// ------------------------
|
||||||
|
const { data, error } = useGetApi<GetCuestionario>(
|
||||||
|
`/cuestionario/${id_cuestionario}/formulario`
|
||||||
|
);
|
||||||
|
// ------------------------
|
||||||
|
// Extracción de preguntas clave
|
||||||
|
// ------------------------
|
||||||
|
const preguntas = data?.cuestionario.secciones.flatMap((seccion) =>
|
||||||
|
seccion.preguntas.map((pregunta) => pregunta.pregunta)
|
||||||
|
);
|
||||||
|
|
||||||
|
const preguntaComunidad = preguntas?.find(
|
||||||
|
(pregunta) =>
|
||||||
|
pregunta.validacion === 'comunidad_alumno' ||
|
||||||
|
pregunta.validacion === 'comunidad_trabajador'
|
||||||
|
);
|
||||||
|
|
||||||
|
const preguntaCuenta = preguntas?.find(
|
||||||
|
(pregunta) =>
|
||||||
|
pregunta.validacion === 'cuenta_alumno' ||
|
||||||
|
pregunta.validacion === 'cuenta_trabajador' ||
|
||||||
|
pregunta.validacion === 'rfc'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Solución: Agregar estado para controlar si ya se hizo la búsqueda
|
||||||
|
const [cuentaBuscada, setCuentaBuscada] = useState<string>(''); // Nuevo estado
|
||||||
|
|
||||||
|
// ------------------------
|
||||||
|
// Efecto: buscar información de cuenta
|
||||||
|
// ------------------------
|
||||||
|
useEffect(() => {
|
||||||
|
const cuentaId = preguntaCuenta?.id_pregunta ?? '';
|
||||||
|
const cuenta = respuestas[cuentaId];
|
||||||
|
|
||||||
|
const esCuentaAlumno = preguntaCuenta?.validacion === 'cuenta_alumno';
|
||||||
|
const esCuentaTrabajador =
|
||||||
|
preguntaCuenta?.validacion === 'cuenta_trabajador' ||
|
||||||
|
preguntaCuenta?.validacion === 'rfc';
|
||||||
|
|
||||||
|
const puedeBuscar =
|
||||||
|
cuenta &&
|
||||||
|
typeof cuenta === 'string' &&
|
||||||
|
((esCuentaAlumno && cuenta.length === 9) ||
|
||||||
|
(esCuentaTrabajador && cuenta.length === 10));
|
||||||
|
|
||||||
|
const fetchCuentaInfo = async () => {
|
||||||
|
try {
|
||||||
|
const endpoint = esCuentaAlumno
|
||||||
|
? `/alumnos/${cuenta}`
|
||||||
|
: `/trabajadores/${cuenta}`;
|
||||||
|
|
||||||
|
const res = await axiosInstance.get<UsuarioDataResponse>(endpoint);
|
||||||
|
|
||||||
|
console.log('Datos de cuenta obtenidos:', res.data);
|
||||||
|
|
||||||
|
// Verificar si realmente hay datos válidos
|
||||||
|
const hayDatosValidos =
|
||||||
|
res.data.nombre ||
|
||||||
|
res.data.apellidos ||
|
||||||
|
res.data.carrera ||
|
||||||
|
res.data.genero;
|
||||||
|
|
||||||
|
if (hayDatosValidos) {
|
||||||
|
setCuentaInfo({
|
||||||
|
nombre: res.data.nombre ? res.data.nombre : '',
|
||||||
|
apellidos: res.data.apellidos ? res.data.apellidos : '',
|
||||||
|
correo: res.data.cuenta
|
||||||
|
? `${res.data.cuenta}@pcpuma.acatlan.unam.mx`
|
||||||
|
: '',
|
||||||
|
genero:
|
||||||
|
res.data.genero === 'M' || res.data.genero === 'F'
|
||||||
|
? res.data.genero
|
||||||
|
: 'M',
|
||||||
|
carrera: res.data.carrera ? res.data.carrera : '',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Si no hay datos válidos, establecer como null
|
||||||
|
setCuentaInfo(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marcar que ya se buscó esta cuenta
|
||||||
|
setCuentaBuscada(cuenta as string);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al obtener datos de cuenta:', error);
|
||||||
|
setCuentaInfo(null);
|
||||||
|
setCuentaBuscada(cuenta as string); // También marcar en caso de error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Solo buscar si puede buscar Y no se ha buscado ya esta cuenta
|
||||||
|
if (puedeBuscar && cuentaBuscada !== cuenta) {
|
||||||
|
fetchCuentaInfo();
|
||||||
|
} else if (!puedeBuscar) {
|
||||||
|
setCuentaInfo(null);
|
||||||
|
setCuentaBuscada(''); // Reset cuando no se puede buscar
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
respuestas,
|
||||||
|
preguntaCuenta?.id_pregunta,
|
||||||
|
preguntaCuenta?.validacion,
|
||||||
|
cuentaBuscada,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// También resetear cuentaBuscada cuando cambie el número de cuenta
|
||||||
|
useEffect(() => {
|
||||||
|
const cuentaId = preguntaCuenta?.id_pregunta ?? '';
|
||||||
|
const cuenta = respuestas[cuentaId];
|
||||||
|
|
||||||
|
// Si la cuenta cambió, resetear el estado de búsqueda
|
||||||
|
if (cuentaBuscada && cuenta !== cuentaBuscada) {
|
||||||
|
setCuentaBuscada('');
|
||||||
|
}
|
||||||
|
}, [respuestas, preguntaCuenta?.id_pregunta, cuentaBuscada]);
|
||||||
|
|
||||||
|
// ------------------------
|
||||||
|
// Efecto: precargar respuestas si hay cuentaInfo
|
||||||
|
// ------------------------
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cuentaInfo) {
|
||||||
|
if (data?.cuestionario.secciones) {
|
||||||
|
const preguntas = data.cuestionario.secciones.flatMap((seccion) =>
|
||||||
|
seccion.preguntas.map((pregunta) => pregunta.pregunta)
|
||||||
|
);
|
||||||
|
|
||||||
|
const idsPrefetch = preguntas
|
||||||
|
.filter((pregunta) =>
|
||||||
|
[
|
||||||
|
'nombre',
|
||||||
|
'apellidos',
|
||||||
|
'correo',
|
||||||
|
'institucion',
|
||||||
|
'carrera',
|
||||||
|
'genero',
|
||||||
|
].includes(pregunta.validacion)
|
||||||
|
)
|
||||||
|
.map((pregunta) => pregunta.id_pregunta);
|
||||||
|
|
||||||
|
setRespuestas((prev) => {
|
||||||
|
const nuevas = { ...prev };
|
||||||
|
idsPrefetch.forEach((id) => {
|
||||||
|
delete nuevas[id];
|
||||||
|
});
|
||||||
|
return nuevas;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nuevasRespuestas: RespuestaFormulario = {};
|
||||||
|
|
||||||
|
if (data?.cuestionario.secciones) {
|
||||||
|
const preguntas = data.cuestionario.secciones.flatMap((seccion) =>
|
||||||
|
seccion.preguntas.map((pregunta) => pregunta.pregunta)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const pregunta of preguntas) {
|
||||||
|
const id = pregunta.id_pregunta;
|
||||||
|
switch (pregunta.validacion) {
|
||||||
|
case 'nombre':
|
||||||
|
nuevasRespuestas[id] = cuentaInfo.nombre;
|
||||||
|
break;
|
||||||
|
case 'apellidos':
|
||||||
|
nuevasRespuestas[id] = cuentaInfo.apellidos;
|
||||||
|
break;
|
||||||
|
case 'correo':
|
||||||
|
nuevasRespuestas[id] = cuentaInfo.correo;
|
||||||
|
break;
|
||||||
|
case 'institucion':
|
||||||
|
nuevasRespuestas[id] = 'FES Acatlán';
|
||||||
|
break;
|
||||||
|
case 'carrera':
|
||||||
|
nuevasRespuestas[id] = cuentaInfo.carrera;
|
||||||
|
break;
|
||||||
|
case 'genero':
|
||||||
|
const generoTexto =
|
||||||
|
cuentaInfo.genero === 'M' ? 'Masculino' : 'Femenino';
|
||||||
|
const opcion = pregunta.opciones?.find(
|
||||||
|
(op) =>
|
||||||
|
op.opcion.opcion.toLowerCase() === generoTexto.toLowerCase()
|
||||||
|
);
|
||||||
|
if (opcion) {
|
||||||
|
nuevasRespuestas[id] = String(opcion.id_opcion);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRespuestas((prev) => ({
|
||||||
|
...prev,
|
||||||
|
...nuevasRespuestas,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [cuentaInfo, data?.cuestionario.secciones]);
|
||||||
|
|
||||||
|
// ------------------------
|
||||||
|
// Funciones auxiliares
|
||||||
|
// ------------------------
|
||||||
|
const actualizarRespuesta = (idPregunta: string, valor: string) => {
|
||||||
|
setRespuestas((prev) => ({ ...prev, [idPregunta]: valor }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnSubmit = async () => {
|
||||||
|
setIsSending(true);
|
||||||
|
if (!data?.cuestionario.id_cuestionario) {
|
||||||
|
alert('No se ha cargado correctamente el formulario');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preguntasObligatorias =
|
||||||
|
data?.cuestionario.secciones.flatMap((seccion) =>
|
||||||
|
seccion.preguntas
|
||||||
|
.filter((pregunta) => pregunta.pregunta.obligatoria)
|
||||||
|
.map((pregunta) => pregunta.pregunta)
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
const faltantes = preguntasObligatorias.filter(
|
||||||
|
(pregunta) =>
|
||||||
|
respuestas[pregunta.id_pregunta] === undefined ||
|
||||||
|
respuestas[pregunta.id_pregunta] === '' ||
|
||||||
|
respuestas[pregunta.id_pregunta] === null
|
||||||
|
);
|
||||||
|
|
||||||
|
const preguntaCorreo = preguntasObligatorias.find(
|
||||||
|
(pregunta) =>
|
||||||
|
pregunta.validacion === 'correo' ||
|
||||||
|
pregunta.validacion === 'correo_institucional'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (faltantes.length > 0) {
|
||||||
|
toast.error('Por favor responde todas las preguntas obligatorias.');
|
||||||
|
setIsSending(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar un posible correo en las respuestas si no hay cuentaInfo
|
||||||
|
let correo =
|
||||||
|
cuentaInfo?.correo === respuestas[preguntaCorreo?.id_pregunta ?? '']
|
||||||
|
? cuentaInfo?.correo
|
||||||
|
: respuestas[preguntaCorreo?.id_pregunta ?? ''];
|
||||||
|
|
||||||
|
if (!correo) {
|
||||||
|
// Buscar en las respuestas una que parezca correo
|
||||||
|
const correoRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
for (const valor of Object.values(respuestas)) {
|
||||||
|
if (typeof valor === 'string' && correoRegex.test(valor)) {
|
||||||
|
correo = valor;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
id_cuestionario: id_cuestionario,
|
||||||
|
correo: correo || '',
|
||||||
|
fecha_envio: new Date().toISOString(),
|
||||||
|
respuestas: Object.entries(respuestas).map(([id_pregunta, valor]) => ({
|
||||||
|
id_pregunta: Number(id_pregunta),
|
||||||
|
valor: isNaN(Number(valor)) ? valor : Number(valor),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await axiosInstance.post<SubmitResponse>(
|
||||||
|
'/cuestionario-respondido/submit',
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
toast(
|
||||||
|
() => (
|
||||||
|
<span
|
||||||
|
className="fs-4 pc-3 py-2"
|
||||||
|
style={{
|
||||||
|
maxWidth: '300px',
|
||||||
|
wordWrap: 'break-word',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{res.data.message.includes('ya ha respondido') ? (
|
||||||
|
<>
|
||||||
|
<i className="bi bi-exclamation-circle-fill text-warning me-2"></i>
|
||||||
|
Respuestas Enviadas Con Éxito
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<i className="bi bi-check-circle-fill text-success me-2"></i>
|
||||||
|
{res.data.message || '¡Formulario enviado!'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
duration: 6000,
|
||||||
|
position: 'bottom-center',
|
||||||
|
style: {
|
||||||
|
maxWidth: '425px',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
handleSubmitFormulario(res.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(getAxiosError(error));
|
||||||
|
toast.error('Hubo un error al enviar el formulario', {
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------------------------
|
||||||
|
// Condición para mostrar el formulario
|
||||||
|
// ------------------------
|
||||||
|
const mostrarFormularioRestante =
|
||||||
|
!preguntaComunidad || // Si no hay pregunta de comunidad, mostrar el formulario
|
||||||
|
(isComunidad &&
|
||||||
|
confirmativeOption.some((opt) =>
|
||||||
|
isComunidad.label?.toLowerCase().includes(opt.toLowerCase())
|
||||||
|
) &&
|
||||||
|
!!cuentaInfo) ||
|
||||||
|
(isComunidad &&
|
||||||
|
negativeOption.some((opt) =>
|
||||||
|
isComunidad.label?.toLowerCase().includes(opt.toLowerCase())
|
||||||
|
));
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error al cargar el cuestionario:', error);
|
||||||
|
return <div>Error al cargar el cuestionario</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{preguntaComunidad && (
|
||||||
|
<div className="my-4">
|
||||||
|
<label
|
||||||
|
className={`form-label ${
|
||||||
|
preguntaComunidad.obligatoria ? 'required' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{preguntaComunidad.pregunta}
|
||||||
|
</label>
|
||||||
|
<RadioOptionGroup
|
||||||
|
name="comunidad"
|
||||||
|
options={preguntaComunidad.opciones.map((op) => ({
|
||||||
|
label: op.opcion.opcion,
|
||||||
|
value: op.id_opcion,
|
||||||
|
}))}
|
||||||
|
selectedValue={isComunidad?.value}
|
||||||
|
onChange={(opt) => {
|
||||||
|
console.log('Opción comunidad seleccionada:', opt);
|
||||||
|
setIsComunidad(opt);
|
||||||
|
actualizarRespuesta(
|
||||||
|
preguntaComunidad.id_pregunta.toString(),
|
||||||
|
String(opt.value)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isComunidad &&
|
||||||
|
confirmativeOption.some((opt) =>
|
||||||
|
isComunidad.label?.toLowerCase().includes(opt.toLowerCase())
|
||||||
|
) &&
|
||||||
|
preguntaCuenta && (
|
||||||
|
<div className="my-4">
|
||||||
|
<label
|
||||||
|
className={`form-label ${
|
||||||
|
preguntaCuenta.obligatoria ? 'required' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{preguntaCuenta.pregunta}
|
||||||
|
</label>
|
||||||
|
<SimpleInput
|
||||||
|
type="text"
|
||||||
|
name="cuenta"
|
||||||
|
placeholder={
|
||||||
|
preguntaCuenta.validacion === 'cuenta_alumno'
|
||||||
|
? 'Ingrese su cuenta'
|
||||||
|
: preguntaCuenta?.validacion === 'rfc'
|
||||||
|
? 'Ingrese su RFC sin homoclave'
|
||||||
|
: preguntaCuenta?.validacion === 'cuenta_trabajador'
|
||||||
|
? 'Ingrese su numero de trabajador'
|
||||||
|
: 'Ingrese su ' + preguntaCuenta.pregunta
|
||||||
|
}
|
||||||
|
maxLength={10}
|
||||||
|
value={respuestas[preguntaCuenta.id_pregunta] || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setRespuestas((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[preguntaCuenta.id_pregunta]: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{mostrarFormularioRestante &&
|
||||||
|
data?.cuestionario.secciones.map((seccion, i) => (
|
||||||
|
<div key={i}>
|
||||||
|
{seccion.preguntas.map((pregunta) => {
|
||||||
|
// Evitar renderizar las preguntas ya manejadas
|
||||||
|
if (
|
||||||
|
pregunta.pregunta.id_pregunta ===
|
||||||
|
preguntaComunidad?.id_pregunta ||
|
||||||
|
pregunta.pregunta.id_pregunta === preguntaCuenta?.id_pregunta
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
pregunta.pregunta.tipo_pregunta.tipo_pregunta ===
|
||||||
|
'Abierta (Respuesta corta)'
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<PrefetchAbiertaCorta
|
||||||
|
key={pregunta.pregunta.id_pregunta}
|
||||||
|
obligatorio={pregunta.pregunta.obligatoria}
|
||||||
|
idPregunta={pregunta.pregunta.id_pregunta}
|
||||||
|
enunciado={pregunta.pregunta.pregunta}
|
||||||
|
validacion={pregunta.pregunta.validacion}
|
||||||
|
respuestas={respuestas}
|
||||||
|
cuentaInfo={cuentaInfo}
|
||||||
|
isComunidad={isComunidad?.label}
|
||||||
|
onChange={actualizarRespuesta}
|
||||||
|
preguntaComunidad={preguntaComunidad?.validacion ?? ''}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
pregunta.pregunta.tipo_pregunta.tipo_pregunta ===
|
||||||
|
'Abierta (Parrafo)'
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div key={pregunta.pregunta.id_pregunta} className="my-4">
|
||||||
|
<label
|
||||||
|
className={`form-label ${
|
||||||
|
pregunta.pregunta.obligatoria ? 'required' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pregunta.pregunta.pregunta}
|
||||||
|
</label>
|
||||||
|
<SimpleInput
|
||||||
|
type="textarea"
|
||||||
|
name={`respuesta_${pregunta.pregunta.id_pregunta}`}
|
||||||
|
placeholder="Ingrese una respuesta"
|
||||||
|
value={respuestas[pregunta.pregunta.id_pregunta] || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setRespuestas((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[pregunta.pregunta.id_pregunta]: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Cerrada') {
|
||||||
|
const opciones: RadioOption<number>[] =
|
||||||
|
pregunta.pregunta.opciones.map((op) => ({
|
||||||
|
label: op.opcion.opcion,
|
||||||
|
value: op.id_opcion,
|
||||||
|
}));
|
||||||
|
let respuestaActual =
|
||||||
|
respuestas[`${pregunta.pregunta.id_pregunta}`];
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!preguntaComunidad ||
|
||||||
|
(confirmativeOption.includes(isComunidad?.label || '') &&
|
||||||
|
cuentaInfo)) &&
|
||||||
|
respuestaActual === undefined // solo si no ha respondido
|
||||||
|
) {
|
||||||
|
if (pregunta.pregunta.validacion === 'genero' && cuentaInfo) {
|
||||||
|
const generoTexto =
|
||||||
|
cuentaInfo.genero === 'M' ? 'Masculino' : 'Femenino';
|
||||||
|
|
||||||
|
const opcionGenero = pregunta.pregunta.opciones.find(
|
||||||
|
(op) =>
|
||||||
|
op.opcion.opcion.toLowerCase() ===
|
||||||
|
generoTexto.toLowerCase()
|
||||||
|
);
|
||||||
|
if (opcionGenero) {
|
||||||
|
respuestaActual = String(opcionGenero.id_opcion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={pregunta.pregunta.id_pregunta}>
|
||||||
|
<label
|
||||||
|
className={`form-label ${
|
||||||
|
pregunta.pregunta.obligatoria ? 'required' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pregunta.pregunta.pregunta}
|
||||||
|
</label>
|
||||||
|
<RadioOptionGroup
|
||||||
|
name={`pregunta_${pregunta.pregunta.id_pregunta}`}
|
||||||
|
options={opciones}
|
||||||
|
selectedValue={
|
||||||
|
respuestaActual ? Number(respuestaActual) : undefined
|
||||||
|
}
|
||||||
|
onChange={(opt) => {
|
||||||
|
actualizarRespuesta(
|
||||||
|
`${pregunta.pregunta.id_pregunta}`,
|
||||||
|
String(opt.value)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={pregunta.pregunta.id_pregunta} className="my-4">
|
||||||
|
<label
|
||||||
|
className={`form-label ${
|
||||||
|
pregunta.pregunta.obligatoria ? 'required' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pregunta.pregunta.pregunta}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleOnSubmit}
|
||||||
|
disabled={!mostrarFormularioRestante || isSending}
|
||||||
|
>
|
||||||
|
{isSending ? 'Enviando...' : 'Enviar Formulario'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,15 +1,10 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React from 'react';
|
||||||
import { useGetApi } from '@/hooks/use-get-api';
|
import { SubmitResponse } from '@/types/submit';
|
||||||
import { GetCuestionario } from '@/types/evento';
|
|
||||||
import SimpleInput from '@/components/input';
|
import SimpleInput from '@/components/input';
|
||||||
import RadioOptionGroup, { RadioOption } from '@/components/radio-option-group';
|
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 Button from '@/components/button';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useFormulario } from '@/context/formulario';
|
||||||
import { getAxiosError } from '@/utils/errors-utils';
|
|
||||||
import { SubmitResponse } from '@/types/submit';
|
|
||||||
|
|
||||||
export type UsuarioDataResponse = {
|
export type UsuarioDataResponse = {
|
||||||
cuenta: string | null;
|
cuenta: string | null;
|
||||||
@@ -17,25 +12,10 @@ export type UsuarioDataResponse = {
|
|||||||
apellidos: string | null;
|
apellidos: string | null;
|
||||||
carrera: string | null;
|
carrera: string | null;
|
||||||
genero: string | null;
|
genero: string | null;
|
||||||
rfc?: string | null; // Solo para trabajadores
|
rfc?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UsuarioData = {
|
|
||||||
nombre: string; // alumno o trabajador
|
|
||||||
apellidos: string; // alumno o trabajador
|
|
||||||
correo: string; // alumno o trabajador (en los alumnos se usa el id_ncuenta en trabajadores ellos deben escribirlo)
|
|
||||||
genero: 'M' | 'F'; // alumno o trabajador
|
|
||||||
carrera: string; // solo para alumnos
|
|
||||||
rfc?: string; // solo para trabajadores se usa para buscar información de cuenta
|
|
||||||
};
|
|
||||||
|
|
||||||
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({
|
export default function FormularioRegistro({
|
||||||
id_cuestionario,
|
|
||||||
handleSubmitFormulario,
|
handleSubmitFormulario,
|
||||||
}: {
|
}: {
|
||||||
evento: string;
|
evento: string;
|
||||||
@@ -44,324 +24,70 @@ export default function FormularioRegistro({
|
|||||||
id_cuestionario: number;
|
id_cuestionario: number;
|
||||||
handleSubmitFormulario: (data: SubmitResponse) => void;
|
handleSubmitFormulario: (data: SubmitResponse) => void;
|
||||||
}) {
|
}) {
|
||||||
// ------------------------
|
const {
|
||||||
// Estados
|
state,
|
||||||
// ------------------------
|
preguntaComunidad,
|
||||||
const [isComunidad, setIsComunidad] = useState<RadioOption<number>>();
|
preguntaCuenta,
|
||||||
const [respuestas, setRespuestas] = useState<RespuestaFormulario>({});
|
esComunidadSi,
|
||||||
const [cuentaInfo, setCuentaInfo] = useState<UsuarioData | null>(null);
|
esExclusivo,
|
||||||
const [isSending, setIsSending] = useState(false);
|
mostrarFormularioCompleto,
|
||||||
const router = useRouter();
|
setRespuesta,
|
||||||
|
submitFormulario,
|
||||||
|
} = useFormulario();
|
||||||
|
|
||||||
// ------------------------
|
const { data, loadingData, errorData, respuestas, isSending, errorBusqueda, loadingBusqueda } = state;
|
||||||
// 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(
|
const comunidadSeleccionada = preguntaComunidad
|
||||||
(pregunta) =>
|
? respuestas[preguntaComunidad.id_pregunta]
|
||||||
pregunta.validacion === 'comunidad_alumno' ||
|
: null;
|
||||||
pregunta.validacion === 'comunidad_trabajador'
|
|
||||||
);
|
|
||||||
|
|
||||||
const preguntaCuenta = preguntas?.find(
|
|
||||||
(pregunta) =>
|
|
||||||
pregunta.validacion === 'cuenta_alumno' ||
|
|
||||||
pregunta.validacion === 'cuenta_trabajador' ||
|
|
||||||
pregunta.validacion === 'rfc'
|
|
||||||
);
|
|
||||||
|
|
||||||
// ------------------------
|
|
||||||
// Efecto: buscar información de cuenta
|
|
||||||
// ------------------------
|
|
||||||
useEffect(() => {
|
|
||||||
const cuentaId = preguntaCuenta?.id_pregunta ?? '';
|
|
||||||
const cuenta = respuestas[cuentaId];
|
|
||||||
|
|
||||||
const esCuentaAlumno = preguntaCuenta?.validacion === 'cuenta_alumno';
|
|
||||||
const esCuentaTrabajador =
|
|
||||||
preguntaCuenta?.validacion === 'cuenta_trabajador' ||
|
|
||||||
preguntaCuenta?.validacion === 'rfc';
|
|
||||||
|
|
||||||
const puedeBuscar =
|
|
||||||
cuenta &&
|
|
||||||
typeof cuenta === 'string' &&
|
|
||||||
((esCuentaAlumno && cuenta.length === 9) ||
|
|
||||||
(esCuentaTrabajador && cuenta.length === 10));
|
|
||||||
|
|
||||||
const fetchCuentaInfo = async () => {
|
|
||||||
try {
|
|
||||||
const endpoint = esCuentaAlumno
|
|
||||||
? `/alumnos/${cuenta}`
|
|
||||||
: `/trabajadores/${cuenta}`;
|
|
||||||
|
|
||||||
const res = await axiosInstance.get<UsuarioDataResponse>(endpoint);
|
|
||||||
|
|
||||||
console.log('Datos de cuenta obtenidos:', res.data);
|
|
||||||
setCuentaInfo({
|
|
||||||
nombre: res.data.nombre ? res.data.nombre : '',
|
|
||||||
apellidos: res.data.apellidos ? res.data.apellidos : '',
|
|
||||||
correo: res.data.cuenta
|
|
||||||
? `${res.data.cuenta}@pcpuma.acatlan.unam.mx`
|
|
||||||
: '',
|
|
||||||
genero:
|
|
||||||
res.data.genero === 'M' || res.data.genero === 'F'
|
|
||||||
? res.data.genero
|
|
||||||
: 'M',
|
|
||||||
carrera: res.data.carrera ? res.data.carrera : '',
|
|
||||||
});
|
|
||||||
// Usar datos fake para pruebas
|
|
||||||
/* setCuentaInfo({
|
|
||||||
nombre: 'Juan',
|
|
||||||
apellidos: 'Pérez',
|
|
||||||
correo: 'juan.perez@pcpuma.acatlan.unam.mx',
|
|
||||||
genero: 'M',
|
|
||||||
carrera: 'Ingeniería en Computación',
|
|
||||||
}); */
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error al obtener datos de cuenta:', error);
|
|
||||||
setCuentaInfo(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (puedeBuscar) {
|
|
||||||
fetchCuentaInfo();
|
|
||||||
} else {
|
|
||||||
setCuentaInfo(null);
|
|
||||||
}
|
|
||||||
}, [respuestas, preguntaCuenta?.id_pregunta, preguntaCuenta?.validacion]);
|
|
||||||
|
|
||||||
// ------------------------
|
|
||||||
// Efecto: precargar respuestas si hay cuentaInfo
|
|
||||||
// ------------------------
|
|
||||||
useEffect(() => {
|
|
||||||
if (!cuentaInfo) {
|
|
||||||
if (data?.cuestionario.secciones) {
|
|
||||||
const preguntas = data.cuestionario.secciones.flatMap((seccion) =>
|
|
||||||
seccion.preguntas.map((pregunta) => pregunta.pregunta)
|
|
||||||
);
|
|
||||||
|
|
||||||
const idsPrefetch = preguntas
|
|
||||||
.filter((pregunta) =>
|
|
||||||
[
|
|
||||||
'nombre',
|
|
||||||
'apellidos',
|
|
||||||
'correo',
|
|
||||||
'institucion',
|
|
||||||
'carrera',
|
|
||||||
'genero',
|
|
||||||
].includes(pregunta.validacion)
|
|
||||||
)
|
|
||||||
.map((pregunta) => pregunta.id_pregunta);
|
|
||||||
|
|
||||||
setRespuestas((prev) => {
|
|
||||||
const nuevas = { ...prev };
|
|
||||||
idsPrefetch.forEach((id) => {
|
|
||||||
delete nuevas[id];
|
|
||||||
});
|
|
||||||
return nuevas;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nuevasRespuestas: RespuestaFormulario = {};
|
|
||||||
|
|
||||||
if (data?.cuestionario.secciones) {
|
|
||||||
const preguntas = data.cuestionario.secciones.flatMap((seccion) =>
|
|
||||||
seccion.preguntas.map((pregunta) => pregunta.pregunta)
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const pregunta of preguntas) {
|
|
||||||
const id = pregunta.id_pregunta;
|
|
||||||
switch (pregunta.validacion) {
|
|
||||||
case 'nombre':
|
|
||||||
nuevasRespuestas[id] = cuentaInfo.nombre;
|
|
||||||
break;
|
|
||||||
case 'apellidos':
|
|
||||||
nuevasRespuestas[id] = cuentaInfo.apellidos;
|
|
||||||
break;
|
|
||||||
case 'correo':
|
|
||||||
nuevasRespuestas[id] = cuentaInfo.correo;
|
|
||||||
break;
|
|
||||||
case 'institucion':
|
|
||||||
nuevasRespuestas[id] = 'FES Acatlán';
|
|
||||||
break;
|
|
||||||
case 'carrera':
|
|
||||||
nuevasRespuestas[id] = cuentaInfo.carrera;
|
|
||||||
break;
|
|
||||||
case 'genero':
|
|
||||||
const generoTexto =
|
|
||||||
cuentaInfo.genero === 'M' ? 'Masculino' : 'Femenino';
|
|
||||||
const opcion = pregunta.opciones?.find(
|
|
||||||
(op) =>
|
|
||||||
op.opcion.opcion.toLowerCase() === generoTexto.toLowerCase()
|
|
||||||
);
|
|
||||||
if (opcion) {
|
|
||||||
nuevasRespuestas[id] = String(opcion.id_opcion);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setRespuestas((prev) => ({
|
|
||||||
...prev,
|
|
||||||
...nuevasRespuestas,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}, [cuentaInfo, data?.cuestionario.secciones]);
|
|
||||||
|
|
||||||
// ------------------------
|
|
||||||
// Funciones auxiliares
|
|
||||||
// ------------------------
|
|
||||||
const actualizarRespuesta = (idPregunta: string, valor: string) => {
|
|
||||||
setRespuestas((prev) => ({ ...prev, [idPregunta]: valor }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnSubmit = async () => {
|
const handleOnSubmit = async () => {
|
||||||
setIsSending(true);
|
|
||||||
if (!data?.cuestionario.id_cuestionario) {
|
if (!data?.cuestionario.id_cuestionario) {
|
||||||
alert('No se ha cargado correctamente el formulario');
|
toast.error('No se ha cargado correctamente el formulario');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const preguntasObligatorias =
|
const preguntasObligatorias = data.cuestionario.secciones.flatMap((seccion) =>
|
||||||
data?.cuestionario.secciones.flatMap((seccion) =>
|
seccion.preguntas
|
||||||
seccion.preguntas
|
.filter((p) => p.pregunta.obligatoria)
|
||||||
.filter((pregunta) => pregunta.pregunta.obligatoria)
|
.map((p) => p.pregunta)
|
||||||
.map((pregunta) => pregunta.pregunta)
|
|
||||||
) || [];
|
|
||||||
|
|
||||||
const faltantes = preguntasObligatorias.filter(
|
|
||||||
(pregunta) =>
|
|
||||||
respuestas[pregunta.id_pregunta] === undefined ||
|
|
||||||
respuestas[pregunta.id_pregunta] === '' ||
|
|
||||||
respuestas[pregunta.id_pregunta] === null
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const preguntaCorreo = preguntasObligatorias.find(
|
const faltantes = preguntasObligatorias.filter(
|
||||||
(pregunta) =>
|
(p) =>
|
||||||
pregunta.validacion === 'correo' ||
|
respuestas[p.id_pregunta] === undefined ||
|
||||||
pregunta.validacion === 'correo_institucional'
|
respuestas[p.id_pregunta] === '' ||
|
||||||
|
respuestas[p.id_pregunta] === null
|
||||||
);
|
);
|
||||||
|
|
||||||
if (faltantes.length > 0) {
|
if (faltantes.length > 0) {
|
||||||
toast.error('Por favor responde todas las preguntas obligatorias.');
|
toast.error('Por favor responde todas las preguntas obligatorias.');
|
||||||
setIsSending(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar un posible correo en las respuestas si no hay cuentaInfo
|
|
||||||
let correo =
|
|
||||||
cuentaInfo?.correo === respuestas[preguntaCorreo?.id_pregunta ?? '']
|
|
||||||
? cuentaInfo?.correo
|
|
||||||
: respuestas[preguntaCorreo?.id_pregunta ?? ''];
|
|
||||||
|
|
||||||
if (!correo) {
|
|
||||||
// Buscar en las respuestas una que parezca correo
|
|
||||||
const correoRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
||||||
for (const valor of Object.values(respuestas)) {
|
|
||||||
if (typeof valor === 'string' && correoRegex.test(valor)) {
|
|
||||||
correo = valor;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
id_cuestionario: id_cuestionario,
|
|
||||||
correo: correo || '',
|
|
||||||
fecha_envio: new Date().toISOString(),
|
|
||||||
respuestas: Object.entries(respuestas).map(([id_pregunta, valor]) => ({
|
|
||||||
id_pregunta: Number(id_pregunta),
|
|
||||||
valor: isNaN(Number(valor)) ? valor : Number(valor),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axiosInstance.post<SubmitResponse>(
|
const res = await submitFormulario(data.cuestionario.id_cuestionario);
|
||||||
'/cuestionario-respondido/submit',
|
toast.success(res.message || '¡Formulario enviado exitosamente!');
|
||||||
payload
|
handleSubmitFormulario(res);
|
||||||
);
|
} catch {
|
||||||
toast(
|
toast.error('Hubo un error al enviar el formulario');
|
||||||
() => (
|
|
||||||
<span
|
|
||||||
className="fs-4 pc-3 py-2"
|
|
||||||
style={{
|
|
||||||
maxWidth: '300px',
|
|
||||||
wordWrap: 'break-word',
|
|
||||||
whiteSpace: 'normal',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{res.data.message.includes('ya ha respondido') ? (
|
|
||||||
<>
|
|
||||||
<i className="bi bi-exclamation-circle-fill text-warning me-2"></i>
|
|
||||||
Respuestas Enviadas Con Éxito
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<i className="bi bi-check-circle-fill text-success me-2"></i>
|
|
||||||
{res.data.message || '¡Formulario enviado!'}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
{
|
|
||||||
duration: 6000,
|
|
||||||
position: 'bottom-center',
|
|
||||||
style: {
|
|
||||||
maxWidth: '425px',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
handleSubmitFormulario(res.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(getAxiosError(error));
|
|
||||||
toast.error('Hubo un error al enviar el formulario', {
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
router.push('/');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ------------------------
|
if (errorData) {
|
||||||
// Condición para mostrar el formulario
|
|
||||||
// ------------------------
|
|
||||||
const mostrarFormularioRestante =
|
|
||||||
!preguntaComunidad || // Si no hay pregunta de comunidad, mostrar el formulario
|
|
||||||
(isComunidad &&
|
|
||||||
confirmativeOption.some((opt) =>
|
|
||||||
isComunidad.label?.toLowerCase().includes(opt.toLowerCase())
|
|
||||||
) &&
|
|
||||||
!!cuentaInfo) ||
|
|
||||||
(isComunidad &&
|
|
||||||
negativeOption.some((opt) =>
|
|
||||||
isComunidad.label?.toLowerCase().includes(opt.toLowerCase())
|
|
||||||
));
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error al cargar el cuestionario:', error);
|
|
||||||
return <div>Error al cargar el cuestionario</div>;
|
return <div>Error al cargar el cuestionario</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loadingData || !data) {
|
||||||
|
return <div>Cargando...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{/* Pregunta de comunidad */}
|
||||||
{preguntaComunidad && (
|
{preguntaComunidad && (
|
||||||
<div className="my-4">
|
<div className="my-4">
|
||||||
<label
|
<label className={`form-label ${preguntaComunidad.obligatoria ? 'required' : ''}`}>
|
||||||
className={`form-label ${
|
|
||||||
preguntaComunidad.obligatoria ? 'required' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{preguntaComunidad.pregunta}
|
{preguntaComunidad.pregunta}
|
||||||
</label>
|
</label>
|
||||||
<RadioOptionGroup
|
<RadioOptionGroup
|
||||||
@@ -370,192 +96,152 @@ export default function FormularioRegistro({
|
|||||||
label: op.opcion.opcion,
|
label: op.opcion.opcion,
|
||||||
value: op.id_opcion,
|
value: op.id_opcion,
|
||||||
}))}
|
}))}
|
||||||
selectedValue={isComunidad?.value}
|
selectedValue={comunidadSeleccionada ? Number(comunidadSeleccionada) : undefined}
|
||||||
onChange={(opt) => {
|
onChange={(opt) =>
|
||||||
console.log('Opción comunidad seleccionada:', opt);
|
setRespuesta(preguntaComunidad.id_pregunta.toString(), String(opt.value))
|
||||||
setIsComunidad(opt);
|
}
|
||||||
actualizarRespuesta(
|
|
||||||
preguntaComunidad.id_pregunta.toString(),
|
|
||||||
String(opt.value)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isComunidad &&
|
|
||||||
confirmativeOption.some((opt) =>
|
{/* Pregunta de cuenta (si respondió "Sí" a comunidad, o en cuestionario exclusivo) */}
|
||||||
isComunidad.label?.toLowerCase().includes(opt.toLowerCase())
|
{(esComunidadSi || esExclusivo) && preguntaCuenta && (
|
||||||
) &&
|
<div className="my-4">
|
||||||
preguntaCuenta && (
|
<label className={`form-label ${preguntaCuenta.obligatoria ? 'required' : ''}`}>
|
||||||
<div className="my-4">
|
{preguntaCuenta.pregunta}
|
||||||
<label
|
</label>
|
||||||
className={`form-label ${
|
<SimpleInput
|
||||||
preguntaCuenta.obligatoria ? 'required' : ''
|
type="text"
|
||||||
}`}
|
name="cuenta"
|
||||||
>
|
placeholder={
|
||||||
{preguntaCuenta.pregunta}
|
preguntaCuenta.validacion === 'cuenta_alumno'
|
||||||
</label>
|
? 'Ingrese su número de cuenta (9 dígitos)'
|
||||||
<SimpleInput
|
: preguntaCuenta.validacion === 'cuenta_trabajador'
|
||||||
type="text"
|
? 'Ingrese su número de trabajador (6 dígitos)'
|
||||||
name="cuenta"
|
: preguntaCuenta.validacion === 'rfc'
|
||||||
placeholder={
|
? 'Ingrese su RFC sin homoclave (10 caracteres)'
|
||||||
preguntaCuenta.validacion === 'cuenta_alumno'
|
: `Ingrese su ${preguntaCuenta.pregunta}`
|
||||||
? 'Ingrese su cuenta'
|
}
|
||||||
: preguntaCuenta?.validacion === 'rfc'
|
maxLength={
|
||||||
? 'Ingrese su RFC sin homoclave'
|
preguntaCuenta.validacion === 'rfc'
|
||||||
: preguntaCuenta?.validacion === 'cuenta_trabajador'
|
? 10
|
||||||
? 'Ingrese su numero de trabajador'
|
: preguntaCuenta.validacion === 'cuenta_alumno'
|
||||||
: 'Ingrese su ' + preguntaCuenta.pregunta
|
? 9
|
||||||
}
|
: 6
|
||||||
maxLength={10}
|
}
|
||||||
value={respuestas[preguntaCuenta.id_pregunta] || ''}
|
value={respuestas[preguntaCuenta.id_pregunta] || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setRespuestas((prev) => ({
|
setRespuesta(preguntaCuenta.id_pregunta.toString(), e.target.value)
|
||||||
...prev,
|
}
|
||||||
[preguntaCuenta.id_pregunta]: e.target.value,
|
/>
|
||||||
}))
|
{loadingBusqueda && (
|
||||||
}
|
<div className="text-muted small mt-1">
|
||||||
/>
|
<span className="spinner-border spinner-border-sm me-1" />
|
||||||
</div>
|
Buscando...
|
||||||
)}
|
</div>
|
||||||
{mostrarFormularioRestante &&
|
)}
|
||||||
data?.cuestionario.secciones.map((seccion, i) => (
|
{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}>
|
<div key={i}>
|
||||||
{seccion.preguntas.map((pregunta) => {
|
{seccion.preguntas.map((pregunta) => {
|
||||||
// Evitar renderizar las preguntas ya manejadas
|
|
||||||
if (
|
if (
|
||||||
pregunta.pregunta.id_pregunta ===
|
pregunta.pregunta.id_pregunta === preguntaComunidad?.id_pregunta ||
|
||||||
preguntaComunidad?.id_pregunta ||
|
|
||||||
pregunta.pregunta.id_pregunta === preguntaCuenta?.id_pregunta
|
pregunta.pregunta.id_pregunta === preguntaCuenta?.id_pregunta
|
||||||
)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (
|
|
||||||
pregunta.pregunta.tipo_pregunta.tipo_pregunta ===
|
|
||||||
'Abierta (Respuesta corta)'
|
|
||||||
) {
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valor = respuestas[pregunta.pregunta.id_pregunta] || '';
|
||||||
|
|
||||||
|
if (pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Abierta (Respuesta corta)') {
|
||||||
return (
|
return (
|
||||||
<PrefetchAbiertaCorta
|
<div key={pregunta.pregunta.id_pregunta} className="my-4">
|
||||||
key={pregunta.pregunta.id_pregunta}
|
<label className={`form-label ${pregunta.pregunta.obligatoria ? 'required' : ''}`}>
|
||||||
obligatorio={pregunta.pregunta.obligatoria}
|
{pregunta.pregunta.pregunta}
|
||||||
idPregunta={pregunta.pregunta.id_pregunta}
|
</label>
|
||||||
enunciado={pregunta.pregunta.pregunta}
|
<SimpleInput
|
||||||
validacion={pregunta.pregunta.validacion}
|
type="text"
|
||||||
respuestas={respuestas}
|
name={`respuesta_${pregunta.pregunta.id_pregunta}`}
|
||||||
cuentaInfo={cuentaInfo}
|
placeholder="Ingrese una respuesta"
|
||||||
isComunidad={isComunidad?.label}
|
value={valor}
|
||||||
onChange={actualizarRespuesta}
|
onChange={(e) =>
|
||||||
preguntaComunidad={preguntaComunidad?.validacion ?? ''}
|
setRespuesta(pregunta.pregunta.id_pregunta.toString(), e.target.value)
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Abierta (Parrafo)') {
|
||||||
pregunta.pregunta.tipo_pregunta.tipo_pregunta ===
|
|
||||||
'Abierta (Parrafo)'
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<div key={pregunta.pregunta.id_pregunta} className="my-4">
|
<div key={pregunta.pregunta.id_pregunta} className="my-4">
|
||||||
<label
|
<label className={`form-label ${pregunta.pregunta.obligatoria ? 'required' : ''}`}>
|
||||||
className={`form-label ${
|
|
||||||
pregunta.pregunta.obligatoria ? 'required' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{pregunta.pregunta.pregunta}
|
{pregunta.pregunta.pregunta}
|
||||||
</label>
|
</label>
|
||||||
<SimpleInput
|
<SimpleInput
|
||||||
type="textarea"
|
type="textarea"
|
||||||
name={`respuesta_${pregunta.pregunta.id_pregunta}`}
|
name={`respuesta_${pregunta.pregunta.id_pregunta}`}
|
||||||
placeholder="Ingrese una respuesta"
|
placeholder="Ingrese una respuesta"
|
||||||
value={respuestas[pregunta.pregunta.id_pregunta] || ''}
|
value={valor}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setRespuestas((prev) => ({
|
setRespuesta(pregunta.pregunta.id_pregunta.toString(), e.target.value)
|
||||||
...prev,
|
|
||||||
[pregunta.pregunta.id_pregunta]: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Cerrada') {
|
if (
|
||||||
const opciones: RadioOption<number>[] =
|
pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Cerrada' ||
|
||||||
pregunta.pregunta.opciones.map((op) => ({
|
pregunta.pregunta.tipo_pregunta.tipo_pregunta === 'Multiple'
|
||||||
label: op.opcion.opcion,
|
) {
|
||||||
value: op.id_opcion,
|
const opciones: RadioOption<number>[] = pregunta.pregunta.opciones.map((op) => ({
|
||||||
}));
|
label: op.opcion.opcion,
|
||||||
let respuestaActual =
|
value: op.id_opcion,
|
||||||
respuestas[`${pregunta.pregunta.id_pregunta}`];
|
}));
|
||||||
|
|
||||||
if (
|
|
||||||
(!preguntaComunidad ||
|
|
||||||
(confirmativeOption.includes(isComunidad?.label || '') &&
|
|
||||||
cuentaInfo)) &&
|
|
||||||
respuestaActual === undefined // solo si no ha respondido
|
|
||||||
) {
|
|
||||||
if (pregunta.pregunta.validacion === 'genero' && cuentaInfo) {
|
|
||||||
const generoTexto =
|
|
||||||
cuentaInfo.genero === 'M' ? 'Masculino' : 'Femenino';
|
|
||||||
|
|
||||||
const opcionGenero = pregunta.pregunta.opciones.find(
|
|
||||||
(op) =>
|
|
||||||
op.opcion.opcion.toLowerCase() ===
|
|
||||||
generoTexto.toLowerCase()
|
|
||||||
);
|
|
||||||
if (opcionGenero) {
|
|
||||||
respuestaActual = String(opcionGenero.id_opcion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={pregunta.pregunta.id_pregunta}>
|
<div key={pregunta.pregunta.id_pregunta} className="my-4">
|
||||||
<label
|
<label className={`form-label ${pregunta.pregunta.obligatoria ? 'required' : ''}`}>
|
||||||
className={`form-label ${
|
|
||||||
pregunta.pregunta.obligatoria ? 'required' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{pregunta.pregunta.pregunta}
|
{pregunta.pregunta.pregunta}
|
||||||
</label>
|
</label>
|
||||||
<RadioOptionGroup
|
<RadioOptionGroup
|
||||||
name={`pregunta_${pregunta.pregunta.id_pregunta}`}
|
name={`pregunta_${pregunta.pregunta.id_pregunta}`}
|
||||||
options={opciones}
|
options={opciones}
|
||||||
selectedValue={
|
selectedValue={valor ? Number(valor) : undefined}
|
||||||
respuestaActual ? Number(respuestaActual) : undefined
|
onChange={(opt) =>
|
||||||
|
setRespuesta(pregunta.pregunta.id_pregunta.toString(), String(opt.value))
|
||||||
}
|
}
|
||||||
onChange={(opt) => {
|
|
||||||
actualizarRespuesta(
|
|
||||||
`${pregunta.pregunta.id_pregunta}`,
|
|
||||||
String(opt.value)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return null;
|
||||||
<div key={pregunta.pregunta.id_pregunta} className="my-4">
|
|
||||||
<label
|
|
||||||
className={`form-label ${
|
|
||||||
pregunta.pregunta.obligatoria ? 'required' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{pregunta.pregunta.pregunta}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<Button
|
|
||||||
className="btn btn-primary"
|
{/* Botón de envío */}
|
||||||
onClick={handleOnSubmit}
|
{mostrarFormularioCompleto && (
|
||||||
disabled={!mostrarFormularioRestante || isSending}
|
<Button
|
||||||
>
|
className="btn btn-primary"
|
||||||
{isSending ? 'Enviando...' : 'Enviar Formulario'}
|
onClick={handleOnSubmit}
|
||||||
</Button>
|
disabled={isSending}
|
||||||
|
>
|
||||||
|
{isSending ? 'Enviando...' : 'Enviar Formulario'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { useGetApi } from '@/hooks/use-get-api';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import FormularioRegistro from '../formulario/formulario-registro';
|
import FormularioRegistro from '../formulario/formulario-registro';
|
||||||
|
import FormularioExito from '../formulario/formulario-exito';
|
||||||
import { GetEventoWithCuestionario } from '@/types/evento';
|
import { GetEventoWithCuestionario } from '@/types/evento';
|
||||||
import { formatearRangoFechas } from '@/utils/date-utils';
|
import { formatearRangoFechas } from '@/utils/date-utils';
|
||||||
import { SubmitResponse } from '@/types/submit';
|
import { SubmitResponse } from '@/types/submit';
|
||||||
import Link from 'next/link';
|
import { FormularioProvider } from '@/context/formulario';
|
||||||
|
|
||||||
export default function FormularioPage({
|
export default function FormularioPage({
|
||||||
id_evento,
|
id_evento,
|
||||||
@@ -64,121 +65,12 @@ export default function FormularioPage({
|
|||||||
// Si hay una respuesta exitosa, mostrar la vista de éxito
|
// Si hay una respuesta exitosa, mostrar la vista de éxito
|
||||||
if (response && response.success) {
|
if (response && response.success) {
|
||||||
return (
|
return (
|
||||||
<div className="container my-5">
|
<FormularioExito
|
||||||
<div className="row justify-content-center">
|
response={response}
|
||||||
<div className="col-12 col-lg-10 col-xl-8">
|
qrImageUrl={qrImageUrl}
|
||||||
<div className="card shadow-sm">
|
nombreEvento={data?.nombre_evento}
|
||||||
<div className="card-body">
|
nombreFormulario={data?.cuestionario.nombre_form}
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +187,8 @@ export default function FormularioPage({
|
|||||||
className="bi bi-calendar-date me-1 text-primary"
|
className="bi bi-calendar-date me-1 text-primary"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
{formatearRangoFechas(data.fecha_inicio, data.fecha_fin)}
|
{formatearRangoFechas(data.fecha_inicio, data.fecha_fin)}{' '}
|
||||||
|
horas.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -340,7 +233,8 @@ export default function FormularioPage({
|
|||||||
{formatearRangoFechas(
|
{formatearRangoFechas(
|
||||||
data.cuestionario.fecha_inicio,
|
data.cuestionario.fecha_inicio,
|
||||||
data.cuestionario.fecha_fin
|
data.cuestionario.fecha_fin
|
||||||
)}
|
)}{' '}
|
||||||
|
horas.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -353,13 +247,18 @@ export default function FormularioPage({
|
|||||||
<div className="col-lg-7">
|
<div className="col-lg-7">
|
||||||
<div className="ps-lg-4">
|
<div className="ps-lg-4">
|
||||||
{data && (
|
{data && (
|
||||||
<FormularioRegistro
|
<FormularioProvider
|
||||||
evento={data.nombre_evento}
|
id_evento={data.id_evento}
|
||||||
cuestionario={data?.cuestionario.nombre_form}
|
id_cuestionario={data.cuestionario.id_cuestionario}
|
||||||
id_evento={data?.id_evento}
|
>
|
||||||
id_cuestionario={data?.cuestionario.id_cuestionario}
|
<FormularioRegistro
|
||||||
handleSubmitFormulario={onSubmitFormulario}
|
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>
|
||||||
</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';
|
||||||
+127
-11
@@ -1,7 +1,6 @@
|
|||||||
import { FormularioCreacion } from '@/types/create-formulario';
|
import { FormularioCreacion } from '@/types/create-formulario';
|
||||||
|
|
||||||
interface Plantilla {
|
interface Plantilla {
|
||||||
imagen: string;
|
|
||||||
nombre: string;
|
nombre: string;
|
||||||
id: string;
|
id: string;
|
||||||
datos: FormularioCreacion;
|
datos: FormularioCreacion;
|
||||||
@@ -9,13 +8,12 @@ interface Plantilla {
|
|||||||
|
|
||||||
export const plantillasDisponibles: Plantilla[] = [
|
export const plantillasDisponibles: Plantilla[] = [
|
||||||
{
|
{
|
||||||
imagen: '/plantilla_trabajadores.png',
|
nombre: 'Registro General (con prellenado para Trabajadores)',
|
||||||
nombre: 'Registro Comunidad Trabajador',
|
|
||||||
id: 'registro-comunidad-estudiantil',
|
id: 'registro-comunidad-estudiantil',
|
||||||
datos: {
|
datos: {
|
||||||
nombre_form: 'Registro',
|
nombre_form: 'Registro',
|
||||||
descripcion:
|
descripcion:
|
||||||
'Con este formulario estaras reservando tu lugar en el evento. Por favor, completa todos los campos obligatorios.',
|
'Con este formulario estarás reservando tu lugar en el evento. Por favor, completa todos los campos obligatorios.',
|
||||||
fecha_inicio: '2025-08-01T08:00:00',
|
fecha_inicio: '2025-08-01T08:00:00',
|
||||||
fecha_fin: '2025-08-10T23:59:59',
|
fecha_fin: '2025-08-10T23:59:59',
|
||||||
id_tipo_cuestionario: 2,
|
id_tipo_cuestionario: 2,
|
||||||
@@ -79,13 +77,12 @@ export const plantillasDisponibles: Plantilla[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
imagen: '/plantilla_alumnos.png',
|
nombre: 'Registro General (con prellenado para Alumnos)',
|
||||||
nombre: 'Registro Comunidad Alumnos',
|
|
||||||
id: 'registro-comunidad-alumnos',
|
id: 'registro-comunidad-alumnos',
|
||||||
datos: {
|
datos: {
|
||||||
nombre_form: 'Registro',
|
nombre_form: 'Registro',
|
||||||
descripcion:
|
descripcion:
|
||||||
'Con este formulario estaras reservando tu lugar en el evento. Por favor, completa todos los campos obligatorios.',
|
'Con este formulario estarás reservando tu lugar en el evento. Por favor, completa todos los campos obligatorios.',
|
||||||
fecha_inicio: '2025-08-01T08:00:00',
|
fecha_inicio: '2025-08-01T08:00:00',
|
||||||
fecha_fin: '2025-08-10T23:59:59',
|
fecha_fin: '2025-08-10T23:59:59',
|
||||||
id_tipo_cuestionario: 1,
|
id_tipo_cuestionario: 1,
|
||||||
@@ -155,13 +152,12 @@ export const plantillasDisponibles: Plantilla[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
imagen: '/plantilla_general.png',
|
|
||||||
nombre: 'Registro General',
|
nombre: 'Registro General',
|
||||||
id: 'registro-general',
|
id: 'registro-general',
|
||||||
datos: {
|
datos: {
|
||||||
nombre_form: 'Registro Para Asistencia General',
|
nombre_form: 'Registro Para Asistencia General',
|
||||||
descripcion:
|
descripcion:
|
||||||
'Con este formulario estaras reservando tu lugar en el evento. Por favor, completa todos los campos obligatorios.',
|
'Con este formulario estarás reservando tu lugar en el evento. Por favor, completa todos los campos obligatorios.',
|
||||||
fecha_inicio: '2025-08-01T08:00:00',
|
fecha_inicio: '2025-08-01T08:00:00',
|
||||||
fecha_fin: '2025-08-10T23:59:59',
|
fecha_fin: '2025-08-10T23:59:59',
|
||||||
id_tipo_cuestionario: 1,
|
id_tipo_cuestionario: 1,
|
||||||
@@ -172,7 +168,7 @@ export const plantillasDisponibles: Plantilla[] = [
|
|||||||
'Proporciona tus datos personales para poder contactarte y enviarte información relevante sobre la Feria de la Sexualidad.',
|
'Proporciona tus datos personales para poder contactarte y enviarte información relevante sobre la Feria de la Sexualidad.',
|
||||||
preguntas: [
|
preguntas: [
|
||||||
{
|
{
|
||||||
titulo: 'Numero de cuenta',
|
titulo: 'Número de cuenta / trabajador',
|
||||||
tipo: 'Abierta (Respuesta corta)',
|
tipo: 'Abierta (Respuesta corta)',
|
||||||
obligatoria: false,
|
obligatoria: false,
|
||||||
},
|
},
|
||||||
@@ -198,7 +194,127 @@ export const plantillasDisponibles: Plantilla[] = [
|
|||||||
titulo: 'Tipo de Asistente',
|
titulo: 'Tipo de Asistente',
|
||||||
tipo: 'Cerrada',
|
tipo: 'Cerrada',
|
||||||
obligatoria: true,
|
obligatoria: true,
|
||||||
opciones: [{ valor: 'Alumno' }, { valor: 'Profesor' }],
|
opciones: [
|
||||||
|
{ valor: 'Alumno' },
|
||||||
|
{ valor: 'Profesor' },
|
||||||
|
{ valor: 'Trabajador' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Vendored
+1
@@ -29,6 +29,7 @@ export interface FormularioCreacion {
|
|||||||
id_tipo_cuestionario: number;
|
id_tipo_cuestionario: number;
|
||||||
id_tipo_evento?: number;
|
id_tipo_evento?: number;
|
||||||
cupo_maximo?: number; // Opcional, si no se requiere un cupo máximo
|
cupo_maximo?: number; // Opcional, si no se requiere un cupo máximo
|
||||||
|
mensaje_correo?: string;
|
||||||
secciones: SeccionFormulario[];
|
secciones: SeccionFormulario[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
@@ -15,6 +15,7 @@ export interface GetCuestionario {
|
|||||||
fecha_fin: string; // Formato ISO: 'YYYY-MM-DDTHH:mm:ss'
|
fecha_fin: string; // Formato ISO: 'YYYY-MM-DDTHH:mm:ss'
|
||||||
fecha_inicio: string; // Formato ISO: 'YYYY-MM-DDTHH:mm:ss'
|
fecha_inicio: string; // Formato ISO: 'YYYY-MM-DDTHH:mm:ss'
|
||||||
id_tipo_cuestionario: number;
|
id_tipo_cuestionario: number;
|
||||||
|
mensaje_correo?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetCuestionarioWithEvento extends GetCuestionario {
|
export interface GetCuestionarioWithEvento extends GetCuestionario {
|
||||||
|
|||||||
Reference in New Issue
Block a user