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
Av. Floral N° 1153 – Puno
- 📞 (051) 123-456
- ✉️ admision@unap.edu.pe
+ 📞 (+51) 957 734 361
+ ✉️ dgadmision@unap.edu.pe
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 }}
-
-
-
-
+
-
-
@@ -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 @@
-
+
@@ -12,57 +12,88 @@
-
-
-
No hay convocatorias vigentes en este momento.
-
+
-
-
+
Principal
- {{ principal.descripcion || principal.subtitulo || 'Proceso de admisión' }}
+ {{ store.procesoPrincipal.descripcion }}
-