diff --git a/next.config.ts b/next.config.ts index db91768..b8119aa 100644 --- a/next.config.ts +++ b/next.config.ts @@ -6,12 +6,12 @@ const nextConfig: NextConfig = { { protocol: 'http', hostname: 'localhost', - port: '3342', //4200 + port: '4200', //4200 }, { protocol: 'https', - hostname: 'venus.acatlan.unam.mx' - } + hostname: 'venus.acatlan.unam.mx', + }, ], }, }; diff --git a/package-lock.json b/package-lock.json index b168c61..82e1a46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7403,6 +7403,96 @@ "peerDependencies": { "@types/emscripten": ">=1.39.6" } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.12.tgz", + "integrity": "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.12.tgz", + "integrity": "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.12.tgz", + "integrity": "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.12.tgz", + "integrity": "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.12.tgz", + "integrity": "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.12.tgz", + "integrity": "sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/public/plantillas/carga-alumnos.xlsx b/public/plantillas/carga-alumnos.xlsx new file mode 100644 index 0000000..8284081 Binary files /dev/null and b/public/plantillas/carga-alumnos.xlsx differ diff --git a/public/plantillas/carga-trabajadores.xlsx b/public/plantillas/carga-trabajadores.xlsx new file mode 100644 index 0000000..33f6470 Binary files /dev/null and b/public/plantillas/carga-trabajadores.xlsx differ diff --git a/src/app/(admins)/user/carga_masiva_usuarios/page.tsx b/src/app/(admins)/user/carga_masiva_usuarios/page.tsx new file mode 100644 index 0000000..bc80b3b --- /dev/null +++ b/src/app/(admins)/user/carga_masiva_usuarios/page.tsx @@ -0,0 +1,224 @@ +'use client'; + +import React, { useRef, useState } from 'react'; +import axiosInstance from '@/utils/api-config'; +import { Button, Form, Alert } from 'react-bootstrap'; +import { FaUpload, FaDownload } from 'react-icons/fa'; + +type TipoCarga = 'alumnos' | 'trabajadores'; + +interface ResultadoCarga { + insertados: number; + omitidos: number; +} + +interface SeccionState { + archivo: File | null; + loading: boolean; + error: string | null; + success: string | null; + resultado: ResultadoCarga | null; +} + +const initialState: SeccionState = { + archivo: null, + loading: false, + error: null, + success: null, + resultado: null, +}; + +export default function Page() { + const [alumnos, setAlumnos] = useState(initialState); + const [trabajadores, setTrabajadores] = useState(initialState); + + const alumnosRef = useRef(null); + const trabajadoresRef = useRef(null); + + const getState = (tipo: TipoCarga) => + tipo === 'alumnos' ? alumnos : trabajadores; + const setState = (tipo: TipoCarga) => + tipo === 'alumnos' ? setAlumnos : setTrabajadores; + + const validarExt = (file: File | null): boolean => { + if (!file) return false; + return /\.xlsx$/i.test(file.name); + }; + + const handleArchivoChange = (tipo: TipoCarga, file: File | null) => { + if (file && !validarExt(file)) { + setState(tipo)((prev) => ({ + ...prev, + archivo: null, + error: 'Selecciona un archivo .xlsx válido', + success: null, + })); + return; + } + setState(tipo)((prev) => ({ + ...prev, + archivo: file, + error: null, + success: null, + })); + }; + + const handleCargaMasiva = async (tipo: TipoCarga) => { + const estado = getState(tipo); + if (!estado.archivo) { + setState(tipo)((prev) => ({ + ...prev, + error: 'Selecciona un archivo xlsx', + })); + return; + } + const formData = new FormData(); + formData.append('file', estado.archivo); + setState(tipo)((prev) => ({ + ...prev, + loading: true, + error: null, + success: null, + })); + try { + const res = await axiosInstance.post(`/${tipo}/cargar`, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + setState(tipo)((prev) => ({ + ...prev, + archivo: null, + success: `${tipo === 'alumnos' ? 'Alumnos' : 'Trabajadores'} cargados exitosamente`, + resultado: res.data ?? null, + })); + const ref = tipo === 'alumnos' ? alumnosRef : trabajadoresRef; + if (ref.current) ref.current.value = ''; + } catch (error) { + const err = error as { response?: { data?: { message?: string } } }; + setState(tipo)((prev) => ({ + ...prev, + error: err?.response?.data?.message || 'Error al cargar el archivo', + })); + } finally { + setState(tipo)((prev) => ({ ...prev, loading: false })); + } + }; + + const handleDescargarPlantilla = (tipo: TipoCarga) => { + const nombre = tipo === 'alumnos' ? 'carga-alumnos' : 'carga-trabajadores'; + const link = document.createElement('a'); + link.href = `/plantillas/${nombre}.xlsx`; + link.setAttribute('download', `${nombre}.xlsx`); + document.body.appendChild(link); + link.click(); + link.remove(); + }; + + const SeccionCarga = ({ + tipo, + label, + }: { + tipo: TipoCarga; + label: string; + }) => { + const estado = getState(tipo); + const inputRef = tipo === 'alumnos' ? alumnosRef : trabajadoresRef; + + return ( +
+

{label}

+ + {estado.error && ( + setState(tipo)((p) => ({ ...p, error: null }))} + dismissible + > + {estado.error} + + )} + {estado.success && ( + + setState(tipo)((p) => ({ ...p, success: null, resultado: null })) + } + dismissible + > +

{estado.success}

+ {estado.resultado && ( +
    +
  • + Insertados: {estado.resultado.insertados} +
  • +
  • + Omitidos: {estado.resultado.omitidos} +
  • +
+ )} +
+ )} + + + Archivo Excel (.xlsx) +
inputRef.current?.click()} + > + +

+ {estado.archivo?.name || + 'Arrastra aquí tu archivo o haz click para buscar'} +

+
+ + handleArchivoChange(tipo, e.target.files?.[0] ?? null) + } + /> +
+ +
+ + + +
+
+ ); + }; + + return ( +
+
+
+
+

Carga masiva de usuarios

+

+ Sube un archivo Excel para registrar alumnos o trabajadores en el + sistema. +

+
+
+
+ + + +
+ ); +} 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 index f8fafba..3861fd1 100644 --- a/src/app/(admins)/user/evento/[id_evento]/formularios/crear/page.tsx +++ b/src/app/(admins)/user/evento/[id_evento]/formularios/crear/page.tsx @@ -1,7 +1,6 @@ '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 FormularioCardPreview from '@/components/formulario/formulario-card-preview'; @@ -193,21 +192,18 @@ export default function Page() {

-
+
{plantillasDisponibles.map((plantilla) => (
handleSeleccionarPlantilla(plantilla.id)} > -
- {`Plantilla +
+ {plantilla.nombre}
))} diff --git a/src/app/(admins)/user/evento/[id_evento]/page.tsx b/src/app/(admins)/user/evento/[id_evento]/page.tsx index 129796c..249e487 100644 --- a/src/app/(admins)/user/evento/[id_evento]/page.tsx +++ b/src/app/(admins)/user/evento/[id_evento]/page.tsx @@ -27,6 +27,40 @@ export default function Page() { {} ); + type TipoCarga = 'alumnos' | 'trabajadores'; + const [tipoCarga, setTipoCarga] = useState('alumnos'); + const [archivoCarga, setArchivoCarga] = useState(null); + const [loadingCarga, setLoadingCarga] = useState(false); + + const handleCargaMasiva = async () => { + if (!archivoCarga) { + toast.error('Selecciona un archivo xlsx'); + return; + } + const formData = new FormData(); + formData.append('file', archivoCarga); + formData.append('id_evento', params.id_evento); + setLoadingCarga(true); + try { + await axiosInstance.post( + `/evento/${params.id_evento}/usuarios`, + formData, + { + headers: { 'Content-Type': 'multipart/form-data' }, + } + ); + toast.success( + `${tipoCarga === 'alumnos' ? 'Alumnos' : 'Trabajadores'} cargados exitosamente` + ); + setArchivoCarga(null); + } catch (error) { + const err = error as { response?: { data?: { message?: string } } }; + toast.error(err?.response?.data?.message || 'Error al cargar el archivo'); + } finally { + setLoadingCarga(false); + } + }; + const handleChange = ( field: keyof CreateEventoType, value: string | Date @@ -88,8 +122,61 @@ export default function Page() { handleOnChange={handleEventoActualizado} /> + {/* Carga masiva de alumnos o trabajadores */} + {tipo_usuario !== 'staff' && ( +
+

Carga masiva de participantes

+

+ Sube un archivo .xlsx para registrar los + participantes que podrán inscribirse a este evento. Solo los + usuarios incluidos en la lista tendrán acceso al registro. +

+ +
+
+ +
+ + +
+
+ +
+ + setArchivoCarga(e.target.files?.[0] ?? null)} + /> +
+ + +
+
+ )} + {/* Validacion del tipo de usuario */} - { tipo_usuario !== "staff" && ( + {tipo_usuario !== 'staff' && (
@@ -103,7 +190,9 @@ export default function Page() { icon="plus" variant="primary" onClick={() => - router.push(`/user/evento/${params.id_evento}/formularios/crear`) + router.push( + `/user/evento/${params.id_evento}/formularios/crear` + ) } > Crear nuevo formulario diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index 8f8b3fc..84d51ee 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -29,6 +29,10 @@ const Navbar: React.FC = () => { href: '/user/eventos/carga_masiva_banner', label: 'Carga Masiva Imagenes', }, + { + href: '/user/carga_masiva_usuarios', + label: 'Carga Masiva Usuarios', + }, ], }, { href: '/user/usuarios', label: 'Usuarios' }, diff --git a/src/containers/create-formulario.tsx b/src/containers/create-formulario.tsx index eb8c981..9d92dc1 100644 --- a/src/containers/create-formulario.tsx +++ b/src/containers/create-formulario.tsx @@ -214,6 +214,13 @@ export default function FormularioEditor({
)} + actualizarCampo('mensaje_correo', e.target.value)} + placeholder="Mensaje que se enviará al correo del participante al registrarse" + /> +