123456
parent
e84ec16504
commit
71182fd292
@ -0,0 +1,199 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Administracion;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\ProcesoAdmision;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ProcesoAdmisionController extends Controller
|
||||||
|
{
|
||||||
|
// GET /api/admin/procesos-admision?include_detalles=1&q=...&estado=...&publicado=1&page=1&per_page=15
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$q = ProcesoAdmision::query();
|
||||||
|
|
||||||
|
if ($request->filled('q')) {
|
||||||
|
$term = $request->string('q');
|
||||||
|
$q->where(function ($sub) use ($term) {
|
||||||
|
$sub->where('titulo', 'like', "%{$term}%")
|
||||||
|
->orWhere('slug', 'like', "%{$term}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('estado')) {
|
||||||
|
$q->where('estado', $request->string('estado'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('publicado')) {
|
||||||
|
$q->where('publicado', (bool) $request->boolean('publicado'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->boolean('include_detalles')) {
|
||||||
|
$q->with(['detalles' => fn($d) => $d->orderBy('id', 'desc')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$q->orderByDesc('id');
|
||||||
|
|
||||||
|
$perPage = (int) $request->input('per_page', 15);
|
||||||
|
|
||||||
|
return response()->json($q->paginate($perPage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/admin/procesos-admision/{id}?include_detalles=1
|
||||||
|
public function show(Request $request, $id)
|
||||||
|
{
|
||||||
|
$q = ProcesoAdmision::query();
|
||||||
|
|
||||||
|
if ($request->boolean('include_detalles')) {
|
||||||
|
$q->with('detalles');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($q->findOrFail($id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/admin/procesos-admision (multipart/form-data)
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'titulo' => ['required','string','max:255'],
|
||||||
|
'subtitulo' => ['nullable','string','max:255'],
|
||||||
|
'descripcion' => ['nullable','string'],
|
||||||
|
'slug' => ['nullable','string','max:120', 'unique:procesos_admision,slug'],
|
||||||
|
'tipo_proceso' => ['nullable','string','max:60'],
|
||||||
|
'modalidad' => ['nullable','string','max:50'],
|
||||||
|
|
||||||
|
'publicado' => ['sometimes','boolean'],
|
||||||
|
'fecha_publicacion' => ['nullable','date'],
|
||||||
|
|
||||||
|
'fecha_inicio_preinscripcion' => ['nullable','date'],
|
||||||
|
'fecha_fin_preinscripcion' => ['nullable','date','after_or_equal:fecha_inicio_preinscripcion'],
|
||||||
|
'fecha_inicio_inscripcion' => ['nullable','date'],
|
||||||
|
'fecha_fin_inscripcion' => ['nullable','date','after_or_equal:fecha_inicio_inscripcion'],
|
||||||
|
|
||||||
|
'fecha_examen1' => ['nullable','date'],
|
||||||
|
'fecha_examen2' => ['nullable','date','after_or_equal:fecha_examen1'],
|
||||||
|
'fecha_resultados' => ['nullable','date'],
|
||||||
|
|
||||||
|
'fecha_inicio_biometrico' => ['nullable','date'],
|
||||||
|
'fecha_fin_biometrico' => ['nullable','date','after_or_equal:fecha_inicio_biometrico'],
|
||||||
|
|
||||||
|
'link_preinscripcion' => ['nullable','string','max:500'],
|
||||||
|
'link_inscripcion' => ['nullable','string','max:500'],
|
||||||
|
'link_resultados' => ['nullable','string','max:500'],
|
||||||
|
'link_reglamento' => ['nullable','string','max:500'],
|
||||||
|
|
||||||
|
'estado' => ['sometimes', Rule::in(['nuevo','publicado','en_proceso','finalizado','cancelado'])],
|
||||||
|
|
||||||
|
// Archivos (uploads)
|
||||||
|
'imagen' => ['nullable','image','mimes:jpg,jpeg,png,webp','max:10240'],
|
||||||
|
'banner' => ['nullable','image','mimes:jpg,jpeg,png,webp','max:10240'],
|
||||||
|
'brochure' => ['nullable','file','mimes:pdf','max:10240'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// slug auto si no viene
|
||||||
|
if (empty($data['slug'])) {
|
||||||
|
$data['slug'] = Str::slug($data['titulo']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['publicado'] = $data['publicado'] ?? false;
|
||||||
|
|
||||||
|
// Guardar archivos
|
||||||
|
if ($request->hasFile('imagen')) {
|
||||||
|
$data['imagen_path'] = $request->file('imagen')->store('admisiones/procesos', 'public');
|
||||||
|
}
|
||||||
|
if ($request->hasFile('banner')) {
|
||||||
|
$data['banner_path'] = $request->file('banner')->store('admisiones/procesos', 'public');
|
||||||
|
}
|
||||||
|
if ($request->hasFile('brochure')) {
|
||||||
|
$data['brochure_path'] = $request->file('brochure')->store('admisiones/procesos', 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
$proceso = ProcesoAdmision::create($data);
|
||||||
|
|
||||||
|
return response()->json($proceso, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH /api/admin/procesos-admision/{id} (multipart/form-data)
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
$proceso = ProcesoAdmision::findOrFail($id);
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'titulo' => ['sometimes','string','max:255'],
|
||||||
|
'subtitulo' => ['sometimes','nullable','string','max:255'],
|
||||||
|
'descripcion' => ['sometimes','nullable','string'],
|
||||||
|
'slug' => ['sometimes','nullable','string','max:120', Rule::unique('procesos_admision','slug')->ignore($proceso->id)],
|
||||||
|
'tipo_proceso' => ['sometimes','nullable','string','max:60'],
|
||||||
|
'modalidad' => ['sometimes','nullable','string','max:50'],
|
||||||
|
|
||||||
|
'publicado' => ['sometimes','boolean'],
|
||||||
|
'fecha_publicacion' => ['sometimes','nullable','date'],
|
||||||
|
|
||||||
|
'fecha_inicio_preinscripcion' => ['sometimes','nullable','date'],
|
||||||
|
'fecha_fin_preinscripcion' => ['sometimes','nullable','date','after_or_equal:fecha_inicio_preinscripcion'],
|
||||||
|
'fecha_inicio_inscripcion' => ['sometimes','nullable','date'],
|
||||||
|
'fecha_fin_inscripcion' => ['sometimes','nullable','date','after_or_equal:fecha_inicio_inscripcion'],
|
||||||
|
|
||||||
|
'fecha_examen1' => ['sometimes','nullable','date'],
|
||||||
|
'fecha_examen2' => ['sometimes','nullable','date','after_or_equal:fecha_examen1'],
|
||||||
|
'fecha_resultados' => ['sometimes','nullable','date'],
|
||||||
|
|
||||||
|
'fecha_inicio_biometrico' => ['sometimes','nullable','date'],
|
||||||
|
'fecha_fin_biometrico' => ['sometimes','nullable','date','after_or_equal:fecha_inicio_biometrico'],
|
||||||
|
|
||||||
|
'link_preinscripcion' => ['sometimes','nullable','string','max:500'],
|
||||||
|
'link_inscripcion' => ['sometimes','nullable','string','max:500'],
|
||||||
|
'link_resultados' => ['sometimes','nullable','string','max:500'],
|
||||||
|
'link_reglamento' => ['sometimes','nullable','string','max:500'],
|
||||||
|
|
||||||
|
'estado' => ['sometimes', Rule::in(['nuevo','publicado','en_proceso','finalizado','cancelado'])],
|
||||||
|
|
||||||
|
// Archivos
|
||||||
|
'imagen' => ['sometimes','nullable','image','mimes:jpg,jpeg,png,webp','max:10240'],
|
||||||
|
'banner' => ['sometimes','nullable','image','mimes:jpg,jpeg,png,webp','max:10240'],
|
||||||
|
'brochure' => ['sometimes','nullable','file','mimes:pdf','max:10240'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// slug auto si lo mandan vacío
|
||||||
|
if (array_key_exists('slug', $data) && empty($data['slug'])) {
|
||||||
|
$data['slug'] = Str::slug($data['titulo'] ?? $proceso->titulo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reemplazo de archivos (borra anterior)
|
||||||
|
if ($request->hasFile('imagen')) {
|
||||||
|
if ($proceso->imagen_path) Storage::disk('public')->delete($proceso->imagen_path);
|
||||||
|
$data['imagen_path'] = $request->file('imagen')->store('admisiones/procesos', 'public');
|
||||||
|
}
|
||||||
|
if ($request->hasFile('banner')) {
|
||||||
|
if ($proceso->banner_path) Storage::disk('public')->delete($proceso->banner_path);
|
||||||
|
$data['banner_path'] = $request->file('banner')->store('admisiones/procesos', 'public');
|
||||||
|
}
|
||||||
|
if ($request->hasFile('brochure')) {
|
||||||
|
if ($proceso->brochure_path) Storage::disk('public')->delete($proceso->brochure_path);
|
||||||
|
$data['brochure_path'] = $request->file('brochure')->store('admisiones/procesos', 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
$proceso->update($data);
|
||||||
|
|
||||||
|
return response()->json($proceso->fresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/admin/procesos-admision/{id}
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$proceso = ProcesoAdmision::findOrFail($id);
|
||||||
|
|
||||||
|
// Borrar archivos asociados
|
||||||
|
if ($proceso->imagen_path) Storage::disk('public')->delete($proceso->imagen_path);
|
||||||
|
if ($proceso->banner_path) Storage::disk('public')->delete($proceso->banner_path);
|
||||||
|
if ($proceso->brochure_path) Storage::disk('public')->delete($proceso->brochure_path);
|
||||||
|
|
||||||
|
$proceso->delete();
|
||||||
|
|
||||||
|
return response()->json(['message' => 'Proceso eliminado']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Administracion;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\ProcesoAdmision;
|
||||||
|
use App\Models\ProcesoAdmisionDetalle;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ProcesoAdmisionDetalleController extends Controller
|
||||||
|
{
|
||||||
|
// GET /api/admin/procesos-admision/{procesoId}/detalles?tipo=requisitos
|
||||||
|
public function index(Request $request, $procesoId)
|
||||||
|
{
|
||||||
|
ProcesoAdmision::findOrFail($procesoId);
|
||||||
|
|
||||||
|
$q = ProcesoAdmisionDetalle::query()->where('proceso_admision_id', $procesoId);
|
||||||
|
|
||||||
|
if ($request->filled('tipo')) {
|
||||||
|
$q->where('tipo', $request->string('tipo'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($q->orderByDesc('id')->get());
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/admin/procesos-admision/{procesoId}/detalles (multipart/form-data)
|
||||||
|
public function store(Request $request, $procesoId)
|
||||||
|
{
|
||||||
|
ProcesoAdmision::findOrFail($procesoId);
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'tipo' => ['required', Rule::in(['requisitos','pagos','vacantes','cronograma'])],
|
||||||
|
'titulo_detalle' => ['required','string','max:255'],
|
||||||
|
'descripcion' => ['nullable','string'],
|
||||||
|
|
||||||
|
'listas' => ['nullable','array'],
|
||||||
|
'meta' => ['nullable','array'],
|
||||||
|
|
||||||
|
'url' => ['nullable','string','max:500'],
|
||||||
|
|
||||||
|
'imagen' => ['nullable','image','mimes:jpg,jpeg,png,webp','max:10240'],
|
||||||
|
'imagen_2' => ['nullable','image','mimes:jpg,jpeg,png,webp','max:10240'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data['proceso_admision_id'] = (int) $procesoId;
|
||||||
|
|
||||||
|
if ($request->hasFile('imagen')) {
|
||||||
|
$data['imagen_path'] = $request->file('imagen')->store('admisiones/detalles', 'public');
|
||||||
|
}
|
||||||
|
if ($request->hasFile('imagen_2')) {
|
||||||
|
$data['imagen_path_2'] = $request->file('imagen_2')->store('admisiones/detalles', 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
$detalle = ProcesoAdmisionDetalle::create($data);
|
||||||
|
|
||||||
|
return response()->json($detalle, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/admin/detalles-admision/{id}
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
return response()->json(ProcesoAdmisionDetalle::findOrFail($id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH /api/admin/detalles-admision/{id} (multipart/form-data)
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
$detalle = ProcesoAdmisionDetalle::findOrFail($id);
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'tipo' => ['sometimes', Rule::in(['requisitos','pagos','vacantes','cronograma'])],
|
||||||
|
'titulo_detalle' => ['sometimes','string','max:255'],
|
||||||
|
'descripcion' => ['sometimes','nullable','string'],
|
||||||
|
|
||||||
|
'listas' => ['sometimes','nullable','array'],
|
||||||
|
'meta' => ['sometimes','nullable','array'],
|
||||||
|
|
||||||
|
'url' => ['sometimes','nullable','string','max:500'],
|
||||||
|
|
||||||
|
'imagen' => ['sometimes','nullable','image','mimes:jpg,jpeg,png,webp','max:10240'],
|
||||||
|
'imagen_2' => ['sometimes','nullable','image','mimes:jpg,jpeg,png,webp','max:10240'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->hasFile('imagen')) {
|
||||||
|
if ($detalle->imagen_path) Storage::disk('public')->delete($detalle->imagen_path);
|
||||||
|
$data['imagen_path'] = $request->file('imagen')->store('admisiones/detalles', 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->hasFile('imagen_2')) {
|
||||||
|
if ($detalle->imagen_path_2) Storage::disk('public')->delete($detalle->imagen_path_2);
|
||||||
|
$data['imagen_path_2'] = $request->file('imagen_2')->store('admisiones/detalles', 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
$detalle->update($data);
|
||||||
|
|
||||||
|
return response()->json($detalle->fresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/admin/detalles-admision/{id}
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$detalle = ProcesoAdmisionDetalle::findOrFail($id);
|
||||||
|
|
||||||
|
if ($detalle->imagen_path) Storage::disk('public')->delete($detalle->imagen_path);
|
||||||
|
if ($detalle->imagen_path_2) Storage::disk('public')->delete($detalle->imagen_path_2);
|
||||||
|
|
||||||
|
$detalle->delete();
|
||||||
|
|
||||||
|
return response()->json(['message' => 'Detalle eliminado']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class ProcesoAdmisionDetalle extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'proceso_admision_detalles';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'proceso_admision_id',
|
||||||
|
'tipo',
|
||||||
|
'titulo_detalle',
|
||||||
|
'descripcion',
|
||||||
|
'listas',
|
||||||
|
'meta',
|
||||||
|
'url',
|
||||||
|
'imagen_path',
|
||||||
|
'imagen_path_2',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'listas' => 'array',
|
||||||
|
'meta' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $appends = ['imagen_url','imagen_url_2'];
|
||||||
|
|
||||||
|
public function proceso(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ProcesoAdmision::class, 'proceso_admision_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getImagenUrlAttribute(): ?string
|
||||||
|
{
|
||||||
|
return $this->imagen_path ? Storage::disk('public')->url($this->imagen_path) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getImagenUrl2Attribute(): ?string
|
||||||
|
{
|
||||||
|
return $this->imagen_path_2 ? Storage::disk('public')->url($this->imagen_path_2) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,276 @@
|
|||||||
|
// src/store/procesoAdmision.store.js
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import api from '../axios'
|
||||||
|
|
||||||
|
export const useProcesoAdmisionStore = defineStore('procesoAdmision', {
|
||||||
|
state: () => ({
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
|
||||||
|
// listado
|
||||||
|
procesos: [],
|
||||||
|
pagination: {
|
||||||
|
current_page: 1,
|
||||||
|
per_page: 15,
|
||||||
|
total: 0,
|
||||||
|
last_page: 1
|
||||||
|
},
|
||||||
|
|
||||||
|
// detalle
|
||||||
|
procesoActual: null,
|
||||||
|
|
||||||
|
// detalles del proceso actual
|
||||||
|
detalles: []
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
_setError(err) {
|
||||||
|
this.error =
|
||||||
|
err?.response?.data?.message ||
|
||||||
|
err?.response?.data?.error ||
|
||||||
|
err?.message ||
|
||||||
|
'Ocurrió un error'
|
||||||
|
},
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// PROCESOS
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
// GET /api/admin/procesos-admision
|
||||||
|
async fetchProcesos(params = {}) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
const { data } = await api.get('/admin/procesos-admision', {
|
||||||
|
params: {
|
||||||
|
per_page: this.pagination.per_page,
|
||||||
|
...params
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Laravel paginate
|
||||||
|
this.procesos = data.data ?? []
|
||||||
|
this.pagination.current_page = data.current_page ?? 1
|
||||||
|
this.pagination.per_page = data.per_page ?? this.pagination.per_page
|
||||||
|
this.pagination.total = data.total ?? this.procesos.length
|
||||||
|
this.pagination.last_page = data.last_page ?? 1
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
this._setError(err)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// GET /api/admin/procesos-admision/{id}?include_detalles=1
|
||||||
|
async fetchProceso(id, { include_detalles = true } = {}) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
const { data } = await api.get(`/admin/procesos-admision/${id}`, {
|
||||||
|
params: { include_detalles: include_detalles ? 1 : 0 }
|
||||||
|
})
|
||||||
|
|
||||||
|
this.procesoActual = data
|
||||||
|
this.detalles = Array.isArray(data.detalles) ? data.detalles : []
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
this._setError(err)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// POST /api/admin/procesos-admision (multipart/form-data)
|
||||||
|
async createProceso(payload) {
|
||||||
|
// payload puede ser objeto normal o FormData
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
const isFormData = payload instanceof FormData
|
||||||
|
|
||||||
|
await api.post('/admin/procesos-admision', payload, {
|
||||||
|
headers: isFormData ? { 'Content-Type': 'multipart/form-data' } : undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// refrescar lista
|
||||||
|
await this.fetchProcesos({ page: 1 })
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
this._setError(err)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// PATCH /api/admin/procesos-admision/{id} (multipart/form-data)
|
||||||
|
async updateProceso(id, payload, { method = 'patch' } = {}) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
const isFormData = payload instanceof FormData
|
||||||
|
|
||||||
|
const { data } = await api.request({
|
||||||
|
url: `/admin/procesos-admision/${id}`,
|
||||||
|
method,
|
||||||
|
data: payload,
|
||||||
|
headers: isFormData ? { 'Content-Type': 'multipart/form-data' } : undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// actualizar cache local
|
||||||
|
if (this.procesoActual?.id === id) {
|
||||||
|
this.procesoActual = data
|
||||||
|
}
|
||||||
|
|
||||||
|
// refrescar listado (opcional, pero recomendado)
|
||||||
|
await this.fetchProcesos({
|
||||||
|
page: this.pagination.current_page,
|
||||||
|
per_page: this.pagination.per_page
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
this._setError(err)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// DELETE /api/admin/procesos-admision/{id}
|
||||||
|
async deleteProceso(id) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
await api.delete(`/admin/procesos-admision/${id}`)
|
||||||
|
|
||||||
|
// limpiar caches
|
||||||
|
this.procesos = this.procesos.filter(p => p.id !== id)
|
||||||
|
if (this.procesoActual?.id === id) {
|
||||||
|
this.procesoActual = null
|
||||||
|
this.detalles = []
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
this._setError(err)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// DETALLES (POR PROCESO)
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
// GET /api/admin/procesos-admision/{procesoId}/detalles?tipo=requisitos
|
||||||
|
async fetchDetalles(procesoId, params = {}) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
const { data } = await api.get(`/admin/procesos-admision/${procesoId}/detalles`, {
|
||||||
|
params
|
||||||
|
})
|
||||||
|
|
||||||
|
this.detalles = Array.isArray(data) ? data : []
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
this._setError(err)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// POST /api/admin/procesos-admision/{procesoId}/detalles (multipart/form-data)
|
||||||
|
async createDetalle(procesoId, payload) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
const isFormData = payload instanceof FormData
|
||||||
|
|
||||||
|
await api.post(`/admin/procesos-admision/${procesoId}/detalles`, payload, {
|
||||||
|
headers: isFormData ? { 'Content-Type': 'multipart/form-data' } : undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.fetchDetalles(procesoId)
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
this._setError(err)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// GET /api/admin/detalles-admision/{id}
|
||||||
|
async fetchDetalleById(detalleId) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
const { data } = await api.get(`/admin/detalles-admision/${detalleId}`)
|
||||||
|
return data
|
||||||
|
} catch (err) {
|
||||||
|
this._setError(err)
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// PATCH /api/admin/detalles-admision/{id} (multipart/form-data)
|
||||||
|
async updateDetalle(detalleId, payload, { method = 'patch' } = {}) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
const isFormData = payload instanceof FormData
|
||||||
|
|
||||||
|
const { data } = await api.request({
|
||||||
|
url: `/admin/detalles-admision/${detalleId}`,
|
||||||
|
method,
|
||||||
|
data: payload,
|
||||||
|
headers: isFormData ? { 'Content-Type': 'multipart/form-data' } : undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// actualizar cache local si existe
|
||||||
|
const idx = this.detalles.findIndex(d => d.id === data.id)
|
||||||
|
if (idx >= 0) this.detalles[idx] = data
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
this._setError(err)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// DELETE /api/admin/detalles-admision/{id}
|
||||||
|
async deleteDetalle(detalleId, procesoId = null) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
await api.delete(`/admin/detalles-admision/${detalleId}`)
|
||||||
|
|
||||||
|
this.detalles = this.detalles.filter(d => d.id !== detalleId)
|
||||||
|
|
||||||
|
// si me pasas procesoId, refresca desde backend
|
||||||
|
if (procesoId) await this.fetchDetalles(procesoId)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
this._setError(err)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -0,0 +1,357 @@
|
|||||||
|
<template>
|
||||||
|
<div class="cursos-container">
|
||||||
|
<div class="cursos-header">
|
||||||
|
<div class="header-title">
|
||||||
|
<h2>Detalles del Proceso</h2>
|
||||||
|
<p class="subtitle">Proceso: {{ store.procesoActual?.titulo || '-' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-space>
|
||||||
|
<a-button @click="goBack"><ArrowLeftOutlined /> Volver</a-button>
|
||||||
|
<a-button type="primary" @click="openCreateModal"><PlusOutlined /> Nuevo Detalle</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filters-section">
|
||||||
|
<a-select v-model:value="tipoFilter" placeholder="Tipo" style="width: 240px" size="large" @change="fetchDetalles">
|
||||||
|
<a-select-option :value="null">Todos</a-select-option>
|
||||||
|
<a-select-option value="requisitos">requisitos</a-select-option>
|
||||||
|
<a-select-option value="pagos">pagos</a-select-option>
|
||||||
|
<a-select-option value="vacantes">vacantes</a-select-option>
|
||||||
|
<a-select-option value="cronograma">cronograma</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
|
||||||
|
<a-button @click="clearFilters" size="large">
|
||||||
|
<ReloadOutlined /> Limpiar
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="store.loading && store.detalles.length === 0" class="loading-state">
|
||||||
|
<a-spin size="large" />
|
||||||
|
<p>Cargando detalles...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="store.detalles.length === 0" class="empty-state">
|
||||||
|
<a-empty description="No hay detalles" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="cursos-table-container">
|
||||||
|
<a-table :data-source="store.detalles" :columns="columns" :loading="store.loading" row-key="id" class="cursos-table">
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'imagenes'">
|
||||||
|
<a-space>
|
||||||
|
<a v-if="record.imagen_url" :href="record.imagen_url" target="_blank">Imagen 1</a>
|
||||||
|
<a v-if="record.imagen_url_2" :href="record.imagen_url_2" target="_blank">Imagen 2</a>
|
||||||
|
<span v-if="!record.imagen_url && !record.imagen_url_2">-</span>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="column.key === 'acciones'">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="link" size="small" @click="editDetalle(record)"><EditOutlined /> Editar</a-button>
|
||||||
|
<a-button type="link" size="small" danger @click="confirmDelete(record)"><DeleteOutlined /> Eliminar</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Crear/Editar -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="modalVisible"
|
||||||
|
:title="isEditing ? 'Editar Detalle' : 'Nuevo Detalle'"
|
||||||
|
:confirm-loading="store.loading"
|
||||||
|
@ok="handleModalOk"
|
||||||
|
@cancel="handleModalCancel"
|
||||||
|
width="920px"
|
||||||
|
class="curso-modal"
|
||||||
|
>
|
||||||
|
<a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical" @finish="onFormSubmit">
|
||||||
|
<div class="grid2">
|
||||||
|
<a-form-item label="Tipo" name="tipo">
|
||||||
|
<a-select v-model:value="formState.tipo" size="large" :disabled="isEditing">
|
||||||
|
<a-select-option value="requisitos">requisitos</a-select-option>
|
||||||
|
<a-select-option value="pagos">pagos</a-select-option>
|
||||||
|
<a-select-option value="vacantes">vacantes</a-select-option>
|
||||||
|
<a-select-option value="cronograma">cronograma</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Título" name="titulo_detalle">
|
||||||
|
<a-input v-model:value="formState.titulo_detalle" size="large" />
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-form-item label="Descripción" name="descripcion">
|
||||||
|
<a-textarea v-model:value="formState.descripcion" :rows="3" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<div class="grid2">
|
||||||
|
<a-form-item label="URL (opcional)" name="url">
|
||||||
|
<a-input v-model:value="formState.url" placeholder="https://..." size="large" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Listas (JSON)" name="listas_json">
|
||||||
|
<a-textarea v-model:value="formState.listas_json" :rows="8" placeholder='Ej: [{"titulo":"DNI","descripcion":"..."}]' />
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-form-item label="Meta (JSON)" name="meta_json">
|
||||||
|
<a-textarea v-model:value="formState.meta_json" :rows="6" placeholder='Ej: {"moneda":"PEN"}' />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-divider>Imágenes (SUBIR)</a-divider>
|
||||||
|
|
||||||
|
<div class="grid2">
|
||||||
|
<a-form-item label="Imagen 1">
|
||||||
|
<a-upload v-model:file-list="fileImg1" :before-upload="() => false" :max-count="1" accept="image/*">
|
||||||
|
<a-button>Seleccionar</a-button>
|
||||||
|
</a-upload>
|
||||||
|
<small class="hint" v-if="isEditing && formState.imagen_url">
|
||||||
|
Actual: <a :href="formState.imagen_url" target="_blank">ver</a>
|
||||||
|
</small>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Imagen 2">
|
||||||
|
<a-upload v-model:file-list="fileImg2" :before-upload="() => false" :max-count="1" accept="image/*">
|
||||||
|
<a-button>Seleccionar</a-button>
|
||||||
|
</a-upload>
|
||||||
|
<small class="hint" v-if="isEditing && formState.imagen_url_2">
|
||||||
|
Actual: <a :href="formState.imagen_url_2" target="_blank">ver</a>
|
||||||
|
</small>
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-footer" v-if="store.error">
|
||||||
|
<a-alert type="error" :message="store.error" show-icon class="error-alert" />
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- Delete modal -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="deleteModalVisible"
|
||||||
|
title="Confirmar Eliminación"
|
||||||
|
@ok="handleDelete"
|
||||||
|
@cancel="deleteModalVisible = false"
|
||||||
|
ok-text="Eliminar"
|
||||||
|
cancel-text="Cancelar"
|
||||||
|
ok-type="danger"
|
||||||
|
width="420px"
|
||||||
|
>
|
||||||
|
<a-alert message="¿Eliminar este detalle?" type="warning" show-icon />
|
||||||
|
<div class="curso-info" v-if="toDelete" style="margin-top:12px;">
|
||||||
|
<p><strong>Tipo:</strong> {{ toDelete.tipo }}</p>
|
||||||
|
<p><strong>Título:</strong> {{ toDelete.titulo_detalle }}</p>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined, ArrowLeftOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { useProcesoAdmisionStore } from '../../../store/procesosAdmisionStore'
|
||||||
|
|
||||||
|
const store = useProcesoAdmisionStore()
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const procesoId = computed(() => Number(route.params.id))
|
||||||
|
const tipoFilter = ref(null)
|
||||||
|
|
||||||
|
const modalVisible = ref(false)
|
||||||
|
const deleteModalVisible = ref(false)
|
||||||
|
const isEditing = ref(false)
|
||||||
|
const toDelete = ref(null)
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
const fileImg1 = ref([])
|
||||||
|
const fileImg2 = ref([])
|
||||||
|
|
||||||
|
const formState = reactive({
|
||||||
|
id: null,
|
||||||
|
tipo: 'requisitos',
|
||||||
|
titulo_detalle: '',
|
||||||
|
descripcion: '',
|
||||||
|
url: '',
|
||||||
|
listas_json: '',
|
||||||
|
meta_json: '',
|
||||||
|
imagen_url: null,
|
||||||
|
imagen_url_2: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
tipo: [{ required: true, message: 'Selecciona tipo', trigger: 'change' }],
|
||||||
|
titulo_detalle: [{ required: true, message: 'Ingresa título', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: 'ID', dataIndex: 'id', key: 'id', width: 70 },
|
||||||
|
{ title: 'Tipo', dataIndex: 'tipo', key: 'tipo', width: 130 },
|
||||||
|
{ title: 'Título', dataIndex: 'titulo_detalle', key: 'titulo_detalle' },
|
||||||
|
{ title: 'Imágenes', key: 'imagenes', width: 180 },
|
||||||
|
{ title: 'Acciones', key: 'acciones', width: 160, align: 'center' }
|
||||||
|
]
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
router.push({ name: 'ProcesosAdmisionList' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilters() {
|
||||||
|
tipoFilter.value = null
|
||||||
|
fetchDetalles()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDetalles() {
|
||||||
|
await store.fetchDetalles(procesoId.value, {
|
||||||
|
tipo: tipoFilter.value || undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetUploads() {
|
||||||
|
fileImg1.value = []
|
||||||
|
fileImg2.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
formState.id = null
|
||||||
|
formState.tipo = 'requisitos'
|
||||||
|
formState.titulo_detalle = ''
|
||||||
|
formState.descripcion = ''
|
||||||
|
formState.url = ''
|
||||||
|
formState.listas_json = ''
|
||||||
|
formState.meta_json = ''
|
||||||
|
formState.imagen_url = null
|
||||||
|
formState.imagen_url_2 = null
|
||||||
|
resetUploads()
|
||||||
|
formRef.value?.resetFields?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateModal() {
|
||||||
|
isEditing.value = false
|
||||||
|
resetForm()
|
||||||
|
modalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function editDetalle(row) {
|
||||||
|
isEditing.value = true
|
||||||
|
resetForm()
|
||||||
|
|
||||||
|
formState.id = row.id
|
||||||
|
formState.tipo = row.tipo
|
||||||
|
formState.titulo_detalle = row.titulo_detalle
|
||||||
|
formState.descripcion = row.descripcion || ''
|
||||||
|
formState.url = row.url || ''
|
||||||
|
formState.listas_json = row.listas ? JSON.stringify(row.listas, null, 2) : ''
|
||||||
|
formState.meta_json = row.meta ? JSON.stringify(row.meta, null, 2) : ''
|
||||||
|
formState.imagen_url = row.imagen_url || null
|
||||||
|
formState.imagen_url_2 = row.imagen_url_2 || null
|
||||||
|
|
||||||
|
modalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleModalOk() {
|
||||||
|
try {
|
||||||
|
await formRef.value.validateFields()
|
||||||
|
await onFormSubmit()
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModalCancel() {
|
||||||
|
modalVisible.value = false
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeJson(text, label) {
|
||||||
|
if (!text || !text.trim()) return null
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
message.error(`JSON inválido en ${label}`)
|
||||||
|
throw new Error('json_invalid')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDetalleFormData() {
|
||||||
|
const fd = new FormData()
|
||||||
|
|
||||||
|
fd.append('tipo', formState.tipo)
|
||||||
|
fd.append('titulo_detalle', formState.titulo_detalle)
|
||||||
|
if (formState.descripcion) fd.append('descripcion', formState.descripcion)
|
||||||
|
if (formState.url) fd.append('url', formState.url)
|
||||||
|
|
||||||
|
const listas = safeJson(formState.listas_json, 'listas')
|
||||||
|
const meta = safeJson(formState.meta_json, 'meta')
|
||||||
|
|
||||||
|
// 👇 OJO: backend valida 'array' para listas/meta (Laravel)
|
||||||
|
// Para FormData, lo más compatible es enviarlo como JSON string y en backend hacer json_decode.
|
||||||
|
// Si tu backend ya espera array directo, entonces NO uses FormData para listas/meta.
|
||||||
|
// Solución simple: envía como string y decodifica en controller.
|
||||||
|
if (listas !== null) fd.append('listas', JSON.stringify(listas))
|
||||||
|
if (meta !== null) fd.append('meta', JSON.stringify(meta))
|
||||||
|
|
||||||
|
const img1 = fileImg1.value?.[0]?.originFileObj
|
||||||
|
const img2 = fileImg2.value?.[0]?.originFileObj
|
||||||
|
if (img1) fd.append('imagen', img1)
|
||||||
|
if (img2) fd.append('imagen_2', img2)
|
||||||
|
|
||||||
|
return fd
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onFormSubmit() {
|
||||||
|
const fd = buildDetalleFormData()
|
||||||
|
|
||||||
|
// method spoof para update multipart
|
||||||
|
if (isEditing.value) {
|
||||||
|
fd.append('_method', 'PATCH')
|
||||||
|
const ok = await store.updateDetalle(formState.id, fd, { method: 'post' })
|
||||||
|
if (ok) message.success('Detalle actualizado')
|
||||||
|
else message.error('Error al actualizar')
|
||||||
|
} else {
|
||||||
|
const ok = await store.createDetalle(procesoId.value, fd)
|
||||||
|
if (ok) message.success('Detalle creado')
|
||||||
|
else message.error('Error al crear')
|
||||||
|
}
|
||||||
|
|
||||||
|
modalVisible.value = false
|
||||||
|
resetForm()
|
||||||
|
await fetchDetalles()
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDelete(row) {
|
||||||
|
toDelete.value = row
|
||||||
|
deleteModalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
const ok = await store.deleteDetalle(toDelete.value.id, procesoId.value)
|
||||||
|
if (ok) message.success('Detalle eliminado')
|
||||||
|
else message.error('Error al eliminar')
|
||||||
|
deleteModalVisible.value = false
|
||||||
|
toDelete.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await store.fetchProceso(procesoId.value, { include_detalles: false })
|
||||||
|
await fetchDetalles()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cursos-container { padding: 0; }
|
||||||
|
.cursos-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; padding:0 8px; gap: 12px; flex-wrap: wrap; }
|
||||||
|
.header-title h2 { margin:0; font-size:24px; font-weight:600; color:#1f1f1f; }
|
||||||
|
.subtitle { margin:4px 0 0; color:#666; font-size:14px; }
|
||||||
|
.filters-section { display:flex; align-items:center; margin-bottom:18px; padding:0 8px; flex-wrap:wrap; gap:16px; }
|
||||||
|
.loading-state { display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:220px; gap:16px; }
|
||||||
|
.empty-state { min-height:220px; display:flex; align-items:center; justify-content:center; }
|
||||||
|
.cursos-table-container { background:white; border-radius:12px; box-shadow:0 2px 8px rgba(0,0,0,.06); border:1px solid #f0f0f0; overflow:hidden; }
|
||||||
|
.cursos-table { border-radius:12px; }
|
||||||
|
.grid2 { display:grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
||||||
|
.grid3 { display:grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; }
|
||||||
|
.hint { display:block; margin-top: 6px; color:#666; }
|
||||||
|
@media (max-width: 768px) { .grid2, .grid3 { grid-template-columns: 1fr; } }
|
||||||
|
</style>
|
||||||
@ -0,0 +1,764 @@
|
|||||||
|
<template>
|
||||||
|
<div class="cursos-container">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="cursos-header">
|
||||||
|
<div class="header-title">
|
||||||
|
<h2>Procesos de Admisión</h2>
|
||||||
|
<p class="subtitle">Crea procesos y luego gestiona sus detalles</p>
|
||||||
|
</div>
|
||||||
|
<a-button type="primary" @click="showCreateModal" class="new-curso-btn">
|
||||||
|
<template #icon><PlusOutlined /></template>
|
||||||
|
Nuevo Proceso
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtros -->
|
||||||
|
<div class="filters-section">
|
||||||
|
<a-input-search
|
||||||
|
v-model:value="searchText"
|
||||||
|
placeholder="Buscar por título o slug..."
|
||||||
|
@search="handleSearch"
|
||||||
|
@press-enter="handleSearch"
|
||||||
|
style="width: 320px"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<template #enterButton><SearchOutlined /></template>
|
||||||
|
</a-input-search>
|
||||||
|
|
||||||
|
<a-select
|
||||||
|
v-model:value="estadoFilter"
|
||||||
|
placeholder="Estado"
|
||||||
|
style="width: 220px"
|
||||||
|
size="large"
|
||||||
|
@change="handleFiltersChange"
|
||||||
|
>
|
||||||
|
<a-select-option :value="null">Todos</a-select-option>
|
||||||
|
<a-select-option value="nuevo">Nuevo</a-select-option>
|
||||||
|
<a-select-option value="publicado">Publicado</a-select-option>
|
||||||
|
<a-select-option value="en_proceso">En proceso</a-select-option>
|
||||||
|
<a-select-option value="finalizado">Finalizado</a-select-option>
|
||||||
|
<a-select-option value="cancelado">Cancelado</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
|
||||||
|
<a-select
|
||||||
|
v-model:value="publicadoFilter"
|
||||||
|
placeholder="Publicado"
|
||||||
|
style="width: 180px"
|
||||||
|
size="large"
|
||||||
|
@change="handleFiltersChange"
|
||||||
|
>
|
||||||
|
<a-select-option :value="null">Todos</a-select-option>
|
||||||
|
<a-select-option :value="true">Sí</a-select-option>
|
||||||
|
<a-select-option :value="false">No</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
|
||||||
|
<a-button @click="handleClearFilters" size="large">
|
||||||
|
<template #icon><ReloadOutlined /></template>
|
||||||
|
Limpiar
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading -->
|
||||||
|
<div v-if="store.loading && store.procesos.length === 0" class="loading-state">
|
||||||
|
<a-spin size="large" />
|
||||||
|
<p>Cargando procesos...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty -->
|
||||||
|
<div v-else-if="store.procesos.length === 0" class="empty-state">
|
||||||
|
<a-empty description="No hay procesos registrados" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabla -->
|
||||||
|
<div v-else class="cursos-table-container">
|
||||||
|
<a-table
|
||||||
|
:data-source="store.procesos"
|
||||||
|
:columns="columns"
|
||||||
|
:loading="store.loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
row-key="id"
|
||||||
|
@change="handleTableChange"
|
||||||
|
class="cursos-table"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<!-- Publicado -->
|
||||||
|
<template v-if="column.key === 'publicado'">
|
||||||
|
<a-switch
|
||||||
|
:checked="!!record.publicado"
|
||||||
|
:loading="togglingId === record.id"
|
||||||
|
checked-children="Sí"
|
||||||
|
un-checked-children="No"
|
||||||
|
@change="togglePublicado(record)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Fechas -->
|
||||||
|
<template v-else-if="column.key === 'fechas'">
|
||||||
|
<div class="mini-dates">
|
||||||
|
<div>
|
||||||
|
<strong>Preinscripción:</strong>
|
||||||
|
{{ shortDate(record.fecha_inicio_preinscripcion) }} - {{ shortDate(record.fecha_fin_preinscripcion) }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Inscripción:</strong>
|
||||||
|
{{ shortDate(record.fecha_inicio_inscripcion) }} - {{ shortDate(record.fecha_fin_inscripcion) }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Ex1:</strong> {{ shortDate(record.fecha_examen1) }}
|
||||||
|
<span v-if="record.fecha_examen2"> | <strong>Ex2:</strong> {{ shortDate(record.fecha_examen2) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Archivos -->
|
||||||
|
<template v-else-if="column.key === 'imagenes'">
|
||||||
|
<a-space>
|
||||||
|
<a v-if="record.imagen_url" :href="record.imagen_url" target="_blank">Imagen</a>
|
||||||
|
<a v-if="record.banner_url" :href="record.banner_url" target="_blank">Banner</a>
|
||||||
|
<a v-if="record.brochure_url" :href="record.brochure_url" target="_blank">Brochure</a>
|
||||||
|
<span v-if="!record.imagen_url && !record.banner_url && !record.brochure_url">-</span>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Created -->
|
||||||
|
<template v-else-if="column.key === 'created_at'">
|
||||||
|
{{ formatDate(record.created_at) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Acciones -->
|
||||||
|
<template v-else-if="column.key === 'acciones'">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="link" size="small" @click="goDetalles(record)">
|
||||||
|
<SettingOutlined /> Detalles
|
||||||
|
</a-button>
|
||||||
|
<a-button type="link" size="small" @click="showEditModal(record)">
|
||||||
|
<EditOutlined /> Editar
|
||||||
|
</a-button>
|
||||||
|
<a-button type="link" size="small" danger @click="confirmDelete(record)">
|
||||||
|
<DeleteOutlined /> Eliminar
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Crear/Editar -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="modalVisible"
|
||||||
|
:title="isEditing ? 'Editar Proceso' : 'Nuevo Proceso'"
|
||||||
|
:confirm-loading="store.loading"
|
||||||
|
@ok="handleModalOk"
|
||||||
|
@cancel="handleModalCancel"
|
||||||
|
width="980px"
|
||||||
|
class="curso-modal"
|
||||||
|
>
|
||||||
|
<a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical" @finish="onFormSubmit">
|
||||||
|
<div class="grid2">
|
||||||
|
<a-form-item label="Título" name="titulo">
|
||||||
|
<a-input v-model:value="formState.titulo" size="large" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Slug" name="slug">
|
||||||
|
<a-input v-model:value="formState.slug" placeholder="admision-2026-i" size="large" />
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid2">
|
||||||
|
<a-form-item label="Subtítulo" name="subtitulo">
|
||||||
|
<a-input v-model:value="formState.subtitulo" size="large" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Tipo proceso" name="tipo_proceso">
|
||||||
|
<a-input v-model:value="formState.tipo_proceso" placeholder="Ej: admisión general" size="large" />
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid2">
|
||||||
|
<a-form-item label="Modalidad (texto)" name="modalidad">
|
||||||
|
<a-input v-model:value="formState.modalidad" placeholder="Ej: ordinario" size="large" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Estado" name="estado">
|
||||||
|
<a-select v-model:value="formState.estado" size="large">
|
||||||
|
<a-select-option value="nuevo">Nuevo</a-select-option>
|
||||||
|
<a-select-option value="publicado">Publicado</a-select-option>
|
||||||
|
<a-select-option value="en_proceso">En proceso</a-select-option>
|
||||||
|
<a-select-option value="finalizado">Finalizado</a-select-option>
|
||||||
|
<a-select-option value="cancelado">Cancelado</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-form-item label="Descripción" name="descripcion">
|
||||||
|
<a-textarea v-model:value="formState.descripcion" :rows="3" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<div class="grid2">
|
||||||
|
<a-form-item label="Publicado" name="publicado">
|
||||||
|
<a-switch v-model:checked="formState.publicado" checked-children="Sí" un-checked-children="No" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Fecha publicación" name="fecha_publicacion">
|
||||||
|
<a-date-picker
|
||||||
|
v-model:value="formState.fecha_publicacion"
|
||||||
|
show-time
|
||||||
|
style="width: 100%"
|
||||||
|
size="large"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||||
|
placeholder="Opcional"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-divider>Fechas</a-divider>
|
||||||
|
|
||||||
|
<!-- PREINSCRIPCIÓN -->
|
||||||
|
<div class="grid2">
|
||||||
|
<a-form-item label="Inicio preinscripción" name="fecha_inicio_preinscripcion">
|
||||||
|
<a-date-picker
|
||||||
|
v-model:value="formState.fecha_inicio_preinscripcion"
|
||||||
|
show-time
|
||||||
|
style="width: 100%"
|
||||||
|
size="large"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||||
|
placeholder="Opcional"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Fin preinscripción" name="fecha_fin_preinscripcion">
|
||||||
|
<a-date-picker
|
||||||
|
v-model:value="formState.fecha_fin_preinscripcion"
|
||||||
|
show-time
|
||||||
|
style="width: 100%"
|
||||||
|
size="large"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||||
|
placeholder="Opcional"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- INSCRIPCIÓN -->
|
||||||
|
<div class="grid2">
|
||||||
|
<a-form-item label="Inicio inscripción" name="fecha_inicio_inscripcion">
|
||||||
|
<a-date-picker
|
||||||
|
v-model:value="formState.fecha_inicio_inscripcion"
|
||||||
|
show-time
|
||||||
|
style="width: 100%"
|
||||||
|
size="large"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Fin inscripción" name="fecha_fin_inscripcion">
|
||||||
|
<a-date-picker
|
||||||
|
v-model:value="formState.fecha_fin_inscripcion"
|
||||||
|
show-time
|
||||||
|
style="width: 100%"
|
||||||
|
size="large"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- EXÁMENES -->
|
||||||
|
<div class="grid2">
|
||||||
|
<a-form-item label="Examen 1" name="fecha_examen1">
|
||||||
|
<a-date-picker
|
||||||
|
v-model:value="formState.fecha_examen1"
|
||||||
|
show-time
|
||||||
|
style="width: 100%"
|
||||||
|
size="large"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Examen 2" name="fecha_examen2">
|
||||||
|
<a-date-picker
|
||||||
|
v-model:value="formState.fecha_examen2"
|
||||||
|
show-time
|
||||||
|
style="width: 100%"
|
||||||
|
size="large"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||||
|
placeholder="Opcional"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RESULTADOS / BIOMÉTRICO -->
|
||||||
|
<div class="grid2">
|
||||||
|
<a-form-item label="Resultados" name="fecha_resultados">
|
||||||
|
<a-date-picker
|
||||||
|
v-model:value="formState.fecha_resultados"
|
||||||
|
show-time
|
||||||
|
style="width: 100%"
|
||||||
|
size="large"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||||
|
placeholder="Opcional"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Inicio biométrico" name="fecha_inicio_biometrico">
|
||||||
|
<a-date-picker
|
||||||
|
v-model:value="formState.fecha_inicio_biometrico"
|
||||||
|
show-time
|
||||||
|
style="width: 100%"
|
||||||
|
size="large"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||||
|
placeholder="Opcional"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid2">
|
||||||
|
<a-form-item label="Fin biométrico" name="fecha_fin_biometrico">
|
||||||
|
<a-date-picker
|
||||||
|
v-model:value="formState.fecha_fin_biometrico"
|
||||||
|
show-time
|
||||||
|
style="width: 100%"
|
||||||
|
size="large"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||||
|
placeholder="Opcional"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-divider>Archivos (SUBIR)</a-divider>
|
||||||
|
|
||||||
|
<div class="grid3">
|
||||||
|
<a-form-item label="Imagen (jpg/png/webp)">
|
||||||
|
<a-upload v-model:file-list="fileImagen" :before-upload="() => false" :max-count="1" accept="image/*">
|
||||||
|
<a-button>Seleccionar</a-button>
|
||||||
|
</a-upload>
|
||||||
|
<small class="hint" v-if="isEditing && formState.imagen_url">
|
||||||
|
Actual: <a :href="formState.imagen_url" target="_blank">ver</a>
|
||||||
|
</small>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Banner (jpg/png/webp)">
|
||||||
|
<a-upload v-model:file-list="fileBanner" :before-upload="() => false" :max-count="1" accept="image/*">
|
||||||
|
<a-button>Seleccionar</a-button>
|
||||||
|
</a-upload>
|
||||||
|
<small class="hint" v-if="isEditing && formState.banner_url">
|
||||||
|
Actual: <a :href="formState.banner_url" target="_blank">ver</a>
|
||||||
|
</small>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Brochure (PDF)">
|
||||||
|
<a-upload v-model:file-list="fileBrochure" :before-upload="() => false" :max-count="1" accept="application/pdf">
|
||||||
|
<a-button>Seleccionar</a-button>
|
||||||
|
</a-upload>
|
||||||
|
<small class="hint" v-if="isEditing && formState.brochure_url">
|
||||||
|
Actual: <a :href="formState.brochure_url" target="_blank">ver</a>
|
||||||
|
</small>
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-divider>Links</a-divider>
|
||||||
|
|
||||||
|
<div class="grid2">
|
||||||
|
<a-form-item label="Link preinscripción" name="link_preinscripcion">
|
||||||
|
<a-input v-model:value="formState.link_preinscripcion" placeholder="https://..." size="large" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Link inscripción" name="link_inscripcion">
|
||||||
|
<a-input v-model:value="formState.link_inscripcion" placeholder="https://..." size="large" />
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid2">
|
||||||
|
<a-form-item label="Link resultados" name="link_resultados">
|
||||||
|
<a-input v-model:value="formState.link_resultados" placeholder="https://..." size="large" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="Link reglamento" name="link_reglamento">
|
||||||
|
<a-input v-model:value="formState.link_reglamento" placeholder="https://..." size="large" />
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-footer" v-if="store.error">
|
||||||
|
<a-alert type="error" :message="store.error" show-icon class="error-alert" />
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- Modal delete -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="deleteModalVisible"
|
||||||
|
title="Confirmar Eliminación"
|
||||||
|
@ok="handleDelete"
|
||||||
|
@cancel="deleteModalVisible = false"
|
||||||
|
ok-text="Eliminar"
|
||||||
|
cancel-text="Cancelar"
|
||||||
|
ok-type="danger"
|
||||||
|
width="420px"
|
||||||
|
>
|
||||||
|
<div class="delete-confirm-content">
|
||||||
|
<a-alert
|
||||||
|
message="¿Eliminar este proceso?"
|
||||||
|
description="Se eliminarán también todos los detalles asociados."
|
||||||
|
type="warning"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
<div class="curso-info" v-if="toDelete">
|
||||||
|
<p><strong>Título:</strong> {{ toDelete.titulo }}</p>
|
||||||
|
<p><strong>Slug:</strong> {{ toDelete.slug }}</p>
|
||||||
|
<p><strong>Estado:</strong> {{ toDelete.estado }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import {
|
||||||
|
PlusOutlined, EditOutlined, DeleteOutlined,
|
||||||
|
SearchOutlined, ReloadOutlined, SettingOutlined
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
import { useProcesoAdmisionStore } from '../../../store/procesosAdmisionStore'
|
||||||
|
|
||||||
|
const store = useProcesoAdmisionStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const modalVisible = ref(false)
|
||||||
|
const deleteModalVisible = ref(false)
|
||||||
|
const isEditing = ref(false)
|
||||||
|
const toDelete = ref(null)
|
||||||
|
const togglingId = ref(null)
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
const searchText = ref('')
|
||||||
|
const estadoFilter = ref(null)
|
||||||
|
const publicadoFilter = ref(null)
|
||||||
|
|
||||||
|
// Upload file lists
|
||||||
|
const fileImagen = ref([])
|
||||||
|
const fileBanner = ref([])
|
||||||
|
const fileBrochure = ref([])
|
||||||
|
|
||||||
|
const formState = reactive({
|
||||||
|
id: null,
|
||||||
|
titulo: '',
|
||||||
|
subtitulo: '',
|
||||||
|
descripcion: '',
|
||||||
|
slug: '',
|
||||||
|
tipo_proceso: '',
|
||||||
|
modalidad: '',
|
||||||
|
publicado: false,
|
||||||
|
fecha_publicacion: null,
|
||||||
|
|
||||||
|
fecha_inicio_preinscripcion: null,
|
||||||
|
fecha_fin_preinscripcion: null,
|
||||||
|
fecha_inicio_inscripcion: null,
|
||||||
|
fecha_fin_inscripcion: null,
|
||||||
|
fecha_examen1: null,
|
||||||
|
fecha_examen2: null,
|
||||||
|
fecha_resultados: null,
|
||||||
|
fecha_inicio_biometrico: null,
|
||||||
|
fecha_fin_biometrico: null,
|
||||||
|
|
||||||
|
link_preinscripcion: '',
|
||||||
|
link_inscripcion: '',
|
||||||
|
link_resultados: '',
|
||||||
|
link_reglamento: '',
|
||||||
|
estado: 'nuevo',
|
||||||
|
|
||||||
|
// para mostrar actuales
|
||||||
|
imagen_url: null,
|
||||||
|
banner_url: null,
|
||||||
|
brochure_url: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
titulo: [{ required: true, message: 'Ingresa el título', trigger: 'blur' }],
|
||||||
|
slug: [{ required: true, message: 'Ingresa el slug', trigger: 'blur' }],
|
||||||
|
estado: [{ required: true, message: 'Selecciona estado', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: 'ID', dataIndex: 'id', key: 'id', width: 70 },
|
||||||
|
{ title: 'Título', dataIndex: 'titulo', key: 'titulo' },
|
||||||
|
{ title: 'Slug', dataIndex: 'slug', key: 'slug', width: 220 },
|
||||||
|
{ title: 'Estado', dataIndex: 'estado', key: 'estado', width: 120 },
|
||||||
|
{ title: 'Publicado', key: 'publicado', width: 120 },
|
||||||
|
{ title: 'Fechas', key: 'fechas', width: 280 },
|
||||||
|
{ title: 'Archivos', key: 'imagenes', width: 220 },
|
||||||
|
{ title: 'Creado', dataIndex: 'created_at', key: 'created_at', width: 140 },
|
||||||
|
{ title: 'Acciones', key: 'acciones', width: 240, align: 'center' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const pagination = computed(() => ({
|
||||||
|
current: store.pagination.current_page,
|
||||||
|
pageSize: store.pagination.per_page,
|
||||||
|
total: store.pagination.total,
|
||||||
|
showSizeChanger: true,
|
||||||
|
pageSizeOptions: ['10', '15', '20', '50'],
|
||||||
|
showTotal: (total, range) => `${range[0]}-${range[1]} de ${total} procesos`
|
||||||
|
}))
|
||||||
|
|
||||||
|
function resetUploads() {
|
||||||
|
fileImagen.value = []
|
||||||
|
fileBanner.value = []
|
||||||
|
fileBrochure.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
formState.id = null
|
||||||
|
formState.titulo = ''
|
||||||
|
formState.subtitulo = ''
|
||||||
|
formState.descripcion = ''
|
||||||
|
formState.slug = ''
|
||||||
|
formState.tipo_proceso = ''
|
||||||
|
formState.modalidad = ''
|
||||||
|
formState.publicado = false
|
||||||
|
formState.fecha_publicacion = null
|
||||||
|
|
||||||
|
formState.fecha_inicio_preinscripcion = null
|
||||||
|
formState.fecha_fin_preinscripcion = null
|
||||||
|
formState.fecha_inicio_inscripcion = null
|
||||||
|
formState.fecha_fin_inscripcion = null
|
||||||
|
formState.fecha_examen1 = null
|
||||||
|
formState.fecha_examen2 = null
|
||||||
|
formState.fecha_resultados = null
|
||||||
|
formState.fecha_inicio_biometrico = null
|
||||||
|
formState.fecha_fin_biometrico = null
|
||||||
|
|
||||||
|
formState.link_preinscripcion = ''
|
||||||
|
formState.link_inscripcion = ''
|
||||||
|
formState.link_resultados = ''
|
||||||
|
formState.link_reglamento = ''
|
||||||
|
formState.estado = 'nuevo'
|
||||||
|
|
||||||
|
formState.imagen_url = null
|
||||||
|
formState.banner_url = null
|
||||||
|
formState.brochure_url = null
|
||||||
|
|
||||||
|
resetUploads()
|
||||||
|
formRef.value?.resetFields?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCreateModal() {
|
||||||
|
isEditing.value = false
|
||||||
|
resetForm()
|
||||||
|
modalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEditModal(row) {
|
||||||
|
isEditing.value = true
|
||||||
|
resetForm()
|
||||||
|
|
||||||
|
Object.assign(formState, {
|
||||||
|
id: row.id,
|
||||||
|
titulo: row.titulo,
|
||||||
|
subtitulo: row.subtitulo,
|
||||||
|
descripcion: row.descripcion,
|
||||||
|
slug: row.slug,
|
||||||
|
tipo_proceso: row.tipo_proceso,
|
||||||
|
modalidad: row.modalidad,
|
||||||
|
publicado: !!row.publicado,
|
||||||
|
fecha_publicacion: row.fecha_publicacion,
|
||||||
|
|
||||||
|
fecha_inicio_preinscripcion: row.fecha_inicio_preinscripcion,
|
||||||
|
fecha_fin_preinscripcion: row.fecha_fin_preinscripcion,
|
||||||
|
fecha_inicio_inscripcion: row.fecha_inicio_inscripcion,
|
||||||
|
fecha_fin_inscripcion: row.fecha_fin_inscripcion,
|
||||||
|
fecha_examen1: row.fecha_examen1,
|
||||||
|
fecha_examen2: row.fecha_examen2,
|
||||||
|
fecha_resultados: row.fecha_resultados,
|
||||||
|
fecha_inicio_biometrico: row.fecha_inicio_biometrico,
|
||||||
|
fecha_fin_biometrico: row.fecha_fin_biometrico,
|
||||||
|
|
||||||
|
link_preinscripcion: row.link_preinscripcion,
|
||||||
|
link_inscripcion: row.link_inscripcion,
|
||||||
|
link_resultados: row.link_resultados,
|
||||||
|
link_reglamento: row.link_reglamento,
|
||||||
|
estado: row.estado,
|
||||||
|
|
||||||
|
imagen_url: row.imagen_url,
|
||||||
|
banner_url: row.banner_url,
|
||||||
|
brochure_url: row.brochure_url
|
||||||
|
})
|
||||||
|
|
||||||
|
modalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleModalOk() {
|
||||||
|
try {
|
||||||
|
await formRef.value.validateFields()
|
||||||
|
await onFormSubmit()
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModalCancel() {
|
||||||
|
modalVisible.value = false
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFormData() {
|
||||||
|
const fd = new FormData()
|
||||||
|
|
||||||
|
const fields = {
|
||||||
|
titulo: formState.titulo,
|
||||||
|
subtitulo: formState.subtitulo || null,
|
||||||
|
descripcion: formState.descripcion || null,
|
||||||
|
slug: formState.slug,
|
||||||
|
tipo_proceso: formState.tipo_proceso || null,
|
||||||
|
modalidad: formState.modalidad || null,
|
||||||
|
|
||||||
|
publicado: formState.publicado ? 1 : 0,
|
||||||
|
fecha_publicacion: formState.fecha_publicacion || null,
|
||||||
|
|
||||||
|
fecha_inicio_preinscripcion: formState.fecha_inicio_preinscripcion || null,
|
||||||
|
fecha_fin_preinscripcion: formState.fecha_fin_preinscripcion || null,
|
||||||
|
fecha_inicio_inscripcion: formState.fecha_inicio_inscripcion || null,
|
||||||
|
fecha_fin_inscripcion: formState.fecha_fin_inscripcion || null,
|
||||||
|
fecha_examen1: formState.fecha_examen1 || null,
|
||||||
|
fecha_examen2: formState.fecha_examen2 || null,
|
||||||
|
fecha_resultados: formState.fecha_resultados || null,
|
||||||
|
fecha_inicio_biometrico: formState.fecha_inicio_biometrico || null,
|
||||||
|
fecha_fin_biometrico: formState.fecha_fin_biometrico || null,
|
||||||
|
|
||||||
|
link_preinscripcion: formState.link_preinscripcion || null,
|
||||||
|
link_inscripcion: formState.link_inscripcion || null,
|
||||||
|
link_resultados: formState.link_resultados || null,
|
||||||
|
link_reglamento: formState.link_reglamento || null,
|
||||||
|
|
||||||
|
estado: formState.estado
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(fields).forEach(([k, v]) => {
|
||||||
|
if (v === null || v === undefined || v === '') return
|
||||||
|
fd.append(k, v)
|
||||||
|
})
|
||||||
|
|
||||||
|
const img = fileImagen.value?.[0]?.originFileObj
|
||||||
|
const ban = fileBanner.value?.[0]?.originFileObj
|
||||||
|
const bro = fileBrochure.value?.[0]?.originFileObj
|
||||||
|
if (img) fd.append('imagen', img)
|
||||||
|
if (ban) fd.append('banner', ban)
|
||||||
|
if (bro) fd.append('brochure', bro)
|
||||||
|
|
||||||
|
return fd
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onFormSubmit() {
|
||||||
|
const fd = buildFormData()
|
||||||
|
|
||||||
|
let ok = false
|
||||||
|
if (isEditing.value) {
|
||||||
|
// multipart + PATCH -> method spoof
|
||||||
|
fd.append('_method', 'PATCH')
|
||||||
|
ok = await store.updateProceso(formState.id, fd, { method: 'post' })
|
||||||
|
} else {
|
||||||
|
ok = await store.createProceso(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
message.success(isEditing.value ? 'Proceso actualizado' : 'Proceso creado')
|
||||||
|
modalVisible.value = false
|
||||||
|
resetForm()
|
||||||
|
await fetchList({ page: 1 })
|
||||||
|
} else {
|
||||||
|
message.error('Error al guardar')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDelete(row) {
|
||||||
|
toDelete.value = row
|
||||||
|
deleteModalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
const ok = await store.deleteProceso(toDelete.value.id)
|
||||||
|
if (ok) message.success('Eliminado')
|
||||||
|
else message.error('Error al eliminar')
|
||||||
|
deleteModalVisible.value = false
|
||||||
|
toDelete.value = null
|
||||||
|
await fetchList({ page: 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function togglePublicado(row) {
|
||||||
|
togglingId.value = row.id
|
||||||
|
try {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('publicado', row.publicado ? 0 : 1)
|
||||||
|
fd.append('_method', 'PATCH')
|
||||||
|
const ok = await store.updateProceso(row.id, fd, { method: 'post' })
|
||||||
|
if (ok) message.success('Publicado actualizado')
|
||||||
|
} finally {
|
||||||
|
togglingId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goDetalles(row) {
|
||||||
|
router.push({ name: 'ProcesoAdmisionDetalles', params: { id: row.id } })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() { fetchList({ page: 1 }) }
|
||||||
|
function handleFiltersChange() { fetchList({ page: 1 }) }
|
||||||
|
|
||||||
|
function handleClearFilters() {
|
||||||
|
searchText.value = ''
|
||||||
|
estadoFilter.value = null
|
||||||
|
publicadoFilter.value = null
|
||||||
|
fetchList({ page: 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTableChange(paginationConfig) {
|
||||||
|
fetchList({ page: paginationConfig.current, per_page: paginationConfig.pageSize })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchList({ page = 1, per_page } = {}) {
|
||||||
|
await store.fetchProcesos({
|
||||||
|
q: searchText.value || undefined,
|
||||||
|
estado: estadoFilter.value ?? undefined,
|
||||||
|
publicado: publicadoFilter.value ?? undefined,
|
||||||
|
page,
|
||||||
|
per_page
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(s) {
|
||||||
|
if (!s) return ''
|
||||||
|
const d = new Date(s)
|
||||||
|
return d.toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortDate(s) {
|
||||||
|
if (!s) return '-'
|
||||||
|
const d = new Date(s)
|
||||||
|
return d.toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit', year: '2-digit' })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => { fetchList() })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cursos-container { padding: 0; }
|
||||||
|
.cursos-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; padding:0 8px; gap: 12px; flex-wrap: wrap; }
|
||||||
|
.header-title h2 { margin:0; font-size:24px; font-weight:600; color:#1f1f1f; }
|
||||||
|
.subtitle { margin:4px 0 0; color:#666; font-size:14px; }
|
||||||
|
.new-curso-btn { height:40px; font-weight:500; }
|
||||||
|
.filters-section { display:flex; align-items:center; margin-bottom:24px; padding:0 8px; flex-wrap:wrap; gap:16px; }
|
||||||
|
.loading-state { display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:300px; gap:16px; }
|
||||||
|
.empty-state { min-height:300px; display:flex; align-items:center; justify-content:center; }
|
||||||
|
.cursos-table-container { background:white; border-radius:12px; box-shadow:0 2px 8px rgba(0,0,0,.06); border:1px solid #f0f0f0; overflow:hidden; }
|
||||||
|
.cursos-table { border-radius:12px; }
|
||||||
|
.grid2 { display:grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
||||||
|
.grid3 { display:grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; }
|
||||||
|
.hint { display:block; margin-top: 6px; color:#666; }
|
||||||
|
.mini-dates { line-height: 1.35; font-size: 12px; color: #444; }
|
||||||
|
@media (max-width: 768px) { .grid2, .grid3 { grid-template-columns: 1fr; } }
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue