Respuestas y escaneo

This commit is contained in:
jalvarado
2025-04-03 15:37:35 -06:00
parent 236522a637
commit 5a3c1a5af7
23 changed files with 994 additions and 293 deletions
+101 -14
View File
@@ -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",
+1
View File
@@ -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

@@ -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>;
}
+99
View File
@@ -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>
</>
);
}
+83
View File
@@ -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>
);
}
+13
View File
@@ -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>
);
}
-9
View File
@@ -1,9 +0,0 @@
import React from 'react';
export default function Page() {
return (
<>
<h2 className='my-4'>Formularios</h2>
</>
);
}
+9 -8
View File
@@ -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>
);
-51
View File
@@ -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>
);
}
+65
View File
@@ -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
View File
@@ -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>
+67
View File
@@ -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,
},
],
},
],
},
},
];
+71 -14
View File
@@ -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;
}
}
}
+89 -60
View File
@@ -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;
}
+13
View File
@@ -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;
+4
View File
@@ -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}`;
}