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:
miguel
2025-08-19 10:26:02 -06:00
parent 24642d1c3b
commit 99bfb46b8b
20 changed files with 610 additions and 207 deletions
+15 -8
View File
@@ -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">
+27 -10
View File
@@ -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}
+235 -75
View File
@@ -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>