From 71182fd292641dce45096edcc174fcdfd97c00d9 Mon Sep 17 00:00:00 2001 From: elmer Date: Thu, 12 Feb 2026 22:15:59 -0500 Subject: [PATCH] 123456 --- .../ProcesoAdmisionController.php | 199 +++++ .../ProcesoAdmisionDetalleController.php | 113 +++ back/app/Models/ProcesoAdmision.php | 60 +- back/app/Models/ProcesoAdmisionDetalle.php | 46 ++ back/routes/api.php | 32 +- front/src/router/index.js | 11 + front/src/store/procesosAdmisionStore.js | 276 +++++++ .../src/views/administrador/layout/layout.vue | 17 +- .../ProcesoAdmisionDetalles.vue | 357 ++++++++ .../procesoadmision/ProcesosAdmisionList.vue | 764 ++++++++++++++++++ 10 files changed, 1848 insertions(+), 27 deletions(-) create mode 100644 back/app/Http/Controllers/Administracion/ProcesoAdmisionController.php create mode 100644 back/app/Http/Controllers/Administracion/ProcesoAdmisionDetalleController.php create mode 100644 back/app/Models/ProcesoAdmisionDetalle.php create mode 100644 front/src/store/procesosAdmisionStore.js create mode 100644 front/src/views/administrador/procesoadmision/ProcesoAdmisionDetalles.vue create mode 100644 front/src/views/administrador/procesoadmision/ProcesosAdmisionList.vue diff --git a/back/app/Http/Controllers/Administracion/ProcesoAdmisionController.php b/back/app/Http/Controllers/Administracion/ProcesoAdmisionController.php new file mode 100644 index 0000000..d7e402f --- /dev/null +++ b/back/app/Http/Controllers/Administracion/ProcesoAdmisionController.php @@ -0,0 +1,199 @@ +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']); + } +} diff --git a/back/app/Http/Controllers/Administracion/ProcesoAdmisionDetalleController.php b/back/app/Http/Controllers/Administracion/ProcesoAdmisionDetalleController.php new file mode 100644 index 0000000..1b797c2 --- /dev/null +++ b/back/app/Http/Controllers/Administracion/ProcesoAdmisionDetalleController.php @@ -0,0 +1,113 @@ +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']); + } +} diff --git a/back/app/Models/ProcesoAdmision.php b/back/app/Models/ProcesoAdmision.php index 39206ba..a91bb00 100644 --- a/back/app/Models/ProcesoAdmision.php +++ b/back/app/Models/ProcesoAdmision.php @@ -3,33 +3,67 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Facades\Storage; class ProcesoAdmision extends Model { protected $table = 'procesos_admision'; protected $fillable = [ - 'nombre', - 'fecha_inicio', - 'fecha_fin', - 'estado' + 'titulo','subtitulo','descripcion','slug', + 'tipo_proceso','modalidad', + 'publicado','fecha_publicacion', + 'fecha_inicio_preinscripcion','fecha_fin_preinscripcion', + 'fecha_inicio_inscripcion','fecha_fin_inscripcion', + 'fecha_examen1','fecha_examen2', + 'fecha_resultados', + 'fecha_inicio_biometrico','fecha_fin_biometrico', + 'imagen_path','banner_path','brochure_path', + 'link_preinscripcion','link_inscripcion','link_resultados','link_reglamento', + 'estado', ]; protected $casts = [ - 'fecha_inicio' => 'datetime', - 'fecha_fin' => 'datetime', - 'estado' => 'boolean' + 'publicado' => 'boolean', + 'fecha_publicacion' => 'datetime', + 'fecha_inicio_preinscripcion' => 'datetime', + 'fecha_fin_preinscripcion' => 'datetime', + 'fecha_inicio_inscripcion' => 'datetime', + 'fecha_fin_inscripcion' => 'datetime', + 'fecha_examen1' => 'datetime', + 'fecha_examen2' => 'datetime', + 'fecha_resultados' => 'datetime', + 'fecha_inicio_biometrico' => 'datetime', + 'fecha_fin_biometrico' => 'datetime', ]; - /* - |-------------------------------------------------------------------------- - | RELACIONES - |-------------------------------------------------------------------------- - */ + protected $appends = ['imagen_url','banner_url','brochure_url']; - // Un proceso tiene muchos resultados + public function detalles(): HasMany + { + return $this->hasMany(ProcesoAdmisionDetalle::class, 'proceso_admision_id'); + } + + public function getImagenUrlAttribute(): ?string + { + return $this->imagen_path ? Storage::disk('public')->url($this->imagen_path) : null; + } + + public function getBannerUrlAttribute(): ?string + { + return $this->banner_path ? Storage::disk('public')->url($this->banner_path) : null; + } + + public function getBrochureUrlAttribute(): ?string + { + return $this->brochure_path ? Storage::disk('public')->url($this->brochure_path) : null; + } + + // Un proceso tiene muchos resultados public function resultados() { return $this->hasMany(ResultadoAdmision::class, 'idproceso'); } } + diff --git a/back/app/Models/ProcesoAdmisionDetalle.php b/back/app/Models/ProcesoAdmisionDetalle.php new file mode 100644 index 0000000..2ca11a1 --- /dev/null +++ b/back/app/Models/ProcesoAdmisionDetalle.php @@ -0,0 +1,46 @@ + '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; + } +} diff --git a/back/routes/api.php b/back/routes/api.php index 27bf6e8..c62b744 100644 --- a/back/routes/api.php +++ b/back/routes/api.php @@ -14,7 +14,9 @@ use App\Http\Controllers\Administracion\ProcesoController; use App\Http\Controllers\PostulanteAuthController; use App\Http\Controllers\ExamenController; use App\Http\Controllers\Administracion\ReglaAreaProcesoController; - +use App\Http\Controllers\Administracion\ProcesoAdmisionController; +use App\Http\Controllers\Administracion\ProcesoAdmisionDetalleController; +use App\Models\ProcesoAdmisionDetalle; Route::get('/user', function (Request $request) { return $request->user(); @@ -133,7 +135,7 @@ Route::middleware(['auth:sanctum'])->prefix('reglas')->group(function () { // Examen - Flujo separado Route::middleware(['auth:postulante'])->group(function () { - // Configuración + Route::get('/examen/procesos', [ExamenController::class, 'procesoexamen']); Route::get('/examen/areas', [ExamenController::class, 'areas']); Route::get('/examen/actual', [ExamenController::class, 'miExamenActual']); @@ -156,4 +158,28 @@ Route::middleware(['auth:postulante'])->group(function () { // Finalizar examen Route::post('/examen/{examen}/finalizar', [ExamenController::class, 'finalizarExamen']); -}); \ No newline at end of file +}); + + +Route::middleware('auth:sanctum')->prefix('admin')->group(function () { + + // PROCESOS + Route::prefix('procesos-admision')->group(function () { + Route::get('/', [ProcesoAdmisionController::class, 'index'])->name('index'); + Route::post('/', [ProcesoAdmisionController::class, 'store'])->name('store'); + Route::get('/{id}', [ProcesoAdmisionController::class, 'show'])->name('show'); + Route::match(['put','patch'], '/{id}', [ProcesoAdmisionController::class, 'update'])->name('update'); + Route::delete('/{id}', [ProcesoAdmisionController::class, 'destroy'])->name('destroy'); + + // DETALLES por proceso + Route::get('/{procesoId}/detalles', [ProcesoAdmisionDetalleController::class, 'index'])->name('detalles.index'); + Route::post('/{procesoId}/detalles', [ProcesoAdmisionDetalleController::class, 'store'])->name('detalles.store'); + }); + + // DETALLES por ID + Route::prefix('detalles-admision')->group(function () { + Route::get('/{id}', [ProcesoAdmisionDetalleController::class, 'show'])->name('show'); + Route::match(['put','patch'], '/{id}', [ProcesoAdmisionDetalleController::class, 'update'])->name('update'); + Route::delete('/{id}', [ProcesoAdmisionDetalleController::class, 'destroy'])->name('destroy'); + }); + }); \ No newline at end of file diff --git a/front/src/router/index.js b/front/src/router/index.js index f261f4c..fccdac8 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -118,6 +118,17 @@ const routes = [ name: 'Reglas', component: () => import('../views/administrador/Procesos/ReglasList.vue'), meta: { requiresAuth: true, role: 'administrador' } + }, + + { + path: '/admin/dashboard/procesos-admision', + name: 'ProcesosAdmisionList', + component: () => import('../views/administrador/procesoadmision/ProcesosAdmisionList.vue') + }, + { + path: '/admin/dashboard/procesos/:id/detalles', + name: 'ProcesoAdmisionDetalles', + component: () => import('../views/administrador/procesoadmision/ProcesoAdmisionDetalles.vue') } ] }, diff --git a/front/src/store/procesosAdmisionStore.js b/front/src/store/procesosAdmisionStore.js new file mode 100644 index 0000000..eaef7a3 --- /dev/null +++ b/front/src/store/procesosAdmisionStore.js @@ -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 + } + } + } +}) diff --git a/front/src/views/administrador/layout/layout.vue b/front/src/views/administrador/layout/layout.vue index e094809..2da78ef 100644 --- a/front/src/views/administrador/layout/layout.vue +++ b/front/src/views/administrador/layout/layout.vue @@ -201,25 +201,20 @@ - + - + - - - + @@ -426,7 +421,7 @@ const handleMenuSelect = ({ key }) => { 'examenes-reglas-lista': { name: 'Reglas' }, - 'lista-areas': { name: 'AcademiaAreas' }, + 'procesos-lista': { name: 'ProcesosAdmisionList' }, 'lista-cursos': { name: 'AcademiaCursos' }, 'resultados': { name: 'AcademiaResultados' }, 'reportes': { name: 'AcademiaReportes' }, diff --git a/front/src/views/administrador/procesoadmision/ProcesoAdmisionDetalles.vue b/front/src/views/administrador/procesoadmision/ProcesoAdmisionDetalles.vue new file mode 100644 index 0000000..3557574 --- /dev/null +++ b/front/src/views/administrador/procesoadmision/ProcesoAdmisionDetalles.vue @@ -0,0 +1,357 @@ + + + + + diff --git a/front/src/views/administrador/procesoadmision/ProcesosAdmisionList.vue b/front/src/views/administrador/procesoadmision/ProcesosAdmisionList.vue new file mode 100644 index 0000000..f68495c --- /dev/null +++ b/front/src/views/administrador/procesoadmision/ProcesosAdmisionList.vue @@ -0,0 +1,764 @@ + + + + +