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",
|
"own": "rent",
|
||||||
"dblCourts": 4,
|
"dblCourts": 4,
|
||||||
"sglCourts": 2,
|
"sglCourts": 2,
|
||||||
"sqmPerDblHall": 330,
|
"sqmPerDblHall": 336,
|
||||||
"sqmPerSglHall": 220,
|
"sqmPerSglHall": 240,
|
||||||
"sqmPerDblOutdoor": 300,
|
"sqmPerDblOutdoor": 312,
|
||||||
"sqmPerSglOutdoor": 200,
|
"sqmPerSglOutdoor": 216,
|
||||||
"ratePeak": 50,
|
"ratePeak": 50,
|
||||||
"rateOffPeak": 35,
|
"rateOffPeak": 35,
|
||||||
"rateSingle": 30,
|
"rateSingle": 30,
|
||||||
|
|||||||
@@ -279,6 +279,30 @@
|
|||||||
-moz-appearance: textfield;
|
-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 buttons ── */
|
||||||
.toggle-group {
|
.toggle-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
const S = {
|
const S = {
|
||||||
venue:'indoor', own:'rent',
|
venue:'indoor', own:'rent',
|
||||||
dblCourts:4, sglCourts:2,
|
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,
|
ratePeak:50, rateOffPeak:35, rateSingle:30,
|
||||||
peakPct:40, hoursPerDay:16, daysPerMonthIndoor:29, daysPerMonthOutdoor:25,
|
peakPct:40, hoursPerDay:16, daysPerMonthIndoor:29, daysPerMonthOutdoor:25,
|
||||||
bookingFee:10, utilTarget:40,
|
bookingFee:10, utilTarget:40,
|
||||||
@@ -173,12 +173,19 @@ function rebuildSpaceInputs(){
|
|||||||
const isIn = S.venue==='indoor';
|
const isIn = S.venue==='indoor';
|
||||||
let h = '';
|
let h = '';
|
||||||
if(isIn){
|
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.')+
|
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',0,400,10,fN,'Total hall space needed per single court. Includes court (120m\u00B2), safety zones, and access. Standard: 200\u2013250m\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 {
|
} 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.')+
|
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',0,350,10,fN,'Outdoor land area per single court. Includes court, surrounding space, and access paths. Standard: 180\u2013220m\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;
|
$('#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.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=()=>{
|
el.querySelectorAll('button').forEach(b=>b.onclick=()=>{
|
||||||
S[key]=b.dataset.val;
|
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();
|
rebuildSpaceInputs(); rebuildCapexInputs(); rebuildOpexInputs(); bindSliders(); render();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -535,7 +546,7 @@ function renderChart(canvasId,type,data,opts={}){
|
|||||||
} else {
|
} else {
|
||||||
defaults.scales = {
|
defaults.scales = {
|
||||||
x:{ticks:{color:'#94A3B8',font:{size:9,family:'Inter'}},grid:{display:false},border:{color:'#E2E8F0'}},
|
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)});
|
charts[canvasId] = new Chart(ctx,{type,data,options:deepMerge(defaults,opts)});
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ class TestCalcDefaultScenario:
|
|||||||
|
|
||||||
def test_sqm_is_hall(self, d):
|
def test_sqm_is_hall(self, d):
|
||||||
# Indoor venue → sqm is hallSqm
|
# 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["hallSqm"] == expected
|
||||||
assert d["sqm"] == expected
|
assert d["sqm"] == expected
|
||||||
|
|
||||||
@@ -377,7 +377,7 @@ class TestCalcOutdoorRent:
|
|||||||
return calc(default_state(venue="outdoor", own="rent"))
|
return calc(default_state(venue="outdoor", own="rent"))
|
||||||
|
|
||||||
def test_sqm_is_outdoor_land(self, d):
|
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["outdoorLandSqm"] == expected
|
||||||
assert d["sqm"] == expected
|
assert d["sqm"] == expected
|
||||||
|
|
||||||
@@ -764,8 +764,8 @@ class TestCalcRegression:
|
|||||||
assert d["totalCourts"] == 6
|
assert d["totalCourts"] == 6
|
||||||
|
|
||||||
def test_hall_sqm(self, d):
|
def test_hall_sqm(self, d):
|
||||||
# 4*330 + 2*220 + 200 + 6*20 = 2080
|
# 4*336 + 2*240 + 200 + 6*20 = 2144
|
||||||
assert d["hallSqm"] == 2080
|
assert d["hallSqm"] == 2144
|
||||||
|
|
||||||
def test_opex_value(self, d):
|
def test_opex_value(self, d):
|
||||||
# Rent + Insurance + Electricity + Heating + Water + Maintenance +
|
# Rent + Insurance + Electricity + Heating + Water + Maintenance +
|
||||||
|
|||||||
Reference in New Issue
Block a user