Add a °C/°F display toggle for Machine Plan and Roast Log temps
Plans are always stored in canonical °C; the unit is a client-side display preference (localStorage) that converts Machine Plan planned temps, Roast Log actual temps, and actuator expected BT on the fly, including band-marker positioning. Toggle lives in Settings & sync.
This commit is contained in:
+25
-1
@@ -310,6 +310,30 @@
|
||||
saved locally first, then synced to your account whenever you are
|
||||
online.
|
||||
</p>
|
||||
<div class="field">
|
||||
<span class="field-label">Temperature unit</span>
|
||||
<div
|
||||
class="segmented small"
|
||||
id="temp-unit-toggle"
|
||||
role="radiogroup"
|
||||
aria-label="Temperature unit"
|
||||
>
|
||||
<label class="seg-opt"
|
||||
><input type="radio" name="tempUnit" value="C" checked /><span
|
||||
>°C</span
|
||||
></label
|
||||
>
|
||||
<label class="seg-opt"
|
||||
><input type="radio" name="tempUnit" value="F" /><span
|
||||
>°F</span
|
||||
></label
|
||||
>
|
||||
</div>
|
||||
<p class="field-note">
|
||||
Changes how Machine Plan and Roast Log temperatures are shown.
|
||||
Plans are always stored in °C, so switching back is lossless.
|
||||
</p>
|
||||
</div>
|
||||
<p class="drawer-result">
|
||||
Install Roast Planner from your browser menu for a focused workspace.
|
||||
Updates wait for your confirmation so an in-progress plan is never
|
||||
@@ -1979,7 +2003,7 @@
|
||||
<tr>
|
||||
<th style="width: 28mm">Milestone</th>
|
||||
<th class="num" style="width: 20mm">Time</th>
|
||||
<th class="num" style="width: 18mm">°C</th>
|
||||
<th class="num" style="width: 18mm" id="print-machine-temp-th">°C</th>
|
||||
<th class="num" style="width: 36mm">Your logged band</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
+66
-3
@@ -26,6 +26,30 @@ let deferredInstallPrompt = null;
|
||||
let lotPicker = null;
|
||||
const BAND_DOMAIN = [60, 220]; // shared °C domain for every band-track in the Machine Plan section
|
||||
|
||||
// Temperature fields are always stored in state.plan as canonical °C — this is purely a
|
||||
// display-layer preference. tempC/actualBt/expectedBt cover Machine Plan, Roast Log, and
|
||||
// the actuator schedule respectively; every other numeric field (%, g, days) is untouched.
|
||||
const TEMP_UNIT_KEY = "roastPlannerTempUnit.v1";
|
||||
const TEMP_FIELD_RE = /\.(tempC|actualBt|expectedBt)$/;
|
||||
let tempUnit = localStorage.getItem(TEMP_UNIT_KEY) === "F" ? "F" : "C";
|
||||
const cToF = (c) => (c * 9) / 5 + 32;
|
||||
const fToC = (f) => ((f - 32) * 5) / 9;
|
||||
function isTempField(name) {
|
||||
return TEMP_FIELD_RE.test(name);
|
||||
}
|
||||
// Canonical °C string -> what the field should display right now.
|
||||
function tempToDisplay(canonical) {
|
||||
const n = Number.parseFloat(canonical);
|
||||
if (!Number.isFinite(n)) return canonical ?? "";
|
||||
return String(Math.round(tempUnit === "F" ? cToF(n) : n));
|
||||
}
|
||||
// What the user typed (in the current display unit) -> canonical °C string.
|
||||
function tempToCanonical(display) {
|
||||
const n = Number.parseFloat(display);
|
||||
if (!Number.isFinite(n)) return display;
|
||||
return String(Math.round((tempUnit === "F" ? fToC(n) : n) * 10) / 10);
|
||||
}
|
||||
|
||||
export const state = { plan: blankPlan() };
|
||||
|
||||
const form = document.getElementById("plan-form");
|
||||
@@ -201,6 +225,7 @@ function renderActuatorTimeline() {
|
||||
function renderActuators() {
|
||||
renderActuatorPrintRows();
|
||||
renderActuatorTimeline();
|
||||
updateTempUnitUI(); // new actuator rows need their expectedBt placeholder set too
|
||||
}
|
||||
|
||||
// ---- populate every control in the form from state.plan
|
||||
@@ -214,7 +239,7 @@ export function renderFormFromPlan() {
|
||||
} else if (el.type === "checkbox") {
|
||||
el.checked = Boolean(value);
|
||||
} else {
|
||||
el.value = value ?? "";
|
||||
el.value = isTempField(name) ? tempToDisplay(value) : (value ?? "");
|
||||
}
|
||||
}
|
||||
updateBlendVisibility(state.plan.fields["2.1"]);
|
||||
@@ -310,7 +335,8 @@ function renderBandMarkers() {
|
||||
const input = row.querySelector('input[name$=".tempC"]');
|
||||
const marker = row.querySelector(".band-marker");
|
||||
if (!input || !marker) continue;
|
||||
const v = Number.parseFloat(input.value);
|
||||
const raw = Number.parseFloat(input.value);
|
||||
const v = Number.isFinite(raw) ? (tempUnit === "F" ? fToC(raw) : raw) : NaN;
|
||||
if (!Number.isFinite(v)) {
|
||||
marker.style.display = "none";
|
||||
continue;
|
||||
@@ -587,7 +613,10 @@ function wireForm() {
|
||||
const el = e.target;
|
||||
if (!el.name) return;
|
||||
if (el.type === "radio" && !el.checked) return;
|
||||
setValueForName(el.name, el.value);
|
||||
setValueForName(
|
||||
el.name,
|
||||
isTempField(el.name) ? tempToCanonical(el.value) : el.value,
|
||||
);
|
||||
if (el.name === "1.1") cultivarAutofill(el.value);
|
||||
if (el.name === "2.1") updateBlendVisibility(el.value);
|
||||
if (el.name.startsWith("blendComponents.")) updateBlendTotal();
|
||||
@@ -595,6 +624,38 @@ function wireForm() {
|
||||
});
|
||||
}
|
||||
|
||||
// Refreshes every unit-facing bit of chrome (labels, placeholders, the toggle itself) after
|
||||
// tempUnit changes. Field *values* are handled separately by renderFormFromPlan().
|
||||
function updateTempUnitUI() {
|
||||
const label = tempUnit === "F" ? "°F" : "°C";
|
||||
for (const input of document.querySelectorAll(
|
||||
'input[name$=".tempC"], input[name$=".actualBt"], input[name$=".expectedBt"]',
|
||||
))
|
||||
input.placeholder = label;
|
||||
for (const span of document.querySelectorAll("#sec-machine .unit-input .unit"))
|
||||
span.textContent = label;
|
||||
const printHeader = document.getElementById("print-machine-temp-th");
|
||||
if (printHeader) printHeader.textContent = label;
|
||||
for (const input of document.querySelectorAll('#temp-unit-toggle input[name="tempUnit"]'))
|
||||
input.checked = input.value === tempUnit;
|
||||
}
|
||||
|
||||
function setTempUnit(unit) {
|
||||
tempUnit = unit === "F" ? "F" : "C";
|
||||
localStorage.setItem(TEMP_UNIT_KEY, tempUnit);
|
||||
updateTempUnitUI();
|
||||
renderFormFromPlan();
|
||||
recompute();
|
||||
}
|
||||
|
||||
function wireTempUnitToggle() {
|
||||
const toggle = document.getElementById("temp-unit-toggle");
|
||||
if (!toggle) return;
|
||||
toggle.addEventListener("change", (e) => {
|
||||
if (e.target.name === "tempUnit") setTempUnit(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
function wireDrawers() {
|
||||
const overlay = document.getElementById("drawer-overlay");
|
||||
const drawers = [...document.querySelectorAll(".drawer")];
|
||||
@@ -941,6 +1002,8 @@ async function init() {
|
||||
initSideNav();
|
||||
wireDrawers();
|
||||
wireToolbar();
|
||||
wireTempUnitToggle();
|
||||
updateTempUnitUI();
|
||||
wirePwa();
|
||||
wireSectionNav();
|
||||
initPrefillPanel({
|
||||
|
||||
Reference in New Issue
Block a user