> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cel-eleague.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Palmarès

> Source métier pour les récompenses, honneurs joueurs, TOTS, MVP et publications de fin de saison.

# Palmarès

Cette page couvre le palmarès CEL au sens large: récompenses de saison,
honneurs joueurs, TOTS, MVP/Ballon d'Or, TOTW et MVP de match.

Elle documente l'état actuellement implémenté dans le code.

## Sources principales

* `convex/schema.ts`
* `convex/competition/season_awards.ts`
* `convex/lib/season_mvp.ts`
* `convex/competition/totw.ts`
* `convex/awards/recipients.ts`
* `web/src/app/admin/season-awards`
* `web/src/app/(app)/palmares/[awardId]/page.tsx`
* `web/src/components/club/ClubPalmaresTab.tsx`
* `web/src/components/player/PlayerHonoursPanel.tsx`

## Modèle de publication

Le palmarès de fin de saison est publié depuis l'admin `season-awards`.

La publication crée:

* une ligne `season_awards`, qui porte le snapshot global publié;
* des lignes `season_award_entries`, qui portent les entrées visibles du
  palmarès de saison;
* des lignes `award_recipients`, qui normalisent les honneurs par joueur.

`award_recipients` sert de table commune pour les honneurs publics joueur:

| Type               | Source               |
| ------------------ | -------------------- |
| `season_champion`  | palmarès de saison   |
| `season_runner_up` | palmarès de saison   |
| `season_mvp`       | palmarès de saison   |
| `best_gk`          | palmarès de saison   |
| `tots`             | palmarès de saison   |
| `totw`             | équipe de la semaine |
| `motm`             | match                |

## Catégories de saison

Les catégories de `season_award_entries` sont:

| Catégorie    | Sens                         |
| ------------ | ---------------------------- |
| `CHAMPION`   | club champion de la saison   |
| `RUNNER_UP`  | club vice-champion           |
| `SEASON_MVP` | MVP/Ballon d'Or de la saison |
| `BEST_GK`    | meilleur gardien             |
| `TOTS`       | onze de saison               |

La page publique `/palmares/[awardId]` expose les trophées de saison et la TOTS
publiée. Les onglets de club et de joueur réutilisent les entrées normalisées
pour afficher les honneurs dans les profils.

## Sortie publique `/palmares/[awardId]` (contrat)

La query publique `getPublishedSeasonAwards` retourne désormais:

* `finalists.mvp`: les 5 meilleurs nommés MVP depuis `season_awards.mvpNomineesSnapshot`;
* `finalists.bestGk`: les 5 meilleurs nommés meilleur gardien depuis
  `season_awards.bestGkNomineesSnapshot`;

Chaque entrée de `finalists` contient au minimum:

* `playerProfileId`, `name`, `avatarUrl`, `clubName`, `clubLogoUrl`, `position`,
  `rank`, `goals`, `assists`, `matchesPlayed`, `score`
  (les champs peuvent être `null` si le snapshot source est incomplet).

La section `tots.players` expose aussi une sous-structure `stats` limitée aux
champs:

* `goals`, `assists`, `matchesPlayed`, `performanceScore`, `ratingAverage`,
  `totwCount`, `matchMvpCount`, `teamRank`.

Pour les catégories `SEASON_MVP` et `BEST_GK`, le snapshot persisté par
`publishSeasonAwards` inclut aussi `ratingAverage` dans `entry.stats`; la valeur
peut être `null` si elle n'est pas calculable.

Pour `BEST_GK`, `entry.stats` inclut aussi:

* `saves`,
* `cleanSheets`.

## Règles de publication

Règles vérifiées dans `publishSeasonAwards`:

* publication réservée aux admins;
* ligue requise en statut `COMPLETED` ou `ARCHIVED`;
* sélection MVP requise;
* sélection meilleur gardien requise;
* sélection TOTS optionnelle si aucune formation complète n'est disponible;
* remplacement d'un palmarès existant possible;
* notifications et emails activés par défaut.

## Réactions B4 sur les palmarès

La query publique `getAwardCongratulations` est disponible côté social:

* `getAwardCongratulations({ awardId, actorUserId? })`
  * Retourne `{ byEntryId: Record<entryId, { count, byReaction, mine }> }`
    pour toutes les entrées de l’award.
  * `count`: total des réactions de l’entrée.
  * `byReaction`: compte par réaction (`MERITE`, `QUELLE_SAISON`, `FIER`,
    `RENDEZVOUS`).
  * `mine`: réaction de l’utilisateur courant ou `null`.

La même surface expose les mutations:

* `congratulate({ awardId, entryId, reaction, actorUserId? })`
  * Ajoute/remplace la réaction d’un utilisateur connecté pour une entrée.
* `uncongratulate({ awardId, entryId, actorUserId? })`
  * Supprime la réaction de l’utilisateur connecté si elle existe.

Spécification B4:

* une seule réaction active par (`entryId`, utilisateur).
* pas d’usage de `collect().length` pour les compteurs; lecture via table
  dénormalisée `award_congratulation_counts`.

## Statut personnel du palmarès (B5)

La query `getMyPalmaresStatus({ awardId, actorUserId? })` retourne, pour
l'utilisateur connecté:

* `awardedEntries`: les `entryId` publics du palmarès où l'utilisateur est
  lauréat;
* `totsSlots`: les slots TOTS publics associés à cet utilisateur;
* `isAwarded`: `true` dès qu'au moins une entrée du palmarès lui correspond.

La résolution croise:

* `season_award_entries.recipientUserId`, utile pour les entrées joueur et les
  destinataires directs;
* `award_recipients`, utile pour les récompenses de club qui sont normalisées
  vers les joueurs actifs au moment de la publication.

## Requêtes de sélection de saison

La couche publique expose désormais deux queries pour trouver des publications:

* `listPublishedSeasonAwards({ leagueId? })`
  → `{ awardId, leagueId, leagueName, season, publishedAt }[]`, triées en
  `publishedAt` décroissant.
* `getLatestPublishedSeasonAwards({ leagueId? })`
  → le même format pour la publication la plus récente, ou `null` si aucune.

Quand `leagueId` est fourni, ces queries restreignent à une ligue précise.

## Modèle de comparaison par poste

TOTS et MVP partagent le même principe: **un joueur n'est jamais comparé à toute
la ligue en vrac, mais aux autres joueurs de son poste**. La chaîne de
résolution du poste se fait en trois temps.

### 1. Normalisation du poste détaillé

Les libellés bruts sont d'abord ramenés à un code détaillé canonique
(`normalizePositionDetailedForStats`). Exemples d'alias:

| Codes bruts                  | Code canonique |
| ---------------------------- | -------------- |
| `GK`, `G`, `GOALKEEPER`      | `GK`           |
| `CB`, `DEF`, `DEFENDER`      | `DC`           |
| `LB`, `LWB`                  | `DG`           |
| `RB`, `RWB`                  | `DD`           |
| `CDM`                        | `MDC`          |
| `CM`, `MID`                  | `MC`           |
| `CAM`                        | `MOC`          |
| `LM` / `LW`                  | `MG` / `AG`    |
| `RM` / `RW`                  | `MD` / `AD`    |
| `ST`, `CF`, `ATT`, `FORWARD` | `BU`           |

On obtient \~13 postes détaillés: `GK`, `DG`, `DC`, `DD`, `MDC`, `MC`, `MOC`,
`MG`, `MD`, `AG`, `AD`, `BU`.

### 2. Famille de comparaison

Après normalisation, le poste détaillé est rattaché à une famille de scoring
(`resolveScorePositionFamily`). C'est cette famille qui sert au calcul des
barèmes de performance.

| Famille    | Postes détaillés       |
| ---------- | ---------------------- |
| `GK`       | `GK`                   |
| `DC`       | `DC`                   |
| `FULLBACK` | `DG`, `DD`             |
| `MDC`      | `MDC`                  |
| `MID`      | `MC`, `MOC`            |
| `WIDE`     | `MG`, `MD`, `AG`, `AD` |
| `BU`       | `BU`                   |

Conséquence: un `DG` et un `DD` partagent le benchmark `FULLBACK`; un `MG`,
un `MD`, un `AG` et un `AD` partagent le benchmark `WIDE`. Les barèmes de
performance (meilleur total, moyenne des 10 meilleurs) sont calculés **par
famille de poste**, pas par poste détaillé isolé.

Le poste détaillé reste conservé pour l'affichage, l'éligibilité aux cases de
formation et la sélection du slot TOTS. Il évite par exemple de traiter un
`MD` comme un latéral `DD`, même si les deux jouent dans un couloir.

### 3. Agrégation multi-postes d'un même joueur

Un joueur joué à plusieurs libellés qui retombent sur la même famille de
comparaison voit ses lignes fusionnées (`aggregateTotsPositionStats`): matchs
dédoublonnés par `matchId`, buts/passes/points cumulés. Le poste représentatif
est celui où il a le plus de matchs.

## Formule TOTS (`tots-v3`)

La TOTS désigne les meilleurs joueurs à chaque poste. La comparaison statistique
se fait au sein de la famille de poste du joueur.

Pour être candidat à un poste, un joueur doit franchir deux seuils:

* un seuil de saison: avoir joué au moins `min(10, matchs max de la saison)`
  matchs au total;
* un seuil de poste: avoir joué au moins `max(15, plafond(20% de ses matchs de
  saison))` matchs **dans la famille de comparaison** considérée.

Le `15` n'est donc qu'un plancher: un joueur qui a disputé 100 matchs doit en
avoir joué au moins 20 au poste pour y être candidat. La régularité ne doit pas
ajouter de points séparés: elle est portée par ces seuils d'éligibilité.

### Performance /70

La performance utilise le total de points cumulés dans la famille, pas une
moyenne par match.

```text theme={null}
Performance = (total points du joueur dans la famille / meilleur total de la famille) x 70
```

Exemple:

* meilleur total de la famille: 500 points;
* total du joueur: 450 points;
* performance: `450 / 500 x 70 = 63`.

### Distinctions /10

```text theme={null}
Distinctions = min(TOTW x 2, 6) + min(MVP match x 2, 4)
```

### Collectif /20

| Classement | Points |
| ---------- | -----: |
| 1er        |     20 |
| 2e         |     18 |
| 3e         |     16 |
| 4e         |     14 |
| 5e         |     12 |
| 6e         |     10 |
| 7e         |      8 |
| 8e         |      6 |
| 9e         |      4 |
| 10e à 14e  |      2 |

### Total TOTS

```text theme={null}
Score TOTS = performance /70 + distinctions /10 + collectif /20
```

## Formule MVP / Ballon d'Or (`mvp-v2`)

Le MVP/Ballon d'Or doit comparer toute la ligue sans favoriser automatiquement
les postes offensifs. Il utilise donc un Performance Index basé sur la famille
de poste.

### Famille retenue et éligibilité (changement v3)

Avant la v3, le MVP notait chaque joueur sur ses **totaux globaux de saison**
(tous postes confondus) et son poste principal. La v3 change cela
(`selectMvpScoringPosition` + `toSeasonMvpCandidate`):

* on regroupe les stats du joueur par famille de comparaison (même
  chaîne que la TOTS), on agrège, puis on ne garde que les postes franchissant
  le seuil d'éligibilité `max(15, plafond(20% des matchs de saison))`;
* parmi ces postes éligibles, on retient **le meilleur** (plus haut total de
  points, puis plus de matchs);
* les `matchs`, `buts`, `passes` et `points` du candidat MVP proviennent de **ce
  seul poste**, pas de ses totaux globaux;
* un joueur qui ne franchit le seuil à **aucun** poste est exclu du classement.

Ensuite, le Performance Index compare le total du joueur aux barèmes **de sa
famille de poste** (meilleur total + moyenne des 10 meilleurs de cette famille).
C'est ce qui
neutralise l'avantage des postes offensifs: un défenseur excellent est noté par
rapport aux meilleurs défenseurs, pas par rapport aux buteurs.

Pour le meilleur gardien, le même mécanisme s'applique en forçant le poste `GK`.

### Performance Index /70

```text theme={null}
A = total points du joueur au poste / meilleur total du poste
B = total points du joueur au poste / moyenne des 10 meilleurs totaux du poste

Performance Index = 0,7 x A + 0,3 x B
Performance MVP = Performance Index x 70
```

Exemple:

* meilleur total du poste: 500;
* moyenne des 10 meilleurs du poste: 476;
* total du joueur: 500;
* `A = 500 / 500 = 1`;
* `B = 500 / 476 = 1,05`;
* `Performance Index = 0,7 x 1 + 0,3 x 1,05 = 1,015`.

La performance MVP doit rester plafonnée à 70 points pour conserver un total
final sur 100.

### Distinctions /10

Le MVP utilise le même système de distinctions que la TOTS:

```text theme={null}
Distinctions = min(TOTW x 2, 6) + min(MVP match x 2, 4)
```

Les bonus Top 5 buteur, passeur ou gardien ne font pas partie de la formule.

### Collectif /20

Le MVP utilise le même bonus collectif que la TOTS.

### Total MVP

```text theme={null}
Score MVP = performance index /70 + distinctions /10 + collectif /20
```

## Versioning

* TOTS: `tots-v3`;
* MVP/Ballon d'Or: `mvp-v2`.

Les versions de formule sont stockées dans les snapshots publiés afin de
préserver les palmarès déjà générés.
