feat: add cupo_maximo field to event creation and editing forms; update related components and types
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' },
|
||||
]}
|
||||
/>
|
||||
|
||||
Vendored
+1
@@ -4,4 +4,5 @@ export interface CreateEventoType {
|
||||
descripcion_evento?: string; // Es opciona
|
||||
fecha_inicio: Date;
|
||||
fecha_fin: Date;
|
||||
cupo_maximo?: number; // Es opcional
|
||||
}
|
||||
|
||||
Vendored
+7
-1
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user