Respuestas y escaneo
This commit is contained in:
Generated
+101
-14
@@ -8,6 +8,7 @@
|
||||
"name": "formularios_front",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
@@ -1962,6 +1963,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/available-typed-arrays": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||
@@ -1988,6 +1995,17 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
||||
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
@@ -2098,7 +2116,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -2239,6 +2256,18 @@
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -2390,6 +2419,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||
@@ -2417,7 +2455,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
@@ -2505,7 +2542,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -2515,7 +2551,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -2553,7 +2588,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
@@ -2566,7 +2600,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -3173,6 +3206,26 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||
@@ -3189,11 +3242,25 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -3234,7 +3301,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
@@ -3259,7 +3325,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
@@ -3347,7 +3412,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -3419,7 +3483,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -3432,7 +3495,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
@@ -3448,7 +3510,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
@@ -4103,7 +4164,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -4133,6 +4193,27 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -4560,6 +4641,12 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
+150
-123
@@ -2,23 +2,13 @@
|
||||
import React, { useState } from 'react';
|
||||
import Pagination from '@/components/pagination';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface Opcion {
|
||||
valor: string;
|
||||
}
|
||||
|
||||
interface Pregunta {
|
||||
titulo: string;
|
||||
tipo: 'Abierta' | 'Cerrada' | 'Multiple';
|
||||
opciones: Opcion[];
|
||||
obligatoria: boolean;
|
||||
}
|
||||
|
||||
interface Seccion {
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
preguntas: Pregunta[];
|
||||
}
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import Select from '@/components/select';
|
||||
import Input from '@/components/input';
|
||||
import Button from '@/components/button';
|
||||
import FloatingInput from '@/components/floating-input';
|
||||
import { Pregunta, Seccion } from '@/types/cuestionario';
|
||||
import { plantillasDisponibles } from '@/data/plantillas';
|
||||
|
||||
export default function AdminFormPage() {
|
||||
const [nombreFormulario, setNombreFormulario] = useState('');
|
||||
@@ -26,9 +16,9 @@ export default function AdminFormPage() {
|
||||
const [secciones, setSecciones] = useState<Seccion[]>([]);
|
||||
const [paginaActual, setPaginaActual] = useState(1);
|
||||
const [bannerPreview, setBannerPreview] = useState<string | null>(null);
|
||||
const [audiencia, setAudiencia] = useState<'interna' | 'externa' | 'ambas'>(
|
||||
'interna'
|
||||
);
|
||||
const [tipoCuestionario, setTipoCuestionario] = useState<number | null>(null);
|
||||
const [fechaInicio, setFechaInicio] = useState<string | null>(null);
|
||||
const [fechaFin, setFechaFin] = useState<string | null>(null);
|
||||
|
||||
const swapItems = <T,>(arr: T[], index1: number, index2: number): T[] => {
|
||||
const result = [...arr];
|
||||
@@ -115,7 +105,11 @@ export default function AdminFormPage() {
|
||||
};
|
||||
const agregarOpcion = (seccionIndex: number, preguntaIndex: number) => {
|
||||
const nuevas = [...secciones];
|
||||
nuevas[seccionIndex].preguntas[preguntaIndex].opciones.push({ valor: '' });
|
||||
if (nuevas[seccionIndex]?.preguntas[preguntaIndex]?.opciones) {
|
||||
nuevas[seccionIndex].preguntas[preguntaIndex].opciones.push({
|
||||
valor: '',
|
||||
});
|
||||
}
|
||||
setSecciones(nuevas);
|
||||
};
|
||||
|
||||
@@ -125,7 +119,7 @@ export default function AdminFormPage() {
|
||||
opcionIndex: number
|
||||
) => {
|
||||
const nuevas = [...secciones];
|
||||
nuevas[seccionIndex].preguntas[preguntaIndex].opciones.splice(
|
||||
nuevas[seccionIndex]?.preguntas[preguntaIndex]?.opciones?.splice(
|
||||
opcionIndex,
|
||||
1
|
||||
);
|
||||
@@ -139,19 +133,45 @@ export default function AdminFormPage() {
|
||||
valor: string
|
||||
) => {
|
||||
const nuevas = [...secciones];
|
||||
nuevas[seccionIndex].preguntas[preguntaIndex].opciones[opcionIndex].valor =
|
||||
valor;
|
||||
if (
|
||||
nuevas[seccionIndex]?.preguntas[preguntaIndex]?.opciones &&
|
||||
nuevas[seccionIndex].preguntas[preguntaIndex].opciones[opcionIndex]
|
||||
) {
|
||||
nuevas[seccionIndex].preguntas[preguntaIndex].opciones[
|
||||
opcionIndex
|
||||
].valor = valor;
|
||||
}
|
||||
setSecciones(nuevas);
|
||||
};
|
||||
|
||||
const guardarFormulario = () => {
|
||||
const guardarFormulario = async () => {
|
||||
if (fechaInicio === null || fechaFin === null) {
|
||||
alert('Las fechas son obligatorias');
|
||||
return;
|
||||
}
|
||||
|
||||
const formatearFecha = (fecha: string, tipo: 'inicio' | 'fin'): string => {
|
||||
const sufijo = tipo === 'inicio' ? 'T00:00:01' : 'T23:59:59';
|
||||
return `${fecha}${sufijo}`;
|
||||
};
|
||||
|
||||
const formData = {
|
||||
nombre: nombreFormulario,
|
||||
nombre_form: nombreFormulario,
|
||||
id_tipo_cuestionario: tipoCuestionario,
|
||||
fecha_inicio: formatearFecha(fechaInicio, 'inicio'),
|
||||
fecha_fin: formatearFecha(fechaFin, 'fin'),
|
||||
descripcion,
|
||||
secciones,
|
||||
};
|
||||
console.log('Formulario guardado:', JSON.stringify(formData));
|
||||
alert('Formulario guardado en consola');
|
||||
|
||||
console.log('Formulario a guardar:', formData);
|
||||
|
||||
try {
|
||||
const res = await axiosInstance.post('/cuestionario', formData);
|
||||
console.log('Formulario guardado:', res.data);
|
||||
} catch (error) {
|
||||
console.error('Error al guardar el formulario:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const seccionActual = secciones[paginaActual - 1];
|
||||
@@ -167,20 +187,51 @@ export default function AdminFormPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const imprimirFormulario = () => {
|
||||
const data = {
|
||||
banner: bannerPreview,
|
||||
nombreFormulario,
|
||||
descripcion,
|
||||
secciones,
|
||||
};
|
||||
console.log('Formulario a imprimir:', JSON.stringify(data));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className='my-4'>Crear nuevo formulario</h2>
|
||||
|
||||
<Select
|
||||
label='Usar una plantilla'
|
||||
options={[
|
||||
{ label: 'Selecciona una plantilla', value: '' },
|
||||
...plantillasDisponibles.map((p) => ({
|
||||
label: p.nombre,
|
||||
value: p.id,
|
||||
})),
|
||||
]}
|
||||
onChange={(e) => {
|
||||
const plantilla = plantillasDisponibles.find(
|
||||
(p) => p.id === e.target.value
|
||||
);
|
||||
if (plantilla) {
|
||||
const {
|
||||
nombre_form,
|
||||
descripcion,
|
||||
fecha_inicio,
|
||||
fecha_fin,
|
||||
id_tipo_cuestionario,
|
||||
secciones,
|
||||
} = plantilla.datos;
|
||||
setNombreFormulario(nombre_form);
|
||||
setDescripcion(descripcion);
|
||||
setFechaInicio(fecha_inicio.slice(0, 10)); // YYYY-MM-DD
|
||||
setFechaFin(fecha_fin.slice(0, 10));
|
||||
setTipoCuestionario(id_tipo_cuestionario);
|
||||
setSecciones(
|
||||
secciones.map((seccion) => ({
|
||||
...seccion,
|
||||
preguntas: seccion.preguntas.map((pregunta) => ({
|
||||
...pregunta,
|
||||
tipo: pregunta.tipo as 'Abierta' | 'Cerrada' | 'Multiple',
|
||||
})),
|
||||
}))
|
||||
);
|
||||
setPaginaActual(1);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className='mb-4'>
|
||||
<label className='form-label'>Banner del formulario</label>
|
||||
<input
|
||||
@@ -206,79 +257,58 @@ export default function AdminFormPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='mb-4'>
|
||||
<label className='form-label d-block'>Audiencia del formulario</label>
|
||||
<div className='form-check form-check-inline'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='radio'
|
||||
name='audiencia'
|
||||
id='audienciaInterna'
|
||||
value='interna'
|
||||
checked={audiencia === 'interna'}
|
||||
onChange={() => setAudiencia('interna')}
|
||||
/>
|
||||
<label className='form-check-label' htmlFor='audienciaInterna'>
|
||||
Interna (facultad)
|
||||
</label>
|
||||
</div>
|
||||
<div className='form-check form-check-inline'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='radio'
|
||||
name='audiencia'
|
||||
id='audienciaExterna'
|
||||
value='externa'
|
||||
checked={audiencia === 'externa'}
|
||||
onChange={() => setAudiencia('externa')}
|
||||
/>
|
||||
<label className='form-check-label' htmlFor='audienciaExterna'>
|
||||
Externa
|
||||
</label>
|
||||
</div>
|
||||
<div className='form-check form-check-inline'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='radio'
|
||||
name='audiencia'
|
||||
id='audienciaAmbas'
|
||||
value='ambas'
|
||||
checked={audiencia === 'ambas'}
|
||||
onChange={() => setAudiencia('ambas')}
|
||||
/>
|
||||
<label className='form-check-label' htmlFor='audienciaAmbas'>
|
||||
Ambas
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
label='Tipo de cuestionario'
|
||||
options={[
|
||||
{ label: 'Seleccione un tipo', value: '' },
|
||||
{ label: 'Tipo 1', value: 1 },
|
||||
{ label: 'Tipo 2', value: 2 },
|
||||
{ label: 'Tipo 3', value: 3 },
|
||||
]}
|
||||
value={tipoCuestionario?.toString() || ''}
|
||||
onChange={(e) => setTipoCuestionario(Number(e.target.value))}
|
||||
/>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>Nombre del formulario</label>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
value={nombreFormulario}
|
||||
onChange={(e) => setNombreFormulario(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label='Nombre del formulario'
|
||||
value={nombreFormulario}
|
||||
onChange={(e) => setNombreFormulario(e.target.value)}
|
||||
placeholder='Ingrese el nombre del formulario'
|
||||
/>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>Descripción</label>
|
||||
<textarea
|
||||
className='form-control'
|
||||
className='form-control bg-white'
|
||||
rows={3}
|
||||
value={descripcion}
|
||||
onChange={(e) => setDescripcion(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='row'>
|
||||
<div className='col-md-6'>
|
||||
<Input
|
||||
label='Fecha de inicio'
|
||||
type='date'
|
||||
value={fechaInicio || ''}
|
||||
onChange={(e) => setFechaInicio(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-md-6'>
|
||||
<Input
|
||||
label='Fecha de fin'
|
||||
type='date'
|
||||
value={fechaFin || ''}
|
||||
onChange={(e) => setFechaFin(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-4'>
|
||||
<button
|
||||
className='btn btn-outline-primary mb-3'
|
||||
onClick={agregarSeccion}
|
||||
>
|
||||
+ Agregar sección
|
||||
</button>
|
||||
<Button outline onClick={agregarSeccion} icon='plus'>
|
||||
Agregar Sección
|
||||
</Button>
|
||||
|
||||
{seccionActual && (
|
||||
<div className='border rounded p-3 mb-4'>
|
||||
@@ -310,14 +340,14 @@ export default function AdminFormPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type='text'
|
||||
className='form-control mb-2'
|
||||
placeholder='Título de la sección'
|
||||
<FloatingInput
|
||||
label='Título de la sección'
|
||||
className={{ container: 'mb-2' }}
|
||||
value={seccionActual.titulo}
|
||||
onChange={(e) =>
|
||||
actualizarSeccion(paginaActual - 1, 'titulo', e.target.value)
|
||||
}
|
||||
placeholder='Título de la sección'
|
||||
/>
|
||||
<textarea
|
||||
className='form-control mb-3'
|
||||
@@ -363,10 +393,9 @@ export default function AdminFormPage() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control my-2'
|
||||
placeholder='Texto de la pregunta'
|
||||
<Input
|
||||
label='Texto de la pregunta'
|
||||
className={{ container: 'my-2' }}
|
||||
value={pregunta.titulo}
|
||||
onChange={(e) =>
|
||||
actualizarPregunta(
|
||||
@@ -376,9 +405,16 @@ export default function AdminFormPage() {
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
placeholder='Texto de la pregunta'
|
||||
/>
|
||||
<select
|
||||
className='form-select mb-2'
|
||||
|
||||
<Select
|
||||
label='Tipo de pregunta'
|
||||
options={[
|
||||
{ label: 'Abierta', value: 'Abierta' },
|
||||
{ label: 'Cerrada', value: 'Cerrada' },
|
||||
{ label: 'Opción múltiple', value: 'Multiple' },
|
||||
]}
|
||||
value={pregunta.tipo}
|
||||
onChange={(e) =>
|
||||
actualizarPregunta(
|
||||
@@ -388,11 +424,7 @@ export default function AdminFormPage() {
|
||||
e.target.value as Pregunta['tipo']
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value='Abierta'>Abierta</option>
|
||||
<option value='Cerrada'>Cerrada</option>
|
||||
<option value='Multiple'>Opción múltiple</option>
|
||||
</select>
|
||||
/>
|
||||
|
||||
<div className='form-check form-switch mb-3'>
|
||||
<input
|
||||
@@ -426,11 +458,11 @@ export default function AdminFormPage() {
|
||||
>
|
||||
+ Agregar opción
|
||||
</button>
|
||||
{pregunta.opciones.map((op, oIdx) => (
|
||||
{(pregunta.opciones ?? []).map((op, oIdx) => (
|
||||
<div key={oIdx} className='input-group mb-2'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
className='form-control bg-white'
|
||||
placeholder={`Opción ${oIdx + 1}`}
|
||||
value={op.valor}
|
||||
onChange={(e) =>
|
||||
@@ -477,14 +509,9 @@ export default function AdminFormPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button className='btn btn-success' onClick={guardarFormulario}>
|
||||
<Button onClick={guardarFormulario} color='success'>
|
||||
Guardar formulario
|
||||
</button>
|
||||
<div className='my-4 d-flex gap-2'>
|
||||
<button className='btn btn-success' onClick={imprimirFormulario}>
|
||||
Imprimir cuestionario
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<nav className='d-flex gap-2 my-4'>
|
||||
<Link href={'/administrador/'} className='text-decoration-none'>
|
||||
<div className='box'>Registros</div>
|
||||
</Link>
|
||||
<Link
|
||||
href={'/administrador/crear-formulario'}
|
||||
className='text-decoration-none'
|
||||
>
|
||||
<div className='box'>Editar formulario</div>
|
||||
</Link>
|
||||
</nav>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
'use client';
|
||||
import { useParams } from 'next/navigation';
|
||||
import React from 'react';
|
||||
|
||||
export default function Page() {
|
||||
const params = useParams<{
|
||||
id_cuestionario: string;
|
||||
}>();
|
||||
const id_cuestionario = params?.id_cuestionario;
|
||||
|
||||
return <div>{id_cuestionario}</div>;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
'use client';
|
||||
|
||||
import { CuestionarioMetadata } from '@/types/cuestionario';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import Link from 'next/link';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
export default function Page() {
|
||||
const [cuestionarios, setCuestionarios] = useState<CuestionarioMetadata[]>(
|
||||
[]
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await axiosInstance.get<CuestionarioMetadata[]>(
|
||||
'/cuestionario'
|
||||
);
|
||||
if (res.status === 200) {
|
||||
setCuestionarios(res.data);
|
||||
} else {
|
||||
setError('Error fetching data');
|
||||
}
|
||||
} catch (error) {
|
||||
setError('Error de conexión');
|
||||
console.error('Error fetching data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <p>Cargando formularios...</p>}
|
||||
{error && <p className='text-danger'>{error}</p>}
|
||||
|
||||
<article className='col-md-12'>
|
||||
<header>
|
||||
<h2>Formularios disponibles</h2>
|
||||
</header>
|
||||
|
||||
<div className='cards-1 mt-5'>
|
||||
<div className='container'>
|
||||
<div className='row'>
|
||||
{cuestionarios.map((cuestionario) => (
|
||||
<div
|
||||
className='col-md-4 my-4'
|
||||
key={cuestionario.id_cuestionario}
|
||||
>
|
||||
<div className='card card-blog h-100'>
|
||||
<div className='card-image'>
|
||||
<Link
|
||||
href={`/administrador/formulario/${cuestionario.id_cuestionario}`}
|
||||
>
|
||||
<img
|
||||
className='img'
|
||||
src='/feria.png'
|
||||
alt='Banner formulario'
|
||||
/>
|
||||
<div className='card-caption'>
|
||||
{cuestionario.nombre_form}
|
||||
</div>
|
||||
</Link>
|
||||
<div className='ripple-cont'></div>
|
||||
</div>
|
||||
|
||||
<div className='table px-3 pt-3 mb-0'>
|
||||
<h6 className='category text-info'>
|
||||
{cuestionario.id_cuestionario} -{' '}
|
||||
{cuestionario.editable ? 'Editable' : 'No editable'}
|
||||
</h6>
|
||||
<p className='card-description'>
|
||||
{cuestionario.descripcion.slice(0, 150)}...
|
||||
</p>
|
||||
<small className='text-muted'>
|
||||
{cuestionario.contador_secciones} sección
|
||||
{cuestionario.contador_secciones !== 1 && 'es'}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{cuestionarios.length === 0 && !loading && (
|
||||
<p className='text-center text-muted'>
|
||||
No hay formularios disponibles.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import Header from '@/components/layout/header';
|
||||
import QrScanner from '@/components/qr-scanner';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React, { useState } from 'react';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
|
||||
const Modal = dynamic(() => import('@/components/modal'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
const [resultado, setResultado] = useState<string | null>(null);
|
||||
const [mostrarModal, setMostrarModal] = useState(false);
|
||||
const [enviando, setEnviando] = useState(false);
|
||||
const [mensaje, setMensaje] = useState<string | null>(null);
|
||||
|
||||
const handleScan = (text: string) => {
|
||||
setResultado(text);
|
||||
setMostrarModal(true);
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!resultado) return;
|
||||
|
||||
try {
|
||||
setEnviando(true);
|
||||
const parsed = JSON.parse(resultado);
|
||||
const { idParticipante, idEvento } = parsed;
|
||||
|
||||
if (!idParticipante || !idEvento) {
|
||||
setMensaje('El código QR no contiene los datos necesarios.');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await axiosInstance.post(
|
||||
`/participante-evento/asistencia/${idParticipante}/${idEvento}`
|
||||
);
|
||||
|
||||
console.log('Respuesta del servidor:', res.data);
|
||||
} catch (error) {
|
||||
console.error('Error registrando asistencia:', error);
|
||||
setMensaje('Error al registrar asistencia.');
|
||||
} finally {
|
||||
setEnviando(false);
|
||||
setTimeout(() => {
|
||||
setMostrarModal(false);
|
||||
setResultado(null);
|
||||
setMensaje(null);
|
||||
}, 2500);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='d-flex flex-column min-vh-100'>
|
||||
<Header small />
|
||||
<main className='py-3'>
|
||||
<div className='container text-center'>
|
||||
<h1 className='mb-4'>Lectura de QRs</h1>
|
||||
{!resultado && <QrScanner onScan={handleScan} />}
|
||||
|
||||
<Modal
|
||||
isVisible={mostrarModal}
|
||||
onClose={() => setMostrarModal(false)}
|
||||
size='md'
|
||||
>
|
||||
<h5 className='mb-3'>Resultado del QR</h5>
|
||||
<pre className='bg-light p-2 rounded'>{resultado}</pre>
|
||||
{mensaje && <p className='mt-2'>{mensaje}</p>}
|
||||
<button
|
||||
className='btn btn-primary mt-3'
|
||||
onClick={handleConfirm}
|
||||
disabled={enviando}
|
||||
>
|
||||
Confirmar
|
||||
</button>
|
||||
</Modal>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import Footer from '@/components/layout/footer';
|
||||
import Header from '@/components/layout/header';
|
||||
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'>{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
'use client';
|
||||
import CheckboxOptionGroup from '@/components/checkbox-option-group';
|
||||
import Input from '@/components/input';
|
||||
import RadioOptionGroup from '@/components/radio-option-group';
|
||||
import { Cuestionario } from '@/types/cuestionario';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import { useParams } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
export default function Page() {
|
||||
const params = useParams<{ id_formulario: string }>();
|
||||
const id_formulario = params?.id_formulario;
|
||||
const [cuestionario, setCuestionario] = useState<Cuestionario | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [respuestas, setRespuestas] = useState<Record<string, any>>({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await axiosInstance.get<Cuestionario>(
|
||||
`/cuestionario/${id_formulario}/formulario`
|
||||
);
|
||||
if (data) {
|
||||
setCuestionario(data);
|
||||
} else {
|
||||
setError('Error fetching data');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
setError('Error de conexión');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (id_formulario) fetchData();
|
||||
}, [id_formulario]);
|
||||
|
||||
const handleInputChange = (id: number, value: string) => {
|
||||
setRespuestas((prev) => ({ ...prev, [id]: value }));
|
||||
};
|
||||
|
||||
const handleRadioChange = (id: number, opcion: any) => {
|
||||
setRespuestas((prev) => ({ ...prev, [id]: opcion.value }));
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (id: number, opcion: any) => {
|
||||
setRespuestas((prev) => {
|
||||
const actuales = prev[id] || [];
|
||||
const existe = actuales.includes(opcion.value);
|
||||
const nuevos = existe
|
||||
? actuales.filter((v: any) => v !== opcion.value)
|
||||
: [...actuales, opcion.value];
|
||||
return { ...prev, [id]: nuevos };
|
||||
});
|
||||
};
|
||||
|
||||
const formatearRespuestas = async () => {
|
||||
const respuestasArray = Object.entries(respuestas).flatMap(
|
||||
([id, valor]) => {
|
||||
if (Array.isArray(valor)) {
|
||||
return valor.map((v) => ({
|
||||
id_pregunta: Number(id),
|
||||
valor: v,
|
||||
}));
|
||||
}
|
||||
return [{ id_pregunta: Number(id), valor }];
|
||||
}
|
||||
);
|
||||
|
||||
const correoEntry = respuestasArray.find(
|
||||
(r) =>
|
||||
typeof r.valor === 'string' &&
|
||||
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(r.valor)
|
||||
);
|
||||
|
||||
const resultado = {
|
||||
id_formulario: Number(id_formulario),
|
||||
correo: correoEntry?.valor || 'correo@no-encontrado.com',
|
||||
respuestas: respuestasArray,
|
||||
fecha_envio: new Date().toISOString(),
|
||||
};
|
||||
|
||||
console.log(resultado);
|
||||
|
||||
try {
|
||||
const res = await axiosInstance.post(
|
||||
'/cuestionario-respondido/submit',
|
||||
resultado
|
||||
);
|
||||
|
||||
console.log(res);
|
||||
} catch (error) {
|
||||
console.error('Error al enviar el formulario:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <p>Cargando...</p>;
|
||||
if (error) return <p className='text-danger'>{error}</p>;
|
||||
if (!cuestionario) return <p>No hay datos</p>;
|
||||
|
||||
return (
|
||||
<div className='container py-4'>
|
||||
<div>
|
||||
<h2>{cuestionario.cuestionario.nombre_form}</h2>
|
||||
<p>{cuestionario.cuestionario.descripcion}</p>
|
||||
|
||||
{cuestionario.cuestionario.secciones.map((seccion, index) => (
|
||||
<section key={index} className='mb-4'>
|
||||
<h4>{seccion.seccion.titulo}</h4>
|
||||
<p>{seccion.seccion.descripcion}</p>
|
||||
|
||||
{seccion.preguntas.map((p) => {
|
||||
const pregunta = p.pregunta;
|
||||
const id = pregunta.id_pregunta;
|
||||
const tipo = pregunta.tipo_pregunta.tipo_pregunta;
|
||||
|
||||
if (tipo === 'Abierta') {
|
||||
return (
|
||||
<Input
|
||||
key={id}
|
||||
label={pregunta.pregunta}
|
||||
name={`pregunta_${id}`}
|
||||
value={respuestas[id] || ''}
|
||||
onChange={(e) => handleInputChange(id, e.target.value)}
|
||||
required={pregunta.obligatoria}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (tipo === 'Cerrada' || tipo === 'Multiple') {
|
||||
const opciones = pregunta.opciones.map((o) => ({
|
||||
label: o.opcion.opcion,
|
||||
value: o.opcion.id_opcion,
|
||||
}));
|
||||
|
||||
if (tipo === 'Multiple') {
|
||||
return (
|
||||
<div key={id} className='mb-3'>
|
||||
<p>{pregunta.pregunta}</p>
|
||||
<CheckboxOptionGroup
|
||||
name={`pregunta_${id}`}
|
||||
options={opciones}
|
||||
selectedValues={respuestas[id] || []}
|
||||
onChange={(opcion) => handleCheckboxChange(id, opcion)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={id} className='mb-3'>
|
||||
<p>{pregunta.pregunta}</p>
|
||||
<RadioOptionGroup
|
||||
name={`pregunta_${id}`}
|
||||
options={opciones}
|
||||
selectedValue={respuestas[id]}
|
||||
onChange={(opcion) => handleRadioChange(id, opcion)}
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</section>
|
||||
))}
|
||||
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary'
|
||||
onClick={formatearRespuestas}
|
||||
>
|
||||
Enviar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<h2 className='my-4'>Formularios</h2>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+9
-8
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
import Footer from '@/components/layout/footer';
|
||||
import Header from '@/components/layout/header';
|
||||
import React, { useState } from 'react';
|
||||
import { res } from '@/data/cuestionario';
|
||||
import React from 'react';
|
||||
/* import { res } from '@/data/cuestionario';
|
||||
import RadioOptionGroup, { RadioOption } from '@/components/radio-option-group';
|
||||
import CheckboxOptionGroup, {
|
||||
CheckboxOption,
|
||||
@@ -11,9 +11,9 @@ import Pagination from '@/components/pagination';
|
||||
import Image from 'next/image';
|
||||
|
||||
type Respuesta = string | number | number[];
|
||||
|
||||
*/
|
||||
export default function Page() {
|
||||
const [respuestas, setRespuestas] = useState<Record<number, Respuesta>>({});
|
||||
/* const [respuestas, setRespuestas] = useState<Record<number, Respuesta>>({});
|
||||
const [paginaActual, setPaginaActual] = useState(1);
|
||||
const totalPaginas = res.cuestionario.secciones.length;
|
||||
|
||||
@@ -42,21 +42,22 @@ export default function Page() {
|
||||
};
|
||||
|
||||
const seccion = res.cuestionario.secciones[paginaActual - 1];
|
||||
|
||||
*/
|
||||
return (
|
||||
<div className='d-flex flex-column min-vh-100'>
|
||||
<Header />
|
||||
<div className='container flex-grow-1'>
|
||||
{/* <div className='container flex-grow-1'>
|
||||
<div className='text-center my-4'>
|
||||
<Image
|
||||
src={'/banner.png'}
|
||||
width={1000}
|
||||
height={300}
|
||||
alt='Ejemplo de banner'
|
||||
className='rounded-4 shadow-sm'
|
||||
className='rounded-4 shadow img-fluid'
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'top',
|
||||
height: 300,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -142,7 +143,7 @@ export default function Page() {
|
||||
Imprimir respuestas en consola
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Header from '@/components/layout/header';
|
||||
import QrScanner from '@/components/qr-scanner';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const Modal = dynamic(() => import('@/components/modal'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
const [resultado, setResultado] = useState<string | null>(null);
|
||||
const [mostrarModal, setMostrarModal] = useState(false);
|
||||
|
||||
const handleScan = (text: string) => {
|
||||
setResultado(text);
|
||||
setMostrarModal(true);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
// Aquí haces algo con el QR (guardar, redirigir, etc)
|
||||
alert('Confirmado: ' + resultado);
|
||||
setMostrarModal(false);
|
||||
setResultado(null); // Limpiar para volver a escanear
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='d-flex flex-column min-vh-100'>
|
||||
<Header small />
|
||||
<main className='py-3'>
|
||||
<div className='container text-center'>
|
||||
<h1 className='mb-4'>Lectura de QRs</h1>
|
||||
{!resultado && <QrScanner onScan={handleScan} />}
|
||||
|
||||
<Modal
|
||||
isVisible={mostrarModal}
|
||||
onClose={() => setMostrarModal(false)}
|
||||
size='md'
|
||||
>
|
||||
<h5 className='mb-3'>Resultado del QR</h5>
|
||||
<pre className='bg-light p-2 rounded'>{resultado}</pre>
|
||||
<button className='btn btn-primary mt-3' onClick={handleConfirm}>
|
||||
Confirmar
|
||||
</button>
|
||||
</Modal>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export interface FloatingInputProps
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className'> {
|
||||
label?: string;
|
||||
className?: {
|
||||
container?: string;
|
||||
label?: string;
|
||||
input?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Componente `FloatingInput` reutilizable con soporte para estilo flotante de Bootstrap.
|
||||
*
|
||||
* Similar al componente `Input`, pero adaptado al sistema `form-floating` de Bootstrap.
|
||||
* Soporta personalización de clases, tipo `password` con visibilidad opcional y accesibilidad.
|
||||
*/
|
||||
export default function FloatingInput(props: FloatingInputProps) {
|
||||
const { className, label, type, name, id, ...rest } = props;
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const isPassword = type === 'password';
|
||||
const inputType = isPassword && showPassword ? 'text' : type;
|
||||
const inputId =
|
||||
id ||
|
||||
name ||
|
||||
(label ? `floating-${label.replace(/\s+/g, '-')}` : undefined);
|
||||
|
||||
const togglePasswordVisibility = () => setShowPassword(!showPassword);
|
||||
|
||||
return (
|
||||
<div className={`form-floating bg-white ${className?.container || 'mb-3'}`}>
|
||||
<input
|
||||
id={inputId}
|
||||
name={name}
|
||||
type={inputType}
|
||||
className={`form-control ${className?.input || ''}`}
|
||||
placeholder={label || ''}
|
||||
{...rest}
|
||||
/>
|
||||
{label && (
|
||||
<label htmlFor={inputId} className={className?.label || ''}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
|
||||
{/* Botón para mostrar/ocultar contraseña */}
|
||||
{isPassword && (
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-light border position-absolute top-50 end-0 translate-middle-y me-2'
|
||||
style={{ zIndex: 5 }}
|
||||
onClick={togglePasswordVisibility}
|
||||
aria-label={
|
||||
showPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'
|
||||
}
|
||||
title={showPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'}
|
||||
>
|
||||
<i className={`bi ${showPassword ? 'bi-eye-slash' : 'bi-eye'}`}></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+14
-14
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export interface InputProps
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "className"> {
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className'> {
|
||||
label?: string;
|
||||
className?: {
|
||||
container?: string;
|
||||
@@ -35,42 +35,42 @@ export default function Input(props: InputProps) {
|
||||
const { className, label, type, name, id, ...rest } = props;
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const isPassword = type === "password";
|
||||
const inputType = isPassword && showPassword ? "text" : type;
|
||||
const isPassword = type === 'password';
|
||||
const inputType = isPassword && showPassword ? 'text' : type;
|
||||
const inputId =
|
||||
id || name || (label ? `input-${label.replace(/\s+/g, "-")}` : undefined);
|
||||
id || name || (label ? `input-${label.replace(/\s+/g, '-')}` : undefined);
|
||||
|
||||
const togglePasswordVisibility = () => setShowPassword(!showPassword);
|
||||
|
||||
return (
|
||||
<div className={className?.container || "mb-3"}>
|
||||
<div className={className?.container || 'mb-3'}>
|
||||
{label && inputId && (
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className={`form-label ${className?.label || ""}`}
|
||||
className={`form-label ${className?.label || ''}`}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className="input-group">
|
||||
<div className='input-group'>
|
||||
<input
|
||||
id={inputId}
|
||||
name={name}
|
||||
type={inputType}
|
||||
className={`form-control bg-white ${className?.input || ""}`}
|
||||
className={`form-control bg-white ${className?.input || ''}`}
|
||||
{...rest}
|
||||
/>
|
||||
{isPassword && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-light border"
|
||||
type='button'
|
||||
className='btn btn-light border'
|
||||
onClick={togglePasswordVisibility}
|
||||
aria-label={
|
||||
showPassword ? "Ocultar contraseña" : "Mostrar contraseña"
|
||||
showPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'
|
||||
}
|
||||
title={showPassword ? "Ocultar contraseña" : "Mostrar contraseña"}
|
||||
title={showPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'}
|
||||
>
|
||||
<i className={`bi ${showPassword ? "bi-eye-slash" : "bi-eye"}`}></i>
|
||||
<i className={`bi ${showPassword ? 'bi-eye-slash' : 'bi-eye'}`}></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { FormularioCreateDto } from '@/types/cuestionario';
|
||||
|
||||
export const plantillasDisponibles = [
|
||||
{
|
||||
nombre: 'Registro para la Feria',
|
||||
id: 'feria-sexualidad',
|
||||
datos: {
|
||||
nombre_form: 'Registro para la Feria de la Sexualidad - FES Acatlán',
|
||||
descripcion:
|
||||
'La Feria de la Sexualidad es un espacio seguro e informativo dirigido a toda la comunidad universitaria. Este registro nos ayudará a organizar mejor las actividades, talleres y charlas, así como conocer tus intereses. Por favor, completa el siguiente formulario para confirmar tu participación.',
|
||||
fecha_inicio: '2025-03-07T00:00:01',
|
||||
fecha_fin: '2025-03-18T23:59:59',
|
||||
id_tipo_cuestionario: 1,
|
||||
evento: 'Feria de la Sexualidad',
|
||||
secciones: [
|
||||
{
|
||||
titulo: 'Información Personal',
|
||||
descripcion:
|
||||
'Proporciona tus datos personales para poder contactarte y enviarte información relevante sobre la Feria de la Sexualidad.',
|
||||
preguntas: [
|
||||
{
|
||||
titulo: '¿Eres parte de la comunidad de la FES Acatlán?',
|
||||
tipo: 'Cerrada',
|
||||
opciones: [{ valor: 'Si' }, { valor: 'No' }],
|
||||
obligatoria: true,
|
||||
},
|
||||
{
|
||||
titulo: 'Numero de cuenta',
|
||||
tipo: 'Abierta',
|
||||
obligatoria: true,
|
||||
},
|
||||
{
|
||||
titulo: 'Nombre(s)',
|
||||
tipo: 'Abierta',
|
||||
obligatoria: true,
|
||||
},
|
||||
{
|
||||
titulo: 'Apellidos',
|
||||
tipo: 'Abierta',
|
||||
obligatoria: true,
|
||||
},
|
||||
{
|
||||
titulo: 'Género',
|
||||
tipo: 'Cerrada',
|
||||
obligatoria: true,
|
||||
opciones: [
|
||||
{ valor: 'Masculino' },
|
||||
{ valor: 'Femenino' },
|
||||
{ valor: 'Prefiero no decirlo' },
|
||||
],
|
||||
},
|
||||
{
|
||||
titulo: 'Institución de procedencia',
|
||||
tipo: 'Abierta',
|
||||
obligatoria: true,
|
||||
},
|
||||
{
|
||||
titulo: 'Correo electrónico',
|
||||
tipo: 'Abierta',
|
||||
obligatoria: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
Vendored
+71
-14
@@ -1,29 +1,29 @@
|
||||
@import "bootstrap/scss/functions";
|
||||
@import 'bootstrap/scss/functions';
|
||||
|
||||
@import "variables";
|
||||
@import 'variables';
|
||||
|
||||
@import "bootstrap/scss/variables";
|
||||
@import "bootstrap/scss/variables-dark";
|
||||
@import 'bootstrap/scss/variables';
|
||||
@import 'bootstrap/scss/variables-dark';
|
||||
|
||||
// Include any default map overrides here
|
||||
$azul: #003d79;
|
||||
$dorado: #d59f0f;
|
||||
|
||||
$custom-colors: (
|
||||
"azul": $azul,
|
||||
"dorado": $dorado,
|
||||
'azul': $azul,
|
||||
'dorado': $dorado,
|
||||
);
|
||||
|
||||
$theme-colors: map-merge($theme-colors, $custom-colors);
|
||||
|
||||
@import "bootstrap/scss/maps";
|
||||
@import "bootstrap/scss/mixins";
|
||||
@import "bootstrap/scss/bootstrap";
|
||||
@import 'bootstrap/scss/maps';
|
||||
@import 'bootstrap/scss/mixins';
|
||||
@import 'bootstrap/scss/bootstrap';
|
||||
|
||||
@import "gradients";
|
||||
@import "extends";
|
||||
@import "table";
|
||||
@import "utilities";
|
||||
@import 'gradients';
|
||||
@import 'extends';
|
||||
@import 'table';
|
||||
@import 'utilities';
|
||||
@import 'sidebar';
|
||||
@import 'hover';
|
||||
@import 'tabs';
|
||||
@@ -33,7 +33,7 @@ $theme-colors: map-merge($theme-colors, $custom-colors);
|
||||
@include media-breakpoint-up(lg) {
|
||||
.bd-layout {
|
||||
display: grid;
|
||||
grid-template-areas: "sidebar main";
|
||||
grid-template-areas: 'sidebar main';
|
||||
grid-template-columns: 1fr 5fr;
|
||||
gap: $spacer * 1.5;
|
||||
}
|
||||
@@ -60,3 +60,60 @@ ul.list-unstyled li {
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
@extend .bg-white, .text-dark, .rounded, .shadow-sm;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.card-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: -1.5rem 1rem 0;
|
||||
height: 60%;
|
||||
@extend .rounded;
|
||||
@extend .shadow;
|
||||
|
||||
img {
|
||||
@extend .img-fluid, .rounded;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card-caption {
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
left: 15px;
|
||||
color: #fff;
|
||||
font-size: 1.25rem;
|
||||
text-shadow: 0 2px 5px rgba(33, 33, 33, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
@extend .p-3;
|
||||
}
|
||||
|
||||
.ftr {
|
||||
@extend .mt-3;
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.author {
|
||||
@extend .text-muted;
|
||||
}
|
||||
|
||||
.stats {
|
||||
float: right;
|
||||
line-height: 30px;
|
||||
font-size: 0.875rem;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+89
-60
@@ -1,70 +1,99 @@
|
||||
interface TipoCuestionario {
|
||||
id_tipo_cuestionario: number;
|
||||
tipo_cuestionario: string;
|
||||
}
|
||||
|
||||
interface Opcion {
|
||||
id_opcion: number;
|
||||
opcion: string;
|
||||
}
|
||||
|
||||
interface PreguntaOpcion {
|
||||
id_pregunta_opcion: number;
|
||||
posicion: number;
|
||||
id_opcion: number;
|
||||
opcion: Opcion;
|
||||
}
|
||||
|
||||
interface TipoPregunta {
|
||||
id_tipo: number;
|
||||
tipo_pregunta: string;
|
||||
}
|
||||
|
||||
interface Pregunta {
|
||||
id_pregunta: number;
|
||||
pregunta: string;
|
||||
contador_opcion: number;
|
||||
obligatoria: boolean;
|
||||
id_tipo_pregunta: number;
|
||||
id_opcion_dependiente: number | null;
|
||||
tipo_pregunta: TipoPregunta;
|
||||
opciones: PreguntaOpcion[];
|
||||
}
|
||||
|
||||
interface SeccionPregunta {
|
||||
id_seccion_pregunta: number;
|
||||
posicion: number;
|
||||
pregunta: Pregunta;
|
||||
}
|
||||
|
||||
interface SeccionInfo {
|
||||
id_seccion: number;
|
||||
contador_pregunta: number;
|
||||
export interface FormularioCreateDto {
|
||||
nombre_form: string;
|
||||
descripcion: string;
|
||||
titulo: string;
|
||||
fecha_inicio: string; // Formato ISO: 'YYYY-MM-DDTHH:mm:ss'
|
||||
fecha_fin: string; // Formato ISO: 'YYYY-MM-DDTHH:mm:ss'
|
||||
id_tipo_cuestionario: number;
|
||||
evento?: string;
|
||||
secciones: Seccion[];
|
||||
}
|
||||
|
||||
interface CuestionarioSeccion {
|
||||
id_cuestionario_seccion: number;
|
||||
export interface Seccion {
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
preguntas: Pregunta[];
|
||||
}
|
||||
|
||||
export interface Pregunta {
|
||||
titulo: string;
|
||||
tipo: 'Abierta' | 'Cerrada' | 'Multiple';
|
||||
opciones?: Opcion[]; // Solo se requiere en tipos Cerrada/Multiple
|
||||
obligatoria: boolean;
|
||||
}
|
||||
|
||||
export interface Opcion {
|
||||
valor: string;
|
||||
}
|
||||
|
||||
export interface CuestionarioMetadata {
|
||||
id_cuestionario: number;
|
||||
nombre_form: string;
|
||||
descripcion: string;
|
||||
fecha_inicio: string | null; // Formato ISO o null
|
||||
fecha_fin: string | null;
|
||||
id_tipo_cuestionario: number;
|
||||
editable: boolean;
|
||||
contador_secciones: number;
|
||||
id_evento: number | null;
|
||||
id_cuestionario_original: number | null;
|
||||
}
|
||||
|
||||
/* ESto es para repsonder el formulario */
|
||||
|
||||
export interface cuestionarioExtendido extends CuestionarioMetadata {
|
||||
secciones: SeccionCompleta[];
|
||||
}
|
||||
|
||||
export interface Cuestionario {
|
||||
cuestionario: cuestionarioExtendido;
|
||||
evento: string | null;
|
||||
tipo_cuestionario: {
|
||||
id_tipo_cuestionario: number;
|
||||
tipo_cuestionario: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SeccionCompleta {
|
||||
id_cuestiorio_seccion: number;
|
||||
posicion: number;
|
||||
seccion: SeccionInfo;
|
||||
seccion: {
|
||||
id_seccion: number;
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
contador_preguntas: number;
|
||||
};
|
||||
preguntas: SeccionPregunta[];
|
||||
}
|
||||
|
||||
interface Cuestionario {
|
||||
id_cuestionario: number;
|
||||
nombre_form: string;
|
||||
contador_secciones: number;
|
||||
descripcion: string;
|
||||
editable: boolean;
|
||||
fecha_inicio: string; // Formato ISO 8601
|
||||
fecha_fin: string; // Formato ISO 8601
|
||||
id_cuestionario_original: number | null;
|
||||
id_tipo_cuestionario: number;
|
||||
secciones: CuestionarioSeccion[];
|
||||
export interface SeccionPregunta {
|
||||
id_seccion_pregunta: number;
|
||||
posicion: number;
|
||||
pregunta: PreguntaCompleta;
|
||||
}
|
||||
export interface PreguntaCompleta {
|
||||
contador_opcion: number;
|
||||
id_opcion_dependiente: number | null;
|
||||
id_pregunta: number;
|
||||
id_tipo_pregunta: number;
|
||||
obligatoria: boolean;
|
||||
opciones: OpcionCompleta[];
|
||||
pregunta: string;
|
||||
tipo_pregunta: {
|
||||
id_tipo: number;
|
||||
tipo_pregunta: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CuestionarioCompleto {
|
||||
tipo_cuestionario: TipoCuestionario;
|
||||
cuestionario: Cuestionario;
|
||||
export interface OpcionCompleta {
|
||||
id_opcion: number;
|
||||
id_pregunta_opcion: number;
|
||||
opcion: {
|
||||
id_opcion: number;
|
||||
opcion: string;
|
||||
};
|
||||
posicion: number;
|
||||
}
|
||||
|
||||
export interface GetFormularioAResponder {
|
||||
cues;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: API, // Replace with your API base URL
|
||||
timeout: 100000, // Request timeout in milliseconds
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
export default axiosInstance;
|
||||
@@ -0,0 +1,4 @@
|
||||
export function formatearFecha(fecha: string, tipo: 'inicio' | 'fin'): string {
|
||||
const sufijo = tipo === 'inicio' ? 'T00:00:01' : 'T23:59:59';
|
||||
return `${fecha}${sufijo}`;
|
||||
}
|
||||
Reference in New Issue
Block a user