Responder y crear formularios

This commit is contained in:
jalvarado
2025-04-01 17:01:58 -06:00
parent cbd2659f98
commit d2759c0afe
10 changed files with 999 additions and 25 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

@@ -0,0 +1,489 @@
'use client';
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[];
}
export default function AdminFormPage() {
const [nombreFormulario, setNombreFormulario] = useState('');
const [descripcion, setDescripcion] = useState('');
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 swapItems = <T,>(arr: T[], index1: number, index2: number): T[] => {
const result = [...arr];
[result[index1], result[index2]] = [result[index2], result[index1]];
return result;
};
const moverSeccion = (from: number, to: number) => {
if (to >= 0 && to < secciones.length) {
setSecciones((prev) => swapItems(prev, from, to));
setPaginaActual(to + 1);
}
};
const moverPregunta = (seccionIndex: number, from: number, to: number) => {
const nuevas = [...secciones];
const preguntas = nuevas[seccionIndex].preguntas;
if (to >= 0 && to < preguntas.length) {
nuevas[seccionIndex].preguntas = swapItems(preguntas, from, to);
setSecciones(nuevas);
}
};
const agregarSeccion = () => {
setSecciones((prev) => [
...prev,
{ titulo: '', descripcion: '', preguntas: [] },
]);
setPaginaActual(secciones.length + 1);
};
const eliminarSeccion = (index: number) => {
const nuevas = secciones.filter((_, i) => i !== index);
setSecciones(nuevas);
setPaginaActual((prev) =>
Math.max(1, prev > nuevas.length ? nuevas.length : prev)
);
};
const actualizarSeccion = <K extends keyof Seccion>(
index: number,
campo: K,
valor: Seccion[K]
) => {
setSecciones((prev) => {
const nuevas = [...prev];
nuevas[index][campo] = valor;
return nuevas;
});
};
const agregarPregunta = (seccionIndex: number) => {
const nuevas = [...secciones];
nuevas[seccionIndex].preguntas.push({
titulo: '',
tipo: 'Abierta',
opciones: [],
obligatoria: false,
});
setSecciones(nuevas);
};
const eliminarPregunta = (seccionIndex: number, preguntaIndex: number) => {
const nuevas = [...secciones];
nuevas[seccionIndex].preguntas.splice(preguntaIndex, 1);
setSecciones(nuevas);
};
const actualizarPregunta = (
seccionIndex: number,
preguntaIndex: number,
campo: keyof Pregunta,
valor: Pregunta[keyof Pregunta]
) => {
setSecciones((prev) => {
const nuevas = [...prev];
const pregunta = nuevas[seccionIndex].preguntas[preguntaIndex];
nuevas[seccionIndex].preguntas[preguntaIndex] = {
...pregunta,
[campo]: valor,
};
return nuevas;
});
};
const agregarOpcion = (seccionIndex: number, preguntaIndex: number) => {
const nuevas = [...secciones];
nuevas[seccionIndex].preguntas[preguntaIndex].opciones.push({ valor: '' });
setSecciones(nuevas);
};
const eliminarOpcion = (
seccionIndex: number,
preguntaIndex: number,
opcionIndex: number
) => {
const nuevas = [...secciones];
nuevas[seccionIndex].preguntas[preguntaIndex].opciones.splice(
opcionIndex,
1
);
setSecciones(nuevas);
};
const actualizarOpcion = (
seccionIndex: number,
preguntaIndex: number,
opcionIndex: number,
valor: string
) => {
const nuevas = [...secciones];
nuevas[seccionIndex].preguntas[preguntaIndex].opciones[opcionIndex].valor =
valor;
setSecciones(nuevas);
};
const guardarFormulario = () => {
const formData = {
nombre: nombreFormulario,
descripcion,
secciones,
};
console.log('Formulario guardado:', JSON.stringify(formData));
alert('Formulario guardado en consola');
};
const seccionActual = secciones[paginaActual - 1];
const handleBannerChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] || null;
if (file) {
const reader = new FileReader();
reader.onload = () => setBannerPreview(reader.result as string);
reader.readAsDataURL(file);
} else {
setBannerPreview(null);
}
};
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>
<div className='mb-4'>
<label className='form-label'>Banner del formulario</label>
<input
type='file'
accept='image/*'
className='form-control mb-2'
onChange={handleBannerChange}
/>
{bannerPreview && (
<div className='text-center'>
<Image
src={bannerPreview}
width={1000}
height={300}
alt='Ejemplo de banner'
className='rounded-4 shadow-sm'
style={{
objectFit: 'cover',
objectPosition: 'center',
}}
/>
</div>
)}
</div>
<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>
<div className='mb-3'>
<label className='form-label'>Descripción</label>
<textarea
className='form-control'
rows={3}
value={descripcion}
onChange={(e) => setDescripcion(e.target.value)}
/>
</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>
<div className='mb-4'>
<button
className='btn btn-outline-primary mb-3'
onClick={agregarSeccion}
>
+ Agregar sección
</button>
{seccionActual && (
<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'>
<button
className='btn btn-sm btn-outline-secondary'
onClick={() =>
moverSeccion(paginaActual - 1, paginaActual - 2)
}
disabled={paginaActual === 1}
>
</button>
<button
className='btn btn-sm btn-outline-secondary'
onClick={() => moverSeccion(paginaActual - 1, paginaActual)}
disabled={paginaActual === secciones.length}
>
</button>
<button
className='btn btn-sm btn-danger'
onClick={() => eliminarSeccion(paginaActual - 1)}
>
Eliminar sección
</button>
</div>
</div>
<input
type='text'
className='form-control mb-2'
placeholder='Título de la sección'
value={seccionActual.titulo}
onChange={(e) =>
actualizarSeccion(paginaActual - 1, 'titulo', e.target.value)
}
/>
<textarea
className='form-control mb-3'
placeholder='Descripción de la sección'
value={seccionActual.descripcion}
onChange={(e) =>
actualizarSeccion(
paginaActual - 1,
'descripcion',
e.target.value
)
}
/>
{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'>
<strong>Pregunta {pIdx + 1}</strong>
<div className='d-flex gap-2'>
<button
className='btn btn-sm btn-outline-secondary'
onClick={() =>
moverPregunta(paginaActual - 1, pIdx, pIdx - 1)
}
disabled={pIdx === 0}
>
</button>
<button
className='btn btn-sm btn-outline-secondary'
onClick={() =>
moverPregunta(paginaActual - 1, pIdx, pIdx + 1)
}
disabled={pIdx === seccionActual.preguntas.length - 1}
>
</button>
<button
className='btn btn-sm btn-outline-danger'
onClick={() => eliminarPregunta(paginaActual - 1, pIdx)}
>
Eliminar
</button>
</div>
</div>
<input
type='text'
className='form-control my-2'
placeholder='Texto de la pregunta'
value={pregunta.titulo}
onChange={(e) =>
actualizarPregunta(
paginaActual - 1,
pIdx,
'titulo',
e.target.value
)
}
/>
<select
className='form-select mb-2'
value={pregunta.tipo}
onChange={(e) =>
actualizarPregunta(
paginaActual - 1,
pIdx,
'tipo',
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
className='form-check-input'
type='checkbox'
id={`obligatoria-${pIdx}`}
checked={pregunta.obligatoria}
onChange={(e) =>
actualizarPregunta(
paginaActual - 1,
pIdx,
'obligatoria',
e.target.checked
)
}
/>
<label
className='form-check-label'
htmlFor={`obligatoria-${pIdx}`}
>
¿Es obligatoria?
</label>
</div>
{(pregunta.tipo === 'Cerrada' ||
pregunta.tipo === 'Multiple') && (
<>
<button
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'>
<input
type='text'
className='form-control'
placeholder={`Opción ${oIdx + 1}`}
value={op.valor}
onChange={(e) =>
actualizarOpcion(
paginaActual - 1,
pIdx,
oIdx,
e.target.value
)
}
/>
<button
className='btn btn-outline-danger'
onClick={() =>
eliminarOpcion(paginaActual - 1, pIdx, oIdx)
}
>
x
</button>
</div>
))}
</>
)}
</div>
))}
<button
className='btn btn-sm btn-outline-primary'
onClick={() => agregarPregunta(paginaActual - 1)}
>
+ Agregar pregunta
</button>
</div>
)}
{secciones.length > 1 && (
<div className='d-flex justify-content-center mb-4'>
<Pagination
currentPage={paginaActual}
totalPages={secciones.length}
onPageChange={setPaginaActual}
/>
</div>
)}
</div>
<button className='btn btn-success' onClick={guardarFormulario}>
Guardar formulario
</button>
<div className='my-4 d-flex gap-2'>
<button className='btn btn-success' onClick={imprimirFormulario}>
Imprimir cuestionario
</button>
</div>
</>
);
}
+27
View File
@@ -0,0 +1,27 @@
import Footer from '@/components/layout/footer';
import Header from '@/components/layout/header';
import Link from 'next/link';
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'>
<nav className='d-flex gap-2 my-4'>
<Link href={'/administrador/'} className='text-decoration-none'>
<div className='box'>Formularios</div>
</Link>
<Link
href={'/administrador/crear-formulario'}
className='text-decoration-none'
>
<div className='box'>Crear cuestionario</div>
</Link>
</nav>
{children}
</main>
<Footer />
</div>
);
}
+9
View File
@@ -0,0 +1,9 @@
import React from 'react';
export default function Page() {
return (
<>
<h2 className='my-4'>Formularios</h2>
</>
);
}
+140 -5
View File
@@ -1,12 +1,147 @@
import Footer from "@/components/layout/footer";
import Header from "@/components/layout/header";
import React from "react";
'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 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">
<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'
style={{
objectFit: 'cover',
objectPosition: 'top',
}}
/>
</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>
+14 -18
View File
@@ -1,4 +1,4 @@
import React, { useMemo } from "react";
import React, { useMemo } from 'react';
interface PaginationProps {
totalPages: number;
@@ -43,15 +43,15 @@ export default function Pagination({
}, [currentPage, totalPages, delta]);
return (
<nav aria-label="Page navigation">
<ul className="pagination gap-1">
<nav aria-label='Page navigation'>
<ul className='pagination gap-1'>
{/* Botón Anterior */}
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
<button
className="page-link shadow-sm rounded border-0 bg-white"
className='page-link box p-0'
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
aria-label="Previous"
aria-label='Previous'
>
&laquo;
</button>
@@ -60,21 +60,17 @@ export default function Pagination({
{/* Números de Página */}
{visiblePages.map((page, index) =>
page === -1 ? (
<li key={`ellipsis-${index}`} className="page-item disabled">
<span className="page-link shadow-sm rounded border-0 bg-white">
...
</span>
<li key={`ellipsis-${index}`} className='page-item disabled'>
<span className='page-link box p-0'>...</span>
</li>
) : (
<li
key={page}
className={`page-item ${currentPage === page ? "active" : ""}`}
className={`page-item ${currentPage === page ? 'active' : ''}`}
>
<button
className={`page-link shadow-sm rounded border-0 ${
currentPage === page
? ""
: "bg-white text-primary"
className={`page-link box p-0 ${
currentPage === page ? 'active' : ''
}`}
onClick={() => handlePageChange(page)}
>
@@ -87,14 +83,14 @@ export default function Pagination({
{/* Botón Siguiente */}
<li
className={`page-item ${
currentPage === totalPages ? "disabled" : ""
currentPage === totalPages ? 'disabled' : ''
}`}
>
<button
className="page-link shadow-sm rounded border-0 bg-white"
className='page-link box p-0'
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
aria-label="Next"
aria-label='Next'
>
&raquo;
</button>
+53
View File
@@ -0,0 +1,53 @@
export const crear = {
nombreFormulario:
'Registro para la Feria de la Sexualidad - Comunidad 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.',
banner: 'base64',
secciones: [
{
titulo: 'Participación en actividades',
descripcion:
'Selecciona las actividades en las que te gustaría participar durante la Feria.',
preguntas: [
{
titulo: '¿Qué tipo de actividades te interesan?',
tipo: 'Multiple',
opciones: [
{ valor: 'Talleres sobre salud sexual' },
{ valor: 'Charlas informativas' },
{ valor: 'Mesas de diálogo y reflexión' },
],
obligatoria: true,
},
{
titulo:
'¿Estás interesado/a en recibir un certificado de participación? ',
tipo: 'Cerrada',
opciones: [{ valor: 'Si' }, { valor: 'No' }],
obligatoria: false,
},
],
},
{
titulo: 'Comentarios',
descripcion:
'Comparte tus ideas, inquietudes o sugerencias para esta y futuras ediciones',
preguntas: [
{
titulo:
'¿Hay algún tema específico que te gustaría que se abordara en la feria? ',
tipo: 'Abierta',
opciones: [],
obligatoria: false,
},
{
titulo: 'Comentarios adicionales',
tipo: 'Abierta',
opciones: [],
obligatoria: false,
},
],
},
],
};
+195
View File
@@ -0,0 +1,195 @@
export const res: CuestionarioCompleto = {
tipo_cuestionario: {
id_tipo_cuestionario: 1,
tipo_cuestionario: 'Encuesta',
},
cuestionario: {
id_cuestionario: 10,
nombre_form: 'Encuesta de Satisfacción',
contador_secciones: 2,
descripcion: 'Encuesta para medir la satisfacción del cliente',
editable: true,
fecha_inicio: '2025-03-26T00:00:00',
fecha_fin: '2025-03-31T23:59:59',
id_cuestionario_original: null,
id_tipo_cuestionario: 1,
secciones: [
{
id_cuestionario_seccion: 1,
posicion: 1,
seccion: {
id_seccion: 100,
contador_pregunta: 2,
descripcion: 'Preguntas generales sobre el servicio',
titulo: 'General',
},
preguntas: [
{
id_seccion_pregunta: 1000,
posicion: 1,
pregunta: {
id_pregunta: 10000,
pregunta: '¿Cómo calificaría nuestro servicio?',
contador_opcion: 4,
obligatoria: true,
id_tipo_pregunta: 1,
id_opcion_dependiente: null,
tipo_pregunta: {
id_tipo: 1,
tipo_pregunta: 'Cerrada',
},
opciones: [
{
id_pregunta_opcion: 50000,
posicion: 1,
id_opcion: 90000,
opcion: {
id_opcion: 90000,
opcion: 'Excelente',
},
},
{
id_pregunta_opcion: 50001,
posicion: 2,
id_opcion: 90001,
opcion: {
id_opcion: 90001,
opcion: 'Bueno',
},
},
{
id_pregunta_opcion: 50002,
posicion: 3,
id_opcion: 90002,
opcion: {
id_opcion: 90002,
opcion: 'Regular',
},
},
{
id_pregunta_opcion: 50003,
posicion: 4,
id_opcion: 90003,
opcion: {
id_opcion: 90003,
opcion: 'Malo',
},
},
],
},
},
{
id_seccion_pregunta: 1001,
posicion: 2,
pregunta: {
id_pregunta: 10001,
pregunta: '¿Recomendaría nuestro servicio?',
contador_opcion: 2,
obligatoria: true,
id_tipo_pregunta: 1,
id_opcion_dependiente: null,
tipo_pregunta: {
id_tipo: 1,
tipo_pregunta: 'Cerrada',
},
opciones: [
{
id_pregunta_opcion: 50004,
posicion: 1,
id_opcion: 90004,
opcion: {
id_opcion: 90004,
opcion: 'Sí',
},
},
{
id_pregunta_opcion: 50005,
posicion: 2,
id_opcion: 90005,
opcion: {
id_opcion: 90005,
opcion: 'No',
},
},
],
},
},
],
},
{
id_cuestionario_seccion: 2,
posicion: 2,
seccion: {
id_seccion: 101,
contador_pregunta: 1,
descripcion: 'Preguntas adicionales para conocer más detalles',
titulo: 'Adicionales',
},
preguntas: [
{
id_seccion_pregunta: 1002,
posicion: 1,
pregunta: {
id_pregunta: 10002,
pregunta: '¿Qué mejorarías en nuestro servicio?',
contador_opcion: 0,
obligatoria: false,
id_tipo_pregunta: 2,
id_opcion_dependiente: null,
tipo_pregunta: {
id_tipo: 2,
tipo_pregunta: 'Abierta',
},
opciones: [],
},
},
{
id_seccion_pregunta: 1003,
posicion: 3,
pregunta: {
id_pregunta: 10003,
pregunta: '¿Qué aspectos del servicio fueron de tu agrado?',
contador_opcion: 3,
obligatoria: false,
id_tipo_pregunta: 3, // Puedes usar este ID para distinguir múltiples
id_opcion_dependiente: null,
tipo_pregunta: {
id_tipo: 3,
tipo_pregunta: 'Multiple',
},
opciones: [
{
id_pregunta_opcion: 50006,
posicion: 1,
id_opcion: 90006,
opcion: {
id_opcion: 90006,
opcion: 'Rapidez',
},
},
{
id_pregunta_opcion: 50007,
posicion: 2,
id_opcion: 90007,
opcion: {
id_opcion: 90007,
opcion: 'Atención al cliente',
},
},
{
id_pregunta_opcion: 50008,
posicion: 3,
id_opcion: 90008,
opcion: {
id_opcion: 90008,
opcion: 'Facilidad de uso',
},
},
],
},
},
],
},
],
},
};
+2 -2
View File
@@ -1,8 +1,8 @@
.box {
@extend .shadow-sm;
@extend .border;
@extend .rounded;
@extend .bg-white;
@extend .p-3;
@extend .p-2;
&.active {
@extend .bg-primary;
+70
View File
@@ -0,0 +1,70 @@
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;
descripcion: string;
titulo: string;
}
interface CuestionarioSeccion {
id_cuestionario_seccion: number;
posicion: number;
seccion: SeccionInfo;
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[];
}
interface CuestionarioCompleto {
tipo_cuestionario: TipoCuestionario;
cuestionario: Cuestionario;
}