diff --git a/back/app/Http/Controllers/Administracion/CalificacionController.php b/back/app/Http/Controllers/Administracion/CalificacionController.php new file mode 100644 index 0000000..5b4d2bd --- /dev/null +++ b/back/app/Http/Controllers/Administracion/CalificacionController.php @@ -0,0 +1,109 @@ +json([ + 'success' => true, + 'data' => $calificaciones + ]); + } + + // ✅ Guardar nueva + public function store(Request $request) + { + $request->validate([ + 'nombre' => 'required|string|max:255', + 'puntos_correcta' => 'required|numeric', + 'puntos_incorrecta' => 'required|numeric', + 'puntos_nula' => 'required|numeric', + 'puntaje_maximo' => 'required|numeric', + ]); + + $calificacion = Calificacion::create($request->all()); + + return response()->json([ + 'success' => true, + 'message' => 'Calificación creada correctamente', + 'data' => $calificacion + ]); + } + + // ✅ Mostrar una + public function show($id) + { + $calificacion = Calificacion::find($id); + + if (!$calificacion) { + return response()->json([ + 'success' => false, + 'message' => 'No encontrada' + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => $calificacion + ]); + } + + // ✅ Actualizar + public function update(Request $request, $id) + { + $calificacion = Calificacion::find($id); + + if (!$calificacion) { + return response()->json([ + 'success' => false, + 'message' => 'No encontrada' + ], 404); + } + + $request->validate([ + 'nombre' => 'required|string|max:255', + 'puntos_correcta' => 'required|numeric', + 'puntos_incorrecta' => 'required|numeric', + 'puntos_nula' => 'required|numeric', + 'puntaje_maximo' => 'required|numeric', + ]); + + $calificacion->update($request->all()); + + return response()->json([ + 'success' => true, + 'message' => 'Calificación actualizada correctamente', + 'data' => $calificacion + ]); + } + + // ✅ Eliminar + public function destroy($id) + { + $calificacion = Calificacion::find($id); + + if (!$calificacion) { + return response()->json([ + 'success' => false, + 'message' => 'No encontrada' + ], 404); + } + + $calificacion->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Calificación eliminada correctamente' + ]); + } +} \ No newline at end of file diff --git a/back/app/Http/Controllers/Administracion/ExamenesController.php b/back/app/Http/Controllers/Administracion/ExamenesController.php deleted file mode 100644 index 944024b..0000000 --- a/back/app/Http/Controllers/Administracion/ExamenesController.php +++ /dev/null @@ -1,465 +0,0 @@ -user(); - - if (!$user->hasRole('administrador')) { - return response()->json([ - 'success' => false, - 'message' => 'No autorizado' - ], 403); - } - - $academia = Academia::where('admin_academia_id', $user->id)->first(); - - if (!$academia) { - return response()->json([ - 'success' => false, - 'message' => 'Academia no encontrada' - ], 404); - } - - $query = $academia->examenes() - ->withCount(['preguntas', 'intentos' => function($query) { - $query->where('estado', 'finalizado'); - }]) - ->latest(); - - if ($request->has('search')) { - $search = $request->search; - $query->where('titulo', 'like', "%{$search}%"); - } - - if ($request->has('publicado')) { - $query->where('publicado', $request->publicado); - } - - if ($request->has('tipo')) { - $query->where('tipo', $request->tipo); - } - - $examenes = $query->paginate(15); - - return response()->json([ - 'success' => true, - 'data' => $examenes - ]); - - } catch (\Exception $e) { - Log::error('Error obteniendo exámenes', [ - 'error' => $e->getMessage() - ]); - - return response()->json([ - 'success' => false, - 'message' => 'Error al cargar los exámenes' - ], 500); - } - } - - public function crearExamen(Request $request) - { - try { - $user = auth()->user(); - - if (!$user->hasRole('administrador')) { - return response()->json([ - 'success' => false, - 'message' => 'No autorizado' - ], 403); - } - - $academia = Academia::where('admin_academia_id', $user->id)->first(); - - if (!$academia) { - return response()->json([ - 'success' => false, - 'message' => 'Academia no encontrada' - ], 404); - } - - $validator = Validator::make($request->all(), [ - 'titulo' => 'required|string|max:255', - 'descripcion' => 'nullable|string', - 'tipo' => 'required|in:practica,simulacro,evaluacion', - 'dificultad' => 'required|in:facil,medio,dificil,avanzado', - - 'duracion_minutos' => 'required|integer|min:1|max:480', - 'intentos_permitidos' => 'required|integer|min:1|max:10', - 'puntaje_minimo' => 'required|numeric|min:0|max:100', - 'preguntas_aleatorias' => 'boolean', - 'mostrar_resultados' => 'boolean', - 'mostrar_respuestas' => 'boolean', - 'publicado' => 'boolean', - 'fecha_inicio' => 'nullable|date', - 'fecha_fin' => 'nullable|date|after_or_equal:fecha_inicio', - 'configuracion' => 'nullable|array' - ]); - - if ($validator->fails()) { - return response()->json([ - 'success' => false, - 'errors' => $validator->errors() - ], 422); - } - - DB::beginTransaction(); - - $examen = Examen::create([ - 'academia_id' => $academia->id, - 'titulo' => $request->titulo, - 'descripcion' => $request->descripcion, - 'tipo' => $request->tipo, - 'dificultad' => $request->dificultad, - 'duracion_minutos' => $request->duracion_minutos, - 'intentos_permitidos' => $request->intentos_permitidos, - 'puntaje_minimo' => $request->puntaje_minimo, - - 'preguntas_aleatorias' => $request->preguntas_aleatorias ?? 0, - - 'mezclar_opciones' => $request->mezclar_opciones ?? true, - 'mostrar_resultados' => $request->mostrar_resultados ?? true, - 'mostrar_respuestas' => $request->mostrar_respuestas ?? false, - 'mostrar_explicaciones' => $request->mostrar_explicaciones ?? false, - - 'activar_timer' => $request->activar_timer ?? true, - 'permitir_navegacion' => $request->permitir_navegacion ?? true, - 'permitir_revisar' => $request->permitir_revisar ?? true, - - 'publicado' => $request->publicado ?? false, - 'fecha_inicio' => $request->fecha_inicio, - 'fecha_fin' => $request->fecha_fin, - 'orden' => $request->orden ?? 1, - - 'configuracion' => $request->configuracion ?? [] - ]); - - DB::commit(); - - Log::info('Examen creado por admin', [ - 'academia_id' => $academia->id, - 'examen_id' => $examen->id, - 'admin_id' => $user->id - ]); - - return response()->json([ - 'success' => true, - 'message' => 'Examen creado exitosamente', - 'data' => $examen - ], 201); - - } catch (\Exception $e) { - DB::rollBack(); - - Log::error('Error creando examen', [ - 'error' => $e->getMessage() - ]); - - return response()->json([ - 'success' => false, - 'message' => 'Error al crear el examen' - ], 500); - } - } - - public function getExamen($examenId) - { - try { - $user = auth()->user(); - - if (!$user->hasRole('administrador')) { - return response()->json([ - 'success' => false, - 'message' => 'No autorizado' - ], 403); - } - - $academia = Academia::where('admin_academia_id', $user->id)->first(); - - if (!$academia) { - return response()->json([ - 'success' => false, - 'message' => 'Academia no encontrada' - ], 404); - } - - $examen = Examen::where('academia_id', $academia->id) - ->with(['preguntas']) - ->find($examenId); - - if (!$examen) { - return response()->json([ - 'success' => false, - 'message' => 'Examen no encontrado' - ], 404); - } - - $estadisticas = DB::table('intentos_examen') - ->where('examen_id', $examenId) - ->where('estado', 'finalizado') - ->selectRaw('COUNT(*) as total_intentos') - ->selectRaw('AVG(porcentaje) as promedio') - ->selectRaw('SUM(CASE WHEN aprobado = 1 THEN 1 ELSE 0 END) as aprobados') - ->first(); - - return response()->json([ - 'success' => true, - 'data' => [ - 'examen' => $examen, - 'estadisticas' => $estadisticas - ] - ]); - - } catch (\Exception $e) { - Log::error('Error obteniendo examen', [ - 'error' => $e->getMessage() - ]); - - return response()->json([ - 'success' => false, - 'message' => 'Error al cargar el examen' - ], 500); - } - } - - public function actualizarExamen(Request $request, $examenId) - { - try { - $user = auth()->user(); - - if (!$user->hasRole('administrador')) { - return response()->json([ - 'success' => false, - 'message' => 'No autorizado' - ], 403); - } - - $academia = Academia::where('admin_academia_id', $user->id)->first(); - - if (!$academia) { - return response()->json([ - 'success' => false, - 'message' => 'Academia no encontrada' - ], 404); - } - - $examen = Examen::where('academia_id', $academia->id)->find($examenId); - - if (!$examen) { - return response()->json([ - 'success' => false, - 'message' => 'Examen no encontrado' - ], 404); - } - - $validator = Validator::make($request->all(), [ - 'titulo' => 'sometimes|string|max:255', - 'descripcion' => 'nullable|string', - 'tipo' => 'sometimes|in:practica,simulacro,evaluacion', - 'duracion_minutos' => 'sometimes|integer|min:1|max:480', - 'intentos_permitidos' => 'sometimes|integer|min:1|max:10', - 'puntaje_minimo' => 'sometimes|numeric|min:0|max:100', - 'preguntas_aleatorias' => 'boolean', - 'mostrar_resultados' => 'boolean', - 'mostrar_respuestas' => 'boolean', - 'publicado' => 'boolean', - 'fecha_inicio' => 'nullable|date', - 'fecha_fin' => 'nullable|date|after_or_equal:fecha_inicio', - 'configuracion' => 'nullable|array' - ]); - - if ($validator->fails()) { - return response()->json([ - 'success' => false, - 'errors' => $validator->errors() - ], 422); - } - - $examen->update($request->only([ - 'titulo', 'descripcion', 'tipo', 'duracion_minutos', 'intentos_permitidos', - 'puntaje_minimo', 'preguntas_aleatorias', 'mostrar_resultados', - 'mostrar_respuestas', 'publicado', 'fecha_inicio', 'fecha_fin', 'configuracion' - ])); - - Log::info('Examen actualizado por admin', [ - 'academia_id' => $academia->id, - 'examen_id' => $examen->id, - 'admin_id' => $user->id - ]); - - return response()->json([ - 'success' => true, - 'message' => 'Examen actualizado exitosamente', - 'data' => $examen - ]); - - } catch (\Exception $e) { - Log::error('Error actualizando examen', [ - 'error' => $e->getMessage() - ]); - - return response()->json([ - 'success' => false, - 'message' => 'Error al actualizar el examen' - ], 500); - } - } - - public function eliminarExamen($examenId) - { - try { - $user = auth()->user(); - - if (!$user->hasRole('administrador')) { - return response()->json([ - 'success' => false, - 'message' => 'No autorizado' - ], 403); - } - - $academia = Academia::where('admin_academia_id', $user->id)->first(); - - if (!$academia) { - return response()->json([ - 'success' => false, - 'message' => 'Academia no encontrada' - ], 404); - } - - $examen = Examen::where('academia_id', $academia->id)->find($examenId); - - if (!$examen) { - return response()->json([ - 'success' => false, - 'message' => 'Examen no encontrado' - ], 404); - } - - $tieneIntentos = $examen->intentos()->exists(); - - if ($tieneIntentos) { - return response()->json([ - 'success' => false, - 'message' => 'No se puede eliminar un examen con intentos realizados' - ], 400); - } - - $examen->delete(); - - Log::info('Examen eliminado por admin', [ - 'academia_id' => $academia->id, - 'examen_id' => $examenId, - 'admin_id' => $user->id - ]); - - return response()->json([ - 'success' => true, - 'message' => 'Examen eliminado exitosamente' - ]); - - } catch (\Exception $e) { - Log::error('Error eliminando examen', [ - 'error' => $e->getMessage() - ]); - - return response()->json([ - 'success' => false, - 'message' => 'Error al eliminar el examen' - ], 500); - } - } - - public function getResultadosExamen($examenId) - { - try { - $user = auth()->user(); - - if (!$user->hasRole('administrador')) { - return response()->json([ - 'success' => false, - 'message' => 'No autorizado' - ], 403); - } - - $academia = Academia::where('admin_academia_id', $user->id)->first(); - - if (!$academia) { - return response()->json([ - 'success' => false, - 'message' => 'Academia no encontrada' - ], 404); - } - - $examen = Examen::where('academia_id', $academia->id)->find($examenId); - - if (!$examen) { - return response()->json([ - 'success' => false, - 'message' => 'Examen no encontrado' - ], 404); - } - - $resultados = DB::table('intentos_examen') - ->join('users', 'intentos_examen.user_id', '=', 'users.id') - ->where('intentos_examen.examen_id', $examenId) - ->where('intentos_examen.estado', 'finalizado') - ->select( - 'users.id as estudiante_id', - 'users.name as estudiante_nombre', - 'users.email as estudiante_email', - 'intentos_examen.numero_intento', - 'intentos_examen.porcentaje', - 'intentos_examen.aprobado', - 'intentos_examen.tiempo_utilizado', - 'intentos_examen.finalizado_en' - ) - ->orderBy('intentos_examen.porcentaje', 'desc') - ->get(); - - $estadisticas = [ - 'total_estudiantes' => $resultados->groupBy('estudiante_id')->count(), - 'promedio' => $resultados->avg('porcentaje'), - 'aprobados' => $resultados->where('aprobado', true)->count(), - 'reprobados' => $resultados->where('aprobado', false)->count(), - 'tiempo_promedio' => $resultados->avg('tiempo_utilizado') - ]; - - return response()->json([ - 'success' => true, - 'data' => [ - 'resultados' => $resultados, - 'estadisticas' => $estadisticas - ] - ]); - - } catch (\Exception $e) { - Log::error('Error obteniendo resultados', [ - 'error' => $e->getMessage() - ]); - - return response()->json([ - 'success' => false, - 'message' => 'Error al cargar los resultados' - ], 500); - } - } - -} \ No newline at end of file diff --git a/back/app/Http/Controllers/Administracion/NoticiaController.php b/back/app/Http/Controllers/Administracion/NoticiaController.php new file mode 100644 index 0000000..03bf1dc --- /dev/null +++ b/back/app/Http/Controllers/Administracion/NoticiaController.php @@ -0,0 +1,209 @@ +get('per_page', 9); + + $query = Noticia::query(); + + if ($request->filled('publicado')) { + $query->where('publicado', $request->boolean('publicado')); + } + + if ($request->filled('categoria')) { + $query->where('categoria', (string) $request->get('categoria')); + } + + if ($request->filled('q')) { + $q = trim((string) $request->get('q')); + $query->where(function ($sub) use ($q) { + $sub->where('titulo', 'like', "%{$q}%") + ->orWhere('descripcion_corta', 'like', "%{$q}%"); + }); + } + + $data = $query + ->orderByDesc('destacado') + ->orderByDesc('fecha_publicacion') + ->orderByDesc('orden') + ->orderByDesc('id') + ->paginate($perPage); + + return response()->json([ + 'success' => true, + 'data' => $data->items(), // incluye imagen_url por el accessor/appends + 'meta' => [ + 'current_page' => $data->currentPage(), + 'last_page' => $data->lastPage(), + 'per_page' => $data->perPage(), + 'total' => $data->total(), + ], + ]); + } + + // GET /api/noticias/{noticia} + public function show(Noticia $noticia) + { + return response()->json([ + 'success' => true, + 'data' => $noticia, // incluye imagen_url por el accessor/appends + ]); + } + + // GET /api/noticias-publicas/{noticia} (o la ruta que uses) + public function showPublic(Noticia $noticia) + { + abort_unless($noticia->publicado, 404); + + return response()->json([ + 'success' => true, + 'data' => $noticia, + ]); + } + + // POST /api/noticias (multipart/form-data si viene imagen) + public function store(Request $request) + { + $data = $request->validate([ + 'titulo' => ['required', 'string', 'max:220'], + 'slug' => ['nullable', 'string', 'max:260', 'unique:noticias,slug'], + 'descripcion_corta' => ['nullable', 'string', 'max:500'], + 'contenido' => ['nullable', 'string'], + 'categoria' => ['nullable', 'string', 'max:80'], + 'tag_color' => ['nullable', 'string', 'max:30'], + + // ✅ dos formas de imagen + 'imagen' => ['nullable', 'file', 'mimes:jpg,jpeg,png,webp', 'max:4096'], + 'imagen_url' => ['nullable', 'url', 'max:600'], + + 'link_url' => ['nullable', 'url', 'max:600'], + 'link_texto' => ['nullable', 'string', 'max:120'], + 'fecha_publicacion' => ['nullable', 'date'], + 'publicado' => ['nullable', 'boolean'], + 'destacado' => ['nullable', 'boolean'], + 'orden' => ['nullable', 'integer'], + ]); + + // slug por defecto (igual tu modelo lo genera, pero aquí lo dejamos por consistencia) + if (empty($data['slug'])) { + $data['slug'] = Str::slug($data['titulo']); + } + + // si viene archivo, manda a storage y prioriza archivo + if ($request->hasFile('imagen')) { + $path = $request->file('imagen')->store('noticias', 'public'); + $data['imagen_path'] = $path; + $data['imagen_url'] = null; // ✅ evita conflicto con url externa + } else { + // si viene imagen_url externa, no debe haber imagen_path + $data['imagen_path'] = null; + } + + // si publican sin fecha, poner ahora + if (!empty($data['publicado']) && empty($data['fecha_publicacion'])) { + $data['fecha_publicacion'] = now(); + } + + $noticia = Noticia::create($data); + + return response()->json([ + 'success' => true, + 'data' => $noticia->fresh(), + ], 201); + } + + // PUT/PATCH /api/noticias/{noticia} + public function update(Request $request, Noticia $noticia) + { + $data = $request->validate([ + 'titulo' => ['sometimes', 'required', 'string', 'max:220'], + 'slug' => ['sometimes', 'nullable', 'string', 'max:260', 'unique:noticias,slug,' . $noticia->id], + 'descripcion_corta' => ['sometimes', 'nullable', 'string', 'max:500'], + 'contenido' => ['sometimes', 'nullable', 'string'], + 'categoria' => ['sometimes', 'nullable', 'string', 'max:80'], + 'tag_color' => ['sometimes', 'nullable', 'string', 'max:30'], + + // ✅ dos formas de imagen + 'imagen' => ['sometimes', 'nullable', 'file', 'mimes:jpg,jpeg,png,webp', 'max:4096'], + 'imagen_url' => ['sometimes', 'nullable', 'url', 'max:600'], + + 'link_url' => ['sometimes', 'nullable', 'url', 'max:600'], + 'link_texto' => ['sometimes', 'nullable', 'string', 'max:120'], + 'fecha_publicacion' => ['sometimes', 'nullable', 'date'], + 'publicado' => ['sometimes', 'boolean'], + 'destacado' => ['sometimes', 'boolean'], + 'orden' => ['sometimes', 'integer'], + ]); + + // Si llega imagen archivo, reemplaza la anterior y limpia imagen_url externa + if ($request->hasFile('imagen')) { + if ($noticia->imagen_path && Storage::disk('public')->exists($noticia->imagen_path)) { + Storage::disk('public')->delete($noticia->imagen_path); + } + $path = $request->file('imagen')->store('noticias', 'public'); + + $data['imagen_path'] = $path; + $data['imagen_url'] = null; // ✅ prioridad archivo + } + + // Si llega imagen_url (externa), borra la imagen física anterior y limpia imagen_path + if (array_key_exists('imagen_url', $data) && !empty($data['imagen_url'])) { + if ($noticia->imagen_path && Storage::disk('public')->exists($noticia->imagen_path)) { + Storage::disk('public')->delete($noticia->imagen_path); + } + $data['imagen_path'] = null; + } + + // Si explícitamente mandan imagen_url = null (quitar url) y no mandan archivo, + // no tocamos imagen_path (se queda como está). Si quieres que también limpie path, + // dime y lo cambiamos. + + // si se marca publicado y no hay fecha, set now + if ( + array_key_exists('publicado', $data) && + $data['publicado'] && + empty($noticia->fecha_publicacion) && + empty($data['fecha_publicacion']) + ) { + $data['fecha_publicacion'] = now(); + } + + // si cambian titulo y slug no vino, regenerar slug (opcional) + if (array_key_exists('titulo', $data) && !array_key_exists('slug', $data)) { + $data['slug'] = Str::slug($data['titulo']); + } + + $noticia->update($data); + + return response()->json([ + 'success' => true, + 'data' => $noticia->fresh(), + ]); + } + + // DELETE /api/noticias/{noticia} + public function destroy(Noticia $noticia) + { + if ($noticia->imagen_path && Storage::disk('public')->exists($noticia->imagen_path)) { + Storage::disk('public')->delete($noticia->imagen_path); + } + + $noticia->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Noticia eliminada correctamente', + ]); + } +} diff --git a/back/app/Http/Controllers/Administracion/PostulanteController.php b/back/app/Http/Controllers/Administracion/PostulanteController.php new file mode 100644 index 0000000..7feb6c0 --- /dev/null +++ b/back/app/Http/Controllers/Administracion/PostulanteController.php @@ -0,0 +1,64 @@ +buscar) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', "%{$request->buscar}%") + ->orWhere('email', 'like', "%{$request->buscar}%") + ->orWhere('dni', 'like', "%{$request->buscar}%"); + }); + } + + $postulantes = $query->orderBy('id', 'desc') + ->paginate(20); + + return response()->json([ + 'success' => true, + 'data' => $postulantes + ]); + } + + public function actualizarPostulante(Request $request, $id) + { + $postulante = Postulante::findOrFail($id); + + $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|email|unique:postulantes,email,' . $postulante->id, + 'dni' => 'required|string|max:20|unique:postulantes,dni,' . $postulante->id, + 'password' => 'nullable|string|min:6' + ]); + + $postulante->update([ + 'name' => $request->name, + 'email' => $request->email, + 'dni' => $request->dni, + ]); + + // 🔹 Solo si envían nueva contraseña + if ($request->filled('password')) { + $postulante->password = $request->password; + $postulante->save(); + } + + return response()->json([ + 'success' => true, + 'message' => 'Postulante actualizado correctamente', + 'data' => $postulante + ]); + } + + +} diff --git a/back/app/Http/Controllers/Administracion/ProcesoAdmisionDetalleController.php b/back/app/Http/Controllers/Administracion/ProcesoAdmisionDetalleController.php index 1b24b87..1ab61fb 100644 --- a/back/app/Http/Controllers/Administracion/ProcesoAdmisionDetalleController.php +++ b/back/app/Http/Controllers/Administracion/ProcesoAdmisionDetalleController.php @@ -30,6 +30,22 @@ class ProcesoAdmisionDetalleController extends Controller { ProcesoAdmision::findOrFail($procesoId); + // ✅ Convertir JSON string -> array antes de validar (cuando llega desde FormData) + if ($request->has('listas') && is_string($request->input('listas'))) { + $decoded = json_decode($request->input('listas'), true); + if (json_last_error() === JSON_ERROR_NONE) { + $request->merge(['listas' => $decoded]); + } + } + + if ($request->has('meta') && is_string($request->input('meta'))) { + $decoded = json_decode($request->input('meta'), true); + if (json_last_error() === JSON_ERROR_NONE) { + $request->merge(['meta' => $decoded]); + } + } + + $data = $request->validate([ 'tipo' => ['required', Rule::in(['requisitos','pagos','vacantes','cronograma'])], 'titulo_detalle' => ['required','string','max:255'], @@ -69,6 +85,22 @@ class ProcesoAdmisionDetalleController extends Controller { $detalle = ProcesoAdmisionDetalle::findOrFail($id); + // ✅ Convertir JSON string -> array antes de validar (cuando llega desde FormData) + if ($request->has('listas') && is_string($request->input('listas'))) { + $decoded = json_decode($request->input('listas'), true); + if (json_last_error() === JSON_ERROR_NONE) { + $request->merge(['listas' => $decoded]); + } + } + + if ($request->has('meta') && is_string($request->input('meta'))) { + $decoded = json_decode($request->input('meta'), true); + if (json_last_error() === JSON_ERROR_NONE) { + $request->merge(['meta' => $decoded]); + } + } + + $data = $request->validate([ 'tipo' => ['sometimes', Rule::in(['requisitos','pagos','vacantes','cronograma'])], 'titulo_detalle' => ['sometimes','string','max:255'], diff --git a/back/app/Http/Controllers/Administracion/ReglaAreaProcesoController.php b/back/app/Http/Controllers/Administracion/ReglaAreaProcesoController.php index 691ffdd..8f13db8 100644 --- a/back/app/Http/Controllers/Administracion/ReglaAreaProcesoController.php +++ b/back/app/Http/Controllers/Administracion/ReglaAreaProcesoController.php @@ -11,7 +11,7 @@ use Illuminate\Support\Facades\DB; class ReglaAreaProcesoController extends Controller { -ion areasProcesos() + public function areasProcesos() { $areasProcesos = DB::table('area_proceso as ap') ->leftJoin('reglas_area_proceso as r', 'ap.id', '=', 'r.area_proceso_id') diff --git a/back/app/Http/Controllers/Controller.php b/back/app/Http/Controllers/Controller.php index 8677cd5..38bdf98 100644 --- a/back/app/Http/Controllers/Controller.php +++ b/back/app/Http/Controllers/Controller.php @@ -4,5 +4,5 @@ namespace App\Http\Controllers; abstract class Controller { - // + } diff --git a/back/app/Http/Controllers/ExamenController.php b/back/app/Http/Controllers/ExamenController.php index 72abdbb..ec76c9b 100644 --- a/back/app/Http/Controllers/ExamenController.php +++ b/back/app/Http/Controllers/ExamenController.php @@ -21,6 +21,7 @@ use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Http; use App\Services\ExamenService; +use Illuminate\Support\Facades\DB; class ExamenController extends Controller { @@ -431,7 +432,7 @@ public function iniciarExamen(Request $request) public function responderPregunta($preguntaAsignadaId, Request $request) { $request->validate([ - 'respuesta' => 'required|string' + 'respuesta' => 'nullable|string' ]); $preguntaAsignada = PreguntaAsignada::with(['examen', 'pregunta']) @@ -454,19 +455,34 @@ public function iniciarExamen(Request $request) } + public function finalizarExamen($examenId) { $examen = Examen::findOrFail($examenId); $postulante = request()->user(); - if ($examen->postulante_id !== $postulante->id) { + + // Validar que el examen le pertenezca al postulante autenticado + if ((int) $examen->postulante_id !== (int) $postulante->id) { return response()->json([ 'success' => false, 'message' => 'No autorizado' ], 403); } - $this->examenService->finalizarExamen($examen); + // (Opcional) Evitar finalizar 2 veces + if ($examen->estado === 'finalizado') { + return response()->json([ + 'success' => false, + 'message' => 'El examen ya está finalizado' + ], 409); + } + + // Finalizar examen + $examen->update([ + 'estado' => 'finalizado', + 'hora_fin' => now(), + ]); return response()->json([ 'success' => true, @@ -475,4 +491,256 @@ public function iniciarExamen(Request $request) } + + public function calificarExamen($examenId, Request $request) + { + $postulante = $request->user(); + if (!$postulante) { + return response()->json(['success' => false, 'mensaje' => 'No autenticado.'], 401); + } + + return DB::transaction(function () use ($examenId, $postulante) { + + // 1) Validar examen del postulante + $examen = DB::table('examenes') + ->where('id', $examenId) + ->where('postulante_id', $postulante->id) + ->first(); + + if (!$examen) { + return response()->json([ + 'success' => false, + 'mensaje' => 'No se encontró un examen para este postulante.' + ], 404); + } + + // 2) Si ya está calificado, devolver existente (y opcionalmente recalcular orden) + $existente = DB::table('resultados_examenes') + ->where('examen_id', $examen->id) + ->first(); + + if ($existente) { + // Obtener proceso_id para poder recalcular si deseas (opcional) + $procesoId = DB::table('area_proceso') + ->where('id', $examen->area_proceso_id) + ->value('proceso_id'); + + + if ($procesoId) $this->recalcularOrdenMerito($procesoId); + + return response()->json([ + 'success' => true, + 'mensaje' => 'Examen ya calificado.', + 'total_puntos' => (float)$existente->total_puntos, + 'total_correctas' => (int)$existente->total_correctas, + 'total_incorrectas' => (int)$existente->total_incorrectas, + 'total_nulas' => (int)$existente->total_nulas, + 'porcentaje_correctas' => (float)$existente->porcentaje_correctas, + 'calificacion_sobre_20' => (float)$existente->calificacion_sobre_20, + 'orden_merito' => $existente->orden_merito, + 'correctas_por_curso' => json_decode($existente->correctas_por_curso, true), + 'incorrectas_por_curso' => json_decode($existente->incorrectas_por_curso ?? '[]', true), + 'preguntas_totales_por_curso' => json_decode($existente->preguntas_totales_por_curso ?? '[]', true), + ]); + } + + // 3) Obtener configuración de calificación desde proceso -> calificaciones + $cfg = DB::table('area_proceso as ap') + ->join('procesos as pr', 'pr.id', '=', 'ap.proceso_id') + ->join('calificaciones as ca', 'ca.id', '=', 'pr.calificacion_id') + ->where('ap.id', $examen->area_proceso_id) + ->select( + 'pr.id as proceso_id', + 'ca.puntos_correcta', + 'ca.puntos_incorrecta', + 'ca.puntos_nula', + 'ca.puntaje_maximo' + ) + ->first(); + + if (!$cfg) { + return response()->json([ + 'success' => false, + 'mensaje' => 'No se ha definido un tipo de calificación para este proceso.' + ], 400); + } + + $puntosCorrecta = (float) $cfg->puntos_correcta; + $puntosIncorrecta = (float) $cfg->puntos_incorrecta; + $puntosNula = (float) $cfg->puntos_nula; + $puntajeMaximo = (float) $cfg->puntaje_maximo; + + // 4) Traer preguntas asignadas con su pregunta y curso + $items = DB::table('preguntas_asignadas as pa') + ->join('preguntas as p', 'p.id', '=', 'pa.pregunta_id') + ->join('cursos as c', 'c.id', '=', 'p.curso_id') + ->where('pa.examen_id', $examen->id) + ->select( + 'pa.id as pa_id', + 'pa.estado as pa_estado', + 'pa.respuesta_usuario', + 'p.respuesta_correcta', + 'c.nombre as curso_nombre' + ) + ->get(); + + if ($items->isEmpty()) { + return response()->json([ + 'success' => false, + 'mensaje' => 'El examen no tiene preguntas asignadas.' + ], 422); + } + + // 5) Calificar y actualizar cada pregunta_asignada + $totalPuntos = 0.0; + $totalCorrectas = 0; + $totalIncorrectas = 0; + $totalNulas = 0; + + $correctasPorCurso = []; + $incorrectasPorCurso = []; + $preguntasTotalesPorCurso = []; + + foreach ($items as $row) { + $curso = $row->curso_nombre; + + $preguntasTotalesPorCurso[$curso] = ($preguntasTotalesPorCurso[$curso] ?? 0) + 1; + $correctasPorCurso[$curso] = $correctasPorCurso[$curso] ?? 0; + $incorrectasPorCurso[$curso] = $incorrectasPorCurso[$curso] ?? 0; + + $nuevoEsCorrecta = 2; // 2 = blanco + $nuevoPuntaje = $puntosNula; // nula/blanco + + if ($row->pa_estado === 'anulada') { + // anulada => nula + $nuevoEsCorrecta = 2; + $nuevoPuntaje = $puntosNula; + $totalNulas++; + } else { + $ru = trim((string) $row->respuesta_usuario); + $rc = trim((string) $row->respuesta_correcta); + + if ($ru === '') { + // blanco + $nuevoEsCorrecta = 2; + $nuevoPuntaje = $puntosNula; + $totalNulas++; + } else { + $ruN = mb_strtoupper($ru); + $rcN = mb_strtoupper($rc); + + if ($rcN !== '' && $ruN === $rcN) { + $nuevoEsCorrecta = 1; + $nuevoPuntaje = $puntosCorrecta; + $totalCorrectas++; + $correctasPorCurso[$curso]++; + } else { + $nuevoEsCorrecta = 0; + $nuevoPuntaje = $puntosIncorrecta; + $totalIncorrectas++; + $incorrectasPorCurso[$curso]++; + } + } + } + + $totalPuntos += (float) $nuevoPuntaje; + + DB::table('preguntas_asignadas') + ->where('id', $row->pa_id) + ->update([ + 'es_correcta' => $nuevoEsCorrecta, + 'puntaje' => $nuevoPuntaje, + 'updated_at' => now(), + ]); + } + + // 6) Resumen + $totalPreguntas = (int) $items->count(); + $porcentajeCorrectas = $totalPreguntas > 0 ? ($totalCorrectas / $totalPreguntas) * 100 : 0; + + $calificacionSobre20 = ($puntajeMaximo > 0) + ? ($totalPuntos / $puntajeMaximo) * 20 + : 0; + + $correctasPorCursoFormato = []; + foreach ($correctasPorCurso as $curso => $corr) { + $y = $preguntasTotalesPorCurso[$curso] ?? 0; + $correctasPorCursoFormato[$curso] = "{$corr} de {$y}"; + } + + // 7) Guardar resultado en resultados_examenes + $resultadoId = DB::table('resultados_examenes')->insertGetId([ + 'postulante_id' => $postulante->id, + 'examen_id' => $examen->id, + 'total_puntos' => round($totalPuntos, 3), + 'correctas_por_curso' => json_encode($correctasPorCursoFormato), + 'incorrectas_por_curso' => json_encode($incorrectasPorCurso), + 'preguntas_totales_por_curso' => json_encode($preguntasTotalesPorCurso), + 'total_correctas' => $totalCorrectas, + 'total_incorrectas' => $totalIncorrectas, + 'total_nulas' => $totalNulas, + 'porcentaje_correctas' => round($porcentajeCorrectas, 2), + 'calificacion_sobre_20' => round($calificacionSobre20, 2), + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // 8) Recalcular orden de mérito por proceso + $this->recalcularOrdenMerito($cfg->proceso_id); + + // 9) Leer orden_merito ya asignado (opcional) + $orden = DB::table('resultados_examenes')->where('id', $resultadoId)->value('orden_merito'); + + DB::table('examenes')->where('id', $examen->id)->update([ + 'estado' => 'calificado', + 'hora_fin' => now(), + ]); + return response()->json([ + 'success' => true, + 'mensaje' => 'Examen calificado exitosamente.', + 'examen_id' => $examen->id, + 'proceso_id' => $cfg->proceso_id, + + 'total_puntos' => round($totalPuntos, 2), + 'total_correctas' => $totalCorrectas, + 'total_incorrectas' => $totalIncorrectas, + 'total_nulas' => $totalNulas, + + 'porcentaje_correctas' => round($porcentajeCorrectas, 2), + 'calificacion_sobre_20' => round($calificacionSobre20, 2), + 'orden_merito' => $orden, + + 'correctas_por_curso' => $correctasPorCursoFormato, + 'incorrectas_por_curso' => $incorrectasPorCurso, + 'preguntas_totales_por_curso' => $preguntasTotalesPorCurso, + ]); + }); + } + + + public function recalcularOrdenMerito($procesoId): void + { + DB::statement(" + UPDATE resultados_examenes r + JOIN ( + SELECT + r2.id, + ROW_NUMBER() OVER ( + ORDER BY + r2.total_puntos DESC, + COALESCE(r2.updated_at, r2.created_at) ASC + ) AS nuevo_orden + FROM resultados_examenes r2 + JOIN examenes e ON e.id = r2.examen_id + JOIN area_proceso ap ON ap.id = e.area_proceso_id + WHERE ap.proceso_id = ? + ) x ON x.id = r.id + SET r.orden_merito = x.nuevo_orden + ", [$procesoId]); + } + + + + + } \ No newline at end of file diff --git a/back/app/Http/Controllers/PostulanteAuthController.php b/back/app/Http/Controllers/PostulanteAuthController.php index 1c782e1..681ff06 100644 --- a/back/app/Http/Controllers/PostulanteAuthController.php +++ b/back/app/Http/Controllers/PostulanteAuthController.php @@ -161,28 +161,28 @@ class PostulanteAuthController extends Controller $pagos = []; - // =============================== - // 1️⃣ PAGOS PYTO PERÚ - // =============================== - $urlPyto = "https://service2.unap.edu.pe/PAYMENTS_MNG/v1/{$dni}/8/"; - $responsePyto = Http::get($urlPyto); - - if ($responsePyto->successful()) { - $dataPyto = $responsePyto->json(); - - if (!empty($dataPyto['data'])) { - foreach ($dataPyto['data'] as $pago) { - $pagos[] = [ - 'tipo' => 'pyto_peru', - 'codigo' => $pago['autorizationCode'] ?? null, - 'monto' => $pago['total'] ?? null, - 'fecha_pago' => $pago['confirmedDate'] ?? null, - 'estado' => true, - 'raw' => $pago // devuelve toda la info original - ]; - } - } - } + // // =============================== + // // 1️⃣ PAGOS PYTO PERÚ + // // =============================== + // $urlPyto = "https://service2.unap.edu.pe/PAYMENTS_MNG/v1/{$dni}/8/"; + // $responsePyto = Http::get($urlPyto); + + // if ($responsePyto->successful()) { + // $dataPyto = $responsePyto->json(); + + // if (!empty($dataPyto['data'])) { + // foreach ($dataPyto['data'] as $pago) { + // $pagos[] = [ + // 'tipo' => 'pyto_peru', + // 'codigo' => $pago['autorizationCode'] ?? null, + // 'monto' => $pago['total'] ?? null, + // 'fecha_pago' => $pago['confirmedDate'] ?? null, + // 'estado' => true, + // 'raw' => $pago // devuelve toda la info original + // ]; + // } + // } + // } // =============================== // 2️⃣ PAGOS CAJA diff --git a/back/app/Http/Controllers/WebController.php b/back/app/Http/Controllers/WebController.php new file mode 100644 index 0000000..4a793b7 --- /dev/null +++ b/back/app/Http/Controllers/WebController.php @@ -0,0 +1,70 @@ +with([ + 'detalles' => function ($query) { + $query->select( + 'id', + 'proceso_admision_id', + 'tipo', + 'titulo_detalle', + 'descripcion', + 'listas', + 'meta', + 'url', + 'imagen_path', + 'imagen_path_2', + 'created_at', + 'updated_at' + ); + } + ]) + ->latest() // 🔥 Esto ordena por created_at DESC + ->get(); + + return response()->json([ + 'success' => true, + 'data' => $procesos + ]); +} + + +} diff --git a/back/app/Models/Examen.php b/back/app/Models/Examen.php index 1a18ae5..9aaf09e 100644 --- a/back/app/Models/Examen.php +++ b/back/app/Models/Examen.php @@ -11,15 +11,22 @@ class Examen extends Model protected $table = 'examenes'; - protected $fillable = [ + protected $fillable = [ 'postulante_id', 'area_proceso_id', - 'pagado', - 'tipo_pago', - 'pago_id', + 'pagado', + 'tipo_pago', + 'pago_id', 'intentos', 'hora_inicio', - + 'estado', + 'hora_fin', + ]; + + protected $casts = [ + 'pagado' => 'boolean', + 'hora_inicio' => 'datetime', + 'hora_fin' => 'datetime', ]; public function postulante() diff --git a/back/app/Models/Noticia.php b/back/app/Models/Noticia.php new file mode 100644 index 0000000..1a61219 --- /dev/null +++ b/back/app/Models/Noticia.php @@ -0,0 +1,66 @@ + 'datetime', + 'publicado' => 'boolean', + 'destacado' => 'boolean', + 'orden' => 'integer', + ]; + + // ✅ se incluirá en el JSON + protected $appends = ['imagen_url']; + + public function getImagenUrlAttribute(): ?string + { + // 1) Si en BD hay una URL externa, úsala + if (!empty($this->attributes['imagen_url'])) { + return $this->attributes['imagen_url']; + } + + // 2) Si hay imagen en storage, genera URL absoluta + if (!empty($this->imagen_path)) { + return url(Storage::disk('public')->url($this->imagen_path)); + } + + return null; + } + + protected static function booted(): void + { + static::saving(function (Noticia $noticia) { + if (!$noticia->slug) { + $noticia->slug = Str::slug($noticia->titulo); + } + }); + } +} diff --git a/back/app/Models/PreguntaAsignada.php b/back/app/Models/PreguntaAsignada.php index 1124262..8862954 100644 --- a/back/app/Models/PreguntaAsignada.php +++ b/back/app/Models/PreguntaAsignada.php @@ -21,7 +21,7 @@ class PreguntaAsignada extends Model ]; protected $casts = [ - 'es_correcta' => 'boolean', + 'es_correcta' => 'integer', 'puntaje' => 'decimal:2' ]; diff --git a/back/app/Models/ProcesoAdmision.php b/back/app/Models/ProcesoAdmision.php index 4c4af06..106c862 100644 --- a/back/app/Models/ProcesoAdmision.php +++ b/back/app/Models/ProcesoAdmision.php @@ -65,5 +65,7 @@ class ProcesoAdmision extends Model { return $this->hasMany(ResultadoAdmision::class, 'idproceso'); } + + } diff --git a/back/app/Models/ResultadoExamen.php b/back/app/Models/ResultadoExamen.php new file mode 100644 index 0000000..d8c7ae9 --- /dev/null +++ b/back/app/Models/ResultadoExamen.php @@ -0,0 +1,37 @@ +belongsTo(Postulante::class); + } + + public function examen() + { + return $this->belongsTo(Examen::class); + } +} diff --git a/back/app/Services/ExamenService.php b/back/app/Services/ExamenService.php index daddafd..0c99677 100644 --- a/back/app/Services/ExamenService.php +++ b/back/app/Services/ExamenService.php @@ -115,39 +115,49 @@ public function obtenerPreguntasExamen(Examen $examen): array - public function guardarRespuesta(PreguntaAsignada $pa, string $respuesta): array - { - if ($pa->estado === 'respondida') { - return ['success' => false, 'message' => 'Ya respondida']; - } +public function guardarRespuesta(PreguntaAsignada $pa, ?string $respuesta): array +{ + if ($pa->estado === 'respondida') { + return ['success' => false, 'message' => 'Ya respondida']; + } - $esCorrecta = $respuesta === $pa->pregunta->respuesta_correcta; + // 🔹 Si está en blanco + if (empty($respuesta)) { $pa->update([ - 'respuesta_usuario' => $respuesta, - 'es_correcta' => $esCorrecta, - 'puntaje_obtenido' => $esCorrecta ? $pa->puntaje_base : 0, + 'respuesta_usuario' => null, + 'es_correcta' => 2, // 2 = blanco + 'puntaje_obtenido' => 0, 'estado' => 'respondida', 'respondida_at' => now() ]); return [ 'success' => true, - 'correcta' => $esCorrecta, - 'puntaje' => $pa->puntaje_obtenido + 'correcta' => 2, + 'puntaje' => 0 ]; } - /** - * Finalizar examen - */ - public function finalizarExamen(Examen $examen): void - { - $examen->update([ - 'estado' => 'finalizado', - 'hora_fin' => now() - ]); - } + // 🔹 Si respondió algo + $esCorrecta = $respuesta === $pa->pregunta->respuesta_correcta; + + $pa->update([ + 'respuesta_usuario' => $respuesta, + 'es_correcta' => $esCorrecta ? 1 : 0, + 'puntaje_obtenido' => $esCorrecta ? $pa->puntaje_base : 0, + 'estado' => 'respondida', + 'respondida_at' => now() + ]); + + return [ + 'success' => true, + 'correcta' => $esCorrecta ? 1 : 0, + 'puntaje' => $pa->puntaje_obtenido + ]; +} + + private function mezclarOpciones(?array $opciones): array { diff --git a/back/routes/api.php b/back/routes/api.php index 2b75a57..66e4db5 100644 --- a/back/routes/api.php +++ b/back/routes/api.php @@ -6,7 +6,6 @@ use App\Http\Controllers\AuthController; use App\Http\Controllers\AcademiaController; use App\Http\Controllers\VinculacionController; use App\Http\Controllers\Administracion\administradorController; -use App\Http\Controllers\Administracion\ExamenesController; use App\Http\Controllers\Administracion\AreaController; use App\Http\Controllers\Administracion\CursoController; use App\Http\Controllers\Administracion\PreguntaController; @@ -16,7 +15,10 @@ use App\Http\Controllers\ExamenController; use App\Http\Controllers\Administracion\ReglaAreaProcesoController; use App\Http\Controllers\Administracion\ProcesoAdmisionController; use App\Http\Controllers\Administracion\ProcesoAdmisionDetalleController; -use App\Models\ProcesoAdmisionDetalle; +use App\Http\Controllers\Administracion\PostulanteController; +use App\Http\Controllers\Administracion\CalificacionController; +use App\Http\Controllers\Administracion\NoticiaController; +use App\Http\Controllers\WebController; Route::get('/user', function (Request $request) { return $request->user(); @@ -63,6 +65,22 @@ Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { }); +Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { + + // NOTICIAS + Route::get('/noticias', [NoticiaController::class, 'index']); + Route::get('/noticias/{noticia}', [NoticiaController::class, 'show']); + + Route::post('/noticias', [NoticiaController::class, 'store']); + + // usa SOLO UNA (PUT o PATCH). Aquí dejo PUT: + Route::put('/noticias/{noticia}', [NoticiaController::class, 'update']); + + Route::delete('/noticias/{noticia}', [NoticiaController::class, 'destroy']); +}); +Route::get('/noticias', [NoticiaController::class, 'index']); +Route::get('/noticias/{noticia:slug}', [NoticiaController::class, 'showPublic']); + Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { Route::get('/cursos', [CursoController::class, 'index']); @@ -74,6 +92,14 @@ Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { }); +Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { + + Route::get('/postulantes', [PostulanteController::class, 'obtenerPostulantes']); + Route::put('/postulantes/{id}', [PostulanteController::class, 'actualizarPostulante']); +}); + + + Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { @@ -85,6 +111,17 @@ Route::middleware(['auth:sanctum'])->prefix('admin')->group(function () { }); +Route::middleware('auth:sanctum')->group(function () { + + + Route::get('/calificaciones', [CalificacionController::class, 'index']); + Route::post('/calificaciones', [CalificacionController::class, 'store']); + Route::get('/calificaciones/{id}', [CalificacionController::class, 'show']); + Route::put('/calificaciones/{id}', [CalificacionController::class, 'update']); + Route::delete('/calificaciones/{id}', [CalificacionController::class, 'destroy']); + +}); + Route::prefix('postulante')->group(function () { @@ -157,6 +194,7 @@ Route::middleware(['auth:postulante'])->group(function () { // Finalizar examen Route::post('/examen/{examen}/finalizar', [ExamenController::class, 'finalizarExamen']); + Route::post('/examen/{examenId}/calificar', [ExamenController::class, 'calificarExamen']); }); @@ -185,4 +223,8 @@ Route::middleware('auth:sanctum')->prefix('admin')->group(function () { Route::match(['put','patch'], '/{id}', [ProcesoAdmisionDetalleController::class, 'update'])->name('update'); Route::delete('/{id}', [ProcesoAdmisionDetalleController::class, 'destroy'])->name('destroy'); }); - }); \ No newline at end of file + }); + + + +Route::get('/procesos-admision', [WebController::class, 'GetProcesoAdmision']); diff --git a/front/index.html b/front/index.html index be16426..b8048bb 100644 --- a/front/index.html +++ b/front/index.html @@ -2,10 +2,9 @@ - + - - Direccion de Admision - UNA Puno + Admisión
diff --git a/front/public/1.jpg b/front/public/1.jpg new file mode 100644 index 0000000..8be3ebf Binary files /dev/null and b/front/public/1.jpg differ diff --git a/front/public/2.jpg b/front/public/2.jpg new file mode 100644 index 0000000..e5ca83b Binary files /dev/null and b/front/public/2.jpg differ diff --git a/front/public/PORTADA.jpg.jpeg b/front/public/PORTADA.jpg.jpeg new file mode 100644 index 0000000..3564490 Binary files /dev/null and b/front/public/PORTADA.jpg.jpeg differ diff --git a/front/public/PORTADAXD.jpg.jpeg b/front/public/PORTADAXD.jpg.jpeg new file mode 100644 index 0000000..a2ff101 Binary files /dev/null and b/front/public/PORTADAXD.jpg.jpeg differ diff --git a/front/public/favicon.ico b/front/public/favicon.ico new file mode 100644 index 0000000..756b3ed Binary files /dev/null and b/front/public/favicon.ico differ diff --git a/front/src/axios.js b/front/src/axios.js index 5e726ea..9e54b0e 100644 --- a/front/src/axios.js +++ b/front/src/axios.js @@ -9,7 +9,7 @@ const api = axios.create({ } }); -// Request interceptor + api.interceptors.request.use( (config) => { const token = localStorage.getItem('token') @@ -25,7 +25,7 @@ api.interceptors.request.use( } ) -// Response interceptor + api.interceptors.response.use( (response) => { return response @@ -33,21 +33,19 @@ api.interceptors.response.use( async (error) => { const originalRequest = error.config - // Si el error es 401 y no es un intento de re-autenticación + if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true - // Limpiar autenticación + localStorage.removeItem('token') localStorage.removeItem('user') - - // Redirigir a login - router.push('/login') + + router.push('account/auth/login') return Promise.reject(error) } - - // Manejar otros errores + if (error.response?.status === 403) { router.push('/unauthorized') } diff --git a/front/src/components/Footer.vue b/front/src/components/Footer.vue index 2f4d7b0..adb1935 100644 --- a/front/src/components/Footer.vue +++ b/front/src/components/Footer.vue @@ -13,7 +13,7 @@ @@ -40,8 +40,8 @@

Contacto

diff --git a/front/src/components/WebPage.vue b/front/src/components/WebPage.vue index ffffa38..39bbdf7 100644 --- a/front/src/components/WebPage.vue +++ b/front/src/components/WebPage.vue @@ -9,66 +9,19 @@ - + - + - + - - - - - - - -
- -
- -
-

{{ detalleModal.descripcion }}

-
- -
-
    -
  • {{ item }}
  • -
-
-
- -
+ -
- -
-
@@ -89,184 +42,9 @@ import StatsSection from './WebPageSections/StatsSection.vue' import NoticiasSection from './WebPageSections/NoticiasSection.vue' import ModalidadesSection from './WebPageSections/ModalidadesSection.vue' import ContactSection from './WebPageSections/ContactSection.vue' -import PreinscripcionModal from './WebPageSections//modal/PreinscripcionModal.vue' - -import { - MedicineBoxOutlined, - BuildOutlined, - CodeOutlined, - BookOutlined, - TrophyOutlined, - BankOutlined, - ExperimentOutlined, - UserOutlined, -} from "@ant-design/icons-vue" - -const procesoStore = useProcesoAdmisionStore() - -const procesosPublicados = computed(() => procesoStore.procesosPublicados) -const procesoPrincipal = computed(() => procesoStore.procesosPublicados[0] || null) - -onMounted(() => { - procesoStore.fetchProcesosPublicados() -}) -const preinscripcionModalVisible = ref(false) -const detalleModalVisible = ref(false) -const detalleModal = ref({ - titulo: '', - descripcion: '', - imagen_url: null, - imagen_url_2: null, - listas: [], -}) -const facultades = [ - { - id: "1", - nombre: "Ciencias de la Salud", - carreras: [ - { - id: 1, - nombre: "Medicina Humana", - grado: "Bachiller", - descripcion: "Formación médica integral con prácticas desde primer año", - vacantes: 50, - puntaje: "1800+", - icono: markRaw(MedicineBoxOutlined), - }, - { - id: 2, - nombre: "Enfermería", - grado: "Bachiller", - descripcion: "Cuidado integral de la salud", - vacantes: 60, - puntaje: "1500+", - icono: markRaw(UserOutlined), - }, - ], - }, - { - id: "2", - nombre: "Ingenierías", - carreras: [ - { - id: 3, - nombre: "Ingeniería Civil", - grado: "Bachiller", - descripcion: "Diseño y construcción de infraestructura", - vacantes: 80, - puntaje: "1700+", - icono: markRaw(BuildOutlined), - }, - { - id: 4, - nombre: "Ingeniería de Sistemas", - grado: "Bachiller", - descripcion: "Desarrollo de software e inteligencia artificial", - vacantes: 100, - puntaje: "1600+", - icono: markRaw(CodeOutlined), - }, - ], - }, - { - id: "3", - nombre: "Derecho y Humanidades", - carreras: [ - { - id: 5, - nombre: "Derecho", - grado: "Bachiller", - descripcion: "Formación jurídica integral", - vacantes: 120, - puntaje: "1550+", - icono: markRaw(BookOutlined), - }, - { - id: 6, - nombre: "Psicología", - grado: "Bachiller", - descripcion: "Ciencias del comportamiento humano", - vacantes: 70, - puntaje: "1450+", - icono: markRaw(UserOutlined), - }, - ], - }, -] -const modalidades = [ - { - id: 1, - nombre: "Admisión Ordinaria", - descripcion: "Examen de conocimientos generales", - estado: "Abierto", - estadoColor: "success", - color: "#1890ff", - icono: markRaw(BookOutlined), - }, - { - id: 2, - nombre: "Evaluación de Talentos", - descripcion: "Para deportistas y artistas destacados", - estado: "Próximamente", - estadoColor: "orange", - color: "#faad14", - icono: markRaw(TrophyOutlined), - }, - { - id: 3, - nombre: "Traslado Externo", - descripcion: "Estudiantes de otras universidades", - estado: "Cerrado", - estadoColor: "red", - color: "#ff4d4f", - icono: markRaw(BankOutlined), - }, - { - id: 4, - nombre: "Segunda Carrera", - descripcion: "Para profesionales graduados", - estado: "Abierto", - estadoColor: "success", - color: "#52c41a", - icono: markRaw(ExperimentOutlined), - }, -] - -const noticias = [ - { - id: 1, - titulo: "Nuevo Laboratorio de Investigación", - descripcion: "Inauguramos el moderno laboratorio de ciencias con tecnología de punta.", - fecha: "15 Nov 2023", - categoria: "Infraestructura", - tagColor: "blue", - imagen: - "https://images.unsplash.com/photo-1532094349884-543bc11b234d?auto=format&fit=crop&w=600&q=80", - }, - { - id: 2, - titulo: "Convenio Internacional", - descripcion: "Firmamos acuerdo con universidad europea para intercambio estudiantil.", - fecha: "10 Nov 2023", - categoria: "Internacional", - tagColor: "green", - imagen: - "https://images.unsplash.com/photo-1523050854058-8df90110c9f1?auto=format&fit=crop&w=600&q=80", - }, - { - id: 3, - titulo: "Resultados Publicados", - descripcion: "Consulta los resultados del examen de admisión extraordinario.", - fecha: "5 Nov 2023", - categoria: "Resultados", - tagColor: "red", - imagen: - "https://images.unsplash.com/photo-1562774053-701939374585?auto=format&fit=crop&w=600&q=80", - }, -] const scrollToConvocatoria = () => { const el = document.getElementById("convocatorias") @@ -277,48 +55,6 @@ const openVirtualTour = () => { window.open("https://example.com", "_blank", "noopener,noreferrer") } -const openPreinscripcion = () => { - preinscripcionModalVisible.value = true -} - -const tipoLabels = { - requisitos: 'Requisitos', - pagos: 'Pagos', - vacantes: 'Vacantes', - cronograma: 'Cronograma', -} - -const showModal = ({ procesoId, tipo }) => { - const proceso = procesosPublicados.value.find(p => p.id === procesoId) - if (!proceso) return - - const detalle = proceso.detalles?.find(d => d.tipo === tipo) - - if (detalle) { - detalleModal.value = { - titulo: detalle.titulo_detalle || tipoLabels[tipo] || tipo, - descripcion: detalle.descripcion || '', - imagen_url: detalle.imagen_url || null, - imagen_url_2: detalle.imagen_url_2 || null, - listas: detalle.listas || [], - } - } else { - detalleModal.value = { - titulo: `${tipoLabels[tipo] || tipo} - ${proceso.titulo}`, - descripcion: '', - imagen_url: null, - imagen_url_2: null, - listas: [], - } - } - - detalleModalVisible.value = true -} - -const submitPreinscripcion = () => { - message.success("Preinscripción iniciada exitosamente") - preinscripcionModalVisible.value = false -} diff --git a/front/src/components/WebPageSections/ConvocatoriasSection.vue b/front/src/components/WebPageSections/ConvocatoriasSection.vue index f94240e..efb442a 100644 --- a/front/src/components/WebPageSections/ConvocatoriasSection.vue +++ b/front/src/components/WebPageSections/ConvocatoriasSection.vue @@ -1,4 +1,4 @@ - + \ No newline at end of file + diff --git a/front/src/components/WebPageSections/NoticiasSection.vue b/front/src/components/WebPageSections/NoticiasSection.vue index a540a9d..ce76beb 100644 --- a/front/src/components/WebPageSections/NoticiasSection.vue +++ b/front/src/components/WebPageSections/NoticiasSection.vue @@ -1,7 +1,8 @@ + - \ No newline at end of file + diff --git a/front/src/components/WebPageSections/ProcessSection.vue b/front/src/components/WebPageSections/ProcessSection.vue index dbfd131..dc3d20e 100644 --- a/front/src/components/WebPageSections/ProcessSection.vue +++ b/front/src/components/WebPageSections/ProcessSection.vue @@ -3,30 +3,111 @@
-

{{ proceso.titulo || 'Proceso de Admisión 2026' }}

+

{{ tituloProceso }}

- {{ proceso.subtitulo || 'Sigue estos pasos para postular' }} + ¿No sabes por dónde empezar? Aquí te guiamos paso a paso y te decimos qué debes hacer hoy.

+ - - - -
+ :items="stepsItems" + /> + + +
+
Guía rápida (para no perderte)
+ +
+ +
+
1) ¿Qué etapa está activa hoy?
+ +
+ + Preinscripción activa (virtual / en línea) + + + + Inscripción activa (presencial en Campus Universitario) + + + + Hoy es el Examen + + + + Hoy salen Resultados + + + + Biométrico activo (solo ingresantes) + + + + Aún no inicia o ya terminó una etapa. Revisa las fechas del proceso. + +
+ +
+ Tip: Si ves “🟢” en una fecha, significa que esa etapa está activa hoy. +
+
+ + +
+
2) ¿Qué debo hacer ahora?
+ +
    +
  • {{ t }}
  • +
+ +
+ + + Iniciar Preinscripción + + + + Ver Reglamento + + + + Ver Resultados + +
+
+
+ + +
+ Importante: La Inscripción se realiza de forma presencial en el + Campus Universitario. Lleva tu DNI y los requisitos solicitados. +
+
+ +
- Fechas referenciales. Verifica el cronograma oficial de la Dirección de Admisión + Cargando proceso...
@@ -35,90 +116,243 @@ @@ -242,6 +476,105 @@ const currentStep = computed(() => { flex-shrink: 0; } +.help-box { + margin-top: 14px; + border-top: 1px dashed #e5e7eb; + padding-top: 12px; +} + +.help-title { + font-weight: 700; + color: #1e3a8a; + margin-bottom: 10px; +} + +.help-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.help-item { + border: 1px solid #eef2ff; + background: #fbfcff; + border-radius: 12px; + padding: 12px; +} + +.help-label { + font-weight: 700; + color: #2c3e50; + margin-bottom: 8px; +} + +.tiny-hint { + margin-top: 10px; + font-size: 0.86rem; + color: #6b7280; +} + +.badges { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.badge { + font-size: 0.82rem; + padding: 6px 10px; + border-radius: 999px; + font-weight: 700; +} + +.badge-blue { + background: rgba(30, 58, 138, 0.08); + color: #1e3a8a; + border: 1px solid rgba(30, 58, 138, 0.18); +} + +.badge-green { + background: rgba(16, 185, 129, 0.08); + color: #047857; + border: 1px solid rgba(16, 185, 129, 0.18); +} + +.badge-orange { + background: rgba(245, 158, 11, 0.10); + color: #92400e; + border: 1px solid rgba(245, 158, 11, 0.22); +} + +.badge-gray { + background: rgba(107, 114, 128, 0.08); + color: #374151; + border: 1px solid rgba(107, 114, 128, 0.18); +} + +.help-list { + margin: 0; + padding-left: 18px; + color: #4b5563; + line-height: 1.55; +} + +.help-actions { + margin-top: 10px; + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +/* Nota campus */ +.campus-note { + margin-top: 12px; + padding: 10px 12px; + border-radius: 12px; + background: #f8fafc; + border: 1px solid #e5e7eb; + color: #374151; + line-height: 1.5; +} + @media (max-width: 992px) { .section-title { font-size: 1.85rem; @@ -255,23 +588,22 @@ const currentStep = computed(() => { .process-section { padding: 24px 0; } - .section-title { font-size: 1.55rem; } - .process-card { padding: 12px 10px 10px; } - .modern-steps { padding: 4px 4px; } - .modern-steps :deep(.ant-steps-item-icon) { width: 28px; height: 28px; line-height: 28px; } + .help-grid { + grid-template-columns: 1fr; + } } diff --git a/front/src/components/WebPageSections/ProgramasSection.vue b/front/src/components/WebPageSections/ProgramasSection.vue index 7e8107e..061c647 100644 --- a/front/src/components/WebPageSections/ProgramasSection.vue +++ b/front/src/components/WebPageSections/ProgramasSection.vue @@ -1,4 +1,3 @@ -