From b110109cb3dbd5653c311cf77d3609bc54c6d7e4 Mon Sep 17 00:00:00 2001 From: miguel Date: Wed, 18 Jun 2025 12:37:47 -0600 Subject: [PATCH] feat: Add event management features, including event editing and participant tracking; refactor layout and API integration --- next.config.ts | 4 + .../[id_evento]/[id_cuestionario]}/page.tsx | 190 +++++++++--------- .../formulario/[id_formulario]/layout.tsx | 25 --- src/app/(admins)/administrador/layout.tsx | 2 +- src/app/(admins)/administrador/page.tsx | 84 +++----- src/app/(admins)/staff/lista-manual/page.tsx | 4 +- src/app/(public)/page.tsx | 13 +- src/components/evento-card.tsx | 18 +- src/containers/edit-evento.tsx | 142 +++++++++++++ .../formulario/formulario-registro.tsx | 10 +- src/data/plantillas.ts | 6 +- src/styles/sass/bootstrap.scss | 12 +- src/types/create-formulario.d.ts | 1 + src/types/participante-evento.d.ts | 3 +- 14 files changed, 316 insertions(+), 198 deletions(-) rename src/app/(admins)/administrador/{formulario/[id_formulario] => evento/[id_evento]/[id_cuestionario]}/page.tsx (52%) delete mode 100644 src/app/(admins)/administrador/formulario/[id_formulario]/layout.tsx create mode 100644 src/containers/edit-evento.tsx diff --git a/next.config.ts b/next.config.ts index 3b38372..7442e11 100644 --- a/next.config.ts +++ b/next.config.ts @@ -8,6 +8,10 @@ const nextConfig: NextConfig = { hostname: 'localhost', port: '4200', }, + { + protocol: 'https', + hostname: 'venus.acatlan.unam.mx' + } ], }, }; diff --git a/src/app/(admins)/administrador/formulario/[id_formulario]/page.tsx b/src/app/(admins)/administrador/evento/[id_evento]/[id_cuestionario]/page.tsx similarity index 52% rename from src/app/(admins)/administrador/formulario/[id_formulario]/page.tsx rename to src/app/(admins)/administrador/evento/[id_evento]/[id_cuestionario]/page.tsx index 232b817..0bca7d1 100644 --- a/src/app/(admins)/administrador/formulario/[id_formulario]/page.tsx +++ b/src/app/(admins)/administrador/evento/[id_evento]/[id_cuestionario]/page.tsx @@ -1,42 +1,56 @@ 'use client'; -import Button from '@/components/button'; -import Input from '@/components/input'; -import Table, { Header } from '@/components/table'; +import SimpleInput from '@/components/input'; +import EditEvento from '@/containers/edit-evento'; +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 { ParticipacionEvento } from '@/types/participante-evento'; import axiosInstance from '@/utils/api-config'; -import { downloadFile } from '@/utils/downloas-utils'; -import { useParams } from 'next/navigation'; -import React, { useEffect } 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; + id_cuestionario: string; +}; export default function Page() { - const params = useParams<{ id_formulario: string }>(); - const id_formulario = Number(params?.id_formulario); + const params = useParams(); + const { data } = useGetApi( + `/evento/${params.id_evento}/cuestionario/${params.id_cuestionario}` + ); + const { data: participantes, setData } = useGetApi( + `/participante-evento/evento/${params.id_cuestionario}` + ); - const [participantes, setParticipantes] = React.useState< - ParticipacionEvento[] - >([]); + const [evento, setEvento] = useState(null); + const [busquedaCorreo, setBusquedaCorreo] = React.useState(''); + const [loading, setLoading] = useState(false); const [loadingAsistencia, setLoadingAsistencia] = React.useState< Record >({}); - const [busquedaCorreo, setBusquedaCorreo] = React.useState(''); - const [loading, setLoading] = React.useState(false); useEffect(() => { - if (!id_formulario) return; - const fetchParticipantes = async () => { - try { - const { data } = await axiosInstance.get( - `/participante-evento/evento/${id_formulario}` - ); - setParticipantes(data); - } catch (error) { - console.error('Error al obtener participantes:', error); - } - }; + if (data) setEvento(data); + }, [data]); - fetchParticipantes(); - }, [id_formulario]); + const handleChange = ( + field: keyof CreateEventoType, + value: string | Date + ) => { + setEvento((prev) => + prev + ? { + ...prev, + [field]: value, + } + : null + ); + }; const confirmarAsistencia = async ( id_participante: number, @@ -53,7 +67,7 @@ export default function Page() { const { data } = await axiosInstance.get( `/participante-evento/evento/${id_evento}` ); - setParticipantes(data); + setData(data); } catch (error) { toast.error('Error al confirmar asistencia'); console.error(error); @@ -62,6 +76,12 @@ export default function Page() { } }; + const handleEventoActualizado = ( + eventoActualizado: GetEventoWithCuestionario + ) => { + setEvento(eventoActualizado); + }; + const headers: Header[] = [ { key: 'id_participante', @@ -74,9 +94,12 @@ export default function Page() { render: (_, row) => row.participante.correo, }, { - key: 'fecha_inscripcion', - label: 'Fecha inscripción', - render: (val) => new Date(val as string).toLocaleString(), + key: 'fecha_registro', + label: 'Fecha registro', + render: (_, row) => + row.fecha_registro + ? new Date(row.fecha_registro).toLocaleString() + : 'Sin registro', }, { key: 'estatus', @@ -93,7 +116,7 @@ export default function Page() { - {participantes.length > 0 && ( + + +
+

Participantes

+
+ +
+
+ + {participantes && participantes.length > 0 && ( <> - row.id_participante} /> diff --git a/src/app/(admins)/administrador/formulario/[id_formulario]/layout.tsx b/src/app/(admins)/administrador/formulario/[id_formulario]/layout.tsx deleted file mode 100644 index 4b2545e..0000000 --- a/src/app/(admins)/administrador/formulario/[id_formulario]/layout.tsx +++ /dev/null @@ -1,25 +0,0 @@ -'use client'; -import Link from 'next/link'; -import { useParams } from 'next/navigation'; -import React from 'react'; - -export default function Layout({ children }: { children: React.ReactNode }) { - const params = useParams<{ id_formulario: string }>(); - - return ( -
- - {children} -
- ); -} diff --git a/src/app/(admins)/administrador/layout.tsx b/src/app/(admins)/administrador/layout.tsx index 5b391e5..e98b8ef 100644 --- a/src/app/(admins)/administrador/layout.tsx +++ b/src/app/(admins)/administrador/layout.tsx @@ -5,7 +5,7 @@ import React from 'react'; export default function Layout({ children }: { children: React.ReactNode }) { return (
-
+
{children}
diff --git a/src/app/(admins)/administrador/page.tsx b/src/app/(admins)/administrador/page.tsx index f888364..5a6695e 100644 --- a/src/app/(admins)/administrador/page.tsx +++ b/src/app/(admins)/administrador/page.tsx @@ -1,48 +1,19 @@ 'use client'; import Button from '@/components/button'; -import FormularioCard from '@/components/formulario-card'; -import { GetCuestionario } from '@/types/cuestionario'; +import EventoCard from '@/components/evento-card'; +import { useGetApi } from '@/hooks/use-get-api'; +import { GetEventoWithCuestionarios } from '@/types/evento'; import Link from 'next/link'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; export default function Page() { - const [eventos, setEventos] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - async function getEventos() { - try { - const res = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/cuestionario`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - } - ); - - if (!res.ok) { - throw new Error(`Error ${res.status}: ${res.statusText}`); - } - - const data: GetCuestionario[] = await res.json(); - setEventos(data); - } catch (err) { - console.error('Error en getEventos:', err); - setError('Error al cargar los eventos'); - } finally { - setLoading(false); - } - } - - getEventos(); - }, []); + const { loading, data, error } = useGetApi( + '/evento/activos/cuestionarios' + ); if (loading) return
Cargando formularios...
; - if (error) return
{error}
; + if (error) return
{error.message}
; return ( <> @@ -52,28 +23,25 @@ export default function Page() { -
-
-
- {eventos.map((cuestionario, index) => { - const fadeClass = `delay-${(index % 5) + 1}`; - - return ( -
- -
- ); - })} -
+ {loading &&
Cargando...
} + {data && ( +
+ {data.map((item, key) => { + const fadeClass = `delay-${(key % 5) + 1}`; + return ( +
+ +
+ ); + })}
-
+ )} ); } diff --git a/src/app/(admins)/staff/lista-manual/page.tsx b/src/app/(admins)/staff/lista-manual/page.tsx index 2dfcfe6..ec0d7eb 100644 --- a/src/app/(admins)/staff/lista-manual/page.tsx +++ b/src/app/(admins)/staff/lista-manual/page.tsx @@ -22,8 +22,8 @@ export default function Page() { render: (_, row) => row.participante.correo, }, { - key: 'fecha_inscripcion', - label: 'Fecha inscripción', + key: 'fecha_registro', + label: 'Fecha registro', render: (val) => new Date(val as string).toLocaleString(), }, { diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx index 79ab79d..060bb2c 100644 --- a/src/app/(public)/page.tsx +++ b/src/app/(public)/page.tsx @@ -26,11 +26,14 @@ export default function Page() { {loading &&
Cargando...
} {data && (
- {data.map((item, key) => ( -
- -
- ))} + {data.map((item, key) => { + const fadeClass = `delay-${(key % 5) + 1}`; + return ( +
+ +
+ ); + })}
)}
diff --git a/src/components/evento-card.tsx b/src/components/evento-card.tsx index a142ada..90b9e4b 100644 --- a/src/components/evento-card.tsx +++ b/src/components/evento-card.tsx @@ -6,8 +6,10 @@ import MarkdownRenderer from './markdown-render'; export default function EventoCard({ evento, + user = 'public', }: { evento: GetEventoWithCuestionarios; + user?: 'public' | 'administrador' | 'staff'; }) { const [verMas, setVerMas] = useState(false); const descripcion = evento.descripcion_evento; @@ -22,7 +24,15 @@ export default function EventoCard({
- + (
{cuestionario.nombre_form} diff --git a/src/containers/edit-evento.tsx b/src/containers/edit-evento.tsx new file mode 100644 index 0000000..a1a82aa --- /dev/null +++ b/src/containers/edit-evento.tsx @@ -0,0 +1,142 @@ +'use client'; +import ImageUploader from '@/components/banner-uploader'; +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 axiosInstance from '@/utils/api-config'; +import { getAxiosError } from '@/utils/errors-utils'; +import React, { useState } from 'react'; +import toast from 'react-hot-toast'; + +interface EditEventoProps { + evento: GetEventoWithCuestionario; + handleChange: (field: keyof CreateEventoType, value: string | Date) => void; + handleOnChange: (eventoActualizado: GetEventoWithCuestionario) => void; +} + +export default function EditEvento({ + evento, + handleChange, + handleOnChange, +}: EditEventoProps) { + const toDateInputValue = (isoString: string) => isoString.slice(0, 10); + + const [loading, setLoading] = useState(false); + const [nuevoBanner, setNuevoBanner] = useState(null); + + const handleOnSave = async () => { + setLoading(true); + try { + // 1. Subir nuevo banner si fue cambiado + if (nuevoBanner) { + const formData = new FormData(); + formData.append('banner', nuevoBanner); + + await axiosInstance.post( + `/evento/${evento.id_evento}/banner`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + } + ); + } + + // 2. Actualizar el evento (sin incluir 'banner') + const res = await axiosInstance.patch(`/evento/${evento.id_evento}`, { + nombre_evento: evento.nombre_evento, + descripcion_evento: evento.descripcion_evento, + tipo_evento: evento.tipo_evento, + fecha_inicio: evento.fecha_inicio, + fecha_fin: evento.fecha_fin, + }); + + toast.success('Evento actualizado correctamente'); + handleOnChange(res.data); // Actualiza en el padre + } catch (error) { + const msg = getAxiosError(error); + toast.error(`Error al actualizar el evento: ${msg.message}`); + } finally { + setLoading(false); + } + }; + + return ( +
+

Editar Información del Evento

+

Modifica los datos del evento que desees actualizar.

+ + { + setNuevoBanner(file); + }} + /> + + handleChange('nombre_evento', e.target.value)} + /> + +
+ +