Funcionalidades

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 con ownerUserId (Integer), parentId, name, sortOrder, createdAt
  • Event.java — campo folderId (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 con directEventCount y totalEventCount (recursivo)
  • createFolder, renameFolder, moveFolder
  • deleteFolder — cascade: subcarpetas → eventos (via EventDeletionService) → carpeta
  • assignEventToFolder(slug, folderId, userId) — asigna o quita evento de carpeta
  • deleteEvent(slug, userId) — hard delete de evento (delega a EventDeletionService)

Límite de profundidad: MAX_FOLDER_DEPTH = 5 (constante estática).

EventDeletionService / EventDeletionServiceImpl

Hard delete completo de un evento:

  1. Fotos — AdminPhotoService.deletePhoto(id) por cada foto: elimina S3 (url + thumbnailUrl + compressedUrl) + hard delete BD
  2. Videos — VideoService.deleteVideo(id) por cada video: elimina S3 (url + thumbnailUrl) + hard delete BD
  3. Registros hijos (eliminar antes del evento por FK NO ACTION de Hibernate):
    • event_ia_moderation_settings
    • event_invitation_cards
    • event_invitees
    • event_status_messages
    • event_tables
    • memories
    • event_floor_markers
  4. 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 evento
  • deleteVideo(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íz
  • currentFolderChildren — subcarpetas del nivel actual
  • currentFolderEvents — eventos del nivel actual (filtro por folderId)
  • rootEvents — alias de currentFolderEvents en raíz
  • breadcrumb: FolderDto[] — construido al navegar
  • enterFolder(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/tree
  • createFolder(userId, dto) → POST /api/folders
  • renameFolder(folderId, userId, name) → PUT /api/folders/{id}/rename
  • moveFolder(folderId, userId, newParentId) → PUT /api/folders/{id}/move
  • deleteFolder(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 tabla photos ni videos
  • payment_transactions — solo se pone event_id = NULL

Notas de deploy

  • La tabla folders y la columna folder_id en events deben existir en producción antes del deploy.
  • El insert de datos en folders es directo via SQL (no Flyway) — ver protocolo en CLAUDE.md.
  • Backend deploy: ./deploy-backend-prod.sh
  • Frontend deploy: ./deploy-frontend.sh