Fix, all good, hola
This commit is contained in:
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 80,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
Generated
+102
-9
@@ -8,13 +8,14 @@
|
||||
"name": "formularios_front",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@yudiel/react-qr-scanner": "^2.2.1",
|
||||
"axios": "^1.8.4",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"next": "15.2.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hot-toast": "^2.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
@@ -1215,6 +1216,12 @@
|
||||
"@popperjs/core": "^2.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/emscripten": {
|
||||
"version": "1.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.40.1.tgz",
|
||||
"integrity": "sha512-sr53lnYkQNhjHNN0oJDdUm5564biioI5DuOpycufDVK7D3y+GR3oUswe2rlwY1nPNyusHbrJ9WoTyIHl4/Bpwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
@@ -1715,6 +1722,20 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@yudiel/react-qr-scanner": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@yudiel/react-qr-scanner/-/react-qr-scanner-2.2.1.tgz",
|
||||
"integrity": "sha512-5tmzoi9d8mqqaxKTxfI9kulS3N3Kph75AOqwU2lnl4IMwjmLcTXS8IyecDUvzFNuoMNhCpofrqqGueXm8IXkjA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"barcode-detector": "^3.0.0",
|
||||
"webrtc-adapter": "9.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17 || ^18 || ^19",
|
||||
"react-dom": "^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
@@ -2023,6 +2044,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/barcode-detector": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/barcode-detector/-/barcode-detector-3.0.1.tgz",
|
||||
"integrity": "sha512-3fCzG/Py4SVgZJhubD1mt7rVprtHEVWrxQN4FUOG0oulPE4193evbgyptxcOYsfTNEtMlWc+Ec9tdxhjlR4/Ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"zxing-wasm": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
|
||||
@@ -2294,7 +2324,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
@@ -3408,6 +3437,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/goober": {
|
||||
"version": "2.1.16",
|
||||
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
|
||||
"integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"csstype": "^3.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -3518,12 +3556,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html5-qrcode": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz",
|
||||
"integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -4699,6 +4731,23 @@
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hot-toast": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz",
|
||||
"integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.3",
|
||||
"goober": "^2.1.16"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16",
|
||||
"react-dom": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@@ -4922,6 +4971,12 @@
|
||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sdp": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
|
||||
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
@@ -5445,6 +5500,18 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "4.39.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.39.1.tgz",
|
||||
"integrity": "sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/typed-array-buffer": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
||||
@@ -5600,6 +5667,19 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webrtc-adapter": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.1.tgz",
|
||||
"integrity": "sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"sdp": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0",
|
||||
"npm": ">=3.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -5727,6 +5807,19 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zxing-wasm": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-2.1.0.tgz",
|
||||
"integrity": "sha512-CvuwDZHRHwg6PeCARaCDIp3dauD4cin0mbHrQQZtMDrr5mblPzCimAjdw/XMhD/Au10q/f5+SAupvYqYvUOg1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/emscripten": "^1.40.0",
|
||||
"type-fest": "^4.35.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/emscripten": ">=1.39.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -9,13 +9,14 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@yudiel/react-qr-scanner": "^2.2.1",
|
||||
"axios": "^1.8.4",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"next": "15.2.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hot-toast": "^2.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
@@ -7,18 +7,20 @@ 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';
|
||||
import { FormularioCreacion, PreguntaFormulario, SeccionFormulario } from '@/types/crear-formulario';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function AdminFormPage() {
|
||||
const [nombreFormulario, setNombreFormulario] = useState('');
|
||||
const [descripcion, setDescripcion] = useState('');
|
||||
const [secciones, setSecciones] = useState<Seccion[]>([]);
|
||||
const [secciones, setSecciones] = useState<SeccionFormulario[]>([]);
|
||||
const [paginaActual, setPaginaActual] = useState(1);
|
||||
const [bannerPreview, setBannerPreview] = useState<string | null>(null);
|
||||
const [tipoCuestionario, setTipoCuestionario] = useState<number | null>(null);
|
||||
const [fechaInicio, setFechaInicio] = useState<string | null>(null);
|
||||
const [fechaFin, setFechaFin] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const swapItems = <T,>(arr: T[], index1: number, index2: number): T[] => {
|
||||
const result = [...arr];
|
||||
@@ -58,10 +60,10 @@ export default function AdminFormPage() {
|
||||
);
|
||||
};
|
||||
|
||||
const actualizarSeccion = <K extends keyof Seccion>(
|
||||
const actualizarSeccion = <K extends keyof SeccionFormulario>(
|
||||
index: number,
|
||||
campo: K,
|
||||
valor: Seccion[K]
|
||||
valor: SeccionFormulario[K]
|
||||
) => {
|
||||
setSecciones((prev) => {
|
||||
const nuevas = [...prev];
|
||||
@@ -90,8 +92,8 @@ export default function AdminFormPage() {
|
||||
const actualizarPregunta = (
|
||||
seccionIndex: number,
|
||||
preguntaIndex: number,
|
||||
campo: keyof Pregunta,
|
||||
valor: Pregunta[keyof Pregunta]
|
||||
campo: keyof PreguntaFormulario,
|
||||
valor: PreguntaFormulario[keyof PreguntaFormulario]
|
||||
) => {
|
||||
setSecciones((prev) => {
|
||||
const nuevas = [...prev];
|
||||
@@ -103,12 +105,11 @@ export default function AdminFormPage() {
|
||||
return nuevas;
|
||||
});
|
||||
};
|
||||
|
||||
const agregarOpcion = (seccionIndex: number, preguntaIndex: number) => {
|
||||
const nuevas = [...secciones];
|
||||
if (nuevas[seccionIndex]?.preguntas[preguntaIndex]?.opciones) {
|
||||
nuevas[seccionIndex].preguntas[preguntaIndex].opciones.push({
|
||||
valor: '',
|
||||
});
|
||||
nuevas[seccionIndex].preguntas[preguntaIndex].opciones!.push({ valor: '' });
|
||||
}
|
||||
setSecciones(nuevas);
|
||||
};
|
||||
@@ -135,18 +136,16 @@ export default function AdminFormPage() {
|
||||
const nuevas = [...secciones];
|
||||
if (
|
||||
nuevas[seccionIndex]?.preguntas[preguntaIndex]?.opciones &&
|
||||
nuevas[seccionIndex].preguntas[preguntaIndex].opciones[opcionIndex]
|
||||
nuevas[seccionIndex].preguntas[preguntaIndex].opciones![opcionIndex]
|
||||
) {
|
||||
nuevas[seccionIndex].preguntas[preguntaIndex].opciones[
|
||||
opcionIndex
|
||||
].valor = valor;
|
||||
nuevas[seccionIndex].preguntas[preguntaIndex].opciones![opcionIndex].valor = valor;
|
||||
}
|
||||
setSecciones(nuevas);
|
||||
};
|
||||
|
||||
const guardarFormulario = async () => {
|
||||
if (fechaInicio === null || fechaFin === null) {
|
||||
alert('Las fechas son obligatorias');
|
||||
if (!fechaInicio || !fechaFin || !tipoCuestionario) {
|
||||
alert('Todos los campos obligatorios deben estar llenos');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,27 +154,35 @@ export default function AdminFormPage() {
|
||||
return `${fecha}${sufijo}`;
|
||||
};
|
||||
|
||||
const formData = {
|
||||
const formData: FormularioCreacion = {
|
||||
nombre_form: nombreFormulario,
|
||||
id_tipo_cuestionario: tipoCuestionario,
|
||||
descripcion,
|
||||
fecha_inicio: formatearFecha(fechaInicio, 'inicio'),
|
||||
fecha_fin: formatearFecha(fechaFin, 'fin'),
|
||||
descripcion,
|
||||
id_tipo_cuestionario: tipoCuestionario,
|
||||
evento: nombreFormulario, // temporalmente igual al nombre del form
|
||||
secciones,
|
||||
};
|
||||
|
||||
console.log('Formulario a guardar:', formData);
|
||||
|
||||
try {
|
||||
const res = await axiosInstance.post('/cuestionario', formData);
|
||||
setLoading(true);
|
||||
await toast.promise(
|
||||
axiosInstance.post('/cuestionario', formData),
|
||||
{
|
||||
loading: 'Guardando formulario...',
|
||||
success: 'Formulario guardado exitosamente!',
|
||||
error: 'Error al guardar el formulario.',
|
||||
}
|
||||
).then((res) => {
|
||||
console.log('Formulario guardado:', res.data);
|
||||
} catch (error) {
|
||||
}).catch((error) => {
|
||||
console.error('Error al guardar el formulario:', error);
|
||||
}
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const seccionActual = secciones[paginaActual - 1];
|
||||
|
||||
const handleBannerChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0] || null;
|
||||
if (file) {
|
||||
@@ -187,67 +194,76 @@ export default function AdminFormPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const seccionActual = secciones[paginaActual - 1];
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className='my-4'>Crear nuevo formulario</h2>
|
||||
<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="row">
|
||||
{secciones.length === 0 &&
|
||||
plantillasDisponibles.map((plantilla, index) => (
|
||||
<div className="col-md-4 mb-3" key={plantilla.id}>
|
||||
<h3>Plantillas</h3>
|
||||
<div
|
||||
className="box w-100 cursor-pointer"
|
||||
onClick={() => {
|
||||
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));
|
||||
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);
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={plantilla.imagen}
|
||||
width={1000}
|
||||
height={300}
|
||||
alt={`Plantilla de formulario ${index + 1}`}
|
||||
className="img-fluid rounded shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className='mb-4'>
|
||||
<label className='form-label'>Banner del formulario</label>
|
||||
<div className="mb-4">
|
||||
<label className="form-label">Banner del formulario</label>
|
||||
<input
|
||||
type='file'
|
||||
accept='image/*'
|
||||
className='form-control mb-2'
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="form-control mb-2"
|
||||
onChange={handleBannerChange}
|
||||
/>
|
||||
{bannerPreview && (
|
||||
<div className='text-center'>
|
||||
<div className="text-center">
|
||||
<Image
|
||||
src={bannerPreview}
|
||||
width={1000}
|
||||
height={300}
|
||||
alt='Ejemplo de banner'
|
||||
className='rounded-4 shadow-sm'
|
||||
alt="Ejemplo de banner"
|
||||
className="rounded-4 shadow-sm"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center',
|
||||
@@ -258,7 +274,7 @@ export default function AdminFormPage() {
|
||||
</div>
|
||||
|
||||
<Select
|
||||
label='Tipo de cuestionario'
|
||||
label="Tipo de cuestionario"
|
||||
options={[
|
||||
{ label: 'Seleccione un tipo', value: '' },
|
||||
{ label: 'Tipo 1', value: 1 },
|
||||
@@ -270,53 +286,53 @@ export default function AdminFormPage() {
|
||||
/>
|
||||
|
||||
<Input
|
||||
label='Nombre del formulario'
|
||||
label="Nombre del formulario"
|
||||
value={nombreFormulario}
|
||||
onChange={(e) => setNombreFormulario(e.target.value)}
|
||||
placeholder='Ingrese el nombre del formulario'
|
||||
placeholder="Ingrese el nombre del formulario"
|
||||
/>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>Descripción</label>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Descripción</label>
|
||||
<textarea
|
||||
className='form-control bg-white'
|
||||
className="form-control bg-white"
|
||||
rows={3}
|
||||
value={descripcion}
|
||||
onChange={(e) => setDescripcion(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='row'>
|
||||
<div className='col-md-6'>
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<Input
|
||||
label='Fecha de inicio'
|
||||
type='date'
|
||||
label="Fecha de inicio"
|
||||
type="date"
|
||||
value={fechaInicio || ''}
|
||||
onChange={(e) => setFechaInicio(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-md-6'>
|
||||
<div className="col-md-6">
|
||||
<Input
|
||||
label='Fecha de fin'
|
||||
type='date'
|
||||
label="Fecha de fin"
|
||||
type="date"
|
||||
value={fechaFin || ''}
|
||||
onChange={(e) => setFechaFin(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-4'>
|
||||
<Button outline onClick={agregarSeccion} icon='plus'>
|
||||
<div className="mb-4">
|
||||
<Button outline onClick={agregarSeccion} icon="plus">
|
||||
Agregar Sección
|
||||
</Button>
|
||||
|
||||
{seccionActual && (
|
||||
<div className='border rounded p-3 mb-4'>
|
||||
<div className='d-flex justify-content-between align-items-center'>
|
||||
<div className="border rounded p-3 mb-4">
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<h5>Sección {paginaActual}</h5>
|
||||
<div className='d-flex gap-2'>
|
||||
<div className="d-flex gap-2">
|
||||
<button
|
||||
className='btn btn-sm btn-outline-secondary'
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
onClick={() =>
|
||||
moverSeccion(paginaActual - 1, paginaActual - 2)
|
||||
}
|
||||
@@ -325,14 +341,14 @@ export default function AdminFormPage() {
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
className='btn btn-sm btn-outline-secondary'
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
onClick={() => moverSeccion(paginaActual - 1, paginaActual)}
|
||||
disabled={paginaActual === secciones.length}
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
<button
|
||||
className='btn btn-sm btn-danger'
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={() => eliminarSeccion(paginaActual - 1)}
|
||||
>
|
||||
Eliminar sección
|
||||
@@ -341,17 +357,17 @@ export default function AdminFormPage() {
|
||||
</div>
|
||||
|
||||
<FloatingInput
|
||||
label='Título de la sección'
|
||||
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'
|
||||
placeholder="Título de la sección"
|
||||
/>
|
||||
<textarea
|
||||
className='form-control mb-3'
|
||||
placeholder='Descripción de la sección'
|
||||
className="form-control mb-3"
|
||||
placeholder="Descripción de la sección"
|
||||
value={seccionActual.descripcion}
|
||||
onChange={(e) =>
|
||||
actualizarSeccion(
|
||||
@@ -363,12 +379,12 @@ export default function AdminFormPage() {
|
||||
/>
|
||||
|
||||
{seccionActual.preguntas.map((pregunta, pIdx) => (
|
||||
<div key={pIdx} className='border rounded p-2 mb-3'>
|
||||
<div className='d-flex justify-content-between align-items-center'>
|
||||
<div key={pIdx} className="border rounded p-2 mb-3">
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<strong>Pregunta {pIdx + 1}</strong>
|
||||
<div className='d-flex gap-2'>
|
||||
<div className="d-flex gap-2">
|
||||
<button
|
||||
className='btn btn-sm btn-outline-secondary'
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
onClick={() =>
|
||||
moverPregunta(paginaActual - 1, pIdx, pIdx - 1)
|
||||
}
|
||||
@@ -377,7 +393,7 @@ export default function AdminFormPage() {
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
className='btn btn-sm btn-outline-secondary'
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
onClick={() =>
|
||||
moverPregunta(paginaActual - 1, pIdx, pIdx + 1)
|
||||
}
|
||||
@@ -386,7 +402,7 @@ export default function AdminFormPage() {
|
||||
↓
|
||||
</button>
|
||||
<button
|
||||
className='btn btn-sm btn-outline-danger'
|
||||
className="btn btn-sm btn-outline-danger"
|
||||
onClick={() => eliminarPregunta(paginaActual - 1, pIdx)}
|
||||
>
|
||||
Eliminar
|
||||
@@ -394,7 +410,7 @@ export default function AdminFormPage() {
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
label='Texto de la pregunta'
|
||||
label="Texto de la pregunta"
|
||||
className={{ container: 'my-2' }}
|
||||
value={pregunta.titulo}
|
||||
onChange={(e) =>
|
||||
@@ -405,11 +421,11 @@ export default function AdminFormPage() {
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
placeholder='Texto de la pregunta'
|
||||
placeholder="Texto de la pregunta"
|
||||
/>
|
||||
|
||||
<Select
|
||||
label='Tipo de pregunta'
|
||||
label="Tipo de pregunta"
|
||||
options={[
|
||||
{ label: 'Abierta', value: 'Abierta' },
|
||||
{ label: 'Cerrada', value: 'Cerrada' },
|
||||
@@ -421,15 +437,15 @@ export default function AdminFormPage() {
|
||||
paginaActual - 1,
|
||||
pIdx,
|
||||
'tipo',
|
||||
e.target.value as Pregunta['tipo']
|
||||
e.target.value as PreguntaFormulario['tipo']
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<div className='form-check form-switch mb-3'>
|
||||
<div className="form-check form-switch mb-3">
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='checkbox'
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
id={`obligatoria-${pIdx}`}
|
||||
checked={pregunta.obligatoria}
|
||||
onChange={(e) =>
|
||||
@@ -442,7 +458,7 @@ export default function AdminFormPage() {
|
||||
}
|
||||
/>
|
||||
<label
|
||||
className='form-check-label'
|
||||
className="form-check-label"
|
||||
htmlFor={`obligatoria-${pIdx}`}
|
||||
>
|
||||
¿Es obligatoria?
|
||||
@@ -453,16 +469,16 @@ export default function AdminFormPage() {
|
||||
pregunta.tipo === 'Multiple') && (
|
||||
<>
|
||||
<button
|
||||
className='btn btn-sm btn-outline-secondary mb-2'
|
||||
className="btn btn-sm btn-outline-secondary mb-2"
|
||||
onClick={() => agregarOpcion(paginaActual - 1, pIdx)}
|
||||
>
|
||||
+ Agregar opción
|
||||
</button>
|
||||
{(pregunta.opciones ?? []).map((op, oIdx) => (
|
||||
<div key={oIdx} className='input-group mb-2'>
|
||||
<div key={oIdx} className="input-group mb-2">
|
||||
<input
|
||||
type='text'
|
||||
className='form-control bg-white'
|
||||
type="text"
|
||||
className="form-control bg-white"
|
||||
placeholder={`Opción ${oIdx + 1}`}
|
||||
value={op.valor}
|
||||
onChange={(e) =>
|
||||
@@ -475,7 +491,7 @@ export default function AdminFormPage() {
|
||||
}
|
||||
/>
|
||||
<button
|
||||
className='btn btn-outline-danger'
|
||||
className="btn btn-outline-danger"
|
||||
onClick={() =>
|
||||
eliminarOpcion(paginaActual - 1, pIdx, oIdx)
|
||||
}
|
||||
@@ -490,7 +506,7 @@ export default function AdminFormPage() {
|
||||
))}
|
||||
|
||||
<button
|
||||
className='btn btn-sm btn-outline-primary'
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
onClick={() => agregarPregunta(paginaActual - 1)}
|
||||
>
|
||||
+ Agregar pregunta
|
||||
@@ -499,7 +515,7 @@ export default function AdminFormPage() {
|
||||
)}
|
||||
|
||||
{secciones.length > 1 && (
|
||||
<div className='d-flex justify-content-center mb-4'>
|
||||
<div className="d-flex justify-content-center mb-4">
|
||||
<Pagination
|
||||
currentPage={paginaActual}
|
||||
totalPages={secciones.length}
|
||||
@@ -509,7 +525,7 @@ export default function AdminFormPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button onClick={guardarFormulario} color='success'>
|
||||
<Button onClick={guardarFormulario} color="success">
|
||||
Guardar formulario
|
||||
</Button>
|
||||
</>
|
||||
|
||||
@@ -1,99 +1,60 @@
|
||||
'use client';
|
||||
import FormularioCard from '@/components/formulario-card';
|
||||
import { GetCuestionario } from '@/types/cuestionario';
|
||||
import React from 'react';
|
||||
|
||||
import { CuestionarioMetadata } from '@/types/cuestionario';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import Link from 'next/link';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
async function getEventos(): Promise<GetCuestionario[]> {
|
||||
try {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/cuestionario`, {
|
||||
method: 'GET',
|
||||
cache: 'no-store', // para que no lo cachee ni en build ni entre requests
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
const [cuestionarios, setCuestionarios] = useState<CuestionarioMetadata[]>(
|
||||
[]
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Error ${res.status}: ${res.statusText}`);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
const data: GetCuestionario[] = await res.json();
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error en getEventos:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
let eventos;
|
||||
|
||||
try {
|
||||
eventos = await getEventos();
|
||||
} catch (error) {
|
||||
console.error('Error al cargar los eventos:', error);
|
||||
return <div>Error al cargar los eventos</div>;
|
||||
}
|
||||
|
||||
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 className="cards-1 mt-3">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
{eventos.map((cuestionario) => (
|
||||
<FormularioCard
|
||||
key={cuestionario.id_cuestionario}
|
||||
cuestionario={cuestionario}
|
||||
link={`/administrador/formulario/${cuestionario.id_cuestionario}`}
|
||||
/>
|
||||
))}
|
||||
{eventos.length === 0 && (
|
||||
<p className="text-center text-muted">
|
||||
No hay formularios disponibles.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+118
-55
@@ -1,76 +1,139 @@
|
||||
'use client';
|
||||
|
||||
import Header from '@/components/layout/header';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Scanner } from '@yudiel/react-qr-scanner';
|
||||
import Button from '@/components/button';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import Header from '@/components/layout/header';
|
||||
import Footer from '@/components/layout/footer';
|
||||
|
||||
const Html5QrcodePlugin = dynamic(() => import('@/components/qr-scanner'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const Modal = dynamic(() => import('@/components/modal'), {
|
||||
ssr: false,
|
||||
});
|
||||
const Modal = dynamic(() => import('@/components/modal'), { ssr: false });
|
||||
|
||||
export default function Page() {
|
||||
const [text, setText] = React.useState<string>('');
|
||||
const [showModal, setShowModal] = React.useState<boolean>(false);
|
||||
const [scannedData, setScannedData] = useState<{
|
||||
id_participante: number;
|
||||
id_evento: number;
|
||||
} | null>(null);
|
||||
|
||||
const onScanSuccess = (decodedText: string) => {
|
||||
setText(decodedText);
|
||||
setShowModal(true); // Muestra el modal al escanear
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [enableScan, setEnableScan] = useState(true);
|
||||
const [statusMessage, setStatusMessage] = useState('');
|
||||
|
||||
const handleScan = (rawValue: string) => {
|
||||
if (!enableScan) return;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(rawValue);
|
||||
if (data.id_participante && data.id_evento) {
|
||||
setScannedData({
|
||||
id_participante: data.id_participante,
|
||||
id_evento: data.id_evento,
|
||||
});
|
||||
setEnableScan(false);
|
||||
setShowModal(true);
|
||||
} else {
|
||||
setStatusMessage('El QR no contiene los campos requeridos');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('QR malformado:', err);
|
||||
setStatusMessage('Error al leer el QR: formato inválido');
|
||||
}
|
||||
};
|
||||
|
||||
const onScanError = (errorMessage: string) => {
|
||||
console.warn('QR Error:', errorMessage);
|
||||
};
|
||||
const handleConfirm = async () => {
|
||||
if (!scannedData) return;
|
||||
|
||||
try {
|
||||
const response = await axiosInstance.post(
|
||||
`/participante-evento/asistencia/${scannedData.id_participante}/${scannedData.id_evento}`
|
||||
);
|
||||
|
||||
console.log('Asistencia registrada:', response.data);
|
||||
} catch (err) {
|
||||
console.error('Error en la petición:', err);
|
||||
setStatusMessage('❌ Error de red al registrar asistencia');
|
||||
}
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setShowModal(false);
|
||||
setEnableScan(true);
|
||||
setScannedData(null);
|
||||
};
|
||||
|
||||
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>
|
||||
<div className="d-flex flex-column min-vh-100">
|
||||
<Header />
|
||||
<main className='container flex-grow-1 d-flex flex-column align-items-center justify-content-center'>
|
||||
<h1 className="mb-4">Lectura de QRs</h1>
|
||||
|
||||
<div className='mb-4'>
|
||||
<Html5QrcodePlugin
|
||||
fps={10}
|
||||
qrbox={500}
|
||||
qrCodeSuccessCallback={onScanSuccess}
|
||||
qrCodeErrorCallback={onScanError}
|
||||
/>
|
||||
</div>
|
||||
{enableScan ? (
|
||||
<>
|
||||
<div style={{ width: '100%', maxWidth: '400px' }}>
|
||||
<Scanner
|
||||
onScan={(codes) => {
|
||||
if (codes.length > 0) {
|
||||
handleScan(codes[0].rawValue);
|
||||
}
|
||||
}}
|
||||
onError={(err) => console.error('QR Error:', err)}
|
||||
constraints={{ facingMode: 'environment' }}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setEnableScan(false)}
|
||||
variant="outline-danger"
|
||||
className="mt-3"
|
||||
>
|
||||
Detener cámara
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-muted mb-3">La cámara está detenida.</p>
|
||||
<Button
|
||||
onClick={() => setEnableScan(true)}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Activar cámara
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<p className='mt-4'>
|
||||
{text
|
||||
? `Texto escaneado: ${text}`
|
||||
: 'Escanea un código QR para ver el texto aquí.'}
|
||||
</p>
|
||||
</div>
|
||||
{statusMessage && <p className="mt-3">{statusMessage}</p>}
|
||||
|
||||
<Modal
|
||||
isVisible={showModal}
|
||||
size="md"
|
||||
onClose={() => {
|
||||
setShowModal(false);
|
||||
setEnableScan(true);
|
||||
setScannedData(null);
|
||||
}}
|
||||
closeButton
|
||||
className={{
|
||||
content: 'p-3',
|
||||
body: 'text-center',
|
||||
}}
|
||||
>
|
||||
<h5 className="mb-3">Datos escaneados</h5>
|
||||
{scannedData ? (
|
||||
<>
|
||||
<p>
|
||||
<strong>ID Participante:</strong> {scannedData.id_participante}
|
||||
</p>
|
||||
<p>
|
||||
<strong>ID Evento:</strong> {scannedData.id_evento}
|
||||
</p>
|
||||
<Button variant="success" onClick={handleConfirm}>
|
||||
Confirmar lectura
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<p>QR inválido</p>
|
||||
)}
|
||||
</Modal>
|
||||
</main>
|
||||
|
||||
<Modal
|
||||
isVisible={showModal}
|
||||
size='md'
|
||||
onClose={handleCloseModal}
|
||||
closeButton
|
||||
className={{
|
||||
content: 'p-3',
|
||||
body: 'text-center',
|
||||
}}
|
||||
>
|
||||
<h5 className='mb-3'>Texto escaneado</h5>
|
||||
<p>{text}</p>
|
||||
|
||||
<Button variant='success' onClick={handleCloseModal}>
|
||||
Confirmar lectura
|
||||
</Button>
|
||||
</Modal>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import React from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className='d-flex flex-column min-vh-100'>
|
||||
<div className="d-flex flex-column min-vh-100">
|
||||
<Header />
|
||||
<main className='container flex-grow-1'>{children}</main>
|
||||
<main className="container flex-grow-1">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import ClientCarousel from '@/client-components/client-carousel';
|
||||
import FormularioCard from '@/components/formulario-card';
|
||||
import { GetCuestionario } from '@/types/cuestionario';
|
||||
import React from 'react';
|
||||
|
||||
async function getEventos(): Promise<GetCuestionario[]> {
|
||||
try {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/cuestionario`, {
|
||||
method: 'GET',
|
||||
cache: 'no-store', // para que no lo cachee ni en build ni entre requests
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Error ${res.status}: ${res.statusText}`);
|
||||
}
|
||||
|
||||
const data: GetCuestionario[] = await res.json();
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error en getEventos:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
let eventos;
|
||||
|
||||
try {
|
||||
eventos = await getEventos();
|
||||
} catch (error) {
|
||||
console.error('Error al cargar los eventos:', error);
|
||||
return <div>Error al cargar los eventos</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto my-5">
|
||||
<ClientCarousel />
|
||||
</div>
|
||||
<div className="cards-1 mt-5">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
{eventos.map((cuestionario) => (
|
||||
<FormularioCard
|
||||
key={cuestionario.id_cuestionario}
|
||||
cuestionario={cuestionario}
|
||||
link={`/registro/${cuestionario.id_cuestionario}`}
|
||||
/>
|
||||
))}
|
||||
{eventos.length === 0 && (
|
||||
<p className="text-center text-muted">
|
||||
No hay formularios disponibles.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +1,38 @@
|
||||
'use client';
|
||||
import Button from '@/components/button';
|
||||
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 { CuestionarioResponse } from '@/types/responder-formulario';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import { useParams } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const params = useParams<{ id_formulario: string }>();
|
||||
const id_formulario = params?.id_formulario;
|
||||
const [cuestionario, setCuestionario] = useState<Cuestionario | null>(null);
|
||||
const [cuestionario, setCuestionario] = useState<CuestionarioResponse | null>(
|
||||
null
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [respuestas, setRespuestas] = useState<Record<string, any>>({});
|
||||
const [respuestas, setRespuestas] = useState<
|
||||
Record<number, string | number | Array<string | number>>
|
||||
>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false); // New state for submission
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await axiosInstance.get<Cuestionario>(
|
||||
const { data } = await axiosInstance.get<CuestionarioResponse>(
|
||||
`/cuestionario/${id_formulario}/formulario`
|
||||
);
|
||||
if (data) {
|
||||
console.log(data);
|
||||
setCuestionario(data);
|
||||
} else {
|
||||
setError('Error fetching data');
|
||||
@@ -42,22 +52,26 @@ export default function Page() {
|
||||
setRespuestas((prev) => ({ ...prev, [id]: value }));
|
||||
};
|
||||
|
||||
const handleRadioChange = (id: number, opcion: any) => {
|
||||
const handleRadioChange = (id: number, opcion: { value: number }) => {
|
||||
setRespuestas((prev) => ({ ...prev, [id]: opcion.value }));
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (id: number, opcion: any) => {
|
||||
const handleCheckboxChange = (id: number, opcion: { value: number }) => {
|
||||
setRespuestas((prev) => {
|
||||
const actuales = prev[id] || [];
|
||||
const actuales: number[] = Array.isArray(prev[id])
|
||||
? (prev[id] as number[])
|
||||
: [];
|
||||
const existe = actuales.includes(opcion.value);
|
||||
const nuevos = existe
|
||||
? actuales.filter((v: any) => v !== opcion.value)
|
||||
? actuales.filter((v: number) => v !== opcion.value)
|
||||
: [...actuales, opcion.value];
|
||||
return { ...prev, [id]: nuevos };
|
||||
});
|
||||
};
|
||||
|
||||
const formatearRespuestas = async () => {
|
||||
if (!validarRespuestasObligatorias()) return;
|
||||
|
||||
const respuestasArray = Object.entries(respuestas).flatMap(
|
||||
([id, valor]) => {
|
||||
if (Array.isArray(valor)) {
|
||||
@@ -83,32 +97,87 @@ export default function Page() {
|
||||
fecha_envio: new Date().toISOString(),
|
||||
};
|
||||
|
||||
console.log(resultado);
|
||||
setIsSubmitting(true); // Set submitting state to true
|
||||
toast
|
||||
.promise(
|
||||
axiosInstance.post('/cuestionario-respondido/submit', resultado),
|
||||
{
|
||||
loading: 'Enviando formulario...',
|
||||
success: 'Formulario enviado con éxito!',
|
||||
error: 'Error al enviar el formulario.',
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
router.push('/');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error al enviar el formulario:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSubmitting(false); // Reset submitting state
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await axiosInstance.post(
|
||||
'/cuestionario-respondido/submit',
|
||||
resultado
|
||||
);
|
||||
const validarRespuestasObligatorias = (): boolean => {
|
||||
const faltantes: number[] = [];
|
||||
|
||||
console.log(res);
|
||||
} catch (error) {
|
||||
console.error('Error al enviar el formulario:', error);
|
||||
cuestionario?.cuestionario.secciones.forEach((seccion) => {
|
||||
seccion.preguntas.forEach(({ pregunta }) => {
|
||||
if (pregunta.obligatoria) {
|
||||
const valor = respuestas[pregunta.id_pregunta];
|
||||
const tipo = pregunta.tipo_pregunta.tipo_pregunta;
|
||||
|
||||
const estaRespondida =
|
||||
(tipo === 'Abierta' &&
|
||||
typeof valor === 'string' &&
|
||||
valor.trim() !== '') ||
|
||||
(tipo === 'Cerrada' && valor !== undefined) ||
|
||||
(tipo === 'Multiple' && Array.isArray(valor) && valor.length > 0);
|
||||
|
||||
if (!estaRespondida) {
|
||||
faltantes.push(pregunta.id_pregunta);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (faltantes.length > 0) {
|
||||
alert(`Responde todas las preguntas obligatorias antes de enviar.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if (loading) return <p>Cargando...</p>;
|
||||
if (error) return <p className='text-danger'>{error}</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>
|
||||
<div className="container py-4">
|
||||
<Button icon="arrow-left" onClick={() => router.back()}>
|
||||
Volver
|
||||
</Button>
|
||||
<div className="text-center mb-4">
|
||||
<Image
|
||||
src={'/banner1.png'}
|
||||
width={1000}
|
||||
height={300}
|
||||
alt="Ejemplo de banner"
|
||||
className="rounded-4 shadow-sm img-fluid"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center',
|
||||
height: 300,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-lg-5 mx-lg-5">
|
||||
<h2>{cuestionario.cuestionario.nombre_form}</h2>
|
||||
<p>{cuestionario.cuestionario.descripcion}</p>
|
||||
|
||||
{cuestionario.cuestionario.secciones.map((seccion, index) => (
|
||||
<section key={index} className='mb-4'>
|
||||
<section key={index} className="mb-4">
|
||||
<h4>{seccion.seccion.titulo}</h4>
|
||||
<p>{seccion.seccion.descripcion}</p>
|
||||
|
||||
@@ -123,7 +192,11 @@ export default function Page() {
|
||||
key={id}
|
||||
label={pregunta.pregunta}
|
||||
name={`pregunta_${id}`}
|
||||
value={respuestas[id] || ''}
|
||||
value={
|
||||
Array.isArray(respuestas[id])
|
||||
? respuestas[id].join(', ')
|
||||
: respuestas[id] || ''
|
||||
}
|
||||
onChange={(e) => handleInputChange(id, e.target.value)}
|
||||
required={pregunta.obligatoria}
|
||||
/>
|
||||
@@ -131,33 +204,47 @@ export default function Page() {
|
||||
}
|
||||
|
||||
if (tipo === 'Cerrada' || tipo === 'Multiple') {
|
||||
const opciones = pregunta.opciones.map((o) => ({
|
||||
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'>
|
||||
<div key={id} className="mb-3">
|
||||
<p>{pregunta.pregunta}</p>
|
||||
<CheckboxOptionGroup
|
||||
name={`pregunta_${id}`}
|
||||
options={opciones}
|
||||
selectedValues={respuestas[id] || []}
|
||||
onChange={(opcion) => handleCheckboxChange(id, opcion)}
|
||||
selectedValues={
|
||||
Array.isArray(respuestas[id]) ? respuestas[id] : []
|
||||
}
|
||||
onChange={(opcion) => {
|
||||
if (typeof opcion.value === 'number') {
|
||||
handleCheckboxChange(id, { value: opcion.value });
|
||||
} else {
|
||||
console.error('Invalid value type:', opcion.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={id} className='mb-3'>
|
||||
<div key={id} className="mb-3">
|
||||
<p>{pregunta.pregunta}</p>
|
||||
<RadioOptionGroup
|
||||
name={`pregunta_${id}`}
|
||||
options={opciones}
|
||||
selectedValue={respuestas[id]}
|
||||
onChange={(opcion) => handleRadioChange(id, opcion)}
|
||||
onChange={(opcion) => {
|
||||
if (typeof opcion.value === 'number') {
|
||||
handleRadioChange(id, { value: opcion.value });
|
||||
} else {
|
||||
console.error('Invalid value type:', opcion.value);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
@@ -170,11 +257,12 @@ export default function Page() {
|
||||
))}
|
||||
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary'
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={formatearRespuestas}
|
||||
disabled={isSubmitting} // Disable button while submitting
|
||||
>
|
||||
Enviar
|
||||
{isSubmitting ? 'Enviando...' : 'Enviar'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Roboto } from "next/font/google";
|
||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
import "@/styles/sass/bootstrap.scss";
|
||||
import BootstrapClient from "@/components/bootstrap-client";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
const roboto = Roboto({
|
||||
weight: ["100", "300", "400", "500", "700", "900"],
|
||||
@@ -27,6 +28,7 @@ export default function RootLayout({
|
||||
<html lang="en">
|
||||
<body className={roboto.className}>
|
||||
<BootstrapClient />
|
||||
<Toaster />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
'use client';
|
||||
import Footer from '@/components/layout/footer';
|
||||
import Header from '@/components/layout/header';
|
||||
import React from 'react';
|
||||
/* import { res } from '@/data/cuestionario';
|
||||
import RadioOptionGroup, { RadioOption } from '@/components/radio-option-group';
|
||||
import CheckboxOptionGroup, {
|
||||
CheckboxOption,
|
||||
} from '@/components/checkbox-option-group';
|
||||
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 [paginaActual, setPaginaActual] = useState(1);
|
||||
const totalPaginas = res.cuestionario.secciones.length;
|
||||
|
||||
const handleRespuesta = (idPregunta: number, valor: string | number) => {
|
||||
setRespuestas((prev) => ({ ...prev, [idPregunta]: valor }));
|
||||
};
|
||||
|
||||
const handleMultipleRespuesta = (
|
||||
idPregunta: number,
|
||||
opcion: CheckboxOption<number>
|
||||
) => {
|
||||
setRespuestas((prev) => {
|
||||
const current = Array.isArray(prev[idPregunta])
|
||||
? (prev[idPregunta] as number[])
|
||||
: [];
|
||||
const updated = current.includes(opcion.value)
|
||||
? current.filter((val) => val !== opcion.value)
|
||||
: [...current, opcion.value];
|
||||
return { ...prev, [idPregunta]: updated };
|
||||
});
|
||||
};
|
||||
|
||||
const imprimirRespuestas = () => {
|
||||
console.log('Respuestas del cuestionario:', respuestas);
|
||||
alert('Respuestas impresas en consola');
|
||||
};
|
||||
|
||||
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='text-center my-4'>
|
||||
<Image
|
||||
src={'/banner.png'}
|
||||
width={1000}
|
||||
height={300}
|
||||
alt='Ejemplo de banner'
|
||||
className='rounded-4 shadow img-fluid'
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'top',
|
||||
height: 300,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<h1 className='mb-4'>{res.cuestionario.nombre_form}</h1>
|
||||
<p className='mb-5 text-muted'>{res.cuestionario.descripcion}</p>
|
||||
|
||||
<div className='mb-5 mx-md-5 px-md-5'>
|
||||
<h3>{seccion.seccion.titulo}</h3>
|
||||
<p className='text-muted'>{seccion.seccion.descripcion}</p>
|
||||
|
||||
{seccion.preguntas.map((preguntaWrap) => {
|
||||
const pregunta = preguntaWrap.pregunta;
|
||||
const id = pregunta.id_pregunta;
|
||||
|
||||
if (pregunta.tipo_pregunta.tipo_pregunta === 'Cerrada') {
|
||||
const opciones: RadioOption<number>[] = pregunta.opciones.map(
|
||||
(op) => ({
|
||||
label: op.opcion.opcion,
|
||||
value: op.opcion.id_opcion,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={id} className='mt-4'>
|
||||
<strong>{pregunta.pregunta}</strong>
|
||||
<RadioOptionGroup
|
||||
name={`pregunta-${id}`}
|
||||
options={opciones}
|
||||
selectedValue={respuestas[id] as number}
|
||||
onChange={(op) => handleRespuesta(id, op.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (pregunta.tipo_pregunta.tipo_pregunta === 'Abierta') {
|
||||
return (
|
||||
<div key={id} className='mt-4'>
|
||||
<strong>{pregunta.pregunta}</strong>
|
||||
<textarea
|
||||
className='form-control bg-white mt-2'
|
||||
rows={3}
|
||||
value={
|
||||
typeof respuestas[id] === 'string' ? respuestas[id] : ''
|
||||
}
|
||||
onChange={(e) => handleRespuesta(id, e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (pregunta.tipo_pregunta.tipo_pregunta === 'Multiple') {
|
||||
const opciones: CheckboxOption<number>[] = pregunta.opciones.map(
|
||||
(op) => ({
|
||||
label: op.opcion.opcion,
|
||||
value: op.opcion.id_opcion,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={id} className='mt-4'>
|
||||
<strong>{pregunta.pregunta}</strong>
|
||||
<CheckboxOptionGroup
|
||||
name={`pregunta-${id}`}
|
||||
options={opciones}
|
||||
selectedValues={(respuestas[id] as number[]) || []}
|
||||
onChange={(op) => handleMultipleRespuesta(id, op)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className='d-flex justify-content-center mb-4'>
|
||||
<Pagination
|
||||
currentPage={paginaActual}
|
||||
totalPages={totalPaginas}
|
||||
onPageChange={setPaginaActual}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='text-center mb-5'>
|
||||
<button className='btn btn-primary' onClick={imprimirRespuestas}>
|
||||
Imprimir respuestas en consola
|
||||
</button>
|
||||
</div>
|
||||
</div> */}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
'use client';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
const Carousel = dynamic(() => import('@/components/carousel'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function ClientCarousel() {
|
||||
return <Carousel />;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import Image from 'next/image';
|
||||
import React from 'react';
|
||||
|
||||
interface CarouselProps {
|
||||
id?: string;
|
||||
images?: {
|
||||
src: string;
|
||||
alt?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export default function Carousel({
|
||||
id = 'carouselExample',
|
||||
images = [
|
||||
{
|
||||
src: '/banner1.png',
|
||||
alt: 'Imagen de ejemplo',
|
||||
},
|
||||
{
|
||||
src: '/banner2.png',
|
||||
alt: 'Imagen de ejemplo',
|
||||
},
|
||||
],
|
||||
}: CarouselProps) {
|
||||
if (!images || images.length === 0) {
|
||||
return <div>No hay imágenes para mostrar</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id={id} className="carousel slide" data-bs-ride="carousel">
|
||||
<div className="carousel-inner">
|
||||
{images.map((img, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`carousel-item ${index === 0 ? 'active' : ''}`}
|
||||
>
|
||||
<Image
|
||||
src={img.src}
|
||||
width={1000}
|
||||
height={400}
|
||||
alt="Ejemplo de banner"
|
||||
className="d-block w-100 rounded-3"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{images.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
className="carousel-control-prev"
|
||||
type="button"
|
||||
data-bs-target={`#${id}`}
|
||||
data-bs-slide="prev"
|
||||
>
|
||||
<span
|
||||
className="carousel-control-prev-icon"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span className="visually-hidden">Anterior</span>
|
||||
</button>
|
||||
<button
|
||||
className="carousel-control-next"
|
||||
type="button"
|
||||
data-bs-target={`#${id}`}
|
||||
data-bs-slide="next"
|
||||
>
|
||||
<span
|
||||
className="carousel-control-next-icon"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span className="visually-hidden">Siguiente</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { GetCuestionario } from '@/types/cuestionario';
|
||||
import React, { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface Props {
|
||||
cuestionario: GetCuestionario;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export default function FormularioCard({ cuestionario, link }: Props) {
|
||||
const [verMas, setVerMas] = useState(false);
|
||||
const descripcion = cuestionario.descripcion;
|
||||
const limite = 100;
|
||||
|
||||
const descripcionRecortada =
|
||||
descripcion.length > limite && !verMas
|
||||
? `${descripcion.slice(0, limite)}...`
|
||||
: descripcion;
|
||||
|
||||
return (
|
||||
<div className="col-md-4 my-4">
|
||||
<div className="card card-blog d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<div className="card-image">
|
||||
<Link href={link}>
|
||||
<Image width={400} height={300} className="img" src="/banner1.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_tipo_cuestionario === 1 ? 'Feria' : 'Evento'}
|
||||
</h6>
|
||||
<p className="card-description mb-0">
|
||||
{descripcionRecortada}
|
||||
{descripcion.length > limite && (
|
||||
<button
|
||||
onClick={() => setVerMas(!verMas)}
|
||||
className="btn btn-link btn-sm p-0 ms-1 align-baseline"
|
||||
style={{ fontSize: '0.875rem' }}
|
||||
>
|
||||
{verMas ? 'Ver menos' : 'Ver más'}
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// components/QrReader.tsx
|
||||
'use client';
|
||||
|
||||
import { Scanner } from '@yudiel/react-qr-scanner';
|
||||
|
||||
export default function QrReader() {
|
||||
return <Scanner onScan={(result) => console.log(result)} />;
|
||||
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
// file: Html5QrcodePlugin.tsx
|
||||
'use client';
|
||||
|
||||
import { Html5QrcodeScanner, Html5QrcodeCameraScanConfig } from 'html5-qrcode';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const qrcodeRegionId = 'html5qr-code-full-region';
|
||||
|
||||
interface Html5QrcodePluginProps extends Partial<Html5QrcodeCameraScanConfig> {
|
||||
verbose?: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
qrCodeSuccessCallback: (decodedText: string, decodedResult: any) => void;
|
||||
qrCodeErrorCallback?: (errorMessage: string) => void;
|
||||
}
|
||||
|
||||
const createConfig = (
|
||||
props: Html5QrcodePluginProps
|
||||
): Html5QrcodeCameraScanConfig => {
|
||||
const config: Html5QrcodeCameraScanConfig = { fps: 10 }; // Default fps value set to 10
|
||||
|
||||
if (props.fps !== undefined) {
|
||||
config.fps = props.fps;
|
||||
}
|
||||
if (props.qrbox !== undefined) {
|
||||
config.qrbox = props.qrbox;
|
||||
}
|
||||
if (props.aspectRatio !== undefined) {
|
||||
config.aspectRatio = props.aspectRatio;
|
||||
}
|
||||
if (props.disableFlip !== undefined) {
|
||||
config.disableFlip = props.disableFlip;
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
const Html5QrcodePlugin: React.FC<Html5QrcodePluginProps> = (props) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => {
|
||||
// Previene ejecución en SSR
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const config = createConfig(props);
|
||||
const verbose = props.verbose === true;
|
||||
|
||||
if (!props.qrCodeSuccessCallback) {
|
||||
throw new Error('qrCodeSuccessCallback is required.');
|
||||
}
|
||||
|
||||
const html5QrcodeScanner = new Html5QrcodeScanner(
|
||||
qrcodeRegionId,
|
||||
config,
|
||||
verbose
|
||||
);
|
||||
html5QrcodeScanner.render(
|
||||
props.qrCodeSuccessCallback,
|
||||
props.qrCodeErrorCallback
|
||||
);
|
||||
|
||||
return () => {
|
||||
html5QrcodeScanner.clear().catch((error) => {
|
||||
console.error('Failed to clear html5QrcodeScanner: ', error);
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div id={qrcodeRegionId} />;
|
||||
};
|
||||
|
||||
export default Html5QrcodePlugin;
|
||||
@@ -1,4 +1,4 @@
|
||||
export const res: CuestionarioCompleto = {
|
||||
export const res = {
|
||||
tipo_cuestionario: {
|
||||
id_tipo_cuestionario: 1,
|
||||
tipo_cuestionario: 'Encuesta',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { FormularioCreateDto } from '@/types/cuestionario';
|
||||
|
||||
export const plantillasDisponibles = [
|
||||
{
|
||||
imagen: '/banner1.png',
|
||||
nombre: 'Registro para la Feria',
|
||||
id: 'feria-sexualidad',
|
||||
datos: {
|
||||
|
||||
Vendored
+4
@@ -117,3 +117,7 @@ ul.list-unstyled li {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
export interface FormularioCreacion {
|
||||
nombre_form: string;
|
||||
descripcion: string;
|
||||
fecha_inicio: string; // ISO 8601 date string
|
||||
fecha_fin: string;
|
||||
id_tipo_cuestionario: number;
|
||||
evento: string;
|
||||
secciones: SeccionFormulario[];
|
||||
}
|
||||
|
||||
export interface SeccionFormulario {
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
preguntas: PreguntaFormulario[];
|
||||
}
|
||||
|
||||
export interface PreguntaFormulario {
|
||||
titulo: string;
|
||||
tipo: 'Cerrada' | 'Abierta' | 'Multiple';
|
||||
opciones?: OpcionFormulario[];
|
||||
obligatoria: boolean;
|
||||
}
|
||||
|
||||
export interface OpcionFormulario {
|
||||
valor: string;
|
||||
}
|
||||
Vendored
+18
-65
@@ -1,3 +1,21 @@
|
||||
/**
|
||||
* Respuesta del endpoint GET /cuestionario
|
||||
*/
|
||||
export interface GetCuestionario {
|
||||
id_cuestionario: number;
|
||||
nombre_form: string;
|
||||
descripcion: string;
|
||||
contador_secciones: number;
|
||||
editable: boolean;
|
||||
fecha_fin: string; // Formato ISO: 'YYYY-MM-DDTHH:mm:ss'
|
||||
fecha_inicio: string; // Formato ISO: 'YYYY-MM-DDTHH:mm:ss'
|
||||
id_tipo_cuestionario: number;
|
||||
}
|
||||
|
||||
/* */
|
||||
/* */
|
||||
/* */
|
||||
|
||||
export interface FormularioCreateDto {
|
||||
nombre_form: string;
|
||||
descripcion: string;
|
||||
@@ -14,13 +32,6 @@ export interface Seccion {
|
||||
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;
|
||||
}
|
||||
@@ -39,61 +50,3 @@ export interface CuestionarioMetadata {
|
||||
}
|
||||
|
||||
/* 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: {
|
||||
id_seccion: number;
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
contador_preguntas: number;
|
||||
};
|
||||
preguntas: SeccionPregunta[];
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
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,73 @@
|
||||
import { GetCuestionario } from "./cuestionario";
|
||||
|
||||
export interface CuestionarioResponse {
|
||||
tipo_cuestionario: TipoCuestionario;
|
||||
evento: Evento;
|
||||
cuestionario: Cuestionario;
|
||||
}
|
||||
|
||||
export interface TipoCuestionario {
|
||||
id_tipo_cuestionario: number;
|
||||
tipo_cuestionario: string;
|
||||
}
|
||||
|
||||
export interface Evento {
|
||||
id_evento: number;
|
||||
nombre_evento: string;
|
||||
tipo_evento: string;
|
||||
fecha_inicio: string; // ISO format date string
|
||||
fecha_fin: string;
|
||||
}
|
||||
|
||||
export interface Cuestionario extends GetCuestionario {
|
||||
secciones: CuestionarioSeccion[];
|
||||
}
|
||||
|
||||
export interface CuestionarioSeccion {
|
||||
id_cuestionario_seccion: number;
|
||||
posicion: number;
|
||||
seccion: Seccion;
|
||||
preguntas: SeccionPregunta[];
|
||||
}
|
||||
|
||||
export interface Seccion {
|
||||
id_seccion: number;
|
||||
contador_pregunta: number;
|
||||
descripcion: string;
|
||||
titulo: string;
|
||||
}
|
||||
|
||||
export interface SeccionPregunta {
|
||||
id_seccion_pregunta: number;
|
||||
posicion: number;
|
||||
pregunta: Pregunta;
|
||||
}
|
||||
|
||||
export 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[];
|
||||
}
|
||||
|
||||
export interface TipoPregunta {
|
||||
id_tipo: number;
|
||||
tipo_pregunta: string;
|
||||
}
|
||||
|
||||
export interface PreguntaOpcion {
|
||||
id_pregunta_opcion: number;
|
||||
posicion: number;
|
||||
id_opcion: number;
|
||||
opcion: Opcion;
|
||||
}
|
||||
|
||||
export interface Opcion {
|
||||
id_opcion: number;
|
||||
opcion: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user