diff --git a/public/assets/image.png b/public/assets/image.png new file mode 100644 index 0000000..fa6b429 Binary files /dev/null and b/public/assets/image.png differ diff --git a/src/app/(admins)/administrador/eventos/crear/page.tsx b/src/app/(admins)/administrador/eventos/crear/page.tsx index 3bc334f..45ba8fd 100644 --- a/src/app/(admins)/administrador/eventos/crear/page.tsx +++ b/src/app/(admins)/administrador/eventos/crear/page.tsx @@ -3,6 +3,7 @@ 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'; @@ -71,20 +72,54 @@ export default function Page() { }; return ( -
- - - {/* Botón de guardar en la parte inferior */} -
+
+ {/* Header */} +
-
- +
+

Creación del Evento

+

+ Completa la siguiente información para describir los detalles + generales del evento. Presiona siguiente para continuar con la + creación del formulario. +

+
+
+
+ +
+ {/* Columna Izquierda - Formulario */} +
+
+ + + {/* Botón de guardar */} +
+ +
+
+
+ + {/* Columna Derecha - Preview */} +
+
+
+

Preview del Evento

+
+

Preview del Evento

+
+ +
+ Nota: Esta preview es la que veras en tu ruta + de eventos y en la ruta publica de eventos +
+
diff --git a/src/app/(admins)/administrador/eventos/page.tsx b/src/app/(admins)/administrador/eventos/page.tsx index 699ce1e..3715f07 100644 --- a/src/app/(admins)/administrador/eventos/page.tsx +++ b/src/app/(admins)/administrador/eventos/page.tsx @@ -1,17 +1,19 @@ 'use client'; -import Button from '@/components/button'; -import CuestionarioCard from '@/components/cuestionario-card'; 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 Link from 'next/link'; import React from 'react'; export default function Page() { - const { loading, data, error } = useGetApi< - GetEventoWithCuestionariosWithCupos[] - >('/evento/activos/cuestionarios'); + const { + loading, + data: activos, + error, + } = useGetApi( + '/evento/activos/cuestionarios' + ); const { data: recientes } = useGetApi( '/evento/recientes/cuestionarios' ); @@ -20,25 +22,19 @@ export default function Page() { if (error) return
{error.message}
; return ( - <> -
-

Eventos

- - - -
+
{loading &&
Cargando...
} - {data && data.length > 0 && ( + {activos && activos.length > 0 && (

Eventos Activos

- {data.map((item, key) => { + {activos.map((evento, key) => { const fadeClass = `delay-${(key % 5) + 1}`; return (
- +
); })} @@ -46,21 +42,26 @@ export default function Page() { )} {recientes && recientes.length > 0 && (
-

Eventos Recientes

+

Formularios Recientes

Eventos de los ultimos 30 días.

- {recientes.map((item, key) => { - const fadeClass = `delay-${(key % 5) + 1}`; - return ( -
- -
- ); - })} + {recientes.map((evento) => + evento.cuestionarios.map((cuestionario, cuestionarioIndex) => { + const fadeClass = `delay-${(cuestionarioIndex % 5) + 1}`; + return ( +
+ +
+ ); + }) + )}
)} - +
); } diff --git a/src/app/(admins)/administrador/layout.tsx b/src/app/(admins)/administrador/layout.tsx index e98b8ef..239525a 100644 --- a/src/app/(admins)/administrador/layout.tsx +++ b/src/app/(admins)/administrador/layout.tsx @@ -1,11 +1,13 @@ 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 (
-
+
+
{children}
diff --git a/src/app/(admins)/user/evento/[id_evento]/formulario/[id_cuestionario]/page.tsx b/src/app/(admins)/user/evento/[id_evento]/formulario/[id_cuestionario]/page.tsx new file mode 100644 index 0000000..bb8a754 --- /dev/null +++ b/src/app/(admins)/user/evento/[id_evento]/formulario/[id_cuestionario]/page.tsx @@ -0,0 +1,192 @@ +'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(); + const { evento } = useEvento(); + + const [cuestionario, setCuestionario] = + React.useState(null); + const [busquedaCorreo, setBusquedaCorreo] = React.useState(''); + const [loadingAsistencia, setLoadingAsistencia] = React.useState< + Record + >({}); + + const { data: participantes, setData } = useGetApi( + `/participante-evento/evento/${params.id_cuestionario}` + ); + const { data } = useGetApi( + `/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( + `/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[] = [ + { + 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 ? ( + + + Asistió + + ) : ( + + ); + }, + }, + ]; + + 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 ( +
+ + + {cuestionario && ( + + )} + +

Participantes

+ + {participantes?.length === 0 && ( +

No hay participantes registrados aún.

+ )} + + {participantes && participantes.length > 0 && ( + <> + setBusquedaCorreo(e.target.value)} + /> + +
+ row.id_participante} + /> + + + )} + + ); +} diff --git a/src/app/(admins)/user/evento/[id_evento]/formularios/crear/page.tsx b/src/app/(admins)/user/evento/[id_evento]/formularios/crear/page.tsx new file mode 100644 index 0000000..36aeb2b --- /dev/null +++ b/src/app/(admins)/user/evento/[id_evento]/formularios/crear/page.tsx @@ -0,0 +1,189 @@ +'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(); + const router = useRouter(); + const { evento, refetch } = useEvento(); + const [datosFormulario, setDatosFormulario] = + useState(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 ( +
+ + +
+

Creación de formulario

+

+ 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. +

+
+ {!datosFormulario && ( +
+

+ Selecciona una plantilla para el formulario: +

+

+ Elige una de las plantillas disponibles para crear tu formulario de + registro. +
+ 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. +
+ + Para asegurar el funcionamiento óptimo del prellenado, modifica la + estructura solo si es necesario. + +

+ +
+ {plantillasDisponibles.map((plantilla) => ( +
handleSeleccionarPlantilla(plantilla.id)} + > +
+ +
+
+ ))} +
+
+ )} + {datosFormulario && ( + setDatosFormulario({ ...nuevo })} + /> + )} + +
+ ); +} diff --git a/src/app/(admins)/user/evento/[id_evento]/layout.tsx b/src/app/(admins)/user/evento/[id_evento]/layout.tsx new file mode 100644 index 0000000..b82638e --- /dev/null +++ b/src/app/(admins)/user/evento/[id_evento]/layout.tsx @@ -0,0 +1,20 @@ +'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(); + + if (!params.id_evento) { + return
Error: ID de evento no encontrado
; + } + + return ( + {children} + ); +} diff --git a/src/app/(admins)/user/evento/[id_evento]/page.tsx b/src/app/(admins)/user/evento/[id_evento]/page.tsx new file mode 100644 index 0000000..8181bbc --- /dev/null +++ b/src/app/(admins)/user/evento/[id_evento]/page.tsx @@ -0,0 +1,130 @@ +'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(); + const { evento, loading, error, updateEvento, setEventoData } = useEvento(); + + const [loadingStates, setLoadingStates] = useState>( + {} + ); + + 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

Cargando...

; + if (error) return

Error: {error}

; + if (!evento) return

No se encontró el evento

; + + 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 ( +
+ + + + +
+
+
+

Formularios del evento

+

+ Aquí puedes ver y gestionar los formularios asociados a este + evento. +

+
+ +
+
+ + {evento?.cuestionarios && evento.cuestionarios.length > 0 && ( +
+ {evento.cuestionarios.map((item, key) => { + const fadeClass = `delay-${(key % 5) + 1}`; + return ( +
+ +
+ ); + })} +
+ )} +
+ ); +} diff --git a/src/app/(admins)/user/eventos/crear/page.tsx b/src/app/(admins)/user/eventos/crear/page.tsx new file mode 100644 index 0000000..45ba8fd --- /dev/null +++ b/src/app/(admins)/user/eventos/crear/page.tsx @@ -0,0 +1,128 @@ +'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(null); + const [evento, setEvento] = useState({ + 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 ( +
+ {/* Header */} +
+
+
+

Creación del Evento

+

+ Completa la siguiente información para describir los detalles + generales del evento. Presiona siguiente para continuar con la + creación del formulario. +

+
+
+
+ +
+ {/* Columna Izquierda - Formulario */} +
+
+ + + {/* Botón de guardar */} +
+ +
+
+
+ + {/* Columna Derecha - Preview */} +
+
+
+

Preview del Evento

+
+

Preview del Evento

+
+ +
+ Nota: Esta preview es la que veras en tu ruta + de eventos y en la ruta publica de eventos +
+
+
+
+
+
+ ); +} diff --git a/src/app/(admins)/user/eventos/page.tsx b/src/app/(admins)/user/eventos/page.tsx new file mode 100644 index 0000000..3715f07 --- /dev/null +++ b/src/app/(admins)/user/eventos/page.tsx @@ -0,0 +1,67 @@ +'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( + '/evento/activos/cuestionarios' + ); + const { data: recientes } = useGetApi( + '/evento/recientes/cuestionarios' + ); + + if (loading) return
Cargando formularios...
; + if (error) return
{error.message}
; + + return ( +
+ {loading &&
Cargando...
} + {activos && activos.length > 0 && ( +
+

Eventos Activos

+ {activos.map((evento, key) => { + const fadeClass = `delay-${(key % 5) + 1}`; + return ( +
+ +
+ ); + })} +
+ )} + {recientes && recientes.length > 0 && ( +
+

Formularios Recientes

+

Eventos de los ultimos 30 días.

+ {recientes.map((evento) => + evento.cuestionarios.map((cuestionario, cuestionarioIndex) => { + const fadeClass = `delay-${(cuestionarioIndex % 5) + 1}`; + return ( +
+ +
+ ); + }) + )} +
+ )} +
+ ); +} diff --git a/src/app/(admins)/user/layout.tsx b/src/app/(admins)/user/layout.tsx new file mode 100644 index 0000000..239525a --- /dev/null +++ b/src/app/(admins)/user/layout.tsx @@ -0,0 +1,15 @@ +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 ( +
+
+ +
{children}
+
+
+ ); +} diff --git a/src/app/(admins)/user/profesores/page.tsx b/src/app/(admins)/user/profesores/page.tsx new file mode 100644 index 0000000..ac94232 --- /dev/null +++ b/src/app/(admins)/user/profesores/page.tsx @@ -0,0 +1,54 @@ +'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(null); + const [response, setResponse] = useState<{ + insertados: number; + omitidos: number; + } | null>(null); + + const handleFileChange = (e: React.ChangeEvent) => { + 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 ( +
+

Subir Excel de Profesores

+ + + + {response && ( +
+

Resultado de la carga:

+

Insertados: {response.insertados}

+

Omitidos: {response.omitidos}

+
+ )} +
+ ); +} diff --git a/src/app/(admins)/user/qr/page.tsx b/src/app/(admins)/user/qr/page.tsx new file mode 100644 index 0000000..d29168d --- /dev/null +++ b/src/app/(admins)/user/qr/page.tsx @@ -0,0 +1,277 @@ +'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 ( +
+
+
+
+
+
+ +
+

Registro de Asistencia

+

+ Escanea el código QR para registrar la asistencia +

+
+ +
+ {enableScan ? ( +
+
+
+ { + if (codes.length > 0) { + handleScan(codes[0].rawValue); + } + }} + onError={(err) => console.error('QR Error:', err)} + constraints={{ facingMode: 'environment' }} + styles={{ + container: { + width: '100%', + maxWidth: '300px', + borderRadius: '8px', + overflow: 'hidden', + }, + }} + /> +
+
+ +
+
+ + + Mantén el código QR centrado en el marco de la cámara + +
+
+ + +
+ ) : ( +
+
+ +
+

Cámara desactivada

+

+ La cámara está detenida. Actívala para continuar escaneando + códigos QR. +

+ +
+ )} + + {statusMessage && ( +
+
+
+ {statusMessage.includes('❌') && ( + + )} + {statusMessage.includes('⚠️') && ( + + )} + {!statusMessage.includes('❌') && + !statusMessage.includes('⚠️') && ( + + )} +
+ {statusMessage} +
+
+ )} +
+
+
+
+ + { + setShowModal(false); + setEnableScan(true); + setScannedData(null); + }} + closeButton + className={{ + content: 'border-0 rounded-3', + body: 'text-center', + }} + > +
+
+ + QR Escaneado Correctamente +
+
+ +
+ {scannedData ? ( + <> +
+
+ +
+
+ Información del participante +
+
+
+
+ + Correo: + + {participante} + +
+
+
+
+ +
+ + +
+ + ) : ( +
+
+ +
+
QR inválido o malformado
+
+ )} +
+
+
+ ); +} diff --git a/src/app/(auth)/login/administradores/page.tsx b/src/app/(auth)/login/administradores/page.tsx deleted file mode 100644 index 6285f2e..0000000 --- a/src/app/(auth)/login/administradores/page.tsx +++ /dev/null @@ -1,66 +0,0 @@ -'use client'; -import Button from '@/components/button'; -import Input from '@/components/input'; -import { useRouter } from 'next/navigation'; -import React, { useState } from 'react'; -import Cookies from 'js-cookie'; - -export default function Page() { - const router = useRouter(); - const [formData, setFormData] = useState({ username: '', password: '' }); - const [error, setError] = useState(''); - - const handleChange = (e: React.ChangeEvent) => { - setFormData({ - ...formData, - [e.target.name]: e.target.value, - }); - setError(''); - }; - - const handleOnSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - const { username, password } = formData; - - if (username === 'user-admin' && password === '@dm1nP@ss') { - Cookies.set('token', 'staff1'); - router.push('/administrador/eventos'); - } else { - setError('Usuario o contraseña incorrectos'); - } - }; - - return ( -
-

Iniciar sesión

-

Administradores

- -
- - - {error && ( -
- {error} -
- )} -
- -
- -
- ); -} diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..cba6ccc --- /dev/null +++ b/src/app/(auth)/login/page.tsx @@ -0,0 +1,164 @@ +'use client'; +import SimpleInput from '@/components/input'; +import PasswordInput from '@/components/password'; +import React from 'react'; +import { useRouter } from 'next/navigation'; + +export default function LoginForm() { + const router = useRouter(); + + // Fake user data + const fakeUsers = [ + { + username: 'user-admin', + password: '@dm1nP@ss', + role: 'administrator', + redirectPath: '/user/eventos', + }, + { + username: 'staff1', + password: 'st@ffP@ss', + role: 'staff', + redirectPath: '/user/eventos', + }, + ]; + + const [loginData, setLoginData] = React.useState({ + username: '', + password: '', + }); + + const [error, setError] = React.useState(''); + const [isLoading, setIsLoading] = React.useState(false); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setLoginData((prevData) => ({ ...prevData, [name]: value })); + // Clear error when user starts typing + if (error) setError(''); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(''); + + // Simulate API call delay + setTimeout(() => { + const user = fakeUsers.find( + (u) => + u.username === loginData.username && u.password === loginData.password + ); + + if (user) { + // Store user data in localStorage (in a real app, you'd use proper session management) + localStorage.setItem( + 'user', + JSON.stringify({ + username: user.username, + role: user.role, + }) + ); + + // Redirect to appropriate dashboard + router.push(user.redirectPath); + } else { + setError('Usuario o contraseña incorrectos'); + } + + setIsLoading(false); + }, 1000); + }; + + return ( +
+
+
+
+
+
+
+
+

+ Iniciar sesión +

+ +
+ Bienvenido de vuelta al{' '} + Sistema de Eventos +
+

+ Ingresa tu nombre de usuario y tu contraseña para acceder + a tu cuenta. +

+ + {/* Error message */} + {error && ( +
{error}
+ )} + +
+ + + +
+ +
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/src/app/(auth)/login/staff/page.tsx b/src/app/(auth)/login/staff/page.tsx deleted file mode 100644 index ee513ef..0000000 --- a/src/app/(auth)/login/staff/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -'use client'; -import Button from '@/components/button'; -import Input from '@/components/input'; -import { useRouter } from 'next/navigation'; -import React, { useState } from 'react'; -import Cookies from 'js-cookie'; - -export default function Page() { - const router = useRouter(); - const [formData, setFormData] = useState({ username: '', password: '' }); - const [error, setError] = useState(''); - - const handleChange = (e: React.ChangeEvent) => { - setFormData({ - ...formData, - [e.target.name]: e.target.value, - }); - setError(''); - }; - - const handleOnSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - const { username, password } = formData; - - if (username === 'staff1' && password === 'st@ffP@ss') { - Cookies.set('token', 'staff1'); - router.push('/staff'); - } else { - setError('Usuario o contraseña incorrectos'); - } - }; - - return ( -
-

Iniciar sesión

-

Staff

- -
- - - {error &&
{error}
} -
- -
- -
- ); -} diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx index 13c6ed6..bbadee4 100644 --- a/src/app/(public)/page.tsx +++ b/src/app/(public)/page.tsx @@ -3,7 +3,7 @@ import ClientCarousel from '@/client-components/client-carousel'; import FormularioCardUser from '@/components/formulario/formulario-card-user'; import { useGetApi } from '@/hooks/use-get-api'; -import { GetEvento, GetEventoWithCuestionariosWithCupos } from '@/types/evento'; +import { GetEventoWithCuestionariosWithCupos } from '@/types/evento'; import React from 'react'; export default function Page() { @@ -35,17 +35,6 @@ export default function Page() {
{data.flatMap((evento) => evento.cuestionarios.map((cuestionario, index) => { - const eventoData: GetEvento = { - id_evento: evento.id_evento, - nombre_evento: evento.nombre_evento, - descripcion_evento: evento.descripcion_evento, - tipo_evento: evento.tipo_evento, - fecha_inicio: evento.fecha_inicio, - fecha_fin: evento.fecha_fin, - asistencias: evento.asistencias, - banner: evento.banner, - }; - return (
diff --git a/src/components/banner-uploader.tsx b/src/components/banner-uploader.tsx index 6f77c0b..96bd186 100644 --- a/src/components/banner-uploader.tsx +++ b/src/components/banner-uploader.tsx @@ -9,6 +9,7 @@ interface BannerUploaderProps { onChange?: (file: File | null) => void; defaultBanner?: string; className?: string; + showPreview?: boolean; } export default function ImageUploader({ @@ -16,6 +17,7 @@ export default function ImageUploader({ onChange, defaultBanner, className = '', + showPreview = true, }: BannerUploaderProps) { const fileInputRef = useRef(null); const [preview, setPreview] = useState(defaultBanner || null); @@ -63,7 +65,7 @@ export default function ImageUploader({ )}
- {preview && ( + {preview && showPreview && (
{/* Fecha del evento */}
-
-
- {new Date(evento.fecha_inicio) - .toLocaleDateString('es-ES', { month: 'short' }) - .toUpperCase()} +
+
+ {mesTexto}
2 ? '1rem' : '1.25rem', + }} > - {(() => { - const fechaInicio = new Date(evento.fecha_inicio); - const fechaFin = new Date(evento.fecha_fin); - const diaInicio = fechaInicio.getDate(); - const diaFin = fechaFin.getDate(); - const esElMismoDia = - fechaInicio.toDateString() === fechaFin.toDateString(); - - return esElMismoDia ? diaInicio : `${diaInicio} - ${diaFin}`; - })()} + {diaTexto}
diff --git a/src/components/evento/evento-card-preview.tsx b/src/components/evento/evento-card-preview.tsx new file mode 100644 index 0000000..5953ef6 --- /dev/null +++ b/src/components/evento/evento-card-preview.tsx @@ -0,0 +1,143 @@ +'use client'; + +import { CreateEventoType } from '@/types/create-evento'; +import Image from 'next/image'; +import React, { useState, useEffect } from 'react'; +import MarkdownRenderer from '@/components/markdown-render'; +import { formatearFechaCard } from '@/utils/date-utils'; +import Button from '../button'; + +interface EventoPreviewProps { + evento: CreateEventoType; + eventoBanner?: File | null; +} + +export default function EventoCardPreview({ + evento, + eventoBanner, +}: EventoPreviewProps) { + const [verMas, setVerMas] = useState(false); + const [bannerUrl, setBannerUrl] = useState('/default-banner.png'); + + const descripcion = evento.descripcion_evento ?? ''; + const limite = 100; + + const descripcionRecortada = + descripcion.length > limite && !verMas + ? `${descripcion.slice(0, limite)}...` + : descripcion; + + // Actualizar URL del banner cuando cambie el archivo + useEffect(() => { + if (eventoBanner) { + const url = URL.createObjectURL(eventoBanner); + setBannerUrl(url); + + // Limpiar URL anterior cuando el componente se desmonte + return () => URL.revokeObjectURL(url); + } else { + setBannerUrl('/default-banner.png'); + } + }, [eventoBanner]); + + // Obtener información de fecha usando la utilidad + const getDateInfo = () => { + if (!evento.fecha_inicio || !evento.fecha_fin) { + return { mesTexto: 'MES', diaTexto: 'DD' }; + } + + return formatearFechaCard(evento.fecha_inicio, evento.fecha_fin); + }; + + const { mesTexto, diaTexto } = getDateInfo(); + + return ( +
+
+ + + {/* Badge indicando que no hay formularios aún */} +
+ Sin formularios +
+
+ +
+ {/* Fecha del evento */} +
+
+
+ {mesTexto} +
+
2 ? '1rem' : '1.25rem', + }} + > + {diaTexto} +
+
+
+
+ {evento.nombre_evento || 'Nombre del evento'} +
+
+
+ + {/* Tipo de evento */} + {evento.tipo_evento && ( +
+ + + {evento.tipo_evento} + +
+ )} + + {/* Descripción */} +
+ {descripcion ? ( + <> + + {descripcion.length > limite && ( + + )} + + ) : ( +

Descripción del evento...

+ )} +
+ + {/* Botón principal - deshabilitado */} + +
+
+ ); +} diff --git a/src/components/formulario/formulario-card-admin.tsx b/src/components/formulario/formulario-card-admin.tsx index b82cc10..01c52ae 100644 --- a/src/components/formulario/formulario-card-admin.tsx +++ b/src/components/formulario/formulario-card-admin.tsx @@ -1,5 +1,148 @@ -import React from 'react'; +import { CuestionarioWithCupo, GetEvento } from '@/types/evento'; +import Image from 'next/image'; +import Link from 'next/link'; +import React, { useState } from 'react'; +import MarkdownRenderer from '../markdown-render'; +import { formatearFechaCard } from '@/utils/date-utils'; +import Button from '../button'; -export default function FormularioCardAdmin() { - return
FormularioCardAdmin
; +export default function FormularioCardAdmin({ + cuestionario, + evento, +}: { + cuestionario: CuestionarioWithCupo; + evento: GetEvento; +}) { + const [verMas, setVerMas] = useState(false); + const descripcion = cuestionario.descripcion ?? ''; + const limite = 100; + + const descripcionRecortada = + descripcion.length > limite && !verMas + ? `${descripcion.slice(0, limite)}...` + : descripcion; + + // Obtener información de fecha usando la utilidad + const { mesTexto, diaTexto } = formatearFechaCard( + cuestionario.fecha_inicio, + cuestionario.fecha_fin + ); + + return ( +
+
+ + + + + {/* Badge de tipo de cuestionario en la esquina superior izquierda */} +
+ + {cuestionario.tipoCuestionario?.tipo_cuestionario || 'Formulario'} + +
+
+ +
+ {/* Fecha del formulario */} +
+
+
+ {mesTexto} +
+
2 ? '1rem' : '1.25rem', + }} + > + {diaTexto} +
+
+
+
+ {cuestionario.nombre_form} +
+ {evento && ( + + + {evento.nombre_evento} + + )} +
+
+ + {/* Descripción */} +
+ + {descripcion.length > limite && ( + + )} +
+ + {/* Información de cupos y estadísticas */} +
+
+
+ +
+ Cupos + + {cuestionario.cupo_maximo !== null + ? `${cuestionario.cupos_usados} / ${cuestionario.cupo_maximo}` + : 'Sin límite'} + +
+
+
+
+ + {/* Botones de acción */} +
+ + Ver/Editar formulario + + + {/* Botón secundario para ver respuestas */} + +
+
+
+ ); } diff --git a/src/components/formulario/formulario-card-user.tsx b/src/components/formulario/formulario-card-user.tsx index 7cb0715..4a86c91 100644 --- a/src/components/formulario/formulario-card-user.tsx +++ b/src/components/formulario/formulario-card-user.tsx @@ -5,6 +5,7 @@ import Link from 'next/link'; import React, { useState } from 'react'; import MarkdownRenderer from '../markdown-render'; import { toParam } from '@/utils/slugify'; +import Button from '../button'; export default function FormularioCardUser({ evento, @@ -17,6 +18,10 @@ export default function FormularioCardUser({ const descripcion = formulario.descripcion ?? ''; const limite = 100; + // Determinar si hay cupos disponibles + const sinCupos = + formulario.cupo_maximo !== null && formulario.cupos_disponibles <= 0; + const descripcionRecortada = descripcion.length > limite && !verMas ? `${descripcion.slice(0, limite)}...` @@ -28,31 +33,47 @@ export default function FormularioCardUser({ style={{ borderRadius: '12px', overflow: 'hidden' }} >
- - - + {sinCupos ? ( +
+ +
+ Sin cupos disponibles +
+
+ ) : ( + + + + )} {/* Badge de cupos en la esquina superior derecha */}
- {formulario.cupo_maximo === null ? ( - Sin límite - ) : ( - - {formulario.cupos_disponibles} cupos - - )} + + {formulario.tipoCuestionario.tipo_cuestionario} +
@@ -92,7 +113,7 @@ export default function FormularioCardUser({
{/* Descripción */} -
+
{descripcion.length > limite && ( + ) : ( + + Registro + + )}
); diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index 18c9c3f..b62154a 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -1,9 +1,21 @@ -import React from "react"; -import Link from "next/link"; +'use client'; +import React from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; const Navbar: React.FC = () => { + const pathname = usePathname(); + + const navItems = [ + { href: '/', label: 'Inicio' }, + { href: '/user/eventos', label: 'Eventos' }, + { href: '/user/qr', label: 'Lector de QRs' }, + { href: '/user/eventos/crear', label: 'Crear Evento' }, + { href: '/signout', label: 'Cerrar sesión' }, + ]; + return ( -