diff --git a/Docs/DEPLOYMENT.md b/Docs/DEPLOYMENT.md new file mode 100644 index 0000000..743e23e --- /dev/null +++ b/Docs/DEPLOYMENT.md @@ -0,0 +1,315 @@ +# Despliegue — CI/CD con GitHub Actions + +Este documento describe cómo quedan configurados el pipeline de build/publish a GHCR y el +despliegue automático en el servidor privado, y qué pasos manuales (una sola vez) hacen falta +para que funcione. + +Los workflows viven en `.github/workflows/deploy-backend.yml` y +`.github/workflows/deploy-frontend.yml`. Cada uno tiene dos jobs: `build` (en un runner de GitHub, +compila y publica la imagen en GHCR) y `deploy` (en el runner self-hosted del servidor, la baja y +reinicia el contenedor correspondiente). Un push a `develop` que solo toca `Club12-Backend/**` +dispara únicamente el workflow de backend, y viceversa; un cambio en `docker-compose.yml` o en los +workflows dispara ambos. + +## 1. Prerrequisitos del servidor + +- Host Debian con Docker Engine + Docker Compose v2 instalados (`docker compose version` debe + responder `v2.x`). +- El usuario que corre el runner (`ghrunner`) debe pertenecer al grupo `docker`: + + ```bash + sudo usermod -aG docker ghrunner + # cerrar sesión y volver a entrar (o `newgrp docker`) para que el cambio de grupo tome efecto + ``` + + Verificar como ese usuario, sin `sudo`: + + ```bash + docker ps + ``` + + Si falla por permisos, el grupo no se aplicó todavía. + +## 2. Registrar los runners self-hosted (manual, una vez por runner) + +Se necesitan **dos runners registrados por separado**, uno por cada `runs-on` label. Un mismo +runner no puede sostener dos jobs en simultáneo. + +Obtener un token de registro en: +`https://github.com/FrancoRu/Taller-de-Integracion/settings/actions/runners/new` + +Backend, en `/home/ghrunner/actions-runner` (o el directorio equivalente): + +```bash +./config.sh --url https://github.com/FrancoRu/Taller-de-Integracion \ + --token \ + --name club12-back \ + --labels Club-12-back-runner \ + --work _work +sudo ./svc.sh install ghrunner && sudo ./svc.sh start +``` + +Repetir en un **segundo directorio de runner, separado**, con: + +```bash +./config.sh --url https://github.com/FrancoRu/Taller-de-Integracion \ + --token \ + --name club12-front \ + --labels Club-12-front-runner \ + --work _work +sudo ./svc.sh install ghrunner && sudo ./svc.sh start +``` + +Notas importantes: + +- `self-hosted` se agrega automáticamente al registrar el runner — **no** hay que pasarlo en + `--labels`. +- Los strings de label son sensibles a mayúsculas/minúsculas y deben coincidir exactamente con los + workflows: `Club-12-back-runner` y `Club-12-front-runner`. +- Ambos runners corren en el mismo host (`192.168.0.200`), sobre el mismo proyecto de compose + (`club12`) — por diseño, para que ambos jobs de `deploy` puedan compartir el mismo directorio de + compose sin pisarse. + +## 3. Bootstrap del `.env` de producción (manual, una sola vez) + +El pipeline **nunca** crea, lee ni sobrescribe el `.env` de producción. Si falta, el paso de +sincronización del job `deploy` falla de forma explícita y detiene el workflow. + +```bash +mkdir -p /home/docker-compose/Club12 +cp .env.example /home/docker-compose/Club12/.env +# completar cada valor CHANGE_ME en /home/docker-compose/Club12/.env +chmod 600 /home/docker-compose/Club12/.env +``` + +Este archivo vive de forma permanente en `/home/docker-compose/Club12/`, un directorio distinto al +workspace efímero que usa `actions/checkout` — por eso `git clean -ffdx` del checkout nunca lo +toca. + +## 4. Visibilidad del paquete en GHCR + +Los paquetes de GHCR son **privados por defecto**, incluso en un repositorio público. El job de +`deploy` ya hace `docker login ghcr.io` con un token con permiso `read:packages`, así que el +`pull` funciona sin cambios adicionales. Alternativa: si se prefiere evitar el login, se puede +cambiar la visibilidad del paquete a público desde la UI de GHCR +(`Package settings → Change visibility`). + +## 5. Rollback manual + +Si un deploy deja el servicio en mal estado, se puede volver a la imagen anterior directamente en +el servidor, sin pasar por GitHub: + +```bash +cd /home/docker-compose/Club12 +docker tag ghcr.io/francoru/club12-backend:previous ghcr.io/francoru/club12-backend:latest +docker compose up -d --no-deps --no-build backend +``` + +(Reemplazar `backend` por `frontend` y el nombre de imagen correspondiente para el frontend.) + +## 6. Desarrollo local (sin cambios) + +`docker-compose.yml` conserva los bloques `build:` como fallback local — el pipeline de CI nunca +los usa (`--no-build` en cada deploy), pero siguen funcionando para levantar el proyecto en la +máquina de desarrollo: + +```bash +docker compose build && docker compose up -d +``` + +En un host que no sea Linux, la ruta absoluta del bind mount del servicio `db` +(`/home/docker/club12/db`) no aplica: usar un `docker-compose.override.yml` (ignorado por +git) que reemplace esa línea de volumen por un volumen nombrado local. + +## 7. Migración a Postgres self-hosted (manual, una sola vez) + +A partir del cambio `selfhosted-postgres-db`, `docker-compose.yml` incluye un servicio +`db` (`postgres:17-alpine`) en la red interna `club12`, **sin puerto publicado**. El +backend deja de hablar con Supabase Postgres y pasa a usar ese contenedor. Supabase +**Storage** (buckets de imágenes y fichas médicas) se sigue usando igual — no se toca. + +El pipeline de CI **no** levanta `db` (`deploy-backend.yml` usa `--no-deps`). El servicio +se levanta una vez en el cutover y se mantiene solo por `restart: unless-stopped`. + +### 7.1 Preparar el host + +```bash +# Directorio de datos de Postgres — en /home (420 GB), NO en la partición raíz (31 GB) +sudo mkdir -p /home/docker/club12/db +sudo chown 999:999 /home/docker/club12/db # uid del usuario postgres en la imagen alpine + +# Directorio de backups +sudo mkdir -p /home/docker/backups/club12 +``` + +### 7.2 Completar el `.env` de producción + +En `/home/docker-compose/Club12/.env` (owner `gh-runner:gh-runner`, mode 600) agregar las +tres claves nuevas y reescribir la connection string. Elegir la contraseña ahora +(`openssl rand -base64 24`); `Username`/`Password` de la connection string **deben +coincidir** con `POSTGRES_USER`/`POSTGRES_PASSWORD`. + +```bash +POSTGRES_USER=postgres +POSTGRES_DB=postgres +POSTGRES_PASSWORD= + +# reemplaza el valor anterior (pooler de Supabase): +ConnectionStrings__DbConnection=Host=db;Port=5432;Database=postgres;Username=postgres;Password=;SSL Mode=Disable + +# activa el backup periódico + restore-desde-panel ya incluido en la app +# (reemplaza al cron manual — ver §7.5): +Backup__Enabled=true +``` + +> Las variables `POSTGRES_*` sólo inicializan la base en el **primer arranque** del +> contenedor (directorio de datos vacío). Después, para cambiar la contraseña hay que +> hacerlo con `ALTER USER` dentro de la base. + +### 7.3 Dump fresco de Supabase + +Desde el host, usando `postgres:17-alpine` como cliente (no depende de la versión de +`pg_dump` del host). Los datos del app viven en **dos** schemas: `public` (Identity / +`AspNet*`) y `Club12` (todo lo demás). Pasá las credenciales como variables de entorno +para no pelear con el URL-encoding de la contraseña; sacá los valores del `.env` actual +(`grep -i DbConnection /home/docker-compose/Club12/.env`) — `User Id=` → `PGUSER`, +`Password=` → `PGPASSWORD`, `Server=` → `PGHOST`. + +```bash +set +H # bash: desactiva la expansión de '!' en la password +cd /home/docker/backups/club12 + +docker run --rm -v "$PWD:/out" \ + -e PGHOST=aws-1-us-east-2.pooler.supabase.com -e PGPORT=5432 -e PGDATABASE=postgres \ + -e PGUSER='' -e PGPASSWORD='' -e PGSSLMODE=require \ + postgres:17-alpine \ + pg_dump --format=custom --no-owner --no-privileges \ + --schema=public --schema='"Club12"' \ + --file=/out/club12-cutover-$(date +%F).dump +``` + +> `--schema='"Club12"'` **con comillas dobles adentro**: `pg_dump` pasa los patrones de +> `--schema` a minúsculas, así que `--schema=Club12` no matchea nada y el dump sale con +> solo la mitad de las tablas. + +Verificá que estén los dos schemas: + +```bash +docker run --rm -v "$PWD:/out" postgres:17-alpine \ + pg_restore --list "/out/club12-cutover-$(date +%F).dump" | grep "TABLE DATA" +``` + +Tenés que ver ~22 tablas en `Club12` (`Teams`, `Players`, `Tournaments`, `Matches`, …) y +8 en `public` (`AspNetUsers`, `__EFMigrationsHistory`, …). + +### 7.4 Cutover (ventana corta — el backend se reinicia) + +```bash +cd /home/docker-compose/Club12 +DUMP=/home/docker/backups/club12/club12-cutover-$(date +%F).dump + +# 1. Levantar SOLO la base y esperar a que esté healthy +docker compose up -d db +docker compose ps db # STATUS = healthy + +# 2. Restaurar el dump dentro del contenedor +docker compose cp "$DUMP" db:/tmp/restore.dump +docker compose exec -T db pg_restore --no-owner --no-privileges -U postgres -d postgres /tmp/restore.dump +docker compose exec -T db rm /tmp/restore.dump +# El único error esperado es `schema "public" already exists` (1, ignorado) — benigno. +# Cualquier otro error rojo: frená y revisá antes de seguir. + +# 3. Verificación +docker compose exec -T db psql -U postgres -d postgres -c '\dt "Club12".*' +docker compose exec -T db psql -U postgres -d postgres -c 'select count(*) from "Club12"."Teams";' +docker compose exec -T db psql -U postgres -d postgres -c 'select count(*) from "public"."AspNetUsers";' + +# 4. Con el .env ya editado (7.2 — cambiar ConnectionStrings__DbConnection a Host=db;…), +# recrear el backend +docker compose up -d backend +docker compose logs --tail=40 backend # sin errores de migración, "Now listening on: http://[::]:8080" + +# 5. Bouncear el frontend: su nginx tiene cacheada la IP vieja del backend → si no, +# todo /api/* da 502 hasta reiniciarlo. +docker compose restart frontend + +# 6. Verificar (el backend no está publicado en el host — va por el proxy :5001) +docker compose ps +curl -s -o /dev/null -w 'http=%{http_code}\n' 'http://localhost:5001/api/tournaments?pageSize=300' +``` + +En el arranque, `MigrateAsync` ve el schema ya restaurado y no hace nada (o aplica sólo +migraciones más nuevas que el dump). El seed está en `Seed:Enabled=false` en producción. + +> El paso 5 (`restart frontend`) queda automatizado en `deploy-backend.yml` para los +> deploys de CI; en el cutover manual hay que hacerlo a mano. + +### 7.5 Backup automático (feature integrado del backend) + +El backend ya trae un sistema de backups completo — no hace falta cron ni script en el +host. Se activa con la variable agregada en §7.2: + +```bash +Backup__Enabled=true +# opcionales, si se quiere otra cadencia (defaults entre paréntesis): +# Backup__IntervalHours=24 (24) +# Backup__RetentionCount=7 (7) +``` + +Con `Backup__Enabled=true`, un `DatabaseBackupHostedService` corre dentro del propio +proceso backend cada `Backup__IntervalHours` horas, genera el dump con `pg_dump` (ya +instalado en la imagen del backend) y lo guarda en el volumen `backup-data` +(`Backup__LocalStoragePath`, default `/app/backups`), podando al `Backup__RetentionCount` +más reciente. + +- **Ver los backups:** panel admin → sección de backups. Cada fila muestra su origen + ("Programado" para los automáticos, "Manual" para los que se disparan a mano desde el + mismo panel). +- **Restaurar:** botón de restore en el panel (`POST /api/backups/{id}/restore`) — sin + SSH al servidor. Antes de restaurar, la app crea automáticamente un backup de + seguridad del estado actual. +- **Verificar la integridad de un dump sin arriesgar producción:** descargarlo desde el + panel y restaurarlo en una base "scratch" separada (no usar el botón de restore de + producción solo para testear que un dump sirve). + +### 7.6 Rollback + +```bash +# En el .env, volver ConnectionStrings__DbConnection al valor del pooler de Supabase +docker compose up -d backend +``` + +No hay migración de schema que revertir. Dejar el contenedor `db` y +`/home/docker/club12/db` en su lugar hasta confirmar que el cutover quedó estable; recién +ahí dar de baja el proyecto de Supabase (los buckets de Storage siguen en uso). + +### 7.7 `docker compose down` — cuidado + +Los datos de `db` son un bind mount: sobreviven `down` y `down -v`. Aun así, `-v` +elimina los volúmenes nombrados (`backup-data`), y borrar `/home/docker/club12/db` a mano +destruye la base. + +## Checklist post-merge + +Estos pasos **no están cubiertos por el cambio de código en sí** — son responsabilidad del +usuario/operador una vez que este cambio se mergea a `develop`, y deben verificarse a mano porque +ningún runner self-hosted existe todavía en este momento: + +- [ ] Registrar ambos runners con las labels exactas `Club-12-back-runner` y + `Club-12-front-runner` (sección 2). +- [ ] Crear el `.env` real en `/home/docker-compose/Club12/.env` a partir de `.env.example` + (sección 3). +- [ ] Hacer un push chico a `develop` que solo toque `Club12-Backend/**` y confirmar que: + - solo corre `deploy-backend.yml` (no `deploy-frontend.yml`); + - el job `build` publica `ghcr.io/francoru/club12-backend:latest`; + - el job `deploy` corre después de que `build` termina, en el runner `Club-12-back-runner`; + - `docker images` muestra `club12-backend:previous` (o el mensaje de "primer deploy" si es la + primera vez); + - `docker compose ps backend` muestra el contenedor recreado y saludable; + - el contenedor de `frontend` **no** se reinicia. +- [ ] Repetir el mismo push de prueba tocando solo `Club12-WebClient/**` y confirmar el mismo + comportamiento en espejo para `deploy-frontend.yml`. +- [ ] Confirmar que `/home/docker-compose/Club12/.env` sigue existiendo después del deploy y que + el servicio toma sus valores (por ejemplo, que la API arranca sin errores de configuración). +- [ ] Confirmar que `docker image prune -f` no deja crecer una pila de imágenes colgantes, y que + las tags `:latest` y `:previous` sobreviven. diff --git a/Docs/MANUAL_USUARIO.md b/Docs/MANUAL_USUARIO.md new file mode 100644 index 0000000..3919bd0 --- /dev/null +++ b/Docs/MANUAL_USUARIO.md @@ -0,0 +1,179 @@ +# Manual de Usuario — Liga Club12 + +Guía de uso del sistema para administradores y visitantes. Para información técnica (arquitectura, instalación, requisitos cubiertos) ver [README.md](./README.md). + +## 1. Perfiles de usuario + +El sistema tiene dos tipos de acceso: + +- **Visitante (sin cuenta)**: puede consultar toda la información pública de la liga desde la página principal, sin necesidad de registrarse ni iniciar sesión. +- **Usuario administrador (con cuenta)**: accede al panel privado para cargar y mantener toda la información de las temporadas y torneos. Los roles definen qué secciones puede administrar cada usuario. + +## 2. Vista pública (sin iniciar sesión) + +El menú principal tiene las secciones: **Inicio**, **Temporadas**, **Campeones**, **Sanciones**, **Novedades**, **Quiénes somos** e **Información** (con las sub-páginas **Ficha médica** y **Reglamento**). + +La navegación de la competición sigue el orden **Temporada → Torneo → División**: no hay un listado plano de "torneos" — se entra por la temporada, se elige el torneo dentro de ella y ahí se accede a cada división. + +Desde estas secciones, cualquier visitante puede: + +- Ver la lista de **temporadas** y, dentro de cada una, sus **torneos** (activos y finalizados). +- Consultar los **equipos** participantes de cada división, con su plantel de jugadores. +- Ver el **fixture** y los **resultados** de los partidos jugados y a jugarse. +- Consultar la **tabla de posiciones** de cada división/etapa. El equipo en 1er puesto se destaca con una corona apenas toma la punta — no hace falta que termine la fase para verlo. +- Ver la **tabla de goleadores** (limitada a los 10 primeros en la vista pública). +- Ver las **llaves de eliminación** ("Llaves") como un árbol de bracket, con los cruces de cada ronda hasta la final. +- **Imprimir** la tabla de posiciones de una división. +- Consultar las **sanciones vigentes** aplicadas a jugadores. +- Leer las **novedades** publicadas en el blog de la liga. +- Ver la sección **Campeones**: el historial de campeones por división/copa de los torneos ya finalizados. +- Leer **Quiénes somos** y, dentro de **Información**, la página de **Ficha médica** (qué es y por qué se pide) y el **Reglamento** de la liga. + +No se requiere ninguna acción de registro para acceder a esta información. + +### 2.1 Ver las llaves de eliminación + +1. Entrar a la división deseada dentro de un torneo. +2. Ir a la pestaña **"Llaves"** (junto a "Partidos" y "Posiciones"). +3. El sistema muestra el árbol de la fase eliminatoria, incluyendo — cuando el torneo tiene más de una copa (por ejemplo Copa Oro y Copa Plata, según el rango de posiciones de la fase de grupos) — cada copa por separado. +4. Si un partido todavía no se jugó, ese cruce se muestra como "A definir" (TBD). Cuando la cantidad de equipos no es potencia de 2 (por ejemplo 5, 6 o 7 equipos), los mejores puestos de la fase de grupos reciben "bye" (pasan directo a la siguiente ronda) — es un comportamiento esperado, no un error. Si el sistema no puede determinar con certeza qué equipo avanza a la siguiente ronda, muestra las rondas en columnas sin línea de conexión en vez de arriesgar una conexión incorrecta. + +### 2.2 Imprimir posiciones + +1. Entrar a la vista de **posiciones** de una división. +2. Usar el botón **"Imprimir"**. +3. Se abre el diálogo de impresión del navegador (permite imprimir en papel o guardar como PDF desde ahí); la hoja se genera sin el menú ni otros elementos de navegación, solo la tabla de posiciones. + +## 3. Iniciar sesión + +1. Hacer clic en "Iniciar sesión" desde la página principal. +2. Ingresar correo electrónico y contraseña. +3. Si es la primera vez que se ingresa con una cuenta creada por un administrador, el sistema puede solicitar cambiar la contraseña antes de continuar. +4. Si se olvidó la contraseña, usar la opción "Recuperar contraseña": el sistema envía un correo con las instrucciones para restablecerla. + +Al iniciar sesión correctamente, el sistema habilita el menú del panel administrativo según el rol del usuario. + +## 4. Panel administrativo + +El menú del panel se organiza en los siguientes grupos: + +- **Competición**: Temporadas, Sanciones, Canchas. +- **Gestión de equipos**: Equipos, Jugadores. +- **Novedades** (blog). +- **Usuarios**. +- **Sistema**: Estadísticas, Registro de auditoría, Administración de datos. +- **Configuración**: Cambiar contraseña, Editar perfil. + +Los torneos, divisiones, fases y partidos no tienen un ítem propio en el menú: se llega a ellos entrando a una temporada y navegando hacia adentro (Temporada → Torneo → División → Fase/Partido). + +### 4.1 Temporadas, torneos y divisiones + +1. **Crear una temporada**: agrupa uno o más torneos (por ejemplo, Apertura y Clausura del mismo año). +2. **Crear un torneo** dentro de la temporada: cargar nombre, fecha de inicio y demás datos generales. +3. **Crear divisiones** dentro del torneo (por ejemplo, categorías por edad, nivel o género). +4. **Inscribir equipos** a cada división. +5. **Generar la etapa** del torneo: el sistema arma automáticamente la fase de grupos y, una vez que termina, las llaves de eliminación directa según la cantidad de equipos clasificados (con "bye" a los mejores puestos si esa cantidad no es potencia de 2). Si el torneo define más de una copa (por ejemplo Copa Oro para los primeros puestos y Copa Plata para el resto), el sistema arma cada bracket por separado, tomando la posición de arranque de cada copa como el "1er sembrado" de su propio cuadro. +6. Una zona puede dividirse en **varios sub-grupos** (por ejemplo "Grupo A" y "Grupo B" dentro de la misma zona) para repartir muchos equipos en fases de grupos más chicas. Una zona con 2 o más sub-grupos siempre necesita al menos una copa configurada: sin ella no habría manera de determinar un campeón entre los sub-grupos, así que el sistema rechaza guardar esa combinación. +7. El torneo no puede pasar a **"En curso"** si alguna zona tiene menos de 2 equipos o si algún equipo inscripto tiene menos de 4 jugadores habilitados (ver 4.3) — el sistema avisa qué falta corregir antes de poder arrancar. + +### 4.2 Equipos + +Esta pestaña lista **clubes**: la identidad estable de una institución (por ejemplo "Central Entrerriano"), que se mantiene igual de una temporada a la otra. Un club agrupa todos sus equipos por temporada — así un club que jugó varios años aparece **una sola vez** en la lista, en vez de una fila repetida por cada temporada en la que participó. + +- **Alta de un equipo nuevo**: nombre, código de tres letras, color de camiseta, escudo. Si ya existe un club con ese nombre, el equipo nuevo queda vinculado a él automáticamente; si el nombre no existe todavía, el sistema crea el club correspondiente. +- Búsqueda y filtrado por nombre de club. +- **Ver** un club abre su historial: la lista de sus equipos, uno por cada temporada/torneo en el que se inscribió, con el código de tres letras y el torneo correspondiente de cada uno. +- **Eliminar** un club solo está permitido si no tiene ningún equipo ni escuadra vinculada; si todavía tiene historial, el sistema rechaza el borrado. +- Desde el historial del club se puede eliminar un equipo puntual (una temporada específica) sin afectar el resto del historial del club. +- Cuerpo técnico: cargar el director técnico y demás staff se hace desde el equipo de esa temporada (dentro del historial del club), no desde la lista de clubes. + +**Escuadras (club matriz)**: cuando una institución tiene más de un equipo dentro de la misma temporada (por ejemplo "Echagüe A" y "Echagüe B"), cada uno se da de alta como su propio club. Para agruparlos bajo la institución: + +1. Entrar al historial de la escuadra (por ejemplo "Echagüe B"). +2. Elegir el club matriz en **"Vincular con club matriz"** y confirmar con **"Vincular"**. +3. El historial de la institución matriz pasa a listar cada escuadra vinculada, y el de la escuadra muestra un enlace a su institución. + +La vinculación es de un solo nivel: una institución no puede a su vez ser escuadra de otro club, y un club que ya tiene sus propias escuadras no puede pasar a ser escuadra de otro. Un club vinculado puede **desvincularse** en cualquier momento desde su propio historial. + +### 4.3 Jugadores + +- Alta de un jugador: datos personales, DNI (único por jugador), equipo al que pertenece. +- Edición o baja de jugadores. +- Búsqueda y filtrado. +- Número de camiseta (dorsal): se asigna por equipo y temporada, no es fijo para el jugador — puede cambiar si pasa a otro equipo o temporada. + +**Ficha médica y habilitación**: para que un jugador pueda sumar puntos en un partido tiene que estar **habilitado** para esa temporada: + +1. Subir el PDF de la ficha médica del jugador (por equipo y temporada). +2. Un administrador la revisa y la **aprueba** o **rechaza**. Solo queda habilitado cuando está aprobada y tiene un archivo real cargado — una aprobación sin archivo no habilita a nadie. +3. Una vez aprobada, la ficha no se puede volver a subir (queda de solo lectura); si hace falta corregirla, primero hay que rechazarla. +4. La habilitación es específica de esa temporada: un jugador que jugó habilitado el año pasado empieza la temporada nueva sin habilitar, aunque no haya cambiado de equipo. + +### 4.4 Partidos + +- Los partidos de fase de grupos y de eliminación se generan automáticamente al crear la etapa. +- Cargar el **resultado** de un partido una vez jugado, planilla por planilla: se cargan los puntos de cada jugador de ambos equipos y el resultado final se calcula sumándolos (no se tipea aparte). El sistema actualiza automáticamente la tabla de posiciones y de goleadores. +- Reglas al cargar el resultado: + - Solo pueden sumar puntos jugadores **habilitados** y sin sanción activa. Si algún jugador cargado no cumple esto, el sistema rechaza el guardado y lista, con nombre y equipo, a todos los jugadores con problema (no solo el primero). + - Un equipo necesita **al menos 4 jugadores habilitados** para que se le pueda cargar un resultado normal; si no llega a ese mínimo, el partido tiene que cargarse como **walkover** ("Marcar W.O.") en lugar de una planilla común. + - No se permiten empates: en fase de grupos el desempate en la tabla es PTS → PG (partidos ganados) → DG (diferencia de gol) → resultado entre los propios equipos empatados → DG de esos cruces; en fase eliminatoria, si el partido termina empatado, se juega tiempo suplementario. +- **Walkover**: un administrador puede marcar un partido como walkover indicando qué equipo se presentó — el sistema carga el resultado reglamentario a favor de ese equipo. +- Consultar el estado de cada partido (programado / jugado / walkover). + +### 4.5 Estadísticas y goleadores + +- Las estadísticas de puntos por jugador se cargan como parte de la planilla del resultado del partido (ver 4.4), no por separado. +- El sistema arma automáticamente la tabla de goleadores del torneo a partir de esta carga. + +### 4.6 Sanciones + +1. Registrar una sanción a un jugador (tipo, motivo, duración). +2. El jugador o su equipo puede presentar una **apelación** desde el sistema. +3. Un administrador revisa la apelación y la marca como **aceptada** o **rechazada**. +4. Si es aceptada, la sanción se levanta; si es rechazada, la sanción sigue vigente. +5. Un jugador sancionado no puede sumar puntos en un partido mientras la sanción esté activa (ver 4.4). + +### 4.7 Canchas (Venues) + +- Alta, edición y baja de las canchas/sedes donde se juegan los partidos. +- Al programar un partido, dos partidos en la misma cancha necesitan al menos 2 horas de diferencia entre sí. + +### 4.8 Usuarios + +- Alta de nuevos usuarios administradores, con asignación de rol. +- Activar o desactivar cuentas existentes. +- Un usuario puede cambiar su propia contraseña o editar su perfil desde **Configuración**. + +### 4.9 Novedades (Blog) + +- Publicar noticias o crónicas de partidos, visibles en la vista pública. +- Editar o eliminar publicaciones existentes. +- Una publicación puede guardarse como borrador antes de hacerla visible al público. + +### 4.10 Sistema + +- **Estadísticas**: panel con métricas generales de uso/carga de la liga. +- **Registro de auditoría**: historial de acciones administrativas relevantes (quién hizo qué y cuándo), para trazabilidad. +- **Administración de datos**: herramientas internas de mantenimiento de datos, de uso ocasional por el equipo técnico. + +## 5. Copias de seguridad + +El sistema incluye un mecanismo de respaldo automático programado de la base de datos. Esta función es de uso interno (no requiere acción del usuario administrador) y se activa desde la configuración del servidor por el equipo técnico responsable del despliegue. + +## 6. Resolución de problemas frecuentes + +| Situación | Qué hacer | +|---|---| +| No puedo iniciar sesión | Verificar correo y contraseña; usar "Recuperar contraseña" si es necesario. Si la cuenta fue desactivada, contactar a un administrador. | +| No veo la opción para administrar cierta sección | El rol asignado a la cuenta no tiene permiso sobre esa sección; contactar a un administrador para revisar el rol. | +| Un partido no aparece en el fixture | Verificar que la etapa del torneo haya sido generada y que el equipo esté correctamente inscripto en la división. | +| Cargué un resultado y la tabla de posiciones no cambió | Revisar que el resultado se haya guardado correctamente (no quedó en estado pendiente); recargar la página. | +| No puedo cargar el resultado de un partido | Revisar que todos los jugadores cargados estén habilitados (ficha médica aprobada con archivo) y sin sanción activa; si el equipo tiene menos de 4 jugadores habilitados, hay que cargar el partido como walkover en lugar de una planilla común. | +| No puedo iniciar el torneo | Revisar que cada zona tenga al menos 2 equipos y que cada equipo inscripto tenga al menos 4 jugadores habilitados; el sistema indica qué falta corregir. | +| No encuentro el equipo de una temporada anterior en la pestaña Equipos | Esa pestaña lista clubes, no equipos por temporada — buscar el club (la institución) y entrar a **Ver** para encontrar el equipo de la temporada buscada en su historial. | +| No puedo eliminar un club | El club todavía tiene equipos o escuadras vinculadas; eliminar primero esos equipos desde el historial del club, o desvincular las escuadras, antes de poder eliminar el club. | +| No puedo guardar una zona con varios sub-grupos | Una zona con 2 o más sub-grupos necesita al menos una copa configurada, para poder determinar un campeón entre los sub-grupos; agregar una copa o reducir la zona a un solo grupo. | + +## 7. Soporte técnico + +Para consultas sobre instalación, configuración del servidor o errores técnicos, ver la documentación técnica en [README.md](./README.md) o contactar al equipo de desarrollo del proyecto. diff --git a/Docs/historias-de-usuario.md b/Docs/historias-de-usuario.md new file mode 100644 index 0000000..ec51ce0 --- /dev/null +++ b/Docs/historias-de-usuario.md @@ -0,0 +1,1067 @@ +# Club 12 — Historias de Usuario + +> Reescritura 2026-09-02: el doc original (reunión Facundo/Franco + decisiones del owner) quedó +> desactualizado en varios puntos estructurales tras sesiones posteriores de desarrollo — más +> notablemente, la aparición de una entidad **Temporada (Season)** por encima de Torneo, que no +> existía cuando se escribió la primera versión. Esta versión fue verificada historia por historia +> contra el código real (backend + frontend), no contra memoria de lo que "se pidió" — cada HU dice +> si sigue vigente, cambió, o quedó obsoleta, citando dónde en el código se verificó. Se mantiene la +> numeración original de HU donde la historia sigue existiendo (el código la referencia por número en +> varios comentarios — renumerar rompería esa trazabilidad); las historias nuevas se agregan al final +> con números nuevos. +> +> **Actualización 2026-09-03**: se agregaron HU-118/HU-119 (umbral de 4 jugadores habilitados por +> equipo para cargar un resultado normal / para iniciar el torneo) y HU-86b (top 10 en el ranking +> público de goleadores), se corrigieron HU-62/HU-73/HU-78/HU-82/HU-109 con lo que cambió en esa +> sesión, y se marcaron dos ítems más de la Épica 24 como resueltos (`MatchId` required, roster +> mostrando "No habilitado" para todos). Mismo criterio que la reescritura de abajo: verificado +> contra código real, no contra memoria de lo pedido. +> +> **Actualización 2026-09-05**: auditoría del checkbox "Fase de grupos" del asistente de torneo, a +> pedido del owner. Se agregaron HU-120 a HU-125 (fin de la Épica 22): hoy "Fase de grupos" es un +> booleano de UN solo grupo por división (no permite dividirla en sub-grupos Zona A/B/C con reparto +> balanceado y umbral mínimo de equipos, como sí necesita un organizador real), y existe un segundo +> mecanismo legado (`CreateAutomatedStagesAsync`) contradictorio con el modelo actual del wizard. Ver +> el bloque "Auditoría 2026-09-05" antes de la Épica 23 para el detalle verificado contra código. +> +> **Actualización 2026-09-06**: HU-120 a HU-124 quedaron implementados (roster por división +> `DivisionTeamRegistration`, sub-grupos con cantidad elegida por el organizador y reparto balanceado, +> reasignación manual, edición posterior de la cantidad de sub-grupos, y el mecanismo legado +> `CreateAutomatedStagesAsync` borrado por completo) — sus textos se reescribieron para describir el +> comportamiento final entregado, no el plan. HU-125 quedó explícitamente fuera de alcance, ahora +> rechazada en el backend en vez de dejarse como un cálculo silenciosamente incorrecto. Se agregó +> HU-128 (sorteo de llave para divisiones sin fase de grupos, con vista previa, sorteo manual, traba +> de re-sorteo y auditoría). +> +> Convenciones: `[BUG]` corrige algo existente · `[NUEVA]` propuesta alineada · `[F2]` Fase 2. +> Prioridad (MoSCoW): **M** Must · **S** Should · **C** Could. +> `✅` = verificada vigente tal cual · `🔄` = vigente pero cambió su implementación (ver nota) · +> `❌` = obsoleta/reemplazada (ver nota) · `⚠️` = vigente pero con una brecha real detectada en el código. + +--- + +## Decisiones transversales (leer antes que nada) + +Aplican a TODAS las historias. Si una historia las contradice, ganan estas. + +- **D1 — Solo 2 tipos de usuario y 2 cuentas.** Sigue vigente. `Owner` y `Admin IT` son los únicos + roles (`Club12-WebClient/src/modules/core/enum/user/userRolesType.ts`); una migración + (`20260828021949_RemoveTournamentAndTeamManagerRoles`) eliminó los roles viejos de la base. +- **D2 — Equipos y jugadores están scopeados a la temporada/torneo.** Sigue vigente como principio, + pero el nivel de scope ya no es solo "torneo": ver **D7** más abajo — ahora hay una capa + `Season` (Temporada) por encima que agrupa torneos, sin cambiar cómo se scopean equipos/jugadores + (siguen colgando del Torneo vía `TeamTournamentRegistration`/`PlayerTeamRegistration`). +- **D3 — Contenido institucional bloqueado para el owner.** Sigue vigente (`/quienes-somos`, + `/reglamento`, ficha médica no tienen ruta de edición en el panel). +- **D4 — Reglamento agnóstico a la app y a la temporada.** Sigue vigente. +- **D5 — Una acción = una transacción.** Sigue vigente (el asistente de torneo, HU-38, sigue + persistiendo todo en una transacción). +- **D6 — ~~Magic link / self-service de password / auto-inscripción son Fase 2~~ — OBSOLETA.** + Magic link de invitación (HU-09) y self-service de reset de contraseña (HU-10) están **totalmente + implementados y en producción**, no diferidos: `Club12-Backend/API/Controllers/AuthController.cs` + expone `POST /api/auth/invite`, `/activate`, `/password-reset/request` y `/password-reset/confirm`; + el frontend tiene una pantalla completa "Olvidé mi contraseña" + (`Club12-WebClient/src/views/auth/forgotPassword.tsx`). Ver HU-09/HU-10 actualizadas. +- **D7 — [NUEVO] Temporada (Season) agrupa Torneos; no reemplaza el scoping por torneo.** Una + `Season` (`Club12-Backend/Domain/Entities/Models/Season.cs`) agrupa varios torneos del mismo + período competitivo (habitualmente uno masculino + uno femenino). Es el punto de entrada del panel + admin (reemplaza a "Torneos" como ítem top-level del sidebar — ver HU-26 actualizada). El FK + `Season → Tournament` es `SetNull`, no `Cascade`: borrar/limpiar una temporada nunca borra sus + torneos ni su historial. Ver HU-113 (nueva). + +### Reglas de negocio resueltas (antes eran incongruencias) + +- **R1 — Sanción en fechas, cleanup por días.** Sigue vigente tal cual (HU-75). +- **R2 — Todo scopeado a la temporada.** Sigue vigente (HU-98), ahora con la capa Season encima (D7). +- **R3 — Fixture se genera al iniciar el torneo, tras asignar equipos.** Sigue vigente, confirmado en + código: `TournamentService.cs` genera el fixture únicamente dentro de la transición a `Ongoing` + ("En curso"). Ver HU-64/HU-108. +- **R4 — Básquet sin empates.** Sigue vigente en fase de grupos (HU-70). La prórroga + (`WentToOvertime`) es un campo puramente informativo que garantiza que no haya empate; el avance + automático de ganador entre rondas de bracket (HU-82) ya está implementado — ver Épica 24. + +--- + +## Épica 1 — Autenticación y acceso + +### HU-01 · Login oculto por URL — `M` ✅ +Sigue vigente tal cual. Sin link visible en la nav pública; solo accesible tipeando `/login`. + +### HU-02 · Ocultar header y footer en el login — `S` ✅ +Sigue vigente. `/login` se renderiza fuera de `PublicLayout`. + +### HU-03 · [BUG] Redirigir al home al cerrar sesión — `M` ✅ +Sigue vigente y arreglado (`SidebarLayout.tsx`'s `handleLogOut` navega a home tras `logOut()`). + +### HU-04 · [BUG] Página 404 sin layout — `S` ✅ +Sigue vigente. El catch-all `*` renderiza `` sin header/footer, con botón al inicio. + +--- + +## Épica 2 — Roles y usuarios + +### HU-05 · Simplificar el modelo de roles a Owner y Admin IT — `M` ✅ +Sigue vigente (ver D1). + +### HU-06 · Editar mi perfil y contraseña — `M` ✅ +Sigue vigente. `/panel/configuracion/editar-perfil` y `/panel/configuracion/cambiar-password`. + +### HU-07 · Listado de usuarios con soft delete — `S` ✅ +Sigue vigente: `isActive` (activar/desactivar) es un toggle distinto de eliminar. + +### HU-08 · Blanquear contraseña / editar usuario a pedido — `S` ✅ +Sigue vigente. + +### HU-09 · Alta de usuario por invitación con magic link — `M` 🔄 +**Ya NO es Fase 2 (ver D6) — está en producción.** +**Como** owner/admin **quiero** crear un usuario cargando solo su email y que reciba un magic link +**para** que active su cuenta desde su correo. +- `POST /api/auth/invite` (Admin/Owner) crea el usuario sin contraseña y dispara el mail de + activación; `POST /api/auth/activate` consume el token y define la primera contraseña. +- Ruta admin: `/panel/usuarios/invitar`. + +### HU-10 · Reset de contraseña self-service por magic link — `M` 🔄 +**Ya NO es Fase 2 (ver D6) — está en producción.** +**Como** usuario **quiero** pedir un magic link para restablecer mi contraseña **para** recuperarme +sin soporte. +- `POST /api/auth/password-reset/request` + `/confirm`; pantalla "¿Olvidaste tu contraseña?" enlazada + desde el login. + +--- + +## Épica 3 — Portada pública, noticias y vista pública del torneo + +### HU-11 · Landing pública con accesos directos — `M` 🔄 +**Cambió el contenido de la portada, no el principio.** +**Como** visitante anónimo **quiero** una portada que me lleve rápido a lo que me interesa sin +loguearme **para** no perder tiempo navegando. +- Hero con dos CTA: **"Ver temporadas"** y **"Campeones"** (ya no "torneos destacados" ni accesos + directos a sanciones). +- Sección **"Temporadas"** (no "torneos") con las temporadas recientes — consistente con D7: la + portada ahora es season-first. +- Sección de campeones recientes y feed de últimas noticias (ver HU-12). +- No requiere autenticación. + +### HU-12 · Feed de noticias con "ver todas" — `M` ✅ +Sigue vigente: últimas noticias en home + "ver todas" a `/blog` paginado. + +### HU-13 · Detalle de noticia con URL por slug — `M` ✅ +Sigue vigente (`/blog/:idOrSlug`). + +### HU-14 · Vista pública del torneo (fixture, resultados y posiciones) — `S` 🔄 +**Sigue existiendo, pero ya no es el punto de entrada principal.** +**Como** visitante **quiero** ver el fixture, resultados y posiciones de un torneo sin loguearme +**para** seguir la competencia. +- Camino principal: `/temporadas` → temporada → tarjeta de torneo → `/torneos/:tournamentId`. +- La ruta plana `/torneos` también sigue existiendo. +- Solo lectura. + +### HU-15 · [BUG] Ocultar el ID en la URL cuando debería ir un slug — `S` ✅ +Sigue vigente: torneo, equipo, partido, división, sanción y blog navegan por slug donde existe. + +### HU-16 · Estado borrador / publicada de noticia — `S` 🔄 +**Ya no es "[NUEVA]" — está implementado.** Chip Publicada/Borrador en el listado admin, switch en +edición; solo las publicadas aparecen en público. + +### HU-17 · Compartir noticia con Open Graph — `C` 🔄 +**Ya no es "[NUEVA]" — está implementado**, y se extendió más allá de noticias: `pageMetadata.ts` +genera Open Graph/Twitter Card para blog, equipo, torneo y temporada, no solo noticias. + +--- + +## Épica 4 — Contenido institucional + +### HU-18 · Ver "Quiénes somos" — `M` ✅ +### HU-19 · Ver reglamento — `M` ✅ +Ambas siguen vigentes tal cual, sin ruta de edición en el panel (D3). + +### HU-20 · Descargar plantilla de ficha médica — `M` 🔄 +Sigue vigente, pero la descarga cambió de mecanismo: hoy es un asset estático público +(`/documents/ficha-medica-club12.pdf`, link `download` directo), no un endpoint del backend. + +### HU-21 · [BUG] Arreglar la descarga de ficha médica en producción — `M` ⚠️ +**No verificable desde el código si el bug original sigue existiendo** — pero el cambio de mecanismo +(HU-20: ahora es un asset estático servido por el frontend, no un endpoint backend) cambia por +completo el modo de falla a auditar. Si vuelve a fallar en producción, el sospechoso ya no es un +problema de ruta/permiso del backend, sino si el PDF quedó efectivamente incluido en el build +estático desplegado. + +--- + +## Épica 5 — Sanciones públicas + +### HU-22 · Consulta pública de sanciones — `M` ✅ +### HU-23 · [BUG] Buscar sanciones por nombre de jugador — `M` ✅ +Sigue vigente y verificado con tests (`PlayerSanctionSearchTests.cs`): el buscador matchea contra el +nombre del jugador sancionado, no solo contra el motivo. + +### HU-24 · [BUG] Búsqueda por coincidencia parcial (contains) — `M` ⚠️ +**`contains` case-insensitive: sí. Insensible a acentos: NO, y es una decisión deliberada, no un bug +pendiente.** El código documenta por qué: requeriría una extensión de Postgres (`unaccent`/`citext`) +no habilitada en el proyecto, y el proveedor de test en SQLite no la traduciría. Este criterio de +aceptación debería sacarse del doc en vez de quedar como pendiente. + +### HU-25 · Filtrar sanciones por torneo — `S` ✅ +Sigue vigente, combinable con la búsqueda por nombre. + +--- + +## Épica 6 — Panel de administración: navegación + +### HU-26 · Simplificar la navegación lateral — `M` 🔄 +**El sidebar cambió más de lo que esta historia anticipaba (ver D7/HU-113).** +**Como** owner/admin **quiero** un sidebar agrupado y sin entidades sueltas **para** navegar por +flujo, no por tabla. +- Divisiones, Fases y Partidos siguen sin ítem propio (se gestionan desde el torneo/división). +- **"Torneos" tampoco tiene ítem propio.** El ítem top-level del grupo "Competición" es + **"Temporadas"**; un torneo se alcanza siempre entrando por su temporada + (`Club12-WebClient/src/views/core/components/SidebarLayout.tsx`, con un test de regresión + (`SidebarLayout.test.tsx`) que confirma que "Torneos" no está en el DOM). +- Se agregó **"Inscripción de equipos"** (`/panel/registro-equipos`) como ítem propio del grupo de + gestión de equipos, y **"Equipos y planteles"** (Épica 10). + +### HU-27 · El asistente de torneo no vive en el sidebar — `S` 🔄 +Sigue vigente en el principio, pero el botón "Nuevo torneo" que lo dispara ya no vive en una página +global de Torneos (que ya no existe en el sidebar) — vive en el detalle de una Temporada +(`AdminSeasonDetailPage.tsx`), y pre-scopea el asistente a esa temporada (`seasonId` en el estado de +navegación). + +### HU-28 · Ajustes de estilo del panel — `C` +No re-verificado en esta pasada (ítem cosmético); sin evidencia de que se haya revertido. + +--- + +## Épica 7 — Gestión de torneos + +### HU-29 · Listado y CRUD de torneos — `M` ❌ OBSOLETA (reemplazada por el flujo de HU-26/HU-27) +La tabla plana de torneos (`TournamentsPage.tsx`, ruta `/panel/torneos`) sigue existiendo como +componente/ruta, pero **no está enlazada desde ningún lado** del panel — quedó huérfana. El flujo +real hoy es: Temporada → tarjetas de torneo agrupadas por categoría (dentro de +`AdminSeasonDetailPage.tsx`). Candidata a eliminarse del código o a documentarse como vista de +respaldo, no como el flujo principal. + +### HU-30 · Vista de torneo con sub-tabs — `M` 🔄 +**Son 4 tabs, no 2.** `TournamentPage.tsx`: `Detalle | Divisiones | Equipos | Asignación`. El tab +**Asignación** es nuevo (ver HU-108) y no tenía HU propia en la versión anterior de este documento. + +### HU-31 · Bloquear alta/edición de estructura si el torneo ya empezó — `M` ✅ +Sigue vigente, con mensaje explícito en el backend +(`ErrorMessages.StructuralEditNotAllowed`): los cambios estructurales solo se permiten en +`OpenForRegistration`. + +### HU-32 · [BUG] Contador de equipos por división en 0 — `M` +No re-verificado en esta pasada. + +### HU-33 · [BUG] Botón deshabilitado con "no hay equipos" cuando sí hay — `M` +No re-verificado en esta pasada. + +### HU-34 · Quitar mínimo/máximo de equipos del torneo — `S` ✅ +Sigue vigente: `minTeams`/`maxTeams` no existen en `Tournament`, el wizard ni la request de +creación (migración `20260828010238_DropTournamentTeamCountLimits`). + +--- + +## Épica 8 — Máquina de estados del torneo + +### HU-35 · Transiciones de estado — `M` 🔄 +**Ya no es estrictamente forward-only.** +Ciclo: `Programado → Inscripción abierta → Inscripción cerrada → En curso → Finalizado`, más +`Cancelado` desde cualquier estado no terminal — **y una excepción agregada**: `En curso → +Inscripción cerrada` ("revertir a borrador"), solo si el torneo no tiene partidos jugados. Ver +HU-114 (nueva). +- ⚠️ **Brecha detectada**: el mapa de transiciones del frontend + (`Club12-WebClient/src/modules/tournament/utils/tournamentStatusTransitions.ts`) todavía lista + solo `Finished`/`Canceled` como próximos estados válidos desde `Ongoing` — no incluye la reversión. + El botón dedicado "Revertir a borrador" en `TournamentPage.tsx` funciona igual porque no usa ese + mapa, pero cualquier OTRO lugar de la UI que sí lo use (ej. un selector genérico de próximo estado) + no va a ofrecer esta opción. Corregir el mapa para que quede consistente con el backend. + +### HU-36 · "Programado" distinto de "Inscripción abierta" — `S` ✅ +### HU-37 · "Inscripción cerrada" habilita la asignación — `M` ✅ +Ambas siguen vigentes tal cual (y la nota "supersedida por HU-108" de la versión anterior del doc +sigue siendo correcta: el fixture se genera al pasar a "En curso", no al cerrar inscripción). + +--- + +## Épica 9 — Asistente de creación de torneo + +### HU-38 · Crear el torneo completo en una sola transacción — `M` ✅ +Sigue vigente: el wizard envía la estructura completa en un solo `createFullTournament`. + +### HU-39 · Datos base del torneo con fechas — `M` 🔄 +**Se agregó un campo obligatorio que no estaba en la versión original: la Temporada.** +El primer paso del wizard ahora también exige elegir `seasonId` ("El torneo debe pertenecer a una +temporada.") — consistente con D7/HU-113. Nombre, descripción y fechas siguen igual. + +### HU-40 · Validar que el límite de inscripción sea anterior al inicio — `S` ✅ + +### HU-41 · Selección de equipos — `M` ❌ OBSOLETA, tal como ya anotaba la versión anterior +Confirmado: el wizard no tiene paso de selección de equipos (`STEP_LABELS` no incluye "Equipos"). +La inscripción vive en HU-107, la asignación a zonas en HU-108. + +### HU-42 · Un equipo asignado a una zona no aparece en otra — `M` ❌ trasladada a HU-108 +Sin cambios respecto a la nota anterior: se valida en la asignación post-cierre, no en el wizard. + +### HU-43 · Configurar la fase de grupo (cantidad de enfrentamientos) — `M` ✅ +### HU-44 · Configurar playoffs con varias copas — `M` ✅ +Ambas siguen vigentes. + +### HU-45 · Mapear posiciones de cada división a cada playoff por rangos — `M` 🔄 +**Ya no lo edita el admin a mano en el wizard — se deriva automáticamente.** +El editor manual de rangos ("1-4 → Oro, 5-8 → Plata") ya no existe en el wizard: los rangos se +**derivan** de la cantidad de clasificados (`qualifiers`) que cada copa tiene configurada, en el +orden en que las copas fueron agregadas (`deriveCupMappings`, ver HU-112 nueva). La validación de +rangos sin solapes sigue existiendo, pero solo como guarda defensiva del lado del servidor +(`PlayoffMappingValidator.cs`). + +### HU-46 · Configurar la serie final al mejor de N — `M` ⚠️ +**El campo de configuración sigue existiendo y funciona en el wizard/UI — pero ver la brecha grande +en HU-82: un "mejor de 3" configurado hoy NO genera 3 partidos reales de forma automática en +producción.** + +### HU-47 · Copa / división cruzada con playoff obligatorio — `M` ⚠️ +**Brecha de validación client-side detectada.** El backend sigue modelando la copa cruzada con playoff +obligatorio, pero la validación del PASO del wizard (`validateCrossCupStep`) no chequea +`cups.length > 0` — el estado inicial arranca con `cups: []`, así que, tal como está escrito hoy, el +formulario permitiría avanzar con una copa cruzada sin ningún playoff configurado, contradiciendo la +regla. No confirmado si el backend rechaza esto de forma independiente al recibir la request — a +revisar antes de confiar en que está cubierto. + +### HU-48 · Femenino como torneo separado, por diseño — `M` ✅ + +### HU-49 · Paso de revisión antes de crear — `S` ✅ +Sigue vigente tal cual la nota anterior: el resumen muestra estructura (divisiones, formato, copas, +series), no equipos por zona (porque ya no se seleccionan en el wizard). + +--- + +## Épica 10 — Equipos y planteles + +### HU-50 · Sección de Equipos en el panel — `M` ✅ + +### HU-51 · Tab de jugadores dentro del equipo — `M` 🔄 +Sigue vigente, pero el equipo tiene más tabs que antes: Detalle, **Jugadores**, Puntuaciones, +Sanciones y **Cuerpo técnico** (nuevo, ver HU-116). + +### HU-52 · Alta de jugadores al plantel — `S` 🔄 +**El mecanismo cambió de formulario modal a fila editable en la tabla.** +**Como** owner/admin **quiero** cargar jugadores directamente en la tabla del plantel **para** dar de +alta varios sin abrir un popup por cada uno. +- "Nuevo Jugador" agrega una fila en modo edición directamente en el `DataGrid` del plantel — no una + ventana modal separada. +- La fila nueva se completa celda por celda (Nombre/Segundo nombre/Apellido/Documento/Fecha de + nacimiento/Equipo/Teléfono/Obra social); Guardar valida y crea; Descartar la elimina sin llamar a la + API. +- Los jugadores YA existentes en la tabla siguen sin ser editables ahí mismo — la edición de un + jugador existente vive en su página de detalle, no inline (decisión explícita, con test de + regresión: "no ofrece una acción Editar en la fila — la edición vive en el detalle"). + +### HU-53 · Copiar plantel de una temporada anterior — `C` 🔄 REUBICADA, no eliminada +**El botón "Importar plantel de una temporada anterior" del tab Jugadores del equipo YA NO EXISTE +— fue reemplazado por importación CSV (HU-117, nueva).** Pero la funcionalidad de copiar un plantel +entero de una temporada previa **sigue existiendo**, reubicada dentro del flujo de **inscripción de +un equipo existente a un nuevo torneo** (`EnrollTeamDialog.tsx`, checkbox "Copiar plantel de su +temporada anterior") — es decir, sigue siendo parte de HU-107, no una acción separada del tab de +plantel. + +### HU-54 · Límites de plantel y dorsales únicos — `C` ✅ +Sigue vigente tal cual: tope configurable de jugadores por plantel, dorsal único por equipo+torneo, +un jugador no puede estar en dos equipos del mismo torneo (`PlayerService.RegisterPlayerToTeamAsync`). + +--- + +## Épica 11 — Ficha médica y elegibilidad + +(Flujo E2E sin cambios respecto a la versión anterior: descarga de plantilla → owner sube y aprueba +la ficha por jugador+equipo+torneo → habilitación → elegibilidad en la carga del partido.) + +### HU-55 · Cargar ficha médica a un jugador para ese torneo y equipo — `M` ✅ +### HU-56 · Bucket de almacenamiento de archivos — `M` 🔄 +**Ya no es "hoy no está habilitado" — está implementado.** Bucket privado `medical-records` dedicado +(`SupabaseMedicalRecordStorage.cs`), con `StoreAsync`/`DownloadAsync` y tests propios. El código está +listo; el único trabajo restante por ambiente es de aprovisionamiento/infra, no de desarrollo. + +### HU-57 · Habilitación del jugador según ficha aprobada — `M` 🔄 +Sigue vigente, con una regla adicional: la habilitación también exige que exista un **archivo +realmente almacenado**, no alcanza con el estado `Approved` sin referencia de archivo. + +### HU-58 · Aprobar / rechazar ficha médica — `S` ✅ +### HU-59 · Ficha médica nueva por temporada — `S` ✅ +### HU-60 · Solo son elegibles los jugadores habilitados — `M` ✅ +### HU-61 · Un jugador sancionado no es convocable — `M` ✅ +### HU-62 · Aviso de jugador no habilitado al armar la fecha — `C` 🔄 +**El aviso ahora lista a TODOS los jugadores no elegibles de una planilla, no solo el primero +encontrado — y por nombre real, nunca por UUID crudo.** Corregido: antes, +`PlayerStatisticService` tiraba una excepción apenas encontraba el PRIMER jugador sancionado o no +habilitado, con el ID del jugador crudo en el mensaje (`El jugador {guid} no está habilitado...`) — +esto obligaba al admin a corregir la planilla de a un jugador por vez, y el mensaje era ilegible. +Ahora (`PlayerStatisticService.FindRosterEligibilityIssuesAsync`) junta TODAS las violaciones de +ambos equipos antes de tirar un único error agrupado por equipo, con el nombre real de cada +jugador (`ErrorMessages.MatchSheet.PlayersNotEligible`). + +Las seis siguen vigentes en el fondo; ver también HU-73 y HU-109 (nuevas reglas relacionadas +agregadas en esta pasada). + +### HU-118 · [NUEVO] Umbral mínimo de habilitados por equipo (walkover automático) — `M` +**Como** owner/admin **quiero** que el sistema bloquee la carga de un resultado normal si un equipo +no tiene suficientes jugadores habilitados **para** forzar que ese partido se cargue como walkover +en vez de como un resultado jugado. +- Un equipo necesita **al menos 4 jugadores habilitados** de su plantel de esa temporada para que se + le pueda cargar un resultado normal (`PlayerStatisticService.EnsureTeamMeetsHabilitadoMinimumAsync`, + usa `TournamentCompletabilityValidator.MinPlayersPerTeam`, que bajó de 5 a 4 y ahora cuenta + jugadores **habilitados**, no simplemente registrados en el plantel). +- Se chequea ANTES de mirar qué jugadores puntuaron en la planilla — un equipo por debajo del + umbral falla esta regla aunque la planilla venga vacía. +- Si un equipo no llega al mínimo, `PUT /api/matches/{id}/result-from-sheets` y + `PUT /api/matches/{id}/result-from-team-sheet` rechazan con 409 y un mensaje que indica cuántos + habilitados tiene y que debe cargarse como walkover (`ErrorMessages.MatchSheet.TeamRequiresWalkOver`); + el walkover en sí sigue siendo una acción manual del admin ("Marcar W.O.", HU-73), no automática — + el sistema bloquea el camino incorrecto, no ejecuta el correcto por su cuenta. + +### HU-119 · [NUEVO] Torneo no puede iniciar si algún equipo no llega al mínimo de habilitados — `M` +**Como** owner/admin **quiero** que el sistema me impida iniciar un torneo si algún equipo inscripto +no tiene suficientes jugadores habilitados **para** no arrancar una temporada que va a terminar en +walkovers por incumplimiento de plantel. +- Extiende la guarda de completitud existente (HU-109): la regla `TeamTooFewPlayers` de + `TournamentCompletabilityValidator` ahora cuenta jugadores **habilitados** (ficha médica + Aprobada + archivo real almacenado) en vez de simplemente registrados, y el mínimo bajó de 5 a 4 + — alineado con el umbral de walkover de HU-118. +- Un equipo con 5 jugadores registrados pero ninguno habilitado ya NO pasa esta guarda (antes sí, + porque solo contaba registros); el torneo se queda bloqueado en `RegistrationClosed` hasta que se + corrija. + +--- + +## Épica 12 — Fixture y jornadas + +### HU-63 · Partidos agrupados por jornada — `M` ✅ + +### HU-64 · Generar el fixture automático al cerrar inscripción — `M` ❌ el disparador que describe está mal +**El disparador real es "En curso" (iniciar el torneo), no "Inscripción cerrada".** Esta era ya una +inconsistencia interna del doc anterior (contradecía a HU-37/HU-108, que sí tenían el disparador +correcto). El código lo confirma sin ambigüedad: `TournamentService.cs` solo llama +`GenerateFixtureAsync` dentro de la rama `newStatus == TournamentStatus.Ongoing`. El resto de la +historia (aleatorio, respeta enfrentamientos configurados, idempotente por división) sigue vigente. + +### HU-65 · Cantidad de fechas y calendario del fixture — `M` ✅ +### HU-66 · Asignar cancha al partido más tarde — `M` ✅ +### HU-67 · Orden de jornadas fijo — `M` ✅ +### HU-68 · Reprogramar / suspender un partido — `C` 🔄 +Sigue vigente, con una precisión de ubicación: la acción vive en la **lista de fixture** (fila del +partido), no en la página de detalle del partido — no es un cambio funcional, solo dónde está el +botón. + +--- + +## Épica 13 — Carga del partido: resultado y goleadores + +### HU-69 · Cargar el resultado del partido — `M` 🔄 +Sigue vigente, con un campo nuevo: checkbox **"Se jugó tiempo extra"** (`Match.WentToOvertime`) — es +puramente informativo, no cambia el cálculo del marcador ni la regla de sin-empates (HU-70). **Además**, +la carga ahora está bloqueada si algún equipo no llega al mínimo de jugadores habilitados — ver +HU-118 (nueva). + +### HU-70 · Sin empates: todo partido cargado tiene ganador — `M` ✅ +### HU-71 · Cargar goleadores del partido — `M` ✅ +### HU-72 · Persistir anotadores y conectarlos al ranking — `M` ✅ + +### HU-73 · Walkover / ausencia (W.O.) — `S` 🔄 +Sigue vigente tal cual como acción MANUAL del admin ("Marcar W.O.", elige el equipo presente, +`MatchService.LoadWalkOverAsync`). **Precisión agregada en esta pasada**: el sistema no dispara un +walkover solo — lo que hace es bloquear la carga de un resultado NORMAL cuando un equipo no llega al +mínimo de 4 habilitados (HU-118), dejando el walkover manual como el único camino disponible para +cerrar ese partido. No hay detección automática de "equipo ausente" más allá de esa guarda. + +--- + +## Épica 14 — Gestión de sanciones (administración) + +### HU-74 · Crear una sanción desde el partido — `M` ✅ +### HU-75 · Duración de la sanción en fechas, cleanup por días — `M` ✅ +### HU-76 · Vencimiento automático de sanciones — `S` ✅ +### HU-77 · Sanción también a equipo o staff — `C` ✅ +Sigue vigente: `SanctionSubjectType` (Jugador/Equipo/Staff) existe en el modelo y la consulta pública +distingue el tipo de sujeto. + +**Sin HU en la versión anterior: workflow de apelación** — ver HU-115 (nueva). + +--- + +## Épica 15 — Clasificación y playoffs + +### HU-78 · Tabla de posiciones por zona/división — `M` 🔄 +Sigue vigente, con un agregado sin HU previa: en una división **sin fase de playoff**, el 1er puesto +de la tabla pública se corona (ícono + fila destacada) **en vivo**, apenas es el líder actual — no +espera a que termine la temporada (`crownFirstPlace` en `PublicDivisionPanel.tsx`/ +`divisionStandings.tsx`). Corregido en esta sesión tras una corrección del owner: la primera versión +solo coronaba una vez decidido el podio; el criterio correcto es que el color siga a la posición +actual, igual que el resto del tinte por rango de clasificación. + +### HU-79 · Sistema de puntaje configurable — `S` ✅ +### HU-80 · Desempate de la tabla (fase de grupos) — `M` ✅ + +Las dos siguen vigentes tal cual. + +### HU-81 · Definir los clasificados a playoffs desde la tabla — `M` 🔄 +Sigue vigente en el concepto, con dos cambios: los rangos ya no son un editor manual (ver HU-45), y +**el poblado de las copas ya no requiere un paso manual separado** — ver HU-82. + +### HU-82 · Bracket de playoffs con siembra, prórroga y BYE — `M` ✅ +**Como** usuario **quiero** ver el cuadro de playoffs con los cruces sembrados **para** seguir la +definición. +- ✅ **Auto-siembra**: al completarse la fase de grupos de una división, las copas de playoff se + siembran **automáticamente** (`StageService.TryAutoSeedPlayoffPhaseAsync`), sin acción manual del + admin. No pisa una siembra ya hecha a mano. +- ✅ **BYE**: sigue funcionando para clasificados que no son potencia de 2. +- ✅ **Sin empates** en fase de grupos; prórroga (`WentToOvertime`) es el mecanismo, ver HU-69. +- ✅ **"Serie al mejor de N" (HU-46) se genera de verdad en producción (corregido 2026-09-02).** + `StageService.SeedPlayoffCupsAsync`/`SeedKnockoutStageAsync`/`SeedMultiGroupCrossCupStageAsync` + crean un `MatchSeries` real (vía `MatchSeriesService`) para cada cruce cuando `Stage.BestOf > 1` — + ya no siembran un `Match` plano. `SeriesInProgressPanel` (tab Playoff de la división, admin) + agrega la acción de UI que faltaba para cargar el 2º/3er partido de una serie + (`addGameToSeries`). +- ✅ **El ganador de cada serie avanza automáticamente (corregido 2026-09-02).** + `StageService.TryAdvanceStageWinnerAsync` toma el ganador de cada slot recién decidido (serie o + partido único) y lo carga en el slot correspondiente de la ronda siguiente del mismo bracket — + llamado desde las 3 acciones de carga de resultado de `MatchController`, y también desde el propio + seeding para propagar los BYE automáticamente. +- ✅ **Partido por el tercer puesto, opcional por copa.** `StageType.ThirdPlace` es un tipo de etapa + propio; cuando la copa lo incluye, los dos perdedores de semifinal avanzan automáticamente a ese + partido (`StageService.AdvanceLosersToThirdPlaceAsync`). No tenía HU propia en ninguna versión + anterior de este documento. + +--- + +## Épica 16 — Estadísticas y rankings + +### HU-83 · Estadísticas globales del sistema — `M` ✅ + +### HU-84 · Estadísticas por torneo — `M` 🔄 +**No existe un tab "Estadísticas" dentro del torneo como tal.** `TournamentPage.tsx` tiene +`Detalle | Divisiones | Equipos | Asignación`, sin tab de estadísticas. Las estadísticas scopeadas a +un torneo viven en los tabs de cada división (Posiciones/Goleadores/Partidos/Playoff) y en la página +global `StatisticsPage.tsx`, filtrable por torneo/temporada. + +### HU-85 · Estadísticas por temporada y de todos los tiempos — `S` ✅ +Sigue vigente tal cual (`ScorerRepository.GetPlayerScoresAsync`). + +### HU-86 · Ranking de goleadores — `S` 🔄 +**Más simple de lo que describía la versión anterior — corregir las expectativas, no solo la +ubicación.** +- ✅ Ahora tiene un lugar fijo en el panel: tab **"Goleadores"** dentro de la división admin + (`DivisionScorersTable.tsx`), con Imprimir + Exportar CSV, igual que Posiciones. +- ✅ Se alimenta de la misma carga de HU-71/72, sin escritura manual aparte. +- ✅ Muestra jugador, dorsal (de la inscripción del torneo, no del puntero denormalizado), puntos y + equipo. +- ❌ **NO** muestra partidos jugados ni promedio — esos campos no existen en `ScorerByPlayerResponse`. +- ❌ **NO** hay ninguna lógica de desempate — el orden es `OrderByDescending(Points)` liso, sin + segundo criterio. Si dos jugadores empatan en puntos, el orden entre ellos es indefinido. + +### HU-87 · Ficha de estadística individual del jugador — `C` ✅ +### HU-88 · Historial de un jugador entre temporadas — `C` ✅ + +Ambas siguen vigentes tal cual. + +### HU-86b · [NUEVO] Top 10 en el ranking de goleadores público — `S` +**Como** visitante **quiero** ver un ranking de goleadores corto en la página pública del torneo +**para** no tener que scrollear una lista larga en una vista que es solo un vistazo rápido. +- La tabla de goleadores de la página pública de una división limita a los primeros 10 + (`PUBLIC_TOP_SCORERS_LIMIT` en `PublicDivisionPanel.tsx`, prop `limit` de `DivisionScorersTable`). +- El panel admin (HU-86) NO tiene este límite — sigue mostrando el ranking completo, paginado. + +### HU-89 · Exportar posiciones / estadísticas — `C` 🔄 +**Solo CSV — no hay exportación a PDF.** El botón "Imprimir" abre el diálogo de impresión nativo del +navegador sobre una hoja con estilos de impresión (el usuario puede elegir "Guardar como PDF" desde +ahí), pero no hay generación de PDF del lado de la app (`jsPDF`/`pdfmake`/servidor) en ningún punto +del código. Corregir "CSV/PDF" a "CSV + impresión nativa del navegador". + +--- + +## Épica 17 — Canchas + +### HU-90 · CRUD de canchas — `M` 🔄 +Sigue vigente, con más de lo que describía: además de nombre/dirección/imagen, cada cancha tiene +**latitud/longitud** con un **mapa interactivo (Leaflet/OpenStreetMap)** — geocodificado +automáticamente desde la dirección tipeada, con un pin arrastrable a mano para corregir. También +tiene `Slug` (link público estable). + +--- + +## Épica 18 — Respaldos y administración de datos + +### HU-91 · Generar respaldo manual — `M` ✅ +### HU-92 · Bloquear la app durante respaldo/restauración — `M` ✅ + +### HU-93 · Respaldo de seguridad automático antes de restaurar — `M` 🔄 +Sigue vigente en el mecanismo (se genera una salvaguarda antes de restaurar), con una corrección: la +salvaguarda **NO se elimina** al terminar con éxito — queda en el catálogo de respaldos, sujeta solo +a la poda normal por retención en el próximo backup. + +### HU-94 · Límite de copias de seguridad — `S` 🔄 +El tope por defecto es **7**, no 5 (`BackupOptions.RetentionCount = 7`). + +### HU-95 · Respaldos programados — `S` 🔄 +El intervalo por defecto es **diario (24 horas)**, no semanal (`BackupOptions.IntervalHours = 24`). +Es configurable, pero "semanal" no es lo que trae por defecto. + +### HU-96 · Borrado total de datos — `S` ✅ +Sigue vigente, restringido a `AdminOrOwner` con confirmación fuerte. + +### HU-97 · Carga de datos de prueba (solo Admin IT) — `S` ❌ ELIMINADA (deliberado, no un bug) +El botón "Cargar Datos de prueba" fue sacado del panel de Administración de datos. Esta historia debe +retirarse del documento, no marcarse como pendiente. + +--- + +## Épica 19 — Modelo season-scoped (fundacional) + +### HU-98 · Equipos y jugadores scopeados por temporada/torneo — `M` 🔄 +El principio sigue vigente tal cual (`TeamTournamentRegistration`/`PlayerTeamRegistration` como +fuente de verdad, sin reasignar `Team.TournamentId`), pero la historia conflaba "temporada" y +"torneo" como lo mismo. Ya no lo son — ver D7/HU-113: `Season` es una entidad propia por encima de +`Tournament`. El scoping de equipos/jugadores sigue siendo a nivel Torneo, sin cambios. + +### HU-99 · Identidad de club estable entre temporadas — `C` 🔄 +**Ya no es "[NUEVA]"/especulativa — está implementada.** Entidad `Club` real +(`Club12-Backend/Domain/Entities/Models/Club.cs`) con `Team.ClubId` opcional y su propio +`ClubService`. Reclasificar de Could/no-empezada a Must/hecha. + +--- + +## Épica 20 — Coherencia técnica transversal + +### HU-100 · Zona horaria Argentina en fechas y horarios — `S` ✅ +Sigue vigente: almacenamiento en UTC, presentación en `America/Argentina/Buenos_Aires`. + +### HU-101 · Auditoría de acciones sensibles — `C` ⚠️ brecha real detectada +**Como** admin IT **quiero** un registro de quién borró datos, restauró respaldos o cambió estados +**para** trazabilidad. +- ✅ Los 4 tipos de acción de la historia original están cubiertos por el enum (`AuditAction`: + `DataWipe`, `BackupRestore`, `TournamentStatusChange`, `PasswordReset`) — no hay auditoría de + "toda mutación administrativa", solo estas 4 (consistente con `Docs/ESTADO-Y-REGLAS.md`, que ya + listaba "auditoría completa" como pendiente). +- ✅ Nombres legibles: se resuelve el nombre real del objetivo (torneo, usuario por email) en vez de + mostrar un UUID crudo, y el detalle queda en español (`ToSpanishLabel()`). +- ✅ **`AuditAction.BackupRestore` se loguea (corregido 2026-09-02).** `BackupOperationsService` + ahora recibe `IAuditService` y llama `LogAsync` al final de `RestoreBackupAsync`. + +--- + +## Épica 21 — Rediseño visual + +### HU-102 · Design system: tema oscuro con acentos naranja — `M` ✅ +### HU-103 · Aplicar el tema al sitio público — `S` ✅ +### HU-104 · Aplicar el tema al panel de administración — `S` ✅ +### HU-105 · Consistencia responsive y accesibilidad — `C` 🔄 +Las cuatro siguen vigentes en el principio; se confirmó además una implementación de sidebar +responsive específica (AppBar colapsable + drawer temporal en mobile, comentarios de código citan +"HU-105" explícitamente) que la versión anterior del doc no detallaba. + +--- + +## Épica 22 — Rediseño del flujo de inscripción y armado del torneo + +### HU-106 · El asistente crea torneo + estructura, sin equipos ni fixture — `M` ✅ +### HU-107 · Inscripción de equipos con el torneo abierto — `M` 🔄 +Sigue vigente tal cual, con una precisión: ahora tiene una **ruta/ítem de sidebar propios** +(`/panel/registro-equipos`, "Inscripción de equipos"), separados tanto del wizard como de la +asignación a zonas — es un paso de su propio ciclo de vida, no una sub-pantalla de otra cosa. La +opción "copiar plantel de temporada anterior" (HU-53) vive acá. + +### HU-108 · Asignar inscriptos a divisiones y generar el fixture al iniciar — `M` 🔄 +Sigue vigente tal cual, con una precisión de UI: el paso de asignación tiene su propio tab +**"Asignación"** en `TournamentPage.tsx` (ver HU-30). + +### HU-109 · Guardas de completitud — `M` 🔄 +Sigue vigente: `TournamentCompletabilityValidator` implementa las 6 reglas (zona con muy pocos +equipos, equipo sin asignar, equipo en más de una zona, rango de playoff más allá de los equipos +asignados, grupo de copa cruzada con muy pocos equipos, y equipo con muy pocos jugadores), con su +propia suite de tests. **La última regla cambió en esta pasada — ver HU-119 (nueva)**: ya no cuenta +jugadores simplemente registrados sino habilitados, y el mínimo bajó de 5 a 4. + +### HU-110 · Copa cruzada con múltiples grupos de tamaño variable — `M` 🔄 +**El "gap actual" que anotaba la versión anterior ya está cerrado.** +`Division.QualifiersPerGroup` es un campo de primera clase; el diseño elegido es una única División +`IsCrossDivisionCup=true` que contiene VARIOS `Stage` de tipo Grupo (no N divisiones cruzadas como +especulaba el doc viejo). El ejemplo real también cambió: ya no es "37 equipos → 10 grupos" — el +seed actual usa una Copa Cruzada masculina de 6 zonas de fase de grupos (5 de 4 equipos + 1 de 3, +ida y vuelta) alimentando un bracket combinado de 12 con byes. + +### HU-111 · Calendario: jornadas de zona y copa no se solapan — `M` ✅ +Sigue vigente: zonas juegan domingos, copa cruzada otro día fijo, mismo helper compartido +(`RoundCalendar`) para producción y seed. + +### HU-112 · [NUEVO] Auto-derivación de las rondas de una copa según su cantidad de clasificados — `M` +**Como** owner/admin **quiero** que la cantidad de rondas de una copa (Final / Semis+Final / +Cuartos+Semis+Final...) se calcule sola a partir de cuántos equipos clasifican **para** no tener que +configurar cada ronda a mano. +- Al definir `qualifiers` (cantidad de clasificados) para una copa, el wizard deriva automáticamente + su estructura de rondas — referenciado en el propio código como "HU-112" + (`submitWizard.ts`/`deriveCupMappings`). Reemplaza el editor manual de rangos que describía la + HU-45 original. + +--- + +## Auditoría 2026-09-05 — "Fase de grupos" no permite sub-dividir una división en zonas + +Pedido del owner: revisar si el checkbox "Fase de grupos" del asistente arma correctamente una +1ª fase de grupos (con 1, 2 o 3 vueltas) + una 2ª fase de playoff opcional, y si permite repartir +una división grande en sub-grupos (Zona A, B, C...) con reparto balanceado y umbral mínimo de +equipos por zona. Verificado contra código real (`Club12-WebClient/src/views/tournament/wizard/`, +`Club12-Backend/Application/Services/StageService.cs`, `TournamentDivisionAssignment.tsx`). + +**Hallazgo central: colisión de vocabulario.** En este código, lo que el wizard llama "zona" +(`ZoneConfig`) es sinónimo de "división" — cada `ZoneConfig` se convierte en UNA división completa +(ej. "Primera División"), no en un sub-grupo dentro de ella. El switch "Fase de grupos" +(`ZoneEditor.hasGroupStage`) es un booleano de UN SOLO grupo: prende/apaga un único `Stage` tipo +`Group` para toda la división, con 1 a 3 vueltas (`roundRobinLegs`) — eso YA funciona bien y coincide +con el pedido de "1era fase, puede jugarse 1/2/3 veces". Lo que NO existe hoy, en ningún lado, es +dividir esa fase de grupos en sub-grupos paralelos (Zona A / Zona B / Zona C) dentro de una misma +división — que es exactamente lo que describe el pedido para divisiones grandes. + +`StageService.CreateStageAsync` lo bloquea explícitamente: un segundo `Stage` tipo `Group` en la +misma división tira `ErrorMessages.Stage.GroupStageAlreadyExistsInDivision`, salvo que la división +sea `IsCrossDivisionCup` (la Copa Cruzada, HU-110) — la única competencia que hoy puede tener varios +grupos ("Grupo 1"..."Grupo N") bajo una misma división. Es decir: la capacidad de fondo YA EXISTE en +el modelo de datos (un `Stage` es simplemente un grupo con nombre y equipos asignados), pero está +reservada por regla de negocio a un caso especial (copa cruzada entre divisiones) en vez de estar +disponible para el uso normal que describe el pedido (sub-dividir UNA división grande). + +### HU-120 · [NUEVO] Dividir una división en sub-grupos (Zona A/B/C) dentro de su fase de grupos — `M` +**Como** owner/admin **quiero** que, al activar "Fase de grupos" para una división, pueda elegir en +cuántos sub-grupos se reparten sus equipos **para** organizar divisiones grandes en pools manejables +antes de un cruce de playoffs, en vez de un único todos-contra-todos gigante. +- La solución más consistente con el código existente es generalizar la capacidad que hoy solo tiene + la Copa Cruzada (`Division.IsCrossDivisionCup`, varios `Stage` tipo `Group` bajo una división) a + CUALQUIER división — hoy el chequeo de "un solo Group stage" en `StageService.CreateStageAsync` + solo se salta para `IsCrossDivisionCup`. +- La pantalla de asignación de equipos (`TournamentDivisionAssignment.tsx`) YA itera sobre una lista + de `groups` (plural) por división y arma una tarjeta de asignación por CADA `Group` stage + encontrado — el hueco real está en el wizard (`ZoneEditor` no permite crear más de un `Group` stage + por división) y en el backend (`CreateStageAsync` lo prohíbe fuera de la copa cruzada), no en la + pantalla de asignación, que ya está preparada para esto. +- Conviene nombrar el nuevo nivel explícitamente "sub-grupo" o "pool" en el modelo (no reusar "zona", + que ya significa "división" en este código) para no perpetuar la ambigüedad que originó esta + auditoría. + +### HU-121 · [IMPLEMENTADO] La cantidad de sub-grupos la define el organizador; el tamaño se reparte balanceado — `M` +**Como** owner/admin **quiero** elegir CUÁNTOS sub-grupos quiero (no un tamaño fijo por grupo) y que +el sistema reparta los equipos lo más parejo posible **para** no terminar con grupos desbalanceados +(ej. "2 grupos de 5 y uno de 6" cuando "4 grupos de 4" sería mejor) ni tener que armarlo a mano. +- El wizard (`ZoneEditor`) pide la cantidad de sub-grupos (`ZoneConfig.subGroupCount`, default `1` = + el comportamiento de siempre, una sola fase de grupos) en vez de un tamaño fijo — a diferencia del + mecanismo legado `CreateAutomatedStagesAsync` (ver HU-124), que forzaba siempre grupos de + exactamente 4 equipos. +- Reparto balanceado real: con `G` sub-grupos y `T` equipos inscriptos en el roster de la división + (`DivisionTeamRegistration`), cada sub-grupo recibe `floor(T/G)` o `ceil(T/G)` equipos — nunca una + diferencia de 2 o más entre el más chico y el más grande. 16 equipos en 3 sub-grupos da 5+5+6. +- Umbral mínimo de 4 equipos por sub-grupo, aplicado en el backend: pedir una cantidad de sub-grupos + que dejaría alguno por debajo de ese piso se rechaza con un mensaje que nombra la cantidad de + equipos y de sub-grupos pedidos (`ErrorMessages.Stage.SubGroupTooFewTeams`), sin crear ni cambiar + ninguna estructura. +- Validado en dos momentos, como estaba previsto: (a) en el wizard, advertencia no bloqueante (todavía + no hay inscriptos reales, solo una estimación); (b) de forma bloqueante en + `TournamentCompletabilityValidator` (extensión de HU-109) al cerrar inscripción/iniciar el torneo, + contra la cantidad real de inscriptos — cubre tanto "muy pocos equipos" como un reparto desbalanceado + a mano (diferencia ≥ 2 entre sub-grupos). +- La cantidad elegida al armar la estructura es un punto de partida, no una decisión final: HU-122 + (reparto automático + movimiento manual) y HU-123 (cambiar la cantidad después) cubren la edición + posterior, siempre antes de que el torneo arranque. + +### HU-122 · [IMPLEMENTADO] Asignar equipos a sub-grupos: automático por defecto, manual siempre disponible — `S` +**Como** owner/admin **quiero** que el sistema reparta los equipos inscriptos entre los sub-grupos +automáticamente, pero pudiendo mover equipos a mano **para** ajustar por criterios que el sistema no +conoce (cercanía geográfica, nivel competitivo, evitar que dos clásicos rivales queden en la misma +zona, etc.). +- "Auto-repartir" (`TournamentDivisionAssignment.tsx` → `AutoDistributeRosterAsync`) vacía los + sub-grupos actuales y vuelve a correr el reparto balanceado sobre TODO el roster de la división — + nunca solo rellena los huecos vacíos, así el resultado queda siempre balanceado aunque el estado + previo no lo estuviera. +- El movimiento manual de un equipo entre dos sub-grupos de la misma división está siempre disponible + (acción "Mover a otro sub-grupo" por equipo, `ReassignTeamToSubGroupAsync`), sin re-disparar el + reparto del resto de los equipos. La única restricción real es no dejar el sub-grupo de origen por + debajo del piso de 4 equipos; por lo demás el movimiento no tiene ninguna restricción extra + inventada del lado del cliente — incluso un movimiento que desbalancea a propósito los sub-grupos se + permite, quedando a criterio del organizador. +- Un equipo puede estar inscripto en el roster de la división sin todavía estar ubicado en ningún + sub-grupo — es un estado válido, no un error, hasta que se corra el auto-reparto o se lo ubique a + mano. + +### HU-123 · [IMPLEMENTADO] Editar la cantidad de sub-grupos de una división antes de que arranque el torneo — `S` +**Como** owner/admin **quiero** poder cambiar la cantidad de sub-grupos de una división después de +creada la estructura pero antes de iniciar el torneo **para** ajustar el armado a la cantidad real de +equipos que se terminaron inscribiendo, que casi nunca coincide con la estimación inicial del wizard. +- "Editar cantidad de sub-grupos" (`RebuildSubGroupsAsync`) reconstruye SOLO la capa de + `Stage`/`StageTeamMatch` de sub-grupos — los borra y crea de nuevo con la nueva cantidad — y vuelve a + repartir balanceado. El roster (`DivisionTeamRegistration`) nunca se toca: los mismos equipos + inscriptos siguen inscriptos, ninguno queda sin registro aunque cambie de sub-grupo. +- Disponible mientras la división sea editable, es decir, mientras el torneo no esté + `Ongoing`/`Finished`/`Canceled` — la misma guarda `EnsureDivisionStructureEditableAsync` que ya usan + la creación/edición de stages y la asignación de equipos. + +### HU-124 · [RESUELTO] Dos mecanismos distintos y contradictorios para armar la fase de grupos — `M` +**Como** desarrollador **quiero** una sola fuente de verdad para "cómo se arma la estructura de +grupos de una división" **para** no tener dos caminos que puedan dejar el torneo en un estado +inconsistente según cuál se use. +- Resuelto por eliminación completa, no por retiro parcial: `StageService.CreateAutomatedStagesAsync`, + su endpoint (`POST /api/stages/generate/{id}`, `StageController.GenerateStagesAndMatches`) y todos + sus llamadores — backend y frontend (`generateStages`/`generateStagesAutomatically`), más la + constante `TournamentBracketSize` — fueron borrados por completo. No tenía ningún llamador real en + la UI, así que no hay reemplazo que mantener compatible. La única fuente de verdad para armar la + fase de grupos de una división es ahora el mecanismo de sub-grupos de HU-120/121/122/123. + +### HU-125 · [FUERA DE ALCANCE] El reparto en sub-grupos rompe el cálculo actual de posiciones→copa (HU-112) — `M` +**Como** owner/admin **quiero** que, con sub-grupos, las copas de playoff clasifiquen por posición +DENTRO de cada sub-grupo (ej. "1° y 2° de cada zona") en vez de por una tabla combinada **para** que +el cruce a playoffs tenga sentido cuando hay más de una zona. +- Confirmado explícitamente fuera de alcance de esta implementación — queda para un cambio posterior, + con su propio diseño de qué tabla combinada tiene sentido entre sub-grupos. +- En vez de dejar que un cálculo combinado sin sentido se compute en silencio, el sistema RECHAZA la + combinación directamente, en cualquier orden de configuración: activar 2 o más sub-grupos en una + división que ya tiene una copa por rango de posiciones (`cupPositionRange`), o configurar una copa + por rango de posiciones en una división que ya tiene 2 o más sub-grupos, devuelven el mismo error + explicando la incompatibilidad y ofreciendo las dos salidas ("usá un solo sub-grupo o quitá el mapeo + de playoff", `ErrorMessages.Stage.SubGroupsIncompatibleWithPositionRangeCups`). Con un solo sub-grupo + (el comportamiento de siempre) las copas por rango de posiciones siguen funcionando exactamente + igual que hoy. + +## Auditoría 2026-09-05 (continuación) — flujos completos con datos reales, pensando como organizador + +Segunda ronda de E2E pedida por el owner: en vez de solo cargar pantallas, completar flujos reales de +punta a punta (crear → editar → eliminar, con guardado real) en Canchas, Jugadores, Novedades, +Sanciones+apelación y Usuarios. Se encontraron y arreglaron 5 bugs reales (ver commits del +2026-09-05: fix del crash al ver un jugador recién creado, fechas de nacimiento con un día de error en +toda la lista de Jugadores, falta el botón Editar en Novedades, aceptar una apelación no tenía efecto +real, y error crudo en inglés al crear un usuario con espacios en el nombre) — quedaron registrados en +la Épica 24 como resueltos. Además, pensando específicamente como organizador de torneo (no solo +QA de pantallas), se verificaron dos puntos más contra el código real: + +### HU-126 · [NUEVO] La fecha límite de inscripción es puramente informativa — no bloquea ni se puede corregir — `M` +**Como** owner/admin **quiero** que la fecha límite de inscripción sea real (bloquee inscripciones +pasada esa fecha) o, si no lo es, poder corregirla libremente **para** no quedar con un dato +inconsistente que no puedo arreglar. +- Verificado en `TeamService.cs` (líneas 360/430/501): inscribir un equipo está controlado + ÚNICAMENTE por `Tournament.Status == OpenForRegistration` — la fecha `TeamRegistrationDeadline` + nunca se compara contra la fecha actual en ningún lado del backend. Es decir, un equipo puede + inscribirse aunque la "fecha límite" mostrada ya haya pasado, mientras el admin no haya cerrado la + inscripción a mano; y a la inversa, cerrar la inscripción antes de esa fecha corta todo aunque el + plazo mostrado diga que falta tiempo. El campo es puramente decorativo. +- El problema real: `TournamentEditPage.tsx` SÍ trata esa fecha como si fuera autoritativa — + `canEditRegistrationDeadline = !registrationClosed && status === OpenForRegistration`, donde + `registrationClosed` se calcula comparando la fecha contra "ahora". Una vez que la fecha pasa, el + campo se bloquea para editar, sin importar que el torneo siga técnicamente abierto a inscripciones + y que ese valor nunca haya bloqueado nada. Un organizador que decide extender la inscripción una + semana más (algo muy común en la práctica) queda con una fecha límite vieja y visiblemente + incorrecta en pantalla, sin ninguna forma de corregirla. +- Arreglo sugerido: o (a) hacer que la fecha realmente bloquee la inscripción de equipos en el + backend (coherente con lo que el campo dice ser), o (b) si se mantiene como solo informativa, + dejarla editable mientras el torneo siga en `OpenForRegistration`, sin importar si la fecha ya pasó + — la opción (a) es la más honesta con el organizador, que espera que "fecha límite" signifique algo. + +### HU-127 · [NUEVO] Reprogramar un partido suspendido no valida choques de cancha ni de equipo — `M` +**Como** owner/admin **quiero** que reprogramar un partido suspendido a una nueva fecha valide que no +choque con otro partido en la misma cancha, o con otro partido del mismo equipo (incluida su copa +cruzada si juega una) **para** no terminar armando sin querer dos partidos imposibles de jugar a la +vez. +- Verificado en `MatchService.SuspendMatchAsync`: cuando se pasa `newMatchDate`, el método hace + `match.MatchDate = newMatchDate.Value` y guarda, sin ningún chequeo de conflicto — a diferencia de + la generación automática del fixture original, que sí respeta separación de horarios por cancha + (regla de las 2 horas mencionada en sesiones previas). Reprogramar a mano puede crear silenciosamente + dos partidos en la misma cancha a la misma hora, o el mismo equipo jugando dos partidos el mismo día + (su zona regular y, si corresponde, la copa cruzada — HU-110, que exime al equipo de la regla de + "una sola zona" pero no dice nada sobre choques de calendario entre ambas competencias). +- No es necesariamente un bloqueo duro: alcanza con una advertencia clara antes de confirmar + ("Esta cancha ya tiene un partido a esa hora" / "Este equipo ya juega otro partido ese día") que el + admin pueda decidir ignorar conscientemente — pero hoy no hay ningún aviso, ni siquiera informativo. + +### HU-128 · [IMPLEMENTADO] Sorteo de llave para divisiones sin fase de grupos — `M` +**Como** owner/admin **quiero** sortear (o armar a mano) el emparejamiento inicial de una llave de +playoffs cuando la división no tiene fase de grupos previa **para** poder organizar un torneo formato +solo-eliminación sin depender de una tabla de posiciones que no existe. +- La inscripción funciona igual que en cualquier división, vía el roster (`DivisionTeamRegistration`) + de HU-120 — no hace falta ningún `Stage` de grupos para inscribir equipos ni para que la pantalla de + asignación muestre un panel donde hacerlo. +- Sorteo aleatorio con vista previa: "Sortear llave" le pide al servidor una vista previa que no + persiste nada, y el organizador puede volver a sortear las veces que quiera antes de confirmar. Al + confirmar, la llave queda IDÉNTICA a la última vista previa mostrada — un token firmado del lado del + servidor (no un sorteo del lado del cliente) garantiza que lo confirmado sea exactamente lo + previsualizado. +- Sorteo manual: el organizador ordena los equipos a mano, sin ningún sorteo aleatorio, y confirma esa + lista exacta. +- Los byes de una cantidad de equipos que no es potencia de 2 se resuelven con el mecanismo existente + (`PlayoffSeeder.SeedPairs` + `TryAdvanceStageWinnerAsync`) — nada nuevo ahí, reusado tal cual. +- Traba de re-sorteo por llave (`Stage` + nombre de bracket, así que llaves paralelas como "Copa de + Oro"/"Copa de Plata" se traban cada una por separado, independientemente del estado del torneo): se + puede volver a sortear libremente mientras NINGÚN partido real se haya jugado; apenas se juega el + primero, un nuevo sorteo de esa llave se rechaza. Un bye nunca cuenta como "jugado" a este efecto — + una llave recién sorteada con byes sigue siendo re-sorteable. +- Cada sorteo, inicial o re-sorteo, queda auditado (`AuditAction.PlayoffDraw`, visible en el panel de + auditoría admin) con el modo (aleatorio/manual) y la cantidad de equipos — una falla al auditar + nunca bloquea el sorteo en sí. +- La vista pública de la llave muestra "Sorteo realizado el [fecha]" a partir de `Stage.DrawnAt`, sin + necesitar estar logueado ni acceder a la auditoría (que es solo para Admin/Owner); no muestra nada + mientras la división no fue sorteada. +- Explícitamente no incluye HU-125 (clasificación por sub-grupo hacia una copa) — una división con + sub-grupos y una copa por rango de posiciones sigue rechazada, como describe HU-125. + +--- + +## Épica 23 — Temporada, sanciones ampliadas y funcionalidades nuevas sin historia previa + +Historias nuevas para funcionalidad que existe en el código pero no tenía HU en ninguna versión +anterior de este documento. + +### HU-113 · [NUEVO] Entidad Temporada (Season) agrupando torneos — `M` +**Como** owner/admin **quiero** agrupar los torneos de un mismo período (habitualmente uno masculino ++ uno femenino) bajo una Temporada **para** organizar el club año a año en vez de torneo por torneo +suelto. +- CRUD de Temporada (nombre, año) en el panel; es el punto de entrada del sidebar admin (ver HU-26). +- El detalle de una temporada lista sus torneos agrupados por categoría, con "Nuevo Torneo" + pre-scopeado a esa temporada. +- Vista pública equivalente (`/temporadas`, `/temporadas/:id`). +- `Season → Tournament` es `SetNull`, no `Cascade`: borrar/limpiar una temporada nunca destruye el + historial de sus torneos. +- No cambia el scoping de equipos/jugadores (sigue siendo a nivel Torneo, D2/HU-98). + +### HU-114 · [NUEVO] Revertir un torneo "En curso" a borrador — `M` +**Como** owner/admin **quiero** poder deshacer el inicio de un torneo si todavía no se jugó nada +**para** corregir una asignación de zonas equivocada sin tener que recrear el torneo entero. +- Transición `Ongoing → RegistrationClosed`, disponible solo si **cero partidos fueron jugados**. +- Borra el fixture generado; conserva las asignaciones de equipos a zona (no hay que rehacerlas). +- Bloqueada (409, con motivo) si algún partido ya se jugó. +- ⚠️ Ver la brecha del mapa de transiciones del frontend anotada en HU-35. + +### HU-115 · [NUEVO] Apelación de sanciones — `S` +**Como** equipo/jugador sancionado **quiero** poder apelar una sanción **para** que el admin la +revise antes de que se cumpla en su totalidad. +- Una sanción activa puede pasar a estado "apelada" con motivo; el admin la resuelve como aceptada + (levanta la sanción) o rechazada (sigue vigente). +- Sin ruta de auto-servicio pública confirmada en esta pasada — a verificar si la apelación la carga + el propio equipo o el admin en su representación. + +### HU-116 · [NUEVO] Cuerpo técnico del equipo (DT / Asistente) — `S` +**Como** owner/admin **quiero** registrar el cuerpo técnico de un equipo (Director Técnico, +Asistente) por temporada **para** llevar esa información igual que el plantel de jugadores. +- Tab "Cuerpo técnico" dentro del equipo (junto a Jugadores/Puntuaciones/Sanciones). +- Roles: solo **DT** y **Asistente** (el rol "DT-Jugador" que existió brevemente fue retirado — + quedan dos roles, no tres). +- Scopeado por equipo + temporada, igual que el plantel. +- Visible también en la página pública del equipo. + +### HU-117 · [NUEVO] Importación de plantel por CSV — `S` +**Como** owner/admin **quiero** cargar varios jugadores de una sola vez subiendo un CSV **para** dar +de alta un plantel completo más rápido que fila por fila. +- Reemplaza al viejo botón "Importar plantel de una temporada anterior" en el tab Jugadores del + equipo (esa funcionalidad de copiar-de-temporada-previa sigue existiendo, pero reubicada — ver + HU-53). +- Columnas en el mismo orden que la fila editable de alta manual (HU-52): Nombre, Segundo nombre, + Apellido, Documento, Fecha de nacimiento, Teléfono, Obra social. +- Cada fila se valida con las mismas reglas que el alta manual antes de enviarse; las filas inválidas + se listan con su motivo y NO se importan — las válidas sí, sin que un error en una fila bloquee al + resto. +- Botón para descargar una plantilla CSV vacía con los headers correctos. + +--- + +## Épica 24 — Deuda técnica y brechas conocidas (no son historias, son follow-ups) + +Recopilado de la auditoría 2026-09-02. No son features nuevas — son casos donde el comportamiento +documentado/esperado y el código real difieren, y alguien tiene que decidir qué lado gana. + +> **Resueltos (2026-09-02, misma sesión, después de la auditoría)**: los ítems 1-5 de abajo ya +> fueron corregidos y están en `develop`. **Resueltos (2026-09-03, sesión de reglas de +> habilitación)**: los ítems 8-10. **Resueltos (2026-09-05, ronda de E2E con flujos completos y +> datos reales)**: los ítems 11-15. HU-126/HU-127 (arriba, fin de la Épica 22) quedaron abiertas de +> esa misma ronda — no son deuda técnica sino comportamiento a decidir con el owner. + +1. ~~**Series playoff Best-of-N no se generan en producción (HU-82/HU-46).**~~ Resuelto: + `StageService.SeedPlayoffCupsAsync`/`SeedKnockoutStageAsync`/`SeedMultiGroupCrossCupStageAsync` + crean un `MatchSeries` real por cruce cuando `Stage.BestOf > 1`, y `SeriesInProgressPanel` (tab + Playoff de División, admin) agrega la acción de UI para cargar el 2º/3er partido de una serie. +2. ~~**Sin avance automático entre rondas de bracket (HU-82).**~~ Resuelto: + `StageService.TryAdvanceStageWinnerAsync` empuja el ganador de cada slot decidido (serie o + partido único) a la ronda siguiente del mismo bracket — llamado desde las 3 acciones de carga de + resultado de `MatchController` y desde el propio seeding (para propagar byes). +3. ~~**`AuditAction.BackupRestore` nunca se loguea (HU-101).**~~ Resuelto: + `BackupOperationsService.RestoreBackupAsync` ahora loguea vía `IAuditService`. +4. ~~**Mapa de transiciones de estado del frontend desactualizado (HU-35/HU-114).**~~ Resuelto: + `tournamentStatusTransitions.ts` incluye `Ongoing → RegistrationClosed`. +5. ~~**Validación del wizard permite copa cruzada sin playoff (HU-47).**~~ Resuelto: + `validateCrossCupStep` rechaza una copa cruzada con `cups.length === 0`. +6. ~~**`TournamentsPage.tsx` (listado plano de torneos) quedó huérfano (HU-29).**~~ Resuelto + (2026-09-02, commit `7eb4dd0`): se eliminaron las tres rutas huérfanas (`/torneos`, + `/panel/torneos`, `/panel/divisiones` sin scope) — cada "Volver"/cancelar ahora usa + `navigate(-1)` en vez de apuntar a una de estas rutas muertas. Esta nota había quedado + desactualizada en la pasada del 2026-09-03: el archivo ya no existe en el repo, solo el texto no + se había corregido. +7. ~~`Docs/QA-CHECKLIST-E2E.md`~~ — el checklist se retiró del repo (ya cumplió su función; el E2E + contra staging que documentaba se completó en la sesión del 2026-09-03, ver ítems 8-10). Si se + necesita un nuevo barrido E2E, generar uno nuevo desde cero contra las rutas reales, no reflotar + el archivo viejo. +8. ~~**`LoadMatchResultFromSheetsRequest.MatchId` marcado `required` rechazaba toda carga de + resultado (HU-69).**~~ Resuelto (2026-09-03): el DTO nunca recibe `matchId` del cliente (viene de + la ruta), pero `required` hacía que `System.Text.Json` rechazara el body entero antes de que el + controller pudiera asignarlo desde la ruta — ningún resultado se pudo cargar por este endpoint + hasta el fix. +9. ~~**El roster de un partido mostraba "No habilitado" para todos, incluso jugadores realmente + habilitados (HU-62).**~~ Resuelto (2026-09-03): `CreateMap()` no + tenía mapeo para `MedicalRecordStatus`/`IsHabilitado`/`JerseyNumber` (viven en + `PlayerTeamRegistration`, no en `Player`), y el include de EF tampoco cargaba esa navegación para + el roster (sí para los goleadores). El dato real en base siempre fue correcto — solo lo que + mostraba la respuesta del partido estaba mal. Corregido con un paso `AfterMap` en + `MatchProfile.cs` + el include faltante en `MatchRepository.GetDetailByIdOrSlugAsync`. +10. ~~**Datos de seed sembrados antes de la feature de ficha médica quedaron con TODOS los jugadores + en `Pending` (HU-57/HU-118/HU-119).**~~ Resuelto (2026-09-03): + `MedicalRecordSeedBackfiller.BackfillMedicalRecordsAsync` ahora también toma como candidatos los + registros `Pending`/`Rejected` cuyo `CreatedBy == AuditConstants.SystemUser` — un valor que solo + escribe el seeder, nunca una acción real de admin — y los aprueba junto con subirles el archivo. + Un registro creado por un admin real, sin importar su estado, nunca entra en este camino. Esto + hace que el self-heal automático de `DataSeeder.SeedAsync` (que ya corre solo con que + `Seed:Enabled=true`, sin flags extra) cubra tanto una base nunca migrada a la feature de ficha + médica como una restaurada desde un backup viejo — no hace falta una acción puntual como la que + se usó para arreglar el dato en vivo esta sesión. +11. ~~**Ver un jugador recién creado desde el panel rompía toda la página (crash).**~~ Resuelto + (2026-09-05): `POST /api/players` (admin-only) devolvía `PublicPlayerResponse` (forma pública, + sin `documentNumber`/`birthDate`/`phoneNumber`/`socialSecurity`) pero el contexto de React seguía + ese objeto incompleto como el jugador "actual"; al navegar al detalle antes de que la carga real + terminara, el render usaba esos campos faltantes y tiraba. Corregido devolviendo + `AdminPlayerResponse` (mismo shape que ya usa `GetPlayerByIdCompleteDataAsync`) desde el create. +12. ~~**Fecha de nacimiento mostrada con un día de menos en toda la lista de Jugadores.**~~ Resuelto + (2026-09-05): la columna usaba `new Date(value).toLocaleDateString()`, que corre la fecha a + horario local (Argentina, UTC-3) para un campo que es solo-fecha (mediodía UTC), restando un día. + Corregido reusando `formatCalendarDate`, el helper ya establecido en el proyecto para este caso + exacto (usado en la ficha de jugador, pero no en el listado). +13. ~~**Novedades: no había forma de editar una publicación ya creada.**~~ Resuelto (2026-09-05): + `BlogPostEditPage` y su ruta (`/panel/blog/:blogPostId/editar`) ya estaban completos y andando, + pero la lista solo ofrecía "Ver" (una previsualización) y "Eliminar" — nada enlazaba a la edición. +14. ~~**Aceptar la apelación de una sanción no tenía ningún efecto real.**~~ Resuelto (2026-09-05): + `FechasRemaining`/`IsActive` se calculaban solo a partir de `Duration` y fechas jugadas, sin mirar + `AppealStatus` en ningún lado — el jugador/equipo seguía suspendido igual, apelación aceptada o no. +15. ~~**Crear un usuario con espacios en el nombre mostraba un error crudo en inglés.**~~ Resuelto + (2026-09-05): no había validación de formato de nombre de usuario en el frontend, así que el + valor viajaba tal cual al backend, que rechazaba con el texto nativo de ASP.NET Identity + ("Username '...' is invalid, can only contain letters or digits.") sin traducir. + +--- + +## Resumen + +- **Test counts**: verificado 2026-09-03: **826 tests backend, 725 tests frontend**, ambos en verde + (la cifra "808/645" de dos pasadas atrás ya está vieja). Correr + `dotnet test Club12-Backend/Solution/Club12.sln` y `npx vitest run` (desde `Club12-WebClient`) + para el número vigente en cualquier momento — no confiar en un número congelado en este documento. +- **Historias obsoletas/reemplazadas**: HU-29 (listado plano de torneos, eliminado — ver Épica 24 + ítem 6), HU-41/HU-42 (superadas por HU-106/107/108, sin cambios respecto a la nota original), + HU-64 (disparador de fixture mal descripto), HU-97 (eliminada a propósito). +- **Épica 24 (deuda técnica) completa**: los 10 ítems están resueltos, ninguno queda abierto. Los + últimos tres en cerrarse (2026-09-03): HU-69 (`MatchId` required bloqueaba toda carga de + resultado), HU-62 (roster mostraba "No habilitado" para todos por un mapeo faltante), y el + self-heal de datos de seed viejos (ítem 10, guarda por `CreatedBy == SystemUser`). +- **Cambio funcional más grande de esta pasada (2026-09-03)**: las tres reglas de habilitación + alrededor de la carga de resultados — HU-118 (umbral de 4 habilitados para cargar un resultado + normal) y HU-119 (mismo umbral para poder iniciar el torneo) — más la corrección del mensaje de + error de HU-62 (lista agrupada por equipo, con nombres reales) y el bug de visualización del + roster (ítem 9 de Épica 24). +- **Cambio estructural más grande de la pasada anterior**: la entidad Temporada (Season) por encima + de Torneo (D7/HU-113), que reordenó la navegación completa del panel admin y el punto de entrada + del wizard.