adsdasdasd

main
Elmer Yujra Condori 2 months ago
parent b62acb8d62
commit 5b16207e51

@ -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)
{

@ -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);
}
}
}
}

@ -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"

@ -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' }
}
]

@ -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)
=============================== */

@ -25,64 +25,61 @@
class="login-form"
>
<!-- Nombre (solo registro) -->
<a-form-item
v-if="isRegister"
label="Nombre completo"
name="name"
>
<a-input
v-model:value="formState.name"
placeholder="Ingrese su nombre"
size="large"
>
<template #prefix>
<UserOutlined />
</template>
</a-input>
</a-form-item>
<!-- Email -->
<a-form-item label="Correo electrónico" name="email">
<a-input
v-model:value="formState.email"
placeholder="ejemplo@correo.com"
size="large"
>
<template #prefix>
<MailOutlined />
</template>
</a-input>
</a-form-item>
<!-- Contraseña -->
<a-form-item label="Contraseña" name="password">
<a-input-password
v-model:value="formState.password"
placeholder="Ingrese su contraseña"
size="large"
>
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<!-- Confirmar contraseña (solo registro) -->
<a-form-item
v-if="isRegister"
label="Confirmar contraseña"
name="password_confirmation"
>
<a-input-password
v-model:value="formState.password_confirmation"
placeholder="Confirme su contraseña"
size="large"
>
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<!-- Nombre (solo registro) -->
<a-form-item v-if="isRegister" label="Nombre completo" name="name">
<a-input
v-model:value="formState.name"
placeholder="Ingrese su nombre"
size="large"
autocomplete="name"
>
<template #prefix>
<UserOutlined />
</template>
</a-input>
</a-form-item>
<!-- Email -->
<a-form-item label="Correo electrónico" name="email">
<a-input
v-model:value="formState.email"
placeholder="ejemplo@correo.com"
size="large"
autocomplete="email"
>
<template #prefix>
<MailOutlined />
</template>
</a-input>
</a-form-item>
<!-- Contraseña -->
<a-form-item label="Contraseña" name="password">
<a-input-password
v-model:value="formState.password"
placeholder="Ingrese su contraseña"
size="large"
autocomplete="current-password"
>
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<!-- Confirmar contraseña (solo registro) -->
<a-form-item v-if="isRegister" label="Confirmar contraseña" name="password_confirmation">
<a-input-password
v-model:value="formState.password_confirmation"
placeholder="Confirme su contraseña"
size="large"
autocomplete="new-password"
>
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<!-- Recordarme (solo login) -->
<div v-if="!isRegister" class="remember-forgot">

@ -489,14 +489,14 @@
class="imagenes-preview">
<h4>Imágenes del Enunciado:</h4>
<div class="imagenes-grid">
<img
v-for="(imagen, index) in preguntaSeleccionada.imagenes"
:key="index"
:src="getImageUrl(imagen)"
:alt="'Imagen ' + (index+1)"
@click="verImagen(getImageUrl(imagen))"
class="clickable-image"
/>
<img
v-for="(imagen, index) in preguntaSeleccionada.imagenes"
:key="index"
:src="imagen"
:alt="'Imagen ' + (index+1)"
@click="verImagen(imagen)"
class="clickable-image"
/>
</div>
</div>
@ -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')
}
}
}

Loading…
Cancel
Save