Add playing handicap and stroke allocation to game summary

- Calculate course handicap per player (WHS formula) in game_summary
- Show Playing Handicaps card with HI, HC, and tee per player
- Mark stroke-receiving holes with accent dots in strokes and skins scorecards
- Add HC footer row to strokes scorecard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Rolf
2026-03-22 11:16:26 +01:00
parent 59a82eb4ad
commit 1daf6a99c4
2 changed files with 72 additions and 1 deletions
+28 -1
View File
@@ -280,6 +280,32 @@ async def game_summary(game_id: str, request: Request, user: dict = Depends(get_
if row["stroke_index"] is not None: if row["stroke_index"] is not None:
hole_stroke_indices[row["hole_number"]] = row["stroke_index"] hole_stroke_indices[row["hole_number"]] = row["stroke_index"]
# Playing handicap per player: HI × (slope / 113) + (course_rating course_par)
course_par = sum(hole_pars.get(h, 0) for h in holes if h in hole_pars)
playing_handicaps: dict[str, int | None] = {}
for p in players:
hc = p.get("handicap_snapshot")
slope = p.get("slope")
rating = p.get("tee_rating")
if hc is not None and slope and rating and course_par:
playing_handicaps[p["id"]] = round(hc * (slope / 113) + (rating - course_par))
else:
playing_handicaps[p["id"]] = None
# Strokes received per player per hole (requires stroke index data on course)
strokes_per_hole: dict[str, dict[int, int]] = {}
played_with_si = {h: hole_stroke_indices[h] for h in holes if h in hole_stroke_indices}
if played_with_si:
n_si = len(played_with_si)
for p in players:
ph = playing_handicaps.get(p["id"])
if ph is not None and ph > 0:
base, extra = divmod(ph, n_si)
strokes_per_hole[p["id"]] = {
h: base + (1 if played_with_si[h] <= extra else 0)
for h in holes if h in played_with_si
}
skins_won, skins_hole_results, unclaimed = None, None, 0 skins_won, skins_hole_results, unclaimed = None, None, 0
if game["format"] == "skins": if game["format"] == "skins":
carryover = bool(game["carryover"]) carryover = bool(game["carryover"])
@@ -291,8 +317,9 @@ async def game_summary(game_id: str, request: Request, user: dict = Depends(get_
return templates.TemplateResponse("games/summary.html", { return templates.TemplateResponse("games/summary.html", {
"request": request, "user": user, "game": dict(game), "request": request, "user": user, "game": dict(game),
"players": players, "scores": scores, "totals": totals, "players": players, "scores": scores, "totals": totals,
"holes": holes, "hole_pars": hole_pars, "holes": holes, "hole_pars": hole_pars, "hole_stroke_indices": hole_stroke_indices,
"results": results, "results": results,
"playing_handicaps": playing_handicaps, "strokes_per_hole": strokes_per_hole,
"skins_won": skins_won, "skins_hole_results": skins_hole_results, "unclaimed": unclaimed, "skins_won": skins_won, "skins_hole_results": skins_hole_results, "unclaimed": unclaimed,
"carryover": bool(game["carryover"]), "carryover": bool(game["carryover"]),
}) })
+44
View File
@@ -48,6 +48,37 @@
{% endif %} {% endif %}
</div> </div>
{# Playing handicaps #}
{% if playing_handicaps %}
<div class="card" style="margin-top:0.75rem; padding:0.75rem 1rem;">
<div style="font-size:0.8rem; color:var(--text-muted); font-weight:500; margin-bottom:0.5rem; text-transform:uppercase; letter-spacing:0.05em;">Playing Handicaps</div>
{% for p in players %}
<div style="display:flex; align-items:center; gap:0.6rem; padding:0.35rem 0; {% if not loop.last %}border-bottom:1px solid var(--border);{% endif %} font-size:0.9rem;">
<span>
{% if p.avatar and p.avatar.startswith('/') %}
<img src="{{ p.avatar }}" style="width:1.3rem;height:1.3rem;border-radius:50%;object-fit:cover;vertical-align:middle;">
{% else %}
{{ p.avatar or '👤' }}
{% endif %}
</span>
<span style="flex:1; font-weight:500;">{{ p.name }}</span>
{% if p.handicap_snapshot is not none %}
<span style="color:var(--text-muted);">HI&nbsp;{{ p.handicap_snapshot }}</span>
{% endif %}
{% set ph = playing_handicaps.get(p.id) %}
{% if ph is not none %}
<span style="font-weight:600; color:var(--accent); min-width:3rem; text-align:right;">HC&nbsp;{{ ph }}</span>
{% else %}
<span style="color:var(--text-muted); min-width:3rem; text-align:right;">HC&nbsp;</span>
{% endif %}
{% if p.tee_name %}
<span style="color:var(--text-muted); font-size:0.8rem; min-width:4rem; text-align:right;">{{ p.tee_name }}</span>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{# Scorecard toggle #} {# Scorecard toggle #}
<div class="scorecard-toggle" style="margin-top:1rem;"> <div class="scorecard-toggle" style="margin-top:1rem;">
<button @click="view = 'strokes'" :class="{ active: view === 'strokes' }">Strokes</button> <button @click="view = 'strokes'" :class="{ active: view === 'strokes' }">Strokes</button>
@@ -85,6 +116,7 @@
{% set s = scores.get(h, {}).get(p.id, {}) %} {% set s = scores.get(h, {}).get(p.id, {}) %}
{% set strokes = s.strokes if s else 0 %} {% set strokes = s.strokes if s else 0 %}
{% set par = hole_pars.get(h, 0) %} {% set par = hole_pars.get(h, 0) %}
{% set sr = strokes_per_hole.get(p.id, {}).get(h, 0) %}
<td class="score-cell"> <td class="score-cell">
{% if strokes %} {% if strokes %}
{% if par %} {% if par %}
@@ -108,6 +140,7 @@
{% else %} {% else %}
{% endif %} {% endif %}
{% if sr > 0 %}<sup style="color:var(--accent); font-size:0.6em; line-height:0;">{{ '·' * sr }}</sup>{% endif %}
</td> </td>
{% endfor %} {% endfor %}
</tr> </tr>
@@ -126,6 +159,15 @@
<td>{{ stroke_total.val or '—' }}</td> <td>{{ stroke_total.val or '—' }}</td>
{% endfor %} {% endfor %}
</tr> </tr>
{% if playing_handicaps %}
<tr>
<td style="color:var(--text-muted); font-size:0.8rem;">HC</td>
{% if hole_pars %}<td></td>{% endif %}
{% for p in players %}
<td style="color:var(--text-muted); font-size:0.8rem;">{{ playing_handicaps.get(p.id, '—') if playing_handicaps.get(p.id) is not none else '—' }}</td>
{% endfor %}
</tr>
{% endif %}
</tfoot> </tfoot>
</table> </table>
@@ -198,6 +240,7 @@
{% set s = scores.get(h, {}).get(p.id, {}) %} {% set s = scores.get(h, {}).get(p.id, {}) %}
{% set strokes = s.strokes if s else 0 %} {% set strokes = s.strokes if s else 0 %}
{% set par = hole_pars.get(h, 0) %} {% set par = hole_pars.get(h, 0) %}
{% set sr = strokes_per_hole.get(p.id, {}).get(h, 0) %}
<td class="score-cell"> <td class="score-cell">
{% if strokes %} {% if strokes %}
{% set is_winner = hr and hr.winner == p.id %} {% set is_winner = hr and hr.winner == p.id %}
@@ -211,6 +254,7 @@
{% else %}<span{% if is_winner %} class="skin-winner"{% endif %}>{{ strokes }}</span>{% endif %} {% else %}<span{% if is_winner %} class="skin-winner"{% endif %}>{{ strokes }}</span>{% endif %}
{% else %}<span{% if is_winner %} class="skin-winner"{% endif %}>{{ strokes }}</span>{% endif %} {% else %}<span{% if is_winner %} class="skin-winner"{% endif %}>{{ strokes }}</span>{% endif %}
{% else %}—{% endif %} {% else %}—{% endif %}
{% if sr > 0 %}<sup style="color:var(--accent); font-size:0.6em; line-height:0;">{{ '·' * sr }}</sup>{% endif %}
</td> </td>
{% endfor %} {% endfor %}
<td class="score-cell"> <td class="score-cell">