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.
323 lines
7.5 KiB
Vue
323 lines
7.5 KiB
Vue
<template>
|
|
<a-modal
|
|
v-model:open="visible"
|
|
:title="`Gestionar Cursos - ${areaNombre}`"
|
|
:confirm-loading="loading"
|
|
width="800px"
|
|
:footer="null"
|
|
@cancel="handleCancel"
|
|
class="course-modal"
|
|
>
|
|
<!-- Barra de búsqueda -->
|
|
<div class="search-section">
|
|
<a-input-search
|
|
v-model:value="searchText"
|
|
placeholder="Buscar cursos..."
|
|
@search="handleSearch"
|
|
style="width: 300px; margin-bottom: 16px"
|
|
>
|
|
<template #enterButton>
|
|
<SearchOutlined />
|
|
</template>
|
|
</a-input-search>
|
|
</div>
|
|
|
|
<!-- Lista de cursos -->
|
|
<div class="courses-container">
|
|
<a-spin :spinning="loading">
|
|
<div class="courses-list">
|
|
<div
|
|
v-for="curso in filteredCursos"
|
|
:key="curso.id"
|
|
:class="['course-item', { 'course-selected': isCursoSelected(curso.id) }]"
|
|
@click="toggleCurso(curso.id)"
|
|
>
|
|
<div class="course-info">
|
|
<div class="course-header">
|
|
<span class="course-codigo">{{ curso.codigo }}</span>
|
|
<a-tag :color="isCursoSelected(curso.id) ? 'green' : 'default'">
|
|
{{ isCursoSelected(curso.id) ? 'Agregado' : 'No agregado' }}
|
|
</a-tag>
|
|
</div>
|
|
<h4 class="course-nombre">{{ curso.nombre }}</h4>
|
|
</div>
|
|
<div class="course-actions">
|
|
<a-button
|
|
:type="isCursoSelected(curso.id) ? 'danger' : 'primary'"
|
|
size="small"
|
|
@click.stop="toggleCurso(curso.id)"
|
|
>
|
|
<template #icon>
|
|
<CheckOutlined v-if="isCursoSelected(curso.id)" />
|
|
<PlusOutlined v-else />
|
|
</template>
|
|
{{ isCursoSelected(curso.id) ? 'Quitar' : 'Agregar' }}
|
|
</a-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</a-spin>
|
|
</div>
|
|
|
|
<!-- Resumen -->
|
|
<div class="summary-section">
|
|
<a-alert
|
|
:message="`${selectedCursosCount} cursos seleccionados de ${cursosDisponibles.length} disponibles`"
|
|
type="info"
|
|
show-icon
|
|
/>
|
|
</div>
|
|
|
|
<!-- Acciones -->
|
|
<div class="modal-footer">
|
|
<a-button @click="handleCancel">Cancelar</a-button>
|
|
<a-button type="primary" @click="handleSave" :loading="loading">
|
|
Guardar cambios
|
|
</a-button>
|
|
</div>
|
|
</a-modal>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
|
import { message } from 'ant-design-vue'
|
|
import { SearchOutlined, PlusOutlined, CheckOutlined } from '@ant-design/icons-vue'
|
|
import { useAreaStore } from '../../../store/area.store'
|
|
|
|
const props = defineProps({
|
|
areaId: {
|
|
type: Number,
|
|
required: true
|
|
},
|
|
areaNombre: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
open: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['update:open', 'courses-updated'])
|
|
|
|
const visible = computed({
|
|
get: () => props.open,
|
|
set: (value) => emit('update:open', value)
|
|
})
|
|
|
|
const areaStore = useAreaStore()
|
|
const searchText = ref('')
|
|
const selectedCursos = ref([])
|
|
|
|
const loading = computed(() => areaStore.loading)
|
|
const cursosDisponibles = computed(() => areaStore.cursosDisponibles || [])
|
|
const cursosVinculados = computed(() => areaStore.cursosVinculados || [])
|
|
|
|
const filteredCursos = computed(() => {
|
|
if (!searchText.value) return cursosDisponibles.value
|
|
|
|
const searchLower = searchText.value.toLowerCase()
|
|
return cursosDisponibles.value.filter(curso =>
|
|
curso.nombre.toLowerCase().includes(searchLower) ||
|
|
curso.codigo.toLowerCase().includes(searchLower)
|
|
)
|
|
})
|
|
|
|
// Contador de cursos seleccionados
|
|
const selectedCursosCount = computed(() => selectedCursos.value.length)
|
|
|
|
// Verificar si un curso está seleccionado
|
|
const isCursoSelected = (cursoId) => {
|
|
return selectedCursos.value.includes(cursoId)
|
|
}
|
|
|
|
// Toggle de selección de curso
|
|
const toggleCurso = (cursoId) => {
|
|
const index = selectedCursos.value.indexOf(cursoId)
|
|
if (index > -1) {
|
|
selectedCursos.value.splice(index, 1)
|
|
} else {
|
|
selectedCursos.value.push(cursoId)
|
|
}
|
|
}
|
|
|
|
// Buscar cursos
|
|
const handleSearch = () => {
|
|
// La búsqueda se maneja en computed filteredCursos
|
|
}
|
|
|
|
// Cancelar
|
|
const handleCancel = () => {
|
|
visible.value = false
|
|
selectedCursos.value = []
|
|
searchText.value = ''
|
|
areaStore.clearState() // Limpiar estado
|
|
}
|
|
|
|
// Guardar cambios
|
|
const handleSave = async () => {
|
|
try {
|
|
const result = await areaStore.vincularCursos(props.areaId, selectedCursos.value)
|
|
if (result?.success) {
|
|
message.success('Cursos actualizados correctamente')
|
|
emit('courses-updated')
|
|
handleCancel()
|
|
} else {
|
|
message.error(areaStore.error || 'Error al guardar los cursos')
|
|
}
|
|
} catch (error) {
|
|
message.error('Error al guardar los cursos')
|
|
}
|
|
}
|
|
|
|
// Función para cargar cursos
|
|
const loadCursos = async () => {
|
|
if (props.areaId) {
|
|
await areaStore.fetchCursosPorArea(props.areaId)
|
|
// Inicializar selección con cursos ya vinculados
|
|
selectedCursos.value = [...areaStore.cursosVinculados]
|
|
}
|
|
}
|
|
|
|
// Cargar cursos cuando se abre el modal
|
|
watch(() => props.open, async (newVal) => {
|
|
if (newVal && props.areaId) {
|
|
// Usar nextTick para asegurar que el modal esté montado
|
|
await nextTick()
|
|
await loadCursos()
|
|
}
|
|
})
|
|
|
|
// También cargar cuando cambia el áreaId (por si cambia mientras el modal está abierto)
|
|
watch(() => props.areaId, async (newVal) => {
|
|
if (props.open && newVal) {
|
|
await loadCursos()
|
|
}
|
|
})
|
|
|
|
// Limpiar al cerrar
|
|
watch(() => props.open, (newVal) => {
|
|
if (!newVal) {
|
|
// Pequeño delay para permitir que la animación de cierre termine
|
|
setTimeout(() => {
|
|
selectedCursos.value = []
|
|
searchText.value = ''
|
|
areaStore.clearState()
|
|
}, 300)
|
|
}
|
|
})
|
|
|
|
// También cargar cursos cuando el componente se monta si ya está abierto
|
|
onMounted(() => {
|
|
if (props.open && props.areaId) {
|
|
loadCursos()
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.course-modal :deep(.ant-modal-body) {
|
|
padding: 20px;
|
|
}
|
|
|
|
.courses-container {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
margin-bottom: 16px;
|
|
border: 1px solid #f0f0f0;
|
|
border-radius: 8px;
|
|
padding: 8px;
|
|
}
|
|
|
|
.courses-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.course-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 12px 16px;
|
|
border: 1px solid #e8e8e8;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
background: white;
|
|
}
|
|
|
|
.course-item:hover {
|
|
background: #fafafa;
|
|
border-color: #d9d9d9;
|
|
}
|
|
|
|
.course-selected {
|
|
background-color: #f6ffed;
|
|
border-color: #b7eb8f;
|
|
}
|
|
|
|
.course-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.course-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.course-codigo {
|
|
font-weight: 600;
|
|
color: #1890ff;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.course-nombre {
|
|
margin: 0;
|
|
font-size: 14px;
|
|
color: #333;
|
|
}
|
|
|
|
.course-actions {
|
|
margin-left: 16px;
|
|
}
|
|
|
|
.search-section {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.summary-section {
|
|
margin-top: 16px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.modal-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
padding-top: 16px;
|
|
border-top: 1px solid #f0f0f0;
|
|
}
|
|
|
|
/* Scrollbar personalizado */
|
|
.courses-container::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.courses-container::-webkit-scrollbar-track {
|
|
background: #f1f1f1;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.courses-container::-webkit-scrollbar-thumb {
|
|
background: #c1c1c1;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.courses-container::-webkit-scrollbar-thumb:hover {
|
|
background: #a8a8a8;
|
|
}
|
|
</style> |