diff --git a/backend/app.py b/backend/app.py index a8cfbab..4bc3a8c 100644 --- a/backend/app.py +++ b/backend/app.py @@ -28,6 +28,8 @@ class Hole(Base): number = Column(Integer, nullable=False) par = Column(Integer, nullable=False) yardage = Column(Integer, nullable=False) + yardage_meters = Column(Integer, default=0) + tee = Column(String, default="White") class Round(Base): __tablename__ = "rounds" @@ -46,6 +48,14 @@ class HoleScore(Base): completed = Column(Boolean, default=False) fairway_hit = Column(Boolean, default=False) gir = Column(Boolean, default=False) + putts_green = Column(Integer, default=0) + putts_off_green = Column(Integer, default=0) + penalty_water = Column(Boolean, default=False) + penalty_ob = Column(Boolean, default=False) + penalty_lost = Column(Boolean, default=False) + bunker_greenside = Column(Boolean, default=False) + bunker_fairway = Column(Boolean, default=False) + unfinished = Column(Boolean, default=False) class Putt(Base): __tablename__ = "putts" @@ -86,15 +96,15 @@ def init_mock(): db.commit() db.refresh(course) holes = [ - Hole(course_id=course.id, number=1, par=4, yardage=350), - Hole(course_id=course.id, number=2, par=3, yardage=180), - Hole(course_id=course.id, number=3, par=5, yardage=520), - Hole(course_id=course.id, number=4, par=4, yardage=390), - Hole(course_id=course.id, number=5, par=3, yardage=160), - Hole(course_id=course.id, number=6, par=4, yardage=410), - Hole(course_id=course.id, number=7, par=5, yardage=540), - Hole(course_id=course.id, number=8, par=4, yardage=370), - Hole(course_id=course.id, number=9, par=3, yardage=190), + Hole(course_id=course.id, number=1, par=4, yardage=350, yardage_meters=320, tee="White"), + Hole(course_id=course.id, number=2, par=3, yardage=180, yardage_meters=165, tee="White"), + Hole(course_id=course.id, number=3, par=5, yardage=520, yardage_meters=480, tee="Yellow"), + Hole(course_id=course.id, number=4, par=4, yardage=390, yardage_meters=360, tee="White"), + Hole(course_id=course.id, number=5, par=3, yardage=160, yardage_meters=145, tee="Red"), + Hole(course_id=course.id, number=6, par=4, yardage=410, yardage_meters=375, tee="Blue"), + Hole(course_id=course.id, number=7, par=5, yardage=540, yardage_meters=495, tee="Yellow"), + Hole(course_id=course.id, number=8, par=4, yardage=370, yardage_meters=340, tee="White"), + Hole(course_id=course.id, number=9, par=3, yardage=190, yardage_meters=175, tee="Red"), ] db.add_all(holes) db.commit() @@ -129,7 +139,7 @@ def start_round(req: StartRoundRequest): db.refresh(round_obj) holes = db.query(Hole).filter(Hole.course_id == req.course_id).all() for h in holes: - hs = HoleScore(round_id=round_obj.id, hole_number=h.number, strokes=0, skipped=False, completed=False, fairway_hit=False, gir=False) + hs = HoleScore(round_id=round_obj.id, hole_number=h.number, strokes=0, skipped=False, completed=False, fairway_hit=False, gir=False, putts_green=0, putts_off_green=0, unfinished=False) db.add(hs) db.commit() db.close() @@ -144,7 +154,7 @@ def get_course(): return { "id": course.id, "name": course.name, - "holes": [{"number": h.number, "par": h.par, "yardage": h.yardage} for h in holes] + "holes": [{"number": h.number, "par": h.par, "yardage": h.yardage, "yardage_meters": h.yardage_meters, "tee": h.tee} for h in holes] } @app.get("/round/{round_id}") @@ -163,9 +173,7 @@ def get_round(round_id: int): "holes": [] } for hs in hole_scores: - putts = db.query(Putt).filter(Putt.hole_score_id == hs.id).all() - penalties = db.query(Penalty).filter(Penalty.hole_score_id == hs.id).all() - bunkers = db.query(Bunker).filter(Bunker.hole_score_id == hs.id).all() + hole = db.query(Hole).filter(Hole.course_id == round_obj.course_id, Hole.number == hs.hole_number).first() result["holes"].append({ "hole_score_id": hs.id, "hole_number": hs.hole_number, @@ -174,21 +182,36 @@ def get_round(round_id: int): "completed": hs.completed, "fairway_hit": hs.fairway_hit, "gir": hs.gir, - "putts": [{"location": p.location} for p in putts], - "penalties": [{"type": p.type} for p in penalties], - "bunkers": [{"location": b.location} for b in bunkers] + "unfinished": hs.unfinished, + "putts_green": hs.putts_green, + "putts_off_green": hs.putts_off_green, + "penalty_water": hs.penalty_water, + "penalty_ob": hs.penalty_ob, + "penalty_lost": hs.penalty_lost, + "bunker_greenside": hs.bunker_greenside, + "bunker_fairway": hs.bunker_fairway, + "hole_info": { + "par": hole.par if hole else None, + "yardage_meters": hole.yardage_meters if hole else 0, + "tee": hole.tee if hole else "White" + } }) db.close() return result class UpdateHoleRequest(BaseModel): - strokes: int = 0 + strokes: int = 1 skipped: bool = False completed: bool = False fairway_hit: bool = False - putts: list = [] - penalties: list = [] - bunkers: list = [] + unfinished: bool = False + putts_green: int = 0 + putts_off_green: int = 0 + penalty_water: bool = False + penalty_ob: bool = False + penalty_lost: bool = False + bunker_greenside: bool = False + bunker_fairway: bool = False @app.post("/hole/{hole_score_id}") def update_hole(hole_score_id: int, req: UpdateHoleRequest): @@ -201,55 +224,58 @@ def update_hole(hole_score_id: int, req: UpdateHoleRequest): hs.skipped = req.skipped hs.completed = req.completed hs.fairway_hit = req.fairway_hit + hs.unfinished = req.unfinished + hs.putts_green = req.putts_green + hs.putts_off_green = req.putts_off_green + hs.penalty_water = req.penalty_water + hs.penalty_ob = req.penalty_ob + hs.penalty_lost = req.penalty_lost + hs.bunker_greenside = req.bunker_greenside + hs.bunker_fairway = req.bunker_fairway - db.query(Putt).filter(Putt.hole_score_id == hole_score_id).delete() - db.query(Penalty).filter(Penalty.hole_score_id == hole_score_id).delete() - db.query(Bunker).filter(Bunker.hole_score_id == hole_score_id).delete() - - for p in req.putts: - db.add(Putt(hole_score_id=hole_score_id, location=p.get("location", "green"))) - for pen in req.penalties: - db.add(Penalty(hole_score_id=hole_score_id, type=pen.get("type", "water"))) - for b in req.bunkers: - db.add(Bunker(hole_score_id=hole_score_id, location=b.get("location", "greenside"))) - - putts = db.query(Putt).filter(Putt.hole_score_id == hole_score_id).all() - putts_count = len(putts) round_obj = db.query(Round).filter(Round.id == hs.round_id).first() hole = db.query(Hole).filter(Hole.course_id == round_obj.course_id, Hole.number == hs.hole_number).first() if hole: - all_green = all(p.location == "green" for p in putts) if putts else True - hs.gir = (hs.strokes - putts_count) <= (hole.par - 2) and all_green + total_putts = hs.putts_green + hs.putts_off_green + all_green = hs.putts_off_green == 0 + hs.gir = (not hs.unfinished) and (hs.strokes - total_putts) <= (hole.par - 2) and all_green db.commit() db.refresh(hs) db.close() - return {"hole_score_id": hs.id, "gir": hs.gir, "strokes": hs.strokes, "putts": putts_count} + return { + "hole_score_id": hs.id, + "gir": hs.gir, + "strokes": hs.strokes, + "putts_green": hs.putts_green, + "putts_off_green": hs.putts_off_green, + "unfinished": hs.unfinished + } @app.get("/stats/{round_id}") def get_stats(round_id: int): db = SessionLocal() hs_list = db.query(HoleScore).filter(HoleScore.round_id == round_id).all() - total_strokes = sum(hs.strokes for hs in hs_list if not hs.skipped) - total_putts = 0 - fairways = 0 - girs = 0 - for hs in hs_list: - if hs.skipped: - continue - putts = db.query(Putt).filter(Putt.hole_score_id == hs.id).all() - total_putts += len(putts) - if hs.fairway_hit: - fairways += 1 - if hs.gir: - girs += 1 + total_strokes = sum(hs.strokes for hs in hs_list if not hs.skipped and not hs.unfinished) + total_putts = sum((hs.putts_green + hs.putts_off_green) for hs in hs_list if not hs.skipped and not hs.unfinished) + fairways = sum(1 for hs in hs_list if hs.fairway_hit and not hs.skipped and not hs.unfinished) + girs = sum(1 for hs in hs_list if hs.gir and not hs.skipped and not hs.unfinished) + penalties = sum( + (1 if hs.penalty_water else 0) + (1 if hs.penalty_ob else 0) + (1 if hs.penalty_lost else 0) + for hs in hs_list if not hs.skipped and not hs.unfinished + ) + bunker_greenside = sum(1 for hs in hs_list if hs.bunker_greenside and not hs.skipped and not hs.unfinished) + bunker_fairway = sum(1 for hs in hs_list if hs.bunker_fairway and not hs.skipped and not hs.unfinished) db.close() return { "total_strokes": total_strokes, "total_putts": total_putts, "fairways_hit": fairways, "gir": girs, - "holes_played": len([h for h in hs_list if not h.skipped]) + "penalty_strokes": penalties, + "bunker_greenside": bunker_greenside, + "bunker_fairway": bunker_fairway, + "holes_played": len([h for h in hs_list if not h.skipped and not h.unfinished]) } app.mount("/", StaticFiles(directory="frontend", html=True), name="static") diff --git a/frontend/app.js b/frontend/app.js index 027fba6..1bc1cd9 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -52,20 +52,27 @@ async function loadRound() { function renderHole(hole) { document.getElementById('hole-score-id').value = hole.hole_score_id; document.getElementById('hole-label').textContent = `Hole ${hole.hole_number}`; - document.getElementById('strokes').value = hole.strokes || 0; - document.getElementById('fairway').checked = hole.fairway_hit; - document.getElementById('skipped').checked = hole.skipped; - document.getElementById('completed').checked = hole.completed; + document.getElementById('strokes').value = hole.strokes || 1; + document.getElementById('unfinished').checked = hole.unfinished || false; + document.getElementById('skipped').checked = hole.skipped || false; + document.getElementById('completed').checked = hole.completed || false; + document.getElementById('fairway').checked = hole.fairway_hit || false; document.getElementById('gir-display').textContent = hole.gir ? 'Yes' : 'No'; - const putts = hole.putts || []; - document.getElementById('putts-input').value = putts.map(p => p.location).join(','); + document.getElementById('putts-green').value = hole.putts_green || 0; + document.getElementById('putts-off-green').value = hole.putts_off_green || 0; - const pens = (hole.penalties || []).map(p => p.type).join(','); - document.getElementById('penalties-input').value = pens; + document.getElementById('pen-water').checked = hole.penalty_water || false; + document.getElementById('pen-ob').checked = hole.penalty_ob || false; + document.getElementById('pen-lost').checked = hole.penalty_lost || false; - const buns = (hole.bunkers || []).map(b => b.location).join(','); - document.getElementById('bunkers-input').value = buns; + document.getElementById('bunker-greenside').checked = hole.bunker_greenside || false; + document.getElementById('bunker-fairway').checked = hole.bunker_fairway || false; + + const info = hole.hole_info || {}; + document.getElementById('hole-info').innerHTML = ` + Par ${info.par} | Tee: ${info.tee} | ${info.yardage_meters}m + `; } function renderNav(holes) { @@ -93,26 +100,30 @@ function nextHole() { async function saveHole() { const id = document.getElementById('hole-score-id').value; - const strokes = parseInt(document.getElementById('strokes').value) || 0; - const fairway = document.getElementById('fairway').checked; + const strokes = parseInt(document.getElementById('strokes').value) || 1; + const unfinished = document.getElementById('unfinished').checked; const skipped = document.getElementById('skipped').checked; const completed = document.getElementById('completed').checked; + const fairway = document.getElementById('fairway').checked; - const puttsStr = document.getElementById('putts-input').value; - const putts = puttsStr.split(',').filter(s => s.trim()).map(s => ({location: s.trim()})); + const puttsGreen = parseInt(document.getElementById('putts-green').value) || 0; + const puttsOffGreen = parseInt(document.getElementById('putts-off-green').value) || 0; - const pensStr = document.getElementById('penalties-input').value; - const penalties = pensStr.split(',').filter(s => s.trim()).map(s => ({type: s.trim()})); + const penWater = document.getElementById('pen-water').checked; + const penOb = document.getElementById('pen-ob').checked; + const penLost = document.getElementById('pen-lost').checked; - const bunsStr = document.getElementById('bunkers-input').value; - const bunkers = bunsStr.split(',').filter(s => s.trim()).map(s => ({location: s.trim()})); + const bunkerGreenside = document.getElementById('bunker-greenside').checked; + const bunkerFairway = document.getElementById('bunker-fairway').checked; const res = await fetch(`/hole/${id}`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ - strokes, skipped, completed, fairway_hit: fairway, - putts, penalties, bunkers + strokes, skipped, completed, fairway_hit: fairway, unfinished, + putts_green: puttsGreen, putts_off_green: puttsOffGreen, + penalty_water: penWater, penalty_ob: penOb, penalty_lost: penLost, + bunker_greenside: bunkerGreenside, bunker_fairway: bunkerFairway }) }); const result = await res.json(); @@ -128,6 +139,9 @@ async function loadStats() {
Total Putts: ${stats.total_putts}
Fairways Hit: ${stats.fairways_hit}
GIR: ${stats.gir}
+Penalty Strokes: ${stats.penalty_strokes}
+Bunker Greenside: ${stats.bunker_greenside}
+Bunker Fairway: ${stats.bunker_fairway}
Holes Played: ${stats.holes_played}
`; } diff --git a/frontend/index.html b/frontend/index.html index b3f12b8..bc9d713 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -11,6 +11,7 @@ label { display: inline-block; margin: 0.25rem 0; } input, select { padding: 0.25rem; } button { padding: 0.4rem 0.8rem; margin-right: 0.5rem; cursor: pointer; } #stats-display p { margin: 0.2rem 0; } +#hole-info { font-size: 0.95rem; color: #555; margin-bottom: 0.5rem; } @@ -36,15 +37,21 @@ button { padding: 0.4rem 0.8rem; margin-right: 0.5rem; cursor: pointer; } Hole 1 +GIR: No