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.

827 lines
21 KiB
Vue

2 months ago
<template>
<div class="resultados-container">
<!-- Header de resultados -->
<a-card class="resultados-header">
<a-result
:status="resultadoStatus"
:title="resultadoTitulo"
:sub-title="resultadoSubtitulo"
>
<template #extra>
<a-space size="large">
<a-button type="primary" @click="verDetalles">
<eye-outlined />
Ver detalles del examen
</a-button>
<a-button @click="volverADashboard">
<home-outlined />
Volver al inicio
</a-button>
</a-space>
</template>
</a-result>
</a-card>
<!-- Estadísticas principales -->
<div class="stats-grid">
<a-card class="stat-card">
<div class="stat-content">
<check-circle-outlined class="stat-icon" style="color: #52c41a;" />
<div class="stat-info">
<div class="stat-value">{{ examenStore.progreso.correctas }}</div>
<div class="stat-label">Respuestas correctas</div>
</div>
</div>
</a-card>
<a-card class="stat-card">
<div class="stat-content">
<close-circle-outlined class="stat-icon" style="color: #f5222d;" />
<div class="stat-info">
<div class="stat-value">{{ respuestasIncorrectas }}</div>
<div class="stat-label">Respuestas incorrectas</div>
</div>
</div>
</a-card>
<a-card class="stat-card">
<div class="stat-content">
<star-outlined class="stat-icon" style="color: #faad14;" />
<div class="stat-info">
<div class="stat-value">{{ examenStore.progreso.puntaje_total }}</div>
<div class="stat-label">Puntaje total</div>
</div>
</div>
</a-card>
<a-card class="stat-card">
<div class="stat-content">
<percentage-outlined class="stat-icon" style="color: #1890ff;" />
<div class="stat-info">
<div class="stat-value">{{ porcentajeCorrectas }}%</div>
<div class="stat-label">Porcentaje de acierto</div>
</div>
</div>
</a-card>
</div>
<!-- Progreso por áreas/temas -->
<a-card class="progreso-card" title="Progreso por área">
<a-progress
:percent="porcentajeCorrectas"
:stroke-color="{
'0%': '#f5222d',
'100%': '#52c41a',
}"
:show-info="false"
/>
<div class="progreso-labels">
<span>0%</span>
<span>50%</span>
<span>100%</span>
</div>
<div class="estado-final">
<a-tag :color="getEstadoColor()" size="large">
{{ getEstadoTexto() }}
</a-tag>
<span class="recomendacion">
{{ recomendacionTexto }}
</span>
</div>
</a-card>
<!-- Detalle de preguntas -->
<a-card class="preguntas-detalle-card" title="Detalle de respuestas">
<a-collapse v-model:activeKey="activeKeys" accordion>
<a-collapse-panel
v-for="pregunta in examenStore.preguntas"
:key="pregunta.id"
:header="`Pregunta ${pregunta.orden}: ${getResumenPregunta(pregunta)}`"
>
<!-- Enunciado -->
<div class="pregunta-enunciado">
<h4>Enunciado:</h4>
<div v-html="formatearEnunciado(pregunta.enunciado || '')"></div>
<!-- Imágenes -->
<div v-if="pregunta.imagenes && pregunta.imagenes.length > 0" class="pregunta-images">
<a-image
v-for="(imagen, idx) in pregunta.imagenes"
:key="idx"
:src="imagen.url"
:alt="`Imagen ${idx + 1}`"
class="detalle-image"
:preview="true"
/>
</div>
</div>
<!-- Opciones -->
<div class="pregunta-opciones">
<h4>Opciones:</h4>
<div
v-for="opcion in pregunta.opciones || []"
:key="opcion.key"
class="opcion-detalle"
:class="{
'correcta': esOpcionCorrecta(pregunta, opcion.key),
'seleccionada': pregunta.respuesta_usuario == opcion.key,
'incorrecta-seleccionada': pregunta.respuesta_usuario == opcion.key && !esOpcionCorrecta(pregunta, opcion.key)
}"
>
<div class="opcion-detalle-content">
<div class="opcion-detalle-letra">
{{ obtenerLetraOpcion(opcion.key) }}
<span v-if="esOpcionCorrecta(pregunta, opcion.key)" class="correct-badge">
</span>
<span v-if="pregunta.respuesta_usuario == opcion.key && !esOpcionCorrecta(pregunta, opcion.key)"
class="incorrect-badge">
</span>
</div>
<div class="opcion-detalle-texto">
{{ opcion.texto }}
</div>
</div>
</div>
</div>
<!-- Resultado de la pregunta -->
<div class="pregunta-resultado">
<a-alert
:type="pregunta.es_correcta ? 'success' : 'error'"
:message="pregunta.es_correcta ? '¡Respuesta correcta!' : 'Respuesta incorrecta'"
:description="getDescripcionResultado(pregunta)"
show-icon
/>
</div>
<!-- Explicación (si existe) -->
<div v-if="pregunta.explicacion" class="pregunta-explicacion">
<h4>Explicación:</h4>
<p>{{ pregunta.explicacion }}</p>
</div>
</a-collapse-panel>
</a-collapse>
</a-card>
<!-- Recomendaciones -->
<a-card class="recomendaciones-card" title="Recomendaciones">
<a-list :data-source="recomendaciones" bordered>
<template #renderItem="{ item, index }">
<a-list-item>
<a-list-item-meta>
<template #title>
<a-tag :color="item.tipo === 'fortaleza' ? 'green' : 'orange'">
{{ item.tipo === 'fortaleza' ? 'Fortaleza' : 'Área a mejorar' }}
</a-tag>
{{ item.titulo }}
</template>
<template #description>
{{ item.descripcion }}
</template>
</a-list-item-meta>
<div v-if="item.accion">
<a-button type="link" size="small">
{{ item.accion }}
</a-button>
</div>
</a-list-item>
</template>
</a-list>
</a-card>
<!-- Acciones finales -->
<a-card class="acciones-card">
<div class="acciones-content">
<a-space size="large">
<a-button type="primary" size="large" @click="crearNuevoExamen">
<plus-outlined />
Realizar nuevo examen
</a-button>
<a-button size="large" @click="imprimirResultados">
<printer-outlined />
Imprimir resultados
</a-button>
<a-button size="large" @click="compartirResultados">
<share-alt-outlined />
Compartir resultados
</a-button>
</a-space>
</div>
</a-card>
<!-- Modal para ver detalles completos -->
<a-modal
v-model:open="modalDetallesVisible"
title="Detalles completos del examen"
width="800px"
:footer="null"
>
<div class="detalles-completos">
<div class="detalle-item">
<strong>Proceso:</strong> {{ examenStore.examenActual?.proceso?.nombre || 'No especificado' }}
</div>
<div class="detalle-item">
<strong>Área:</strong> {{ examenStore.examenActual?.area?.nombre || 'No especificado' }}
</div>
<div class="detalle-item">
<strong>Fecha de inicio:</strong> {{ formatFecha(examenStore.examenActual?.hora_inicio) }}
</div>
<div class="detalle-item">
<strong>Duración:</strong> {{ calcularDuracion() }}
</div>
<div class="detalle-item">
<strong>Intentos:</strong> {{ examenStore.intentos }}
</div>
<div class="detalle-item">
<strong>Estado:</strong>
<a-tag :color="getEstadoColor()">
{{ getEstadoTexto() }}
</a-tag>
</div>
<a-divider />
<h3>Resumen estadístico</h3>
<div class="estadisticas-detalle">
<a-row :gutter="16">
<a-col :span="6" v-for="stat in estadisticasDetalle" :key="stat.label">
<div class="estadistica-item">
<div class="estadistica-valor">{{ stat.value }}</div>
<div class="estadistica-label">{{ stat.label }}</div>
</div>
</a-col>
</a-row>
</div>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useExamenStore } from '../store/examen.store'
import { message } from 'ant-design-vue'
import {
CheckCircleOutlined,
CloseCircleOutlined,
StarOutlined,
PercentageOutlined,
EyeOutlined,
HomeOutlined,
PlusOutlined,
PrinterOutlined,
ShareAltOutlined,
TrophyOutlined,
BulbOutlined
} from '@ant-design/icons-vue'
const router = useRouter()
const examenStore = useExamenStore()
// Estados reactivos
const activeKeys = ref([])
const modalDetallesVisible = ref(false)
// Computed properties
const respuestasIncorrectas = computed(() => {
return examenStore.progreso.respondidas - examenStore.progreso.correctas
})
const porcentajeCorrectas = computed(() => {
if (examenStore.progreso.total === 0) return 0
return Math.round((examenStore.progreso.correctas / examenStore.progreso.total) * 100)
})
const resultadoStatus = computed(() => {
if (porcentajeCorrectas.value >= 80) return 'success'
if (porcentajeCorrectas.value >= 60) return 'warning'
return 'error'
})
const resultadoTitulo = computed(() => {
if (porcentajeCorrectas.value >= 80) return '¡Excelente resultado!'
if (porcentajeCorrectas.value >= 60) return 'Buen trabajo'
return 'Necesitas practicar más'
})
const resultadoSubtitulo = computed(() => {
return `Obtuviste ${examenStore.progreso.correctas} de ${examenStore.progreso.total} respuestas correctas (${porcentajeCorrectas.value}%)`
})
const recomendacionTexto = computed(() => {
if (porcentajeCorrectas.value >= 80) {
return 'Tu nivel es excelente. Sigue manteniendo el buen trabajo.'
} else if (porcentajeCorrectas.value >= 60) {
return 'Tienes un buen nivel, pero hay áreas que puedes mejorar.'
} else {
return 'Recomendamos repasar los temas antes de intentar nuevamente.'
}
})
const recomendaciones = computed(() => {
const recomendacionesList = []
if (porcentajeCorrectas.value >= 80) {
recomendacionesList.push({
tipo: 'fortaleza',
titulo: 'Excelente comprensión',
descripcion: 'Demuestras un dominio sólido de los temas evaluados.',
accion: 'Ver temas avanzados'
})
} else if (porcentajeCorrectas.value >= 60) {
recomendacionesList.push({
tipo: 'fortaleza',
titulo: 'Bases sólidas',
descripcion: 'Tienes buenos fundamentos en la mayoría de temas.',
accion: 'Profundizar conocimientos'
})
recomendacionesList.push({
tipo: 'mejora',
titulo: 'Áreas específicas',
descripcion: 'Algunos temas requieren más atención.',
accion: 'Repasar temas'
})
} else {
recomendacionesList.push({
tipo: 'mejora',
titulo: 'Revisión general',
descripcion: 'Recomendamos repasar todos los temas del área.',
accion: 'Iniciar repaso'
})
}
// Recomendaciones basadas en respuestas incorrectas
if (respuestasIncorrectas.value > examenStore.progreso.total / 2) {
recomendacionesList.push({
tipo: 'mejora',
titulo: 'Tiempo de estudio',
descripcion: 'Dedica más tiempo al estudio antes de evaluarte nuevamente.',
accion: 'Programar estudio'
})
}
return recomendacionesList
})
const estadisticasDetalle = computed(() => [
{ label: 'Total preguntas', value: examenStore.progreso.total },
{ label: 'Respondidas', value: examenStore.progreso.respondidas },
{ label: 'Correctas', value: examenStore.progreso.correctas },
{ label: 'Incorrectas', value: respuestasIncorrectas.value },
{ label: 'Puntaje máximo', value: examenStore.progreso.total * 10 }, // Asumiendo 10 puntos por pregunta
{ label: 'Puntaje obtenido', value: examenStore.progreso.puntaje_total },
{ label: 'Porcentaje', value: `${porcentajeCorrectas.value}%` },
{ label: 'Tasa de acierto', value: `${Math.round((examenStore.progreso.correctas / examenStore.progreso.respondidas) * 100) || 0}%` }
])
// Funciones de utilidad
const formatearEnunciado = (enunciado) => {
if (!enunciado) return ''
return enunciado.replace(/\n/g, '<br>')
}
const obtenerLetraOpcion = (key) => {
const letras = ['A', 'B', 'C', 'D', 'E', 'F']
return letras[key] || `Opción ${key + 1}`
}
const getResumenPregunta = (pregunta) => {
const estado = pregunta.es_correcta ? '✓ Correcta' : '✗ Incorrecta'
return `${estado} - Puntaje: ${pregunta.puntaje_obtenido || 0}`
}
const esOpcionCorrecta = (pregunta, opcionKey) => {
// En un sistema real, esto vendría del backend
// Por ahora, asumimos que la primera opción es correcta (para demo)
return opcionKey == 0
}
const getDescripcionResultado = (pregunta) => {
if (pregunta.es_correcta) {
return `Obtuviste ${pregunta.puntaje_obtenido || 0} puntos por esta respuesta correcta.`
} else {
return `Tu respuesta fue: ${obtenerLetraOpcion(parseInt(pregunta.respuesta_usuario))}. La respuesta correcta era: ${obtenerLetraOpcion(0)}.`
}
}
const formatFecha = (fecha) => {
if (!fecha) return 'No disponible'
const date = new Date(fecha)
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
}
const calcularDuracion = () => {
if (!examenStore.examenActual?.hora_inicio) return 'No disponible'
const inicio = new Date(examenStore.examenActual.hora_inicio)
const fin = new Date() // Asumimos que terminó ahora
const duracionMs = fin - inicio
const minutos = Math.floor(duracionMs / (1000 * 60))
const segundos = Math.floor((duracionMs % (1000 * 60)) / 1000)
return `${minutos} min ${segundos} seg`
}
const getEstadoColor = () => {
if (porcentajeCorrectas.value >= 80) return 'green'
if (porcentajeCorrectas.value >= 60) return 'blue'
return 'red'
}
const getEstadoTexto = () => {
if (porcentajeCorrectas.value >= 80) return 'Excelente'
if (porcentajeCorrectas.value >= 60) return 'Aprobado'
return 'Reprobado'
}
// Funciones de acciones
const verDetalles = () => {
modalDetallesVisible.value = true
}
const volverADashboard = () => {
router.push('/dashboard')
}
const crearNuevoExamen = () => {
// Resetear el examen actual y volver al dashboard
examenStore.resetExamen()
examenStore.resetConfiguracion()
router.push('/dashboard')
}
const imprimirResultados = () => {
window.print()
}
const compartirResultados = () => {
const resultados = `Mis resultados del examen: ${porcentajeCorrectas.value}% de acierto (${examenStore.progreso.correctas}/${examenStore.progreso.total})`
if (navigator.share) {
navigator.share({
title: 'Resultados de mi examen',
text: resultados,
url: window.location.href
})
} else {
// Fallback: copiar al portapapeles
navigator.clipboard.writeText(resultados)
.then(() => {
message.success('Resultados copiados al portapapeles')
})
.catch(() => {
message.info(resultados)
})
}
}
// Inicialización
onMounted(() => {
// Verificar que tenemos resultados
if (!examenStore.tieneExamenActual || !examenStore.tienePreguntasGeneradas) {
message.warning('No hay resultados de examen disponibles')
router.push('/dashboard')
return
}
// Expandir la primera pregunta por defecto
if (examenStore.preguntas.length > 0) {
activeKeys.value = [examenStore.preguntas[0].id]
}
})
</script>
<style scoped>
.resultados-container {
max-width: 1200px;
margin: 0 auto;
padding: 16px;
}
.resultados-header {
margin-bottom: 24px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
transition: all 0.3s;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.stat-content {
display: flex;
align-items: center;
gap: 16px;
}
.stat-icon {
font-size: 32px;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 28px;
font-weight: bold;
color: #333;
line-height: 1;
}
.stat-label {
font-size: 14px;
color: #666;
margin-top: 4px;
}
.progreso-card {
margin-bottom: 24px;
}
.progreso-labels {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 12px;
color: #666;
}
.estado-final {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
.recomendacion {
font-style: italic;
color: #666;
max-width: 400px;
}
.preguntas-detalle-card {
margin-bottom: 24px;
}
.pregunta-enunciado {
margin-bottom: 20px;
padding: 16px;
background: #fafafa;
border-radius: 4px;
}
.pregunta-enunciado h4 {
margin-bottom: 8px;
color: #333;
}
.pregunta-images {
margin-top: 16px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.detalle-image {
max-width: 200px;
border-radius: 4px;
border: 1px solid #f0f0f0;
}
.pregunta-opciones {
margin-bottom: 20px;
}
.pregunta-opciones h4 {
margin-bottom: 12px;
color: #333;
}
.opcion-detalle {
padding: 12px;
margin-bottom: 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
}
.opcion-detalle.correcta {
border-color: #52c41a;
background-color: #f6ffed;
}
.opcion-detalle.seleccionada {
border-color: #1890ff;
background-color: #e6f7ff;
}
.opcion-detalle.incorrecta-seleccionada {
border-color: #f5222d;
background-color: #fff1f0;
}
.opcion-detalle-content {
display: flex;
align-items: center;
gap: 12px;
}
.opcion-detalle-letra {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
border-radius: 50%;
font-weight: bold;
font-size: 12px;
flex-shrink: 0;
position: relative;
}
.opcion-detalle.correcta .opcion-detalle-letra {
background-color: #52c41a;
color: white;
}
.opcion-detalle.seleccionada .opcion-detalle-letra {
background-color: #1890ff;
color: white;
}
.opcion-detalle.incorrecta-seleccionada .opcion-detalle-letra {
background-color: #f5222d;
color: white;
}
.correct-badge, .incorrect-badge {
position: absolute;
top: -6px;
right: -6px;
width: 12px;
height: 12px;
border-radius: 50%;
font-size: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.correct-badge {
background-color: #52c41a;
color: white;
}
.incorrect-badge {
background-color: #f5222d;
color: white;
}
.opcion-detalle-texto {
flex: 1;
line-height: 1.4;
}
.pregunta-resultado {
margin-bottom: 16px;
}
.pregunta-explicacion {
padding: 16px;
background: #f6ffed;
border-radius: 4px;
border-left: 3px solid #52c41a;
}
.pregunta-explicacion h4 {
margin-bottom: 8px;
color: #333;
}
.recomendaciones-card {
margin-bottom: 24px;
}
.acciones-card {
margin-bottom: 24px;
}
.acciones-content {
display: flex;
justify-content: center;
}
.detalles-completos {
padding: 8px;
}
.detalle-item {
margin-bottom: 12px;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.detalle-item:last-child {
border-bottom: none;
}
.estadisticas-detalle {
margin-top: 20px;
}
.estadistica-item {
text-align: center;
padding: 16px;
background: #fafafa;
border-radius: 4px;
margin-bottom: 16px;
}
.estadistica-valor {
font-size: 24px;
font-weight: bold;
color: #1890ff;
margin-bottom: 4px;
}
.estadistica-label {
font-size: 12px;
color: #666;
text-transform: uppercase;
}
/* Estilos para impresión */
@media print {
.resultados-container {
padding: 0;
}
.acciones-card,
.ant-space,
.ant-btn,
.ant-collapse-arrow {
display: none !important;
}
.ant-collapse-content {
display: block !important;
height: auto !important;
}
}
/* Responsive */
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.estado-final {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.acciones-content .ant-space {
flex-direction: column;
width: 100%;
}
.acciones-content .ant-btn {
width: 100%;
margin-bottom: 8px;
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
.stat-content {
justify-content: center;
text-align: center;
}
}
</style>