feat: add admin management page and types
- Implemented the admin management page with a table displaying admin details. - Created the Administrador type definition for better type safety. - Added SubmitResponse type for handling submission responses. - Included placeholder components for form card previews.
This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
import ImageUploader from '@/components/banner-uploader';
|
||||
import Input from '@/components/input';
|
||||
import Select from '@/components/select';
|
||||
import { useGetApi } from '@/hooks/use-get-api';
|
||||
import { CreateEventoType } from '@/types/create-evento';
|
||||
import { tipoEventos } from '@/utils/arrays';
|
||||
import { TipoEvento } from '@/types/evento';
|
||||
import { formatDateLocal } from '@/utils/date-utils';
|
||||
import { commands } from '@uiw/react-md-editor';
|
||||
import dynamic from 'next/dynamic';
|
||||
@@ -24,6 +25,7 @@ export default function CreateEvento({
|
||||
handleChange,
|
||||
handleBannerChange,
|
||||
}: CreateEventoProps) {
|
||||
const { data: tipo_eventos } = useGetApi<TipoEvento[]>('tipo-evento');
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<h4 className="mb-3">Detalles del Evento</h4>
|
||||
@@ -75,13 +77,18 @@ export default function CreateEvento({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
label="Tipo de evento"
|
||||
value={evento.tipo_evento}
|
||||
placeholder="Selecciona un tipo de evento"
|
||||
onChange={(e) => handleChange('tipo_evento', e.target.value)}
|
||||
options={tipoEventos}
|
||||
/>
|
||||
{tipo_eventos && (
|
||||
<Select
|
||||
label="Tipo de evento"
|
||||
value={evento.tipo_evento}
|
||||
placeholder="Selecciona un tipo de evento"
|
||||
onChange={(e) => handleChange('tipo_evento', e.target.value)}
|
||||
options={tipo_eventos.map((tipo) => ({
|
||||
value: tipo.id_tipo_evento,
|
||||
label: tipo.tipo_evento,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
TiposValidacion,
|
||||
} from '@/types/create-formulario';
|
||||
import SimpleInput from '@/components/input';
|
||||
import Select from '@/components/select';
|
||||
import Select, { Option } from '@/components/select';
|
||||
import Button from '@/components/button';
|
||||
import { GetEventoWithCuestionariosWithCupos } from '@/types/evento';
|
||||
import { formatearRangoFechas } from '@/utils/date-utils';
|
||||
@@ -14,6 +14,7 @@ interface Props {
|
||||
evento?: GetEventoWithCuestionariosWithCupos;
|
||||
formulario: FormularioCreacion;
|
||||
onChange: (formularioActualizado: FormularioCreacion) => void;
|
||||
tipo_eventos?: Option[];
|
||||
}
|
||||
|
||||
const tipoPreguntaOpciones: TipoPregunta[] = [
|
||||
@@ -56,6 +57,7 @@ export default function FormularioEditor({
|
||||
formulario,
|
||||
onChange,
|
||||
evento,
|
||||
tipo_eventos,
|
||||
}: Props) {
|
||||
function actualizarCampo<K extends keyof FormularioCreacion>(
|
||||
campo: K,
|
||||
@@ -212,15 +214,30 @@ export default function FormularioEditor({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Select
|
||||
label="Tipo de Cuestionario"
|
||||
value={formulario.id_tipo_cuestionario}
|
||||
onChange={(e) =>
|
||||
actualizarCampo('id_tipo_cuestionario', Number(e.target.value))
|
||||
}
|
||||
options={tiposCuestionario}
|
||||
placeholder="Selecciona un tipo"
|
||||
/>
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<Select
|
||||
label="Tipo de Cuestionario"
|
||||
value={formulario.id_tipo_cuestionario}
|
||||
onChange={(e) =>
|
||||
actualizarCampo('id_tipo_cuestionario', Number(e.target.value))
|
||||
}
|
||||
options={tiposCuestionario}
|
||||
placeholder="Selecciona un tipo"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<Select
|
||||
label="Tipo de evento"
|
||||
value={formulario.id_tipo_evento}
|
||||
onChange={(e) =>
|
||||
actualizarCampo('id_tipo_evento', Number(e.target.value))
|
||||
}
|
||||
options={tipo_eventos!}
|
||||
placeholder="Selecciona un tipo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ 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;
|
||||
@@ -35,8 +36,13 @@ type RespuestaFormulario = Record<string, string | number>; // id_pregunta: resp
|
||||
|
||||
export default function FormularioRegistro({
|
||||
id_cuestionario,
|
||||
handleSubmitFormulario,
|
||||
}: {
|
||||
evento: string;
|
||||
cuestionario: string;
|
||||
id_evento: number;
|
||||
id_cuestionario: number;
|
||||
handleSubmitFormulario: (data: SubmitResponse) => void;
|
||||
}) {
|
||||
// ------------------------
|
||||
// Estados
|
||||
@@ -77,7 +83,8 @@ export default function FormularioRegistro({
|
||||
// Efecto: buscar información de cuenta
|
||||
// ------------------------
|
||||
useEffect(() => {
|
||||
const cuenta = respuestas[preguntaCuenta?.id_pregunta ?? ''];
|
||||
const cuentaId = preguntaCuenta?.id_pregunta ?? '';
|
||||
const cuenta = respuestas[cuentaId];
|
||||
|
||||
const esCuentaAlumno = preguntaCuenta?.validacion === 'cuenta_alumno';
|
||||
const esCuentaTrabajador =
|
||||
@@ -130,7 +137,7 @@ export default function FormularioRegistro({
|
||||
} else {
|
||||
setCuentaInfo(null);
|
||||
}
|
||||
}, [respuestas[preguntaCuenta?.id_pregunta ?? '']]);
|
||||
}, [respuestas, preguntaCuenta?.id_pregunta, preguntaCuenta?.validacion]);
|
||||
|
||||
// ------------------------
|
||||
// Efecto: precargar respuestas si hay cuentaInfo
|
||||
@@ -212,7 +219,6 @@ export default function FormularioRegistro({
|
||||
};
|
||||
|
||||
const handleOnSubmit = async () => {
|
||||
console.log('Enviando respuestas:', respuestas);
|
||||
setIsSending(true);
|
||||
if (!data?.cuestionario.id_cuestionario) {
|
||||
alert('No se ha cargado correctamente el formulario');
|
||||
@@ -273,7 +279,7 @@ export default function FormularioRegistro({
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await axiosInstance.post(
|
||||
const res = await axiosInstance.post<SubmitResponse>(
|
||||
'/cuestionario-respondido/submit',
|
||||
payload
|
||||
);
|
||||
@@ -282,7 +288,7 @@ export default function FormularioRegistro({
|
||||
<span
|
||||
className="fs-4 pc-3 py-2"
|
||||
style={{
|
||||
maxWidth: '400px',
|
||||
maxWidth: '300px',
|
||||
wordWrap: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
}}
|
||||
@@ -290,7 +296,7 @@ export default function FormularioRegistro({
|
||||
{res.data.message.includes('ya ha respondido') ? (
|
||||
<>
|
||||
<i className="bi bi-exclamation-circle-fill text-warning me-2"></i>
|
||||
{res.data.message}
|
||||
Respuestas Enviadas Con Éxito
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -308,13 +314,13 @@ export default function FormularioRegistro({
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
handleSubmitFormulario(res.data);
|
||||
} catch (error) {
|
||||
console.log(getAxiosError(error));
|
||||
toast.error('Hubo un error al enviar el formulario', {
|
||||
duration: 5000,
|
||||
});
|
||||
} finally {
|
||||
// Redirigir al usuario después de enviar el formulario
|
||||
router.push('/');
|
||||
}
|
||||
};
|
||||
@@ -367,7 +373,6 @@ export default function FormularioRegistro({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isComunidad &&
|
||||
confirmativeOption.some((opt) =>
|
||||
isComunidad.label?.toLowerCase().includes(opt.toLowerCase())
|
||||
@@ -404,7 +409,6 @@ export default function FormularioRegistro({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mostrarFormularioRestante &&
|
||||
data?.cuestionario.secciones.map((seccion, i) => (
|
||||
<div key={i}>
|
||||
@@ -535,7 +539,6 @@ export default function FormularioRegistro({
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
className="btn btn-primary"
|
||||
onClick={handleOnSubmit}
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
import MarkdownRenderer from '@/components/markdown-render';
|
||||
import { useGetApi } from '@/hooks/use-get-api';
|
||||
import Image from 'next/image';
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import FormularioRegistro from '../formulario/formulario-registro';
|
||||
import { GetEventoWithCuestionario } from '@/types/evento';
|
||||
import { formatFecha } from '@/utils/date-utils';
|
||||
import { formatearRangoFechas } from '@/utils/date-utils';
|
||||
import { SubmitResponse } from '@/types/submit';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function FormularioPage({
|
||||
id_evento,
|
||||
@@ -14,14 +16,172 @@ export default function FormularioPage({
|
||||
id_evento: string;
|
||||
id_cuestionario: string;
|
||||
}) {
|
||||
const [response, setResponse] = useState<SubmitResponse | null>(null);
|
||||
const [qrImageUrl, setQrImageUrl] = useState<string | null>(null);
|
||||
const { data, error } = useGetApi<GetEventoWithCuestionario>(
|
||||
`/evento/${id_evento}/cuestionario/${id_cuestionario}`
|
||||
);
|
||||
|
||||
// Procesar el QR buffer cuando se reciba la respuesta
|
||||
useEffect(() => {
|
||||
if (!response?.qr_buffer) {
|
||||
setQrImageUrl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const binaryString = atob(response.qr_buffer);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
const blob = new Blob([bytes], { type: 'image/png' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
setQrImageUrl(url);
|
||||
} catch (error) {
|
||||
console.error('Error al procesar el QR buffer:', error);
|
||||
setQrImageUrl(null);
|
||||
}
|
||||
}, [response?.qr_buffer]);
|
||||
|
||||
// Cleanup del QR cuando el componente se desmonte o cambie la respuesta
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (qrImageUrl) {
|
||||
URL.revokeObjectURL(qrImageUrl);
|
||||
}
|
||||
};
|
||||
}, [qrImageUrl]);
|
||||
|
||||
if (error) {
|
||||
return <div>Error al cargar el cuestionario: {error.message}</div>;
|
||||
}
|
||||
|
||||
const onSubmitFormulario = async (res: SubmitResponse) => {
|
||||
setResponse(res);
|
||||
};
|
||||
|
||||
// Si hay una respuesta exitosa, mostrar la vista de éxito
|
||||
if (response && response.success) {
|
||||
return (
|
||||
<div className="container my-5">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10 col-xl-8">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-body">
|
||||
<div className="row g-4">
|
||||
<div className="col-md-6">
|
||||
<div className="h-100 d-flex flex-column justify-content-center">
|
||||
<div className="text-center mb-4">
|
||||
<div className="mb-3">
|
||||
<div
|
||||
className="d-inline-flex align-items-center justify-content-center bg-success rounded-circle"
|
||||
style={{ width: '60px', height: '60px' }}
|
||||
>
|
||||
<i
|
||||
className="bi bi-check-lg text-white"
|
||||
style={{ fontSize: '2rem' }}
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="fw-bold text-success mb-2">
|
||||
¡Felicidades!
|
||||
</h2>
|
||||
<p className="text-muted mb-0">
|
||||
Has completado exitosamente el formulario
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<i
|
||||
className="bi bi-calendar-event text-primary me-3"
|
||||
style={{ fontSize: '1.3rem' }}
|
||||
></i>
|
||||
<h6 className="mb-0 text-primary">Evento</h6>
|
||||
</div>
|
||||
<p className="mb-0 fs-6 fw-semibold ms-5">
|
||||
{data?.nombre_evento}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<i
|
||||
className="bi bi-file-text text-info me-3"
|
||||
style={{ fontSize: '1.3rem' }}
|
||||
></i>
|
||||
<h6 className="mb-0 text-info">Formulario</h6>
|
||||
</div>
|
||||
<p className="mb-0 fs-6 fw-semibold ms-5">
|
||||
{data?.cuestionario.nombre_form}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{response.message && (
|
||||
<div className="mb-3">
|
||||
<div className="alert alert-info" role="alert">
|
||||
<div className="d-flex align-items-start">
|
||||
<i className="bi bi-info-circle me-2 mt-1"></i>
|
||||
<div>
|
||||
<strong>Mensaje:</strong>
|
||||
<p className="mb-0 mt-1">{response.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="h-100 d-flex flex-column justify-content-center align-items-center">
|
||||
{qrImageUrl ? (
|
||||
<div className="text-center">
|
||||
<h5 className="mb-3 text-secondary">
|
||||
<i className="bi bi-qr-code me-2"></i>
|
||||
Código QR
|
||||
</h5>
|
||||
<div className="p-3 bg-white rounded shadow-sm">
|
||||
<Image
|
||||
src={qrImageUrl}
|
||||
alt="Código QR"
|
||||
width={250}
|
||||
height={250}
|
||||
className="rounded"
|
||||
style={{ maxWidth: '100%', height: 'auto' }}
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-muted">
|
||||
<i
|
||||
className="bi bi-qr-code-scan"
|
||||
style={{ fontSize: '4rem' }}
|
||||
></i>
|
||||
<p className="mt-3 mb-0">
|
||||
No hay código QR disponible
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-4">
|
||||
<Link href="/" className="btn btn-primary btn-lg me-3">
|
||||
Ir al inicio
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container my-5">
|
||||
{/* Sección de banners mejorada */}
|
||||
@@ -109,87 +269,83 @@ export default function FormularioPage({
|
||||
<div className="row">
|
||||
{/* Columna izquierda - Información */}
|
||||
<div className="col-lg-5">
|
||||
{/* Información del evento */}
|
||||
<div className="mb-4">
|
||||
<h1 className="mb-3">{data?.nombre_evento}</h1>
|
||||
{data?.descripcion_evento && (
|
||||
<div className="mb-3">
|
||||
<MarkdownRenderer markdown={data?.descripcion_evento} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data?.fecha_fin && data?.fecha_inicio && (
|
||||
<div className="mb-3">
|
||||
<p className="mb-1 fw-semibold text-muted">
|
||||
<i className="bi bi-calendar-event me-2"></i>
|
||||
Horario del evento:
|
||||
</p>
|
||||
<p className="text-muted ps-4">
|
||||
<i className="bi bi-clock me-1 text-primary"></i>
|
||||
{formatFecha(data.fecha_inicio!, {
|
||||
horas: true,
|
||||
minutos: true,
|
||||
})}{' '}
|
||||
al{' '}
|
||||
{formatFecha(data.fecha_fin!, {
|
||||
horas: true,
|
||||
minutos: true,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Información del formulario */}
|
||||
{data?.cuestionario && (
|
||||
<div className="card border-0 bg-light">
|
||||
{/* Tarjeta: Evento principal */}
|
||||
<section aria-labelledby="evento-heading" className="mb-1">
|
||||
<div className="card border-0 shadow-sm">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title text-primary mb-3">
|
||||
<i className="bi bi-file-earmark-text me-2"></i>
|
||||
Registro al formulario
|
||||
</h5>
|
||||
<h2
|
||||
id="evento-heading"
|
||||
className="h4 mb-2 d-flex align-items-center gap-2"
|
||||
>
|
||||
<i className="bi bi-calendar-event text-primary"></i>
|
||||
Evento
|
||||
</h2>
|
||||
|
||||
<div className="mb-3">
|
||||
<h6 className="fw-bold mb-2">Te estás registrando a:</h6>
|
||||
<p className="mb-1">
|
||||
<span className="badge bg-primary me-2">Formulario</span>
|
||||
<strong>{data.cuestionario.nombre_form}</strong>
|
||||
</p>
|
||||
<p className="mb-0 text-muted small">
|
||||
<span className="badge bg-secondary me-2">Evento</span>
|
||||
{data.nombre_evento}
|
||||
</p>
|
||||
</div>
|
||||
<h3 className="h3 mb-2">{data?.nombre_evento}</h3>
|
||||
|
||||
{data.cuestionario.descripcion && (
|
||||
<div className="mb-3">
|
||||
<h6 className="fw-bold mb-2">Descripción:</h6>
|
||||
<MarkdownRenderer
|
||||
markdown={data.cuestionario.descripcion}
|
||||
/>
|
||||
{data?.descripcion_evento && (
|
||||
<div className="text-muted mb-3">
|
||||
<MarkdownRenderer markdown={data?.descripcion_evento} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.cuestionario.fecha_inicio &&
|
||||
data.cuestionario.fecha_fin && (
|
||||
<div className="mb-0">
|
||||
<h6 className="fw-bold mb-2">Horario del formulario:</h6>
|
||||
<p className="text-muted small mb-0">
|
||||
<i className="bi bi-clock me-1"></i>
|
||||
{formatFecha(data.cuestionario.fecha_inicio, {
|
||||
horas: true,
|
||||
minutos: true,
|
||||
})}{' '}
|
||||
al{' '}
|
||||
{formatFecha(data.cuestionario.fecha_fin, {
|
||||
horas: true,
|
||||
minutos: true,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{data?.fecha_inicio && data?.fecha_fin && (
|
||||
<p className="text-muted mb-0">
|
||||
<i
|
||||
className="bi bi-calendar-date me-1 text-primary"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{formatearRangoFechas(data.fecha_inicio, data.fecha_fin)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Tarjeta: Sub-evento (Cuestionario) */}
|
||||
{data?.cuestionario && (
|
||||
<section aria-labelledby="cuestionario-heading">
|
||||
<div className="card border-0 shadow-sm">
|
||||
<div className="card-body">
|
||||
<div className="d-flex justify-content-between align-items-start mb-2">
|
||||
<h2
|
||||
id="cuestionario-heading"
|
||||
className="h4 mb-0 d-flex align-items-center gap-2"
|
||||
>
|
||||
<i className="bi bi-ui-checks-grid text-success"></i>
|
||||
Registro a cuestionario
|
||||
</h2>
|
||||
<span className="badge bg-success-subtle text-success border border-success-subtle">
|
||||
Sub-evento
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="h3 mb-1">{data.cuestionario.nombre_form}</h3>
|
||||
|
||||
{data.cuestionario.descripcion && (
|
||||
<div className="text-muted mb-3">
|
||||
<MarkdownRenderer
|
||||
markdown={data.cuestionario.descripcion}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.cuestionario.fecha_inicio &&
|
||||
data.cuestionario.fecha_fin && (
|
||||
<p className="text-muted mb-0 fs-5 fw-bold text-decoration-underline">
|
||||
<i
|
||||
className="bi bi-clock me-1 text-primary"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{formatearRangoFechas(
|
||||
data.cuestionario.fecha_inicio,
|
||||
data.cuestionario.fecha_fin
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -198,7 +354,11 @@ export default function FormularioPage({
|
||||
<div className="ps-lg-4">
|
||||
{data && (
|
||||
<FormularioRegistro
|
||||
evento={data.nombre_evento}
|
||||
cuestionario={data?.cuestionario.nombre_form}
|
||||
id_evento={data?.id_evento}
|
||||
id_cuestionario={data?.cuestionario.id_cuestionario}
|
||||
handleSubmitFormulario={onSubmitFormulario}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user