feat: add cupo_maximo field to event creation and editing forms; update related components and types

This commit is contained in:
miguel
2025-06-23 12:40:40 -06:00
parent 50e37aa8fa
commit 613cc0b711
8 changed files with 268 additions and 127 deletions
@@ -76,6 +76,7 @@ export default function Page() {
const createFormulario = await axiosInstance.post('/cuestionario', {
id_evento: createEvento.data.id_evento,
cupo_maximo: Number(evento.cupo_maximo),
...datosFormulario,
});
if (createFormulario.data.id_formulario) {
@@ -1,17 +1,14 @@
'use client';
import SimpleInput from '@/components/input';
import EditEvento from '@/containers/edit-evento';
import Table, { Header } from '@/components/table';
import { useGetApi } from '@/hooks/use-get-api';
import { GetEventoWithCuestionario } from '@/types/evento';
import { useParams } from 'next/navigation';
import React, { useState, useEffect } from 'react';
import { CreateEventoType } from '@/types/create-evento';
import { GetCuestionario } from '@/types/cuestionario';
import { ParticipacionEvento } from '@/types/participante-evento';
import axiosInstance from '@/utils/api-config';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import React from 'react';
import toast from 'react-hot-toast';
import Table, { Header } from '@/components/table';
import Button from '@/components/button';
import { downloadFile } from '@/utils/downloas-utils';
type Params = {
id_evento: string;
@@ -20,37 +17,17 @@ type Params = {
export default function Page() {
const params = useParams<Params>();
const { data } = useGetApi<GetEventoWithCuestionario>(
`/evento/${params.id_evento}/cuestionario/${params.id_cuestionario}`
);
const { data: participantes, setData } = useGetApi<ParticipacionEvento[]>(
`/participante-evento/evento/${params.id_cuestionario}`
);
const [evento, setEvento] = useState<GetEventoWithCuestionario | null>(null);
const [busquedaCorreo, setBusquedaCorreo] = React.useState('');
const [loading, setLoading] = useState(false);
const [loadingAsistencia, setLoadingAsistencia] = React.useState<
Record<number, boolean>
>({});
useEffect(() => {
if (data) setEvento(data);
}, [data]);
const handleChange = (
field: keyof CreateEventoType,
value: string | Date
) => {
setEvento((prev) =>
prev
? {
...prev,
[field]: value,
}
: null
);
};
const { data: participantes, setData } = useGetApi<ParticipacionEvento[]>(
`/participante-evento/evento/${params.id_cuestionario}`
);
const { data: cuestionario } = useGetApi<GetCuestionario>(
`/cuestionario/${params.id_cuestionario}`
);
const confirmarAsistencia = async (
id_participante: number,
@@ -76,12 +53,6 @@ export default function Page() {
}
};
const handleEventoActualizado = (
eventoActualizado: GetEventoWithCuestionario
) => {
setEvento(eventoActualizado);
};
const headers: Header<ParticipacionEvento>[] = [
{
key: 'id_participante',
@@ -144,63 +115,19 @@ export default function Page() {
p.participante.correo.toLowerCase().includes(busquedaCorreo.toLowerCase())
);
if (!evento) return <p>Cargando...</p>;
const handleDownload = async () => {
setLoading(true);
try {
const res = await axiosInstance.get(
`/cuestionario-respondido/reporte-respuestas/${params.id_cuestionario}`,
{
responseType: 'blob',
}
);
downloadFile(
res.data,
`reporte_respuestas_formulario_${params.id_cuestionario}`,
'csv'
);
} catch (error) {
console.error('Error al descargar el archivo:', error);
toast.error('Error al descargar el archivo');
} finally {
setLoading(false);
}
};
return (
<div className="container my-4">
<h2 className="mb-4">Evento</h2>
<EditEvento
evento={evento}
handleChange={handleChange}
handleOnChange={handleEventoActualizado}
/>
<div className="d-flex justify-content-between align-items-center mt-4">
<h3 className="mt-5">Participantes</h3>
<div>
<Button onClick={handleDownload} icon="download" variant='success' disabled={loading}>
{loading ? (
<>
<span
className="spinner-border spinner-border-sm me-2"
role="status"
aria-hidden="true"
></span>
Descargando...
</>
) : (
'Descargar respuestas'
)}
</Button>
</div>
</div>
<div>
<h2 className="mt-4">{cuestionario?.nombre_form}</h2>
<p className="text-muted mb-4">
Aquí puedes ver los participantes registrados y confirmar su asistencia.
</p>
<Link
href={`/administrador/evento/${params.id_evento}`}
className="btn btn-link mb-3"
>
<i className="bi bi-arrow-left me-2"></i>
Volver
</Link>
{participantes && participantes.length > 0 && (
<>
<SimpleInput
@@ -0,0 +1,155 @@
'use client';
import EditEvento from '@/containers/edit-evento';
import { useGetApi } from '@/hooks/use-get-api';
import {
CuestionarioWithCupo,
GetEventoWithCuestionarios,
} from '@/types/evento';
import { useParams } from 'next/navigation';
import React, { useState, useEffect } from 'react';
import { CreateEventoType } from '@/types/create-evento';
import axiosInstance from '@/utils/api-config';
import toast from 'react-hot-toast';
import Table, { Header } from '@/components/table';
import Button from '@/components/button';
import { downloadFile } from '@/utils/downloas-utils';
import Link from 'next/link';
type Params = {
id_evento: string;
};
export default function Page() {
const params = useParams<Params>();
const { data } = useGetApi<GetEventoWithCuestionarios>(
`/evento/${params.id_evento}/cuestionarios`
);
const [evento, setEvento] = useState<GetEventoWithCuestionarios | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (data) setEvento(data);
}, [data]);
const handleChange = (
field: keyof CreateEventoType,
value: string | Date
) => {
setEvento((prev) =>
prev
? {
...prev,
[field]: value,
}
: null
);
};
const handleEventoActualizado = (
eventoActualizado: GetEventoWithCuestionarios
) => {
setEvento(eventoActualizado);
};
const headers: Header<CuestionarioWithCupo>[] = [
{
key: 'nombre_form',
label: 'Nombre del cuestionario',
},
{
key: 'cupo_maximo',
label: 'Cupos',
render: (_, row) => {
if (row.cupo_maximo === null) {
return 'Sin límite';
}
return `${row.cupos_usados ?? 0} / ${row.cupo_maximo}`;
},
},
{
key: 'banner',
label: 'Ver / Editar',
render: (_, row) => (
<div className="d-flex gap-2">
<Link
className="btn btn-primary"
href={`/administrador/evento/${params.id_evento}/${row.id_cuestionario}`}
>
Ver/Editar formulario
</Link>
</div>
),
},
{
key: 'banner',
label: 'Descargar respuestas',
render: (_, row) => (
<Button
variant="success"
icon="download"
onClick={() => handleDownload(row.id_cuestionario, row.nombre_form)}
disabled={loading}
>
{loading ? (
<>
<span
className="spinner-border spinner-border-sm me-2"
role="status"
aria-hidden="true"
></span>
Descargando...
</>
) : (
'Descargar respuestas'
)}
</Button>
),
},
];
if (!evento) return <p>Cargando...</p>;
const handleDownload = async (id_cuestionario: number, nombre: string) => {
setLoading(true);
try {
const res = await axiosInstance.get(
`/cuestionario-respondido/reporte-respuestas/${id_cuestionario}`,
{
responseType: 'blob',
}
);
downloadFile(res.data, `${nombre}`, 'csv');
} catch (error) {
console.error('Error al descargar el archivo:', error);
toast.error('Error al descargar el archivo');
} finally {
setLoading(false);
}
};
return (
<div className="container my-4">
<h2 className="mb-4">Evento</h2>
<EditEvento
evento={evento}
handleChange={handleChange}
handleOnChange={handleEventoActualizado}
/>
{data?.cuestionarios && data.cuestionarios.length > 0 && (
<>
<h3 className="mt-5">Cuestionarios del evento</h3>
<div className="mt-2">
<Table headers={headers} data={data.cuestionarios ?? []} />
</div>
</>
)}
</div>
);
}
+42 -17
View File
@@ -1,3 +1,4 @@
'use client';
import { GetEventoWithCuestionarios } from '@/types/evento';
import Image from 'next/image';
import Link from 'next/link';
@@ -12,7 +13,7 @@ export default function EventoCard({
user?: 'public' | 'administrador' | 'staff';
}) {
const [verMas, setVerMas] = useState(false);
const descripcion = evento.descripcion_evento;
const descripcion = evento.descripcion_evento ?? '';
const limite = 100;
const descripcionRecortada =
@@ -26,11 +27,11 @@ export default function EventoCard({
<div className="card-image scale-hover-1">
<Link
href={
user === 'public'
? `/evento/${evento.nombre_evento.split(' ').join('_')}/${
user === 'administrador' || user === 'staff'
? `/${user}/evento/${evento.id_evento}`
: `/evento/${evento.nombre_evento.split(' ').join('_')}/${
evento.id_evento
}/${evento.cuestionarios[0]?.id_cuestionario}`
: `/${user}/evento/${evento.id_evento}/${evento.cuestionarios[0]?.id_cuestionario}`
}`
}
>
<Image
@@ -44,8 +45,16 @@ export default function EventoCard({
</Link>
<div className="ripple-cont"></div>
</div>
<div className="card-body">
<div className="card-description mb-0">
{evento.cuestionarios[0]?.cupo_maximo === null ? (
<span className="badge bg-success">Sin límite</span>
) : (
<span className="badge bg-primary mb-2">
{evento.cuestionarios[0]?.cupos_disponibles} cupos disponibles
</span>
)}
<MarkdownRenderer markdown={descripcionRecortada} />
{descripcion.length > limite && (
<button
@@ -57,21 +66,37 @@ export default function EventoCard({
</button>
)}
</div>
{evento.cuestionarios.length > 0 &&
evento.cuestionarios.map((cuestionario) => (
<div key={cuestionario.id_cuestionario} className="">
<Link
href={
user === 'public'
? `/evento/${evento.nombre_evento.split(' ').join('_')}/${
evento.id_evento
}/${cuestionario.id_cuestionario}`
: `/${user}/evento/${evento.id_evento}/${cuestionario.id_cuestionario}`
}
className="btn mt-2 w-100 btn-outline-primary"
>
Regístrate
</Link>
{user === 'administrador' || user === 'staff' ? (
<Link
href={`/${user}/evento/${evento.id_evento}`}
className="btn mt-2 w-100 btn-outline-primary"
>
Ver evento
</Link>
) : cuestionario.cupos_disponibles === 0 &&
cuestionario.cupo_maximo !== null ? (
<button
className="btn mt-2 w-100 btn-outline-secondary"
disabled
>
Cupo lleno
</button>
) : (
<Link
href={`/evento/${evento.nombre_evento
.split(' ')
.join('_')}/${evento.id_evento}/${
cuestionario.id_cuestionario
}`}
className="btn mt-2 w-100 btn-outline-primary"
>
Regístrate
</Link>
)}
</div>
))}
</div>
+27 -9
View File
@@ -33,12 +33,24 @@ export default function CreateEvento({
<ImageUploader label="Banner del evento" onChange={handleBannerChange} />
<Input
label="Nombre del evento"
required
value={evento.nombre_evento}
onChange={(e) => handleChange('nombre_evento', e.target.value)}
/>
<div className="col-md-8">
<Input
label="Nombre del evento"
required
value={evento.nombre_evento}
onChange={(e) => handleChange('nombre_evento', e.target.value)}
/>
</div>
<div className="col-md-4">
<Input
label="Cupo máximo"
type='number'
required
value={evento.cupo_maximo}
onChange={(e) => handleChange('cupo_maximo', e.target.value)}
/>
</div>
<div className="mb-4">
<label className="form-label">Descripción del evento</label>
@@ -66,6 +78,14 @@ export default function CreateEvento({
{ label: 'Conferencia', value: 'conferencia' },
{ label: 'Taller', value: 'taller' },
{ label: 'Seminario', value: 'seminario' },
{ label: 'Curso', value: 'curso' },
{ label: 'Webinar', value: 'webinar' },
{ label: 'Reunión', value: 'reunion' },
{ label: 'Feria', value: 'feria' },
{ label: 'Concierto', value: 'concierto' },
{ label: 'Exposición', value: 'exposicion' },
{ label: 'Deportivo', value: 'deportivo' },
{ label: 'Cultural', value: 'cultural' },
{ label: 'Otro', value: 'otro' },
]}
/>
@@ -88,9 +108,7 @@ export default function CreateEvento({
type="datetime-local"
className={{ container: 'mb-3' }}
value={formatDateLocal(evento.fecha_fin)}
onChange={(e) =>
handleChange('fecha_fin', new Date(e.target.value))
}
onChange={(e) => handleChange('fecha_fin', new Date(e.target.value))}
/>
</div>
</div>
+12 -4
View File
@@ -4,7 +4,7 @@ import Button from '@/components/button';
import Input from '@/components/input';
import Select from '@/components/select';
import { CreateEventoType } from '@/types/create-evento';
import { GetEventoWithCuestionario } from '@/types/evento';
import { GetEventoWithCuestionarios } from '@/types/evento';
import axiosInstance from '@/utils/api-config';
import { getAxiosError } from '@/utils/errors-utils';
import { commands } from '@uiw/react-md-editor';
@@ -14,9 +14,9 @@ import toast from 'react-hot-toast';
const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false });
interface EditEventoProps {
evento: GetEventoWithCuestionario;
evento: GetEventoWithCuestionarios;
handleChange: (field: keyof CreateEventoType, value: string | Date) => void;
handleOnChange: (eventoActualizado: GetEventoWithCuestionario) => void;
handleOnChange: (eventoActualizado: GetEventoWithCuestionarios) => void;
}
export default function EditEvento({
@@ -95,7 +95,7 @@ export default function EditEvento({
<label className="form-label">Descripción del evento</label>
<div data-color-mode="light">
<MDEditor
value={evento.descripcion_evento}
value={evento.descripcion_evento ?? ''}
onChange={(value) => {
const texto = value || '';
if (texto.length <= 500) {
@@ -117,6 +117,14 @@ export default function EditEvento({
{ label: 'Conferencia', value: 'conferencia' },
{ label: 'Taller', value: 'taller' },
{ label: 'Seminario', value: 'seminario' },
{ label: 'Curso', value: 'curso' },
{ label: 'Webinar', value: 'webinar' },
{ label: 'Reunión', value: 'reunion' },
{ label: 'Feria', value: 'feria' },
{ label: 'Concierto', value: 'concierto' },
{ label: 'Exposición', value: 'exposicion' },
{ label: 'Deportivo', value: 'deportivo' },
{ label: 'Cultural', value: 'cultural' },
{ label: 'Otro', value: 'otro' },
]}
/>
+1
View File
@@ -4,4 +4,5 @@ export interface CreateEventoType {
descripcion_evento?: string; // Es opciona
fecha_inicio: Date;
fecha_fin: Date;
cupo_maximo?: number; // Es opcional
}
+7 -1
View File
@@ -23,12 +23,18 @@ export interface Cuestionario {
fecha_fin: string; // formato ISO 8601
id_tipo_cuestionario: number;
id_evento: number;
cupo_maximo: number | null;
tipoCuestionario: {
id_tipo_cuestionario: number;
tipo_cuestionario: string;
};
}
export interface CuestionarioWithCupo extends Cuestionario {
cupos_usados: number;
cupos_disponibles: number;
}
export interface GetCuestionario {
tipo_cuestionario: {
id_tipo_cuestionario: number;
@@ -77,7 +83,7 @@ export interface CuestionarioWithSecciones extends Cuestionario {
}
export interface GetEventoWithCuestionarios extends GetEvento {
cuestionarios: Cuestionario[];
cuestionarios: CuestionarioWithCupo[];
}
export interface GetEventoWithCuestionario extends GetEvento {