Feature: Carpetas / Jerarquía de Eventos
Sistema de organización jerárquica **puramente visual** para eventos en el admin dashboard. Los eventos tienen sus propios permisos y configuraciones — esto es solo orden. El usuario agrupa eventos en carpetas y subcarpetas.
Feature: Carpetas / Jerarquía de Eventos
Estado: ✅ Implementado y en producción
¿Qué es esto?
Sistema de organización jerárquica puramente visual para eventos en el admin dashboard. Los eventos tienen sus propios permisos y configuraciones — esto es solo orden. El usuario agrupa eventos en carpetas y subcarpetas.
Pantalla: https://postea.la/admin/event-hub/{id} → sección "Seleccionar Evento" (componente event-grid-selector)
Perfil de usuario objetivo: Clientes corporativos (universidades, empresas) con 20-100+ eventos.
Caso de uso concreto: UPC
📁 Facultad de Ingeniería
📁 2025
📋 Graduación Sistemas - Julio
📋 Graduación Industrial - Noviembre
📁 2024
📋 Graduación Sistemas - Julio
📁 Facultad de Administración
📁 2025
📋 Graduación MBA - Agosto
📁 Eventos Institucionales
📋 Feria de Empleo 2025
📋 Evento suelto (sin carpeta — siempre válido)
Modelo de datos implementado
Tabla: folders (nombre final en BD)
CREATE TABLE folders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
owner_user_id INT NOT NULL,
parent_id BIGINT NULL,
name VARCHAR(100) NOT NULL,
sort_order INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_folder_parent FOREIGN KEY (parent_id) REFERENCES folders(id),
CONSTRAINT fk_folder_user FOREIGN KEY (owner_user_id) REFERENCES users(id)
);
Campo en tabla events
ALTER TABLE events ADD COLUMN folder_id BIGINT NULL;
ALTER TABLE events ADD CONSTRAINT fk_event_folder FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE SET NULL;
ON DELETE SET NULL: si se elimina una carpeta sin cascade (edge case), los eventos no se pierden.
Entidades Java
Folder.java— entidad conownerUserId(Integer),parentId,name,sortOrder,createdAtEvent.java— campofolderId(Long, nullable)
Nota: ownerUserId en Folder es Integer (mismo tipo que en Event). Al comparar con event.getOwnerUserId() (primitivo long) usar ownerUserId.longValue() != event.getOwnerUserId() — .equals() entre Integer y long siempre devuelve false.
API implementada
Base: /api/folders
GET /api/folders/tree?userId={id} → árbol completo con contadores
POST /api/folders?userId={id} → { name, parentId? }
PUT /api/folders/{id}/rename?userId={id} → { name }
PUT /api/folders/{id}/move?userId={id} → { newParentId } (null = mover a raíz)
DELETE /api/folders/{id}?userId={id} → cascade completo (subcarpetas + eventos)
PUT /api/folders/assign/{eventSlug}?userId={id} → { folderId } (null = quitar de carpeta)
DELETE /api/folders/events/{eventSlug}?userId={id} → hard delete completo del evento
Backend: estructura de services
FolderService / FolderServiceImpl
Toda la lógica de negocio de carpetas:
getTree(ownerUserId)— árbol completo condirectEventCountytotalEventCount(recursivo)createFolder,renameFolder,moveFolderdeleteFolder— cascade: subcarpetas → eventos (viaEventDeletionService) → carpetaassignEventToFolder(slug, folderId, userId)— asigna o quita evento de carpetadeleteEvent(slug, userId)— hard delete de evento (delega aEventDeletionService)
Límite de profundidad: MAX_FOLDER_DEPTH = 5 (constante estática).
EventDeletionService / EventDeletionServiceImpl
Hard delete completo de un evento:
- Fotos —
AdminPhotoService.deletePhoto(id)por cada foto: elimina S3 (url+thumbnailUrl+compressedUrl) + hard delete BD - Videos —
VideoService.deleteVideo(id)por cada video: elimina S3 (url+thumbnailUrl) + hard delete BD - Registros hijos (eliminar antes del evento por FK
NO ACTIONde Hibernate):event_ia_moderation_settingsevent_invitation_cardsevent_inviteesevent_status_messagesevent_tablesmemoriesevent_floor_markers
- Evento — hard delete del registro
Las tablas con FK CASCADE se limpian automáticamente: event_stats, event_activity_tracking, custom_albums, payment_transactions (SET NULL).
Nota FK crítica: Hibernate genera constraints FK con NO ACTION además de las FK originales de MySQL con CASCADE. MySQL ejecuta ambas — la NO ACTION bloquea el delete aunque la otra tenga CASCADE. Por eso hay que eliminar los hijos explícitamente antes de borrar el evento.
VideoService / VideoServiceImpl
Extraído de VideoController. Provee:
findByEventSlug(slug)— lista videos del eventodeleteVideo(id)— elimina S3 (url+thumbnailUrl) + hard delete BD
El endpoint de soft-delete para invitados DELETE /api/videos/{id} (ventana 30 min) sigue en VideoController sin cambios.
DTOs implementados
| DTO | Novedades |
|---|---|
FolderDto |
id, parentId, name, sortOrder, directEventCount, totalEventCount, children, createdAt |
CreateFolderDto |
name, parentId (nullable) |
RenameFolderDto |
name |
MoveFolderDto |
newParentId (nullable = mover a raíz) |
AssignEventToFolderDto |
folderId (nullable = quitar de carpeta) |
EventWithStatsDto |
+ campo folderId (Long, nullable) — necesario para que el frontend categorice eventos |
EventWithStatsDto.folderId se puebla en AdminPhotoService con .folderId(event.getFolderId()). Sin este campo, los eventos asignados a carpetas seguirían apareciendo en "Eventos sin carpeta" al recargar.
Frontend: event-grid-selector
Componente: EventGridSelectorComponent
ViewEncapsulation.None — necesario para que los estilos del overlay CDK (renderizados en <body>) reciban las clases del componente.
Navegación
currentFolderId: number | null— null = raízcurrentFolderChildren— subcarpetas del nivel actualcurrentFolderEvents— eventos del nivel actual (filtro porfolderId)rootEvents— alias decurrentFolderEventsen raízbreadcrumb: FolderDto[]— construido al navegarenterFolder(folder)/navigateHome()/navigateTo(index)— navegación
Menú contextual con CDK Overlay
Los menús [⋮] usan FlexibleConnectedPositionStrategy de Angular CDK — se renderizan en <body> via TemplatePortal, completamente fuera del stacking context del grid.
Por qué: CDK DragDrop aplica transform a los items del grid, creando un nuevo stacking context. El z-index dentro de esos elementos queda atrapado. CDK Overlay escapa ese problema al renderizar en <body>.
Bug corregido: closeMenuOverlay() NO debe limpiar activeEvent/activeFolder. El backdrop del overlay dispara antes que el click en el botón del menú, lo que limpiaba el active antes de que openDeleteEventModal lo recibiera. La solución fue sacar esas asignaciones de closeMenuOverlay().
Drag & drop
Reordenamiento (entre cards del mismo nivel): HTML5 nativo (draggable="true" + eventos dragstart/dragover/dragleave/dragend/drop). Indicador visual: sombra morada izquierda/derecha según qué mitad de la card destino se hova. Usa moveItemInArray de CDK solo para el reorden final en el array.
Mover a carpeta: los folder cards escuchan dragover/dragleave/drop nativos. Al soltar sobre una carpeta, se llama assignEvent(ev, targetFolderId) que hace PUT /api/folders/assign/{slug}.
CDK DragDrop fue removido completamente del reordenamiento — se quedó solo el moveItemInArray de utilería.
FolderService (Angular)
frontend/src/app/services/folder.service.ts — métodos:
getFolderTree(userId)→GET /api/folders/treecreateFolder(userId, dto)→POST /api/foldersrenameFolder(folderId, userId, name)→PUT /api/folders/{id}/renamemoveFolder(folderId, userId, newParentId)→PUT /api/folders/{id}/movedeleteFolder(folderId, userId)→DELETE /api/folders/{id}assignEventToFolder(slug, userId, folderId)→PUT /api/folders/assign/{slug}deleteEvent(slug, userId)→DELETE /api/folders/events/{slug}
Validaciones de negocio
| Caso | Comportamiento |
|---|---|
| Crear subcarpeta cuando depth = MAX_FOLDER_DEPTH | 400 — "Máximo 5 niveles" |
| Mover carpeta a sí misma | 400 |
| Mover carpeta a uno de sus descendientes | 400 — detectado con isDescendant() |
| Mover carpeta que resultaría en depth > MAX | 400 |
| Operar sobre carpeta de otro usuario | 403 |
| Carpeta inexistente | 404 |
| Evento no encontrado al asignar/eliminar | 400 |
| Evento de otro usuario | 403 |
Lo que NO se elimina al borrar un evento
- Carpetas (quedan intactas)
- El usuario propietario
- Logos de usuario (
users/{id}/company-logo/) - Logo del evento (
events/{slug}/logo/) — no está en tablaphotosnivideos payment_transactions— solo se poneevent_id = NULL
Notas de deploy
- La tabla
foldersy la columnafolder_ideneventsdeben existir en producción antes del deploy. - El insert de datos en
folderses directo via SQL (no Flyway) — ver protocolo en CLAUDE.md. - Backend deploy:
./deploy-backend-prod.sh - Frontend deploy:
./deploy-frontend.sh