--- asset_id: MIN-EL-Gustavo-20260630-v01 tipo: MIN — Minuta del día owner: Gustavo sherpa: GusX ratificador: Gustavo fecha: 2026-06-30 intellibank: IB-EL-EmpowerLabs subbank: EQ-EL-Equipo proyecto: RCH — Rebelocity Club · perfil-usuario --- ## Qué se hizo ### Revisión de pendientes del 2026-06-29 - Leída minuta `MIN-EL-Gustavo-20260629-v01` y backlog `BCK-EL-Gustavo-RCH-v01`. - Identificados 3 pendientes abiertos: condición de bloqueo del toggle "Quiero registrar mi club", panel de miembros backend+Angular, estado `loading` en `app-mi-club`. --- ### Pendiente #1 — Toggle "Quiero registrar mi club": lógica de 3 estados **Análisis del problema:** - El toggle tenía un rol ambiguo: controlaba tanto la *intención de crear* un club (antes de que existiera) como el *acceso* al club ya creado. - Resultado: si el usuario ya tenía un club y apagaba el toggle, el tab desaparecía sin poder volver a él fácilmente. **Decisión:** reemplazar el toggle por lógica de 3 estados determinados por el backend, no por control local. **Implementación — `perfil-usuario.component.ts`:** - Propiedad `hasClub = false` añadida junto a `myClub`. - Nuevo método `loadMyClub()`: llama `club_serv.getMyClub(captainId)` al iniciar; si responde con data → `hasClub = true` y activa `visibility` del tab `mi-club` en `opsList` de forma permanente. - Constructor: `loadMyClub()` llamado junto a `loadMyMembership()`, ambos bajo el mismo guard `!isProfilePublic && app === 'Rebelocity Club'`. **Implementación — `perfil-usuario.component.html`:** - Bloque del toggle reemplazado por 3 `ng-container`: - **Estado A** (`hasClub`): row estático "Ya tienes un club registrado → Ir a Mi club". Tab siempre visible, sin toggle. - **Estado B** (`!hasClub && myClub`): toggle visualmente bloqueado (`opacity: 0.45; pointer-events: none`) con hint "Para registrar tu propio club primero debes salir del club al que perteneces". - **Estado C** (`!hasClub && !myClub`): toggle libre original + hint condicional "Edita la información de tu club en la pestaña Mi club →" cuando `wantsClubRegister = true`. **Implementación — `perfil-usuario.component.less`:** - Nueva clase `.prof-mi-club-info` para el Estado A: acento azul primario, consistente con el sistema visual existente (`prof-club-block`). - `.prof-club-register-hint` ya existía — reutilizado sin cambios. --- ### Pendiente #2 — Panel de miembros: backend + conexión Angular **Contexto técnico analizado:** - Tablas: `club_members` (genniux_rch) y `clubs` (genniux_rch). Datos de usuario en `genniux_board.perfil` (email, displayName) y `genniux_board.puesto` (id = user_id en club_members). - Conexiones: `$pcn` → genniux_rch · `$pcn2` → genniux_board. Cross-DB JOINs válidos desde `$pcn` usando `genniux_board.tablename` (mismo servidor, root). - Regla de negocio confirmada: **un atleta = un club activo a la vez**. Se valida en backend al agregar. **Backend — `general.php` (3 funciones nuevas):** - `getClubMembers($clubId)` — SELECT con JOIN cross-DB `genniux_board.puesto` + `genniux_board.perfil`. Filtra `status = 'active'`. Devuelve `member_id`, `user_id`, `nombre`, `image`, `miembro_desde`. - `addClubMember($clubId, $searchTerm)`: 1. Busca usuario en `$pcn2` por `email` exacto o `displayName LIKE %term%`. 2. Valida regla un-club: si ya tiene `status = 'active'` en cualquier club → rechaza con mensaje específico (mismo club vs. otro). 3. INSERT `club_members` con `status = 'active'` + `resolved_at = NOW()` + UPDATE `clubs.active_members + 1`. Transacción. - `removeClubMember($memberId, $clubId)` — UPDATE `status = 'left'` validando `club_id` + `status = 'active'`. UPDATE `active_members = GREATEST(active_members - 1, 0)`. Transacción. **Backend — `index.php` (3 rutas nuevas):** - `POST /rch/clubs/members` → `getClubMembers($clubId)` - `POST /rch/clubs/members/add` → `addClubMember($clubId, $searchTerm)` - `POST /rch/clubs/members/remove` → `removeClubMember($memberId, $clubId)` **Frontend — `club.service.ts` (3 métodos nuevos):** - `getClubMembers(clubId)` → `POST /rch/clubs/members` - `addClubMember(clubId, searchTerm)` → `POST /rch/clubs/members/add` - `removeClubMember(memberId, clubId)` → `POST /rch/clubs/members/remove` **Frontend — `mi-club.component.ts`:** - Import `alertOptions` añadido. - Interface `ClubMember` extendida con `member_id: number`. - `loadMembers()` — llamado desde `ngOnInit` una vez que `clubId` está disponible. Mapea respuesta del backend a `ClubMember[]` con `getInitials()`. - `onAddMember()` — valida query no vacío, llama `addClubMember`, refresca lista con `loadMembers()` al éxito. Muestra mensaje de error del backend si falla. - `onRemoveMember(member)` — swal de confirmación con nombre del atleta, llama `removeClubMember`, refresca lista al éxito. - Helper privado `getInitials(nombre)`. **Frontend — `mi-club.component.html`:** - Botón "Agregar" conectado a `onAddMember()`. - Botón "Eliminar" conectado a `onRemoveMember(m)`. --- ### Debugging — `getClubMembers` no mostraba miembros **Síntoma:** el panel de miembros quedaba vacío pese a que el flujo Angular (service → component → template) estaba correctamente conectado. **Causa raíz:** la dirección del cross-DB JOIN estaba invertida. La conexión nativa de cada tabla determina qué nombres llevan prefijo de base de datos: - `$pcn` → conectado a `genniux_rch`: `club_members` y `clubs` **sin prefijo**. - `genniux_board.puesto` y `genniux_board.perfil` **con prefijo**, por ser cross-DB desde `$pcn`. La versión inicial usaba `$pcn2` (genniux_board) como base y prefijaba `genniux_rch.*`, lo cual resultaba en errores de `prepare()` silenciosos (PDO en modo `ERRMODE_SILENT`) que llegaban a la función como un `Fatal error` al llamar `->execute()` sobre `false`. **Método de diagnóstico:** aislar la variable probando primero una consulta sin JOIN (`SELECT * FROM genniux_rch.club_members WHERE club_id = :club_id` vía `$pcn2`) para confirmar que la conectividad cross-DB en sí funcionaba, y luego reintroducir JOINs uno a la vez (`puesto` → `perfil` → `clubs`) hasta aislar el punto de falla. **Query final correcta** (`general.php`, `getClubMembers`): ```sql SELECT cm.id AS member_id, cm.resolved_at AS miembro_desde, p.displayName AS nombre FROM club_members cm JOIN clubs c ON c.id = cm.club_id JOIN genniux_board.puesto pu ON pu.id = cm.user_id JOIN genniux_board.perfil p ON p.wikiname = pu.colaborador WHERE cm.club_id = :club_id AND cm.status = 'active' ORDER BY cm.resolved_at ASC ``` Conexión: `$pcn` (genniux_rch). Se añadió `try/catch` para que cualquier error de `prepare()`/`execute()` se devuelva como JSON en vez de un fatal error silencioso. **Fix adicional — formato de fecha:** el front mostraba la fecha completa (`2026-05-15 14:20:00`). Se agregó `formatMesAnio()` en `mi-club.component.ts` (usa `Intl`/`toLocaleDateString('es-ES', { month: 'long' })`) para mostrar "Miembro desde mayo 2026". --- ## Decisiones - La visibilidad del tab "Mi club" se determina exclusivamente por el backend (`getMyClub`), no por el estado local del toggle. Una vez que `hasClub = true`, el tab es permanente durante esa sesión. - El Estado B (toggle bloqueado) implementa la regla de negocio del mockup: un usuario miembro de otro club no puede registrar el suyo sin salir primero. - No se contempla eliminar un club propio — el Estado A es siempre de solo acceso/edición. - Regla un-atleta-un-club aplicada en backend (`addClubMember`); el front muestra el mensaje de error devuelto por el servidor. - Cross-DB JOIN desde `$pcn` (genniux_rch) hacia `genniux_board.puesto` y `genniux_board.perfil` — válido porque ambas DBs están en el mismo servidor con credenciales root. - **Regla de prefijo cross-DB:** la base de datos nativa de la conexión PDO (`$pcn` → genniux_rch, `$pcn2` → genniux_board) nunca lleva prefijo; solo la base "remota" en el JOIN lo lleva. Invertir esto rompe la query. - PDO en `configrch.php` usa `ERRMODE_SILENT` por defecto → `prepare()` puede devolver `false` sin excepción. Envolver queries cross-DB en `try/catch` + chequeo explícito de `$query === false` para evitar fatal errors silenciosos. --- ### Pendiente #3 — Estado `loading` en `app-mi-club` **Solución:** spinner CSS usando la propiedad `loading: boolean` que ya existía en el componente (iniciaba en `true`, pasaba a `false` en el callback de `getMyClub()`). Solo faltaba usarla en el template. **Implementación — `mi-club.component.html`:** - Bloque `