Cambios sin modificar

This commit is contained in:
santiago
2025-10-07 18:48:06 +02:00
8 changed files with 954 additions and 86 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ const nextConfig: NextConfig = {
{
protocol: 'http',
hostname: 'localhost',
port: '4200',
port: '3342', //4200
},
{
protocol: 'https',
+220 -9
View File
@@ -11,13 +11,15 @@
"@uiw/react-md-editor": "^4.0.7",
"@yudiel/react-qr-scanner": "^2.2.1",
"axios": "^1.8.4",
"bootstrap": "^5.3.3",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.11.3",
"js-cookie": "^3.0.5",
"next": "15.3.3",
"react": "^19.0.0",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.0.0",
"react-hot-toast": "^2.5.2",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"xlsx": "^0.18.5"
},
@@ -891,6 +893,75 @@
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@restart/hooks": {
"version": "0.4.16",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
"integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==",
"license": "MIT",
"dependencies": {
"dequal": "^2.0.3"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@restart/ui": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.9.4.tgz",
"integrity": "sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@popperjs/core": "^2.11.8",
"@react-aria/ssr": "^3.5.0",
"@restart/hooks": "^0.5.0",
"@types/warning": "^3.0.3",
"dequal": "^2.0.3",
"dom-helpers": "^5.2.0",
"uncontrollable": "^8.0.4",
"warning": "^4.0.3"
},
"peerDependencies": {
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
}
},
"node_modules/@restart/ui/node_modules/@restart/hooks": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.1.tgz",
"integrity": "sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q==",
"license": "MIT",
"dependencies": {
"dequal": "^2.0.3"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@restart/ui/node_modules/uncontrollable": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz",
"integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.14.0"
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -1032,6 +1103,12 @@
"integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.0.12",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz",
@@ -1051,12 +1128,27 @@
"@types/react": "^19.0.0"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
"node_modules/@types/warning": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz",
"integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.29.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.0.tgz",
@@ -2002,9 +2094,9 @@
"license": "ISC"
},
"node_modules/bootstrap": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
"version": "5.3.8",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
"integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
"funding": [
{
"type": "github",
@@ -2268,6 +2360,12 @@
"node": ">= 6"
}
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT"
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -2607,6 +2705,16 @@
"node": ">=0.10.0"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -4083,6 +4191,15 @@
"node": ">= 0.4"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/is-alphabetical": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
@@ -4592,7 +4709,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@@ -4739,7 +4855,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -5791,7 +5906,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -6132,7 +6246,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@@ -6140,6 +6253,19 @@
"react-is": "^16.13.1"
}
},
"node_modules/prop-types-extra": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
"integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
"license": "MIT",
"dependencies": {
"react-is": "^16.3.2",
"warning": "^4.0.0"
},
"peerDependencies": {
"react": ">=0.14.0"
}
},
"node_modules/property-information": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
@@ -6196,6 +6322,37 @@
"node": ">=0.10.0"
}
},
"node_modules/react-bootstrap": {
"version": "2.10.10",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.10.tgz",
"integrity": "sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.24.7",
"@restart/hooks": "^0.4.9",
"@restart/ui": "^1.9.4",
"@types/prop-types": "^15.7.12",
"@types/react-transition-group": "^4.4.6",
"classnames": "^2.3.2",
"dom-helpers": "^5.2.1",
"invariant": "^2.2.4",
"prop-types": "^15.8.1",
"prop-types-extra": "^1.1.0",
"react-transition-group": "^4.4.5",
"uncontrollable": "^7.2.1",
"warning": "^4.0.3"
},
"peerDependencies": {
"@types/react": ">=16.14.8",
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-dom": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
@@ -6225,11 +6382,25 @@
"react-dom": ">=16"
}
},
"node_modules/react-icons": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
"license": "MIT",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT"
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
"license": "MIT"
},
"node_modules/react-markdown": {
@@ -6259,6 +6430,22 @@
"react": ">=18"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -7509,6 +7696,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/uncontrollable": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
"integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.6.3",
"@types/react": ">=16.9.11",
"invariant": "^2.2.4",
"react-lifecycles-compat": "^3.0.4"
},
"peerDependencies": {
"react": ">=15.0.0"
}
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
@@ -7693,6 +7895,15 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/web-namespaces": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
+3 -1
View File
@@ -14,13 +14,15 @@
"@uiw/react-md-editor": "^4.0.7",
"@yudiel/react-qr-scanner": "^2.2.1",
"axios": "^1.8.4",
"bootstrap": "^5.3.3",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.11.3",
"js-cookie": "^3.0.5",
"next": "15.3.3",
"react": "^19.0.0",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.0.0",
"react-hot-toast": "^2.5.2",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"xlsx": "^0.18.5"
},
@@ -0,0 +1,133 @@
'use client'
import axiosInstance from "@/utils/api-config";
import React, { useState } from "react";
import { Button, Form, ProgressBar } from "react-bootstrap";
import { FaUpload } from "react-icons/fa";
export default function CargaMasiva() {
const [xlsx, setXlsx] = useState<File | null>(null);
const [progress, setProgress] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(false);
const validarExt = (file: File | null) => {
if (!file) return;
const extPermitidas = /(.xlsx)$/i;
if (!extPermitidas.exec(file.name)) {
console.error("Asegúrate de ingresar un archivo .xlsx");
setXlsx(null);
} else {
setXlsx(file);
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] || null;
validarExt(file);
};
const enviarCargaMasiva = async () => {
if (!xlsx) return;
const formData = new FormData();
formData.append("file", xlsx);
try {
setLoading(true);
setProgress(0);
const res = await axiosInstance.post(
"/cuestionario/carga-masiva",
formData,
{
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: (event) => {
if (event.total) {
const porcentaje = Math.round(
(event.loaded * 100) / event.total
);
setProgress(porcentaje);
}
},
}
);
//Falta hacer validacion
console.log("Carga masiva exitosa", res.data);
setTimeout(() => {
setProgress(100);
setLoading(false);
}, 500);
} catch (error) {
console.error("Carga masiva fallida", error);
setLoading(false);
setProgress(0);
}
};
return (
<div className="container-fluid my-4">
<div className="row mb-4">
<div className="col-12">
<div className="p-4 border rounded bg-light">
<h2 className="mb-2">Carga Masiva de Eventos</h2>
<p className="mb-0 text-muted">
Sube tu archivo excel para poder crear los eventos.
</p>
</div>
</div>
</div>
<Form.Group className="mb-3">
<Form.Label>Archivo Excel</Form.Label>
<div
className="border p-4 text-center rounded"
style={{ cursor: "pointer" }}
onClick={() => document.getElementById("xlsxInput")?.click()}
>
<FaUpload size={40} className="mb-2" />
<p className="mb-1">
{xlsx?.name ||
"Arrastra aquí tu archivo o da click aquí para buscar"}
</p>
<p className="is-size-6">Tamaño máximo 20MB</p>
<p className="is-size-7">
Si al momento de elegir un archivo este no se selecciona,
haga click en cancelar en la ventana emergente e intente de
nuevo.
</p>
</div>
<input
id="xlsxInput"
type="file"
accept=".xlsx"
style={{ display: "none" }}
onChange={handleFileChange}
/>
</Form.Group>
<div className="d-flex gap-2 mt-3">
<Button
variant="outline-primary"
disabled={!xlsx || loading}
onClick={enviarCargaMasiva}
>
{loading ? "Subiendo..." : "Enviar archivo"}
</Button>
</div>
{/* Barra de progreso */}
{loading && (
<div className="mt-3">
<ProgressBar
now={progress}
label={`${progress}%`}
animated
striped
variant="info"
/>
</div>
)}
</div>
);
}
@@ -0,0 +1,161 @@
"use client";
import React, { useState } from "react";
import { Button, Image } from "react-bootstrap";
import { FaUpload } from "react-icons/fa";
import axiosInstance from "@/utils/api-config";
//Para poder subir las imagenes
interface UploadedImage {
originalName: string;
filename: string;
path: string;
}
export default function BannerUpload() {
const [files, setFiles] = useState<File[]>([]);
const [loading, setLoading] = useState(false);
const [uploaded, setUploaded] = useState<UploadedImage[]>([]);
const [progress, setProgress] = useState(0);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setFiles(Array.from(e.target.files));
}
};
const handleUpload = async () => {
if (files.length === 0) {
alert("Selecciona al menos un archivo");
return;
}
const formData = new FormData();
files.forEach((file) => {
formData.append("banners", file);
});
try {
setLoading(true);
setProgress(0);
const res = await axiosInstance.post("/cuestionario/banners/bulk", formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
setProgress(
Math.round((progressEvent.loaded * 100) / progressEvent.total)
);
}
},
});
//Falta hacer validacion
setUploaded(res.data);
alert("Banners subidos correctamente ✅");
setFiles([]);
} catch (error) {
console.error(error);
alert(error || "Error al subir archivos");
} finally {
setLoading(false);
}
};
return (
<div className="container-fluid my-4">
<div className="row mb-4">
<div className="col-12">
<div className="p-4 border rounded bg-light">
<h2 className="mb-2">Subir Banners</h2>
<p className="mb-0 text-muted">
Arrastra o selecciona imágenes para subir tus banners (jpg, png, webp).
</p>
</div>
</div>
</div>
{/* Área Drag & Drop */}
<div
className="border p-4 text-center rounded"
style={{ cursor: "pointer" }}
onClick={() => document.getElementById("bannerInput")?.click()}
>
<FaUpload size={40} className="mb-2 text-muted" />
<p className="mb-1">
Arrastra aquí tus imágenes o haz clic para buscar
</p>
<p className="is-size-6">Tamaño máximo 20MB</p>
</div>
<input
id="bannerInput"
type="file"
accept=".jpg,.jpeg,.png,.webp"
multiple
style={{ display: "none" }}
onChange={handleFileChange}
/>
{/* Previsualización */}
{files.length > 0 && (
<div className="d-flex flex-wrap gap-3 mt-4">
{files.map((file, i) => (
<div
key={i}
className="border rounded overflow-hidden shadow-sm"
style={{ width: "350px", height: "350px" }}
>
<Image
src={URL.createObjectURL(file)}
alt={file.name}
className="img-fluid h-75 w-100 object-fit-cover"
/>
<p className="small text-center bg-light p-2 m-0">{file.name}</p>
</div>
))}
</div>
)}
{/* Botón */}
<div className="d-flex gap-2 mt-3">
<Button
variant="outline-primary"
disabled={loading}
onClick={handleUpload}
>
{loading ? "Subiendo..." : "Subir Banners"}
</Button>
</div>
{/* Barra de progreso */}
{loading && (
<div className="progress mt-3" style={{ height: "8px" }}>
<div
className="progress-bar progress-bar-striped bg-primary"
role="progressbar"
style={{ width: `${progress}%` }}
></div>
</div>
)}
{/* Archivos subidos */}
{uploaded.length > 0 && (
<div className="mt-4">
<h5 className="mb-2 text-muted">Archivos subidos:</h5>
<ul className="list-group">
{uploaded.map((file, i) => (
<li key={i} className="list-group-item d-flex justify-content-between align-items-center">
<span>{file.originalName}</span>
<a href={`/${file.path}`} target="_blank" className="text-primary">
Ver
</a>
</li>
))}
</ul>
</div>
)}
</div>
);
}
+70 -50
View File
@@ -23,34 +23,37 @@ export default function Page() {
console.log('QR Escaneado:', rawValue);
if (!enableScan) return;
try {
if (rawValue) {
try {
await axiosInstance.post(
`/participante-evento/decode-token`, {
token: rawValue
}
);
if (!rawValue) {
setStatusMessage('⚠️ El QR no contiene los campos requeridos');
return;
}
setToken(rawValue);
setTokenValid(true);
setShowModal(true);
setEnableScan(false);
} catch (err) {
console.warn('Participante no registrado en el cuestionario:', err);
setStatusMessage(
'❌ El participante no está registrado en este evento.'
);
}
try {
const response = await axiosInstance.post('/participante-evento/decode-token', { token: rawValue });
if (response.data) {
// Token válido
setToken(rawValue);
setTokenValid(true);
setShowModal(true);
setEnableScan(false);
} else {
setStatusMessage('⚠️ El QR no contiene los campos requeridos');
// Token caducado o cualquier otro error
setStatusMessage(`❌ Error el codigo qr ya caduco`);
setTokenValid(false);
setShowModal(true); // mostramos modal con el mensaje de error
setEnableScan(false);
}
} catch (err) {
console.error('QR malformado:', err);
setStatusMessage('❌ Error al leer el QR: formato inválido');
console.error('Error al decodificar el QR:', err);
setStatusMessage('❌ Error al leer el QR');
setTokenValid(false);
setShowModal(true);
setEnableScan(false);
}
};
const handleConfirm = async () => {
if (!tokenValid) return;
@@ -61,10 +64,17 @@ export default function Page() {
}
);
setParticipante(response.data.data.participante);
setMessage(response.data.message);
console.log('Asistencia registrada:', response.data);
toast.success('✅ Asistencia registrada correctamente');
//Validacion
if(response.data.success === false) {
console.error("El usuario ya se registro");
setMessage(response.data.message);
toast.error("❌ Error el usaurio ya se registro previamente.")
} else {
setParticipante(response.data.data.participante);
setMessage(response.data.message);
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');
@@ -229,40 +239,50 @@ export default function Page() {
<div className="modal-body p-4">
{tokenValid ? (
<>
<div className="d-grid gap-2 d-md-flex justify-content-center">
// Modal de confirmación de asistencia
<div className="d-grid gap-2 d-md-flex justify-content-center">
<Button
variant="success"
onClick={handleConfirm}
className="px-4 py-2 rounded-pill fw-semibold"
>
<i className="bi bi-check2 me-2"></i>
Confirmar Asistencia
</Button>
<Button
variant="outline-secondary"
onClick={() => {
setShowModal(false);
setEnableScan(true);
setTokenValid(false);
setToken('');
}}
className="px-4 py-2 rounded-pill"
>
<i className="bi bi-x-lg me-2"></i>
Cancelar
</Button>
</div>
) : (
// Modal de error
<div className="py-4">
<div className="mb-3">
<i className="bi bi-exclamation-triangle-fill text-warning" style={{ fontSize: '2.5rem' }}></i>
</div>
<h6 className="text-danger mb-0">{statusMessage}</h6>
<div className="mt-3">
<Button
variant="success"
onClick={handleConfirm}
className="px-4 py-2 rounded-pill fw-semibold"
>
<i className="bi bi-check2 me-2"></i>
Confirmar Asistencia
</Button>
<Button
variant="outline-secondary"
variant="secondary"
onClick={() => {
setShowModal(false);
setEnableScan(true);
setTokenValid(false);
setToken('');
setStatusMessage('');
}}
className="px-4 py-2 rounded-pill"
>
<i className="bi bi-x-lg me-2"></i>
Cancelar
Cerrar
</Button>
</div>
</>
) : (
<div className="py-4">
<div className="mb-3">
<i
className="bi bi-exclamation-triangle-fill text-warning"
style={{ fontSize: '2.5rem' }}
></i>
</div>
<h6 className="text-danger mb-0">QR inválido o malformado</h6>
</div>
)}
</div>
+313 -6
View File
@@ -1,12 +1,158 @@
'use client';
import Button from '@/components/button';
import Table, { Header } from '@/components/table';
const Modal = dynamic(() => import('@/components/modal'), { ssr: false });
import { useGetApi } from '@/hooks/use-get-api';
import { Administrador } from '@/types/administradores';
import React from 'react';
import React, { useState } from 'react';
import axiosInstance from '@/utils/api-config';
import dynamic from 'next/dynamic';
export default function Page() {
const { data } = useGetApi<Administrador[]>('/administrador');
const { data, setData } = useGetApi<Administrador[]>('/administrador');
// Estado del modal
const [isModalVisible, setIsModalVisible] = useState(false);
const [modalMode, setModalMode] = useState<'add' | 'edit'>('add');
const [selectedUser, setSelectedUser] = useState<Administrador | null>(null);
// Estado del modal de eliminación
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const [userToDelete, setUserToDelete] = useState<Administrador | null>(null);
// Estado del formulario
const [formData, setFormData] = useState({
nombre_usuario: '',
correo: '',
password: '',
id_tipo_user: '',
});
// Handlers del modal
const handleOpenAddModal = () => {
setModalMode('add');
setSelectedUser(null);
setFormData({
nombre_usuario: '',
correo: '',
password: '',
id_tipo_user: '',
});
setIsModalVisible(true);
};
const handleOpenEditModal = (user: Administrador) => {
setModalMode('edit');
setSelectedUser(user);
setFormData({
nombre_usuario: user.nombre_usuario,
correo: user.correo,
password: '',
id_tipo_user: user.tipoUser.id.toString(),
});
setIsModalVisible(true);
};
const handleCloseModal = () => {
setIsModalVisible(false);
setSelectedUser(null);
};
// Handlers del modal de eliminación
const handleOpenDeleteModal = (user: Administrador) => {
setUserToDelete(user);
setIsDeleteModalVisible(true);
};
const handleCloseDeleteModal = () => {
setIsDeleteModalVisible(false);
setUserToDelete(null);
};
const handleConfirmDelete = async () => {
// Aquí iría la lógica para eliminar el usuario
try {
if (userToDelete) {
// Lógica para eliminar el usuario
const response = await axiosInstance.delete(
`/administrador/${userToDelete.id_administrador}`
);
console.log('Usuario eliminado:', response.data);
// Actualizar la lista de usuarios en el estado
if (data) {
const updatedData = data.filter(
(user) => user.id_administrador !== userToDelete.id_administrador
);
setData(updatedData);
}
}
} catch (error) {
console.error('Error al eliminar el usuario:', error);
}
handleCloseDeleteModal();
};
// Handler para envío del formulario
const handleSubmitForm = async () => {
try {
if (modalMode === 'add') {
// Crear nuevo usuario
const payload = {
nombre_usuario: formData.nombre_usuario,
correo: formData.correo,
password: formData.password,
id_tipo_user: parseInt(formData.id_tipo_user),
};
const response = await axiosInstance.post('/administrador', payload);
console.log('Usuario creado:', response.data);
// Actualizar la lista de usuarios
if (data) {
setData([
...data,
{
correo: formData.correo,
id_administrador: response.data.id_administrador,
nombre_usuario: formData.nombre_usuario,
tipoUser: {
id: parseInt(formData.id_tipo_user),
tipo: formData.id_tipo_user === '1' ? 'Administrador' : 'Staff',
},
},
]);
}
} else if (modalMode === 'edit' && selectedUser) {
// Editar usuario existente
const payload = {
nombre_usuario: formData.nombre_usuario,
correo: formData.correo,
id_tipo_user: parseInt(formData.id_tipo_user),
};
const response = await axiosInstance.put(
`/administrador/${selectedUser.id_administrador}`,
payload
);
console.log('Usuario actualizado:', response.data);
// Actualizar la lista de usuarios
if (data) {
const updatedData = data.map((user) =>
user.id_administrador === selectedUser.id_administrador
? { ...user, ...response.data }
: user
);
setData(updatedData);
}
}
handleCloseModal();
} catch (error) {
console.error('Error al guardar el usuario:', error);
}
};
const headers: Header<Administrador>[] = [
{ key: 'id_administrador', label: 'ID' },
@@ -19,12 +165,16 @@ export default function Page() {
},
{
key: 'tipoUser',
render: () => (
render: (_, row) => (
<div className="d-flex gap-2">
<Button className="w-100" disabled>
<Button className="w-100" onClick={() => handleOpenEditModal(row)}>
Editar
</Button>
<Button variant="danger" className="w-100" disabled>
<Button
variant="danger"
className="w-100"
onClick={() => handleOpenDeleteModal(row)}
>
Eliminar
</Button>
</div>
@@ -37,9 +187,166 @@ export default function Page() {
<div>
<div className="d-flex justify-content-between align-items-center my-3">
<h1>Administradores</h1>
<Button variant="primary">Agregar Administrador</Button>
<Button variant="primary" onClick={handleOpenAddModal}>
Agregar Administrador
</Button>
</div>
{data && <Table headers={headers} data={data} />}
<Modal
isVisible={isModalVisible}
onClose={handleCloseModal}
size="lg"
closeButton={true}
>
<div className="p-3">
<h3 className="mb-4">
{modalMode === 'add'
? 'Agregar Nuevo Administrador'
: 'Editar Administrador'}
</h3>
{modalMode === 'edit' && selectedUser && (
<div className="mb-3">
<p>
<strong>ID:</strong> {selectedUser.id_administrador}
</p>
<p>
<strong>Usuario actual:</strong> {selectedUser.nombre_usuario}
</p>
<p>
<strong>Correo actual:</strong> {selectedUser.correo}
</p>
<p>
<strong>Tipo:</strong> {selectedUser.tipoUser.tipo}
</p>
</div>
)}
<div className="mb-3">
<label htmlFor="nombre_usuario" className="form-label">
Nombre de Usuario
</label>
<input
type="text"
className="form-control"
id="nombre_usuario"
value={formData.nombre_usuario}
onChange={(e) =>
setFormData((prev) => ({
...prev,
nombre_usuario: e.target.value,
}))
}
placeholder="Ingrese el nombre de usuario"
/>
</div>
<div className="mb-3">
<label htmlFor="correo" className="form-label">
Correo Electrónico
</label>
<input
type="email"
className="form-control"
id="correo"
value={formData.correo}
onChange={(e) =>
setFormData((prev) => ({
...prev,
correo: e.target.value,
}))
}
placeholder="Ingrese el correo electrónico"
/>
</div>
{modalMode === 'add' && (
<div className="mb-3">
<label htmlFor="password" className="form-label">
Contraseña
</label>
<input
type="password"
className="form-control"
id="password"
value={formData.password}
onChange={(e) =>
setFormData((prev) => ({
...prev,
password: e.target.value,
}))
}
placeholder="Ingrese la contraseña"
/>
</div>
)}
<div className="mb-4">
<label htmlFor="tipoUser" className="form-label">
Tipo de Usuario
</label>
<select
className="form-select"
id="tipoUser"
value={formData.id_tipo_user}
onChange={(e) =>
setFormData((prev) => ({
...prev,
id_tipo_user: e.target.value,
}))
}
>
<option value="">Seleccione un tipo</option>
<option value="1">Administrador</option>
<option value="2">Staff</option>
</select>
</div>
<div className="d-flex gap-2 justify-content-end">
<Button variant="secondary" onClick={handleCloseModal}>
Cancelar
</Button>
<Button variant="primary" onClick={handleSubmitForm}>
{modalMode === 'add' ? 'Crear Administrador' : 'Guardar Cambios'}
</Button>
</div>
</div>
</Modal>
{/* Modal de confirmación de eliminación */}
<Modal
isVisible={isDeleteModalVisible}
onClose={handleCloseDeleteModal}
size="md"
closeButton={true}
>
<div className="p-4 text-center">
<div className="mb-4">
<i className="bi bi-exclamation-triangle text-warning display-1"></i>
</div>
<h4 className="mb-3">¿Está seguro de eliminar al usuario?</h4>
{userToDelete && (
<div className="mb-4">
<p className="fs-5 fw-bold text-danger">
{userToDelete.nombre_usuario}
</p>
<p className="text-muted">Esta acción no se puede deshacer</p>
</div>
)}
<div className="d-flex gap-3 justify-content-center">
<Button variant="secondary" onClick={handleCloseDeleteModal}>
Cancelar
</Button>
<Button variant="danger" onClick={handleConfirmDelete}>
, Eliminar
</Button>
</div>
</div>
</Modal>
</div>
);
}
+53 -19
View File
@@ -3,26 +3,43 @@ import React from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
type NavChild = {
href: string; // siempre string
label: string;
};
type NavItem =
| { href: string; label: string } // items normales
| { label: string; children: NavChild[] } // items con submenu
const Navbar: React.FC = () => {
const pathname = usePathname();
const tipo_usuario = sessionStorage.getItem('axxxd')?.replace(/"/g, '') || '';
const navItemsAdmin = [
const navItemsAdmin: NavItem[] = [
{ href: '/', label: 'Inicio' },
{ href: '/user/eventos', label: 'Eventos' },
{ href: '/user/qr', label: 'Lector de QRs' },
{ href: '/user/eventos/crear', label: 'Crear Evento' },
{
label: 'Carga Masiva',
children: [
{ href: '/user/eventos/carga_masiva', label: 'Carga Masiva Eventos' },
{ href: '/user/eventos/carga_masiva_banner', label: 'Carga Masiva Imagenes' },
],
},
{ href: '/user/usuarios', label: 'Usuarios' },
{ href: '/login', label: 'Cerrar sesión' },
];
const navItemsUser = [
const navItemsUser: NavItem[] = [
{ href: '/user/eventos', label: 'Eventos' },
{ href: '/user/qr', label: 'Lector de QRs' },
{ href: '/login', label: 'Cerrar sesión' },
];
const navItems = tipo_usuario === "administrador" ? navItemsAdmin : navItemsUser;
const navItems = tipo_usuario === 'administrador' ? navItemsAdmin : navItemsUser;
return (
<nav className="navbar navbar-expand-lg navbar-dark bg-azul">
@@ -54,24 +71,41 @@ const Navbar: React.FC = () => {
></button>
</div>
<div className="offcanvas-body align-items-center justify-content-between">
<h2 className="h5 mb-0 text-white">
Sistema de Registro de Eventos
</h2>
<h2 className="h5 mb-0 text-white">Sistema de Registro de Eventos</h2>
<ul className="navbar-nav gap-lg-5">
{navItems.map((item, index) => (
<li
key={index}
className={`nav-item`}
data-bs-dismiss="offcanvas"
>
<Link
className={`nav-link ${
pathname === item.href ? 'text-white' : ''
}`}
href={item.href}
>
{item.label}
</Link>
<li key={index} className="nav-item" data-bs-dismiss="offcanvas">
{'href' in item ? (
<Link
className={`nav-link ${
pathname === item.href ? 'text-white' : ''
}`}
href={item.href}
>
{item.label}
</Link>
) : (
<div className="nav-item dropdown">
<a
className="nav-link dropdown-toggle"
href="#"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{item.label}
</a>
<ul className="dropdown-menu">
{item.children.map((child: NavChild, idx: number) => (
<li key={idx}>
<Link className="dropdown-item" href={child.href}>
{child.label}
</Link>
</li>
))}
</ul>
</div>
)}
</li>
))}
</ul>