fix planner toggle active state, improve space defaults
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 += '<div class="space-facts">';
|
||||
h += '<div class="space-facts__title">Reference dimensions</div>';
|
||||
h += '<div class="space-facts__item"><span>Double court playing area</span><span class="mono">20\u00D710m = 200 m\u00B2</span></div>';
|
||||
h += '<div class="space-facts__item"><span>Single court playing area</span><span class="mono">20\u00D76m = 120 m\u00B2</span></div>';
|
||||
h += '<div class="space-facts__item"><span>+ 2m buffer all around</span><span class="mono">24\u00D714m = 336 m\u00B2 / 24\u00D710m = 240 m\u00B2</span></div>';
|
||||
h += '<div class="space-facts__item"><span>Min. ceiling height (indoor)</span><span class="mono">8\u201310m clear</span></div>';
|
||||
h += '</div>';
|
||||
$('#inp-space').innerHTML = h;
|
||||
}
|
||||
|
||||
@@ -237,6 +244,10 @@ function buildToggle(id,opts,key){
|
||||
el.innerHTML = opts.map(o=>`<button data-val="${o.v}" class="toggle-btn ${S[key]===o.v?'toggle-btn--active':''}">${o.l}</button>`).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)});
|
||||
|
||||
@@ -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 +
|
||||
|
||||
Reference in New Issue
Block a user