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>
|
||||
<a-card class="procesos-card" :bordered="false">
|
||||
<a-card class="card" :bordered="true">
|
||||
<template #title>
|
||||
<div class="card-title">
|
||||
<div class="title-left">
|
||||
<div class="title-main">Mis procesos de admisión</div>
|
||||
<div class="title-sub">Resultados registrados por DNI</div>
|
||||
<div class="header">
|
||||
<div class="headerLeft">
|
||||
<div class="title">Mis procesos de admisión</div>
|
||||
<div class="subtitle">Resultados registrados por DNI</div>
|
||||
</div>
|
||||
|
||||
<div class="title-right">
|
||||
<a-space>
|
||||
<a-button @click="obtenerProcesos" :loading="loading">Actualizar</a-button>
|
||||
</a-space>
|
||||
<div class="headerRight">
|
||||
<a-button @click="obtenerProcesos" :loading="loading" class="btn" block>
|
||||
Actualizar
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<div class="top-summary">
|
||||
<a-alert v-if="!loading" type="info" show-icon class="summary-alert">
|
||||
Total de procesos: <strong>{{ procesos.length }}</strong>
|
||||
</a-alert>
|
||||
<!-- Top tools -->
|
||||
<div class="tools">
|
||||
<div class="counter">
|
||||
<span class="counterLabel">Total</span>
|
||||
<span class="counterValue">{{ procesosFiltrados.length }}</span>
|
||||
</div>
|
||||
|
||||
<a-input
|
||||
v-model:value="search"
|
||||
allow-clear
|
||||
placeholder="Buscar por nombre de proceso..."
|
||||
class="search-input"
|
||||
placeholder="Buscar por nombre de proceso…"
|
||||
class="search"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
class="procesos-table"
|
||||
:dataSource="procesosFiltrados"
|
||||
:columns="columns"
|
||||
rowKey="id"
|
||||
:pagination="{ pageSize: 7, showSizeChanger: false }"
|
||||
:scroll="{ x: 900 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- Nombre -->
|
||||
<template v-if="column.key === 'nombre'">
|
||||
<div class="nombre">
|
||||
{{ record.nombre || '-' }}
|
||||
</div>
|
||||
</template>
|
||||
<!-- Desktop/tablet: tabla -->
|
||||
<div v-if="!isMobile" class="tableWrap">
|
||||
<a-table
|
||||
class="table"
|
||||
:dataSource="procesosFiltrados"
|
||||
:columns="columns"
|
||||
rowKey="id"
|
||||
:pagination="{ pageSize: 7, showSizeChanger: false }"
|
||||
:scroll="{ x: 900 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'nombre'">
|
||||
<div class="nombre">{{ record.nombre || "-" }}</div>
|
||||
<div class="meta">ID: {{ record.id }}</div>
|
||||
</template>
|
||||
|
||||
<!-- Puntaje -->
|
||||
<template v-else-if="column.key === 'puntaje'">
|
||||
<span class="puntaje">
|
||||
{{ record.puntaje ?? '-' }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'puntaje'">
|
||||
<div class="puntaje">{{ record.puntaje ?? "-" }}</div>
|
||||
<div class="meta">Puntaje</div>
|
||||
</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 === 'apto'">
|
||||
<a-tag :color="aptoColor(record.apto)" class="tag-pill">
|
||||
{{ aptoTexto(record.apto) }}
|
||||
</a-tag>
|
||||
<template v-else-if="column.key === 'acciones'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="verDetalle(record)">Ver detalle</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Acciones -->
|
||||
<template v-else-if="column.key === 'acciones'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="verDetalle(record)">Ver detalle</a-button>
|
||||
</a-space>
|
||||
<template #emptyText>
|
||||
<a-empty description="No se encontraron procesos" />
|
||||
</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>
|
||||
<a-empty description="No se encontraron procesos" />
|
||||
<div class="itemGrid">
|
||||
<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>
|
||||
</a-table>
|
||||
|
||||
<a-empty v-else description="No se encontraron procesos" />
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import api from '../../axiosPostulante' // ✅ ajusta la ruta a tu axios
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import api from "../../axiosPostulante";
|
||||
|
||||
const procesos = ref([])
|
||||
const loading = ref(false)
|
||||
const search = ref('')
|
||||
const procesos = ref([]);
|
||||
const loading = ref(false);
|
||||
const search = ref("");
|
||||
|
||||
const columns = [
|
||||
{ title: 'Proceso', dataIndex: 'nombre', key: 'nombre', width: 420 },
|
||||
{ title: 'Puntaje', dataIndex: 'puntaje', key: 'puntaje', width: 140 },
|
||||
{ title: 'Estado', dataIndex: 'apto', key: 'apto', width: 160 },
|
||||
{ title: 'Acciones', key: 'acciones', width: 160 }
|
||||
]
|
||||
{ title: "Proceso", dataIndex: "nombre", key: "nombre", width: 420 },
|
||||
{ title: "Puntaje", dataIndex: "puntaje", key: "puntaje", width: 140 },
|
||||
{ title: "Estado", dataIndex: "apto", key: "apto", width: 160 },
|
||||
{ title: "Acciones", key: "acciones", width: 160 },
|
||||
];
|
||||
|
||||
const obtenerProcesos = async () => {
|
||||
loading.value = true
|
||||
loading.value = true;
|
||||
try {
|
||||
// ✅ Ruta: crea una ruta GET que apunte a misProcesos()
|
||||
// Ejemplo: Route::get('/postulante/mis-procesos', ...)
|
||||
const { data } = await api.get('/postulante/mis-procesos')
|
||||
|
||||
const { data } = await api.get("/postulante/mis-procesos");
|
||||
if (data?.success) {
|
||||
procesos.value = Array.isArray(data.data) ? data.data : []
|
||||
procesos.value = Array.isArray(data.data) ? data.data : [];
|
||||
} else {
|
||||
message.error('No se pudieron obtener los procesos')
|
||||
message.error("No se pudieron obtener los procesos");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
message.error(e.response?.data?.message || 'Error al cargar procesos')
|
||||
console.error(e);
|
||||
message.error(e.response?.data?.message || "Error al cargar procesos");
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const aptoTexto = (apto) => {
|
||||
// en DB puede venir 1/0, true/false, "1"/"0"
|
||||
if (apto === 1 || apto === true || apto === '1') return 'APTO'
|
||||
if (apto === 0 || apto === false || apto === '0') return 'NO APTO'
|
||||
return String(apto ?? '-').toUpperCase()
|
||||
}
|
||||
if (apto === 1 || apto === true || apto === "1") return "APTO";
|
||||
if (apto === 0 || apto === false || apto === "0") return "NO APTO";
|
||||
return String(apto ?? "-").toUpperCase();
|
||||
};
|
||||
|
||||
const aptoColor = (apto) => {
|
||||
if (apto === 1 || apto === true || apto === '1') return 'green'
|
||||
if (apto === 0 || apto === false || apto === '0') return 'red'
|
||||
return 'default'
|
||||
}
|
||||
/** ✅ Un solo color: no usamos verde/rojo; solo estilos neutros + primary sutil */
|
||||
const statusClass = (apto) => {
|
||||
if (apto === 1 || apto === true || apto === "1") return "ok";
|
||||
if (apto === 0 || apto === false || apto === "0") return "bad";
|
||||
return "neutral";
|
||||
};
|
||||
|
||||
const procesosFiltrados = computed(() => {
|
||||
const q = search.value.trim().toLowerCase()
|
||||
if (!q) return procesos.value
|
||||
return procesos.value.filter(p =>
|
||||
String(p.nombre || '').toLowerCase().includes(q)
|
||||
)
|
||||
})
|
||||
const q = search.value.trim().toLowerCase();
|
||||
if (!q) return procesos.value;
|
||||
return procesos.value.filter((p) => String(p.nombre || "").toLowerCase().includes(q));
|
||||
});
|
||||
|
||||
const verDetalle = (record) => {
|
||||
// ✅ Aquí puedes navegar a otra vista si tienes ruta
|
||||
// router.push({ name: 'DetalleProceso', params: { procesoId: record.id } })
|
||||
message.info(`Proceso: ${record.nombre} | Puntaje: ${record.puntaje ?? '-'}`)
|
||||
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(() => {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
.procesos-card {
|
||||
/* =========================
|
||||
Base (formal 17+, sin degradados)
|
||||
1 color acento: primary
|
||||
========================= */
|
||||
.card {
|
||||
max-width: 1100px;
|
||||
margin: 20px auto;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.10);
|
||||
background: #fff;
|
||||
margin: 16px auto;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
/* Header */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
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;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
justify-content: space-between;
|
||||
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-size: 18px;
|
||||
color: #111827;
|
||||
color: var(--ant-colorPrimary, #1677ff); /* ✅ único acento */
|
||||
}
|
||||
.meta {
|
||||
font-size: 12px;
|
||||
color: var(--ant-colorTextSecondary, #6b7280);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.title-sub {
|
||||
font-weight: 650;
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
/* Status pill (sin verde/rojo) */
|
||||
.statusPill {
|
||||
display: inline-flex;
|
||||
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 {
|
||||
display: flex;
|
||||
/* Mobile cards */
|
||||
.cards {
|
||||
display: grid;
|
||||
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;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
max-width: 360px;
|
||||
border-radius: 12px;
|
||||
.itemTop {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nombre {
|
||||
font-weight: 850;
|
||||
color: #111827;
|
||||
.itemTitle {
|
||||
font-weight: 900;
|
||||
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;
|
||||
color: #1677ff;
|
||||
color: var(--ant-colorTextHeading, #111827);
|
||||
}
|
||||
.v.strong {
|
||||
color: var(--ant-colorPrimary, #1677ff);
|
||||
}
|
||||
|
||||
.tag-pill {
|
||||
border-radius: 999px;
|
||||
font-weight: 800;
|
||||
padding: 2px 10px;
|
||||
.itemActions {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.btnPrimary {
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.procesos-table :deep(.ant-table) {
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
/* Responsive tools */
|
||||
@media (max-width: 640px) {
|
||||
.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