login2026
parent
87e72bc029
commit
489cfd8f6b
@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Administracion;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Calificacion;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
class CalificacionController extends Controller
|
||||||
|
{
|
||||||
|
// ✅ Listar todas
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$calificaciones = Calificacion::all();
|
||||||
|
|
||||||
|
return response()->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'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,465 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace App\Http\Controllers\Administracion;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Models\Academia;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\Examen;
|
|
||||||
use App\Models\Pregunta;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Validator;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
|
|
||||||
class ExamenesController extends Controller
|
|
||||||
{
|
|
||||||
|
|
||||||
public function getExamenes(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);
|
|
||||||
}
|
|
||||||
|
|
||||||
$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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Administracion;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Postulante;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
class PostulanteController extends Controller
|
||||||
|
{
|
||||||
|
public function obtenerPostulantes(Request $request)
|
||||||
|
{
|
||||||
|
$query = Postulante::query();
|
||||||
|
|
||||||
|
if ($request->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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ResultadoExamen extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'resultados_examenes';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'postulante_id',
|
||||||
|
'examen_id',
|
||||||
|
'total_puntos',
|
||||||
|
'correctas_por_curso',
|
||||||
|
'incorrectas_por_curso',
|
||||||
|
'preguntas_totales_por_curso',
|
||||||
|
'total_correctas',
|
||||||
|
'total_incorrectas',
|
||||||
|
'total_nulas',
|
||||||
|
'porcentaje_correctas',
|
||||||
|
'calificacion_sobre_20',
|
||||||
|
'orden_merito',
|
||||||
|
'probabilidad_ingreso',
|
||||||
|
'programa_recomendado',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function postulante()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Postulante::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function examen()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Examen::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,181 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<a-card :bordered="false" class="main-card">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header">
|
||||||
|
<h2>Gestión de Calificaciones</h2>
|
||||||
|
<a-button type="primary" @click="openModal()">
|
||||||
|
Nueva Calificación
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabla -->
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="calificaciones"
|
||||||
|
:loading="loading"
|
||||||
|
row-key="id"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<template #actions="{ record }">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="link" @click="openModal(record)">
|
||||||
|
Editar
|
||||||
|
</a-button>
|
||||||
|
|
||||||
|
<a-popconfirm
|
||||||
|
title="¿Seguro de eliminar?"
|
||||||
|
@confirm="eliminar(record.id)"
|
||||||
|
>
|
||||||
|
<a-button type="link" danger>
|
||||||
|
Eliminar
|
||||||
|
</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="modalVisible"
|
||||||
|
:title="form.id ? 'Editar Calificación' : 'Nueva Calificación'"
|
||||||
|
@ok="guardar"
|
||||||
|
@cancel="cerrarModal"
|
||||||
|
ok-text="Guardar"
|
||||||
|
>
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item label="Nombre">
|
||||||
|
<a-input v-model:value="form.nombre" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Puntos Correcta">
|
||||||
|
<a-input-number v-model:value="form.puntos_correcta" style="width:100%" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Puntos Incorrecta">
|
||||||
|
<a-input-number v-model:value="form.puntos_incorrecta" style="width:100%" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Puntos Nula">
|
||||||
|
<a-input-number v-model:value="form.puntos_nula" style="width:100%" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Puntaje Máximo">
|
||||||
|
<a-input-number v-model:value="form.puntaje_maximo" style="width:100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
import api from '../../../axios'
|
||||||
|
|
||||||
|
const calificaciones = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const modalVisible = ref(false)
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
id: null,
|
||||||
|
nombre: '',
|
||||||
|
puntos_correcta: 0,
|
||||||
|
puntos_incorrecta: 0,
|
||||||
|
puntos_nula: 0,
|
||||||
|
puntaje_maximo: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: 'Nombre', dataIndex: 'nombre' },
|
||||||
|
{ title: 'Correcta', dataIndex: 'puntos_correcta' },
|
||||||
|
{ title: 'Incorrecta', dataIndex: 'puntos_incorrecta' },
|
||||||
|
{ title: 'Nula', dataIndex: 'puntos_nula' },
|
||||||
|
{ title: 'Máximo', dataIndex: 'puntaje_maximo' },
|
||||||
|
{ title: 'Acciones', key: 'actions', slots: { customRender: 'actions' } }
|
||||||
|
]
|
||||||
|
|
||||||
|
const cargar = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { data } = await api .getAll()
|
||||||
|
calificaciones.value = data.data
|
||||||
|
} catch (error) {
|
||||||
|
message.error('Error al cargar')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openModal = (record = null) => {
|
||||||
|
if (record) {
|
||||||
|
form.value = { ...record }
|
||||||
|
} else {
|
||||||
|
form.value = {
|
||||||
|
id: null,
|
||||||
|
nombre: '',
|
||||||
|
puntos_correcta: 0,
|
||||||
|
puntos_incorrecta: 0,
|
||||||
|
puntos_nula: 0,
|
||||||
|
puntaje_maximo: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const cerrarModal = () => {
|
||||||
|
modalVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const guardar = async () => {
|
||||||
|
try {
|
||||||
|
if (form.value.id) {
|
||||||
|
await api .update(form.value.id, form.value)
|
||||||
|
message.success('Actualizado correctamente')
|
||||||
|
} else {
|
||||||
|
await api .create(form.value)
|
||||||
|
message.success('Creado correctamente')
|
||||||
|
}
|
||||||
|
|
||||||
|
cerrarModal()
|
||||||
|
cargar()
|
||||||
|
} catch (error) {
|
||||||
|
message.error('Error al guardar')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const eliminar = async (id) => {
|
||||||
|
try {
|
||||||
|
await api .delete(id)
|
||||||
|
message.success('Eliminado correctamente')
|
||||||
|
cargar()
|
||||||
|
} catch (error) {
|
||||||
|
message.error('Error al eliminar')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
cargar()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-wrapper {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,201 @@
|
|||||||
|
<template>
|
||||||
|
<div class="postulantes-container">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header">
|
||||||
|
<div>
|
||||||
|
<h2 class="page-title">Gestión de Postulantes</h2>
|
||||||
|
<p class="page-subtitle">Administra los usuarios registrados</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-input-search
|
||||||
|
v-model:value="search"
|
||||||
|
placeholder="Buscar por nombre, email o DNI"
|
||||||
|
style="width: 300px"
|
||||||
|
@search="fetchPostulantes"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabla -->
|
||||||
|
<a-card>
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="postulantes"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
row-key="id"
|
||||||
|
@change="handleTableChange"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
|
||||||
|
<!-- Nombre -->
|
||||||
|
<template v-if="column.key === 'name'">
|
||||||
|
<strong>{{ record.name }}</strong>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Email -->
|
||||||
|
<template v-else-if="column.key === 'email'">
|
||||||
|
{{ record.email }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- DNI -->
|
||||||
|
<template v-else-if="column.key === 'dni'">
|
||||||
|
<a-tag color="blue">{{ record.dni }}</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Última actividad -->
|
||||||
|
<template v-else-if="column.key === 'last_activity'">
|
||||||
|
{{ record.last_activity ?? 'Sin actividad' }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Acciones -->
|
||||||
|
<template v-else-if="column.key === 'acciones'">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="text" @click="editar(record)">
|
||||||
|
Editar
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- Modal Editar -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="showModal"
|
||||||
|
title="Editar Postulante"
|
||||||
|
@ok="guardarCambios"
|
||||||
|
@cancel="resetForm"
|
||||||
|
>
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item label="Nombre">
|
||||||
|
<a-input v-model:value="form.name" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Email">
|
||||||
|
<a-input v-model:value="form.email" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="DNI">
|
||||||
|
<a-input v-model:value="form.dni" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Nueva contraseña (opcional)">
|
||||||
|
<a-input-password v-model:value="form.password" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import api from '../../../axios'
|
||||||
|
|
||||||
|
const postulantes = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const search = ref('')
|
||||||
|
const showModal = ref(false)
|
||||||
|
const editingId = ref(null)
|
||||||
|
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
dni: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: 'Nombre', dataIndex: 'name', key: 'name' },
|
||||||
|
{ title: 'Email', dataIndex: 'email', key: 'email' },
|
||||||
|
{ title: 'DNI', dataIndex: 'dni', key: 'dni' },
|
||||||
|
{ title: 'Última actividad', dataIndex: 'last_activity', key: 'last_activity' },
|
||||||
|
{ title: 'Acciones', key: 'acciones', align: 'center' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const fetchPostulantes = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { data } = await api.get('/admin/postulantes', {
|
||||||
|
params: {
|
||||||
|
page: pagination.current,
|
||||||
|
buscar: search.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
postulantes.value = data.data.data
|
||||||
|
pagination.total = data.data.total
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
message.error('Error al cargar postulantes')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editar = (record) => {
|
||||||
|
editingId.value = record.id
|
||||||
|
form.name = record.name
|
||||||
|
form.email = record.email
|
||||||
|
form.dni = record.dni
|
||||||
|
form.password = ''
|
||||||
|
showModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const guardarCambios = async () => {
|
||||||
|
try {
|
||||||
|
await api.put(`/admin/postulantes/${editingId.value}`, form)
|
||||||
|
message.success('Postulante actualizado correctamente')
|
||||||
|
showModal.value = false
|
||||||
|
fetchPostulantes()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.response?.data?.message || 'Error al actualizar')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTableChange = (pag) => {
|
||||||
|
pagination.current = pag.current
|
||||||
|
fetchPostulantes()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
showModal.value = false
|
||||||
|
editingId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchPostulantes)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.postulantes-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,222 +1,389 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-card class="procesos-card" :bordered="false">
|
<a-card class="card" :bordered="true">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="card-title">
|
<div class="header">
|
||||||
<div class="title-left">
|
<div class="headerLeft">
|
||||||
<div class="title-main">Mis procesos de admisión</div>
|
<div class="title">Mis procesos de admisión</div>
|
||||||
<div class="title-sub">Resultados registrados por DNI</div>
|
<div class="subtitle">Resultados registrados por DNI</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="title-right">
|
<div class="headerRight">
|
||||||
<a-space>
|
<a-button @click="obtenerProcesos" :loading="loading" class="btn" block>
|
||||||
<a-button @click="obtenerProcesos" :loading="loading">Actualizar</a-button>
|
Actualizar
|
||||||
</a-space>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<a-spin :spinning="loading">
|
<a-spin :spinning="loading">
|
||||||
<div class="top-summary">
|
<!-- Top tools -->
|
||||||
<a-alert v-if="!loading" type="info" show-icon class="summary-alert">
|
<div class="tools">
|
||||||
Total de procesos: <strong>{{ procesos.length }}</strong>
|
<div class="counter">
|
||||||
</a-alert>
|
<span class="counterLabel">Total</span>
|
||||||
|
<span class="counterValue">{{ procesosFiltrados.length }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="search"
|
v-model:value="search"
|
||||||
allow-clear
|
allow-clear
|
||||||
placeholder="Buscar por nombre de proceso..."
|
placeholder="Buscar por nombre de proceso…"
|
||||||
class="search-input"
|
class="search"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-table
|
<!-- Desktop/tablet: tabla -->
|
||||||
class="procesos-table"
|
<div v-if="!isMobile" class="tableWrap">
|
||||||
:dataSource="procesosFiltrados"
|
<a-table
|
||||||
:columns="columns"
|
class="table"
|
||||||
rowKey="id"
|
:dataSource="procesosFiltrados"
|
||||||
:pagination="{ pageSize: 7, showSizeChanger: false }"
|
:columns="columns"
|
||||||
:scroll="{ x: 900 }"
|
rowKey="id"
|
||||||
>
|
:pagination="{ pageSize: 7, showSizeChanger: false }"
|
||||||
<template #bodyCell="{ column, record }">
|
:scroll="{ x: 900 }"
|
||||||
<!-- Nombre -->
|
>
|
||||||
<template v-if="column.key === 'nombre'">
|
<template #bodyCell="{ column, record }">
|
||||||
<div class="nombre">
|
<template v-if="column.key === 'nombre'">
|
||||||
{{ record.nombre || '-' }}
|
<div class="nombre">{{ record.nombre || "-" }}</div>
|
||||||
</div>
|
<div class="meta">ID: {{ record.id }}</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Puntaje -->
|
<template v-else-if="column.key === 'puntaje'">
|
||||||
<template v-else-if="column.key === 'puntaje'">
|
<div class="puntaje">{{ record.puntaje ?? "-" }}</div>
|
||||||
<span class="puntaje">
|
<div class="meta">Puntaje</div>
|
||||||
{{ record.puntaje ?? '-' }}
|
</template>
|
||||||
</span>
|
|
||||||
</template>
|
<template v-else-if="column.key === 'apto'">
|
||||||
|
<span class="statusPill" :class="statusClass(record.apto)">
|
||||||
|
{{ aptoTexto(record.apto) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Apto -->
|
<template v-else-if="column.key === 'acciones'">
|
||||||
<template v-else-if="column.key === 'apto'">
|
<a-space>
|
||||||
<a-tag :color="aptoColor(record.apto)" class="tag-pill">
|
<a-button size="small" @click="verDetalle(record)">Ver detalle</a-button>
|
||||||
{{ aptoTexto(record.apto) }}
|
</a-space>
|
||||||
</a-tag>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Acciones -->
|
<template #emptyText>
|
||||||
<template v-else-if="column.key === 'acciones'">
|
<a-empty description="No se encontraron procesos" />
|
||||||
<a-space>
|
|
||||||
<a-button size="small" @click="verDetalle(record)">Ver detalle</a-button>
|
|
||||||
</a-space>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</a-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile: cards -->
|
||||||
|
<div v-else class="cards">
|
||||||
|
<template v-if="procesosFiltrados.length">
|
||||||
|
<div v-for="p in procesosFiltrados" :key="p.id" class="itemCard">
|
||||||
|
<div class="itemTop">
|
||||||
|
<div class="itemTitle">{{ p.nombre || "-" }}</div>
|
||||||
|
<span class="statusPill" :class="statusClass(p.apto)">
|
||||||
|
{{ aptoTexto(p.apto) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template #emptyText>
|
<div class="itemGrid">
|
||||||
<a-empty description="No se encontraron procesos" />
|
<div class="kv">
|
||||||
|
<div class="k">Puntaje</div>
|
||||||
|
<div class="v strong">{{ p.puntaje ?? "-" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="kv">
|
||||||
|
<div class="k">ID</div>
|
||||||
|
<div class="v">{{ p.id }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="itemActions">
|
||||||
|
<a-button type="primary" class="btnPrimary" block @click="verDetalle(p)">
|
||||||
|
Ver detalle
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
|
||||||
|
<a-empty v-else description="No se encontraron procesos" />
|
||||||
|
</div>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</a-card>
|
</a-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from "ant-design-vue";
|
||||||
import api from '../../axiosPostulante' // ✅ ajusta la ruta a tu axios
|
import api from "../../axiosPostulante";
|
||||||
|
|
||||||
const procesos = ref([])
|
const procesos = ref([]);
|
||||||
const loading = ref(false)
|
const loading = ref(false);
|
||||||
const search = ref('')
|
const search = ref("");
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: 'Proceso', dataIndex: 'nombre', key: 'nombre', width: 420 },
|
{ title: "Proceso", dataIndex: "nombre", key: "nombre", width: 420 },
|
||||||
{ title: 'Puntaje', dataIndex: 'puntaje', key: 'puntaje', width: 140 },
|
{ title: "Puntaje", dataIndex: "puntaje", key: "puntaje", width: 140 },
|
||||||
{ title: 'Estado', dataIndex: 'apto', key: 'apto', width: 160 },
|
{ title: "Estado", dataIndex: "apto", key: "apto", width: 160 },
|
||||||
{ title: 'Acciones', key: 'acciones', width: 160 }
|
{ title: "Acciones", key: "acciones", width: 160 },
|
||||||
]
|
];
|
||||||
|
|
||||||
const obtenerProcesos = async () => {
|
const obtenerProcesos = async () => {
|
||||||
loading.value = true
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
// ✅ Ruta: crea una ruta GET que apunte a misProcesos()
|
const { data } = await api.get("/postulante/mis-procesos");
|
||||||
// Ejemplo: Route::get('/postulante/mis-procesos', ...)
|
|
||||||
const { data } = await api.get('/postulante/mis-procesos')
|
|
||||||
|
|
||||||
if (data?.success) {
|
if (data?.success) {
|
||||||
procesos.value = Array.isArray(data.data) ? data.data : []
|
procesos.value = Array.isArray(data.data) ? data.data : [];
|
||||||
} else {
|
} else {
|
||||||
message.error('No se pudieron obtener los procesos')
|
message.error("No se pudieron obtener los procesos");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e);
|
||||||
message.error(e.response?.data?.message || 'Error al cargar procesos')
|
message.error(e.response?.data?.message || "Error al cargar procesos");
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const aptoTexto = (apto) => {
|
const aptoTexto = (apto) => {
|
||||||
// en DB puede venir 1/0, true/false, "1"/"0"
|
if (apto === 1 || apto === true || apto === "1") return "APTO";
|
||||||
if (apto === 1 || apto === true || apto === '1') return 'APTO'
|
if (apto === 0 || apto === false || apto === "0") return "NO APTO";
|
||||||
if (apto === 0 || apto === false || apto === '0') return 'NO APTO'
|
return String(apto ?? "-").toUpperCase();
|
||||||
return String(apto ?? '-').toUpperCase()
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const aptoColor = (apto) => {
|
/** ✅ Un solo color: no usamos verde/rojo; solo estilos neutros + primary sutil */
|
||||||
if (apto === 1 || apto === true || apto === '1') return 'green'
|
const statusClass = (apto) => {
|
||||||
if (apto === 0 || apto === false || apto === '0') return 'red'
|
if (apto === 1 || apto === true || apto === "1") return "ok";
|
||||||
return 'default'
|
if (apto === 0 || apto === false || apto === "0") return "bad";
|
||||||
}
|
return "neutral";
|
||||||
|
};
|
||||||
|
|
||||||
const procesosFiltrados = computed(() => {
|
const procesosFiltrados = computed(() => {
|
||||||
const q = search.value.trim().toLowerCase()
|
const q = search.value.trim().toLowerCase();
|
||||||
if (!q) return procesos.value
|
if (!q) return procesos.value;
|
||||||
return procesos.value.filter(p =>
|
return procesos.value.filter((p) => String(p.nombre || "").toLowerCase().includes(q));
|
||||||
String(p.nombre || '').toLowerCase().includes(q)
|
});
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const verDetalle = (record) => {
|
const verDetalle = (record) => {
|
||||||
// ✅ Aquí puedes navegar a otra vista si tienes ruta
|
message.info(`Proceso: ${record.nombre} | Puntaje: ${record.puntaje ?? "-"}`);
|
||||||
// router.push({ name: 'DetalleProceso', params: { procesoId: record.id } })
|
};
|
||||||
message.info(`Proceso: ${record.nombre} | Puntaje: ${record.puntaje ?? '-'}`)
|
|
||||||
|
/* ✅ Responsive real: detecta móvil para cambiar a cards */
|
||||||
|
const isMobile = ref(false);
|
||||||
|
let mq = null;
|
||||||
|
|
||||||
|
function setMobile() {
|
||||||
|
isMobile.value = window.matchMedia("(max-width: 640px)").matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
obtenerProcesos()
|
obtenerProcesos();
|
||||||
})
|
mq = window.matchMedia("(max-width: 640px)");
|
||||||
|
setMobile();
|
||||||
|
// addEventListener es lo moderno; fallback por compatibilidad
|
||||||
|
if (mq.addEventListener) mq.addEventListener("change", setMobile);
|
||||||
|
else mq.addListener(setMobile);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (!mq) return;
|
||||||
|
if (mq.removeEventListener) mq.removeEventListener("change", setMobile);
|
||||||
|
else mq.removeListener(setMobile);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.procesos-card {
|
/* =========================
|
||||||
|
Base (formal 17+, sin degradados)
|
||||||
|
1 color acento: primary
|
||||||
|
========================= */
|
||||||
|
.card {
|
||||||
max-width: 1100px;
|
max-width: 1100px;
|
||||||
margin: 20px auto;
|
margin: 16px auto;
|
||||||
border-radius: 18px;
|
border-radius: 14px;
|
||||||
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.10);
|
|
||||||
background: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
/* Header */
|
||||||
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: flex-start;
|
}
|
||||||
|
.headerLeft {
|
||||||
|
min-width: 240px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-weight: 900;
|
||||||
|
color: var(--ant-colorTextHeading, #111827);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ant-colorTextSecondary, #6b7280);
|
||||||
|
}
|
||||||
|
.headerRight {
|
||||||
|
min-width: 180px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-left {
|
/* Tools */
|
||||||
|
.tools {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 220px 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.counter {
|
||||||
|
border: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.08));
|
||||||
|
background: var(--ant-colorFillAlter, #fafafa);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: space-between;
|
||||||
gap: 4px;
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
.counterLabel {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ant-colorTextSecondary, #6b7280);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.counterValue {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: var(--ant-colorTextHeading, #111827);
|
||||||
|
}
|
||||||
|
.search {
|
||||||
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-main {
|
/* Table */
|
||||||
|
.tableWrap {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.table :deep(.ant-table) {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.nombre {
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--ant-colorTextHeading, #111827);
|
||||||
|
}
|
||||||
|
.puntaje {
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
font-size: 18px;
|
color: var(--ant-colorPrimary, #1677ff); /* ✅ único acento */
|
||||||
color: #111827;
|
}
|
||||||
|
.meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ant-colorTextSecondary, #6b7280);
|
||||||
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-sub {
|
/* Status pill (sin verde/rojo) */
|
||||||
font-weight: 650;
|
.statusPill {
|
||||||
color: #6b7280;
|
display: inline-flex;
|
||||||
font-size: 13px;
|
align-items: center;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.08));
|
||||||
|
background: var(--ant-colorFillAlter, #fafafa);
|
||||||
|
color: var(--ant-colorTextHeading, #111827);
|
||||||
|
}
|
||||||
|
.statusPill.ok {
|
||||||
|
border-color: rgba(22,119,255,.35);
|
||||||
|
background: rgba(22,119,255,.08);
|
||||||
|
}
|
||||||
|
.statusPill.bad {
|
||||||
|
border-color: rgba(0,0,0,.10);
|
||||||
|
background: rgba(0,0,0,.04);
|
||||||
|
}
|
||||||
|
.statusPill.neutral {
|
||||||
|
opacity: .85;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-summary {
|
/* Mobile cards */
|
||||||
display: flex;
|
.cards {
|
||||||
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-alert {
|
.itemCard {
|
||||||
|
border: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.08));
|
||||||
|
background: var(--ant-colorBgContainer, #fff);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
margin: 0;
|
padding: 12px;
|
||||||
flex: 1;
|
|
||||||
min-width: 280px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.itemTop {
|
||||||
max-width: 360px;
|
display: flex;
|
||||||
border-radius: 12px;
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nombre {
|
.itemTitle {
|
||||||
font-weight: 850;
|
font-weight: 900;
|
||||||
color: #111827;
|
color: var(--ant-colorTextHeading, #111827);
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.puntaje {
|
.itemGrid {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kv {
|
||||||
|
border: 1px solid var(--ant-colorBorderSecondary, rgba(0,0,0,.06));
|
||||||
|
background: var(--ant-colorFillAlter, #fafafa);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
.k {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ant-colorTextSecondary, #6b7280);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.v {
|
||||||
|
margin-top: 4px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
color: #1677ff;
|
color: var(--ant-colorTextHeading, #111827);
|
||||||
|
}
|
||||||
|
.v.strong {
|
||||||
|
color: var(--ant-colorPrimary, #1677ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-pill {
|
.itemActions {
|
||||||
border-radius: 999px;
|
margin-top: 12px;
|
||||||
font-weight: 800;
|
}
|
||||||
padding: 2px 10px;
|
.btnPrimary {
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.procesos-table :deep(.ant-table) {
|
/* Responsive tools */
|
||||||
border-radius: 14px;
|
@media (max-width: 640px) {
|
||||||
overflow: hidden;
|
.card {
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.headerRight {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.tools {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.itemGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue