diff --git a/app/Http/Controllers/EvaluacionController.php b/app/Http/Controllers/EvaluacionController.php new file mode 100644 index 00000000..44817faf --- /dev/null +++ b/app/Http/Controllers/EvaluacionController.php @@ -0,0 +1,64 @@ +withCount(['preguntas', 'submissions']) + ->latest() + ->get(); + + return view('evaluacion.index', compact('evaluaciones')); + } + + public function create() + { + return view('evaluacion.create'); + } + + public function store(Request $request) + { + // Manejado por Livewire EvaluacionBuilder + } + + public function show(Evaluacion $evaluacion) + { + $evaluacion->load(['tipo', 'preguntas.tipo', 'preguntas.opciones', 'planteles', 'niveles']); + + return view('evaluacion.show', compact('evaluacion')); + } + + public function edit(Evaluacion $evaluacion) + { + $evaluacion->load(['preguntas.opciones', 'planteles', 'niveles']); + + return view('evaluacion.create', compact('evaluacion')); + } + + public function update(Request $request, Evaluacion $evaluacion) + { + // Manejado por Livewire EvaluacionBuilder + } + + public function destroy(Evaluacion $evaluacion) + { + $evaluacion->delete(); + + return redirect()->route('evaluacion.index')->with('info', 'Evaluación eliminada.'); + } + + public function toggleStatus(Request $request, Evaluacion $evaluacion) + { + $request->validate(['status' => 'required|in:borrador,activa,cerrada']); + $evaluacion->update(['status' => $request->status]); + + return back()->with('info', 'Estado actualizado.'); + } +} diff --git a/app/Http/Controllers/EvaluacionResponderController.php b/app/Http/Controllers/EvaluacionResponderController.php new file mode 100644 index 00000000..06e09330 --- /dev/null +++ b/app/Http/Controllers/EvaluacionResponderController.php @@ -0,0 +1,10 @@ +tipos = EvaluacionTipo::all(['id', 'nombre', 'slug'])->toArray(); + $this->preguntaTipos = PreguntaTipo::all(['id', 'nombre', 'slug'])->toArray(); + $this->planteles = Plantel::orderBy('name')->get(['id', 'name'])->toArray(); + $this->niveles = Nivel::orderBy('name')->get(['id', 'name'])->toArray(); + + if ($evaluacion instanceof Evaluacion && $evaluacion->exists) { + $this->evaluacionId = $evaluacion->id; + $this->titulo = $evaluacion->titulo; + $this->descripcion = $evaluacion->descripcion ?? ''; + $this->evaluacionTipoId = (string) $evaluacion->evaluacion_tipo_id; + $this->status = $evaluacion->status; + $this->fechaInicio = $evaluacion->fecha_inicio?->format('Y-m-d') ?? ''; + $this->fechaFin = $evaluacion->fecha_fin?->format('Y-m-d') ?? ''; + $this->anonima = (bool) $evaluacion->anonima; + $this->selectedPlanteles = $evaluacion->planteles->pluck('id')->map(fn($v) => (string) $v)->toArray(); + $this->selectedNiveles = $evaluacion->niveles->pluck('id')->map(fn($v) => (string) $v)->toArray(); + + $this->preguntas = $evaluacion->preguntas->map(fn($p) => [ + 'id' => $p->id, + 'texto' => $p->texto, + 'pregunta_tipo_id' => (string) $p->pregunta_tipo_id, + 'requerida' => (bool) $p->requerida, + 'opciones' => $p->opciones->map(fn($o) => [ + 'id' => $o->id, + 'texto' => $o->texto, + ])->toArray(), + ])->toArray(); + } + } + + public function addPregunta(): void + { + $defaultTipo = $this->preguntaTipos[0]['id'] ?? ''; + $this->preguntas[] = [ + 'id' => null, + 'texto' => '', + 'pregunta_tipo_id' => (string) $defaultTipo, + 'requerida' => true, + 'opciones' => [], + ]; + } + + public function removePregunta(int $index): void + { + array_splice($this->preguntas, $index, 1); + $this->preguntas = array_values($this->preguntas); + } + + public function movePregunta(int $index, string $direction): void + { + $swap = $direction === 'up' ? $index - 1 : $index + 1; + + if ($swap < 0 || $swap >= count($this->preguntas)) { + return; + } + + [$this->preguntas[$index], $this->preguntas[$swap]] = [$this->preguntas[$swap], $this->preguntas[$index]]; + } + + public function addOpcion(int $preguntaIndex): void + { + $this->preguntas[$preguntaIndex]['opciones'][] = ['id' => null, 'texto' => '']; + } + + public function removeOpcion(int $preguntaIndex, int $opcionIndex): void + { + array_splice($this->preguntas[$preguntaIndex]['opciones'], $opcionIndex, 1); + $this->preguntas[$preguntaIndex]['opciones'] = array_values($this->preguntas[$preguntaIndex]['opciones']); + } + + public function guardar(): void + { + $this->validate([ + 'titulo' => 'required|string|max:255', + 'descripcion' => 'nullable|string', + 'evaluacionTipoId' => 'required|exists:evaluacion_tipos,id', + 'status' => 'required|in:borrador,activa,cerrada', + 'fechaInicio' => 'nullable|date', + 'fechaFin' => 'nullable|date', + 'selectedPlanteles' => 'array', + 'selectedNiveles' => 'array', + 'preguntas' => 'required|array|min:1', + 'preguntas.*.texto' => 'required|string|max:1000', + 'preguntas.*.pregunta_tipo_id' => 'required|exists:pregunta_tipos,id', + 'preguntas.*.opciones.*.texto' => 'required|string|max:500', + ], [ + 'titulo.required' => 'El título es obligatorio.', + 'evaluacionTipoId.required' => 'Selecciona el tipo de evaluación.', + 'preguntas.required' => 'Agrega al menos una pregunta.', + 'preguntas.min' => 'Agrega al menos una pregunta.', + 'preguntas.*.texto.required' => 'El texto de la pregunta es obligatorio.', + 'preguntas.*.pregunta_tipo_id.required' => 'Selecciona el tipo de respuesta.', + 'preguntas.*.opciones.*.texto.required' => 'El texto de la opción es obligatorio.', + ]); + + DB::transaction(function () { + $data = [ + 'titulo' => $this->titulo, + 'descripcion' => $this->descripcion ?: null, + 'evaluacion_tipo_id' => $this->evaluacionTipoId, + 'status' => $this->status, + 'fecha_inicio' => $this->fechaInicio ?: null, + 'fecha_fin' => $this->fechaFin ?: null, + 'anonima' => $this->anonima, + ]; + + if ($this->evaluacionId) { + $evaluacion = Evaluacion::findOrFail($this->evaluacionId); + $evaluacion->update($data); + } else { + $evaluacion = Evaluacion::create($data); + $this->evaluacionId = $evaluacion->id; + } + + $evaluacion->planteles()->sync($this->selectedPlanteles); + $evaluacion->niveles()->sync($this->selectedNiveles); + + // Eliminar preguntas removidas + $keptIds = collect($this->preguntas)->pluck('id')->filter()->values()->toArray(); + $evaluacion->preguntas()->whereNotIn('id', $keptIds)->delete(); + + foreach ($this->preguntas as $orden => $pData) { + if (!empty($pData['id'])) { + $pregunta = Pregunta::findOrFail($pData['id']); + $pregunta->update([ + 'texto' => $pData['texto'], + 'pregunta_tipo_id' => $pData['pregunta_tipo_id'], + 'requerida' => $pData['requerida'] ?? true, + 'orden' => $orden, + ]); + } else { + $pregunta = $evaluacion->preguntas()->create([ + 'texto' => $pData['texto'], + 'pregunta_tipo_id' => $pData['pregunta_tipo_id'], + 'requerida' => $pData['requerida'] ?? true, + 'orden' => $orden, + ]); + } + + $tipoSlug = collect($this->preguntaTipos) + ->firstWhere('id', (int) $pData['pregunta_tipo_id'])['slug'] ?? ''; + + if ($tipoSlug === 'opcion_multiple') { + $keptOpcionIds = collect($pData['opciones'] ?? []) + ->pluck('id')->filter()->values()->toArray(); + $pregunta->opciones()->whereNotIn('id', $keptOpcionIds)->delete(); + + foreach ($pData['opciones'] ?? [] as $opOrden => $opData) { + if (!empty($opData['id'])) { + PreguntaOpcion::findOrFail($opData['id'])->update([ + 'texto' => $opData['texto'], + 'orden' => $opOrden, + ]); + } else { + $pregunta->opciones()->create([ + 'texto' => $opData['texto'], + 'orden' => $opOrden, + ]); + } + } + } else { + $pregunta->opciones()->delete(); + } + } + }); + + session()->flash('info', 'Evaluación guardada correctamente.'); + $this->redirect(route('evaluacion.index')); + } + + public function render() + { + return view('livewire.evaluacion-builder'); + } +} diff --git a/app/Models/Evaluacion.php b/app/Models/Evaluacion.php new file mode 100644 index 00000000..1bb4da14 --- /dev/null +++ b/app/Models/Evaluacion.php @@ -0,0 +1,45 @@ + 'boolean', + 'fecha_inicio' => 'date', + 'fecha_fin' => 'date', + ]; + + public function tipo(): BelongsTo + { + return $this->belongsTo(EvaluacionTipo::class, 'evaluacion_tipo_id'); + } + + public function preguntas(): HasMany + { + return $this->hasMany(Pregunta::class)->orderBy('orden'); + } + + public function planteles(): BelongsToMany + { + return $this->belongsToMany(Plantel::class, 'evaluacion_plantel'); + } + + public function niveles(): BelongsToMany + { + return $this->belongsToMany(Nivel::class, 'evaluacion_nivel'); + } + + public function submissions(): HasMany + { + return $this->hasMany(EvaluacionSubmission::class); + } +} diff --git a/app/Models/EvaluacionSubmission.php b/app/Models/EvaluacionSubmission.php new file mode 100644 index 00000000..1d6d1741 --- /dev/null +++ b/app/Models/EvaluacionSubmission.php @@ -0,0 +1,27 @@ +belongsTo(Evaluacion::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function respuestas(): HasMany + { + return $this->hasMany(Respuesta::class, 'submission_id'); + } +} diff --git a/app/Models/EvaluacionTipo.php b/app/Models/EvaluacionTipo.php new file mode 100644 index 00000000..9e014336 --- /dev/null +++ b/app/Models/EvaluacionTipo.php @@ -0,0 +1,17 @@ +hasMany(Evaluacion::class); + } +} diff --git a/app/Models/Nivel.php b/app/Models/Nivel.php index a62e496a..9613e1a1 100644 --- a/app/Models/Nivel.php +++ b/app/Models/Nivel.php @@ -42,4 +42,9 @@ class Nivel extends Model 'documento_tipo_id' ); } + + public function evaluaciones() : BelongsToMany + { + return $this->belongsToMany(Evaluacion::class, 'evaluacion_nivel'); + } } diff --git a/app/Models/Plantel.php b/app/Models/Plantel.php index 0dc645b2..2242090e 100644 --- a/app/Models/Plantel.php +++ b/app/Models/Plantel.php @@ -35,4 +35,9 @@ class Plantel extends Model { return $this->belongsToMany(Turno::class); } + + public function evaluaciones() : BelongsToMany + { + return $this->belongsToMany(Evaluacion::class, 'evaluacion_plantel'); + } } diff --git a/app/Models/Pregunta.php b/app/Models/Pregunta.php new file mode 100644 index 00000000..07533e47 --- /dev/null +++ b/app/Models/Pregunta.php @@ -0,0 +1,32 @@ +belongsTo(Evaluacion::class); + } + + public function tipo(): BelongsTo + { + return $this->belongsTo(PreguntaTipo::class, 'pregunta_tipo_id'); + } + + public function opciones(): HasMany + { + return $this->hasMany(PreguntaOpcion::class)->orderBy('orden'); + } + + public function respuestas(): HasMany + { + return $this->hasMany(Respuesta::class); + } +} diff --git a/app/Models/PreguntaOpcion.php b/app/Models/PreguntaOpcion.php new file mode 100644 index 00000000..b28e011a --- /dev/null +++ b/app/Models/PreguntaOpcion.php @@ -0,0 +1,18 @@ +belongsTo(Pregunta::class); + } +} diff --git a/app/Models/PreguntaTipo.php b/app/Models/PreguntaTipo.php new file mode 100644 index 00000000..627d2f05 --- /dev/null +++ b/app/Models/PreguntaTipo.php @@ -0,0 +1,17 @@ +hasMany(Pregunta::class); + } +} diff --git a/app/Models/Respuesta.php b/app/Models/Respuesta.php new file mode 100644 index 00000000..f281c34e --- /dev/null +++ b/app/Models/Respuesta.php @@ -0,0 +1,26 @@ +belongsTo(EvaluacionSubmission::class); + } + + public function pregunta(): BelongsTo + { + return $this->belongsTo(Pregunta::class); + } + + public function opcion(): BelongsTo + { + return $this->belongsTo(PreguntaOpcion::class, 'opcion_id'); + } +} diff --git a/database/migrations/2026_04_30_121640_create_evaluacion_tipos_table.php b/database/migrations/2026_04_30_121640_create_evaluacion_tipos_table.php new file mode 100644 index 00000000..fe2ec96a --- /dev/null +++ b/database/migrations/2026_04_30_121640_create_evaluacion_tipos_table.php @@ -0,0 +1,21 @@ +id(); + $table->string('nombre'); + $table->string('slug')->unique(); + }); + DB::table('evaluacion_tipos')->insert([ + ['nombre' => 'Docente', 'slug' => 'docente'], + ['nombre' => 'Alumno', 'slug' => 'alumno'], + ['nombre' => 'Personal administrativo', 'slug' => 'administrativo'], + ]); + } + public function down(): void { Schema::dropIfExists('evaluacion_tipos'); } +}; diff --git a/database/migrations/2026_04_30_121641_create_pregunta_tipos_table.php b/database/migrations/2026_04_30_121641_create_pregunta_tipos_table.php new file mode 100644 index 00000000..8afe90aa --- /dev/null +++ b/database/migrations/2026_04_30_121641_create_pregunta_tipos_table.php @@ -0,0 +1,23 @@ +id(); + $table->string('nombre'); + $table->string('slug')->unique(); + }); + DB::table('pregunta_tipos')->insert([ + ['nombre' => 'Texto libre', 'slug' => 'texto_libre'], + ['nombre' => 'Opcion multiple', 'slug' => 'opcion_multiple'], + ['nombre' => 'Escala del 1 al 5', 'slug' => 'escala_5'], + ['nombre' => 'Escala del 1 al 10', 'slug' => 'escala_10'], + ['nombre' => 'Si o No', 'slug' => 'si_no'], + ]); + } + public function down(): void { Schema::dropIfExists('pregunta_tipos'); } +}; diff --git a/database/migrations/2026_04_30_121642_create_evaluaciones_table.php b/database/migrations/2026_04_30_121642_create_evaluaciones_table.php new file mode 100644 index 00000000..2bd3d1b1 --- /dev/null +++ b/database/migrations/2026_04_30_121642_create_evaluaciones_table.php @@ -0,0 +1,22 @@ +id(); + $table->string('titulo'); + $table->text('descripcion')->nullable(); + $table->unsignedBigInteger('evaluacion_tipo_id'); + $table->enum('status', ['borrador', 'activa', 'cerrada'])->default('borrador'); + $table->date('fecha_inicio')->nullable(); + $table->date('fecha_fin')->nullable(); + $table->boolean('anonima')->default(false); + $table->timestamps(); + $table->foreign('evaluacion_tipo_id')->references('id')->on('evaluacion_tipos'); + }); + } + public function down(): void { Schema::dropIfExists('evaluaciones'); } +}; diff --git a/database/migrations/2026_04_30_121643_create_preguntas_table.php b/database/migrations/2026_04_30_121643_create_preguntas_table.php new file mode 100644 index 00000000..71ee7957 --- /dev/null +++ b/database/migrations/2026_04_30_121643_create_preguntas_table.php @@ -0,0 +1,21 @@ +id(); + $table->unsignedBigInteger('evaluacion_id'); + $table->unsignedBigInteger('pregunta_tipo_id'); + $table->text('texto'); + $table->unsignedSmallInteger('orden')->default(0); + $table->boolean('requerida')->default(true); + $table->timestamps(); + $table->foreign('evaluacion_id')->references('id')->on('evaluaciones')->onDelete('cascade'); + $table->foreign('pregunta_tipo_id')->references('id')->on('pregunta_tipos'); + }); + } + public function down(): void { Schema::dropIfExists('preguntas'); } +}; diff --git a/database/migrations/2026_04_30_121644_create_pregunta_opciones_table.php b/database/migrations/2026_04_30_121644_create_pregunta_opciones_table.php new file mode 100644 index 00000000..4c19556e --- /dev/null +++ b/database/migrations/2026_04_30_121644_create_pregunta_opciones_table.php @@ -0,0 +1,17 @@ +id(); + $table->unsignedBigInteger('pregunta_id'); + $table->string('texto'); + $table->unsignedSmallInteger('orden')->default(0); + $table->foreign('pregunta_id')->references('id')->on('preguntas')->onDelete('cascade'); + }); + } + public function down(): void { Schema::dropIfExists('pregunta_opciones'); } +}; diff --git a/database/migrations/2026_04_30_121645_create_evaluacion_submissions_table.php b/database/migrations/2026_04_30_121645_create_evaluacion_submissions_table.php new file mode 100644 index 00000000..14bd5e1e --- /dev/null +++ b/database/migrations/2026_04_30_121645_create_evaluacion_submissions_table.php @@ -0,0 +1,19 @@ +id(); + $table->unsignedBigInteger('evaluacion_id'); + $table->unsignedBigInteger('user_id'); + $table->timestamps(); + $table->unique(['evaluacion_id', 'user_id']); + $table->foreign('evaluacion_id')->references('id')->on('evaluaciones')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + public function down(): void { Schema::dropIfExists('evaluacion_submissions'); } +}; diff --git a/database/migrations/2026_04_30_121645_create_respuestas_table.php b/database/migrations/2026_04_30_121645_create_respuestas_table.php new file mode 100644 index 00000000..b17174c7 --- /dev/null +++ b/database/migrations/2026_04_30_121645_create_respuestas_table.php @@ -0,0 +1,21 @@ +id(); + $table->unsignedBigInteger('submission_id'); + $table->unsignedBigInteger('pregunta_id'); + $table->text('valor')->nullable(); + $table->unsignedBigInteger('opcion_id')->nullable(); + $table->timestamps(); + $table->foreign('submission_id')->references('id')->on('evaluacion_submissions')->onDelete('cascade'); + $table->foreign('pregunta_id')->references('id')->on('preguntas')->onDelete('cascade'); + $table->foreign('opcion_id')->references('id')->on('pregunta_opciones')->nullOnDelete(); + }); + } + public function down(): void { Schema::dropIfExists('respuestas'); } +}; diff --git a/database/migrations/2026_04_30_121648_create_evaluacion_plantel_table.php b/database/migrations/2026_04_30_121648_create_evaluacion_plantel_table.php new file mode 100644 index 00000000..963781fc --- /dev/null +++ b/database/migrations/2026_04_30_121648_create_evaluacion_plantel_table.php @@ -0,0 +1,18 @@ +unsignedBigInteger('evaluacion_id'); + $table->unsignedBigInteger('plantel_id'); + $table->primary(['evaluacion_id', 'plantel_id']); + $table->foreign('evaluacion_id')->references('id')->on('evaluaciones')->onDelete('cascade'); + $table->foreign('plantel_id')->references('id')->on('plantels'); + }); + } + public function down(): void { Schema::dropIfExists('evaluacion_plantel'); } +}; diff --git a/database/migrations/2026_04_30_121649_create_evaluacion_nivel_table.php b/database/migrations/2026_04_30_121649_create_evaluacion_nivel_table.php new file mode 100644 index 00000000..a358d80b --- /dev/null +++ b/database/migrations/2026_04_30_121649_create_evaluacion_nivel_table.php @@ -0,0 +1,18 @@ +unsignedBigInteger('evaluacion_id'); + $table->unsignedBigInteger('nivel_id'); + $table->primary(['evaluacion_id', 'nivel_id']); + $table->foreign('evaluacion_id')->references('id')->on('evaluaciones')->onDelete('cascade'); + $table->foreign('nivel_id')->references('id')->on('nivels'); + }); + } + public function down(): void { Schema::dropIfExists('evaluacion_nivel'); } +}; diff --git a/resources/views/evaluacion/create.blade.php b/resources/views/evaluacion/create.blade.php new file mode 100644 index 00000000..e37f5369 --- /dev/null +++ b/resources/views/evaluacion/create.blade.php @@ -0,0 +1,21 @@ +@extends('layouts.landing') + +@section('title', isset($evaluacion) ? 'Editar Evaluación' : 'Nueva Evaluación') + +@section('content') +
| Título | +Dirigida a | +Estado | +Preguntas | +Respuestas | +Vigencia | +Acciones | +
|---|---|---|---|---|---|---|
| + {{ $evaluacion->titulo }} + @if($evaluacion->anonima) + + + + @endif + | ++ @php $slug = $evaluacion->tipo?->slug; @endphp + @if($slug === 'docente') + Docentes + @elseif($slug === 'alumno') + Alumnos + @elseif($slug === 'administrativo') + Administrativos + @else + — + @endif + | ++ @if($evaluacion->status === 'activa') + Activa + @elseif($evaluacion->status === 'borrador') + Borrador + @else + Cerrada + @endif + | +{{ $evaluacion->preguntas_count }} | +{{ $evaluacion->submissions_count }} | ++ @if($evaluacion->fecha_inicio || $evaluacion->fecha_fin) + + {{ $evaluacion->fecha_inicio?->format('d/m/Y') ?? '—' }} + → + {{ $evaluacion->fecha_fin?->format('d/m/Y') ?? '—' }} + + @else + Sin fecha límite + @endif + | ++ + + + + + + + | +
| + + No hay evaluaciones registradas. + | +||||||
{{ $evaluacion->descripcion }}
+{{ $pregunta->texto }}
+ {{ $pregunta->tipo?->nombre }} +Esta evaluación no tiene preguntas.
+ @endforelse ++ + Agrega al menos una opción de respuesta. +
+ @endif +No hay preguntas todavía.
+Haz clic en "Agregar pregunta" para comenzar a construir la evaluación.
+No hay planteles registrados.
+ @endforelse +No hay niveles registrados.
+ @endforelse +