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:
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
@@ -12,6 +12,8 @@ import { useParams, useRouter } from 'next/navigation';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import { getAxiosError } from '@/utils/errors-utils';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useGetApi } from '@/hooks/use-get-api';
|
||||
import { TipoEvento } from '@/types/evento';
|
||||
|
||||
type Params = {
|
||||
id_evento: string;
|
||||
@@ -23,6 +25,7 @@ export default function Page() {
|
||||
const { evento, refetch } = useEvento();
|
||||
const [datosFormulario, setDatosFormulario] =
|
||||
useState<FormularioCreacion | null>(null);
|
||||
const { data: tipo_eventos } = useGetApi<TipoEvento[]>(`/tipo-evento`);
|
||||
|
||||
const handleSeleccionarPlantilla = (plantillaId: string) => {
|
||||
const seleccionada = plantillasDisponibles.find(
|
||||
@@ -223,6 +226,10 @@ export default function Page() {
|
||||
formulario: datosFormulario,
|
||||
onChange: (nuevo) => setDatosFormulario({ ...nuevo }),
|
||||
evento: evento ? evento : undefined,
|
||||
tipo_eventos: tipo_eventos?.map((tipo) => ({
|
||||
value: tipo.id_tipo_evento,
|
||||
label: tipo.tipo_evento,
|
||||
})),
|
||||
});
|
||||
return formularioEditor.informacionBasica();
|
||||
})()}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
import Button from '@/components/button';
|
||||
import Table, { Header } from '@/components/table';
|
||||
import { useGetApi } from '@/hooks/use-get-api';
|
||||
import { Administrador } from '@/types/administradores';
|
||||
import React from 'react';
|
||||
|
||||
export default function Page() {
|
||||
const { data } = useGetApi<Administrador[]>('/administrador');
|
||||
|
||||
const headers: Header<Administrador>[] = [
|
||||
{ key: 'id_administrador', label: 'ID' },
|
||||
{ key: 'nombre_usuario', label: 'Nombre de Usuario' },
|
||||
{ key: 'correo', label: 'Correo' },
|
||||
{
|
||||
key: 'tipoUser',
|
||||
render: (_, row) => row.tipoUser.tipo,
|
||||
label: 'Tipo de Usuario',
|
||||
},
|
||||
{
|
||||
key: 'tipoUser',
|
||||
render: () => (
|
||||
<div className="d-flex gap-2">
|
||||
<Button className="w-100" disabled>
|
||||
Editar
|
||||
</Button>
|
||||
<Button variant="danger" className="w-100" disabled>
|
||||
Eliminar
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
label: 'Acciones',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="d-flex justify-content-between align-items-center my-3">
|
||||
<h1>Administradores</h1>
|
||||
<Button variant="primary">Agregar Administrador</Button>
|
||||
</div>
|
||||
{data && <Table headers={headers} data={data} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,28 +3,13 @@ import SimpleInput from '@/components/input';
|
||||
import PasswordInput from '@/components/password';
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
|
||||
export default function LoginForm() {
|
||||
const router = useRouter();
|
||||
|
||||
// Fake user data
|
||||
const fakeUsers = [
|
||||
{
|
||||
username: 'user-admin',
|
||||
password: '@dm1nP@ss',
|
||||
role: 'administrator',
|
||||
redirectPath: '/user/eventos',
|
||||
},
|
||||
{
|
||||
username: 'staff1',
|
||||
password: 'st@ffP@ss',
|
||||
role: 'staff',
|
||||
redirectPath: '/user/eventos',
|
||||
},
|
||||
];
|
||||
|
||||
const [loginData, setLoginData] = React.useState({
|
||||
username: '',
|
||||
nombre_usuario: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
@@ -38,36 +23,53 @@ export default function LoginForm() {
|
||||
if (error) setError('');
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
// Simulate API call delay
|
||||
setTimeout(() => {
|
||||
const user = fakeUsers.find(
|
||||
(u) =>
|
||||
u.username === loginData.username && u.password === loginData.password
|
||||
);
|
||||
try {
|
||||
const response = await axiosInstance.post('/administrador/login', {
|
||||
nombre_usuario: loginData.nombre_usuario,
|
||||
password: loginData.password,
|
||||
});
|
||||
|
||||
if (user) {
|
||||
// Store user data in localStorage (in a real app, you'd use proper session management)
|
||||
localStorage.setItem(
|
||||
'user',
|
||||
JSON.stringify({
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
})
|
||||
);
|
||||
// Guardar el token en sessionStorage
|
||||
if (response.data.token) {
|
||||
sessionStorage.setItem('token', response.data.token);
|
||||
|
||||
// Redirect to appropriate dashboard
|
||||
router.push(user.redirectPath);
|
||||
// Opcional: guardar información adicional del usuario si viene en la respuesta
|
||||
if (response.data.user) {
|
||||
sessionStorage.setItem('user', JSON.stringify(response.data.user));
|
||||
}
|
||||
|
||||
// Redireccionar al dashboard
|
||||
router.push('/user/eventos');
|
||||
} else {
|
||||
setError('Usuario o contraseña incorrectos');
|
||||
setError('No se recibió token de autenticación');
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error('Error en login:', error);
|
||||
|
||||
// Type guard para axios error
|
||||
if (error && typeof error === 'object' && 'response' in error) {
|
||||
const axiosError = error as {
|
||||
response?: { status?: number; data?: { message?: string } };
|
||||
};
|
||||
|
||||
if (axiosError.response?.status === 401) {
|
||||
setError('Credenciales incorrectas');
|
||||
} else if (axiosError.response?.data?.message) {
|
||||
setError(axiosError.response.data.message);
|
||||
} else {
|
||||
setError('Error al iniciar sesión. Inténtalo de nuevo.');
|
||||
}
|
||||
} else {
|
||||
setError('Error al iniciar sesión. Inténtalo de nuevo.');
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -88,7 +90,7 @@ export default function LoginForm() {
|
||||
<span className="text-dorado">Sistema de Eventos</span>
|
||||
</h6>
|
||||
<p className="text-muted mt-2 mb-4">
|
||||
Ingresa tu nombre de usuario y tu contraseña para acceder
|
||||
Ingresa tu correo electrónico y tu contraseña para acceder
|
||||
a tu cuenta.
|
||||
</p>
|
||||
|
||||
@@ -101,9 +103,9 @@ export default function LoginForm() {
|
||||
<SimpleInput
|
||||
label="Nombre de usuario"
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value={loginData.username}
|
||||
id="nombre_usuario"
|
||||
name="nombre_usuario"
|
||||
value={loginData.nombre_usuario}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
|
||||
@@ -105,7 +105,7 @@ export default function EventoCardAdmin({
|
||||
Formularios disponibles:
|
||||
</h6>
|
||||
<div className="row row-cols-1 g-2">
|
||||
{evento.cuestionarios.map((cuestionario) => (
|
||||
{evento.cuestionarios.slice(0, 3).map((cuestionario) => (
|
||||
<div key={cuestionario.id_cuestionario} className="col">
|
||||
<div className="py-2 px-3 bg-light border-0">
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
|
||||
@@ -5,6 +5,7 @@ import Link from 'next/link';
|
||||
import React, { useState } from 'react';
|
||||
import MarkdownRenderer from '../markdown-render';
|
||||
import { toParam } from '@/utils/slugify';
|
||||
import { formatearFechaCard, formatearHorario } from '@/utils/date-utils';
|
||||
import Button from '../button';
|
||||
|
||||
export default function FormularioCardUser({
|
||||
@@ -22,6 +23,22 @@ export default function FormularioCardUser({
|
||||
const sinCupos =
|
||||
formulario.cupo_maximo !== null && formulario.cupos_disponibles <= 0;
|
||||
|
||||
// Verificar si el evento es el mismo día
|
||||
const fechaInicio = new Date(formulario.fecha_inicio);
|
||||
const fechaFin = new Date(formulario.fecha_fin);
|
||||
const esElMismoDia = fechaInicio.toDateString() === fechaFin.toDateString();
|
||||
|
||||
// Obtener información de fecha formateada usando las utilidades
|
||||
const { mesTexto, diaTexto } = formatearFechaCard(
|
||||
formulario.fecha_inicio,
|
||||
formulario.fecha_fin
|
||||
);
|
||||
|
||||
// Obtener horario formateado si es el mismo día
|
||||
const horarioFormateado = esElMismoDia
|
||||
? formatearHorario(formulario.fecha_inicio, formulario.fecha_fin)
|
||||
: null;
|
||||
|
||||
const descripcionRecortada =
|
||||
descripcion.length > limite && !verMas
|
||||
? `${descripcion.slice(0, limite)}...`
|
||||
@@ -78,7 +95,7 @@ export default function FormularioCardUser({
|
||||
{/* Badge de cupos en la esquina superior derecha */}
|
||||
<div className="position-absolute top-0 end-0 m-2">
|
||||
<span className="badge bg-primary">
|
||||
{formulario.tipoCuestionario.tipo_cuestionario}
|
||||
{formulario.tipoCuestionario.tipo_cuestionario}{' '}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,34 +104,24 @@ export default function FormularioCardUser({
|
||||
{/* Fecha del evento */}
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<div className="me-3">
|
||||
<div className="text-muted text-center small">
|
||||
{new Date(formulario.fecha_inicio)
|
||||
.toLocaleDateString('es-ES', { month: 'short' })
|
||||
.toUpperCase()}
|
||||
</div>
|
||||
<div className="text-muted text-center small">{mesTexto}</div>
|
||||
<div
|
||||
className="fw-bold h4 mb-0"
|
||||
style={{ lineHeight: 1, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{(() => {
|
||||
const fechaInicio = new Date(formulario.fecha_inicio);
|
||||
const fechaFin = new Date(formulario.fecha_fin);
|
||||
const diaInicio = fechaInicio.getDate();
|
||||
const diaFin = fechaFin.getDate();
|
||||
const esElMismoDia =
|
||||
fechaInicio.toDateString() === fechaFin.toDateString();
|
||||
|
||||
return esElMismoDia ? diaInicio : `${diaInicio} - ${diaFin}`;
|
||||
})()}
|
||||
{diaTexto}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="small text-muted fw-bold">
|
||||
{evento.nombre_evento}
|
||||
</span>
|
||||
<span className="text-muted fw-bold">{evento.nombre_evento}</span>
|
||||
<h5 className="card-title mb-1 fw-bold">
|
||||
{formulario.nombre_form}
|
||||
</h5>
|
||||
{formulario.tipoEvento && (
|
||||
<h6 className="small text-muted">
|
||||
{formulario.tipoEvento.tipo_evento}
|
||||
</h6>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -133,27 +140,50 @@ export default function FormularioCardUser({
|
||||
</div>
|
||||
|
||||
{/* Información de cupos y estadísticas */}
|
||||
<div className="mb-3">
|
||||
<div
|
||||
className={`py-2 px-3 rounded ${
|
||||
sinCupos ? 'bg-primary-subtle' : 'bg-light'
|
||||
}`}
|
||||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<i className={`bi bi-people-fill me-2 text-primary`}></i>
|
||||
<div>
|
||||
<small className="text-muted d-block">Cupos</small>
|
||||
<span className={`fw-semibold text-primary`}>
|
||||
{formulario.cupo_maximo !== null
|
||||
? `${formulario.cupos_usados} / ${formulario.cupo_maximo}`
|
||||
: 'Sin límite'}
|
||||
</span>
|
||||
{sinCupos && (
|
||||
<div className="small text-primary mt-1">
|
||||
<i className="bi bi-exclamation-circle me-1"></i>
|
||||
Cupos agotados
|
||||
<div className="row mb-3">
|
||||
{/* Columna de horario - solo visible si es el mismo día */}
|
||||
{esElMismoDia && (
|
||||
<div className="col-6">
|
||||
<div
|
||||
className={`py-2 px-3 rounded ${
|
||||
sinCupos ? 'bg-primary-subtle' : 'bg-light'
|
||||
}`}
|
||||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<i className={`bi bi-clock me-2 text-azul`}></i>
|
||||
<div>
|
||||
<small className="text-muted d-block">Horario</small>
|
||||
<span className={`fw-semibold text-azul`}>
|
||||
{horarioFormateado}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Columna de cupos - siempre visible */}
|
||||
<div className={esElMismoDia ? 'col-6' : 'col-12'}>
|
||||
<div
|
||||
className={`py-2 px-3 rounded ${
|
||||
sinCupos ? 'bg-primary-subtle' : 'bg-light'
|
||||
}`}
|
||||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<i className={`bi bi-people-fill me-2 text-primary`}></i>
|
||||
<div>
|
||||
<small className="text-muted d-block">Cupos</small>
|
||||
<span className={`fw-semibold text-primary`}>
|
||||
{formulario.cupo_maximo !== null
|
||||
? `${formulario.cupos_usados} / ${formulario.cupo_maximo}`
|
||||
: 'Sin límite'}
|
||||
</span>
|
||||
{sinCupos && (
|
||||
<div className="small text-primary mt-1">
|
||||
<i className="bi bi-exclamation-circle me-1"></i>
|
||||
Cupos agotados
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ const Navbar: React.FC = () => {
|
||||
{ href: '/user/eventos', label: 'Eventos' },
|
||||
{ href: '/user/qr', label: 'Lector de QRs' },
|
||||
{ href: '/user/eventos/crear', label: 'Crear Evento' },
|
||||
{ href: '/user/usuarios', label: 'Usuarios' },
|
||||
{ href: '/signout', label: 'Cerrar sesión' },
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useId } from 'react';
|
||||
|
||||
interface Option {
|
||||
export interface Option {
|
||||
value: string | number;
|
||||
label: string;
|
||||
}
|
||||
@@ -74,7 +74,10 @@ const Select: React.FC<SelectProps> = ({
|
||||
|
||||
return (
|
||||
<div className={`mb-3 ${className?.container ?? ''}`}>
|
||||
<label htmlFor={selectId} className={`form-label ${className?.label ?? ''}`}>
|
||||
<label
|
||||
htmlFor={selectId}
|
||||
className={`form-label ${className?.label ?? ''}`}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
<select
|
||||
|
||||
@@ -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>
|
||||
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
export interface Administrador {
|
||||
id_administrador: number;
|
||||
nombre_usuario: string;
|
||||
correo: string;
|
||||
tipoUser: {
|
||||
id: number;
|
||||
tipo: string;
|
||||
};
|
||||
}
|
||||
Vendored
+21
-20
@@ -1,25 +1,25 @@
|
||||
export type TiposValidacion =
|
||||
| 'correo'
|
||||
| 'correo_institucional'
|
||||
| 'telefono'
|
||||
| 'nombre'
|
||||
| 'entero'
|
||||
| 'apellidos'
|
||||
| 'decimal'
|
||||
| 'comunidad_alumno'
|
||||
| 'cuenta_alumno'
|
||||
| 'comunidad_trabajador'
|
||||
| 'cuenta_trabajador'
|
||||
| 'genero'
|
||||
| 'carrera'
|
||||
| 'institucion'
|
||||
| 'rfc'
|
||||
| 'correo'
|
||||
| 'correo_institucional'
|
||||
| 'telefono'
|
||||
| 'nombre'
|
||||
| 'entero'
|
||||
| 'apellidos'
|
||||
| 'decimal'
|
||||
| 'comunidad_alumno'
|
||||
| 'cuenta_alumno'
|
||||
| 'comunidad_trabajador'
|
||||
| 'cuenta_trabajador'
|
||||
| 'genero'
|
||||
| 'carrera'
|
||||
| 'institucion'
|
||||
| 'rfc';
|
||||
|
||||
export type TipoPregunta =
|
||||
| 'Abierta (Respuesta corta)'
|
||||
| 'Abierta (Parrafo)'
|
||||
| 'Cerrada'
|
||||
| 'Multiple';
|
||||
export type TipoPregunta =
|
||||
| 'Abierta (Respuesta corta)'
|
||||
| 'Abierta (Parrafo)'
|
||||
| 'Cerrada'
|
||||
| 'Multiple';
|
||||
|
||||
export interface FormularioCreacion {
|
||||
nombre_form: string;
|
||||
@@ -27,6 +27,7 @@ export interface FormularioCreacion {
|
||||
fecha_inicio: string; // ISO 8601 date string
|
||||
fecha_fin: string;
|
||||
id_tipo_cuestionario: number;
|
||||
id_tipo_evento?: number;
|
||||
cupo_maximo?: number; // Opcional, si no se requiere un cupo máximo
|
||||
secciones: SeccionFormulario[];
|
||||
}
|
||||
|
||||
Vendored
+9
@@ -28,6 +28,10 @@ export interface Cuestionario {
|
||||
id_tipo_cuestionario: number;
|
||||
tipo_cuestionario: string;
|
||||
};
|
||||
tipoEvento: {
|
||||
id_tipo_evento: number;
|
||||
tipo_evento: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface CuestionarioWithCupo extends Cuestionario {
|
||||
@@ -93,3 +97,8 @@ export interface GetEventoWithCuestionarios extends GetEvento {
|
||||
cuestionarios: Cuestionario[];
|
||||
total_cuestionarios: number;
|
||||
}
|
||||
|
||||
export interface TipoEvento {
|
||||
id_tipo_evento: number;
|
||||
tipo_evento: string;
|
||||
}
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
export interface SubmitResponse {
|
||||
success: boolean;
|
||||
registrado: boolean;
|
||||
message: string;
|
||||
qr_token: string;
|
||||
qr_buffer: string;
|
||||
}
|
||||
@@ -14,4 +14,46 @@ const axiosInstance = axios.create({
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor para agregar el token de autenticación
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
// Obtener el token del sessionStorage
|
||||
const token = sessionStorage.getItem('token');
|
||||
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor para manejar errores de autenticación
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response) => {
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
// Si el token ha expirado o es inválido (401)
|
||||
if (error.response?.status === 401) {
|
||||
// Limpiar el sessionStorage
|
||||
sessionStorage.removeItem('token');
|
||||
sessionStorage.removeItem('user');
|
||||
|
||||
// Redireccionar al login solo si no estamos ya en la página de login
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
!window.location.pathname.includes('/login')
|
||||
) {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default axiosInstance;
|
||||
|
||||
@@ -268,3 +268,63 @@ export function formatearFechaCard(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrae y formatea el horario entre dos fechas.
|
||||
* Retorna el rango de horas en formato "HH:MM a HH:MM Hrs".
|
||||
*
|
||||
* @param {string | Date} fechaInicio - Fecha de inicio en formato ISO 8601 o objeto Date.
|
||||
* @param {string | Date} fechaFin - Fecha de fin en formato ISO 8601 o objeto Date.
|
||||
* @returns {string} Horario formateado en formato "HH:MM a HH:MM Hrs".
|
||||
* @throws {Error} Lanza un error si alguna de las fechas proporcionadas no es válida.
|
||||
*
|
||||
* @example
|
||||
* formatearHorario('2025-08-13T13:00:00.000Z', '2025-08-13T14:00:00.000Z');
|
||||
* // Retorna: "07:00 a 08:00 Hrs" (ajustado a UTC-6)
|
||||
*
|
||||
* @example
|
||||
* formatearHorario(new Date('2025-08-13T19:30:00.000Z'), new Date('2025-08-13T21:45:00.000Z'));
|
||||
* // Retorna: "13:30 a 15:45 Hrs"
|
||||
*
|
||||
* @example
|
||||
* formatearHorario('2025-08-13T13:00:00.000Z', '2025-08-13T13:00:00.000Z');
|
||||
* // Retorna: "07:00 Hrs" (misma hora)
|
||||
*/
|
||||
export function formatearHorario(
|
||||
fechaInicio: string | Date,
|
||||
fechaFin: string | Date
|
||||
): string {
|
||||
// Convertir a Date si son strings
|
||||
const inicio =
|
||||
typeof fechaInicio === 'string' ? new Date(fechaInicio) : fechaInicio;
|
||||
const fin = typeof fechaFin === 'string' ? new Date(fechaFin) : fechaFin;
|
||||
|
||||
if (isNaN(inicio.getTime())) {
|
||||
throw new Error(`Fecha de inicio inválida: ${fechaInicio}`);
|
||||
}
|
||||
|
||||
if (isNaN(fin.getTime())) {
|
||||
throw new Error(`Fecha de fin inválida: ${fechaFin}`);
|
||||
}
|
||||
|
||||
// Ajustar a zona horaria UTC-6 (México)
|
||||
const horaInicio = (inicio.getUTCHours() - 6 + 24) % 24;
|
||||
const minutosInicio = inicio.getUTCMinutes().toString().padStart(2, '0');
|
||||
|
||||
const horaFin = (fin.getUTCHours() - 6 + 24) % 24;
|
||||
const minutosFin = fin.getUTCMinutes().toString().padStart(2, '0');
|
||||
|
||||
const horaInicioFormateada = `${horaInicio
|
||||
.toString()
|
||||
.padStart(2, '0')}:${minutosInicio}`;
|
||||
const horaFinFormateada = `${horaFin
|
||||
.toString()
|
||||
.padStart(2, '0')}:${minutosFin}`;
|
||||
|
||||
// Si es la misma hora, solo mostrar una vez
|
||||
if (horaInicioFormateada === horaFinFormateada) {
|
||||
return `${horaInicioFormateada} Hrs`;
|
||||
}
|
||||
|
||||
return `${horaInicioFormateada} a ${horaFinFormateada}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user