619 lines
19 KiB
Vue
619 lines
19 KiB
Vue
<template>
|
|
<div class="container mt-5">
|
|
<!-- Notificación flotante sin botón de cierre -->
|
|
<b-notification
|
|
v-if="pendingItems.length && !isMobile"
|
|
type="is-warning"
|
|
class="notification-floating"
|
|
aria-close-label="Not applicable"
|
|
:closable="false"
|
|
>
|
|
<p><strong>Te faltan contestar:</strong></p>
|
|
<ul>
|
|
<li v-for="(item, idx) in pendingItems" :key="idx">
|
|
{{ item }}
|
|
</li>
|
|
</ul>
|
|
</b-notification>
|
|
|
|
<div>
|
|
<div
|
|
v-for="(item, index) in sortedItems"
|
|
:key="item.id || item.idTabla"
|
|
class="mb-6"
|
|
v-if="isItemVisible(item)"
|
|
>
|
|
<!-- PREGUNTAS (texto, selección única, selección múltiple) -->
|
|
<div v-if="item.tipo" class="mt-6">
|
|
<label :for="item.id" class="form-label">
|
|
{{ item.numeroPregunta }}. {{ item.texto }}
|
|
</label>
|
|
|
|
<!-- Selección Única -->
|
|
<div v-if="item.tipo === 'seleccionUnica'">
|
|
<div v-for="opcion in item.opciones" :key="opcion" class="SINO">
|
|
<b-radio
|
|
v-model="respuestas[item.id]"
|
|
:id="item.id"
|
|
:native-value="opcion"
|
|
type="is-info"
|
|
>
|
|
{{ opcion }}
|
|
</b-radio>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Selección Múltiple -->
|
|
<div v-if="item.tipo === 'seleccionMultiple'">
|
|
<div v-for="opcion in item.opciones" :key="opcion" class="SINO">
|
|
<input
|
|
class="checkb"
|
|
type="checkbox"
|
|
:value="opcion"
|
|
v-model="respuestas[item.id]"
|
|
/>
|
|
{{ opcion }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Texto -->
|
|
<div v-if="item.tipo === 'texto'">
|
|
<b-input
|
|
:id="item.id"
|
|
v-model="respuestas[item.id]"
|
|
|
|
:maxlength="item.limite"
|
|
show-counter
|
|
placeholder="Escribe tu respuesta aquí"
|
|
></b-input>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TABLAS -->
|
|
<div class="table-responsive" v-else>
|
|
<template
|
|
v-if="
|
|
index === 0 ||
|
|
item.numeroPregunta !== sortedItems[index - 1].numeroPregunta
|
|
"
|
|
>
|
|
<h3 class="mb-3">
|
|
{{ item.numeroPregunta }}. {{ item.preguntaTabla }}
|
|
</h3>
|
|
</template>
|
|
|
|
<h5 v-if="item.subPreguntaTabla" class="mb-3">
|
|
{{ item.subPreguntaTabla }}
|
|
</h5>
|
|
|
|
<table class="table table-bordered text-center">
|
|
<thead>
|
|
<tr>
|
|
<th></th>
|
|
<th v-for="opcion in item.renglones[0].opciones" :key="opcion">
|
|
{{ opcion }}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="renglon in item.renglones" :key="renglon.idRenglon">
|
|
<td>{{ renglon.textoRenglon }}</td>
|
|
<td v-for="opcion in renglon.opciones" :key="opcion">
|
|
<input
|
|
type="radio"
|
|
:name="`tabla-${item.idTabla}-renglon-${renglon.idRenglon}`"
|
|
:value="opcion"
|
|
v-model="respuestas[item.idTabla][renglon.idRenglon]"
|
|
/>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- BOTÓN ENVIAR -->
|
|
<div class="my-6 mb-6">
|
|
<b-field class="centro">
|
|
<button
|
|
class="button is-success is-medium"
|
|
@click="submitForm"
|
|
:disabled="!isFormComplete"
|
|
>
|
|
Enviar
|
|
</button>
|
|
</b-field>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import Buefy from 'buefy'
|
|
import 'buefy/dist/buefy.css'
|
|
import axios from 'axios'
|
|
|
|
export default {
|
|
|
|
|
|
props: {
|
|
idServicio: { type: Number, required: true },
|
|
responsable: { type: Object, required: true },
|
|
imprimirError: { type: Function, required: true },
|
|
imprimirMensaje: { type: Function, required: true },
|
|
imprimirWarning: { type: Function, required: true },
|
|
updateIsLoading: { type: Function, required: true },
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
isLoading: false,
|
|
respuestas: {},
|
|
|
|
formulario: {
|
|
titulo:
|
|
'2025 Evaluación de la institución receptora de servicio social',
|
|
descripcion: `Estimado(a) responsable de programa de servicio social:
|
|
Te solicitamos llenar cuidadosamente los campos solicitados a continuación para poder
|
|
validar el término del servicio social de nuestro alumno(a).
|
|
|
|
Los datos están sujetos al aviso de privacidad integral que se puede consultar en el sitio web: www.acatlan.unam.mx/normatividad`,
|
|
|
|
preguntas: [
|
|
{
|
|
numeroPregunta: 1,
|
|
id: 'actividadesUniversitario',
|
|
tipo: 'texto',
|
|
limite: 200,
|
|
texto:
|
|
'Mencione las principales actividades que realizó el/la universitario(a) en su programa de servicio social',
|
|
},
|
|
{
|
|
numeroPregunta: 2,
|
|
id: 'calificacionGeneral',
|
|
tipo: 'seleccionUnica',
|
|
texto:
|
|
'Haciendo una evaluación general, ¿Cómo califica el desempeño del universitario(a)?',
|
|
opciones: ['Excelente', 'Bueno', 'Regular', 'Deficiente'],
|
|
},
|
|
{
|
|
numeroPregunta: 4,
|
|
id: 'cursosComplementarios',
|
|
tipo: 'texto',
|
|
limite: 200,
|
|
texto:
|
|
'¿Qué cursos considera que complementarían la formación de nuestros egresados?',
|
|
},
|
|
{
|
|
numeroPregunta: 5,
|
|
id: 'contratariaFuturo',
|
|
tipo: 'seleccionUnica',
|
|
texto: '¿Considera contratar en un futuro al universitario(a)?',
|
|
opciones: ['Sí', 'No'],
|
|
},
|
|
{
|
|
numeroPregunta: 6,
|
|
id: 'porqueNoContrataria',
|
|
tipo: 'texto',
|
|
limite: 200,
|
|
texto:
|
|
'Si su respuesta a la pregunta anterior fue NO, comente ¿por qué?',
|
|
condicional: {
|
|
preguntaId: 'contratariaFuturo',
|
|
valor: 'No',
|
|
},
|
|
},
|
|
],
|
|
|
|
tablas: [
|
|
// ----- PREGUNTA 3, dividida en sub-secciones A, B, C, D ------
|
|
{
|
|
idTabla: 301,
|
|
numeroPregunta: 3,
|
|
preguntaTabla:
|
|
'Haciendo una evaluación general, ¿Cómo Califica el desempeño del universitario(a)?',
|
|
subPreguntaTabla: 'A. Habilidades personales',
|
|
renglones: [
|
|
{
|
|
idRenglon: 1,
|
|
textoRenglon: 'Puntualidad',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
{
|
|
idRenglon: 2,
|
|
textoRenglon: 'Eficiencia',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
{
|
|
idRenglon: 3,
|
|
textoRenglon: 'Organizado',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
{
|
|
idRenglon: 4,
|
|
textoRenglon: 'Eficacia',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
idTabla: 302,
|
|
numeroPregunta: 3,
|
|
preguntaTabla:
|
|
'3. Haciendo una evaluación general, ¿Cómo califica el desempeño del universitario(a)?',
|
|
subPreguntaTabla: 'B. Habilidades profesionales',
|
|
renglones: [
|
|
{
|
|
idRenglon: 1,
|
|
textoRenglon: 'Disposición',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
{
|
|
idRenglon: 2,
|
|
textoRenglon: 'Proactivo/anticipación',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
{
|
|
idRenglon: 3,
|
|
textoRenglon: 'Propositivo',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
{
|
|
idRenglon: 4,
|
|
textoRenglon: 'Resolutivo',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
idTabla: 303,
|
|
numeroPregunta: 3,
|
|
preguntaTabla:
|
|
'3. Haciendo una evaluación general, ¿Cómo califica el desempeño del universitario(a)?',
|
|
subPreguntaTabla: 'C. Habilidades sociales',
|
|
renglones: [
|
|
{
|
|
idRenglon: 1,
|
|
textoRenglon: 'Empatía',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
{
|
|
idRenglon: 2,
|
|
textoRenglon: 'Trabajo en equipo',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
{
|
|
idRenglon: 3,
|
|
textoRenglon: 'Comunicación asertiva',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
{
|
|
idRenglon: 4,
|
|
textoRenglon: 'Receptividad',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
{
|
|
idRenglon: 5,
|
|
textoRenglon: 'Tolerancia a la frustración',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
idTabla: 304,
|
|
numeroPregunta: 3,
|
|
preguntaTabla:
|
|
'3. Haciendo una evaluación general, ¿Cómo califica el desempeño del universitario(a)?',
|
|
subPreguntaTabla: 'D. Conocimientos',
|
|
renglones: [
|
|
{
|
|
idRenglon: 1,
|
|
textoRenglon: 'Conocimientos teóricos',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
{
|
|
idRenglon: 2,
|
|
textoRenglon: 'Conocimientos metodológicos',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
{
|
|
idRenglon: 3,
|
|
textoRenglon: 'Pensamiento crítico',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
{
|
|
idRenglon: 4,
|
|
textoRenglon: 'Formación académica general',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
{
|
|
idRenglon: 5,
|
|
textoRenglon: 'Uso tecnológico',
|
|
opciones: ['Deficiente', 'Básico', 'Intermedio', 'Destacado'],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
}
|
|
},
|
|
|
|
created() {
|
|
// Inicializa las respuestas de cada pregunta
|
|
this.formulario.preguntas.forEach((pregunta) => {
|
|
if (pregunta.tipo === 'seleccionMultiple') {
|
|
this.$set(this.respuestas, pregunta.id, [])
|
|
} else {
|
|
this.$set(this.respuestas, pregunta.id, '')
|
|
}
|
|
})
|
|
|
|
// Inicializa las respuestas para cada tabla
|
|
this.formulario.tablas.forEach((tabla) => {
|
|
this.$set(this.respuestas, tabla.idTabla, {})
|
|
tabla.renglones.forEach((renglon) => {
|
|
this.$set(this.respuestas[tabla.idTabla], renglon.idRenglon, null)
|
|
})
|
|
})
|
|
},
|
|
|
|
computed: {
|
|
|
|
|
|
isMobile() {
|
|
return window.innerWidth < 576
|
|
},
|
|
|
|
|
|
// Combina preguntas y tablas en un array ordenado
|
|
sortedItems() {
|
|
const items = [...this.formulario.preguntas, ...this.formulario.tablas]
|
|
return items.sort((a, b) => {
|
|
if (a.numeroPregunta === b.numeroPregunta) {
|
|
return (a.idTabla || 0) - (b.idTabla || 0)
|
|
}
|
|
return a.numeroPregunta - b.numeroPregunta
|
|
})
|
|
},
|
|
|
|
// Determina los ítems (preguntas/tablas) que siguen pendientes
|
|
pendingItems() {
|
|
let missing = []
|
|
|
|
// Revisar preguntas (solo las que estén visibles)
|
|
this.formulario.preguntas.forEach((p) => {
|
|
if (!this.isItemVisible(p)) return
|
|
const answer = this.respuestas[p.id]
|
|
let answered = false
|
|
|
|
if (p.tipo === 'seleccionMultiple') {
|
|
answered = answer && answer.length > 0
|
|
} else {
|
|
answered = answer !== null && answer !== ''
|
|
}
|
|
|
|
if (!answered) {
|
|
missing.push(`Pregunta ${p.numeroPregunta}`)
|
|
}
|
|
})
|
|
|
|
// Revisar tablas
|
|
this.formulario.tablas.forEach((t) => {
|
|
let allAnswered = t.renglones.every(
|
|
(r) =>
|
|
this.respuestas[t.idTabla] &&
|
|
this.respuestas[t.idTabla][r.idRenglon] !== null
|
|
)
|
|
if (!allAnswered) {
|
|
missing.push(`Tabla ${t.numeroPregunta}`)
|
|
}
|
|
})
|
|
|
|
return missing
|
|
},
|
|
|
|
// El formulario se considera completo si no hay ítems pendientes
|
|
isFormComplete() {
|
|
return this.pendingItems.length === 0
|
|
},
|
|
},
|
|
|
|
methods: {
|
|
// Determina si un item (pregunta/tabla) debe mostrarse (condicional)
|
|
isItemVisible(item) {
|
|
if (item.condicional) {
|
|
// Checa la respuesta de la pregunta 'preguntaId'
|
|
const respuestaCond = this.respuestas[item.condicional.preguntaId]
|
|
return respuestaCond === item.condicional.valor
|
|
}
|
|
return true
|
|
},
|
|
|
|
submitForm() {
|
|
if (!this.validateResponses()) {
|
|
return
|
|
}
|
|
|
|
console.log("tokens", this.responsable.token );
|
|
|
|
console.log('Respuestas enviadas:', this.respuestas)
|
|
|
|
// Convertir a formato p1, p2, p3_A_1, etc.
|
|
const respuestaFormateada = this.formatearRespuestas(this.respuestas)
|
|
console.log('Respuestas formateadas:', respuestaFormateada)
|
|
|
|
let data = {
|
|
...respuestaFormateada,
|
|
idServicio: localStorage.getItem('idServicio'),
|
|
}
|
|
|
|
|
|
|
|
|
|
this.isLoading = true
|
|
axios
|
|
.post(`${process.env.api}/cuestionario_programa`, data , this.responsable.token)
|
|
.then(() => {
|
|
this.$buefy.dialog.alert({
|
|
title: 'Success',
|
|
message: 'Se enviaron los datos correctamente',
|
|
type: 'is-success',
|
|
hasIcon: true,
|
|
icon: 'checkbox-marked-circle',
|
|
ariaRole: 'alertdialog',
|
|
ariaModal: true,
|
|
})
|
|
localStorage.removeItem('idCuestionarioAlumno')
|
|
this.$router.push('/alumno')
|
|
})
|
|
.catch((error) => {
|
|
this.$buefy.dialog.alert({
|
|
title: 'Error',
|
|
message:
|
|
error.response?.data?.message || 'Error al enviar el formulario',
|
|
type: 'is-danger',
|
|
})
|
|
})
|
|
.finally(() => {
|
|
this.isLoading = false
|
|
})
|
|
},
|
|
|
|
// Valida que las preguntas y tablas visibles estén contestadas
|
|
validateResponses() {
|
|
// Validar preguntas
|
|
for (const pregunta of this.formulario.preguntas) {
|
|
if (!this.isItemVisible(pregunta)) continue
|
|
|
|
const respuesta = this.respuestas[pregunta.id]
|
|
|
|
// Verifica que no esté vacía
|
|
if (pregunta.tipo === 'seleccionMultiple') {
|
|
if (!respuesta || respuesta.length === 0) {
|
|
this.$buefy.dialog.alert({
|
|
title: 'Error',
|
|
message: `Por favor, selecciona al menos una opción en la pregunta ${pregunta.numeroPregunta}.`,
|
|
type: 'is-danger',
|
|
})
|
|
return false
|
|
}
|
|
} else {
|
|
if (respuesta === null || respuesta === '') {
|
|
this.$buefy.dialog.alert({
|
|
title: 'Error',
|
|
message: `Por favor, responde la pregunta ${pregunta.numeroPregunta}.`,
|
|
type: 'is-danger',
|
|
})
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Si es texto y excede 500 caracteres
|
|
if (pregunta.tipo === 'texto' && respuesta.length > 500) {
|
|
this.$buefy.dialog.alert({
|
|
title: 'Error',
|
|
message: `La respuesta de la pregunta ${pregunta.numeroPregunta} es demasiado larga (máx. 500 caracteres).`,
|
|
type: 'is-danger',
|
|
})
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Validar tablas
|
|
for (const tabla of this.formulario.tablas) {
|
|
for (const renglon of tabla.renglones) {
|
|
const respRenglon = this.respuestas[tabla.idTabla]?.[renglon.idRenglon]
|
|
if (respRenglon === null) {
|
|
this.$buefy.dialog.alert({
|
|
title: 'Error',
|
|
message: `Por favor, responde la tabla ${tabla.numeroPregunta}, fila "${renglon.textoRenglon}".`,
|
|
type: 'is-danger',
|
|
})
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
},
|
|
|
|
// Convierte las respuestas en formato { p1: ..., p3_A_1: ..., etc. }
|
|
formatearRespuestas(respuestas) {
|
|
const resultado = {}
|
|
|
|
for (const key in respuestas) {
|
|
const esTabla = !isNaN(Number(key))
|
|
if (esTabla) {
|
|
// Es una tabla
|
|
const idTabla = parseInt(key, 10)
|
|
const tablaDef = this.formulario.tablas.find(
|
|
(t) => t.idTabla === idTabla
|
|
)
|
|
if (!tablaDef) continue
|
|
|
|
const numPregunta = tablaDef.numeroPregunta
|
|
let subLetra = ''
|
|
if (tablaDef.subPreguntaTabla) {
|
|
const match = tablaDef.subPreguntaTabla.match(/^([A-E])\./)
|
|
if (match) subLetra = match[1]
|
|
}
|
|
|
|
const respuestasTabla = respuestas[key]
|
|
for (const idRenglon in respuestasTabla) {
|
|
const valor = respuestasTabla[idRenglon]
|
|
let clave = `p${numPregunta}_${idRenglon}`
|
|
if (subLetra) {
|
|
clave = `p${numPregunta}_${subLetra}_${idRenglon}`
|
|
}
|
|
resultado[clave] = valor
|
|
}
|
|
} else {
|
|
// Es una pregunta normal
|
|
const preguntaDef = this.formulario.preguntas.find(
|
|
(p) => p.id === key
|
|
)
|
|
if (!preguntaDef) continue
|
|
|
|
const numPregunta = preguntaDef.numeroPregunta
|
|
const clave = `p${numPregunta}`
|
|
resultado[clave] = respuestas[key]
|
|
}
|
|
}
|
|
|
|
return resultado
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Notificación flotante en parte inferior derecha, sin botón de cerrar */
|
|
.notification-floating {
|
|
position: fixed !important;
|
|
bottom: 2rem;
|
|
right: 2rem;
|
|
z-index: 9999;
|
|
max-width: 350px;
|
|
width: auto; /* o 90% si prefieres */
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* Ajustes en pantallas pequeñas (ej. < 480px) */
|
|
@media (max-width: 480px) {
|
|
.notification-floating {
|
|
right: 1rem;
|
|
left: 1rem; /* Opcional: para centrar la notificación */
|
|
bottom: 1rem;
|
|
max-width: none; /* Para que no se limite a 350px */
|
|
width: auto; /* O 90%, como prefieras */
|
|
}
|
|
}
|
|
|
|
|
|
.table-responsive {
|
|
width: 100%;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
</style>
|