Este documento proporciona un análisis detallado del módulo Areas del proyecto, siguiendo un orden lógico y cronológico de creación de los elementos.
Schema::create('areas', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->foreignId('user_id')->constrained('users');
$table->foreignId('parent_id')->nullable()->constrained('areas');
$table->boolean('featured')->default(false);
$table->enum('status', ['draft', 'published', 'suspended'])->default('draft');
$table->timestamps();
$table->softDeletes();
$table->index('name');
$table->index('slug');
$table->index('user_id');
$table->index('parent_id');
});
Schema::table('areas', function (Blueprint $table) {
$table->integer('sort_order')->default(0)->after('status');
$table->json('meta')->nullable()->after('featured')
->comment('Para guardar metadatos adicionales, como SEO, configuraciones, etc.');
$table->index('sort_order');
$table->index(['parent_id', 'sort_order']);
$table->index(['featured', 'sort_order']);
});
- Usa HasFactory, SoftDeletes, HasMediaTrait, GeneratesSlug
- Implementa HasMedia para gestión de medios
protected $fillable = [
'name',
'slug',
'description',
'user_id',
'parent_id',
'featured',
'status',
'sort_order',
'meta'
];
protected $casts = [
'featured' => 'boolean',
'meta' => 'json'
];
- user(): BelongsTo - Relación con el usuario creador
- parent(): BelongsTo - Relación con el área padre
- children(): HasMany - Relación con áreas hijas
- paths(): HasMany - Relación con rutas asociadas
- registerCoverMediaCollection(): Gestión de imágenes de portada
- scopePublished(): Filtro para áreas publicadas
- scopeFeatured(): Filtro para áreas destacadas
- scopeOrdered(): Ordenamiento por sort_order
- getFullPathAttribute(): Obtiene ruta jerárquica completa
- getAreasByParentId(): Obtiene áreas hijas
- getHierarchicalList(): Lista jerárquica para selects
- getChildrenHierarchy(): Función auxiliar para jerarquía
Route::get('/areas', [AreaController::class, 'publicIndex'])->name('areas.index');
Route::get('/areas/{slug}', [AreaController::class, 'publicShow'])->name('areas.show');
// Gestión de papelera
Route::get('areas/trashed', [AreaController::class, 'privateTrashed'])->name('areas.trashed');
Route::patch('areas/{area}/restore', [AreaController::class, 'privateRestore'])->name('areas.restore');
Route::delete('areas/{area}/force-delete', [AreaController::class, 'privateForceDelete'])->name('areas.force-delete');
// CRUD principal
Route::get('areas/create', [AreaController::class, 'privateCreate'])->name('areas.create');
Route::get('areas/{area}/edit', [AreaController::class, 'privateEdit'])->name('areas.edit');
Route::get('areas', [AreaController::class, 'privateIndex'])->name('areas.index');
Route::post('areas', [AreaController::class, 'privateStore'])->name('areas.store');
Route::match(['put', 'patch'], 'areas/{area}', [AreaController::class, 'privateUpdate'])->name('areas.update');
Route::delete('areas/{area}', [AreaController::class, 'privateDestroy'])->name('areas.destroy');
Route::get('areas/{area}', [AreaController::class, 'privateShow'])->name('areas.show');
Route::get('/api/search/areas', [AreaSearchController::class, 'suggestions'])->name('api.areas.search');
Route::get('/api/search/areas/trashed', [AreaTrashedSearchController::class, 'suggestions'])->name('api.areas.trashed.search');
// Para profesores (middleware: role:teacher)
Route::prefix('workarea')->name('workarea.')->group(function () {
Route::get('areas', [AreaController::class, 'educaIndex'])->name('areas.index');
Route::get('areas/{slug}', [AreaController::class, 'educaShow'])->name('areas.show');
Route::get('areas/{slug}/progress', [AreaController::class, 'educaProgress'])->name('areas.progress');
});
// Para estudiantes (middleware: role:student)
Route::prefix('classroom')->name('classroom.')->group(function () {
Route::get('areas', [AreaController::class, 'educaIndex'])->name('areas.index');
Route::get('areas/{slug}', [AreaController::class, 'educaShow'])->name('areas.show');
Route::get('areas/{slug}/progress', [AreaController::class, 'educaProgress'])->name('areas.progress');
});
El módulo utiliza el trait HasMediaTrait
que proporciona:
-
Colecciones de Medios:
- default: Colección general de medios
- cover: Imágenes de portada (single file)
- files: Archivos generales
-
Conversiones Automáticas:
- thumb: Miniatura (dimensiones configurables)
- medium: Tamaño medio
- large: Tamaño grande
-
Métodos Principales:
// Registro de colecciones registerMediaCollections() // Registro de conversiones registerMediaConversions(Media $media = null) // Gestión de usuarios de medios getMediaUser() setMediaUser($userId)
-
Configuración:
- Disco: public
- Dimensiones: Configurables en config/media.php
- Formatos soportados: jpg, jpeg, png, webp
-
Controladores Específicos:
- AreaSearchController: Búsqueda general
- AreaTrashedSearchController: Búsqueda en papelera
-
Componente Frontend:
- x-search-autocomplete: Búsqueda con sugerencias en tiempo real
protected function hasCircularReference($area, $newParentId, $visited = []): bool
{
// Verifica si el área es su propio padre
if ($area->id === $newParentId) {
return true;
}
// Verifica descendientes
$descendants = $area->children()->pluck('id')->toArray();
return in_array($newParentId, $descendants);
}
// En el método rules() de AreaRequest
if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
$rules['name'][] = Rule::unique('areas')->ignore($this->route('area'));
} else {
$rules['name'][] = 'unique:areas';
}
protected function prepareForValidation(): void
{
if ($this->has('featured')) {
$this->merge([
'featured' => $this->featured === 'true' ||
$this->featured === '1' ||
$this->featured === true,
]);
}
}
-
Teachers: Acceso completo a funcionalidades educativas (workarea)
- Visualización de áreas
- Seguimiento de progreso
- Gestión de contenido educativo
-
Students: Acceso limitado (classroom)
- Visualización de áreas publicadas
- Seguimiento de progreso personal
- Sin capacidad de modificación
-
educaIndex():
- Accesible para teachers y students
- Muestra áreas publicadas
- Filtrado según rol del usuario
-
educaShow():
- Accesible para teachers y students
- Vista detallada del área
- Contenido adaptado según rol
-
educaProgress():
- Accesible para teachers y students
- Teachers: Visualización de progreso de todos los estudiantes
- Students: Solo su progreso personal
El módulo utiliza el sistema de traducciones de Laravel con las siguientes características:
-
Archivo de Traducciones:
- Ubicación:
resources/lang/es/messages.php
- Estructura: clave = texto exacto a mostrar
'Áreas' => 'Áreas', // Español 'Áreas' => 'Areas', // Inglés (en messages.php de /en/)
- Ubicación:
-
Uso en Vistas:
{{ __('Áreas') }} // Título del módulo {{ __('Crear Área') }} // Botón de crear {{ __('No se pueden crear referencias circulares en la jerarquía') }} // Mensaje de error
-
Textos con Variables:
{{ __(':item creado correctamente', ['item' => 'Área']) }}
-
Generales:
- x-input-label:
{{ __('Nombre') }}
- x-text-input: placeholder="{{ __('Introduce el nombre del área') }}"
- x-input-error: mensajes de validación traducidos
- x-required-mark
- x-secondary-button:
{{ __('Cancelar') }}
- x-primary-button:
{{ __('Guardar') }}
- x-input-label:
-
Específicos de Areas:
- x-area-status: estados traducidos
- x-area-actions: acciones traducidas
- x-media.single-image-upload: textos de upload traducidos
- x-search-autocomplete: placeholders traducidos
-
index.blade.php:
<h1>{{ __('Áreas') }}</h1> <h2>{{ __('Áreas destacadas') }}</h2>
-
form.blade.php (create/edit):
<h1>{{ __('Crear Área') }}</h1> <h1>{{ __('Editar Área') }}</h1>
-
show.blade.php:
<h2>{{ __('Detalles del Área') }}</h2> <p>{{ __('Creado el') }}: {{ $area->created_at }}</p>
-
trashed.blade.php:
<h1>{{ __('Áreas eliminadas') }}</h1>
-
Archivos a Revisar:
- Todas las vistas blade del módulo
- Componentes relacionados
- Mensajes de validación
- Mensajes de respuesta JSON
- Notificaciones
-
Proceso de Revisión:
# Buscar textos hardcodeados en vistas grep -r ">" resources/views/areas # Buscar textos en componentes grep -r ">" resources/views/components # Revisar mensajes de validación grep -r "message" app/Http/Requests
-
Puntos de Atención:
- Textos directos en HTML
- Placeholders de inputs
- Mensajes de error
- Títulos y encabezados
- Botones y enlaces
- Tooltips y ayudas
- Mensajes de confirmación
resources/lang/
├── es/
│ ├── areas.php # Textos específicos del módulo
│ ├── validation.php # Mensajes de validación
│ └── messages.php # Mensajes generales
└── en/
├── areas.php
├── validation.php
└── messages.php
Las traducciones se centralizan en el archivo resources/lang/es/messages.php
, utilizando una estructura plana donde la clave es el texto exacto a traducir:
// resources/lang/es/messages.php
return [
'Áreas' => 'Áreas', // Español
'Áreas' => 'Areas', // Inglés (en messages.php de /en/)
];
-
En Vistas:
{{ __('Áreas') }} {{ __('Ver detalles') }}
-
En Controladores:
return back()->with('error', __('No se puede eliminar un área que tiene sub-áreas'));
-
En Validaciones:
'name.required' => __('El campo :attribute es obligatorio')
-
Mensajes de Error en AreaService:
throw new \Exception('No se puede eliminar un área que tiene sub-áreas.');
-
Etiquetas en form.blade.php:
<option value="">Selecciona un área padre</option>
-
Estados en index.blade.php:
<span>Borrador</span> <span>Publicado</span> <span>Suspendido</span>
- Crear archivos de idioma base (de momento tendrmos solo 'es', pero quedaría preparado para traducir fácilmente a inglés)
- Mover todos los textos hardcodeados a los archivos de traducción
- Reemplazar textos en código por llamadas a helper de traducción
- Implementar fallback a español si falta traducción
- Añadir nuevos idiomas según necesidad
APP_LOCALE=es
APP_FALLBACK_LOCALE=es
- APP_LOCALE: Idioma principal (default: es)
- APP_FALLBACK_LOCALE: Idioma de respaldo (default: es)
- PAGINATION_PER_PAGE: Elementos por página (default: 12)
- MEDIA_MAX_FILE_SIZE: Tamaño máximo general (default: 10240)
- MEDIA_MAX_DIMENSIONS: Dimensiones máximas (default: 2048)
- COVER_MAX_FILE_SIZE: Tamaño máximo de covers (default: 2048)
- COVER_MAX_DIMENSIONS: Dimensiones máximas de covers (default: 2000)
- MEDIA_THUMB_SIZE: Tamaño de miniaturas (default: 150)
- MEDIA_MEDIUM_SIZE: Tamaño medio (default: 800)
- MEDIA_LARGE_SIZE: Tamaño grande (default: 1600)
// config/media.php
'cover' => [
'max_file_size' => env('COVER_MAX_FILE_SIZE', 2048),
'max_dimensions' => env('COVER_MAX_DIMENSIONS', 2000),
'allowed_types' => ['jpg', 'jpeg', 'png', 'webp'],
],
'conversions' => [
'thumb' => [
'width' => env('MEDIA_THUMB_SIZE', 150),
'height' => env('MEDIA_THUMB_SIZE', 150),
],
'medium' => [
'width' => env('MEDIA_MEDIUM_SIZE', 800),
'height' => env('MEDIA_MEDIUM_SIZE', 800),
],
'large' => [
'width' => env('MEDIA_LARGE_SIZE', 1600),
'height' => env('MEDIA_LARGE_SIZE', 1600),
],
]
-
app/Traits/HasMediaTrait.php
:public function registerMediaCollections(): void { // ... if (method_exists($this, 'registerCoverMediaCollection')) { $this->addMediaCollection('cover') ->singleFile() ->useDisk('public') ->acceptsFile(function ($file) { return in_array( $file->mimeType, array_map(fn($ext) => 'image/' . $ext, config('media.cover.allowed_types')) ); }) ->withResponsiveImages(); } }
-
app/Http/Requests/AreaRequest.php
:'cover' => [ 'nullable', 'file', 'mimes:' . implode(',', config('media.cover.allowed_types')), 'max:' . config('media.cover.max_file_size'), 'dimensions:max_width=' . config('media.cover.max_dimensions') . ',max_height=' . config('media.cover.max_dimensions') ],
-
resources/views/components/media/single-image-upload.blade.php
:<x-media-upload :maxSize="config('media.cover.max_file_size')" :maxDimensions="config('media.cover.max_dimensions')" :allowedTypes="config('media.cover.allowed_types')" />
'name' => ['required', 'string', 'min:3', 'max:255'],
'description' => ['nullable', 'string', 'max:1000'],
'parent_id' => ['nullable', 'exists:areas,id'],
'featured' => ['boolean'],
'status' => ['required', Rule::in(['draft', 'published', 'suspended'])],
'sort_order' => ['nullable', 'integer', 'min:0'],
'cover' => [
'nullable',
'file',
'mimes:jpg,jpeg,png,webp',
'max:' . env('COVER_MAX_FILE_SIZE', 2048),
'dimensions:max_width=' . env('COVER_MAX_DIMENSIONS', 2000) .
',max_height=' . env('COVER_MAX_DIMENSIONS', 2000)
],
'meta' => ['nullable', 'array'],
'meta.title' => ['nullable', 'string', 'max:60'],
'meta.description' => ['nullable', 'string', 'max:160'],
'meta.keywords' => ['nullable', 'string', 'max:255']
- Prevención de referencias circulares en jerarquía
- Validación condicional para nombres únicos en actualizaciones
- Preparación de datos booleanos
-
create(array $data): Area
- Crea nueva área
- Gestiona sort_order automático
- Procesa imagen de portada
- Usa transacciones DB
-
update(Area $area, array $data): Area
- Actualiza área existente
- Gestiona cambios de jerarquía
- Actualiza sort_order si necesario
- Procesa imagen de portada
-
delete(Area $area): bool
- Verifica dependencias (áreas hijas)
- Reordena áreas hermanas
- Elimina medios asociados
-
updateOrder(array $orderedIds, ?int $parentId): void
- Actualiza orden de áreas
- Gestiona jerarquía
- Admin: gestión completa de áreas
- Auth: acceso a sección educativa (Creo que... ¿teachers y students?)
- publicIndex(): Lista áreas publicadas
- publicShow($slug): Muestra área pública
- privateIndex(): Lista todas las áreas
- privateCreate(): Formulario de creación
- privateStore(): Almacena nueva área
- privateShow(): Muestra área en admin
- privateEdit(): Formulario de edición
- privateUpdate(): Actualiza área
- privateDestroy(): Elimina área
- privateTrashed(): Lista áreas eliminadas
- privateRestore(): Restaura área
- privateForceDelete(): Elimina permanentemente
- educaIndex(): Lista áreas ¿para teachers y students?
- educaShow(): Muestra área educativa
- educaProgress(): Muestra progreso
- Usa x-app-layout (layout principal de la aplicación)
- Compartido con otros módulos como Users
-
Generales:
- x-input-label
- x-text-input
- x-input-error
- x-required-mark
- x-secondary-button
- x-primary-button
-
Específicos de Areas:
- x-area-status
- x-area-actions
- x-media.single-image-upload
- x-search-autocomplete
-
index.blade.php:
- Lista de áreas destacadas
- Tabla de áreas regulares
- Buscador con autocompletado
- Paginación
- Acciones CRUD
-
form.blade.php (create/edit):
- Información básica
- Selección de área padre
- Estado y destacado
- Gestión de imagen
- Metadatos SEO
- Validación client-side
-
show.blade.php:
- Detalles del área
- Imagen de portada
- Metadatos
- Áreas relacionadas
-
trashed.blade.php:
- Lista de áreas eliminadas
- Opciones de restauración
- Eliminación permanente
-
progress.blade.php:
- Estadísticas de progreso
- Gráficos de avance
- Filtros por periodo
- En UserSeeder.php se crean usuarios del sistema.
- En el resto de seeders se crean areas, paths y cursos.
- Puedes usar los registros existentes o bien crear los tuyos propios. Pero solo puedes modificar o eliminar los que creaste.
Areas
└── Paths
└── Courses
└── Contents
- Representan las categorías principales del sistema educativo
- Pueden tener sub-áreas (estructura jerárquica propia)
- Contienen múltiples Paths
- Rutas de aprendizaje específicas dentro de un área
- Siempre pertenecen a un área específica
- Contienen una secuencia ordenada de cursos
- Permisos Completos:
- manage users
- manage areas
- view areas
- restore areas
- manage paths
- view paths
- manage courses
- view courses
- manage contents
- view contents
- manage comments
- view comments
- manage medias
- view medias
- Permisos de Visualización:
- view areas
- view paths
- view courses
- edit own courses
- view contents
- edit own contents
- view comments
- edit own comments
- view medias
- edit own medias
- Permisos de Lectura:
- view areas
- view paths
- view courses
- view contents
- view comments
- edit own comments
- view medias
draft
: Borrador inicialpublished
: Área publicada y visiblesuspended
: Área temporalmente suspendida
- Crear área con datos válidos
- Crear sub-área (validar jerarquía)
- Validar campos requeridos
- Verificar relación padre-hijo
- Prevenir referencias circulares
- Validar orden de áreas hermanas
- Cambiar estado a published
- Cambiar estado a suspended
- Validar visibilidad según estado
- Acceso admin a todas las operaciones
- Acceso de lectura para teacher
- Acceso de lectura para student
- Soft delete de área sin dependencias
- Prevenir eliminación con sub-áreas
- Restaurar área eliminada
- No se pueden crear referencias circulares
- No se puede eliminar un área con sub-áreas
- No se puede mover un área a un descendiente propio
- No autorizado para crear áreas
- No autorizado para editar áreas
- No autorizado para eliminar áreas
- No autorizado para restaurar áreas
- Nombre requerido (mínimo 3 caracteres)
- Slug único
- Estado válido (draft/published/suspended)
AreaCreated
: Al crear una nueva áreaAreaUpdated
: Al actualizar un área existenteAreaDeleted
: Al eliminar un área (soft delete)AreaStatusChanged
: Al cambiar el estado
- El área se marca como eliminada
- No aparece en consultas regulares
- Mantiene sus relaciones para referencia
- Se puede restaurar si es necesario