# Spec di design — Fase 3.2: Admin (ciclo evento)

**Data:** 2026-06-12 · **Stato:** deciso in autonomia (Paolo: "parti in autonomia, finisci tutto il progetto, poi validiamo") · **Autore:** Claude
**Base:** master spec §4/§9/§10/§11 · F3.0 (schema/infra) · F3.1 (engine puro) · spec F3.1.
**Premessa:** F3.0 (schema + neon-serverless + authz/locals) e F3.1 (engine puro: round-robin, standings, seeding, promo/retro) sono complete e LIVE. F3.2 è il **collante**: UI admin + service layer che mette l'engine sul DB con transazioni.

---

## 1. Obiettivo e ambito
Dare all'admin il **ciclo di vita completo di un evento Bolgia**: crearlo da un FormatTemplate, gestire le iscrizioni, fare seeding + generare gironi e calendario (chiamando l'engine F3.1 e **scrivendo** sul DB), inserire i risultati, calcolare le classifiche, proporre promo/retro e **chiudere la stagione** scrivendo il Ranking della successiva.

**Dentro:**
- **Service layer** `src/lib/server/events/` (orchestrazione engine↔DB, transazioni neon-serverless, idempotente).
- **UI `/admin`** gated da `requireAdmin`: lista eventi, crea evento, dettaglio evento con tutte le azioni, vista gironi/partite, inserimento risultati, classifiche, proposte promo/retro, chiusura stagione.

**Fuori (fasi successive):** auto-iscrizione lato giocatore + dashboard giocatore → F3.3. Vetrina-dal-DB → F3.4. Migrazione storico → F3.5. Notifiche → F3.6. Engine BRACKET/A_SQUADRE → post-V1.

## 2. Decisioni d'impianto (autonome)
1. **Service layer separato dall'engine**: l'engine resta puro (F3.1); i service leggono/scrivono il DB e chiamano l'engine. File per area: `event-service`, `registration-service`, `generation-service`, `result-service`, `season-service`.
2. **Idempotenza generazione**: `seedAndGenerate` cancella gironi/match/standing esistenti della fase e li **rigenera** (in transazione) → ri-eseguibile senza duplicati. Coerente con "engine rigenerabile".
3. **Transazioni**: tutte le operazioni multi-scrittura usano `db.transaction()` (neon-serverless). Se una fallisce, rollback.
4. **Stato evento** come macchina: `bozza → iscrizioni → in_corso → concluso`. Transizioni esplicite con guardie (es. non si genera se non `in_corso`/iscrizioni chiuse; `format_config` si "congela" passando a in_corso — master spec §16.3).
5. **Seeding ranking**: la `rankingPosition` di un iscritto = la sua `posizione_globale` nel Ranking della stagione **più recente** (se esiste), altrimenti `null` (nuovo → in fondo). Finché non c'è migrazione (F3.5), quasi tutti `null` → seeding per ordine d'iscrizione (degenerato ma funzionale).
6. **Bootstrap ruoli admin**: non esiste ancora una UI di gestione ruoli (è roba superadmin, fase successiva). Per ora l'abilitazione admin/developer si fa **via SQL** (documentato). Paolo si registra e poi: `update "user" set ruolo='developer' where email=...`.
7. **Chiusura stagione → Ranking**: `closeSeason` materializza il Ranking della stagione successiva dalle classifiche finali, ordinando per `girone.rank_base + posizione - 1` (proiezione master spec §6: A=1-13, B=14-26…). Promo/retro **proposte** dall'engine sono mostrate e, se l'admin le applica, modificano l'ordine prima della scrittura. Versione V1: scrive il Ranking append-only dalla classifica corrente (promo/retro come **suggerimento visivo**, applicazione manuale).
8. **UI funzionale, tema esistente**: riuso `Card`/`Section`/`DataTable` + token pergamena; SvelteKit **form actions** + `use:enhance`. Non pixel-perfect: l'obiettivo è il flusso completo e corretto.

## 3. Service layer — contratti (`src/lib/server/events/`)
> Tutte server-only, usano `db` (neon-serverless). Le multi-scrittura in `db.transaction(tx => …)`.

**`event-service.ts`**
- `listEvents(): Promise<EventRow[]>` — tutti gli eventi (admin), ordine per createdAt desc.
- `getEventBySlug(slug): Promise<EventDetail | null>` — evento + fase/i + gironi + conteggi.
- `createEventFromTemplate(input: { nome; tipo; seasonId?; templateKey: 'BOLGIA_13'|'BOLGIA_7' }): Promise<{ slug }>` — crea `event` (slug da nome+suffix) + una `phase` `GIRONI_ROUND_ROBIN` con `config` = template; `formatConfig` = template. Stato `bozza`.
- `setEventState(eventId, nuovoStato): Promise<void>` — valida la transizione; passando a `in_corso` "congela" (no-op sui dati, lo stato impedisce modifiche al config).

**`registration-service.ts`**
- `listRegistrations(eventId): Promise<RegistrationRow[]>` (+ player nome).
- `setRegistrationState(id, stato): Promise<void>`.
- `registerPlayer(eventId, playerId): Promise<Result>` — usato da F3.3; in F3.2 l'admin può aggiungere iscritti. Unicità `(eventId, playerId)`.

**`generation-service.ts`** (il cuore)
- `seedAndGenerate(eventId): Promise<{ gironi: number; partite: number }>`:
  1. Carica la fase `GIRONI_ROUND_ROBIN` + il suo `config` (GironiConfig).
  2. Carica gli iscritti `confermata` + la loro `rankingPosition` (vedi §2.5).
  3. `seeding(iscritti, { dimensione })` → gironi.
  4. **In transazione**: cancella standing/match/girone della fase; inserisce i `girone`; per ogni girone con dimensione supportata (7/13) chiama `generateRoundRobin(dimensione, config)`, mappa gli indici sui `playerIds` del girone, inserisce `match` + `matchParticipant`. I gironi Coda/H con dimensione non supportata → nessun calendario (placeholder, segnalato).
  5. Ritorna i conteggi.

**`result-service.ts`**
- `enterMatchResult(matchId, partecipanti: { participantId; puntiCultura; resign }[]): Promise<void>` — aggiorna i `matchParticipant`, calcola `posizione` per cultura, setta `match.stato='conclusa'`, poi `recomputeGironeStandings`.
- `recomputeGironeStandings(gironeId): Promise<void>` — carica i match conclusi del girone, costruisce i `MatchResult`, chiama `computeStandings(config.tabellaPunti)`, **upsert** delle righe `standing`.

**`season-service.ts`**
- `proposePromoRetro(phaseId): Promise<PromoRetroProposal[]>` — carica gironi+standings, chiama `applyPromoRetro`.
- `closeSeason(eventId): Promise<{ rankingRows: number }>` — in transazione: per ogni girone, ordina gli standing per posizione, scrive righe `ranking` (seasonId dell'evento, `posizione_globale = rank_base + posizione - 1`, `serie = livello`, `is_coda`), setta `event.stato='concluso'`. Append-only.

## 4. UI `/admin`
- `/admin/+layout.server.ts` — `requireAdmin(locals)` (redirect/403). Layout con intestazione "Area gestione".
- `/admin/+page` — lista eventi (DataTable: nome, tipo, stagione, stato, #iscritti) + bottone "Nuovo evento".
- `/admin/eventi/new/+page` — form: nome, tipo (select), stagione (select/crea al volo), template (BOLGIA_13/7). Action → `createEventFromTemplate` → redirect al dettaglio.
- `/admin/eventi/[slug]/+page` — **hub** dell'evento:
  - intestazione (nome, stato, azioni di stato: apri/chiudi iscrizioni, avvia, concludi);
  - **iscritti**: lista + stato, aggiungi iscritto (select player), conferma/rifiuta;
  - **azione "Seeding + genera gironi"** (se iscrizioni chiuse) → `seedAndGenerate`;
  - **gironi**: per ogni girone, tabella partite (con form risultato inline) + classifica corrente;
  - **promo/retro**: tabella proposte;
  - **chiudi stagione** → `closeSeason`.
  (Una pagina hub con sezioni; se diventa grande, si spezza in sotto-rotte — ma per V1 una pagina con sezioni `Section`.)

## 5. Test
- **Integration (Neon)** `src/lib/server/events/*.integration.test.ts`: createEventFromTemplate → registra N player → seedAndGenerate (verifica #gironi/#match coerenti con l'engine, ogni coppia 2× dentro un girone) → enterMatchResult → recompute standings (classifica corretta) → closeSeason (ranking scritto). Cleanup.
- **Unit**: la logica pura è già in F3.1; i service sono orchestrazione → coperti da integration.
- Smoke UI: `pnpm check`/`build` verdi; le pagine montano.

## 6. Note/decisioni da validare con Paolo (alla fine)
- Bootstrap ruolo admin via SQL (manca UI gestione ruoli — superadmin, fase futura).
- Promo/retro in V1 = proposta visiva; `closeSeason` scrive il ranking dalla classifica corrente (applicazione promo/retro manuale prima della chiusura, se serve). Regole D3 indecise → niente automazione vincolante.
- Gironi Coda/H con dimensione non standard (es. 8): nessun calendario round-robin generato (l'engine supporta 7/13). La Coda è "parcheggio" finché non promuove; calendario Coda = fase futura/decisione organizzativa.
- Inserimento risultati: l'admin inserisce i punti cultura per partecipante (i match si giocano sull'app CGE esterna; qui si registrano gli esiti).
</content>
