feat: Enhance event and form management with previews and improved UI
- Added EventoCardPreview component to display event details with existing and new banners. - Updated FormularioCardAdmin and FormularioCardUser components to handle event images and display questionnaire counts. - Improved FormularioEditor with better layout and added preview functionality for forms. - Introduced EmptyEventsState component for better user experience when no events are available. - Enhanced FormularioCardPreview to support both existing and new forms with dynamic banner handling. - Refactored code for better readability and maintainability across components.
This commit is contained in:
-192
@@ -1,192 +0,0 @@
|
||||
'use client';
|
||||
import Breadcrumb from '@/components/breadcrumb';
|
||||
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 { useParams } from 'next/navigation';
|
||||
import React, { useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useEvento } from '@/context/evento';
|
||||
|
||||
type Params = {
|
||||
id_evento: string;
|
||||
id_cuestionario: string;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const params = useParams<Params>();
|
||||
const { evento } = useEvento();
|
||||
|
||||
const [cuestionario, setCuestionario] =
|
||||
React.useState<GetCuestionario | null>(null);
|
||||
const [busquedaCorreo, setBusquedaCorreo] = React.useState('');
|
||||
const [loadingAsistencia, setLoadingAsistencia] = React.useState<
|
||||
Record<number, boolean>
|
||||
>({});
|
||||
|
||||
const { data: participantes, setData } = useGetApi<ParticipacionEvento[]>(
|
||||
`/participante-evento/evento/${params.id_cuestionario}`
|
||||
);
|
||||
const { data } = useGetApi<GetCuestionario>(
|
||||
`/cuestionario/${params.id_cuestionario}`
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) setCuestionario(data);
|
||||
}, [data]);
|
||||
|
||||
const confirmarAsistencia = async (
|
||||
id_participante: number,
|
||||
id_evento: number
|
||||
) => {
|
||||
setLoadingAsistencia((prev) => ({ ...prev, [id_participante]: true }));
|
||||
try {
|
||||
await axiosInstance.post(
|
||||
`/participante-evento/asistencia/${id_participante}/${id_evento}`
|
||||
);
|
||||
|
||||
toast.success('Asistencia confirmada');
|
||||
// Actualiza lista
|
||||
const { data } = await axiosInstance.get<ParticipacionEvento[]>(
|
||||
`/participante-evento/evento/${id_evento}`
|
||||
);
|
||||
setData(data);
|
||||
} catch (error) {
|
||||
toast.error('Error al confirmar asistencia');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingAsistencia((prev) => ({ ...prev, [id_participante]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const headers: Header<ParticipacionEvento>[] = [
|
||||
{
|
||||
key: 'id_participante',
|
||||
label: '#',
|
||||
render: (_, row) => row.participante.id_participante,
|
||||
},
|
||||
{
|
||||
key: 'participante',
|
||||
label: 'Correo',
|
||||
render: (_, row) => row.participante.correo,
|
||||
},
|
||||
{
|
||||
key: 'fecha_registro',
|
||||
label: 'Fecha registro',
|
||||
render: (_, row) =>
|
||||
row.fecha_registro
|
||||
? new Date(row.fecha_registro).toLocaleString()
|
||||
: 'Sin registro',
|
||||
},
|
||||
{
|
||||
key: 'estatus',
|
||||
label: 'Asistencia',
|
||||
render: (_, row) => {
|
||||
const isLoading = loadingAsistencia[row.id_participante];
|
||||
|
||||
return row.fecha_asistencia ? (
|
||||
<span className="text-success">
|
||||
<i className="bi bi-check-circle-fill me-2"></i>
|
||||
Asistió
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-sm btn-outline-success"
|
||||
onClick={() =>
|
||||
confirmarAsistencia(row.id_participante, row.id_cuestionario)
|
||||
}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<span
|
||||
className="spinner-border spinner-border-sm me-2"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Confirmando...
|
||||
</>
|
||||
) : (
|
||||
'Confirmar asistencia'
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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) =>
|
||||
p.participante.correo.toLowerCase().includes(busquedaCorreo.toLowerCase())
|
||||
);
|
||||
|
||||
const breadcrumbItems = [
|
||||
{ label: 'Inicio', href: '/administrador/eventos' },
|
||||
{
|
||||
label: evento?.nombre_evento || 'Evento',
|
||||
href: `/administrador/evento/${params.id_evento}`,
|
||||
},
|
||||
{ label: cuestionario?.nombre_form || 'Formulario' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="my-4">
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
|
||||
{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
|
||||
label="Buscar por correo"
|
||||
placeholder="ejemplo@correo.com"
|
||||
value={busquedaCorreo}
|
||||
onChange={(e) => setBusquedaCorreo(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="mt-4">
|
||||
<Table
|
||||
headers={headers}
|
||||
data={participantesFiltrados ?? []}
|
||||
rowKey={(row) => row.id_participante}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
'use client';
|
||||
import { plantillasDisponibles } from '@/data/plantillas';
|
||||
import { FormularioCreacion } from '@/types/create-formulario';
|
||||
import Image from 'next/image';
|
||||
import React, { useState } from 'react';
|
||||
import CreateFormulario from '@/containers/create-formulario';
|
||||
import Button from '@/components/button';
|
||||
import Breadcrumb from '@/components/breadcrumb';
|
||||
import { useEvento } from '@/context/evento';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import { getAxiosError } from '@/utils/errors-utils';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
type Params = {
|
||||
id_evento: string;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const params = useParams<Params>();
|
||||
const router = useRouter();
|
||||
const { evento, refetch } = useEvento();
|
||||
const [datosFormulario, setDatosFormulario] =
|
||||
useState<FormularioCreacion | null>(null);
|
||||
|
||||
const handleSeleccionarPlantilla = (plantillaId: string) => {
|
||||
const seleccionada = plantillasDisponibles.find(
|
||||
(p) => p.id === plantillaId
|
||||
);
|
||||
if (seleccionada) {
|
||||
setDatosFormulario(seleccionada.datos);
|
||||
console.log('Plantilla seleccionada:', seleccionada);
|
||||
}
|
||||
};
|
||||
|
||||
const breadcrumbItems = [
|
||||
{ label: 'Inicio', href: '/administrador/eventos' },
|
||||
{
|
||||
label: evento?.nombre_evento || 'Evento',
|
||||
href: `/administrador/evento/${params.id_evento}`,
|
||||
},
|
||||
{ label: 'Crear formulario' },
|
||||
];
|
||||
|
||||
const handleCrearEvento = async () => {
|
||||
if (!datosFormulario || !evento) {
|
||||
toast.error('Faltan datos para crear el formulario');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar fechas del formulario respecto al evento
|
||||
const fechaInicioFormulario = new Date(datosFormulario.fecha_inicio);
|
||||
const fechaFinFormulario = new Date(datosFormulario.fecha_fin);
|
||||
const fechaInicioEvento = new Date(evento.fecha_inicio);
|
||||
const fechaFinEvento = new Date(evento.fecha_fin);
|
||||
|
||||
// Convertir a solo fecha (sin hora) para comparación
|
||||
const fechaInicioFormularioSoloFecha = new Date(
|
||||
fechaInicioFormulario.getFullYear(),
|
||||
fechaInicioFormulario.getMonth(),
|
||||
fechaInicioFormulario.getDate()
|
||||
);
|
||||
const fechaFinFormularioSoloFecha = new Date(
|
||||
fechaFinFormulario.getFullYear(),
|
||||
fechaFinFormulario.getMonth(),
|
||||
fechaFinFormulario.getDate()
|
||||
);
|
||||
const fechaInicioEventoSoloFecha = new Date(
|
||||
fechaInicioEvento.getFullYear(),
|
||||
fechaInicioEvento.getMonth(),
|
||||
fechaInicioEvento.getDate()
|
||||
);
|
||||
const fechaFinEventoSoloFecha = new Date(
|
||||
fechaFinEvento.getFullYear(),
|
||||
fechaFinEvento.getMonth(),
|
||||
fechaFinEvento.getDate()
|
||||
);
|
||||
|
||||
console.log('Fechas del formulario:', {
|
||||
inicio: fechaInicioFormulario,
|
||||
fin: fechaFinFormulario,
|
||||
});
|
||||
console.log('Fechas del evento:', {
|
||||
inicio: fechaInicioEvento,
|
||||
fin: fechaFinEvento,
|
||||
});
|
||||
|
||||
if (fechaInicioFormularioSoloFecha < fechaInicioEventoSoloFecha) {
|
||||
toast.error(
|
||||
'La fecha de inicio del formulario debe ser posterior o igual a la fecha de inicio del evento'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fechaFinFormularioSoloFecha > fechaFinEventoSoloFecha) {
|
||||
toast.error(
|
||||
'La fecha de fin del formulario debe ser anterior o igual a la fecha de fin del evento'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fechaInicioFormulario > fechaFinFormulario) {
|
||||
toast.error(
|
||||
'La fecha de inicio del formulario debe ser anterior a la fecha de fin'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axiosInstance.post(`/cuestionario`, {
|
||||
id_evento: evento?.id_evento || params.id_evento,
|
||||
...datosFormulario,
|
||||
});
|
||||
console.log('Formulario creado:', res.data);
|
||||
refetch();
|
||||
toast.success('Formulario creado exitosamente');
|
||||
router.push(`/administrador/evento/${params.id_evento}`);
|
||||
} catch (error) {
|
||||
const msg = getAxiosError(error);
|
||||
toast.error(msg.message || 'Error al crear el formulario');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-4">
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
|
||||
<div className="p-4 border rounded bg-light mt-2">
|
||||
<h2 className="mb-2">Creación de formulario</h2>
|
||||
<p className="mb-0 text-muted">
|
||||
Esta sección te permite crear y configurar sub-eventos en forma de
|
||||
formularios asociados al evento. Selecciona una plantilla compatible,
|
||||
ajusta los campos necesarios y publícalo para habilitar el registro,
|
||||
encuestas o confirmaciones dentro de este evento.
|
||||
</p>
|
||||
</div>
|
||||
{!datosFormulario && (
|
||||
<div className="border p-3 rounded my-4">
|
||||
<h2 className="h5 fw-bold mb-2">
|
||||
Selecciona una plantilla para el formulario:
|
||||
</h2>
|
||||
<p className="text-secondary small lh-sm">
|
||||
Elige una de las plantillas disponibles para crear tu formulario de
|
||||
registro.
|
||||
<br />
|
||||
Estas plantillas están diseñadas para garantizar la correcta
|
||||
integración con el sistema, permitiendo que los datos se obtengan y
|
||||
se prellenan automáticamente de forma precisa.
|
||||
<br />
|
||||
<span className="text-danger fw-semibold">
|
||||
Para asegurar el funcionamiento óptimo del prellenado, modifica la
|
||||
estructura solo si es necesario.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div className="d-flex gap-4 flex-wrap">
|
||||
{plantillasDisponibles.map((plantilla) => (
|
||||
<div
|
||||
className="mb-3 cursor-pointer"
|
||||
key={plantilla.id}
|
||||
onClick={() => handleSeleccionarPlantilla(plantilla.id)}
|
||||
>
|
||||
<div className="text-center">
|
||||
<Image
|
||||
src={plantilla.imagen}
|
||||
width={200}
|
||||
height={200}
|
||||
alt={`Plantilla de formulario ${plantilla.nombre}`}
|
||||
className="img-fluid rounded shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{datosFormulario && (
|
||||
<CreateFormulario
|
||||
evento={evento ? evento : undefined}
|
||||
formulario={datosFormulario}
|
||||
onChange={(nuevo) => setDatosFormulario({ ...nuevo })}
|
||||
/>
|
||||
)}
|
||||
<Button className="mb-4" onClick={handleCrearEvento}>
|
||||
Crear Evento y Formulario
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
'use client';
|
||||
import { EventoProvider } from '@/context/evento/evento-context';
|
||||
import { useParams } from 'next/navigation';
|
||||
import React from 'react';
|
||||
|
||||
type Params = {
|
||||
id_evento: string;
|
||||
};
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const params = useParams<Params>();
|
||||
|
||||
if (!params.id_evento) {
|
||||
return <div>Error: ID de evento no encontrado</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<EventoProvider id_evento={params.id_evento}>{children}</EventoProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
'use client';
|
||||
import EditEvento from '@/containers/edit-evento';
|
||||
import { GetEventoWithCuestionariosWithCupos } from '@/types/evento';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import React, { useState } from 'react';
|
||||
import { CreateEventoType } from '@/types/create-evento';
|
||||
import { useEvento } from '@/context/evento';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import toast from 'react-hot-toast';
|
||||
import { downloadFile } from '@/utils/downloas-utils';
|
||||
import NewCuestionarioCard from '@/components/new-cuestionario-card';
|
||||
import Breadcrumb from '@/components/breadcrumb';
|
||||
import Button from '@/components/button';
|
||||
|
||||
type Params = {
|
||||
id_evento: string;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const params = useParams<Params>();
|
||||
const { evento, loading, error, updateEvento, setEventoData } = useEvento();
|
||||
|
||||
const [loadingStates, setLoadingStates] = useState<Record<number, boolean>>(
|
||||
{}
|
||||
);
|
||||
|
||||
const handleChange = (
|
||||
field: keyof CreateEventoType,
|
||||
value: string | Date
|
||||
) => {
|
||||
// Update the evento state directly using the context
|
||||
updateEvento({ [field]: value });
|
||||
};
|
||||
|
||||
const handleEventoActualizado = (
|
||||
eventoActualizado: GetEventoWithCuestionariosWithCupos
|
||||
) => {
|
||||
// Set the complete updated evento data
|
||||
setEventoData(eventoActualizado);
|
||||
};
|
||||
|
||||
if (loading) return <p>Cargando...</p>;
|
||||
if (error) return <p>Error: {error}</p>;
|
||||
if (!evento) return <p>No se encontró el evento</p>;
|
||||
|
||||
const handleDownload = async (id_cuestionario: number, nombre: string) => {
|
||||
setLoadingStates((prev) => ({ ...prev, [id_cuestionario]: 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 {
|
||||
setLoadingStates((prev) => ({ ...prev, [id_cuestionario]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-4">
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{ label: 'Eventos', href: '/administrador/eventos' },
|
||||
{
|
||||
label: `${evento.nombre_evento}`,
|
||||
href: `/administrador/evento/${params.id_evento}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<EditEvento
|
||||
evento={evento}
|
||||
handleChange={handleChange}
|
||||
handleOnChange={handleEventoActualizado}
|
||||
/>
|
||||
|
||||
<div className="p-4 border rounded bg-light">
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h2 className="mb-0">Formularios del evento</h2>
|
||||
<p className="mb-0 text-muted">
|
||||
Aquí puedes ver y gestionar los formularios asociados a este
|
||||
evento.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/administrador/evento/${params.id_evento}/formularios/crear`
|
||||
)
|
||||
}
|
||||
>
|
||||
Crear nuevo formulario
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{evento?.cuestionarios && evento.cuestionarios.length > 0 && (
|
||||
<div className="row mt-3">
|
||||
{evento.cuestionarios.map((item, key) => {
|
||||
const fadeClass = `delay-${(key % 5) + 1}`;
|
||||
return (
|
||||
<div
|
||||
className={`col-md-6 col-lg-4 my-4 fade-in-up-bounce ${fadeClass}`}
|
||||
key={key}
|
||||
>
|
||||
<NewCuestionarioCard
|
||||
cuestionario={item}
|
||||
user="administrador"
|
||||
onDownload={handleDownload}
|
||||
loading={loadingStates[item.id_cuestionario] || false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { CreateEventoType } from '@/types/create-evento';
|
||||
import CreateEvento from '@/containers/create-evento';
|
||||
import EventoCardPreview from '@/components/evento/evento-card-preview';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getAxiosError } from '@/utils/errors-utils';
|
||||
import Button from '@/components/button';
|
||||
|
||||
export default function Page() {
|
||||
const [eventoBanner, setEventoBanner] = useState<File | null>(null);
|
||||
const [evento, setEvento] = useState<CreateEventoType>({
|
||||
tipo_evento: '',
|
||||
nombre_evento: '',
|
||||
descripcion_evento: '',
|
||||
fecha_inicio: new Date(),
|
||||
fecha_fin: new Date(),
|
||||
});
|
||||
const router = useRouter();
|
||||
|
||||
const handleChange = (
|
||||
field: keyof CreateEventoType,
|
||||
value: string | Date
|
||||
) => {
|
||||
setEvento((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleBannerChange = (file: File | null) => {
|
||||
setEventoBanner(file);
|
||||
};
|
||||
|
||||
const handleCrearEvento = async () => {
|
||||
try {
|
||||
const createEvento = await axiosInstance.post('/evento', evento);
|
||||
|
||||
if (createEvento) {
|
||||
const formData = new FormData();
|
||||
if (eventoBanner) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
toast.success('Evento y formulario creados exitosamente');
|
||||
router.push(
|
||||
`/administrador/evento/${createEvento.data.id_evento}/formularios/crear`
|
||||
);
|
||||
} catch (error) {
|
||||
const msg = getAxiosError(error);
|
||||
toast.error(
|
||||
`Error al crear el evento o formulario: ${
|
||||
msg.message || 'Error desconocido'
|
||||
}`
|
||||
);
|
||||
console.error('Error al crear evento y formulario:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container-fluid my-4">
|
||||
{/* Header */}
|
||||
<div className="row mb-4">
|
||||
<div className="col-12">
|
||||
<div className="p-4 border rounded bg-light">
|
||||
<h2 className="mb-2">Creación del Evento</h2>
|
||||
<p className="mb-0 text-muted">
|
||||
Completa la siguiente información para describir los detalles
|
||||
generales del evento. Presiona siguiente para continuar con la
|
||||
creación del formulario.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
{/* Columna Izquierda - Formulario */}
|
||||
<div className="col-xl-7 col-lg-6">
|
||||
<div className="pe-lg-4">
|
||||
<CreateEvento
|
||||
evento={evento}
|
||||
handleChange={handleChange}
|
||||
handleBannerChange={handleBannerChange}
|
||||
/>
|
||||
|
||||
{/* Botón de guardar */}
|
||||
<div className="d-flex justify-content-end">
|
||||
<Button icon="arrow-right" onClick={handleCrearEvento}>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Columna Derecha - Preview */}
|
||||
<div className="col-xl-5 col-lg-6">
|
||||
<div className="ps-lg-4">
|
||||
<div className="sticky-top" style={{ top: '20px' }}>
|
||||
<h4 className="mb-3 d-none d-lg-block">Preview del Evento</h4>
|
||||
<div className="d-lg-none mt-4">
|
||||
<h4 className="mb-3">Preview del Evento</h4>
|
||||
</div>
|
||||
<EventoCardPreview evento={evento} eventoBanner={eventoBanner} />
|
||||
<div className="alert alert-info text-center">
|
||||
<strong>Nota:</strong> Esta preview es la que veras en tu ruta
|
||||
de eventos y en la ruta publica de eventos
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import EventoCardAdmin from '@/components/evento/evento-card-admin';
|
||||
import FormularioCardAdmin from '@/components/formulario/formulario-card-admin';
|
||||
import { useGetApi } from '@/hooks/use-get-api';
|
||||
import { GetEventoWithCuestionariosWithCupos } from '@/types/evento';
|
||||
import React from 'react';
|
||||
|
||||
export default function Page() {
|
||||
const {
|
||||
loading,
|
||||
data: activos,
|
||||
error,
|
||||
} = useGetApi<GetEventoWithCuestionariosWithCupos[]>(
|
||||
'/evento/activos/cuestionarios'
|
||||
);
|
||||
const { data: recientes } = useGetApi<GetEventoWithCuestionariosWithCupos[]>(
|
||||
'/evento/recientes/cuestionarios'
|
||||
);
|
||||
|
||||
if (loading) return <div>Cargando formularios...</div>;
|
||||
if (error) return <div className="alert alert-danger">{error.message}</div>;
|
||||
|
||||
return (
|
||||
<div className="my-4">
|
||||
{loading && <div>Cargando...</div>}
|
||||
{activos && activos.length > 0 && (
|
||||
<div className="row">
|
||||
<h1>Eventos Activos</h1>
|
||||
{activos.map((evento, key) => {
|
||||
const fadeClass = `delay-${(key % 5) + 1}`;
|
||||
return (
|
||||
<div
|
||||
className={`col-md-6 col-lg-4 my-4 fade-in-up-bounce ${fadeClass}`}
|
||||
key={key}
|
||||
>
|
||||
<EventoCardAdmin evento={evento} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{recientes && recientes.length > 0 && (
|
||||
<div className="row">
|
||||
<h2>Formularios Recientes</h2>
|
||||
<p className="text-muted">Eventos de los ultimos 30 días.</p>
|
||||
{recientes.map((evento) =>
|
||||
evento.cuestionarios.map((cuestionario, cuestionarioIndex) => {
|
||||
const fadeClass = `delay-${(cuestionarioIndex % 5) + 1}`;
|
||||
return (
|
||||
<div
|
||||
className={`col-md-6 col-lg-4 my-4 fade-in-up-bounce ${fadeClass}`}
|
||||
key={`${evento.id_evento}-${cuestionario.id_cuestionario}`}
|
||||
>
|
||||
<FormularioCardAdmin
|
||||
cuestionario={cuestionario}
|
||||
evento={evento}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import Footer from '@/components/layout/footer';
|
||||
import Header from '@/components/layout/header';
|
||||
import Navbar from '@/components/navbar';
|
||||
import React from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="d-flex flex-column min-vh-100">
|
||||
<Header small />
|
||||
<Navbar />
|
||||
<main className="container flex-grow-1">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
'use client';
|
||||
import SimpleInput from '@/components/input';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export default function Page() {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [response, setResponse] = useState<{
|
||||
insertados: number;
|
||||
omitidos: number;
|
||||
} | null>(null);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
setSelectedFile(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!selectedFile) return;
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedFile);
|
||||
|
||||
try {
|
||||
const res = await axiosInstance.post('/trabajadores/cargar', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
|
||||
setResponse(res.data);
|
||||
} catch (error) {
|
||||
console.error('Error al subir el archivo:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Subir Excel de Profesores</h2>
|
||||
<SimpleInput type="file" onChange={handleFileChange} />
|
||||
<button onClick={handleSubmit} disabled={!selectedFile}>
|
||||
Enviar
|
||||
</button>
|
||||
|
||||
{response && (
|
||||
<div>
|
||||
<h3>Resultado de la carga:</h3>
|
||||
<p>Insertados: {response.insertados}</p>
|
||||
<p>Omitidos: {response.omitidos}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import Footer from '@/components/layout/footer';
|
||||
import Header from '@/components/layout/header';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className='d-flex flex-column min-vh-100'>
|
||||
<Header />
|
||||
<main className='container flex-grow-1'>
|
||||
<nav className='d-flex gap-2 my-4'>
|
||||
<Link href={'/staff/'} className='text-decoration-none'>
|
||||
<div className='box'>Escaner</div>
|
||||
</Link>
|
||||
<Link
|
||||
href={'/staff/lista-manual'}
|
||||
className='text-decoration-none'
|
||||
>
|
||||
<div className='box'>Lista manual</div>
|
||||
</Link>
|
||||
</nav>
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
'use client';
|
||||
import Button from '@/components/button';
|
||||
import Input from '@/components/input';
|
||||
import Select from '@/components/select';
|
||||
import Table, { Header } from '@/components/table';
|
||||
import {
|
||||
GetCuestionario,
|
||||
GetCuestionarioWithEvento,
|
||||
} from '@/types/cuestionario';
|
||||
import { GetEvento } from '@/types/evento';
|
||||
import { ParticipacionEvento } from '@/types/participante-evento';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import React, { useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function Page() {
|
||||
const headers: Header<ParticipacionEvento>[] = [
|
||||
{
|
||||
key: 'id_participante',
|
||||
label: '#',
|
||||
render: (_, row) => row.participante.id_participante,
|
||||
},
|
||||
{
|
||||
key: 'participante',
|
||||
label: 'Correo',
|
||||
render: (_, row) => row.participante.correo,
|
||||
},
|
||||
{
|
||||
key: 'fecha_registro',
|
||||
label: 'Fecha registro',
|
||||
render: (val) => new Date(val as string).toLocaleString(),
|
||||
},
|
||||
{
|
||||
key: 'estatus',
|
||||
label: 'Asistencia',
|
||||
render: (_, row) => {
|
||||
const isLoading = loadingAsistencia[row.id_participante];
|
||||
|
||||
return row.fecha_asistencia ? (
|
||||
<span className="text-success">
|
||||
<i className="bi bi-check-circle-fill me-2"></i>
|
||||
Asistió
|
||||
</span>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
outline
|
||||
onClick={() =>
|
||||
confirmarAsistencia(row.id_participante, row.id_cuestionario)
|
||||
}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<span
|
||||
className="spinner-border spinner-border-sm me-2"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Confirmando...
|
||||
</>
|
||||
) : (
|
||||
'Confirmar asistencia'
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const [eventos, setEventos] = React.useState<GetCuestionarioWithEvento[]>([]);
|
||||
const [participantes, setParticipantes] = React.useState<
|
||||
ParticipacionEvento[]
|
||||
>([]);
|
||||
const [loadingAsistencia, setLoadingAsistencia] = React.useState<
|
||||
Record<number, boolean>
|
||||
>({});
|
||||
const [busquedaCorreo, setBusquedaCorreo] = React.useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const getEventos = async () => {
|
||||
try {
|
||||
const { data } = await axiosInstance.get<GetCuestionario[]>(
|
||||
`/cuestionario`
|
||||
);
|
||||
if (!data) throw new Error('No se encontraron eventos');
|
||||
|
||||
const cuestionariosConEvento = await Promise.all(
|
||||
data.map(async (cuestionario) => {
|
||||
const { data: evento } = await axiosInstance.get<GetEvento>(
|
||||
`/evento/${cuestionario.id_evento}`
|
||||
);
|
||||
return { ...cuestionario, evento };
|
||||
})
|
||||
);
|
||||
|
||||
setEventos(cuestionariosConEvento);
|
||||
} catch (error) {
|
||||
console.error('Error en getEventos:', error);
|
||||
}
|
||||
};
|
||||
|
||||
getEventos();
|
||||
}, []);
|
||||
|
||||
const handleOnSelect = async (idSeleccionado: number) => {
|
||||
try {
|
||||
const { data } = await axiosInstance.get<ParticipacionEvento[]>(
|
||||
`/participante-evento/evento/${idSeleccionado}`
|
||||
);
|
||||
setParticipantes(data);
|
||||
} catch (error) {
|
||||
console.error('Error al obtener participante-evento:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmarAsistencia = async (
|
||||
id_participante: number,
|
||||
id_cuestionario: number
|
||||
) => {
|
||||
setLoadingAsistencia((prev) => ({ ...prev, [id_participante]: true }));
|
||||
try {
|
||||
await axiosInstance.post(
|
||||
`/participante-evento/asistencia/${id_participante}/${id_cuestionario}`
|
||||
);
|
||||
|
||||
toast.success('Asistencia confirmada');
|
||||
// Actualiza lista
|
||||
await handleOnSelect(id_cuestionario);
|
||||
} catch (error) {
|
||||
toast.error('Error al confirmar asistencia');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingAsistencia((prev) => ({ ...prev, [id_participante]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const participantesFiltrados = participantes.filter((p) =>
|
||||
p.participante.correo.toLowerCase().includes(busquedaCorreo.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
label="Selecciona un evento"
|
||||
options={eventos.map((evento) => ({
|
||||
value: evento.id_cuestionario,
|
||||
label: `${evento.evento.nombre_evento} - ${evento.nombre_form}`,
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
const id = Number(e?.target?.value ?? e);
|
||||
if (!isNaN(id)) handleOnSelect(id);
|
||||
}}
|
||||
placeholder="Selecciona un evento"
|
||||
/>
|
||||
{participantes.length > 0 && (
|
||||
<>
|
||||
<Input
|
||||
label="Buscar por correo"
|
||||
placeholder="ejemplo@correo.com"
|
||||
value={busquedaCorreo}
|
||||
onChange={(e) => setBusquedaCorreo(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="mt-4">
|
||||
<Table
|
||||
headers={headers}
|
||||
data={participantesFiltrados}
|
||||
rowKey={(row) => row.id_participante}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{participantes.length === 0 && (
|
||||
<div className="alert alert-warning mt-4">
|
||||
No hay participantes registrados para este evento.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Scanner } from '@yudiel/react-qr-scanner';
|
||||
import Button from '@/components/button';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const Modal = dynamic(() => import('@/components/modal'), { ssr: false });
|
||||
|
||||
export default function Page() {
|
||||
const [scannedData, setScannedData] = useState<{
|
||||
id_participante: number;
|
||||
id_cuestionario: number;
|
||||
} | null>(null);
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [enableScan, setEnableScan] = useState(true);
|
||||
const [statusMessage, setStatusMessage] = useState('');
|
||||
|
||||
const [participante, setParticipante] = useState('');
|
||||
|
||||
const handleScan = async (rawValue: string) => {
|
||||
console.log('QR Escaneado:', rawValue);
|
||||
if (!enableScan) return;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(rawValue);
|
||||
if (data.id_participante && data.id_cuestionario) {
|
||||
try {
|
||||
const response = await axiosInstance.get(
|
||||
`/participante-evento/${data.id_participante}/${data.id_cuestionario}`
|
||||
);
|
||||
|
||||
setParticipante(response.data.participante.correo);
|
||||
setScannedData({
|
||||
id_participante: data.id_participante,
|
||||
id_cuestionario: data.id_cuestionario,
|
||||
});
|
||||
setShowModal(true);
|
||||
setEnableScan(false);
|
||||
} catch (err) {
|
||||
console.warn('Participante no registrado en el cuestionario:', err);
|
||||
setStatusMessage(
|
||||
'❌ El participante no está registrado en este evento.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setStatusMessage('⚠️ El QR no contiene los campos requeridos');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('QR malformado:', err);
|
||||
setStatusMessage('❌ Error al leer el QR: formato inválido');
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!scannedData) return;
|
||||
|
||||
try {
|
||||
const response = await axiosInstance.post(
|
||||
`/participante-evento/asistencia/${scannedData.id_participante}/${scannedData.id_cuestionario}`
|
||||
);
|
||||
|
||||
console.log('Asistencia registrada:', response.data);
|
||||
toast.success('✅ Asistencia registrada correctamente');
|
||||
} catch (err) {
|
||||
console.error('Error en la petición:', err);
|
||||
setStatusMessage('❌ Error de red al registrar asistencia');
|
||||
}
|
||||
|
||||
setShowModal(false);
|
||||
setEnableScan(true);
|
||||
setScannedData(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="container flex-grow-1 d-flex flex-column align-items-center justify-content-center">
|
||||
<h1 className="mb-4">Lectura de QRs</h1>
|
||||
|
||||
{enableScan ? (
|
||||
<>
|
||||
<div style={{ width: '100%', maxWidth: '400px' }}>
|
||||
<Scanner
|
||||
onScan={(codes) => {
|
||||
if (codes.length > 0) {
|
||||
handleScan(codes[0].rawValue);
|
||||
}
|
||||
}}
|
||||
onError={(err) => console.error('QR Error:', err)}
|
||||
constraints={{ facingMode: 'environment' }}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setEnableScan(false)}
|
||||
variant="outline-danger"
|
||||
className="mt-3"
|
||||
>
|
||||
Detener cámara
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-muted mb-3">La cámara está detenida.</p>
|
||||
<Button onClick={() => setEnableScan(true)} variant="outline-primary">
|
||||
Activar cámara
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{statusMessage && <p className="mt-3">{statusMessage}</p>}
|
||||
|
||||
<Modal
|
||||
isVisible={showModal}
|
||||
size="md"
|
||||
onClose={() => {
|
||||
setShowModal(false);
|
||||
setEnableScan(true);
|
||||
setScannedData(null);
|
||||
}}
|
||||
closeButton
|
||||
className={{
|
||||
content: 'p-3',
|
||||
body: 'text-center',
|
||||
}}
|
||||
>
|
||||
<h5 className="mb-3">Datos escaneados</h5>
|
||||
{scannedData ? (
|
||||
<>
|
||||
<p>
|
||||
<strong>Participante:</strong> {participante}
|
||||
</p>
|
||||
<Button variant="success" onClick={handleConfirm}>
|
||||
Confirmar lectura
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<p>QR inválido</p>
|
||||
)}
|
||||
</Modal>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -143,10 +143,10 @@ export default function Page() {
|
||||
);
|
||||
|
||||
const breadcrumbItems = [
|
||||
{ label: 'Inicio', href: '/administrador/eventos' },
|
||||
{ label: 'Inicio', href: '/user/eventos' },
|
||||
{
|
||||
label: evento?.nombre_evento || 'Evento',
|
||||
href: `/administrador/evento/${params.id_evento}`,
|
||||
href: `/user/evento/${params.id_evento}`,
|
||||
},
|
||||
{ label: cuestionario?.nombre_form || 'Formulario' },
|
||||
];
|
||||
@@ -158,6 +158,7 @@ export default function Page() {
|
||||
{cuestionario && (
|
||||
<EditFormulario
|
||||
cuestionario={cuestionario}
|
||||
evento={evento}
|
||||
handleChange={handleChange}
|
||||
handleOnChange={handleCuestionarioActualizado}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FormularioCreacion } from '@/types/create-formulario';
|
||||
import Image from 'next/image';
|
||||
import React, { useState } from 'react';
|
||||
import CreateFormulario from '@/containers/create-formulario';
|
||||
import FormularioCardPreview from '@/components/formulario/formulario-card-preview';
|
||||
import Button from '@/components/button';
|
||||
import Breadcrumb from '@/components/breadcrumb';
|
||||
import { useEvento } from '@/context/evento';
|
||||
@@ -27,17 +28,46 @@ export default function Page() {
|
||||
const seleccionada = plantillasDisponibles.find(
|
||||
(p) => p.id === plantillaId
|
||||
);
|
||||
if (seleccionada) {
|
||||
setDatosFormulario(seleccionada.datos);
|
||||
if (seleccionada && evento) {
|
||||
// Crear una copia de los datos de la plantilla
|
||||
const datosPlantilla = { ...seleccionada.datos };
|
||||
|
||||
// Ajustar las fechas para que estén dentro del rango del evento
|
||||
const fechaInicioEvento = new Date(evento.fecha_inicio);
|
||||
const fechaFinEvento = new Date(evento.fecha_fin);
|
||||
|
||||
// Formatear las fechas para inputs datetime-local (YYYY-MM-DDTHH:MM)
|
||||
const formatoFechaInput = (fecha: Date) => {
|
||||
return fecha.toISOString().slice(0, 16);
|
||||
};
|
||||
|
||||
// Establecer fecha de inicio del formulario igual a la del evento
|
||||
datosPlantilla.fecha_inicio = formatoFechaInput(fechaInicioEvento);
|
||||
|
||||
// Establecer fecha de fin del formulario igual a la del evento
|
||||
datosPlantilla.fecha_fin = formatoFechaInput(fechaFinEvento);
|
||||
|
||||
setDatosFormulario(datosPlantilla);
|
||||
console.log('Plantilla seleccionada:', seleccionada);
|
||||
console.log('Fechas ajustadas al evento:', {
|
||||
inicio: datosPlantilla.fecha_inicio,
|
||||
fin: datosPlantilla.fecha_fin,
|
||||
});
|
||||
} else if (seleccionada) {
|
||||
// Si no hay evento disponible, usar las fechas originales de la plantilla
|
||||
setDatosFormulario(seleccionada.datos);
|
||||
console.log(
|
||||
'Plantilla seleccionada (sin ajuste de fechas):',
|
||||
seleccionada
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const breadcrumbItems = [
|
||||
{ label: 'Inicio', href: '/administrador/eventos' },
|
||||
{ label: 'Inicio', href: '/user/eventos' },
|
||||
{
|
||||
label: evento?.nombre_evento || 'Evento',
|
||||
href: `/administrador/evento/${params.id_evento}`,
|
||||
href: `/user/evento/${params.id_evento}`,
|
||||
},
|
||||
{ label: 'Crear formulario' },
|
||||
];
|
||||
@@ -114,7 +144,7 @@ export default function Page() {
|
||||
console.log('Formulario creado:', res.data);
|
||||
refetch();
|
||||
toast.success('Formulario creado exitosamente');
|
||||
router.push(`/administrador/evento/${params.id_evento}`);
|
||||
router.push(`/user/evento/${params.id_evento}`);
|
||||
} catch (error) {
|
||||
const msg = getAxiosError(error);
|
||||
toast.error(msg.message || 'Error al crear el formulario');
|
||||
@@ -122,18 +152,25 @@ export default function Page() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-4">
|
||||
<div className="container-fluid my-4">
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
|
||||
<div className="p-4 border rounded bg-light mt-2">
|
||||
<h2 className="mb-2">Creación de formulario</h2>
|
||||
<p className="mb-0 text-muted">
|
||||
Esta sección te permite crear y configurar sub-eventos en forma de
|
||||
formularios asociados al evento. Selecciona una plantilla compatible,
|
||||
ajusta los campos necesarios y publícalo para habilitar el registro,
|
||||
encuestas o confirmaciones dentro de este evento.
|
||||
</p>
|
||||
{/* Header */}
|
||||
<div className="row mb-4">
|
||||
<div className="col-12">
|
||||
<div className="p-4 border rounded bg-light">
|
||||
<h2 className="mb-2">Creación de formulario</h2>
|
||||
<p className="mb-0 text-muted">
|
||||
Esta sección te permite crear y configurar sub-eventos en forma de
|
||||
formularios asociados al evento. Selecciona una plantilla
|
||||
compatible, ajusta los campos necesarios y publícalo para
|
||||
habilitar el registro, encuestas o confirmaciones dentro de este
|
||||
evento.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!datosFormulario && (
|
||||
<div className="border p-3 rounded my-4">
|
||||
<h2 className="h5 fw-bold mb-2">
|
||||
@@ -174,16 +211,80 @@ export default function Page() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{datosFormulario && (
|
||||
<CreateFormulario
|
||||
evento={evento ? evento : undefined}
|
||||
formulario={datosFormulario}
|
||||
onChange={(nuevo) => setDatosFormulario({ ...nuevo })}
|
||||
/>
|
||||
<>
|
||||
<div className="row">
|
||||
{/* Columna Izquierda - Formulario */}
|
||||
<div className="col-xl-7 col-lg-6">
|
||||
<div className="pe-lg-4">
|
||||
{(() => {
|
||||
const formularioEditor = CreateFormulario({
|
||||
formulario: datosFormulario,
|
||||
onChange: (nuevo) => setDatosFormulario({ ...nuevo }),
|
||||
evento: evento ? evento : undefined,
|
||||
});
|
||||
return formularioEditor.informacionBasica();
|
||||
})()}
|
||||
|
||||
{/* Botón de crear */}
|
||||
<div className="d-flex justify-content-end mt-4">
|
||||
<Button icon="save" onClick={handleCrearEvento}>
|
||||
Crear Formulario
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Columna Derecha - Preview */}
|
||||
<div className="col-xl-5 col-lg-6">
|
||||
<div className="ps-lg-4">
|
||||
<div className="sticky-top" style={{ top: '20px' }}>
|
||||
<h4 className="mb-3 d-none d-lg-block">
|
||||
Preview del Formulario
|
||||
</h4>
|
||||
<div className="d-lg-none mt-4">
|
||||
<h4 className="mb-3">Preview del Formulario</h4>
|
||||
</div>
|
||||
<FormularioCardPreview
|
||||
formulario={datosFormulario}
|
||||
evento={evento || undefined}
|
||||
/>
|
||||
|
||||
{/* Alert de imagen del evento */}
|
||||
{evento?.banner && (
|
||||
<div className="alert alert-info mt-3">
|
||||
<i className="bi bi-info-circle-fill me-2"></i>
|
||||
<strong>Nota:</strong> Al no proporcionar una imagen para
|
||||
este formulario, se usará automáticamente la imagen del
|
||||
evento.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="alert alert-warning text-center mt-3">
|
||||
<strong>Vista previa:</strong> Así se verá tu formulario en
|
||||
las listas de formularios.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Secciones y preguntas - Ancho completo */}
|
||||
<div className="row mt-5">
|
||||
<div className="col-12">
|
||||
{(() => {
|
||||
const formularioEditor = CreateFormulario({
|
||||
formulario: datosFormulario,
|
||||
onChange: (nuevo) => setDatosFormulario({ ...nuevo }),
|
||||
evento: evento ? evento : undefined,
|
||||
});
|
||||
return formularioEditor.seccionesYPreguntas();
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Button className="mb-4" onClick={handleCrearEvento}>
|
||||
Crear Evento y Formulario
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import { useEvento } from '@/context/evento';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import toast from 'react-hot-toast';
|
||||
import { downloadFile } from '@/utils/downloas-utils';
|
||||
import NewCuestionarioCard from '@/components/new-cuestionario-card';
|
||||
import Breadcrumb from '@/components/breadcrumb';
|
||||
import Button from '@/components/button';
|
||||
import FormularioCardAdmin from '@/components/formulario/formulario-card-admin';
|
||||
|
||||
type Params = {
|
||||
id_evento: string;
|
||||
@@ -44,7 +44,11 @@ export default function Page() {
|
||||
if (error) return <p>Error: {error}</p>;
|
||||
if (!evento) return <p>No se encontró el evento</p>;
|
||||
|
||||
const handleDownload = async (id_cuestionario: number, nombre: string) => {
|
||||
const handleDownload = async (
|
||||
id_cuestionario: number,
|
||||
nombre_formulario: string,
|
||||
nombre_evento: string
|
||||
) => {
|
||||
setLoadingStates((prev) => ({ ...prev, [id_cuestionario]: true }));
|
||||
|
||||
try {
|
||||
@@ -55,7 +59,7 @@ export default function Page() {
|
||||
}
|
||||
);
|
||||
|
||||
downloadFile(res.data, `${nombre}`, 'csv');
|
||||
downloadFile(res.data, `${nombre_evento} - ${nombre_formulario}`, 'csv');
|
||||
} catch (error) {
|
||||
console.error('Error al descargar el archivo:', error);
|
||||
toast.error('Error al descargar el archivo');
|
||||
@@ -68,10 +72,10 @@ export default function Page() {
|
||||
<div className="my-4">
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{ label: 'Eventos', href: '/administrador/eventos' },
|
||||
{ label: 'Eventos', href: '/user/eventos' },
|
||||
{
|
||||
label: `${evento.nombre_evento}`,
|
||||
href: `/administrador/evento/${params.id_evento}`,
|
||||
href: `/user/evento/${params.id_evento}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -95,9 +99,7 @@ export default function Page() {
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/administrador/evento/${params.id_evento}/formularios/crear`
|
||||
)
|
||||
router.push(`/user/evento/${params.id_evento}/formularios/crear`)
|
||||
}
|
||||
>
|
||||
Crear nuevo formulario
|
||||
@@ -114,11 +116,11 @@ export default function Page() {
|
||||
className={`col-md-6 col-lg-4 my-4 fade-in-up-bounce ${fadeClass}`}
|
||||
key={key}
|
||||
>
|
||||
<NewCuestionarioCard
|
||||
<FormularioCardAdmin
|
||||
cuestionario={item}
|
||||
user="administrador"
|
||||
onDownload={handleDownload}
|
||||
loading={loadingStates[item.id_cuestionario] || false}
|
||||
evento={evento}
|
||||
handleDownload={handleDownload}
|
||||
disabled={loadingStates[item.id_cuestionario] || false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function Page() {
|
||||
|
||||
toast.success('Evento y formulario creados exitosamente');
|
||||
router.push(
|
||||
`/administrador/evento/${createEvento.data.id_evento}/formularios/crear`
|
||||
`/user/evento/${createEvento.data.id_evento}/formularios/crear`
|
||||
);
|
||||
} catch (error) {
|
||||
const msg = getAxiosError(error);
|
||||
|
||||
@@ -4,9 +4,15 @@ import EventoCardAdmin from '@/components/evento/evento-card-admin';
|
||||
import FormularioCardAdmin from '@/components/formulario/formulario-card-admin';
|
||||
import { useGetApi } from '@/hooks/use-get-api';
|
||||
import { GetEventoWithCuestionariosWithCupos } from '@/types/evento';
|
||||
import React from 'react';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import { downloadFile } from '@/utils/downloas-utils';
|
||||
import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function Page() {
|
||||
const [loadingStates, setLoadingStates] = useState<Record<number, boolean>>(
|
||||
{}
|
||||
);
|
||||
const {
|
||||
loading,
|
||||
data: activos,
|
||||
@@ -21,6 +27,30 @@ export default function Page() {
|
||||
if (loading) return <div>Cargando formularios...</div>;
|
||||
if (error) return <div className="alert alert-danger">{error.message}</div>;
|
||||
|
||||
const handleDownload = async (
|
||||
id_cuestionario: number,
|
||||
nombre_formulario: string,
|
||||
nombre_evento: string
|
||||
) => {
|
||||
setLoadingStates((prev) => ({ ...prev, [id_cuestionario]: true }));
|
||||
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/cuestionario-respondido/reporte-respuestas/${id_cuestionario}`,
|
||||
{
|
||||
responseType: 'blob',
|
||||
}
|
||||
);
|
||||
|
||||
downloadFile(res.data, `${nombre_evento} - ${nombre_formulario}`, 'csv');
|
||||
} catch (error) {
|
||||
console.error('Error al descargar el archivo:', error);
|
||||
toast.error('Error al descargar el archivo');
|
||||
} finally {
|
||||
setLoadingStates((prev) => ({ ...prev, [id_cuestionario]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-4">
|
||||
{loading && <div>Cargando...</div>}
|
||||
@@ -55,6 +85,10 @@ export default function Page() {
|
||||
<FormularioCardAdmin
|
||||
cuestionario={cuestionario}
|
||||
evento={evento}
|
||||
handleDownload={handleDownload}
|
||||
disabled={
|
||||
loadingStates[cuestionario.id_cuestionario] || false
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+48
-19
@@ -2,6 +2,7 @@
|
||||
|
||||
import ClientCarousel from '@/client-components/client-carousel';
|
||||
import FormularioCardUser from '@/components/formulario/formulario-card-user';
|
||||
import EmptyEventsState from '@/components/empty-events-state';
|
||||
import { useGetApi } from '@/hooks/use-get-api';
|
||||
import { GetEventoWithCuestionariosWithCupos } from '@/types/evento';
|
||||
import React from 'react';
|
||||
@@ -13,25 +14,53 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mx-auto mb-5 mt-3 fade-in-down-bounce">
|
||||
<ClientCarousel
|
||||
images={
|
||||
data
|
||||
? data.map((evento) => ({
|
||||
src: `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`,
|
||||
alt: `Banner de ${evento.nombre_evento}`,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
src: '/banner.jpeg',
|
||||
alt: 'Banner de eventos activos',
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{loading && <div>Cargando...</div>}
|
||||
{data && (
|
||||
{data && data.length > 0 && (
|
||||
<div className="mx-auto mb-5 mt-3 fade-in-down-bounce">
|
||||
<ClientCarousel
|
||||
images={
|
||||
data && data.length > 0
|
||||
? data.map((evento) => ({
|
||||
src: `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`,
|
||||
alt: `Banner de ${evento.nombre_evento}`,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
src: '/default-banner.jpeg',
|
||||
alt: 'Banner de eventos activos',
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className="text-center py-5 fade-in-scale">
|
||||
<div className="d-flex justify-content-center align-items-center mb-3">
|
||||
<div
|
||||
className="spinner-border text-primary me-3"
|
||||
role="status"
|
||||
style={{ width: '2rem', height: '2rem' }}
|
||||
>
|
||||
<span className="visually-hidden">Cargando...</span>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="mb-0 text-primary">Cargando eventos...</h5>
|
||||
<small className="text-muted">
|
||||
Esto solo tomará unos segundos
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && data && data.length === 0 && (
|
||||
<div className="flex-grow-1 d-flex flex-column justify-content-center align-items-center">
|
||||
<EmptyEventsState />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && data && data.length > 0 && (
|
||||
<div className="row">
|
||||
{data.flatMap((evento) =>
|
||||
evento.cuestionarios.map((cuestionario, index) => {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Button from './button';
|
||||
|
||||
export default function EmptyEventsState() {
|
||||
return (
|
||||
<div className="container py-5">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-lg-8">
|
||||
<div className="text-center fade-in-scale">
|
||||
{/* Ilustración principal con Bootstrap */}
|
||||
<div className="mb-2">
|
||||
<div
|
||||
className="d-inline-flex align-items-center justify-content-center bg-dorado bg-opacity-10 rounded-circle mb-4"
|
||||
style={{ width: '150px', height: '150px' }}
|
||||
>
|
||||
<i className="bi bi-calendar-x text-dorado display-1"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Título y mensaje principal */}
|
||||
<div className="mb-5">
|
||||
<h1 className="display-5 fw-bold text-azul mb-3 fade-in-up delay-1">
|
||||
No hay eventos disponibles
|
||||
</h1>
|
||||
<p className="lead text-muted mb-0 fade-in-up delay-2">
|
||||
En este momento no tenemos eventos activos para mostrar.
|
||||
<br className="d-none d-md-block" />
|
||||
<span className="text-azul fw-semibold">
|
||||
¡No te preocupes!
|
||||
</span>{' '}
|
||||
Pronto habrá nuevas oportunidades de participación.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Botón de acción usando clases Bootstrap */}
|
||||
<div className="fade-in-up delay-4">
|
||||
<Button
|
||||
icon="bi bi-arrow-clockwise"
|
||||
onClick={() => window.location.reload()}
|
||||
size="lg"
|
||||
className="rounded-pill px-5"
|
||||
>
|
||||
Actualizar pagina
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Mensaje adicional */}
|
||||
<div className="mt-4 fade-in-up delay-5">
|
||||
<small className="text-muted">
|
||||
<i className="bi bi-info-circle me-1"></i>
|
||||
Esta página se actualiza automáticamente para mostrar los
|
||||
eventos más recientes
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export default function EventoCardAdmin({
|
||||
style={{ borderRadius: '12px', overflow: 'hidden' }}
|
||||
>
|
||||
<div className="position-relative">
|
||||
<Link href={`/administrador/evento/${evento.id_evento}`}>
|
||||
<Link href={`/user/evento/${evento.id_evento}`}>
|
||||
<Image
|
||||
width={400}
|
||||
height={250}
|
||||
@@ -110,7 +110,7 @@ export default function EventoCardAdmin({
|
||||
<div className="py-2 px-3 bg-light border-0">
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<Link
|
||||
href={`/administrador/evento/${evento.id_evento}/formulario/${cuestionario.id_cuestionario}`}
|
||||
href={`/user/evento/${evento.id_evento}/formulario/${cuestionario.id_cuestionario}`}
|
||||
className="text-decoration-none fw-semibold"
|
||||
>
|
||||
{cuestionario.nombre_form}
|
||||
@@ -131,7 +131,7 @@ export default function EventoCardAdmin({
|
||||
<i className="bi bi-clipboard-x fs-4 text-muted mb-2 d-block"></i>
|
||||
<p className="text-muted mb-2 small">Sin formularios disponibles</p>
|
||||
<Link
|
||||
href={`/administrador/evento/${evento.id_evento}/formularios/crear`}
|
||||
href={`/user/evento/${evento.id_evento}/formularios/crear`}
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
>
|
||||
<i className="bi bi-plus-circle me-1"></i>
|
||||
@@ -142,7 +142,7 @@ export default function EventoCardAdmin({
|
||||
|
||||
{/* Botón principal */}
|
||||
<Link
|
||||
href={`/administrador/evento/${evento.id_evento}`}
|
||||
href={`/user/evento/${evento.id_evento}`}
|
||||
className="btn btn-primary w-100 rounded-pill"
|
||||
>
|
||||
Ver/Editar evento
|
||||
|
||||
@@ -10,11 +10,15 @@ import Button from '../button';
|
||||
interface EventoPreviewProps {
|
||||
evento: CreateEventoType;
|
||||
eventoBanner?: File | null;
|
||||
existingBanner?: string | null; // Banner existente desde la API
|
||||
cuestionariosCount?: number; // Número de formularios existentes
|
||||
}
|
||||
|
||||
export default function EventoCardPreview({
|
||||
evento,
|
||||
eventoBanner,
|
||||
existingBanner,
|
||||
cuestionariosCount = 0,
|
||||
}: EventoPreviewProps) {
|
||||
const [verMas, setVerMas] = useState(false);
|
||||
const [bannerUrl, setBannerUrl] = useState<string>('/default-banner.png');
|
||||
@@ -27,18 +31,25 @@ export default function EventoCardPreview({
|
||||
? `${descripcion.slice(0, limite)}...`
|
||||
: descripcion;
|
||||
|
||||
// Actualizar URL del banner cuando cambie el archivo
|
||||
// Actualizar URL del banner cuando cambie el archivo o cuando haya un banner existente
|
||||
useEffect(() => {
|
||||
if (eventoBanner) {
|
||||
// Si hay un nuevo banner seleccionado, usarlo
|
||||
const url = URL.createObjectURL(eventoBanner);
|
||||
setBannerUrl(url);
|
||||
|
||||
// Limpiar URL anterior cuando el componente se desmonte
|
||||
return () => URL.revokeObjectURL(url);
|
||||
} else if (existingBanner) {
|
||||
// Si no hay nuevo banner pero sí hay uno existente desde la API, usarlo
|
||||
setBannerUrl(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/banners/${existingBanner}`
|
||||
);
|
||||
} else {
|
||||
// Si no hay ningún banner, usar el banner por defecto
|
||||
setBannerUrl('/default-banner.png');
|
||||
}
|
||||
}, [eventoBanner]);
|
||||
}, [eventoBanner, existingBanner]);
|
||||
|
||||
// Obtener información de fecha usando la utilidad
|
||||
const getDateInfo = () => {
|
||||
@@ -66,9 +77,16 @@ export default function EventoCardPreview({
|
||||
style={{ objectFit: 'cover', height: '200px' }}
|
||||
/>
|
||||
|
||||
{/* Badge indicando que no hay formularios aún */}
|
||||
{/* Badge de cantidad de formularios en la esquina superior derecha */}
|
||||
<div className="position-absolute top-0 end-0 m-2">
|
||||
<span className="badge bg-secondary">Sin formularios</span>
|
||||
{cuestionariosCount > 0 ? (
|
||||
<span className="badge bg-primary">
|
||||
{cuestionariosCount} formulario
|
||||
{cuestionariosCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
) : (
|
||||
<span className="badge bg-secondary">Sin formularios</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,9 +9,17 @@ import Button from '../button';
|
||||
export default function FormularioCardAdmin({
|
||||
cuestionario,
|
||||
evento,
|
||||
handleDownload,
|
||||
disabled,
|
||||
}: {
|
||||
cuestionario: CuestionarioWithCupo;
|
||||
evento: GetEvento;
|
||||
handleDownload: (
|
||||
id_cuestionario: number,
|
||||
nombre_formulario: string,
|
||||
nombre_evento: string
|
||||
) => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
const [verMas, setVerMas] = useState(false);
|
||||
const descripcion = cuestionario.descripcion ?? '';
|
||||
@@ -35,7 +43,7 @@ export default function FormularioCardAdmin({
|
||||
>
|
||||
<div className="position-relative">
|
||||
<Link
|
||||
href={`/administrador/evento/${evento.id_evento}/formulario/${cuestionario.id_cuestionario}`}
|
||||
href={`/user/evento/${evento.id_evento}/formulario/${cuestionario.id_cuestionario}`}
|
||||
>
|
||||
<Image
|
||||
width={400}
|
||||
@@ -44,13 +52,25 @@ export default function FormularioCardAdmin({
|
||||
src={
|
||||
cuestionario.banner
|
||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${cuestionario.banner}`
|
||||
: `${process.env.NEXT_PUBLIC_API_URL}/banners/default-banner.png`
|
||||
: evento.banner
|
||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`
|
||||
: `/default-banner.png`
|
||||
}
|
||||
alt="Banner formulario"
|
||||
style={{ objectFit: 'cover', height: '200px' }}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Mensaje cuando se usa la imagen del evento */}
|
||||
{!cuestionario.banner && evento.banner && (
|
||||
<div className="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center bg-black bg-opacity-25">
|
||||
<div className="bg-white bg-opacity-90 px-3 py-2 rounded-pill text-center small fw-bold">
|
||||
<i className="bi bi-info-circle-fill me-1 text-primary"></i>
|
||||
Usando imagen del evento
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Badge de tipo de cuestionario en la esquina superior izquierda */}
|
||||
<div className="position-absolute top-0 start-0 m-2">
|
||||
<span className="badge bg-info">
|
||||
@@ -130,14 +150,27 @@ export default function FormularioCardAdmin({
|
||||
{/* Botones de acción */}
|
||||
<div className="d-grid gap-2">
|
||||
<Link
|
||||
href={`/administrador/evento/${evento.id_evento}/formulario/${cuestionario.id_cuestionario}`}
|
||||
href={`/user/evento/${evento.id_evento}/formulario/${cuestionario.id_cuestionario}`}
|
||||
className="btn btn-primary rounded-pill"
|
||||
>
|
||||
Ver/Editar formulario
|
||||
</Link>
|
||||
|
||||
{/* Botón secundario para ver respuestas */}
|
||||
<Button outline variant="success" size="sm" className="rounded-pill">
|
||||
<Button
|
||||
outline
|
||||
variant="success"
|
||||
size="sm"
|
||||
className="rounded-pill"
|
||||
onClick={() =>
|
||||
handleDownload(
|
||||
cuestionario.id_cuestionario,
|
||||
cuestionario.nombre_form,
|
||||
evento.nombre_evento
|
||||
)
|
||||
}
|
||||
disabled={disabled}
|
||||
>
|
||||
<i className="bi bi-download me-2"></i>
|
||||
Descargar respuestas
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
import { GetCuestionario } from '@/types/cuestionario';
|
||||
import { FormularioCreacion } from '@/types/create-formulario';
|
||||
import { GetEvento, GetEventoWithCuestionariosWithCupos } from '@/types/evento';
|
||||
import Image from 'next/image';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import MarkdownRenderer from '../markdown-render';
|
||||
import { formatearFechaCard } from '@/utils/date-utils';
|
||||
|
||||
interface FormularioCardPreviewProps {
|
||||
// Para formularios existentes
|
||||
cuestionario?: GetCuestionario;
|
||||
// Para formularios en creación
|
||||
formulario?: FormularioCreacion;
|
||||
// Evento asociado
|
||||
evento?: GetEvento | GetEventoWithCuestionariosWithCupos | null;
|
||||
// Banner personalizado
|
||||
nuevoBanner?: File | null;
|
||||
}
|
||||
|
||||
export default function FormularioCardPreview({
|
||||
cuestionario,
|
||||
formulario,
|
||||
evento,
|
||||
nuevoBanner,
|
||||
}: FormularioCardPreviewProps) {
|
||||
const [verMas, setVerMas] = useState(false);
|
||||
const [bannerUrl, setBannerUrl] = useState<string>('/default-banner.png');
|
||||
|
||||
// Determinar qué datos usar (formulario existente o en creación)
|
||||
const isCreating = !!formulario && !cuestionario;
|
||||
const data = cuestionario || formulario;
|
||||
|
||||
// Manejar la imagen del banner
|
||||
useEffect(() => {
|
||||
if (nuevoBanner) {
|
||||
// Si hay un nuevo banner seleccionado, usarlo
|
||||
const url = URL.createObjectURL(nuevoBanner);
|
||||
setBannerUrl(url);
|
||||
return () => URL.revokeObjectURL(url);
|
||||
} else if (!isCreating && cuestionario?.banner) {
|
||||
// Si es un formulario existente y tiene banner propio
|
||||
setBannerUrl(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/banners/${cuestionario.banner}`
|
||||
);
|
||||
} else if (evento?.banner) {
|
||||
// Si hay banner del evento, usarlo
|
||||
setBannerUrl(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`
|
||||
);
|
||||
} else {
|
||||
// Banner por defecto
|
||||
setBannerUrl('/default-banner.png');
|
||||
}
|
||||
}, [nuevoBanner, evento?.banner, cuestionario?.banner, isCreating]);
|
||||
|
||||
if (!data) {
|
||||
return null; // No hay datos para mostrar
|
||||
}
|
||||
|
||||
const descripcion = data.descripcion ?? '';
|
||||
const limite = 100;
|
||||
|
||||
const descripcionRecortada =
|
||||
descripcion.length > limite && !verMas
|
||||
? `${descripcion.slice(0, limite)}...`
|
||||
: descripcion;
|
||||
|
||||
// Obtener información de fecha usando la utilidad
|
||||
const getDateInfo = () => {
|
||||
if (!data.fecha_inicio || !data.fecha_fin) {
|
||||
return { mesTexto: 'MES', diaTexto: 'DD' };
|
||||
}
|
||||
|
||||
let fechaInicio: Date, fechaFin: Date;
|
||||
|
||||
if (isCreating) {
|
||||
// Para formularios en creación, las fechas vienen como string
|
||||
fechaInicio = new Date(data.fecha_inicio);
|
||||
fechaFin = new Date(data.fecha_fin);
|
||||
} else {
|
||||
// Para formularios existentes, convertir a Date también
|
||||
fechaInicio = new Date(data.fecha_inicio);
|
||||
fechaFin = new Date(data.fecha_fin);
|
||||
}
|
||||
|
||||
return formatearFechaCard(fechaInicio, fechaFin);
|
||||
};
|
||||
|
||||
const { mesTexto, diaTexto } = getDateInfo();
|
||||
|
||||
// Determinar si se está usando la imagen del evento
|
||||
const isUsingEventImage =
|
||||
!nuevoBanner && (isCreating || !cuestionario?.banner) && evento?.banner;
|
||||
|
||||
// Obtener el tipo de cuestionario
|
||||
const getTipoCuestionario = () => {
|
||||
const tipoId = isCreating
|
||||
? (formulario as FormularioCreacion).id_tipo_cuestionario
|
||||
: (cuestionario as GetCuestionario).id_tipo_cuestionario;
|
||||
|
||||
if (tipoId === 1) return 'Registro';
|
||||
if (tipoId === 2) return 'Evaluación';
|
||||
return 'Formulario';
|
||||
};
|
||||
|
||||
// Obtener nombre del formulario
|
||||
const getNombreFormulario = () => {
|
||||
return data.nombre_form || 'Nombre del formulario';
|
||||
};
|
||||
|
||||
// Obtener información de cupos
|
||||
const getCuposInfo = () => {
|
||||
if (isCreating) {
|
||||
const form = formulario as FormularioCreacion;
|
||||
return form.cupo_maximo && form.cupo_maximo > 0
|
||||
? `0 / ${form.cupo_maximo}`
|
||||
: 'Sin límite';
|
||||
} else {
|
||||
const cuest = cuestionario as GetCuestionario;
|
||||
return cuest.cupo_maximo !== null && cuest.cupo_maximo !== undefined
|
||||
? `0 / ${cuest.cupo_maximo}`
|
||||
: 'Sin límite';
|
||||
}
|
||||
};
|
||||
|
||||
// Obtener información de secciones y preguntas (solo para formularios en creación)
|
||||
const getSeccionesInfo = () => {
|
||||
if (!isCreating || !formulario) return null;
|
||||
|
||||
const totalSecciones = formulario.secciones.length;
|
||||
const totalPreguntas = formulario.secciones.reduce(
|
||||
(total, seccion) => total + seccion.preguntas.length,
|
||||
0
|
||||
);
|
||||
|
||||
return { totalSecciones, totalPreguntas };
|
||||
};
|
||||
|
||||
const seccionesInfo = getSeccionesInfo();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="card shadow-sm border-0 mb-2"
|
||||
style={{ borderRadius: '12px', overflow: 'hidden' }}
|
||||
>
|
||||
<div className="position-relative">
|
||||
<Image
|
||||
width={400}
|
||||
height={250}
|
||||
className="img-fluid w-100"
|
||||
src={bannerUrl}
|
||||
alt="Banner formulario"
|
||||
style={{ objectFit: 'cover', height: '200px' }}
|
||||
/>
|
||||
|
||||
{/* Mensaje cuando se usa la imagen del evento */}
|
||||
{isUsingEventImage && (
|
||||
<div className="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center bg-black bg-opacity-25">
|
||||
<div className="bg-white bg-opacity-90 px-3 py-2 rounded-pill text-center small fw-bold">
|
||||
<i className="bi bi-info-circle-fill me-1 text-primary"></i>
|
||||
Usando imagen del evento
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Badge de tipo de cuestionario en la esquina superior izquierda */}
|
||||
<div className="position-absolute top-0 start-0 m-2">
|
||||
<span className="badge bg-info">{getTipoCuestionario()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-body p-3 d-flex flex-column">
|
||||
{/* Fecha del formulario */}
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<div className="me-3" style={{ minWidth: '50px' }}>
|
||||
<div
|
||||
className="text-muted text-center small"
|
||||
style={{
|
||||
fontSize: mesTexto.includes(' - ') ? '0.65rem' : '0.75rem',
|
||||
lineHeight: 1.1,
|
||||
}}
|
||||
>
|
||||
{mesTexto}
|
||||
</div>
|
||||
<div
|
||||
className="fw-bold mb-0 text-center"
|
||||
style={{
|
||||
lineHeight: 1,
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: diaTexto.toString().length > 2 ? '1rem' : '1.25rem',
|
||||
}}
|
||||
>
|
||||
{diaTexto}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="card-title mb-1 fw-bold">{getNombreFormulario()}</h5>
|
||||
{evento && (
|
||||
<small className="text-muted">
|
||||
<i className="bi bi-calendar-event me-1"></i>
|
||||
{evento.nombre_evento}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Descripción */}
|
||||
<div className="card-description flex-grow-1 mb-3">
|
||||
{descripcion ? (
|
||||
<>
|
||||
<MarkdownRenderer markdown={descripcionRecortada} />
|
||||
{descripcion.length > limite && (
|
||||
<button
|
||||
onClick={() => setVerMas(!verMas)}
|
||||
className="btn btn-link btn-sm p-0 ms-1 align-baseline text-decoration-none"
|
||||
style={{ fontSize: '0.875rem' }}
|
||||
>
|
||||
{verMas ? 'Ver menos' : 'Ver más'}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-muted small">Descripción del formulario...</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Información de cupos */}
|
||||
<div className="mb-3">
|
||||
<div className="py-2 px-3 bg-light rounded">
|
||||
<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">{getCuposInfo()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Información de secciones y preguntas (solo para formularios en creación) */}
|
||||
{seccionesInfo && (
|
||||
<div className="mb-3">
|
||||
<div className="py-2 px-3 bg-light rounded">
|
||||
<div className="d-flex justify-content-between align-items-center text-sm">
|
||||
<div className="d-flex align-items-center">
|
||||
<i className="bi bi-list-task me-2 text-success"></i>
|
||||
<span className="fw-semibold">
|
||||
{seccionesInfo.totalSecciones} sección
|
||||
{seccionesInfo.totalSecciones !== 1 ? 'es' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="d-flex align-items-center">
|
||||
<i className="bi bi-question-circle me-2 text-warning"></i>
|
||||
<span className="fw-semibold">
|
||||
{seccionesInfo.totalPreguntas} pregunta
|
||||
{seccionesInfo.totalPreguntas !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Botón de acción */}
|
||||
<div className="d-grid gap-2">
|
||||
<button className="btn btn-primary rounded-pill" disabled>
|
||||
Ver/Editar formulario
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-success btn-sm rounded-pill"
|
||||
disabled
|
||||
>
|
||||
<i className="bi bi-download me-2"></i>
|
||||
Descargar respuestas
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -27,6 +27,12 @@ export default function FormularioCardUser({
|
||||
? `${descripcion.slice(0, limite)}...`
|
||||
: descripcion;
|
||||
|
||||
const imgSrc = formulario.banner
|
||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${formulario.banner}`
|
||||
: evento.banner
|
||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`
|
||||
: `/default-banner.png`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="card shadow-sm border-0"
|
||||
@@ -39,7 +45,7 @@ export default function FormularioCardUser({
|
||||
width={400}
|
||||
height={250}
|
||||
className="img-fluid w-100"
|
||||
src={`${process.env.NEXT_PUBLIC_API_URL}/banners/${formulario.banner}`}
|
||||
src={imgSrc}
|
||||
alt="Banner formulario"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
@@ -62,7 +68,7 @@ export default function FormularioCardUser({
|
||||
width={400}
|
||||
height={250}
|
||||
className="img-fluid w-100"
|
||||
src={`${process.env.NEXT_PUBLIC_API_URL}/banners/${formulario.banner}`}
|
||||
src={imgSrc}
|
||||
alt="Banner formulario"
|
||||
style={{ objectFit: 'cover', height: '200px' }}
|
||||
/>
|
||||
|
||||
@@ -146,28 +146,31 @@ export default function FormularioEditor({
|
||||
onChange(actualizado);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row border p-3 rounded my-4">
|
||||
<h2 className="mb-3">Editar Formulario</h2>
|
||||
// Renderizar solo información básica del formulario
|
||||
const renderInformacionBasica = () => (
|
||||
<div className="p-4">
|
||||
<h4 className="mb-3">Información del Formulario</h4>
|
||||
|
||||
<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="row">
|
||||
<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 className="col-md-4">
|
||||
<SimpleInput
|
||||
label="Cupo máximo"
|
||||
type="number"
|
||||
placeholder="Opcional"
|
||||
value={formulario.cupo_maximo || ''}
|
||||
onChange={(e) =>
|
||||
actualizarCampo('cupo_maximo', Number(e.target.value) || 0)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SimpleInput
|
||||
@@ -194,10 +197,18 @@ export default function FormularioEditor({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{evento && (
|
||||
<div className="badge bg-primary fs-6 w-100 mb-3">
|
||||
Este evento se realizara{' '}
|
||||
{formatearRangoFechas(evento.fecha_inicio, evento.fecha_fin)}
|
||||
<div className="alert alert-primary d-flex align-items-center">
|
||||
<i className="bi bi-info-circle-fill me-2"></i>
|
||||
<div>
|
||||
<strong>Evento:</strong> {evento.nombre_evento}
|
||||
<br />
|
||||
<small>
|
||||
Se realizará{' '}
|
||||
{formatearRangoFechas(evento.fecha_inicio, evento.fecha_fin)}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -210,14 +221,27 @@ export default function FormularioEditor({
|
||||
options={tiposCuestionario}
|
||||
placeholder="Selecciona un tipo"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
<hr className="my-4" />
|
||||
// Renderizar secciones y preguntas
|
||||
const renderSeccionesYPreguntas = () => (
|
||||
<div className="mt-4">
|
||||
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 className="mb-0">Secciones y Preguntas</h4>
|
||||
<Button
|
||||
className="btn btn-success"
|
||||
onClick={agregarSeccion}
|
||||
icon="plus"
|
||||
>
|
||||
Agregar sección
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Secciones */}
|
||||
{formulario.secciones.map((seccion, seccionIdx) => (
|
||||
<div key={seccionIdx} className="mb-4 p-3 border rounded bg-light">
|
||||
<div className="d-flex justify-content-between align-items-center mb-2">
|
||||
<h4 className="mb-0">Sección {seccionIdx + 1}</h4>
|
||||
<h5 className="mb-0">Sección {seccionIdx + 1}</h5>
|
||||
<Button
|
||||
variant="danger"
|
||||
icon="trash"
|
||||
@@ -316,6 +340,7 @@ export default function FormularioEditor({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-check mb-2">
|
||||
<input
|
||||
className="form-check-input"
|
||||
@@ -376,16 +401,11 @@ export default function FormularioEditor({
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="text-center mt-4">
|
||||
<Button
|
||||
className="btn btn-success"
|
||||
onClick={agregarSeccion}
|
||||
icon="plus"
|
||||
>
|
||||
Agregar sección
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return {
|
||||
informacionBasica: renderInformacionBasica,
|
||||
seccionesYPreguntas: renderSeccionesYPreguntas,
|
||||
};
|
||||
}
|
||||
|
||||
+143
-117
@@ -3,9 +3,11 @@ import ImageUploader from '@/components/banner-uploader';
|
||||
import Button from '@/components/button';
|
||||
import Input from '@/components/input';
|
||||
import Select from '@/components/select';
|
||||
import EventoCardPreview from '@/components/evento/evento-card-preview';
|
||||
import { CreateEventoType } from '@/types/create-evento';
|
||||
import { GetEventoWithCuestionariosWithCupos } from '@/types/evento';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import { tipoEventos } from '@/utils/arrays';
|
||||
import { formatDateLocal } from '@/utils/date-utils';
|
||||
import { getAxiosError } from '@/utils/errors-utils';
|
||||
import { commands } from '@uiw/react-md-editor';
|
||||
@@ -14,6 +16,8 @@ import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false });
|
||||
|
||||
const MAX_LENGTH = 500;
|
||||
|
||||
interface EditEventoProps {
|
||||
evento: GetEventoWithCuestionariosWithCupos;
|
||||
handleChange: (field: keyof CreateEventoType, value: string | Date) => void;
|
||||
@@ -68,136 +72,158 @@ export default function EditEvento({
|
||||
}
|
||||
};
|
||||
|
||||
// Convertir evento a CreateEventoType para el preview
|
||||
const eventoPreview: CreateEventoType = {
|
||||
tipo_evento: evento.tipo_evento,
|
||||
nombre_evento: evento.nombre_evento,
|
||||
descripcion_evento: evento.descripcion_evento,
|
||||
fecha_inicio: new Date(evento.fecha_inicio),
|
||||
fecha_fin: new Date(evento.fecha_fin),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-2">
|
||||
<div className="p-4 border rounded bg-light">
|
||||
<h2 className="mb-2">Editar Información del Evento</h2>
|
||||
<p className="mb-0 text-muted">
|
||||
Modifica los datos del evento que desees actualizar y confrimalos.
|
||||
</p>
|
||||
<div className="container-fluid my-4">
|
||||
{/* Header */}
|
||||
<div className="row mb-4">
|
||||
<div className="col-12">
|
||||
<div className="p-4 border rounded bg-light">
|
||||
<h2 className="mb-2">Editar Información del Evento</h2>
|
||||
<p className="mb-0 text-muted">
|
||||
Modifica los datos del evento que desees actualizar y confirmalos.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="row">
|
||||
{/* Columna Izquierda - Banner y Nombre */}
|
||||
<div className="col-lg-6">
|
||||
<div className="pe-lg-3">
|
||||
<ImageUploader
|
||||
label="Banner del evento"
|
||||
defaultBanner={
|
||||
evento.banner
|
||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`
|
||||
: undefined
|
||||
}
|
||||
onChange={(file) => {
|
||||
setNuevoBanner(file);
|
||||
}}
|
||||
/>
|
||||
<p className="text-muted mt-2 small">
|
||||
<strong>
|
||||
<i className="bi bi-info-circle-fill me-2"></i>
|
||||
Esta es la imagen que se verá en el carrusel del evento.
|
||||
</strong>
|
||||
<br />
|
||||
<strong>
|
||||
<i className="bi bi-image me-2"></i>
|
||||
Dimensiones recomendadas: 1200x600 píxeles.
|
||||
</strong>
|
||||
<br />
|
||||
<strong>
|
||||
<i className="bi bi-file-earmark-image me-2"></i>
|
||||
Formatos soportados: PNG, JPG, JPEG.
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Columna Derecha - Descripción, Tipo y Fechas */}
|
||||
<div className="col-lg-6">
|
||||
<div className="ps-lg-3">
|
||||
<h4 className="mb-3">Detalles del Evento</h4>
|
||||
|
||||
<Input
|
||||
label="Nombre del evento"
|
||||
required
|
||||
value={evento.nombre_evento}
|
||||
onChange={(e) => handleChange('nombre_evento', e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="row">
|
||||
{/* Columna Izquierda - Formulario */}
|
||||
<div className="col-xl-7 col-lg-6">
|
||||
<div className="pe-lg-4">
|
||||
<div className="p-4">
|
||||
{/* Banner */}
|
||||
<div className="mb-4">
|
||||
<label className="form-label">Descripción del evento</label>
|
||||
<div data-color-mode="light">
|
||||
<MDEditor
|
||||
value={evento.descripcion_evento ?? ''}
|
||||
onChange={(value) => {
|
||||
const texto = value || '';
|
||||
if (texto.length <= 500) {
|
||||
handleChange('descripcion_evento', texto);
|
||||
<ImageUploader
|
||||
label="Banner del evento"
|
||||
defaultBanner={
|
||||
evento.banner
|
||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`
|
||||
: undefined
|
||||
}
|
||||
onChange={(file) => {
|
||||
setNuevoBanner(file);
|
||||
}}
|
||||
/>
|
||||
<p className="text-muted mt-2 small">
|
||||
<strong>
|
||||
<i className="bi bi-info-circle-fill me-2"></i>
|
||||
Esta es la imagen que se verá en el carrusel del evento.
|
||||
</strong>
|
||||
<br />
|
||||
<strong>
|
||||
<i className="bi bi-image me-2"></i>
|
||||
Dimensiones recomendadas: 1200x600 píxeles.
|
||||
</strong>
|
||||
<br />
|
||||
<strong>
|
||||
<i className="bi bi-file-earmark-image me-2"></i>
|
||||
Formatos soportados: PNG, JPG, JPEG.
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Detalles del evento */}
|
||||
<div className="col-12">
|
||||
<h4 className="mb-3">Detalles del Evento</h4>
|
||||
|
||||
<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>
|
||||
<div data-color-mode="light">
|
||||
<MDEditor
|
||||
value={evento.descripcion_evento ?? ''}
|
||||
onChange={(value) => {
|
||||
const texto = value || '';
|
||||
if (texto.length <= MAX_LENGTH) {
|
||||
handleChange('descripcion_evento', texto);
|
||||
}
|
||||
}}
|
||||
height={200}
|
||||
commands={[commands.bold, commands.italic, commands.hr]}
|
||||
/>
|
||||
</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}
|
||||
/>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<Input
|
||||
label="Fecha de inicio"
|
||||
type="datetime-local"
|
||||
className={{ container: 'mb-3' }}
|
||||
value={formatDateLocal(new Date(evento.fecha_inicio))}
|
||||
onChange={(e) =>
|
||||
handleChange('fecha_inicio', new Date(e.target.value))
|
||||
}
|
||||
}}
|
||||
height={200}
|
||||
commands={[commands.bold, commands.italic, commands.hr]}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Input
|
||||
label="Fecha de fin"
|
||||
type="datetime-local"
|
||||
className={{ container: 'mb-3' }}
|
||||
value={formatDateLocal(new Date(evento.fecha_fin))}
|
||||
onChange={(e) =>
|
||||
handleChange('fecha_fin', new Date(e.target.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</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={[
|
||||
{ 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' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<Input
|
||||
label="Fecha de inicio"
|
||||
type="datetime-local"
|
||||
className={{ container: 'mb-3' }}
|
||||
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="datetime-local"
|
||||
className={{ container: 'mb-3' }}
|
||||
value={formatDateLocal(new Date(evento.fecha_fin))}
|
||||
onChange={(e) =>
|
||||
handleChange('fecha_fin', new Date(e.target.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{/* Botón de guardar */}
|
||||
<div className="d-flex justify-content-end mt-4">
|
||||
<Button icon="save" disabled={loading} onClick={handleOnSave}>
|
||||
{loading ? 'Guardando...' : 'Guardar Cambios'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botón de guardar en la parte inferior */}
|
||||
<div className="row mt-4">
|
||||
<div className="col-12">
|
||||
<div className="d-flex justify-content-end">
|
||||
<Button icon="save" disabled={loading} onClick={handleOnSave}>
|
||||
{loading ? 'Guardando...' : 'Guardar Cambios'}
|
||||
</Button>
|
||||
{/* Columna Derecha - Preview */}
|
||||
<div className="col-xl-5 col-lg-6">
|
||||
<div className="ps-lg-4">
|
||||
<div className="sticky-top" style={{ top: '20px' }}>
|
||||
<h4 className="mb-3 d-none d-lg-block">Preview del Evento</h4>
|
||||
<div className="d-lg-none mt-4">
|
||||
<h4 className="mb-3">Preview del Evento</h4>
|
||||
</div>
|
||||
<EventoCardPreview
|
||||
evento={eventoPreview}
|
||||
eventoBanner={nuevoBanner}
|
||||
existingBanner={evento.banner}
|
||||
cuestionariosCount={evento.cuestionarios?.length || 0}
|
||||
/>
|
||||
<div className="alert alert-info text-center mt-3">
|
||||
<strong>Vista previa:</strong> Así se verá tu evento en las
|
||||
listas de eventos.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+162
-112
@@ -1,7 +1,9 @@
|
||||
import ImageUploader from '@/components/banner-uploader';
|
||||
import Button from '@/components/button';
|
||||
import SimpleInput from '@/components/input';
|
||||
import FormularioCardPreview from '@/components/formulario/formulario-card-preview';
|
||||
import { GetCuestionario } from '@/types/cuestionario';
|
||||
import { GetEvento } from '@/types/evento';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import { formatDateLocal } from '@/utils/date-utils';
|
||||
import { getAxiosError } from '@/utils/errors-utils';
|
||||
@@ -11,12 +13,14 @@ import toast from 'react-hot-toast';
|
||||
|
||||
interface EditFormularioProps {
|
||||
cuestionario: GetCuestionario;
|
||||
evento: GetEvento | null;
|
||||
handleChange: (field: keyof GetCuestionario, value: string | Date) => void;
|
||||
handleOnChange: (eventoActualizado: GetCuestionario) => void;
|
||||
}
|
||||
|
||||
export default function EditFormulario({
|
||||
cuestionario,
|
||||
evento,
|
||||
handleChange,
|
||||
}: EditFormularioProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -65,130 +69,176 @@ export default function EditFormulario({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-2">
|
||||
<div className="p-4 border rounded bg-light">
|
||||
<h2 className="mb-2">Editar Información del Formulario</h2>
|
||||
<p className="mb-0 text-muted">
|
||||
Modifica los datos del formulario que desees actualizar y confrimalos.
|
||||
</p>
|
||||
<div className="container-fluid my-4">
|
||||
{/* Header */}
|
||||
<div className="row mb-4">
|
||||
<div className="col-12">
|
||||
<div className="p-4 border rounded bg-light">
|
||||
<h2 className="mb-2">Editar Información del Formulario</h2>
|
||||
<p className="mb-0 text-muted">
|
||||
Modifica los datos del formulario que desees actualizar y
|
||||
confirmalos.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="row">
|
||||
{/* Columna Izquierda - Banner */}
|
||||
<div className="col-lg-6">
|
||||
<div className="pe-lg-3">
|
||||
<ImageUploader
|
||||
label="Banner del formulario"
|
||||
defaultBanner={
|
||||
cuestionario.banner
|
||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${cuestionario.banner}`
|
||||
: undefined
|
||||
}
|
||||
onChange={(file) => {
|
||||
setNuevoBanner(file);
|
||||
}}
|
||||
/>
|
||||
<p className="text-muted mt-2 small">
|
||||
<strong>
|
||||
<i className="bi bi-info-circle-fill me-2"></i>
|
||||
Esta es la imagen que se usara para la carta de presentación
|
||||
del formulario.
|
||||
</strong>
|
||||
<br />
|
||||
<strong>
|
||||
<i className="bi bi-image me-2"></i>
|
||||
Dimensiones recomendadas: 1200x600 píxeles.
|
||||
</strong>
|
||||
<br />
|
||||
<strong>
|
||||
<i className="bi bi-file-earmark-image me-2"></i>
|
||||
Formatos soportados: PNG, JPG, JPEG.
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Columna Derecha - Detalles del formulario */}
|
||||
<div className="col-lg-6">
|
||||
<div className="ps-lg-3">
|
||||
<h4 className="mb-3">Detalles del Formulario</h4>
|
||||
|
||||
<SimpleInput
|
||||
label="Nombre del formulario"
|
||||
required
|
||||
value={cuestionario.nombre_form}
|
||||
onChange={(e) => handleChange('nombre_form', e.target.value)}
|
||||
/>
|
||||
|
||||
<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 className="mb-4">
|
||||
<label className="form-label">Descripción del formulario</label>
|
||||
<div data-color-mode="light">
|
||||
<MDEditor
|
||||
value={cuestionario.descripcion ?? ''}
|
||||
onChange={(value) => {
|
||||
const texto = value || '';
|
||||
if (texto.length <= 500) {
|
||||
handleChange(
|
||||
'descripcion' as keyof GetCuestionario,
|
||||
texto
|
||||
);
|
||||
<div className="row">
|
||||
{/* Columna Izquierda - Formulario */}
|
||||
<div className="col-xl-7 col-lg-6">
|
||||
<div className="pe-lg-4">
|
||||
<div className="p-4">
|
||||
<div className="row">
|
||||
{/* Banner */}
|
||||
<div className="col-12">
|
||||
<div className="mb-4">
|
||||
<ImageUploader
|
||||
label="Banner del formulario"
|
||||
defaultBanner={
|
||||
cuestionario.banner
|
||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${cuestionario.banner}`
|
||||
: undefined
|
||||
}
|
||||
}}
|
||||
height={200}
|
||||
commands={[commands.bold, commands.italic, commands.hr]}
|
||||
onChange={(file) => {
|
||||
setNuevoBanner(file);
|
||||
}}
|
||||
/>
|
||||
<p className="text-muted mt-2 small">
|
||||
<strong>
|
||||
<i className="bi bi-info-circle-fill me-2"></i>
|
||||
Esta es la imagen que se usara para la carta de
|
||||
presentación del formulario.
|
||||
</strong>
|
||||
<br />
|
||||
<strong>
|
||||
<i className="bi bi-image me-2"></i>
|
||||
Dimensiones recomendadas: 1200x600 píxeles.
|
||||
</strong>
|
||||
<br />
|
||||
<strong>
|
||||
<i className="bi bi-file-earmark-image me-2"></i>
|
||||
Formatos soportados: PNG, JPG, JPEG.
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
{/* Alert sobre imagen por defecto */}
|
||||
{!cuestionario.banner && !nuevoBanner && evento?.banner && (
|
||||
<div className="alert alert-info mt-3">
|
||||
<i className="bi bi-info-circle me-2"></i>
|
||||
<strong>Imagen por defecto:</strong> Si no seleccionas
|
||||
una imagen específica para este formulario, se utilizará
|
||||
automáticamente la imagen del evento.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detalles del formulario */}
|
||||
<div className="col-12">
|
||||
<h4 className="mb-3">Detalles del Formulario</h4>
|
||||
|
||||
<SimpleInput
|
||||
label="Nombre del formulario"
|
||||
required
|
||||
value={cuestionario.nombre_form}
|
||||
onChange={(e) =>
|
||||
handleChange('nombre_form', e.target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<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 className="mb-4">
|
||||
<label className="form-label">
|
||||
Descripción del formulario
|
||||
</label>
|
||||
<div data-color-mode="light">
|
||||
<MDEditor
|
||||
value={cuestionario.descripcion ?? ''}
|
||||
onChange={(value) => {
|
||||
const texto = value || '';
|
||||
if (texto.length <= 500) {
|
||||
handleChange(
|
||||
'descripcion' as keyof GetCuestionario,
|
||||
texto
|
||||
);
|
||||
}
|
||||
}}
|
||||
height={200}
|
||||
commands={[commands.bold, commands.italic, commands.hr]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<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>
|
||||
{/* Botón de guardar */}
|
||||
<div className="d-flex justify-content-end mt-4">
|
||||
<Button icon="save" disabled={loading} onClick={handleOnSave}>
|
||||
{loading ? 'Guardando...' : 'Guardar Cambios'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botón de guardar en la parte inferior */}
|
||||
<div className="row mt-4">
|
||||
<div className="col-12">
|
||||
<div className="d-flex justify-content-end">
|
||||
<Button icon="save" disabled={loading} onClick={handleOnSave}>
|
||||
{loading ? 'Guardando...' : 'Guardar Cambios'}
|
||||
</Button>
|
||||
{/* Columna Derecha - Preview */}
|
||||
<div className="col-xl-5 col-lg-6">
|
||||
<div className="ps-lg-4">
|
||||
<div className="sticky-top" style={{ top: '20px' }}>
|
||||
<h4 className="mb-3 d-none d-lg-block">Preview del Formulario</h4>
|
||||
<div className="d-lg-none mt-4">
|
||||
<h4 className="mb-3">Preview del Formulario</h4>
|
||||
</div>
|
||||
<FormularioCardPreview
|
||||
cuestionario={cuestionario}
|
||||
evento={evento}
|
||||
nuevoBanner={nuevoBanner}
|
||||
/>
|
||||
<div className="alert alert-info text-center mt-3">
|
||||
<strong>Vista previa:</strong> Así se verá tu formulario en las
|
||||
listas de formularios.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user