import { FIELD_IDS, blankPlan } from "/shared/fields.js";
import { computeLedger } from "/shared/ledger.js";
import { formatDuration, formatSigned, parseRangeMidpoint } from "/shared/time.js";
import { CULTIVARS, findCultivar } from "/shared/reference-data.js";
import { buildPlanCurve, pointsToPathD, tToX, tempToY } from "/shared/curve.js";
import { initPrefillPanel } from "./prefill-ui.js";
import { initAlogPanel } from "./alog-ui.js";
import { initPrint } from "./print.js";
const FIELD_ID_SET = new Set(FIELD_IDS);
const STORAGE_KEY = "roastPlannerPlan.v1";
export const state = { plan: loadFromStorage() ?? blankPlan() };
const form = document.getElementById("plan-form");
// ---- nested path get/set for names like "temps.charge.tempC" or "blendComponents.0.cultivar"
function getPath(obj, path) {
return path.split(".").reduce((o, k) => (o == null ? undefined : o[k]), obj);
}
function setPath(obj, path, value) {
const parts = path.split(".");
let node = obj;
for (let i = 0; i < parts.length - 1; i++) {
if (node[parts[i]] === undefined || node[parts[i]] === null) node[parts[i]] = {};
node = node[parts[i]];
}
node[parts[parts.length - 1]] = value;
}
function valueForName(name) {
if (FIELD_ID_SET.has(name)) return state.plan.fields[name] ?? "";
const v = getPath(state.plan, name);
return v ?? "";
}
function setValueForName(name, value) {
if (FIELD_ID_SET.has(name)) {
state.plan.fields[name] = value;
} else {
setPath(state.plan, name, value);
}
}
// ---- dynamic rows: blend components + actuator schedule
function renderBlendRows() {
const tbody = document.getElementById("blend-rows");
tbody.innerHTML = "";
state.plan.blendComponents.forEach((_, i) => {
const tr = document.createElement("tr");
tr.innerHTML = `
|
|
|
|
|
`;
tbody.appendChild(tr);
});
}
function renderActuatorRows() {
const tbody = document.getElementById("actuator-rows");
tbody.innerHTML = "";
state.plan.actuators.forEach((_, i) => {
const tr = document.createElement("tr");
tr.innerHTML = `
|
|
|
|
|
`;
tbody.appendChild(tr);
});
}
// ---- populate every control in the form from state.plan
export function renderFormFromPlan() {
for (const el of form.querySelectorAll("input, select, textarea")) {
const name = el.name;
if (!name) continue;
const value = valueForName(name);
if (el.type === "radio") {
el.checked = el.value === value;
} else if (el.type === "checkbox") {
el.checked = Boolean(value);
} else {
el.value = value ?? "";
}
}
}
function fmtOut(id, text) {
const el = document.querySelector(`[data-out="${id}"]`);
if (el) el.textContent = text ?? "—";
}
function renderLedger() {
const ledger = computeLedger(state.plan);
const d = (s) => (s === null || s === undefined ? "—" : formatDuration(s));
const ds = (s) => (s === null || s === undefined ? "—" : formatSigned(s));
fmtOut("l1", d(ledger.lines.l1));
fmtOut("l2", ds(ledger.lines.l2));
fmtOut("l3", ds(ledger.lines.l3));
fmtOut("A", d(ledger.A));
fmtOut("yellow", d(ledger.yellow));
fmtOut("maillard", d(ledger.maillard));
fmtOut("l7", d(ledger.lines.l7));
fmtOut("l8", ds(ledger.lines.l8));
fmtOut("l9", ds(ledger.lines.l9));
fmtOut("C", d(ledger.C));
fmtOut("D", d(ledger.D));
const pct = (v) => (v === null || v === undefined ? "—" : `${v.toFixed(1)}%`);
fmtOut("check-drying", pct(ledger.checks.drying.pct));
fmtOut("check-maillard", pct(ledger.checks.maillard.pct));
fmtOut("check-dtr", pct(ledger.checks.dtr.pct));
fmtOut("check-ceiling", d(ledger.checks.ceiling.valueS));
for (const [key, check] of Object.entries(ledger.checks)) {
const cell = document.querySelector(`[data-pass="${key}"]`);
if (!cell) continue;
cell.classList.remove("pass", "fail", "unknown");
cell.classList.add(check.pass === null ? "unknown" : check.pass ? "pass" : "fail");
}
// Box 8 / box 11 derived time columns
fmtOut("t-charge", "0:00");
fmtOut("t-yellow", d(ledger.yellow));
fmtOut("t-fc", d(ledger.A));
fmtOut("t-drop", d(ledger.D));
fmtOut("pa-charge", "0:00");
fmtOut("pa-tp", state.plan.temps.tp.time || "—");
fmtOut("pa-yellow", d(ledger.yellow));
fmtOut("pa-fc", d(ledger.A));
fmtOut("pa-drop", d(ledger.D));
document.getElementById("back-coffee-name").textContent = state.plan.fields["0.1"] || "";
return ledger;
}
function renderCurve(ledger) {
const planPoints = buildPlanCurve(state.plan, ledger);
const planGroup = document.getElementById("plan-curve");
planGroup.innerHTML = "";
if (planPoints.length > 0) {
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", pointsToPathD(planPoints));
path.setAttribute("fill", "none");
path.setAttribute("stroke", "#1a1512");
path.setAttribute("stroke-width", "1.6");
planGroup.appendChild(path);
for (const p of planPoints) {
const c = document.createElementNS("http://www.w3.org/2000/svg", "circle");
c.setAttribute("cx", tToX(p.timeS).toFixed(1));
c.setAttribute("cy", tempToY(p.tempC).toFixed(1));
c.setAttribute("r", "3.2");
c.setAttribute("fill", "#1a1512");
planGroup.appendChild(c);
}
}
const ref = state.plan.reference;
const refGroup = document.getElementById("ref-curve");
refGroup.innerHTML = "";
if (ref?.curve?.length) {
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
const d = ref.curve
.map((p, i) => `${i === 0 ? "M" : "L"}${tToX(p.t).toFixed(1)},${tempToY(p.bt).toFixed(1)}`)
.join(" ");
path.setAttribute("d", d);
path.setAttribute("fill", "none");
path.setAttribute("stroke", "#5a5048");
path.setAttribute("stroke-width", "1.3");
path.setAttribute("stroke-dasharray", "4 3");
path.setAttribute("opacity", ".65");
refGroup.appendChild(path);
for (const m of ref.milestones ?? []) {
if (m.tempC === null) continue;
const c = document.createElementNS("http://www.w3.org/2000/svg", "circle");
c.setAttribute("cx", tToX(m.timeS).toFixed(1));
c.setAttribute("cy", tempToY(m.tempC).toFixed(1));
c.setAttribute("r", "2.6");
c.setAttribute("fill", "none");
c.setAttribute("stroke", "#5a5048");
c.setAttribute("stroke-width", "1.2");
refGroup.appendChild(c);
}
}
}
export function recompute() {
const ledger = renderLedger();
renderCurve(ledger);
autosave();
}
function autosave() {
clearTimeout(autosave._t);
autosave._t = setTimeout(() => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.plan));
document.getElementById("autosave-status").textContent = `saved ${new Date().toLocaleTimeString()}`;
} catch {
/* storage unavailable — non-fatal */
}
}, 400);
}
function loadFromStorage() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
}
function cultivarAutofill(name) {
const row = findCultivar(name);
if (!row) return;
setValueForName("1.2", row.group);
setValueForName("1.4", formatDuration(parseRangeMidpoint(row.fcAnchor)));
setValueForName("1.5", row.profile.join("|"));
setValueForName("1.7", formatSigned(row.devModS));
renderFormFromPlan();
}
function wireCultivarDatalist() {
const list = document.getElementById("cultivar-list");
list.innerHTML = CULTIVARS.map((c) => `