You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

664 lines
17 KiB
Vue

2 months ago
<template>
<div class="examen-panel">
<!-- Header con información del examen -->
<a-card>
<div class="examen-header">
<div class="header-info">
<h2>{{ examenInfo.proceso || 'Proceso no disponible' }}</h2>
<p><strong>Área:</strong> {{ examenInfo.area || 'Área no disponible' }}</p>
<p><strong>Duración:</strong> {{ examenInfo.duracion }} minutos</p>
<p><strong>Intentos:</strong> {{ examenInfo.intentos }} / {{ examenInfo.intentos_maximos }}</p>
<p><strong>Tiempo restante:</strong></p>
</div>
<div class="timer">
<a-statistic-countdown
:value="timerValue"
@finish="finalizarExamenAutomaticamente"
format="HH:mm:ss"
/>
</div>
</div>
</a-card>
<!-- Preguntas -->
<a-card
v-for="(pregunta, index) in preguntasTransformadas"
:key="pregunta.id"
class="pregunta-card"
style="margin-top: 16px;"
>
<template #title>
<div class="pregunta-header">
<span class="pregunta-numero">Pregunta {{ index + 1 }}</span>
<span class="curso-tag">
<a-tag color="blue">{{ pregunta.curso }}</a-tag>
</span>
<a-tag :color="pregunta.respondida ? 'green' : 'orange'">
{{ pregunta.estado === 'respondida' ? 'Respondida' : 'Pendiente' }}
</a-tag>
</div>
</template>
<!-- Enunciado -->
<div class="enunciado" v-html="pregunta.enunciado"></div>
<!-- Contenido adicional -->
<div v-if="pregunta.extra && pregunta.extra !== pregunta.enunciado"
class="extra" v-html="pregunta.extra"></div>
<!-- Opciones múltiples -->
<div class="opciones" v-if="pregunta.opciones && pregunta.opciones.length">
<a-radio-group
v-model:value="pregunta.respuestaSeleccionada"
@change="responderPregunta(pregunta)"
:disabled="pregunta.estado === 'respondida'"
>
<a-space direction="vertical" style="width: 100%;">
<a-radio
v-for="opcion in pregunta.opcionesOrdenadas"
:key="opcion.key"
:value="opcion.key.toString()" <!-- CONVERTIR A STRING -->
class="opcion-radio"
>
<span class="opcion-key">{{ getLetraOpcion(opcion.key) }}.</span>
<span v-html="opcion.texto" class="opcion-texto"></span>
</a-radio>
</a-space>
</a-radio-group>
<!-- Mostrar respuesta seleccionada -->
<div v-if="pregunta.respuestaSeleccionada" class="seleccion-actual">
<a-alert
:message="`Ha seleccionado: ${getTextoOpcionSeleccionada(pregunta)}`"
type="info"
show-icon
style="margin-top: 12px;"
/>
</div>
</div>
<!-- Pregunta abierta (si no hay opciones) -->
<div v-else class="pregunta-abierta">
<a-textarea
v-model:value="pregunta.respuestaTexto"
placeholder="Escriba su respuesta aquí..."
:rows="4"
:disabled="pregunta.estado === 'respondida'"
@blur="responderPreguntaTexto(pregunta)"
/>
</div>
<!-- Información de respuesta correcta (solo para debug) -->
<div v-if="debugMode" class="debug-info">
<a-alert
:message="`Respuesta correcta: ${pregunta.respuesta} (key: ${pregunta.respuestaKey})`"
type="warning"
show-icon
style="margin-top: 12px;"
/>
</div>
</a-card>
<!-- Resumen y botones -->
<a-card style="margin-top: 24px;">
<div class="resumen-examen">
<h3>Resumen del Examen</h3>
<p><strong>Total preguntas:</strong> {{ preguntasTransformadas.length }}</p>
<p><strong>Respondidas:</strong> {{ preguntasRespondidas }} de {{ preguntasTransformadas.length }}</p>
<p><strong>Progreso:</strong></p>
<a-progress :percent="porcentajeCompletado" :stroke-color="{
'0%': '#108ee9',
'100%': '#87d068',
}" />
</div>
<div class="action-buttons" style="margin-top: 24px; text-align: center;">
<a-button
type="primary"
size="large"
:loading="finalizando"
@click="finalizarExamen"
:disabled="!todasRespondidas"
>
{{ todasRespondidas ? 'Finalizar Examen' : `Responda todas las preguntas (${preguntasTransformadas.length - preguntasRespondidas} pendientes)` }}
</a-button>
<a-button
type="default"
size="large"
style="margin-left: 12px;"
@click="guardarYSalir"
>
Guardar y salir
</a-button>
</div>
</a-card>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useExamenStore } from '../../store/examen.store'
import { message, Modal } from 'ant-design-vue'
const route = useRoute()
const router = useRouter()
const examenStore = useExamenStore()
const finalizando = ref(false)
const debugMode = ref(false) // Cambiar a true para depuración
const timerValue = ref(null)
// Computed properties
const examenInfo = computed(() => {
if (!examenStore.examenActual) {
return {
proceso: null,
area: null,
duracion: 60,
intentos: 0,
intentos_maximos: 3
}
}
return {
proceso: examenStore.examenActual?.proceso || 'Proceso no disponible',
area: examenStore.examenActual?.area || 'Área no disponible',
duracion: examenStore.examenActual?.duracion || 60,
intentos: examenStore.examenActual?.intentos || 0,
intentos_maximos: examenStore.examenActual?.intentos_maximos || 3
}
})
// Transformar preguntas - CORREGIDO para manejar keys numéricas
const preguntasTransformadas = computed(() => {
if (!examenStore.preguntas || !Array.isArray(examenStore.preguntas)) {
return []
}
return examenStore.preguntas.map(pregunta => {
// Encontrar la key correcta para la respuesta
let respuestaKey = null
if (pregunta.opciones && pregunta.respuesta) {
const opcionCorrecta = pregunta.opciones.find(op =>
op.texto === pregunta.respuesta ||
op.key.toString() === pregunta.respuesta.toString()
)
respuestaKey = opcionCorrecta ? opcionCorrecta.key : null
}
// Ordenar opciones por key
const opcionesOrdenadas = pregunta.opciones ?
[...pregunta.opciones].sort((a, b) => a.key - b.key) :
[]
return {
...pregunta,
respuestaKey, // Guardar la key de la respuesta correcta
opcionesOrdenadas,
// Agregar propiedades reactivas
respuestaSeleccionada: null,
respuestaTexto: '',
// El estado real viene del backend: 'pendiente' o 'respondida'
}
})
})
// Helper para convertir key numérico a letra
const getLetraOpcion = (key) => {
const letras = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
return letras[key] || `Opción ${key}`
}
// Obtener texto de opción seleccionada
const getTextoOpcionSeleccionada = (pregunta) => {
if (!pregunta.respuestaSeleccionada) return ''
const opcion = pregunta.opcionesOrdenadas.find(
op => op.key.toString() === pregunta.respuestaSeleccionada.toString()
)
return opcion ? opcion.texto : 'Opción no encontrada'
}
const preguntasRespondidas = computed(() => {
return preguntasTransformadas.value.filter(p =>
p.estado === 'respondida' || p.respuestaSeleccionada || p.respuestaTexto
).length
})
const porcentajeCompletado = computed(() => {
if (preguntasTransformadas.length === 0) return 0
return Math.round((preguntasRespondidas.value / preguntasTransformadas.value.length) * 100)
})
const todasRespondidas = computed(() => {
return preguntasTransformadas.value.every(p =>
p.estado === 'respondida' || p.respuestaSeleccionada || p.respuestaTexto
)
})
// Métodos
const responderPregunta = async (pregunta) => {
if (!pregunta.respuestaSeleccionada) return
try {
// Asegurarse de que enviamos string (no número)
const respuestaString = pregunta.respuestaSeleccionada.toString()
// Encontrar el texto completo de la opción seleccionada
const opcionSeleccionada = pregunta.opcionesOrdenadas.find(
op => op.key.toString() === respuestaString
)
// Enviar el TEXTO de la opción, no la key
const textoRespuesta = opcionSeleccionada ? opcionSeleccionada.texto : respuestaString
const result = await examenStore.responderPregunta(
pregunta.id, // ID de PreguntaAsignada
textoRespuesta // Enviar el texto, no la key
)
if (result.success) {
// Actualizar estado local
pregunta.estado = 'respondida'
message.success('Respuesta guardada correctamente')
// Verificar si es correcta
if (result.correcta) {
message.info('¡Respuesta correcta!')
} else {
message.warning('Respuesta incorrecta')
}
} else {
message.error(result.message || 'Error al guardar respuesta')
// Revertir selección si falla
pregunta.respuestaSeleccionada = null
}
} catch (error) {
message.error('Error al guardar respuesta')
console.error('Error:', error)
pregunta.respuestaSeleccionada = null
}
}
const responderPreguntaTexto = async (pregunta) => {
if (!pregunta.respuestaTexto.trim()) return
try {
const result = await examenStore.responderPregunta(
pregunta.id,
pregunta.respuestaTexto
)
if (result.success) {
pregunta.estado = 'respondida'
message.success('Respuesta guardada')
} else {
message.error(result.message || 'Error al guardar respuesta')
}
} catch (error) {
message.error('Error al guardar respuesta')
console.error(error)
}
}
const finalizarExamen = async () => {
if (!todasRespondidas.value) {
message.warning(`Por favor responda todas las preguntas. ${preguntasTransformadas.value.length - preguntasRespondidas.value} pendientes.`)
return
}
Modal.confirm({
title: '¿Está seguro de finalizar el examen?',
content: 'Una vez finalizado no podrá modificar sus respuestas.',
okText: 'Sí, finalizar',
cancelText: 'Cancelar',
onOk: async () => {
try {
finalizando.value = true
const result = await examenStore.finalizarExamen(route.params.examenId)
if (result.success) {
message.success('Examen finalizado correctamente')
router.push({
name: 'panel-resultados',
params: { examenId: route.params.examenId }
})
} else {
message.error(result.message || 'Error al finalizar examen')
}
} catch (error) {
message.error('Error al finalizar examen')
console.error(error)
} finally {
finalizando.value = false
}
}
})
}
const guardarYSalir = async () => {
// Primero guardar todas las respuestas pendientes
const preguntasPendientes = preguntasTransformadas.value.filter(p =>
(p.respuestaSeleccionada || p.respuestaTexto) && p.estado !== 'respondida'
)
if (preguntasPendientes.length > 0) {
Modal.confirm({
title: 'Guardar respuestas pendientes',
content: `Tiene ${preguntasPendientes.length} respuesta(s) pendientes de guardar. ¿Desea guardarlas antes de salir?`,
okText: 'Guardar y salir',
cancelText: 'Salir sin guardar',
onOk: async () => {
try {
// Guardar cada respuesta pendiente
for (const pregunta of preguntasPendientes) {
if (pregunta.respuestaSeleccionada) {
await responderPregunta(pregunta)
} else if (pregunta.respuestaTexto) {
await responderPreguntaTexto(pregunta)
}
}
message.success('Respuestas guardadas')
router.push({ name: 'DashboardPostulante' })
} catch (error) {
message.error('Error al guardar respuestas')
}
},
onCancel: () => {
router.push({ name: 'DashboardPostulante' })
}
})
} else {
router.push({ name: 'DashboardPostulante' })
}
}
const finalizarExamenAutomaticamente = () => {
message.warning('Tiempo agotado. El examen se finalizará automáticamente.')
finalizarExamen()
}
const calcularTiempoRestante = () => {
if (examenStore.examenActual?.hora_inicio && examenInfo.value.duracion) {
const horaInicio = new Date(examenStore.examenActual.hora_inicio)
const duracionMs = examenInfo.value.duracion * 60 * 1000
const tiempoFinal = horaInicio.getTime() + duracionMs
timerValue.value = tiempoFinal
// Verificar si ya se agotó el tiempo
if (Date.now() > tiempoFinal) {
finalizarExamenAutomaticamente()
}
} else {
// Si no hay hora_inicio, usar duración por defecto desde ahora
timerValue.value = Date.now() + (examenInfo.value.duracion * 60 * 1000)
}
}
// Lifecycle
onMounted(async () => {
console.log('=== DATOS DEL EXAMEN ===')
try {
await examenStore.iniciarExamen(route.params.examenId)
// ahora las preguntas deberían estar disponibles
if (!examenStore.preguntas || examenStore.preguntas.length === 0) {
message.error('No se encontraron preguntas para este examen')
router.push({ name: 'DashboardPostulante' })
return
}
calcularTiempoRestante()
setInterval(calcularTiempoRestante, 30000)
} catch (err) {
console.error(err)
message.error('Error al cargar el examen')
router.push({ name: 'DashboardPostulante' })
}
})
</script>
<style scoped>
/* Estilos mejorados */
.examen-panel {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.examen-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
padding: 16px;
background: #f0f9ff;
border-radius: 8px;
border: 1px solid #d6e4ff;
}
.header-info h2 {
margin: 0 0 8px 0;
color: #1890ff;
font-size: 20px;
}
.header-info p {
margin: 4px 0;
color: #595959;
}
.timer {
text-align: right;
min-width: 180px;
}
.timer :deep(.ant-statistic-content) {
font-size: 28px;
font-weight: bold;
color: #fa541c;
}
.pregunta-card {
margin-bottom: 24px;
border: 1px solid #e8e8e8;
border-radius: 8px;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.3s;
}
.pregunta-card:hover {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.12);
}
.pregunta-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.pregunta-numero {
font-weight: bold;
font-size: 18px;
color: #1890ff;
}
.curso-tag {
flex-grow: 1;
margin-left: 12px;
}
.enunciado {
font-size: 16px;
line-height: 1.7;
margin: 20px 0;
padding: 20px;
background: #fafafa;
border-radius: 8px;
border-left: 4px solid #1890ff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.enunciado :deep(math) {
font-size: 1.1em;
}
.extra {
font-size: 14px;
line-height: 1.6;
margin: 16px 0;
padding: 16px;
background: #fff7e6;
border-radius: 6px;
border: 1px solid #ffd591;
color: #d46b08;
}
.opciones {
margin-top: 24px;
}
.opcion-radio {
display: flex;
align-items: center;
padding: 16px 20px;
margin: 10px 0;
border: 2px solid #d9d9d9;
border-radius: 8px;
transition: all 0.3s;
min-height: 60px;
cursor: pointer;
}
.opcion-radio:hover {
border-color: #40a9ff;
background: #f0f9ff;
transform: translateY(-2px);
}
.opcion-radio:deep(.ant-radio) {
margin-right: 12px;
}
.opcion-key {
font-weight: bold;
color: #1890ff;
margin-right: 12px;
min-width: 24px;
font-size: 16px;
}
.opcion-texto {
flex: 1;
line-height: 1.6;
font-size: 15px;
}
.seleccion-actual {
margin-top: 16px;
}
.debug-info {
margin-top: 16px;
}
.pregunta-abierta textarea {
margin-top: 16px;
font-size: 15px;
line-height: 1.6;
}
.resumen-examen {
padding: 16px;
}
.resumen-examen h3 {
margin-bottom: 16px;
color: #1890ff;
}
.resumen-examen p {
margin: 8px 0;
color: #595959;
}
.action-buttons {
padding: 24px 0;
border-top: 1px solid #f0f0f0;
margin-top: 32px;
display: flex;
justify-content: center;
gap: 12px;
}
:deep(.ant-card-head) {
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
}
:deep(.ant-card-body) {
padding: 24px;
}
/* Estilos para matemáticas */
.enunciado :deep(.katex) {
font-size: 1.1em;
}
.extra :deep(.katex) {
font-size: 1em;
}
/* Responsive */
@media (max-width: 768px) {
.examen-panel {
padding: 10px;
}
.examen-header {
flex-direction: column;
align-items: stretch;
}
.timer {
text-align: left;
margin-top: 16px;
}
.pregunta-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.curso-tag {
margin-left: 0;
}
.enunciado {
padding: 16px;
font-size: 15px;
}
.opcion-radio {
padding: 12px 16px;
}
.action-buttons {
flex-direction: column;
gap: 16px;
}
.action-buttons button {
width: 100%;
}
}
</style>