You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

746 lines
25 KiB
PHTML

2 months ago
<?php
namespace App\Http\Controllers;
use App\Models\Postulante;
use App\Models\Proceso;
use App\Models\Area;
use App\Models\Curso;
use App\Models\Pregunta;
use App\Models\Alternativa;
use App\Models\Examen;
use App\Models\PreguntaAsignada;
use App\Models\AlternativaAsignada;
use App\Models\DetalleResultado;
use App\Models\RespuestaPostulante;
use App\Models\ResultadoExamen;
use App\Models\Recomendaciones;
use App\Models\Pago;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Http;
use App\Services\ExamenService;
2 months ago
use Illuminate\Support\Facades\DB;
2 months ago
class ExamenController extends Controller
{
protected $examenService;
public function __construct(ExamenService $examenService)
{
$this->examenService = $examenService;
}
public function procesoexamen(Request $request)
{
$postulante = $request->user();
$procesos = Proceso::where('activo', 1)
->whereNotExists(function ($q) use ($postulante) {
$q->select(\DB::raw(1))
->from('examenes')
->join('area_proceso', 'area_proceso.id', '=', 'examenes.area_proceso_id')
->whereColumn('area_proceso.proceso_id', 'procesos.id')
->where('examenes.postulante_id', $postulante->id);
})
->select('id', 'nombre', 'requiere_pago')
->get();
return response()->json($procesos);
}
public function areas(Request $request)
{
$procesoId = $request->query('proceso_id');
$areas = Area::select('areas.id', 'areas.nombre')
->whereHas('procesos', function ($query) use ($procesoId) {
$query->where('procesos.id', $procesoId)
->where('procesos.activo', 1);
})
->with(['procesos' => function ($q) use ($procesoId) {
$q->where('procesos.id', $procesoId)
->select('procesos.id')
->withPivot('id', 'proceso_id', 'area_id');
}])
->get()
->map(function ($area) {
$pivot = $area->procesos->first()->pivot;
return [
'area_id' => $area->id,
'nombre' => $area->nombre,
2 months ago
'area_proceso_id' => $pivot->id,
2 months ago
];
});
return response()->json($areas);
}
public function crearExamen(Request $request)
{
$postulante = $request->user();
$request->validate([
'area_proceso_id' => 'required|exists:area_proceso,id',
]);
// 🔥 Obtener TODO desde el pivot
$areaProceso = \DB::table('area_proceso')
->join('procesos', 'procesos.id', '=', 'area_proceso.proceso_id')
->join('areas', 'areas.id', '=', 'area_proceso.area_id')
->where('area_proceso.id', $request->area_proceso_id)
->select(
'area_proceso.id',
'area_proceso.area_id',
'area_proceso.proceso_id',
'procesos.requiere_pago'
)
->first();
if (!$areaProceso) {
return response()->json(['message' => 'Relación área-proceso inválida'], 400);
}
$yaDioProceso = Examen::join('area_proceso', 'area_proceso.id', '=', 'examenes.area_proceso_id')
->where('examenes.postulante_id', $postulante->id)
->where('area_proceso.proceso_id', $areaProceso->proceso_id)
->exists();
if ($yaDioProceso) {
return response()->json([
'message' => 'Ya rendiste un examen para este proceso'
], 400);
}
$pagado = 0;
$pagoId = null;
// 💰 Validación de pago
if ($areaProceso->requiere_pago) {
$request->validate([
'tipo_pago' => 'required|in:pyto_peru,banco_nacion,caja',
'codigo_pago' => 'required',
]);
$response = $this->validarPago(
$request->tipo_pago,
$request->codigo_pago,
$postulante->dni
);
if (!$response['estado']) {
return response()->json(['message' => 'Pago inválido'], 400);
}
$pago = Pago::firstOrCreate(
[
'codigo_pago' => $request->codigo_pago,
'tipo_pago' => $request->tipo_pago,
],
[
'postulante_id' => $postulante->id,
'monto' => $response['monto'],
'fecha_pago' => $response['fecha_pago'],
]
);
if ($pago->utilizado) {
return response()->json(['message' => 'Pago ya utilizado'], 400);
}
$pago->update(['utilizado' => true]);
$pagado = 1;
$pagoId = $pago->id;
}
// 🧠 Crear / actualizar examen
$examen = Examen::updateOrCreate(
['postulante_id' => $postulante->id,
'area_proceso_id' => $areaProceso->id, // 🔥 pivot
'pagado' => $pagado,
'tipo_pago' => $request->tipo_pago ?? null,
'pago_id' => $pagoId,
]
);
return response()->json([
'success' => true,
'examen_id' => $examen->id,
'mensaje' => 'Examen creado correctamente',
]);
}
public function validarPago($tipoPago, $codigoPago, $dni)
{
return match ($tipoPago) {
'pyto_peru' => $this->validarPagoPytoPeru($codigoPago, $dni),
'banco_nacion' => $this->validarPagoBancoNacion($codigoPago, $dni),
'caja' => $this->validarPagoCaja($codigoPago, $dni),
default => ['estado' => false],
};
}
private function validarPagoBancoNacion($secuencia, $dni)
{
$url = 'https://inscripciones.admision.unap.edu.pe/api/get-pagos-banco-secuencia';
$response = Http::post($url, [
'secuencia' => $secuencia,
]);
if (!$response->successful()) {
\Log::error('Error en la solicitud a la API', ['status' => $response->status(), 'body' => $response->body()]);
return ['estado' => false, 'message' => 'Error en la solicitud a la API'];
}
$data = $response->json();
if (isset($data['estado']) && $data['estado'] === true) {
foreach ($data['datos'] as $pago) {
if (isset($pago['dni']) && trim($pago['dni']) == trim($dni)) {
return [
'estado' => true,
'monto' => $pago['imp_pag'],
'fecha_pago' => $pago['fch_pag'],
];
}
}
}
return ['estado' => false, 'message' => 'Datos no válidos o no encontrados'];
}
private function validarPagoPytoPeru($authorizationCode, $dni)
{
$url = "https://service2.unap.edu.pe/PAYMENTS_MNG/v1/{$dni}/8/";
$response = Http::get($url);
if ($response->successful()) {
$data = $response->json();
if (isset($data['data'][0]['autorizationCode']) && $data['data'][0]['autorizationCode'] === $authorizationCode) {
return [
'estado' => true,
'monto' => $data['data'][0]['total'],
'fecha_pago' => $data['data'][0]['confirmedDate'],
];
}
}
return ['estado' => false, 'message' => 'El código de autorización no coincide.'];
}
private function validarPagoCaja($codigoPago, $dni)
{
$url = "https://inscripciones.admision.unap.edu.pe/api/get-pago-caja/{$dni}/{$codigoPago}";
$response = Http::get($url);
if (!$response->successful()) {
\Log::error('Error API Caja', [
'status' => $response->status(),
'body' => $response->body()
]);
return ['estado' => false, 'message' => 'Error en la API de Caja'];
}
$data = $response->json();
if (
isset($data['paymentTitle'], $data['paymentAmount'], $data['paymentDatetime']) &&
trim($data['paymentTitle']) === trim($codigoPago)
) {
return [
'estado' => true,
'monto' => (float) $data['paymentAmount'],
'fecha_pago' => $data['paymentDatetime'],
];
}
return ['estado' => false, 'message' => 'Pago no válido o no encontrado'];
}
public function miExamenActual(Request $request)
{
$postulante = $request->user();
2 months ago
2 months ago
$examen = Examen::join('area_proceso', 'area_proceso.id', '=', 'examenes.area_proceso_id')
->join('areas', 'areas.id', '=', 'area_proceso.area_id')
->join('procesos', 'procesos.id', '=', 'area_proceso.proceso_id')
->where('examenes.postulante_id', $postulante->id)
->select(
'examenes.id',
'examenes.pagado',
'examenes.tipo_pago',
2 months ago
'examenes.intentos',
2 months ago
'areas.id as area_id',
'areas.nombre as area_nombre',
'procesos.id as proceso_id',
'procesos.nombre as proceso_nombre',
2 months ago
'procesos.intentos_maximos as proceso_intentos_maximos'
2 months ago
)
2 months ago
->latest('examenes.created_at')
2 months ago
->first();
if (!$examen) {
return response()->json([
'success' => true,
'mensaje' => 'No tienes exámenes asignados actualmente',
'examen' => null
]);
}
return response()->json([
'success' => true,
'examen' => [
'id' => $examen->id,
'intentos' => $examen->intentos,
'intentos_maximos' => $examen->proceso_intentos_maximos,
'proceso' => [
'id' => $examen->proceso_id,
'nombre' => $examen->proceso_nombre,
],
'area' => [
'id' => $examen->area_id,
'nombre' => $examen->area_nombre,
],
'pagado' => $examen->pagado,
'tipo_pago' => $examen->tipo_pago ?? null,
]
]);
}
2 months ago
2 months ago
public function generarPreguntas($examenId)
{
$examen = Examen::findOrFail($examenId);
$postulante = request()->user();
if ($examen->postulante_id !== $postulante->id) {
return response()->json([
'success' => false,
'message' => 'No autorizado'
], 403);
}
if ($examen->preguntasAsignadas()->exists()) {
return response()->json([
'success' => true,
'message' => 'El examen ya tiene preguntas generadas',
'ya_tiene_preguntas' => true,
'total_preguntas' => $examen->preguntasAsignadas()->count()
]);
}
$resultado = $this->examenService->generarPreguntasExamen($examen);
if (!$resultado['success']) {
return response()->json($resultado, 400);
}
return response()->json([
'success' => true,
'message' => 'Preguntas generadas exitosamente',
'ya_tiene_preguntas' => false,
'total_preguntas' => $examen->preguntasAsignadas()->count()
]);
}
public function iniciarExamen(Request $request)
{
$request->validate([
'examen_id' => 'required|exists:examenes,id'
]);
$examen = Examen::findOrFail($request->examen_id);
$postulante = $request->user();
if ($examen->postulante_id !== $postulante->id) {
return response()->json([
'success' => false,
'message' => 'No autorizado'
], 403);
}
$areaProceso = \DB::table('area_proceso')
->join('procesos', 'area_proceso.proceso_id', '=', 'procesos.id')
->join('areas', 'area_proceso.area_id', '=', 'areas.id')
->where('area_proceso.id', $examen->area_proceso_id)
->select(
'procesos.id as proceso_id',
'procesos.nombre as proceso_nombre',
'procesos.duracion as proceso_duracion',
'procesos.intentos_maximos as proceso_intentos_maximos',
'areas.nombre as area_nombre'
)
->first();
if (!$examen->preguntasAsignadas()->exists()) {
return response()->json([
'success' => false,
'message' => 'El examen no tiene preguntas generadas'
], 400);
}
if ($areaProceso && $examen->intentos >= $areaProceso->proceso_intentos_maximos) {
return response()->json([
'success' => false,
'message' => 'Has alcanzado el número máximo de intentos para este proceso'
], 403);
}
2 months ago
$examen->increment('intentos');
2 months ago
2 months ago
$examen->update([
2 months ago
'hora_inicio' => now(),
2 months ago
'estado' => 'en_progreso'
]);
2 months ago
$preguntas = $this->examenService->obtenerPreguntasExamen($examen);
return response()->json([
'success' => true,
'examen' => [
'id' => $examen->id,
'estado' => $examen->estado,
'hora_inicio' => $examen->hora_inicio,
'intentos' => $examen->intentos,
'intentos_maximos' => $areaProceso->proceso_intentos_maximos ?? null,
'proceso' => $areaProceso->proceso_nombre ?? null,
'duracion' => $areaProceso->proceso_duracion ?? null,
'area' => $areaProceso->area_nombre ?? null
],
'preguntas' => $preguntas
]);
}
2 months ago
2 months ago
public function responderPregunta($preguntaAsignadaId, Request $request)
{
$request->validate([
2 months ago
'respuesta' => 'nullable|string'
2 months ago
]);
$preguntaAsignada = PreguntaAsignada::with(['examen', 'pregunta'])
->findOrFail($preguntaAsignadaId);
$postulante = $request->user();
if ($preguntaAsignada->examen->postulante_id !== $postulante->id) {
return response()->json([
'success' => false,
'message' => 'No autorizado'
], 403);
}
$resultado = $this->examenService->guardarRespuesta(
$preguntaAsignada,
$request->respuesta
);
return response()->json($resultado);
}
2 months ago
2 months ago
2 months ago
public function finalizarExamen($examenId)
{
$examen = Examen::findOrFail($examenId);
$postulante = request()->user();
2 months ago
// Validar que el examen le pertenezca al postulante autenticado
if ((int) $examen->postulante_id !== (int) $postulante->id) {
2 months ago
return response()->json([
'success' => false,
'message' => 'No autorizado'
], 403);
}
2 months ago
// (Opcional) Evitar finalizar 2 veces
if ($examen->estado === 'finalizado') {
return response()->json([
'success' => false,
'message' => 'El examen ya está finalizado'
], 409);
}
// Finalizar examen
$examen->update([
'estado' => 'finalizado',
'hora_fin' => now(),
]);
2 months ago
return response()->json([
'success' => true,
'message' => 'Examen finalizado exitosamente'
]);
}
2 months ago
public function calificarExamen($examenId, Request $request)
{
$postulante = $request->user();
if (!$postulante) {
return response()->json(['success' => false, 'mensaje' => 'No autenticado.'], 401);
}
return DB::transaction(function () use ($examenId, $postulante) {
// 1) Validar examen del postulante
$examen = DB::table('examenes')
->where('id', $examenId)
->where('postulante_id', $postulante->id)
->first();
if (!$examen) {
return response()->json([
'success' => false,
'mensaje' => 'No se encontró un examen para este postulante.'
], 404);
}
// 2) Si ya está calificado, devolver existente (y opcionalmente recalcular orden)
$existente = DB::table('resultados_examenes')
->where('examen_id', $examen->id)
->first();
if ($existente) {
// Obtener proceso_id para poder recalcular si deseas (opcional)
$procesoId = DB::table('area_proceso')
->where('id', $examen->area_proceso_id)
->value('proceso_id');
if ($procesoId) $this->recalcularOrdenMerito($procesoId);
return response()->json([
'success' => true,
'mensaje' => 'Examen ya calificado.',
'total_puntos' => (float)$existente->total_puntos,
'total_correctas' => (int)$existente->total_correctas,
'total_incorrectas' => (int)$existente->total_incorrectas,
'total_nulas' => (int)$existente->total_nulas,
'porcentaje_correctas' => (float)$existente->porcentaje_correctas,
'calificacion_sobre_20' => (float)$existente->calificacion_sobre_20,
'orden_merito' => $existente->orden_merito,
'correctas_por_curso' => json_decode($existente->correctas_por_curso, true),
'incorrectas_por_curso' => json_decode($existente->incorrectas_por_curso ?? '[]', true),
'preguntas_totales_por_curso' => json_decode($existente->preguntas_totales_por_curso ?? '[]', true),
]);
}
// 3) Obtener configuración de calificación desde proceso -> calificaciones
$cfg = DB::table('area_proceso as ap')
->join('procesos as pr', 'pr.id', '=', 'ap.proceso_id')
->join('calificaciones as ca', 'ca.id', '=', 'pr.calificacion_id')
->where('ap.id', $examen->area_proceso_id)
->select(
'pr.id as proceso_id',
'ca.puntos_correcta',
'ca.puntos_incorrecta',
'ca.puntos_nula',
'ca.puntaje_maximo'
)
->first();
if (!$cfg) {
return response()->json([
'success' => false,
'mensaje' => 'No se ha definido un tipo de calificación para este proceso.'
], 400);
}
$puntosCorrecta = (float) $cfg->puntos_correcta;
$puntosIncorrecta = (float) $cfg->puntos_incorrecta;
$puntosNula = (float) $cfg->puntos_nula;
$puntajeMaximo = (float) $cfg->puntaje_maximo;
// 4) Traer preguntas asignadas con su pregunta y curso
$items = DB::table('preguntas_asignadas as pa')
->join('preguntas as p', 'p.id', '=', 'pa.pregunta_id')
->join('cursos as c', 'c.id', '=', 'p.curso_id')
->where('pa.examen_id', $examen->id)
->select(
'pa.id as pa_id',
'pa.estado as pa_estado',
'pa.respuesta_usuario',
'p.respuesta_correcta',
'c.nombre as curso_nombre'
)
->get();
if ($items->isEmpty()) {
return response()->json([
'success' => false,
'mensaje' => 'El examen no tiene preguntas asignadas.'
], 422);
}
// 5) Calificar y actualizar cada pregunta_asignada
$totalPuntos = 0.0;
$totalCorrectas = 0;
$totalIncorrectas = 0;
$totalNulas = 0;
$correctasPorCurso = [];
$incorrectasPorCurso = [];
$preguntasTotalesPorCurso = [];
foreach ($items as $row) {
$curso = $row->curso_nombre;
$preguntasTotalesPorCurso[$curso] = ($preguntasTotalesPorCurso[$curso] ?? 0) + 1;
$correctasPorCurso[$curso] = $correctasPorCurso[$curso] ?? 0;
$incorrectasPorCurso[$curso] = $incorrectasPorCurso[$curso] ?? 0;
$nuevoEsCorrecta = 2; // 2 = blanco
$nuevoPuntaje = $puntosNula; // nula/blanco
if ($row->pa_estado === 'anulada') {
// anulada => nula
$nuevoEsCorrecta = 2;
$nuevoPuntaje = $puntosNula;
$totalNulas++;
} else {
$ru = trim((string) $row->respuesta_usuario);
$rc = trim((string) $row->respuesta_correcta);
if ($ru === '') {
// blanco
$nuevoEsCorrecta = 2;
$nuevoPuntaje = $puntosNula;
$totalNulas++;
} else {
$ruN = mb_strtoupper($ru);
$rcN = mb_strtoupper($rc);
if ($rcN !== '' && $ruN === $rcN) {
$nuevoEsCorrecta = 1;
$nuevoPuntaje = $puntosCorrecta;
$totalCorrectas++;
$correctasPorCurso[$curso]++;
} else {
$nuevoEsCorrecta = 0;
$nuevoPuntaje = $puntosIncorrecta;
$totalIncorrectas++;
$incorrectasPorCurso[$curso]++;
}
}
}
$totalPuntos += (float) $nuevoPuntaje;
DB::table('preguntas_asignadas')
->where('id', $row->pa_id)
->update([
'es_correcta' => $nuevoEsCorrecta,
'puntaje' => $nuevoPuntaje,
'updated_at' => now(),
]);
}
// 6) Resumen
$totalPreguntas = (int) $items->count();
$porcentajeCorrectas = $totalPreguntas > 0 ? ($totalCorrectas / $totalPreguntas) * 100 : 0;
$calificacionSobre20 = ($puntajeMaximo > 0)
? ($totalPuntos / $puntajeMaximo) * 20
: 0;
$correctasPorCursoFormato = [];
foreach ($correctasPorCurso as $curso => $corr) {
$y = $preguntasTotalesPorCurso[$curso] ?? 0;
$correctasPorCursoFormato[$curso] = "{$corr} de {$y}";
}
// 7) Guardar resultado en resultados_examenes
$resultadoId = DB::table('resultados_examenes')->insertGetId([
'postulante_id' => $postulante->id,
'examen_id' => $examen->id,
'total_puntos' => round($totalPuntos, 3),
'correctas_por_curso' => json_encode($correctasPorCursoFormato),
'incorrectas_por_curso' => json_encode($incorrectasPorCurso),
'preguntas_totales_por_curso' => json_encode($preguntasTotalesPorCurso),
'total_correctas' => $totalCorrectas,
'total_incorrectas' => $totalIncorrectas,
'total_nulas' => $totalNulas,
'porcentaje_correctas' => round($porcentajeCorrectas, 2),
'calificacion_sobre_20' => round($calificacionSobre20, 2),
'created_at' => now(),
'updated_at' => now(),
]);
// 8) Recalcular orden de mérito por proceso
$this->recalcularOrdenMerito($cfg->proceso_id);
// 9) Leer orden_merito ya asignado (opcional)
$orden = DB::table('resultados_examenes')->where('id', $resultadoId)->value('orden_merito');
DB::table('examenes')->where('id', $examen->id)->update([
'estado' => 'calificado',
'hora_fin' => now(),
]);
return response()->json([
'success' => true,
'mensaje' => 'Examen calificado exitosamente.',
'examen_id' => $examen->id,
'proceso_id' => $cfg->proceso_id,
'total_puntos' => round($totalPuntos, 2),
'total_correctas' => $totalCorrectas,
'total_incorrectas' => $totalIncorrectas,
'total_nulas' => $totalNulas,
'porcentaje_correctas' => round($porcentajeCorrectas, 2),
'calificacion_sobre_20' => round($calificacionSobre20, 2),
'orden_merito' => $orden,
'correctas_por_curso' => $correctasPorCursoFormato,
'incorrectas_por_curso' => $incorrectasPorCurso,
'preguntas_totales_por_curso' => $preguntasTotalesPorCurso,
]);
});
}
public function recalcularOrdenMerito($procesoId): void
{
DB::statement("
UPDATE resultados_examenes r
JOIN (
SELECT
r2.id,
ROW_NUMBER() OVER (
ORDER BY
r2.total_puntos DESC,
COALESCE(r2.updated_at, r2.created_at) ASC
) AS nuevo_orden
FROM resultados_examenes r2
JOIN examenes e ON e.id = r2.examen_id
JOIN area_proceso ap ON ap.id = e.area_proceso_id
WHERE ap.proceso_id = ?
) x ON x.id = r.id
SET r.orden_merito = x.nuevo_orden
", [$procesoId]);
}
2 months ago
}