diff --git a/back/app/Http/Controllers/Administracion/PreguntaController.php b/back/app/Http/Controllers/Administracion/PreguntaController.php index fb93216..226950c 100644 --- a/back/app/Http/Controllers/Administracion/PreguntaController.php +++ b/back/app/Http/Controllers/Administracion/PreguntaController.php @@ -57,209 +57,185 @@ class PreguntaController extends Controller } } - public function getPregunta($id) - { - $pregunta = Pregunta::find($id); - if (!$pregunta) { - return response()->json([ - 'success' => false, - 'message' => 'Pregunta no encontrada' - ], 404); - } +public function getPregunta($id) +{ + $pregunta = Pregunta::find($id); + + if (!$pregunta) { return response()->json([ - 'success' => true, - 'data' => $pregunta - ]); + 'success' => false, + 'message' => 'Pregunta no encontrada' + ], 404); } - public function agregarPreguntaCurso(Request $request) - { - try { - $user = auth()->user(); +$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); - if (!$user->hasRole('Admin')) { - return response()->json([ - 'success' => false, - 'message' => 'No autorizado' - ], 403); - } + return response()->json([ + 'success' => true, + 'data' => $pregunta + ]); +} - $validator = Validator::make($request->all(), [ - 'curso_id' => 'required|exists:cursos,id', - 'enunciado' => 'required|string', - 'enunciado_adicional' => 'nullable|string', - 'opciones' => 'required|array|min:2', - 'opciones.*' => 'required|string', - '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', - ], [ - 'opciones.required' => 'Debe agregar al menos 2 opciones', - 'opciones.min' => 'Debe agregar al menos 2 opciones', - 'respuesta_correcta.required' => 'Debe seleccionar una respuesta correcta', - ]); +public function agregarPreguntaCurso(Request $request) +{ + try { + $user = auth()->user(); + if (!$user->hasRole('administrador')) { + return response()->json(['success' => false, 'message' => 'No autorizado'], 403); + } - if ($validator->fails()) { - return response()->json([ - 'success' => false, - 'errors' => $validator->errors() - ], 422); - } + // 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', + ]); - // Validar que la respuesta correcta esté en las opciones - if (!in_array($request->respuesta_correcta, $request->opciones)) { - return response()->json([ - 'success' => false, - 'errors' => ['respuesta_correcta' => ['La respuesta correcta debe estar entre las opciones']] - ], 422); - } + 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); - // Procesar imágenes del enunciado - $imagenesPaths = []; - if ($request->hasFile('imagenes')) { - foreach ($request->file('imagenes') as $imagen) { - $path = $imagen->store('preguntas/enunciados', 'public'); - $imagenesPaths[] = $path; - } + // 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 - $imagenesExplicacionPaths = []; - if ($request->hasFile('imagenes_explicacion')) { - foreach ($request->file('imagenes_explicacion') as $imagen) { - $path = $imagen->store('preguntas/explicaciones', 'public'); - $imagenesExplicacionPaths[] = $path; - } + // 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 } + } - $pregunta = Pregunta::create([ - 'curso_id' => $request->curso_id, - 'enunciado' => $request->enunciado, - 'enunciado_adicional' => $request->enunciado_adicional, - 'opciones' => $request->opciones, - 'respuesta_correcta' => $request->respuesta_correcta, - 'explicacion' => $request->explicacion, - 'nivel_dificultad' => $request->nivel_dificultad, - 'activo' => $request->boolean('activo'), - 'imagenes' => $imagenesPaths, - 'imagenes_explicacion' => $imagenesExplicacionPaths, - ]); + // 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, + ]); - Log::info('Pregunta creada', [ - 'pregunta_id' => $pregunta->id, - 'curso_id' => $request->curso_id, - 'user_id' => $user->id - ]); + return response()->json(['success' => true, 'message' => 'Pregunta creada correctamente', 'data' => $pregunta], 201); - 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); + } +} - } 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); } - } - public function actualizarPregunta(Request $request, $id) - { $pregunta = Pregunta::find($id); - if (!$pregunta) { - return response()->json([ - 'success' => false, - 'message' => 'Pregunta no encontrada' - ], 404); + return response()->json(['success' => false, 'message' => 'Pregunta no encontrada'], 404); } + // Validación (igual que antes) $validator = Validator::make($request->all(), [ - 'enunciado' => 'required|string', + 'curso_id' => 'required|exists:cursos,id', + 'enunciado' => 'required|string', 'enunciado_adicional' => 'nullable|string', - 'opciones' => 'required|array|min:2', - 'opciones.*' => 'required|string', - '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', + '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); + return response()->json(['success' => false, 'errors' => $validator->errors()], 422); } - // Validar que la respuesta correcta esté en las opciones - if (!in_array($request->respuesta_correcta, $request->opciones)) { - return response()->json([ - 'success' => false, - 'errors' => ['respuesta_correcta' => ['La respuesta correcta debe estar entre las opciones']] - ], 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); } - // Procesar nuevas imágenes del enunciado - $imagenesActuales = $pregunta->imagenes ?? []; + // 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[] = $path; + $imagenesActuales[] = url(Storage::url($path)); } } - // Procesar nuevas imágenes de la explicación - $imagenesExplicacionActuales = $pregunta->imagenes_explicacion ?? []; + // 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[] = $path; + $imagenesExplicacionActuales[] = url(Storage::url($path)); } } - // Si se enviaron imágenes existentes en edición - if ($request->has('imagenes_existentes')) { - $imagenesActuales = $request->imagenes_existentes; - } - - if ($request->has('imagenes_explicacion_existentes')) { - $imagenesExplicacionActuales = $request->imagenes_explicacion_existentes; - } - $pregunta->update([ - 'enunciado' => $request->enunciado, + 'curso_id' => $request->curso_id, + 'enunciado' => $request->enunciado, 'enunciado_adicional' => $request->enunciado_adicional, - 'opciones' => $request->opciones, - 'respuesta_correcta' => $request->respuesta_correcta, - 'explicacion' => $request->explicacion, - 'nivel_dificultad' => $request->nivel_dificultad, - 'activo' => $request->boolean('activo'), - 'imagenes' => $imagenesActuales, + '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 - ]); + 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/database/seeders/RolePermissionSeeder.php b/back/database/seeders/RolePermissionSeeder.php new file mode 100644 index 0000000..e69de29 diff --git a/back/database/seeders/RoleSeeder.php b/back/database/seeders/RoleSeeder.php index b641064..3b92244 100644 --- a/back/database/seeders/RoleSeeder.php +++ b/back/database/seeders/RoleSeeder.php @@ -5,27 +5,54 @@ namespace Database\Seeders; use Illuminate\Database\Seeder; use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Permission; -use Illuminate\Support\Facades\Schema; +use Spatie\Permission\PermissionRegistrar; class RoleSeeder extends Seeder { public function run(): void { - // Evita problemas si se vuelve a ejecutar - app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); + // 🔥 Limpiar cache de permisos para evitar conflictos + app()[PermissionRegistrar::class]->forgetCachedPermissions(); - // Roles base + /* ================= PERMISOS ================= */ + $permissions = [ + // Ejemplo: CRUD de preguntas + 'ver-preguntas', + 'crear-preguntas', + 'editar-preguntas', + 'eliminar-preguntas', + + // Ejemplo: CRUD de cursos + 'ver-cursos', + 'crear-cursos', + 'editar-cursos', + 'eliminar-cursos', + ]; + + foreach ($permissions as $permission) { + Permission::firstOrCreate([ + 'name' => $permission, + 'guard_name' => 'web', + ]); + } + + /* ================= ROLES ================= */ $roles = [ - 'usuario', - 'administrador', - 'superadmin', + 'usuario' => [], // rol básico sin permisos + 'administrador' => $permissions, // asigna todos los permisos al administrador + 'superadmin' => $permissions, // opcionalmente igual que administrador ]; - foreach ($roles as $role) { - Role::firstOrCreate([ - 'name' => $role, + foreach ($roles as $roleName => $rolePermissions) { + $role = Role::firstOrCreate([ + 'name' => $roleName, 'guard_name' => 'web', ]); + + // Asignar permisos si los hay + if (!empty($rolePermissions)) { + $role->syncPermissions($rolePermissions); + } } } } diff --git a/front/package-lock.json b/front/package-lock.json index 166a51f..d10d92b 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -1110,6 +1110,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -1119,6 +1120,7 @@ "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" }, @@ -1224,6 +1226,7 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -1233,7 +1236,6 @@ "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" }, @@ -1246,6 +1248,7 @@ "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", @@ -1257,6 +1260,7 @@ "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" }, @@ -1268,7 +1272,8 @@ "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" + "license": "MIT", + "peer": true }, "node_modules/combined-stream": { "version": "1.0.8", @@ -1331,6 +1336,7 @@ "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" } @@ -1348,7 +1354,8 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dom-align": { "version": "1.12.4", @@ -1380,7 +1387,8 @@ "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" + "license": "MIT", + "peer": true }, "node_modules/entities": { "version": "7.0.1", @@ -1510,6 +1518,7 @@ "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" @@ -1583,6 +1592,7 @@ "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.*" } @@ -1686,6 +1696,7 @@ "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" } @@ -1722,6 +1733,7 @@ "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" }, @@ -1827,6 +1839,7 @@ "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" }, @@ -1842,6 +1855,7 @@ "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" }, @@ -1854,6 +1868,7 @@ "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" } @@ -1863,6 +1878,7 @@ "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" } @@ -1885,7 +1901,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1919,6 +1934,7 @@ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" } @@ -1980,6 +1996,7 @@ "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" } @@ -1988,7 +2005,8 @@ "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" + "license": "ISC", + "peer": true }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", @@ -2060,7 +2078,8 @@ "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" + "license": "ISC", + "peer": true }, "node_modules/shallow-equal": { "version": "1.2.1", @@ -2091,6 +2110,7 @@ "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", @@ -2105,6 +2125,7 @@ "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" }, @@ -2168,7 +2189,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -2243,7 +2263,6 @@ "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", @@ -2335,13 +2354,15 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "license": "ISC" + "license": "ISC", + "peer": true }, "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", @@ -2355,13 +2376,15 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" + "license": "ISC", + "peer": true }, "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", @@ -2384,6 +2407,7 @@ "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/src/router/index.js b/front/src/router/index.js index 4f7b4a3..c772e72 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -14,6 +14,11 @@ const routes = [ component: Login, meta: { guest: true } }, + { + path: '/unauthorized', + name: 'Unauthorized', + component: () => import('../views/403.vue') + }, { path: '/usuario/dashboard', name: 'dashboard', @@ -51,14 +56,14 @@ const routes = [ path: '/admin/dashboard/cursos/:id/preguntas', name: 'CursoPreguntas', component: () => import('../views/administrador/cursos/PreguntasCursoView.vue'), - meta: { requiresAuth: true } + meta: { requiresAuth: true, role: 'administrador' } }, { path: '/admin/dashboard/procesos', name: 'Procesos', component: () => import('../views/administrador/Procesos/ProcesosList.vue'), - meta: { requiresAuth: true } + meta: { requiresAuth: true, role: 'administrador' } } ] diff --git a/front/src/store/pregunta.store.js b/front/src/store/pregunta.store.js index 0f7acce..e5d0121 100644 --- a/front/src/store/pregunta.store.js +++ b/front/src/store/pregunta.store.js @@ -56,61 +56,57 @@ export const usePreguntaStore = defineStore('pregunta', { /* =============================== CREAR PREGUNTA (CON IMÁGENES) =============================== */ - async crearPregunta(data) { +async crearPregunta(formData) { this.loading = true this.errors = null - + try { - const formData = new FormData() - - // Campos simples - formData.append('curso_id', data.curso_id) - formData.append('enunciado', data.enunciado) - formData.append('nivel_dificultad', data.nivel_dificultad) - - 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)) - - // Imágenes del enunciado - if (data.imagenes?.length) { - data.imagenes.forEach(img => { - formData.append('imagenes[]', img) - }) + const response = await api.post('/admin/preguntas', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + + if (response.data.success) { + return response.data + } else { + throw new Error(response.data.message || 'Error al crear pregunta') } - - // Imágenes de la explicación - if (data.imagenes_explicacion?.length) { - data.imagenes_explicacion.forEach(img => { - formData.append('imagenes_explicacion[]', img) - }) + } catch (error) { + if (error.response?.status === 422) { + this.errors = error.response.data.errors } - - const res = await api.post('/admin/preguntas', formData, { - headers: { 'Content-Type': 'multipart/form-data' }, + throw error + } finally { + this.loading = false + } + }, + + async actualizarPregunta(id, formData) { + this.loading = true + this.errors = null + + try { + const response = await api.post(`/admin/preguntas/${id}`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } }) - - this.preguntas.unshift(res.data.data) - return res.data.data + + if (response.data.success) { + return response.data + } else { + throw new Error(response.data.message || 'Error al actualizar pregunta') + } } catch (error) { - this.errors = - error.response?.status === 422 - ? error.response.data.errors - : error.response?.data || error.message + if (error.response?.status === 422) { + this.errors = error.response.data.errors + } throw error } finally { this.loading = false } }, - /* =============================== ACTUALIZAR PREGUNTA (SUMA IMÁGENES) =============================== */ diff --git a/front/src/views/Login.vue b/front/src/views/Login.vue index c4b3e13..6b05dfc 100644 --- a/front/src/views/Login.vue +++ b/front/src/views/Login.vue @@ -25,64 +25,61 @@ class="login-form" > - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/front/src/views/administrador/cursos/PreguntasCursoView.vue b/front/src/views/administrador/cursos/PreguntasCursoView.vue index 664cd6d..9af568e 100644 --- a/front/src/views/administrador/cursos/PreguntasCursoView.vue +++ b/front/src/views/administrador/cursos/PreguntasCursoView.vue @@ -489,14 +489,14 @@ class="imagenes-preview">

Imágenes del Enunciado:

- +
@@ -1002,9 +1002,11 @@ const submitPreguntaForm = async () => { formData.append('explicacion', formPreguntaState.explicacion) } - // Agregar opciones como JSON + // Agregar opciones como array (no JSON stringificado) const opcionesValidas = formPreguntaState.opciones.filter(op => op && op.trim() !== '') - formData.append('opciones', JSON.stringify(opcionesValidas)) + opcionesValidas.forEach((opcion, index) => { + formData.append(`opciones[${index}]`, opcion) + }) if (formPreguntaState.respuesta_correcta) { formData.append('respuesta_correcta', formPreguntaState.respuesta_correcta) @@ -1025,9 +1027,17 @@ const submitPreguntaForm = async () => { }) if (isEditingPregunta.value) { - // Para edición, también enviar imágenes existentes que no fueron eliminadas - formData.append('imagenes_existentes', JSON.stringify(formPreguntaState.imagenes_existentes)) - formData.append('imagenes_explicacion_existentes', JSON.stringify(formPreguntaState.imagenes_explicacion_existentes)) + // 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) await preguntaStore.actualizarPregunta(formPreguntaState.id, formData) message.success('Pregunta actualizada correctamente') @@ -1043,7 +1053,15 @@ const submitPreguntaForm = async () => { } catch (error) { console.error('Error al guardar pregunta:', error) - message.error('Error al guardar la pregunta') + // 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 => { + errorList.forEach(err => message.error(err)) + }) + } else { + message.error('Error al guardar la pregunta') + } } }