diff --git a/back/app/Http/Controllers/Administracion/AreaController.php b/back/app/Http/Controllers/Administracion/AreaController.php index d2e0447..d964139 100644 --- a/back/app/Http/Controllers/Administracion/AreaController.php +++ b/back/app/Http/Controllers/Administracion/AreaController.php @@ -4,41 +4,41 @@ namespace App\Http\Controllers\Administracion; use App\Http\Controllers\Controller; use App\Models\Area; +use App\Models\Curso; +use App\Models\proceso; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; class AreaController extends Controller { - /** - * Listar áreas (con búsqueda, filtro y paginación) - */ - public function index(Request $request) - { - $query = Area::query(); + public function index(Request $request) + { + $query = Area::withCount(['cursos', 'procesos']); - // 🔍 Buscar por nombre o código - if ($request->filled('search')) { - $search = $request->search; - $query->where(function ($q) use ($search) { - $q->where('nombre', 'like', "%{$search}%") - ->where('codigo', 'like', "%{$search}%"); - }); - } + // 🔍 Buscar por nombre o código + if ($request->filled('search')) { + $search = $request->search; + $query->where(function ($q) use ($search) { + $q->where('nombre', 'like', "%{$search}%") + ->orWhere('codigo', 'like', "%{$search}%"); + }); + } - // 🔄 Filtrar por estado - if ($request->filled('activo')) { - $query->where('activo', $request->activo); - } + // 🔄 Filtrar por estado + if (!is_null($request->activo)) { + $query->where('activo', $request->activo); + } - $areas = $query - ->orderBy('created_at', 'desc') - ->paginate($request->get('per_page', 10)); + $areas = $query + ->orderBy('created_at', 'desc') + ->paginate($request->get('per_page', 10)); + + return response()->json([ + 'success' => true, + 'data' => $areas + ]); + } - return response()->json([ - 'success' => true, - 'data' => $areas - ]); - } /** * Crear área @@ -189,4 +189,306 @@ class AreaController extends Controller 'message' => 'Área eliminada correctamente' ]); } + + + + + public function vincularCursosArea(Request $request, $areaId) + { + try { + $user = auth()->user(); + + if (!$user->hasRole('administrador')) { + return response()->json(['success' => false, 'message' => 'No autorizado'], 403); + } + + $area = Area::find($areaId); + + if (!$area) { + return response()->json(['success' => false, 'message' => 'Área no encontrada'], 404); + } + + $validator = Validator::make($request->all(), [ + 'cursos' => 'required|array', + 'cursos.*' => 'required|integer|exists:cursos,id' + ]); + + if ($validator->fails()) { + return response()->json(['success' => false, 'errors' => $validator->errors()], 422); + } + + // Sincronizar cursos + $area->cursos()->sync($request->cursos); + + // Recargar cursos + $area->load('cursos:id,nombre,codigo'); + + return response()->json([ + 'success' => true, + 'message' => 'Cursos vinculados a la área exitosamente', + 'data' => $area + ]); + + } catch (\Exception $e) { + Log::error('Error vinculando cursos a área', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + 'area_id' => $areaId, + 'request_data' => $request->all() + ]); + + return response()->json(['success' => false, 'message' => 'Error al vincular cursos: ' . $e->getMessage()], 500); + } + } + + + public function getCursosPorArea(Request $request, $areaId) + { + try { + $user = auth()->user(); + + if (!$user->hasRole('administrador')) { + return response()->json(['success' => false, 'message' => 'No autorizado'], 403); + } + + $area = Area::find($areaId); + + if (!$area) { + return response()->json(['success' => false, 'message' => 'Área no encontrada'], 404); + } + + $todosLosCursos = Curso::select('id', 'nombre', 'codigo') + ->orderBy('nombre') + ->get(); + + $cursosVinculadosIds = $area->cursos->pluck('id')->toArray(); + + return response()->json([ + 'success' => true, + 'data' => [ + 'todos_los_cursos' => $todosLosCursos, + 'cursos_vinculados' => $cursosVinculadosIds + ] + ]); + + } catch (\Exception $e) { + Log::error('Error obteniendo cursos por área', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + 'area_id' => $areaId + ]); + + return response()->json(['success' => false, 'message' => 'Error al cargar cursos: ' . $e->getMessage()], 500); + } + } + + + public function desvincularCursoArea(Request $request, $areaId) + { + try { + $user = auth()->user(); + + if (!$user->hasRole('administrador')) { + return response()->json(['success' => false, 'message' => 'No autorizado'], 403); + } + + $area = Area::find($areaId); + + if (!$area) { + return response()->json(['success' => false, 'message' => 'Área no encontrada'], 404); + } + + $validator = Validator::make($request->all(), [ + 'curso_id' => 'required|exists:cursos,id' + ]); + + if ($validator->fails()) { + return response()->json(['success' => false, 'errors' => $validator->errors()], 422); + } + + $area->cursos()->detach($request->curso_id); + + return response()->json([ + 'success' => true, + 'message' => 'Curso desvinculado de la área exitosamente' + ]); + + } catch (\Exception $e) { + Log::error('Error desvinculando curso de área', [ + 'error' => $e->getMessage() + ]); + + return response()->json(['success' => false, 'message' => 'Error al desvincular curso de la área'], 500); + } + } + + + public function vincularProcesosArea(Request $request, $areaId) +{ + try { + $user = auth()->user(); + + if (!$user->hasRole('administrador')) { + return response()->json([ + 'success' => false, + 'message' => 'No autorizado' + ], 403); + } + + $area = Area::find($areaId); + + if (!$area) { + return response()->json([ + 'success' => false, + 'message' => 'Área no encontrada' + ], 404); + } + + $validator = Validator::make($request->all(), [ + 'procesos' => 'required|array', + 'procesos.*' => 'required|integer|exists:procesos,id', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors() + ], 422); + } + + // 🔄 Sincronizar procesos + $area->procesos()->sync($request->procesos); + + // 🔁 Recargar procesos vinculados + $area->load('procesos:id,nombre,tipo_proceso'); + + return response()->json([ + 'success' => true, + 'message' => 'Procesos vinculados a la área exitosamente', + 'data' => $area + ]); + + } catch (\Exception $e) { + Log::error('Error vinculando procesos a área', [ + 'error' => $e->getMessage(), + 'area_id' => $areaId, + 'request' => $request->all(), + ]); + + return response()->json([ + 'success' => false, + 'message' => 'Error al vincular procesos' + ], 500); + } +} + +public function getProcesosPorArea(Request $request, $areaId) +{ + try { + $user = auth()->user(); + + if (!$user->hasRole('administrador')) { + return response()->json([ + 'success' => false, + 'message' => 'No autorizado' + ], 403); + } + + $area = Area::find($areaId); + + if (!$area) { + return response()->json([ + 'success' => false, + 'message' => 'Área no encontrada' + ], 404); + } + + $todosLosProcesos = Proceso::select( + 'id', + 'nombre', + 'tipo_proceso', + 'activo' + ) + ->orderBy('nombre') + ->get(); + + $procesosVinculadosIds = $area + ->procesos + ->pluck('id') + ->toArray(); + + return response()->json([ + 'success' => true, + 'data' => [ + 'todos_los_procesos' => $todosLosProcesos, + 'procesos_vinculados' => $procesosVinculadosIds + ] + ]); + + } catch (\Exception $e) { + Log::error('Error obteniendo procesos por área', [ + 'error' => $e->getMessage(), + 'area_id'=> $areaId + ]); + + return response()->json([ + 'success' => false, + 'message' => 'Error al cargar procesos' + ], 500); + } +} + +public function desvincularProcesoArea(Request $request, $areaId) +{ + try { + $user = auth()->user(); + + if (!$user->hasRole('administrador')) { + return response()->json([ + 'success' => false, + 'message' => 'No autorizado' + ], 403); + } + + $area = Area::find($areaId); + + if (!$area) { + return response()->json([ + 'success' => false, + 'message' => 'Área no encontrada' + ], 404); + } + + $validator = Validator::make($request->all(), [ + 'proceso_id' => 'required|exists:procesos,id' + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors() + ], 422); + } + + $area->procesos()->detach($request->proceso_id); + + return response()->json([ + 'success' => true, + 'message' => 'Proceso desvinculado de la área exitosamente' + ]); + + } catch (\Exception $e) { + Log::error('Error desvinculando proceso de área', [ + 'error' => $e->getMessage() + ]); + + return response()->json([ + 'success' => false, + 'message' => 'Error al desvincular proceso' + ], 500); + } +} + + + } diff --git a/back/app/Http/Controllers/Administracion/PreguntaController.php b/back/app/Http/Controllers/Administracion/PreguntaController.php index 226950c..fdda79f 100644 --- a/back/app/Http/Controllers/Administracion/PreguntaController.php +++ b/back/app/Http/Controllers/Administracion/PreguntaController.php @@ -59,183 +59,204 @@ class PreguntaController extends Controller -public function getPregunta($id) -{ - $pregunta = Pregunta::find($id); - - if (!$pregunta) { - return response()->json([ - 'success' => false, - 'message' => 'Pregunta no encontrada' - ], 404); - } - -$pregunta->imagenes = collect($pregunta->imagenes ?? [])->map(fn($path) => $path ? url(Storage::url($path)) : null); -$pregunta->imagenes_explicacion = collect($pregunta->imagenes_explicacion ?? [])->map(fn($path) => $path ? url(Storage::url($path)) : null); - - return response()->json([ - 'success' => true, - 'data' => $pregunta - ]); -} - -public function agregarPreguntaCurso(Request $request) -{ - try { - $user = auth()->user(); - if (!$user->hasRole('administrador')) { - return response()->json(['success' => false, 'message' => 'No autorizado'], 403); - } - - // Validación (igual que antes) - $validator = Validator::make($request->all(), [ - 'curso_id' => 'required|exists:cursos,id', - 'enunciado' => 'required|string', - 'enunciado_adicional' => 'nullable|string', - 'opciones' => 'required', - 'respuesta_correcta' => 'required|string', - 'explicacion' => 'nullable|string', - 'nivel_dificultad' => 'required|in:facil,medio,dificil', - 'activo' => 'boolean', - 'imagenes' => 'nullable|array', - 'imagenes.*' => 'image|mimes:jpg,jpeg,png,gif,webp|max:2048', - 'imagenes_explicacion' => 'nullable|array', - 'imagenes_explicacion.*' => 'image|mimes:jpg,jpeg,png,gif,webp|max:2048', - ]); - - if ($validator->fails()) { - return response()->json(['success' => false, 'errors' => $validator->errors()], 422); - } - - // Opciones - $opciones = is_string($request->opciones) ? json_decode($request->opciones, true) : $request->opciones; - $opcionesValidas = array_map('trim', $opciones); - - // Validar respuesta correcta - if (!in_array($request->respuesta_correcta, $opcionesValidas)) { - return response()->json(['success' => false, 'errors' => ['respuesta_correcta' => ['La respuesta correcta debe estar entre las opciones']]] ,422); - } - - // Procesar imágenes del enunciado y devolver URLs completas - $imagenesUrls = []; - if ($request->hasFile('imagenes')) { - foreach ($request->file('imagenes') as $imagen) { - $path = $imagen->store('preguntas/enunciados', 'public'); - $imagenesUrls[] = url(Storage::url($path)); // URL completa + public function getPregunta($id) + { + $pregunta = Pregunta::find($id); + + if (!$pregunta) { + return response()->json([ + 'success' => false, + 'message' => 'Pregunta no encontrada' + ], 404); } - } - - // Procesar imágenes de la explicación - $imagenesExplicacionUrls = []; - if ($request->hasFile('imagenes_explicacion')) { - foreach ($request->file('imagenes_explicacion') as $imagen) { - $path = $imagen->store('preguntas/explicaciones', 'public'); - $imagenesExplicacionUrls[] = url(Storage::url($path)); // URL completa - } - } - - // Crear pregunta - $pregunta = Pregunta::create([ - 'curso_id' => $request->curso_id, - 'enunciado' => $request->enunciado, - 'enunciado_adicional' => $request->enunciado_adicional, - 'opciones' => $opcionesValidas, - 'respuesta_correcta' => $request->respuesta_correcta, - 'explicacion' => $request->explicacion, - 'nivel_dificultad' => $request->nivel_dificultad, - 'activo' => $request->boolean('activo'), - 'imagenes' => $imagenesUrls, - 'imagenes_explicacion' => $imagenesExplicacionUrls, - ]); - return response()->json(['success' => true, 'message' => 'Pregunta creada correctamente', 'data' => $pregunta], 201); + $pregunta->imagenes = collect($pregunta->imagenes ?? [])->map(fn($path) => $path ? url(Storage::url($path)) : null); + $pregunta->imagenes_explicacion = collect($pregunta->imagenes_explicacion ?? [])->map(fn($path) => $path ? url(Storage::url($path)) : null); - } catch (\Exception $e) { - Log::error('Error creando pregunta', ['error' => $e->getMessage()]); - return response()->json(['success' => false, 'message' => 'Error al crear la pregunta'], 500); - } -} - - -public function actualizarPregunta(Request $request, $id) -{ - try { - $user = auth()->user(); - if (!$user->hasRole('administrador')) { - return response()->json(['success' => false, 'message' => 'No autorizado'], 403); - } - - $pregunta = Pregunta::find($id); - if (!$pregunta) { - return response()->json(['success' => false, 'message' => 'Pregunta no encontrada'], 404); - } - - // Validación (igual que antes) - $validator = Validator::make($request->all(), [ - 'curso_id' => 'required|exists:cursos,id', - 'enunciado' => 'required|string', - 'enunciado_adicional' => 'nullable|string', - 'opciones' => 'required', - 'respuesta_correcta' => 'required|string', - 'explicacion' => 'nullable|string', - 'nivel_dificultad' => 'required|in:facil,medio,dificil', - 'activo' => 'boolean', - 'imagenes' => 'nullable|array', - 'imagenes.*' => 'image|mimes:jpg,jpeg,png,gif,webp|max:2048', - 'imagenes_explicacion' => 'nullable|array', - 'imagenes_explicacion.*' => 'image|mimes:jpg,jpeg,png,gif,webp|max:2048', - ]); - - if ($validator->fails()) { - return response()->json(['success' => false, 'errors' => $validator->errors()], 422); - } - - // Opciones - $opciones = is_string($request->opciones) ? json_decode($request->opciones, true) : $request->opciones; - $opcionesValidas = array_map('trim', $opciones); - - if (!in_array($request->respuesta_correcta, $opcionesValidas)) { - return response()->json(['success' => false, 'errors' => ['respuesta_correcta' => ['La respuesta correcta debe estar entre las opciones']]], 422); + return response()->json([ + 'success' => true, + 'data' => $pregunta + ]); } - // Imágenes del enunciado - $imagenesActuales = $request->imagenes_existentes ?? $pregunta->imagenes ?? []; - if ($request->hasFile('imagenes')) { - foreach ($request->file('imagenes') as $imagen) { - $path = $imagen->store('preguntas/enunciados', 'public'); - $imagenesActuales[] = url(Storage::url($path)); + public function agregarPreguntaCurso(Request $request) + { + try { + $user = auth()->user(); + if (!$user->hasRole('administrador')) { + return response()->json(['success' => false, 'message' => 'No autorizado'], 403); + } + + // Validación (igual que antes) + $validator = Validator::make($request->all(), [ + 'curso_id' => 'required|exists:cursos,id', + 'enunciado' => 'required|string', + 'enunciado_adicional' => 'nullable|string', + 'opciones' => 'required', + 'respuesta_correcta' => 'required|string', + 'explicacion' => 'nullable|string', + 'nivel_dificultad' => 'required|in:facil,medio,dificil', + 'activo' => 'boolean', + 'imagenes' => 'nullable|array', + 'imagenes.*' => 'image|mimes:jpg,jpeg,png,gif,webp|max:2048', + 'imagenes_explicacion' => 'nullable|array', + 'imagenes_explicacion.*' => 'image|mimes:jpg,jpeg,png,gif,webp|max:2048', + ]); + + if ($validator->fails()) { + return response()->json(['success' => false, 'errors' => $validator->errors()], 422); + } + + // Opciones + $opciones = is_string($request->opciones) ? json_decode($request->opciones, true) : $request->opciones; + $opcionesValidas = array_map('trim', $opciones); + + // Validar respuesta correcta + if (!in_array($request->respuesta_correcta, $opcionesValidas)) { + return response()->json(['success' => false, 'errors' => ['respuesta_correcta' => ['La respuesta correcta debe estar entre las opciones']]] ,422); + } + + // Procesar imágenes del enunciado y devolver URLs completas + $imagenesUrls = []; + if ($request->hasFile('imagenes')) { + foreach ($request->file('imagenes') as $imagen) { + $path = $imagen->store('preguntas/enunciados', 'public'); + $imagenesUrls[] = url(Storage::url($path)); // URL completa + } + } + + // Procesar imágenes de la explicación + $imagenesExplicacionUrls = []; + if ($request->hasFile('imagenes_explicacion')) { + foreach ($request->file('imagenes_explicacion') as $imagen) { + $path = $imagen->store('preguntas/explicaciones', 'public'); + $imagenesExplicacionUrls[] = url(Storage::url($path)); // URL completa + } + } + + // Crear pregunta + $pregunta = Pregunta::create([ + 'curso_id' => $request->curso_id, + 'enunciado' => $request->enunciado, + 'enunciado_adicional' => $request->enunciado_adicional, + 'opciones' => $opcionesValidas, + 'respuesta_correcta' => $request->respuesta_correcta, + 'explicacion' => $request->explicacion, + 'nivel_dificultad' => $request->nivel_dificultad, + 'activo' => $request->boolean('activo'), + 'imagenes' => $imagenesUrls, + 'imagenes_explicacion' => $imagenesExplicacionUrls, + ]); + + return response()->json(['success' => true, 'message' => 'Pregunta creada correctamente', 'data' => $pregunta], 201); + + } catch (\Exception $e) { + Log::error('Error creando pregunta', ['error' => $e->getMessage()]); + return response()->json(['success' => false, 'message' => 'Error al crear la pregunta'], 500); } } - // Imágenes de la explicación - $imagenesExplicacionActuales = $request->imagenes_explicacion_existentes ?? $pregunta->imagenes_explicacion ?? []; - if ($request->hasFile('imagenes_explicacion')) { - foreach ($request->file('imagenes_explicacion') as $imagen) { - $path = $imagen->store('preguntas/explicaciones', 'public'); - $imagenesExplicacionActuales[] = url(Storage::url($path)); + + public function actualizarPregunta(Request $request, $id) + { + try { + $user = auth()->user(); + if (!$user->hasRole('administrador')) { + return response()->json(['success' => false, 'message' => 'No autorizado'], 403); + } + + $pregunta = Pregunta::find($id); + if (!$pregunta) { + return response()->json(['success' => false, 'message' => 'Pregunta no encontrada'], 404); + } + + // Validación + $validator = Validator::make($request->all(), [ + 'curso_id' => 'required|exists:cursos,id', + 'enunciado' => 'required|string', + 'enunciado_adicional' => 'nullable|string', + 'opciones' => 'required', + 'respuesta_correcta' => 'required|string', + 'explicacion' => 'nullable|string', + 'nivel_dificultad' => 'required|in:facil,medio,dificil', + 'activo' => 'boolean', + 'imagenes' => 'nullable|array', + 'imagenes.*' => 'image|mimes:jpg,jpeg,png,gif,webp|max:2048', + 'imagenes_explicacion' => 'nullable|array', + 'imagenes_explicacion.*' => 'image|mimes:jpg,jpeg,png,gif,webp|max:2048', + ]); + + if ($validator->fails()) { + return response()->json(['success' => false, 'errors' => $validator->errors()], 422); + } + + // Decodificar opciones + $opciones = is_string($request->opciones) ? json_decode($request->opciones, true) : $request->opciones; + $opcionesValidas = array_map('trim', $opciones); + + if (!in_array($request->respuesta_correcta, $opcionesValidas)) { + return response()->json([ + 'success' => false, + 'errors' => ['respuesta_correcta' => ['La respuesta correcta debe estar entre las opciones']] + ], 422); + } + + // --- Imágenes del enunciado --- + $imagenesActuales = $request->input('imagenes_existentes', $pregunta->imagenes ?? []); + if (is_string($imagenesActuales)) { + $imagenesActuales = json_decode($imagenesActuales, true) ?? []; + } + + if ($request->hasFile('imagenes')) { + foreach ($request->file('imagenes') as $imagen) { + $path = $imagen->store('preguntas/enunciados', 'public'); + $imagenesActuales[] = url(Storage::url($path)); + } + } + + // --- Imágenes de la explicación --- + $imagenesExplicacionActuales = $request->input('imagenes_explicacion_existentes', $pregunta->imagenes_explicacion ?? []); + if (is_string($imagenesExplicacionActuales)) { + $imagenesExplicacionActuales = json_decode($imagenesExplicacionActuales, true) ?? []; + } + + if ($request->hasFile('imagenes_explicacion')) { + foreach ($request->file('imagenes_explicacion') as $imagen) { + $path = $imagen->store('preguntas/explicaciones', 'public'); + $imagenesExplicacionActuales[] = url(Storage::url($path)); + } + } + + // Actualizar pregunta + $pregunta->update([ + 'curso_id' => $request->curso_id, + 'enunciado' => $request->enunciado, + 'enunciado_adicional' => $request->enunciado_adicional, + 'opciones' => $opcionesValidas, + 'respuesta_correcta' => $request->respuesta_correcta, + 'explicacion' => $request->explicacion, + 'nivel_dificultad' => $request->nivel_dificultad, + 'activo' => $request->boolean('activo'), + 'imagenes' => $imagenesActuales, + 'imagenes_explicacion' => $imagenesExplicacionActuales, + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Pregunta actualizada correctamente', + 'data' => $pregunta + ]); + + } catch (\Exception $e) { + Log::error('Error actualizando pregunta', ['error' => $e->getMessage()]); + return response()->json([ + 'success' => false, + 'message' => 'Error al actualizar la pregunta' + ], 500); } } - $pregunta->update([ - 'curso_id' => $request->curso_id, - 'enunciado' => $request->enunciado, - 'enunciado_adicional' => $request->enunciado_adicional, - 'opciones' => $opcionesValidas, - 'respuesta_correcta' => $request->respuesta_correcta, - 'explicacion' => $request->explicacion, - 'nivel_dificultad' => $request->nivel_dificultad, - 'activo' => $request->boolean('activo'), - 'imagenes' => $imagenesActuales, - 'imagenes_explicacion' => $imagenesExplicacionActuales, - ]); - return response()->json(['success' => true, 'message' => 'Pregunta actualizada correctamente', 'data' => $pregunta]); - - } catch (\Exception $e) { - Log::error('Error actualizando pregunta', ['error' => $e->getMessage()]); - return response()->json(['success' => false, 'message' => 'Error al actualizar la pregunta'], 500); - } -} public function eliminarPregunta($id) { diff --git a/back/app/Models/Area.php b/back/app/Models/Area.php index 3ffcce2..45de851 100644 --- a/back/app/Models/Area.php +++ b/back/app/Models/Area.php @@ -25,10 +25,6 @@ class Area extends Model /* ================= RELACIONES ================= */ - public function cursos() - { - return $this->belongsToMany(Curso::class, 'area_curso'); - } public function examenes() { @@ -91,4 +87,15 @@ class Area extends Model { return $this->examenes()->count(); } + + public function cursos() + { + return $this->belongsToMany(Curso::class, 'area_curso')->withTimestamps(); + } + + public function procesos() + { + return $this->belongsToMany(Proceso::class, 'area_proceso')->withTimestamps(); + } + } diff --git a/back/app/Models/Calificacion.php b/back/app/Models/Calificacion.php new file mode 100644 index 0000000..2b80cd6 --- /dev/null +++ b/back/app/Models/Calificacion.php @@ -0,0 +1,26 @@ +hasMany(Proceso::class); + } +} diff --git a/back/app/Models/Curso.php b/back/app/Models/Curso.php index 729e07f..0e50e67 100644 --- a/back/app/Models/Curso.php +++ b/back/app/Models/Curso.php @@ -24,10 +24,11 @@ class Curso extends Model ]; - + public function areas() { - return $this->belongsToMany(Area::class, 'area_curso'); + return $this->belongsToMany(Area::class, 'area_curso') + ->withTimestamps(); } diff --git a/back/app/Models/Pregunta.php b/back/app/Models/Pregunta.php index e6ec570..5303342 100644 --- a/back/app/Models/Pregunta.php +++ b/back/app/Models/Pregunta.php @@ -57,4 +57,6 @@ class Pregunta extends Model ->orWhere('explicacion', 'like', "%{$texto}%"); }); } + + } diff --git a/back/app/Models/Proceso.php b/back/app/Models/Proceso.php index e41c23e..e03fa1f 100644 --- a/back/app/Models/Proceso.php +++ b/back/app/Models/Proceso.php @@ -112,4 +112,12 @@ class Proceso extends Model } }); } + + public function areas() + { + return $this->belongsToMany( + Area::class, + 'area_proceso' + )->withTimestamps(); + } } diff --git a/back/routes/api.php b/back/routes/api.php index 2e67e12..22263a4 100644 --- a/back/routes/api.php +++ b/back/routes/api.php @@ -46,6 +46,16 @@ Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { Route::delete('/areas/{id}', [AreaController::class, 'destroy']); Route::patch('/areas/{id}/toggle', [AreaController::class, 'toggleEstado']); + Route::post('/areas/{area}/vincular-cursos', [AreaController::class, 'vincularCursosArea']); + Route::post('/areas/{area}/desvincular-curso', [AreaController::class, 'desvincularCursoArea']); + Route::get('/areas/{area}/cursos-disponibles', [AreaController::class, 'getCursosPorArea']); + + + Route::post('areas/{area}/vincular-procesos', [AreaController::class, 'vincularProcesosArea']); + Route::get('areas/{area}/procesos-disponibles', [AreaController::class, 'getProcesosPorArea'] ); + Route::post('areas/{area}/desvincular-procesos', [AreaController::class, 'desvincularProcesoArea'] ); + + }); Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { diff --git a/front/package-lock.json b/front/package-lock.json index d10d92b..dd9f2ca 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -8,11 +8,19 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@vueup/vue-quill": "^1.2.0", "ant-design-vue": "^4.2.6", "axios": "^1.13.3", "chart.js": "^4.5.1", "dayjs": "^1.11.19", + "katex": "^0.16.28", + "markdown-it": "^14.1.0", + "markdown-it-katex": "^2.0.3", + "marked": "^17.0.1", "pinia": "^3.0.4", + "quill": "^1.3.4", + "quill-blot-formatter": "^1.0.5", + "quill-delta": "^5.1.0", "vue": "^3.5.24", "vue-chartjs": "^5.3.3", "vue-qrcode": "^2.2.2", @@ -1105,12 +1113,81 @@ "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", "license": "MIT" }, + "node_modules/@vueup/vue-quill": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vueup/vue-quill/-/vue-quill-1.2.0.tgz", + "integrity": "sha512-kd5QPSHMDpycklojPXno2Kw2JSiKMYduKYQckTm1RJoVDA557MnyUXgcuuDpry4HY/Rny9nGNcK+m3AHk94wag==", + "license": "MIT", + "dependencies": { + "quill": "^1.3.7", + "quill-delta": "^4.2.2" + }, + "peerDependencies": { + "vue": "^3.2.41" + } + }, + "node_modules/@vueup/vue-quill/node_modules/fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", + "license": "Apache-2.0" + }, + "node_modules/@vueup/vue-quill/node_modules/parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==", + "license": "BSD-3-Clause" + }, + "node_modules/@vueup/vue-quill/node_modules/quill": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", + "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", + "license": "BSD-3-Clause", + "dependencies": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.2", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + } + }, + "node_modules/@vueup/vue-quill/node_modules/quill-delta": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-4.2.2.tgz", + "integrity": "sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg==", + "license": "MIT", + "dependencies": { + "fast-diff": "1.2.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" + } + }, + "node_modules/@vueup/vue-quill/node_modules/quill-delta/node_modules/fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "license": "Apache-2.0" + }, + "node_modules/@vueup/vue-quill/node_modules/quill/node_modules/quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "license": "MIT", + "dependencies": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -1120,7 +1197,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1170,6 +1246,12 @@ "vue": ">=3.2.0" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/array-tree-filter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz", @@ -1208,6 +1290,24 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1221,12 +1321,27 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -1236,6 +1351,7 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -1248,19 +1364,26 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "license": "ISC", - "peer": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1272,8 +1395,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -1287,6 +1409,15 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/compute-scroll-into-view": { "version": "1.0.20", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", @@ -1336,11 +1467,73 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "license": "MIT", + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1354,8 +1547,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dom-align": { "version": "1.12.4", @@ -1387,8 +1579,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/entities": { "version": "7.0.1", @@ -1495,6 +1686,24 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, + "node_modules/eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "license": "Apache-2.0" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1518,7 +1727,6 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "license": "MIT", - "peer": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -1587,12 +1795,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "license": "ISC", - "peer": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -1646,6 +1862,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1691,12 +1919,43 @@ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", "license": "MIT" }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -1710,6 +1969,24 @@ "node": ">=0.10.0" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-what": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", @@ -1728,12 +2005,36 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/katex": { + "version": "0.16.28", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.28.tgz", + "integrity": "sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "license": "MIT", - "peer": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -1753,6 +2054,19 @@ "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "license": "MIT" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1774,6 +2088,73 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-katex": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/markdown-it-katex/-/markdown-it-katex-2.0.3.tgz", + "integrity": "sha512-nUkkMtRWeg7OpdflamflE/Ho/pWl64Lk9wNBKOmaj33XkQdumhXAIYhI0WO03GeiycPCsxbmX536V5NEXpC3Ng==", + "license": "MIT", + "dependencies": { + "katex": "^0.6.0" + } + }, + "node_modules/markdown-it-katex/node_modules/katex": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.6.0.tgz", + "integrity": "sha512-rS4mY3SvHYg5LtQV6RBcK0if7ur6plyEukAOV+jGGPqFImuzu8fHL6M752iBmRGoUyF0bhZbAPoezehn7xYksA==", + "license": "MIT", + "dependencies": { + "match-at": "^0.1.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/marked": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz", + "integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/match-at": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/match-at/-/match-at-0.1.1.tgz", + "integrity": "sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q==" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1783,6 +2164,12 @@ "node": ">= 0.4" } }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -1834,12 +2221,36 @@ "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==", "license": "MIT" }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "license": "MIT", - "peer": true, "dependencies": { "p-try": "^2.0.0" }, @@ -1855,7 +2266,6 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "license": "MIT", - "peer": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -1868,17 +2278,25 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } }, + "node_modules/parchment": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.1.tgz", + "integrity": "sha512-+9UT5NZVBCsdRqi3vJ8n73iPEHlA+OcHzf8F+AFLw/XP6VDg737zZQ0yMfDqC6QiSSpkrNXdZ2XcCNPBgDuiSQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 5.3", + "npm": ">= 3.5" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -1901,6 +2319,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -1934,7 +2353,6 @@ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" } @@ -1973,6 +2391,15 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", @@ -1991,12 +2418,91 @@ "node": ">=10.13.0" } }, + "node_modules/quill": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.4.tgz", + "integrity": "sha512-JNQtAA8jRhFEM1zlpb8Cee/4JHvgdKMWQ0boZJFIsQAH1X0vdAYISsjKPMdI4dOgj1XUG1cSUXeLPxSpoD7Ryg==", + "license": "BSD-3-Clause", + "dependencies": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.1", + "parchment": "1.1.1", + "quill-delta": "^3.6.2" + } + }, + "node_modules/quill-blot-formatter": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/quill-blot-formatter/-/quill-blot-formatter-1.0.5.tgz", + "integrity": "sha512-iVmuEdmMIpvERBnnDfosWul6VAVN6tqQRruUzAEwA9ZbQ/Ef7DTHGZDUR4KklXpxM+z50opFp6m1NhNdN6HJhw==", + "license": "Apache-2.0", + "dependencies": { + "deepmerge": "^2.0.0" + }, + "peerDependencies": { + "quill": "^1.3.4" + } + }, + "node_modules/quill-delta": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", + "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", + "license": "MIT", + "dependencies": { + "fast-diff": "^1.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/quill/node_modules/fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", + "license": "Apache-2.0" + }, + "node_modules/quill/node_modules/quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "license": "MIT", + "dependencies": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2005,8 +2511,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", @@ -2078,8 +2583,39 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC", - "peer": true + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/shallow-equal": { "version": "1.2.1", @@ -2110,7 +2646,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", - "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2125,7 +2660,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2183,12 +2717,19 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -2263,6 +2804,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.27", "@vue/compiler-sfc": "3.5.27", @@ -2354,15 +2896,13 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -2376,15 +2916,13 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "license": "MIT", - "peer": true, "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", @@ -2407,7 +2945,6 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "license": "ISC", - "peer": true, "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" diff --git a/front/package.json b/front/package.json index cb418d6..2f193e7 100644 --- a/front/package.json +++ b/front/package.json @@ -9,11 +9,19 @@ "preview": "vite preview" }, "dependencies": { + "@vueup/vue-quill": "^1.2.0", "ant-design-vue": "^4.2.6", "axios": "^1.13.3", "chart.js": "^4.5.1", "dayjs": "^1.11.19", + "katex": "^0.16.28", + "markdown-it": "^14.1.0", + "markdown-it-katex": "^2.0.3", + "marked": "^17.0.1", "pinia": "^3.0.4", + "quill": "^1.3.4", + "quill-blot-formatter": "^1.0.5", + "quill-delta": "^5.1.0", "vue": "^3.5.24", "vue-chartjs": "^5.3.3", "vue-qrcode": "^2.2.2", diff --git a/front/src/main.js b/front/src/main.js index bc590eb..72954b7 100644 --- a/front/src/main.js +++ b/front/src/main.js @@ -6,6 +6,7 @@ import { useUserStore } from './store/user' import Antd from 'ant-design-vue' import 'ant-design-vue/dist/reset.css' +import 'katex/dist/katex.min.css' const app = createApp(App) const pinia = createPinia() diff --git a/front/src/store/area.store.js b/front/src/store/area.store.js index 8e2efc1..cfda482 100644 --- a/front/src/store/area.store.js +++ b/front/src/store/area.store.js @@ -5,7 +5,11 @@ export const useAreaStore = defineStore('area', { state: () => ({ areas: [], area: null, - + cursosDisponibles: [], // todos los cursos + cursosVinculados: [], + procesosDisponibles: [], + procesosVinculados: [], + // paginación pagination: { current_page: 1, @@ -21,13 +25,44 @@ export const useAreaStore = defineStore('area', { loading: false, errors: null, + error: null, }), + getters: { + // Cursos ya vinculados (objetos completos) + cursosVinculadosCompletos: (state) => { + return state.cursosDisponibles.filter(curso => + state.cursosVinculados.includes(curso.id) + ); + }, + + // Cursos disponibles (no vinculados) + cursosDisponiblesFiltrados: (state) => { + return state.cursosDisponibles.filter(curso => + !state.cursosVinculados.includes(curso.id) + ); + }, + + // Procesos ya vinculados (objetos completos) + procesosVinculadosCompletos: (state) => { + return state.procesosDisponibles.filter(proceso => + state.procesosVinculados.includes(proceso.id) + ); + }, + + // Procesos disponibles (no vinculados) + procesosDisponiblesFiltrados: (state) => { + return state.procesosDisponibles.filter(proceso => + !state.procesosVinculados.includes(proceso.id) + ); + } + }, + actions: { - /* ============================= - * LISTAR ÁREAS (con filtros) - * GET /api/admin/areas - * ============================= */ + // ============================= + // LISTAR ÁREAS (con filtros) + // GET /api/admin/areas + // ============================= async fetchAreas(params = {}) { this.loading = true this.errors = null @@ -53,10 +88,10 @@ export const useAreaStore = defineStore('area', { } }, - /* ============================= - * MOSTRAR ÁREA - * GET /api/admin/areas/{id} - * ============================= */ + // ============================= + // MOSTRAR ÁREA + // GET /api/admin/areas/{id} + // ============================= async fetchArea(id) { this.loading = true this.errors = null @@ -71,10 +106,10 @@ export const useAreaStore = defineStore('area', { } }, - /* ============================= - * CREAR ÁREA - * POST /api/admin/areas - * ============================= */ + // ============================= + // CREAR ÁREA + // POST /api/admin/areas + // ============================= async createArea(payload) { this.loading = true this.errors = null @@ -93,10 +128,10 @@ export const useAreaStore = defineStore('area', { } }, - /* ============================= - * ACTUALIZAR ÁREA - * PUT /api/admin/areas/{id} - * ============================= */ + // ============================= + // ACTUALIZAR ÁREA + // PUT /api/admin/areas/{id} + // ============================= async updateArea(id, payload) { this.loading = true this.errors = null @@ -115,43 +150,42 @@ export const useAreaStore = defineStore('area', { } }, - /* ============================= - * ACTIVAR / DESACTIVAR ÁREA - * PATCH /api/admin/areas/{id}/toggle - * ============================= */ + // ============================= + // ACTIVAR / DESACTIVAR ÁREA + // PATCH /api/admin/areas/{id}/toggle + // ============================= async toggleArea(id) { - this.loading = true - this.errors = null + this.loading = true + this.errors = null - try { + try { const res = await api.patch(`/admin/areas/${id}/toggle`) // actualizar lista const index = this.areas.findIndex(a => a.id === id) if (index !== -1) { - this.areas[index].activo = res.data.data.activo + this.areas[index].activo = res.data.data.activo } // actualizar área actual si se está viendo if (this.area?.id === id) { - this.area.activo = res.data.data.activo + this.area.activo = res.data.data.activo } return res.data - } catch (error) { + } catch (error) { console.error(error) if (error.response) this.errors = error.response.data return null - } finally { + } finally { this.loading = false - } + } }, - - /* ============================= - * ELIMINAR ÁREA - * DELETE /api/admin/areas/{id} - * ============================= */ + // ============================= + // ELIMINAR ÁREA + // DELETE /api/admin/areas/{id} + // ============================= async deleteArea(id) { this.loading = true @@ -166,9 +200,169 @@ export const useAreaStore = defineStore('area', { } }, - /* ============================= - * SETTERS DE FILTROS - * ============================= */ + // ============================= + // CURSOS POR ÁREA + // GET /api/areas/{areaId}/cursos-disponibles + // ============================= + async fetchCursosPorArea(areaId) { + this.loading = true + this.error = null + try { + const response = await api.get(`/admin/areas/${areaId}/cursos-disponibles`) + if (response.data.success) { + this.cursosDisponibles = response.data.data.todos_los_cursos || [] + this.cursosVinculados = response.data.data.cursos_vinculados || [] + return response.data + } else { + this.error = response.data.message || 'Error cargando cursos' + return null + } + } catch (err) { + this.error = err.response?.data?.message || err.message + console.error('Error fetching cursos:', err) + return null + } finally { + this.loading = false + } + }, + + // ============================= + // VINCULAR CURSOS + // POST /api/areas/{areaId}/vincular-cursos + // ============================= + async vincularCursos(areaId, cursosIds) { + this.loading = true + this.error = null + try { + const response = await api.post(`/admin/areas/${areaId}/vincular-cursos`, { + cursos: cursosIds + }) + if (response.data.success) { + this.cursosVinculados = cursosIds + return response.data + } else { + this.error = response.data.message || 'Error vinculando cursos' + return null + } + } catch (err) { + this.error = err.response?.data?.message || err.message + console.error('Error vinculando cursos:', err) + return null + } finally { + this.loading = false + } + }, + + // ============================= + // DESVINCULAR CURSO + // POST /api/areas/{areaId}/desvincular-curso + // ============================= + async desvincularCurso(areaId, cursoId) { + this.loading = true + this.error = null + try { + const response = await api.post(`/admin/areas/${areaId}/desvincular-curso`, { + curso_id: cursoId + }) + if (response.data.success) { + this.cursosVinculados = this.cursosVinculados.filter(id => id !== cursoId) + return response.data + } else { + this.error = response.data.message || 'Error desvinculando curso' + return null + } + } catch (err) { + this.error = err.response?.data?.message || err.message + console.error('Error desvinculando curso:', err) + return null + } finally { + this.loading = false + } + }, + + // ============================= + // PROCESOS POR ÁREA + // GET /api/areas/{areaId}/procesos-disponibles + // ============================= + async fetchProcesosPorArea(areaId) { + this.loading = true + this.error = null + try { + const response = await api.get(`/admin/areas/${areaId}/procesos-disponibles`) + if (response.data.success) { + this.procesosDisponibles = response.data.data.todos_los_procesos || [] + this.procesosVinculados = response.data.data.procesos_vinculados || [] + return response.data + } else { + this.error = response.data.message || 'Error cargando procesos' + return null + } + } catch (err) { + this.error = err.response?.data?.message || err.message + console.error('Error fetching procesos:', err) + return null + } finally { + this.loading = false + } + }, + + // ============================= + // VINCULAR PROCESOS + // POST /api/areas/{areaId}/vincular-procesos + // ============================= + async vincularProcesos(areaId, procesosIds) { + this.loading = true + this.error = null + try { + const response = await api.post(`/admin/areas/${areaId}/vincular-procesos`, { + procesos: procesosIds + }) + if (response.data.success) { + this.procesosVinculados = procesosIds + return response.data + } else { + this.error = response.data.message || 'Error vinculando procesos' + return null + } + } catch (err) { + this.error = err.response?.data?.message || err.message + console.error('Error vinculando procesos:', err) + return null + } finally { + this.loading = false + } + }, + + // ============================= + // DESVINCULAR PROCESO + // POST /api/areas/{areaId}/desvincular-proceso + // ============================= + async desvincularProceso(areaId, procesoId) { + this.loading = true + this.error = null + try { + const response = await api.post(`/admin/areas/${areaId}/desvincular-proceso`, { + proceso_id: procesoId + }) + if (response.data.success) { + this.procesosVinculados = this.procesosVinculados.filter(id => id !== procesoId) + return response.data + } else { + this.error = response.data.message || 'Error desvinculando proceso' + return null + } + } catch (err) { + this.error = err.response?.data?.message || err.message + console.error('Error desvinculando proceso:', err) + return null + } finally { + this.loading = false + } + }, + + // ============================= + // ACCIONES DE LIMPIEZA + // ============================= setSearch(search) { this.filters.search = search }, @@ -184,6 +378,16 @@ export const useAreaStore = defineStore('area', { clearErrors() { this.errors = null + this.error = null }, - }, -}) + + clearState() { + this.cursosDisponibles = [] + this.cursosVinculados = [] + this.procesosDisponibles = [] + this.procesosVinculados = [] + this.errors = null + this.error = null + } + } +}) \ No newline at end of file diff --git a/front/src/store/areacursoStore.js b/front/src/store/areacursoStore.js new file mode 100644 index 0000000..444b049 --- /dev/null +++ b/front/src/store/areacursoStore.js @@ -0,0 +1,74 @@ +// stores/areasStore.js +import { defineStore } from 'pinia'; +import axios from 'axios'; + +export const useAreasStore = defineStore('areas', { + state: () => ({ + cursosDisponibles: [], // todos los cursos + cursosVinculados: [], // cursos vinculados a la área + loading: false, + error: null, + }), + + actions: { + // Obtener cursos de un área + async fetchCursosPorArea(areaId) { + this.loading = true; + this.error = null; + try { + const response = await axios.get(`/areas/${areaId}/cursos-disponibles`); + if (response.data.success) { + this.cursosDisponibles = response.data.data.todos_los_cursos; + this.cursosVinculados = response.data.data.cursos_vinculados; + } else { + this.error = 'Error cargando cursos'; + } + } catch (err) { + this.error = err.response?.data?.message || err.message; + } finally { + this.loading = false; + } + }, + + // Vincular cursos a un área + async vincularCursos(areaId, cursosIds) { + this.loading = true; + this.error = null; + try { + const response = await axios.post(`/areas/${areaId}/vincular-cursos`, { + cursos: cursosIds + }); + if (response.data.success) { + this.cursosVinculados = cursosIds; + } else { + this.error = 'Error vinculando cursos'; + } + } catch (err) { + this.error = err.response?.data?.message || err.message; + } finally { + this.loading = false; + } + }, + + // Desvincular un curso de un área + async desvincularCurso(areaId, cursoId) { + this.loading = true; + this.error = null; + try { + const response = await axios.post(`/areas/${areaId}/desvincular-curso`, { + curso_id: cursoId + }); + if (response.data.success) { + // Actualizar lista local + this.cursosVinculados = this.cursosVinculados.filter(id => id !== cursoId); + } else { + this.error = 'Error desvinculando curso'; + } + } catch (err) { + this.error = err.response?.data?.message || err.message; + } finally { + this.loading = false; + } + } + } +}); diff --git a/front/src/store/pregunta.store.js b/front/src/store/pregunta.store.js index e5d0121..c0cb537 100644 --- a/front/src/store/pregunta.store.js +++ b/front/src/store/pregunta.store.js @@ -91,6 +91,7 @@ async crearPregunta(formData) { headers: { 'Content-Type': 'multipart/form-data' } + }) if (response.data.success) { @@ -110,63 +111,32 @@ async crearPregunta(formData) { /* =============================== ACTUALIZAR PREGUNTA (SUMA IMÁGENES) =============================== */ - async actualizarPregunta(id, data) { - this.loading = true - this.errors = null - - try { - const formData = new FormData() - - formData.append('enunciado', data.enunciado) - formData.append('nivel_dificultad', data.nivel_dificultad) - formData.append('activo', data.activo ? 1 : 0) - - if (data.enunciado_adicional) - formData.append('enunciado_adicional', data.enunciado_adicional) - - if (data.respuesta_correcta) - formData.append('respuesta_correcta', data.respuesta_correcta) - - if (data.explicacion) - formData.append('explicacion', data.explicacion) - - if (data.opciones) - formData.append('opciones', JSON.stringify(data.opciones)) - - if (data.imagenes?.length) { - data.imagenes.forEach(img => { - formData.append('imagenes[]', img) - }) - } - - if (data.imagenes_explicacion?.length) { - data.imagenes_explicacion.forEach(img => { - formData.append('imagenes_explicacion[]', img) - }) - } - - formData.append('_method', 'PUT') - - const res = await api.post(`/admin/preguntas/${id}`, formData, { - headers: { 'Content-Type': 'multipart/form-data' }, - }) - - const index = this.preguntas.findIndex(p => p.id === id) - if (index !== -1) this.preguntas[index] = res.data.data - - if (this.pregunta?.id === id) this.pregunta = res.data.data - - return res.data.data - } catch (error) { - this.errors = - error.response?.status === 422 - ? error.response.data.errors - : error.response?.data || error.message - throw error - } finally { - this.loading = false +async actualizarPregunta(id, formData) { + this.loading = true + this.errors = null + + try { + // Usar PUT directamente + const response = await api.put(`/admin/preguntas/${id}`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' } - }, + }) + + if (response.data.success) { + return response.data + } else { + throw new Error(response.data.message || 'Error al actualizar pregunta') + } + } catch (error) { + if (error.response?.status === 422) { + this.errors = error.response.data.errors + } + throw error + } finally { + this.loading = false + } +}, /* =============================== ELIMINAR PREGUNTA diff --git a/front/src/views/administrador/areas/AreasList.vue b/front/src/views/administrador/areas/AreasList.vue index 6c039e8..21f79a0 100644 --- a/front/src/views/administrador/areas/AreasList.vue +++ b/front/src/views/administrador/areas/AreasList.vue @@ -97,9 +97,43 @@ {{ formatDate(record.created_at) }} + + + + + + @@ -215,9 +264,12 @@ import { EditOutlined, DeleteOutlined, SearchOutlined, - ReloadOutlined + ReloadOutlined, + BookOutlined, + ApartmentOutlined, } from '@ant-design/icons-vue' - +import CourseModal from '../areas/CursosModal.vue' +import ProcesosModal from '../areas/ProcesosModal.vue' // Store const areaStore = useAreaStore() @@ -237,6 +289,32 @@ const formState = reactive({ nombre: '', codigo: '' }) +const courseModalVisible = ref(false) +const selectedAreaForCourses = ref(null) +const processModalVisible = ref(false) +const selectedAreaForProcesses = ref(null) + +const showCourseModal = (area) => { + selectedAreaForCourses.value = area + courseModalVisible.value = true +} +// Método para actualizar después de cambios en cursos +const handleCoursesUpdated = () => { + // Recargar las áreas para reflejar cambios + areaStore.fetchAreas() + message.success('Cursos actualizados correctamente') +} + + +const showProcessModal = (area) => { + selectedAreaForProcesses.value = area + processModalVisible.value = true +} + +const handleProcessesUpdated = () => { + areaStore.fetchAreas() + message.success('Procesos actualizados correctamente') +} // Reglas de validación del formulario const formRules = { @@ -260,7 +338,6 @@ const pagination = computed(() => ({ showTotal: (total, range) => `${range[0]}-${range[1]} de ${total} áreas` })) -// Columnas de la tabla const columns = [ { title: 'ID', @@ -280,6 +357,20 @@ const columns = [ key: 'codigo', width: 120 }, + { + title: 'Cursos', + key: 'cursos_count', + width: 120, + align: 'center', + // customRender: ({ record }) => record.cursos_count || 0 + }, + { + title: 'Procesos', // NUEVA COLUMNA + key: 'procesos_count', + width: 120, + align: 'center', + // customRender: ({ record }) => record.procesos_count || 0 + }, { title: 'Estado', dataIndex: 'activo', @@ -295,11 +386,10 @@ const columns = [ { title: 'Acciones', key: 'acciones', - width: 180, + width: 250, // Aumentar el ancho por los dos botones nuevos align: 'center' } ] - // Métodos const showCreateModal = () => { isEditing.value = false @@ -528,7 +618,9 @@ onMounted(() => { .areas-table { border-radius: 12px; } - + .areas-table { + min-width: 900px; /* Aumentar por las nuevas columnas */ + } .areas-table :deep(.ant-table) { border-radius: 12px; } @@ -552,7 +644,16 @@ onMounted(() => { padding: 4px 8px; height: auto; } + /* Para los botones de cursos y procesos */ + .action-btn:nth-child(1), + .action-btn:nth-child(2) { + color: #1890ff; + } + .action-btn:nth-child(1):hover, + .action-btn:nth-child(2):hover { + color: #40a9ff; + } .action-btn :deep(.anticon) { font-size: 14px; } @@ -634,8 +735,27 @@ onMounted(() => { overflow-x: auto; } - .areas-table { - min-width: 800px; - } + + + /* En el scoped style */ +.action-btn { + padding: 4px 8px; + height: auto; + font-size: 12px; +} + +.action-btn :deep(.anticon) { + font-size: 12px; + margin-right: 4px; +} + +/* Para el botón de cursos específicamente */ +.action-btn:first-child { + color: #1890ff; +} + +.action-btn:first-child:hover { + color: #40a9ff; +} } \ No newline at end of file diff --git a/front/src/views/administrador/areas/CursosModal.vue b/front/src/views/administrador/areas/CursosModal.vue new file mode 100644 index 0000000..1529ad8 --- /dev/null +++ b/front/src/views/administrador/areas/CursosModal.vue @@ -0,0 +1,325 @@ + + + + + \ No newline at end of file diff --git a/front/src/views/administrador/areas/ProcesosModal.vue b/front/src/views/administrador/areas/ProcesosModal.vue new file mode 100644 index 0000000..d88c50a --- /dev/null +++ b/front/src/views/administrador/areas/ProcesosModal.vue @@ -0,0 +1,303 @@ + + + + + \ No newline at end of file diff --git a/front/src/views/administrador/cursos/MarkdownLatex.vue b/front/src/views/administrador/cursos/MarkdownLatex.vue new file mode 100644 index 0000000..a169ddf --- /dev/null +++ b/front/src/views/administrador/cursos/MarkdownLatex.vue @@ -0,0 +1,137 @@ + + + + + \ No newline at end of file diff --git a/front/src/views/administrador/cursos/PreguntasCursoView.vue b/front/src/views/administrador/cursos/PreguntasCursoView.vue index 9af568e..82496f8 100644 --- a/front/src/views/administrador/cursos/PreguntasCursoView.vue +++ b/front/src/views/administrador/cursos/PreguntasCursoView.vue @@ -201,7 +201,7 @@ :confirm-loading="preguntaStore.loading" @ok="handleModalPreguntaOk" @cancel="handleModalPreguntaCancel" - width="900px" + width="1200px" class="pregunta-modal" > - +
+
+
+ Editor + Markdown & LaTeX +
+ +
+ + Tips: Usa **negrita**, *cursiva*, $$fórmulas$$, `código` + +
+
+
+
+ Vista Previa + Actualización en tiempo real +
+
+
+ +
+
+ +

El contenido aparecerá aquí mientras escribes...

+
+
+
+
@@ -276,14 +306,39 @@ + - +
+
+
+ Editor + Markdown & LaTeX +
+ +
+
+
+ Vista Previa +
+
+
+ +
+
+ +

Contenido adicional aparecerá aquí...

+
+
+
+
@@ -353,11 +408,40 @@ - +
+
+
+ Editor + Markdown & LaTeX +
+ +
+ + Tips: Usa $$fórmulas$$ para ecuaciones, **importante**, listas con * + +
+
+
+
+ Vista Previa +
+
+
+ +
+
+ +

La explicación aparecerá aquí mientras escribes...

+
+
+
+
@@ -481,32 +565,34 @@ @cancel="verPreguntaModalVisible = false" >
- -
- + +
+ +
+ +
-

Imágenes del Enunciado:

+ class="imagenes-enunciado">
- +
- - + +

Información Adicional:

-
+
- +

Opciones de Respuesta:

- +

Explicación:

-
+
- +
-

Imágenes de la Explicación:

+ class="imagenes-explicacion">
- +
@@ -594,12 +679,10 @@
- Vista previa - @@ -612,6 +695,7 @@ import { useRoute, useRouter } from 'vue-router' import { usePreguntaStore } from '../../../store/pregunta.store' import { useCursoStore } from '../../../store/curso.store' import { message } from 'ant-design-vue' +import MarkdownLatex from '../cursos/MarkdownLatex.vue' import { PlusOutlined, EditOutlined, @@ -830,10 +914,10 @@ const getImageUrl = (path) => { const eliminarImagenExistente = (tipo, index) => { if (tipo === 'enunciado') { - formPreguntaState.imagenes_existentes.splice(index, 1) + formPreguntaState.imagenes_existentes = formPreguntaState.imagenes_existentes.filter((_, i) => i !== index) message.success('Imagen eliminada (se aplicará al guardar)') } else if (tipo === 'explicacion') { - formPreguntaState.imagenes_explicacion_existentes.splice(index, 1) + formPreguntaState.imagenes_explicacion_existentes = formPreguntaState.imagenes_explicacion_existentes.filter((_, i) => i !== index) message.success('Imagen eliminada (se aplicará al guardar)') } } @@ -844,6 +928,19 @@ const verImagen = (url) => { modalImagenVisible.value = true } +// Handlers para vista previa en tiempo real +const handleEnunciadoInput = () => { + // Actualización automática a través de v-model +} + +const handleAdicionalInput = () => { + // Actualización automática a través de v-model +} + +const handleExplicacionInput = () => { + // Actualización automática a través de v-model +} + // Métodos principales const goBack = () => { router.push({ name: 'AcademiaCursos' }) @@ -976,87 +1073,74 @@ const handleModalPreguntaCancel = () => { const submitPreguntaForm = async () => { try { - // Validar antes de enviar + const erroresValidacion = validarFormulario() if (erroresValidacion.length > 0) { - erroresValidacion.forEach(error => { - message.error(error) - }) + erroresValidacion.forEach(error => message.error(error)) return } - // Crear FormData para enviar archivos + // Crear FormData const formData = new FormData() - - // Agregar campos básicos formData.append('curso_id', formPreguntaState.curso_id) formData.append('enunciado', formPreguntaState.enunciado) formData.append('nivel_dificultad', formPreguntaState.nivel_dificultad) formData.append('activo', formPreguntaState.activo ? 1 : 0) - + if (formPreguntaState.enunciado_adicional) { formData.append('enunciado_adicional', formPreguntaState.enunciado_adicional) } - if (formPreguntaState.explicacion) { formData.append('explicacion', formPreguntaState.explicacion) } - - // Agregar opciones como array (no JSON stringificado) + + // Opciones const opcionesValidas = formPreguntaState.opciones.filter(op => op && op.trim() !== '') - opcionesValidas.forEach((opcion, index) => { - formData.append(`opciones[${index}]`, opcion) - }) - - if (formPreguntaState.respuesta_correcta) { - formData.append('respuesta_correcta', formPreguntaState.respuesta_correcta) - } - - // Agregar nuevas imágenes del enunciado + formData.append('opciones', JSON.stringify(opcionesValidas)) + formData.append('respuesta_correcta', formPreguntaState.respuesta_correcta) + + // NUEVAS IMÁGENES DEL ENUNCIADO imagenesEnunciadoFiles.value.forEach(file => { if (file.originFileObj) { formData.append('imagenes[]', file.originFileObj) } }) - - // Agregar nuevas imágenes de la explicación + + // NUEVAS IMÁGENES DE EXPLICACIÓN imagenesExplicacionFiles.value.forEach(file => { if (file.originFileObj) { formData.append('imagenes_explicacion[]', file.originFileObj) } }) - + + // IMÁGENES EXISTENTES (solo arrays, nunca strings dobles) if (isEditingPregunta.value) { - // Para edición, también enviar imágenes existentes if (formPreguntaState.imagenes_existentes?.length) { formData.append('imagenes_existentes', JSON.stringify(formPreguntaState.imagenes_existentes)) } - if (formPreguntaState.imagenes_explicacion_existentes?.length) { formData.append('imagenes_explicacion_existentes', JSON.stringify(formPreguntaState.imagenes_explicacion_existentes)) } - - // IMPORTANTE: Enviar curso_id también en actualización - formData.append('curso_id', formPreguntaState.curso_id) - + + // Llamar a store para actualizar await preguntaStore.actualizarPregunta(formPreguntaState.id, formData) message.success('Pregunta actualizada correctamente') } else { + // Crear nueva pregunta await preguntaStore.crearPregunta(formData) message.success('Pregunta creada correctamente') } - + + // Cerrar modal y limpiar formulario modalPreguntaVisible.value = false resetPreguntaForm() preguntaStore.errors = null await fetchPreguntas() - + } catch (error) { console.error('Error al guardar pregunta:', error) - // Mostrar errores específicos del backend si existen if (error.response && error.response.data.errors) { - const errors = error.response.data.errors - Object.values(errors).forEach(errorList => { + Object.values(error.response.data.errors).forEach(errorList => { errorList.forEach(err => message.error(err)) }) } else { @@ -1362,7 +1446,6 @@ onMounted(async () => { overflow: hidden; text-overflow: ellipsis; display: -webkit-box; - -webkit-box-orient: vertical; } @@ -1379,6 +1462,137 @@ onMounted(async () => { padding: 24px; } +/* Nuevos estilos para editor con vista previa */ +.editor-container { + display: flex; + gap: 16px; + margin-bottom: 16px; + height: 300px; +} + +.editor-column, +.preview-column { + flex: 1; + display: flex; + flex-direction: column; + height: 100%; +} + +.editor-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + padding-bottom: 8px; + border-bottom: 1px solid #f0f0f0; +} + +.editor-header span { + font-weight: 500; + color: #333; +} + +.markdown-editor { + flex: 1; + resize: none; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 14px; + line-height: 1.5; + padding: 12px; + border: 1px solid #d9d9d9; + border-radius: 6px; + background-color: #fafafa; +} + +.markdown-editor:focus { + border-color: #1890ff; + background-color: #fff; +} + +.editor-tips { + margin-top: 8px; + padding: 8px; + background-color: #f6ffed; + border-radius: 4px; + border: 1px solid #b7eb8f; + font-size: 12px; + color: #666; +} + +.preview-content { + flex: 1; + overflow-y: auto; + padding: 12px; + border: 1px solid #d9d9d9; + border-radius: 6px; + background-color: #fff; + min-height: 200px; +} + +.empty-preview { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: #999; + text-align: center; +} + +.empty-preview p { + margin-top: 8px; + font-size: 14px; +} + +.markdown-preview { + font-size: 14px; + line-height: 1.6; +} + +.markdown-preview :deep(h1) { + font-size: 1.5em; + margin-bottom: 0.5em; +} + +.markdown-preview :deep(h2) { + font-size: 1.3em; + margin-bottom: 0.5em; +} + +.markdown-preview :deep(p) { + margin-bottom: 1em; +} + +.markdown-preview :deep(ul), +.markdown-preview :deep(ol) { + margin-left: 1.5em; + margin-bottom: 1em; +} + +.markdown-preview :deep(code) { + background-color: #f5f5f5; + padding: 2px 4px; + border-radius: 3px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.9em; +} + +.markdown-preview :deep(pre) { + background-color: #f5f5f5; + padding: 12px; + border-radius: 6px; + overflow-x: auto; + margin-bottom: 1em; +} + +.markdown-preview :deep(blockquote) { + border-left: 4px solid #1890ff; + padding-left: 12px; + margin-left: 0; + color: #666; + font-style: italic; +} + /* Estilos adicionales */ .section-card { margin-bottom: 16px; @@ -1537,7 +1751,7 @@ onMounted(async () => { border-radius: 3px; } -.enunciado-completo { +.enunciado-principal { font-size: 16px; line-height: 1.6; margin-bottom: 20px; @@ -1725,5 +1939,74 @@ onMounted(async () => { max-width: 150px; max-height: 150px; } + + .editor-container { + flex-direction: column; + height: auto; + } + + .editor-column, + .preview-column { + width: 100%; + min-height: 200px; + } + + .markdown-editor { + min-height: 150px; + } + + .pregunta-view h4 { + margin-top: 16px; + margin-bottom: 8px; + color: #333; + } + + .imagenes-grid { + display: flex; + flex-wrap: wrap; + gap: 12px; + justify-content: center; + margin: 8px 0; + } + + .centered-image { + max-width: 100%; + height: auto; + cursor: pointer; + border-radius: 4px; + transition: transform 0.2s; + } + + .centered-image:hover { + transform: scale(1.05); + } + + .opcion-view { + display: flex; + align-items: center; + padding: 6px 12px; + margin-bottom: 6px; + border: 1px solid #e0e0e0; + border-radius: 4px; + } + + .opcion-view.correcta { + background-color: #f6ffed; + border-color: #b7eb8f; + } + + .opcion-letter { + font-weight: bold; + margin-right: 8px; + } + + .opcion-correct { + margin-left: auto; + } + + .explicacion-view, + .enunciado-adicional { + margin-top: 12px; + } } \ No newline at end of file diff --git a/front/src/views/administrador/cursos/RenderQuillContent.vue b/front/src/views/administrador/cursos/RenderQuillContent.vue new file mode 100644 index 0000000..6f637ff --- /dev/null +++ b/front/src/views/administrador/cursos/RenderQuillContent.vue @@ -0,0 +1,118 @@ + + + + + \ No newline at end of file