feat: implement event and questionnaire creation with updated forms; add editing capabilities for questionnaires and events

This commit is contained in:
miguel
2025-06-23 18:59:59 -06:00
parent 613cc0b711
commit 544d089824
12 changed files with 265 additions and 69 deletions
@@ -53,36 +53,31 @@ export default function Page() {
const handleCrearEventoYFormulario = async () => {
try {
const createEvento = await axiosInstance.post('/evento', evento);
const createEventoWithCuestionario = await axiosInstance.post('/cuestionario/withEvento', {
evento: evento,
cuestionario: datosFormulario,
});
if (eventoBanner) {
if (createEventoWithCuestionario) {
const formData = new FormData();
formData.append('banner', eventoBanner);
const bannercreated = await axiosInstance.post(
`/evento/${createEvento.data.id_evento}/banner`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
console.log('Banner creado:', bannercreated.data);
if (eventoBanner) {
formData.append('banner', eventoBanner);
const bannercreated = await axiosInstance.post(
`/evento/${createEventoWithCuestionario.data.id_evento}/banner`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
console.log('Banner creado:', bannercreated.data);
}
}
console.log('Evento creado:', createEvento.data);
console.log('Datos del formulario:', datosFormulario);
const createFormulario = await axiosInstance.post('/cuestionario', {
id_evento: createEvento.data.id_evento,
cupo_maximo: Number(evento.cupo_maximo),
...datosFormulario,
});
if (createFormulario.data.id_formulario) {
console.log('Formulario creado:', createFormulario.data);
}
console.log('Evento y formulario creados:', createEventoWithCuestionario.data);
toast.success('Evento y formulario creados exitosamente');
router.push(`/administrador`);
} catch (error) {
const msg = getAxiosError(error);
@@ -1,13 +1,14 @@
'use client';
import SimpleInput from '@/components/input';
import Table, { Header } from '@/components/table';
import EditFormulario from '@/containers/edit-formulario';
import { useGetApi } from '@/hooks/use-get-api';
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 React, { useEffect } from 'react';
import toast from 'react-hot-toast';
type Params = {
@@ -17,6 +18,9 @@ type Params = {
export default function Page() {
const params = useParams<Params>();
const [cuestionario, setCuestionario] =
React.useState<GetCuestionario | null>(null);
const [busquedaCorreo, setBusquedaCorreo] = React.useState('');
const [loadingAsistencia, setLoadingAsistencia] = React.useState<
Record<number, boolean>
@@ -25,10 +29,14 @@ export default function Page() {
const { data: participantes, setData } = useGetApi<ParticipacionEvento[]>(
`/participante-evento/evento/${params.id_cuestionario}`
);
const { data: cuestionario } = useGetApi<GetCuestionario>(
const { data } = useGetApi<GetCuestionario>(
`/cuestionario/${params.id_cuestionario}`
);
useEffect(() => {
if (data) setCuestionario(data);
}, [data]);
const confirmarAsistencia = async (
id_participante: number,
id_evento: number
@@ -109,6 +117,23 @@ export default function Page() {
},
];
const handleChange = (field: keyof GetCuestionario, value: string | Date) => {
setCuestionario((prev) =>
prev
? {
...prev,
[field]: value,
}
: null
);
};
const handleCuestionarioActualizado = (
eventoActualizado: GetCuestionario
) => {
setCuestionario(eventoActualizado);
};
const participantesFiltrados =
participantes &&
participantes.filter((p) =>
@@ -116,11 +141,7 @@ export default function Page() {
);
return (
<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>
<div className="my-4">
<Link
href={`/administrador/evento/${params.id_evento}`}
className="btn btn-link mb-3"
@@ -128,6 +149,23 @@ export default function Page() {
<i className="bi bi-arrow-left me-2"></i>
Volver
</Link>
<h2 className="mb-4">Formulario</h2>
{cuestionario && (
<EditFormulario
cuestionario={cuestionario}
handleChange={handleChange}
handleOnChange={handleCuestionarioActualizado}
/>
)}
<h3 className="mt-5">Participantes</h3>
{participantes?.length === 0 && (
<p className="text-muted">No hay participantes registrados aún.</p>
)}
{participantes && participantes.length > 0 && (
<>
<SimpleInput
@@ -15,7 +15,6 @@ import Button from '@/components/button';
import { downloadFile } from '@/utils/downloas-utils';
import Link from 'next/link';
type Params = {
id_evento: string;
};
@@ -133,7 +132,15 @@ export default function Page() {
};
return (
<div className="container my-4">
<div className="my-4">
<Link
href={`/administrador`}
className="btn btn-link mb-3"
>
<i className="bi bi-arrow-left me-2"></i>
Volver
</Link>
<h2 className="mb-4">Evento</h2>
<EditEvento
+1 -1
View File
@@ -49,7 +49,7 @@ export default function EventoCard({
<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-success mb-2">Sin límite</span>
) : (
<span className="badge bg-primary mb-2">
{evento.cuestionarios[0]?.cupos_disponibles} cupos disponibles
+6 -18
View File
@@ -33,24 +33,12 @@ export default function CreateEvento({
<ImageUploader label="Banner del evento" onChange={handleBannerChange} />
<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>
<Input
label="Nombre del evento"
required
value={evento.nombre_evento}
onChange={(e) => handleChange('nombre_evento', e.target.value)}
/>
<div className="mb-4">
<label className="form-label">Descripción del evento</label>
+21 -7
View File
@@ -33,7 +33,7 @@ const tiposValidacion: TiposValidacion[] = [
'cuenta_alumno',
'comunidad_trabajador',
'cuenta_trabajador',
'rfc'
'rfc',
];
type CampoPreguntaSimple =
@@ -140,14 +140,28 @@ export default function FormularioEditor({ formulario, onChange }: Props) {
};
return (
<div className="border p-3 rounded my-4">
<div className="row border p-3 rounded my-4">
<h2 className="mb-3">Editar Formulario</h2>
<SimpleInput
label="Nombre del Formulario"
value={formulario.nombre_form}
onChange={(e) => actualizarCampo('nombre_form', e.target.value)}
/>
<div className="col-md-8">
<SimpleInput
label="Nombre del Formulario"
value={formulario.nombre_form}
onChange={(e) => actualizarCampo('nombre_form', e.target.value)}
/>
</div>
<div className="col-md-4">
<SimpleInput
label="Cupo máximo (Si no se requiere, dejar en blanco)"
type="number"
required
value={formulario.cupo_maximo}
onChange={(e) =>
actualizarCampo('cupo_maximo', Number(e.target.value))
}
/>
</div>
<SimpleInput
label="Descripción del Formulario"
+9 -7
View File
@@ -6,6 +6,7 @@ import Select from '@/components/select';
import { CreateEventoType } from '@/types/create-evento';
import { GetEventoWithCuestionarios } from '@/types/evento';
import axiosInstance from '@/utils/api-config';
import { formatDateLocal } from '@/utils/date-utils';
import { getAxiosError } from '@/utils/errors-utils';
import { commands } from '@uiw/react-md-editor';
import dynamic from 'next/dynamic';
@@ -24,7 +25,6 @@ export default function EditEvento({
handleChange,
handleOnChange,
}: EditEventoProps) {
const toDateInputValue = (isoString: string) => isoString.slice(0, 10);
const [loading, setLoading] = useState(false);
const [nuevoBanner, setNuevoBanner] = useState<File | null>(null);
@@ -132,20 +132,22 @@ export default function EditEvento({
<div className="col-md-6">
<Input
label="Fecha de inicio"
type="date"
type="datetime-local"
className={{ container: 'mb-3' }}
value={toDateInputValue(evento.fecha_inicio)}
onChange={(e) => handleChange('fecha_inicio', e.target.value)}
value={formatDateLocal(new Date(evento.fecha_inicio))}
onChange={(e) =>
handleChange('fecha_inicio', new Date(e.target.value))
}
/>
</div>
<div className="col-md-6">
<Input
label="Fecha de fin"
type="date"
type="datetime-local"
className={{ container: 'mb-3' }}
value={toDateInputValue(evento.fecha_fin)}
onChange={(e) => handleChange('fecha_fin', e.target.value)}
value={formatDateLocal(new Date(evento.fecha_fin))}
onChange={(e) => handleChange('fecha_fin', new Date(e.target.value))}
/>
</div>
+151
View File
@@ -0,0 +1,151 @@
import ImageUploader from '@/components/banner-uploader';
import Button from '@/components/button';
import SimpleInput from '@/components/input';
import { GetCuestionario } from '@/types/cuestionario';
import axiosInstance from '@/utils/api-config';
import { formatDateLocal } from '@/utils/date-utils';
import { getAxiosError } from '@/utils/errors-utils';
import MDEditor, { commands } from '@uiw/react-md-editor';
import React, { useState } from 'react';
import toast from 'react-hot-toast';
interface EditFormularioProps {
cuestionario: GetCuestionario;
handleChange: (field: keyof GetCuestionario, value: string | Date) => void;
handleOnChange: (eventoActualizado: GetCuestionario) => void;
}
export default function EditFormulario({
cuestionario,
handleChange,
}: EditFormularioProps) {
const [loading, setLoading] = useState(false);
const [nuevoBanner, setNuevoBanner] = useState<File | null>(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(
`/cuestionario/${cuestionario.id_cuestionario}/banner`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
}
// 2. Actualizar el evento (sin incluir 'banner')
const res = await axiosInstance.patch(
`/cuestionario/${cuestionario.id_cuestionario}`,
{
nombre_form: cuestionario.nombre_form,
descripcion: cuestionario.descripcion,
fecha_inicio: cuestionario.fecha_inicio,
fecha_fin: cuestionario.fecha_fin,
}
);
console.log('Evento actualizado:', res.data);
toast.success('Evento actualizado correctamente');
} catch (error) {
const msg = getAxiosError(error);
toast.error(`Error al actualizar el evento: ${msg.message}`);
} finally {
setLoading(false);
}
};
return (
<div className="row border p-3 rounded my-4">
<h2>Editar Información del formulario</h2>
<p>Modifica los datos del evento que desees actualizar.</p>
<ImageUploader
label="Banner del formulario"
defaultBanner={
cuestionario.banner
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${cuestionario.banner}`
: undefined
}
onChange={(file) => {
setNuevoBanner(file);
}}
/>
<div className="col-md-8">
<SimpleInput
label="Nombre del formulario"
required
value={cuestionario.nombre_form}
onChange={(e) => handleChange('nombre_form', e.target.value)}
/>
</div>
<div className="col-md-4">
<SimpleInput
label="Cupo máximo (Si no se requiere, dejar en blanco)"
type="number"
value={cuestionario.cupo_maximo ?? ''}
onChange={(e) =>
handleChange('cupo_maximo', e.target.value ? e.target.value : '')
}
/>
</div>
<div className="mb-4">
<label className="form-label">Descripción del evento</label>
<div data-color-mode="light">
<MDEditor
value={cuestionario.descripcion ?? ''}
onChange={(value) => {
const texto = value || '';
if (texto.length <= 500) {
handleChange(
'descripcion_evento' as keyof GetCuestionario,
texto
);
}
}}
height={250}
commands={[commands.bold, commands.italic, commands.hr]}
/>
</div>
</div>
<div className="col-md-6">
<SimpleInput
label="Fecha de inicio"
type="datetime-local"
className={{ container: 'mb-3' }}
value={formatDateLocal(new Date(cuestionario.fecha_inicio))}
onChange={(e) =>
handleChange('fecha_inicio', new Date(e.target.value))
}
/>
</div>
<div className="col-md-6">
<SimpleInput
label="Fecha de fin"
type="datetime-local"
className={{ container: 'mb-3' }}
value={formatDateLocal(new Date(cuestionario.fecha_fin))}
onChange={(e) => handleChange('fecha_fin', new Date(e.target.value))}
/>
</div>
<div>
<Button icon="save" disabled={loading} onClick={handleOnSave}>
{loading ? 'Guardando...' : 'Guardar Cambios'}
</Button>
</div>
</div>
);
}
-1
View File
@@ -165,5 +165,4 @@ Inicia esta nueva etapa con un recorrido especial por nuestras instalaciones, pe
/*
Recorrido de bienvenida para nuevo personal
Damos la más cordial bienvenida al personal de nuevo ingreso a nuestra Facultad. Este recorrido está diseñado para familiarizarlos con las principales áreas administrativas, académicas y de servicios. También tendrán la oportunidad de conocer al equipo de trabajo y obtener información clave para integrarse con éxito a la comunidad universitaria.
*/
-1
View File
@@ -4,5 +4,4 @@ export interface CreateEventoType {
descripcion_evento?: string; // Es opciona
fecha_inicio: Date;
fecha_fin: Date;
cupo_maximo?: number; // Es opcional
}
+1
View File
@@ -27,6 +27,7 @@ export interface FormularioCreacion {
fecha_inicio: string; // ISO 8601 date string
fecha_fin: string;
id_tipo_cuestionario: number;
cupo_maximo?: number; // Opcional, si no se requiere un cupo máximo
secciones: SeccionFormulario[];
}
+2
View File
@@ -4,8 +4,10 @@
export interface GetCuestionario {
id_cuestionario: number;
nombre_form: string;
banner?: string;
descripcion: string;
contador_secciones: number;
cupo_maximo?: number | null; // Puede ser null si no hay límite
editable: boolean;
fecha_fin: string; // Formato ISO: 'YYYY-MM-DDTHH:mm:ss'
fecha_inicio: string; // Formato ISO: 'YYYY-MM-DDTHH:mm:ss'