2af36ea272
- Modelos completos: Evaluacion, EvaluacionTipo, Pregunta, PreguntaTipo,
PreguntaOpcion, EvaluacionSubmission, Respuesta (con $table explícito
para nombres en español que Laravel pluraliza incorrectamente)
- Relaciones evaluaciones() añadidas a Plantel y Nivel
- EvaluacionBuilder (Livewire): constructor interactivo con preguntas
de tipo texto libre, opción múltiple, escala 1-5/1-10 y sí/no;
reorden con flechas, vista previa por tipo, selección de planteles
y niveles, configuración de estado, fechas y flag de anónima
- EvaluacionController: CRUD completo + toggleStatus
- Vistas: index, create/edit (con Livewire builder), show con detalle
de preguntas y cambio de estado
- Rutas resource /evaluacion + PATCH /evaluacion/{id}/status
- Migraciones: evaluaciones, preguntas, pregunta_opciones,
evaluacion_submissions, respuestas, evaluacion_plantel, evaluacion_nivel
- Menú: enlace Evaluaciones en panel de Administrador
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
214 lines
9.0 KiB
PHP
214 lines
9.0 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire;
|
|
|
|
use App\Models\Evaluacion;
|
|
use App\Models\EvaluacionTipo;
|
|
use App\Models\Nivel;
|
|
use App\Models\Plantel;
|
|
use App\Models\Pregunta;
|
|
use App\Models\PreguntaOpcion;
|
|
use App\Models\PreguntaTipo;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Livewire\Component;
|
|
|
|
class EvaluacionBuilder extends Component
|
|
{
|
|
public ?int $evaluacionId = null;
|
|
|
|
public string $titulo = '';
|
|
public string $descripcion = '';
|
|
public string $evaluacionTipoId = '';
|
|
public string $status = 'borrador';
|
|
public string $fechaInicio = '';
|
|
public string $fechaFin = '';
|
|
public bool $anonima = false;
|
|
|
|
public array $selectedPlanteles = [];
|
|
public array $selectedNiveles = [];
|
|
public array $preguntas = [];
|
|
|
|
// Catálogos cargados en mount (solo lectura en la vista)
|
|
public array $tipos = [];
|
|
public array $preguntaTipos = [];
|
|
public array $planteles = [];
|
|
public array $niveles = [];
|
|
|
|
public function mount($evaluacion = null): void
|
|
{
|
|
$this->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');
|
|
}
|
|
}
|