From 7cb41d91f27ba289e72b3e9609e10c81373ae69e Mon Sep 17 00:00:00 2001 From: Deeman Date: Mon, 16 Feb 2026 18:06:03 +0100 Subject: [PATCH] fix planner toggle active state, improve space defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Toggle buttons (Indoor/Outdoor, Rent/Buy) now visually update their active state on click - Space requirement sliders start from minimum court size (200m² double, 120m² single) instead of 0 - Defaults updated to court + 2m buffer (336/240/312/216 m²) - Reference dimensions panel shows standard court sizes - Chart.js font updated from JetBrains Mono to Commit Mono Co-Authored-By: Claude Opus 4.6 --- .../src/padelnomics/planner/calculator.py | 8 +++---- .../src/padelnomics/static/css/planner.css | 24 +++++++++++++++++++ .../src/padelnomics/static/js/planner.js | 23 +++++++++++++----- padelnomics/tests/test_calculator.py | 8 +++---- 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/padelnomics/src/padelnomics/planner/calculator.py b/padelnomics/src/padelnomics/planner/calculator.py index e467ba1..44d2922 100644 --- a/padelnomics/src/padelnomics/planner/calculator.py +++ b/padelnomics/src/padelnomics/planner/calculator.py @@ -19,10 +19,10 @@ DEFAULTS = { "own": "rent", "dblCourts": 4, "sglCourts": 2, - "sqmPerDblHall": 330, - "sqmPerSglHall": 220, - "sqmPerDblOutdoor": 300, - "sqmPerSglOutdoor": 200, + "sqmPerDblHall": 336, + "sqmPerSglHall": 240, + "sqmPerDblOutdoor": 312, + "sqmPerSglOutdoor": 216, "ratePeak": 50, "rateOffPeak": 35, "rateSingle": 30, diff --git a/padelnomics/src/padelnomics/static/css/planner.css b/padelnomics/src/padelnomics/static/css/planner.css index 6d6a521..14494ba 100644 --- a/padelnomics/src/padelnomics/static/css/planner.css +++ b/padelnomics/src/padelnomics/static/css/planner.css @@ -279,6 +279,30 @@ -moz-appearance: textfield; } +/* ── Space reference facts ── */ +.space-facts { + margin-top: 10px; + padding: 10px 12px; + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: 6px; + font-size: 11px; + color: var(--txt-3); +} +.space-facts__title { + font-weight: 600; + color: var(--txt-2); + margin-bottom: 6px; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.04em; +} +.space-facts__item { + display: flex; + justify-content: space-between; + padding: 2px 0; +} + /* ── Toggle buttons ── */ .toggle-group { display: flex; diff --git a/padelnomics/src/padelnomics/static/js/planner.js b/padelnomics/src/padelnomics/static/js/planner.js index c73cce3..af27f61 100644 --- a/padelnomics/src/padelnomics/static/js/planner.js +++ b/padelnomics/src/padelnomics/static/js/planner.js @@ -2,7 +2,7 @@ const S = { venue:'indoor', own:'rent', dblCourts:4, sglCourts:2, - sqmPerDblHall:330, sqmPerSglHall:220, sqmPerDblOutdoor:300, sqmPerSglOutdoor:200, + sqmPerDblHall:336, sqmPerSglHall:240, sqmPerDblOutdoor:312, sqmPerSglOutdoor:216, ratePeak:50, rateOffPeak:35, rateSingle:30, peakPct:40, hoursPerDay:16, daysPerMonthIndoor:29, daysPerMonthOutdoor:25, bookingFee:10, utilTarget:40, @@ -173,12 +173,19 @@ function rebuildSpaceInputs(){ const isIn = S.venue==='indoor'; let h = ''; if(isIn){ - h += slider('sqmPerDblHall','Hall m\u00B2 per Double Court',0,600,10,fN,'Total hall space needed per double court. Includes court (200m\u00B2), safety zones, circulation, and minimum clearances. Standard: 300\u2013350m\u00B2.')+ - slider('sqmPerSglHall','Hall m\u00B2 per Single Court',0,400,10,fN,'Total hall space needed per single court. Includes court (120m\u00B2), safety zones, and access. Standard: 200\u2013250m\u00B2.'); + h += slider('sqmPerDblHall','Hall m\u00B2 per Double Court',200,600,10,fN,'Total hall space needed per double court. Includes court (200m\u00B2), safety zones, circulation, and minimum clearances. Standard: 300\u2013350m\u00B2.')+ + slider('sqmPerSglHall','Hall m\u00B2 per Single Court',120,400,10,fN,'Total hall space needed per single court. Includes court (120m\u00B2), safety zones, and access. Standard: 200\u2013250m\u00B2.'); } else { - h += slider('sqmPerDblOutdoor','Land m\u00B2 per Double Court',0,500,10,fN,'Outdoor land area per double court. Includes court area, drainage slopes, access paths, and buffer zones. Standard: 280\u2013320m\u00B2.')+ - slider('sqmPerSglOutdoor','Land m\u00B2 per Single Court',0,350,10,fN,'Outdoor land area per single court. Includes court, surrounding space, and access paths. Standard: 180\u2013220m\u00B2.'); + h += slider('sqmPerDblOutdoor','Land m\u00B2 per Double Court',200,500,10,fN,'Outdoor land area per double court. Includes court area, drainage slopes, access paths, and buffer zones. Standard: 280\u2013320m\u00B2.')+ + slider('sqmPerSglOutdoor','Land m\u00B2 per Single Court',120,350,10,fN,'Outdoor land area per single court. Includes court, surrounding space, and access paths. Standard: 180\u2013220m\u00B2.'); } + h += '
'; + h += '
Reference dimensions
'; + h += '
Double court playing area20\u00D710m = 200 m\u00B2
'; + h += '
Single court playing area20\u00D76m = 120 m\u00B2
'; + h += '
+ 2m buffer all around24\u00D714m = 336 m\u00B2 / 24\u00D710m = 240 m\u00B2
'; + h += '
Min. ceiling height (indoor)8\u201310m clear
'; + h += '
'; $('#inp-space').innerHTML = h; } @@ -237,6 +244,10 @@ function buildToggle(id,opts,key){ el.innerHTML = opts.map(o=>``).join(''); el.querySelectorAll('button').forEach(b=>b.onclick=()=>{ S[key]=b.dataset.val; + // Update active state on all buttons in this toggle group + el.querySelectorAll('button').forEach(btn=>{ + btn.classList.toggle('toggle-btn--active', btn.dataset.val===S[key]); + }); rebuildSpaceInputs(); rebuildCapexInputs(); rebuildOpexInputs(); bindSliders(); render(); }); } @@ -535,7 +546,7 @@ function renderChart(canvasId,type,data,opts={}){ } else { defaults.scales = { x:{ticks:{color:'#94A3B8',font:{size:9,family:'Inter'}},grid:{display:false},border:{color:'#E2E8F0'}}, - y:{ticks:{color:'#94A3B8',font:{size:9,family:'JetBrains Mono'}},grid:{color:'rgba(0,0,0,0.04)'},border:{color:'#E2E8F0'}}, + y:{ticks:{color:'#94A3B8',font:{size:9,family:'Commit Mono'}},grid:{color:'rgba(0,0,0,0.04)'},border:{color:'#E2E8F0'}}, }; } charts[canvasId] = new Chart(ctx,{type,data,options:deepMerge(defaults,opts)}); diff --git a/padelnomics/tests/test_calculator.py b/padelnomics/tests/test_calculator.py index 9ec9e85..a313997 100644 --- a/padelnomics/tests/test_calculator.py +++ b/padelnomics/tests/test_calculator.py @@ -179,7 +179,7 @@ class TestCalcDefaultScenario: def test_sqm_is_hall(self, d): # Indoor venue → sqm is hallSqm - expected = 4 * 330 + 2 * 220 + 200 + 6 * 20 + expected = 4 * 336 + 2 * 240 + 200 + 6 * 20 assert d["hallSqm"] == expected assert d["sqm"] == expected @@ -377,7 +377,7 @@ class TestCalcOutdoorRent: return calc(default_state(venue="outdoor", own="rent")) def test_sqm_is_outdoor_land(self, d): - expected = 4 * 300 + 2 * 200 + 100 + expected = 4 * 312 + 2 * 216 + 100 assert d["outdoorLandSqm"] == expected assert d["sqm"] == expected @@ -764,8 +764,8 @@ class TestCalcRegression: assert d["totalCourts"] == 6 def test_hall_sqm(self, d): - # 4*330 + 2*220 + 200 + 6*20 = 2080 - assert d["hallSqm"] == 2080 + # 4*336 + 2*240 + 200 + 6*20 = 2144 + assert d["hallSqm"] == 2144 def test_opex_value(self, d): # Rent + Insurance + Electricity + Heating + Water + Maintenance +