Fix New plan feeling like a no-op; add plan delete and why-panel collapse
Test and deploy / test-and-deploy (push) Successful in 1m34s

New plan reset the form correctly but left the Plans drawer open over
it, so the reset was invisible until the user closed the drawer
themselves. Now closes it immediately, matching selectPlan().

Also adds a delete button per plan in the Plans drawer (server route
already existed) - deleting the currently open plan resets to a fresh
blank one, matching New plan.

Why-panel teaching callouts ("WHY CULTIVAR SETS THE CLOCK" etc.) now
default collapsed instead of wall-to-wall expanded on first load, with
an "Expand all" button in the header on both the planner and cupping
pages. Persistence flips from tracking collapsed-vs-default-open to
expanded-vs-default-closed to match.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-07-31 15:34:56 -04:00
co-authored by Claude Sonnet 5
parent 2237c199c1
commit 84ce75dc14
5 changed files with 100 additions and 31 deletions
+42 -6
View File
@@ -16,10 +16,11 @@ import { initPrefillPanel } from "./prefill-ui.js?v=__ASSET_VERSION__";
import { initAlogPanel } from "./alog-ui.js?v=__ASSET_VERSION__";
import { initPrint } from "./print.js?v=__ASSET_VERSION__";
import { initLotPicker } from "./lot-picker.js?v=__ASSET_VERSION__";
import { protectedFetch, csrfToken } from "./api.js?v=__ASSET_VERSION__";
import { api, protectedFetch, csrfToken } from "./api.js?v=__ASSET_VERSION__";
import { initSideNav } from "./nav.js?v=__ASSET_VERSION__";
import { wireWhyPanels } from "./why-panels.js?v=__ASSET_VERSION__";
import { initFieldHelp } from "./field-help.js?v=__ASSET_VERSION__";
import { showToast } from "./toast.js?v=__ASSET_VERSION__";
const FIELD_ID_SET = new Set(FIELD_IDS);
const STORAGE_PREFIX = "roastPlannerPlan.v2";
@@ -823,6 +824,7 @@ async function loadPlans() {
list.replaceChildren(
...plans.map((plan) => {
const item = document.createElement("li");
item.className = "plan-list-row";
const button = document.createElement("button");
button.type = "button";
button.className = "plan-list-item";
@@ -833,7 +835,20 @@ async function loadPlans() {
updated.textContent = `Updated ${new Date(plan.updated_at).toLocaleDateString()}`;
button.append(title, updated);
button.addEventListener("click", () => selectPlan(plan));
item.append(button);
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "icon-btn plan-list-delete";
deleteBtn.setAttribute("aria-label", `Delete ${title.textContent}`);
deleteBtn.textContent = "✕";
// A sibling of the select button, not nested inside it — stopPropagation alone
// wouldn't be enough to keep a click here from also selecting the plan otherwise.
deleteBtn.addEventListener("click", (event) => {
event.stopPropagation();
deletePlan(plan);
});
item.append(button, deleteBtn);
return item;
}),
);
@@ -860,10 +875,9 @@ async function selectPlan(plan) {
lotPicker?.refreshRefineSuggestion();
document.querySelector("[data-close-drawer]")?.click();
}
async function newPlan() {
if (!confirm("Start a new plan? Your current plan is already saved locally."))
return;
if (!(await flushCurrentPlan())) return;
// Shared by newPlan() and deletePlan() (when the deleted plan was the one on screen) — resets
// the whole app to a fresh, unsynced plan.
function resetToBlankPlan() {
remotePlanId = null;
draftSyncedAt = null;
state.plan = blankPlan();
@@ -875,6 +889,28 @@ async function newPlan() {
lotPicker?.renderOptions();
lotPicker?.refreshRefineSuggestion();
}
async function newPlan() {
if (!confirm("Start a new plan? Your current plan is already saved locally."))
return;
if (!(await flushCurrentPlan())) return;
resetToBlankPlan();
// Without this, the Plans drawer stays open over the now-blank form — the reset happens but
// is invisible, so it doesn't read as "starting something new" until the user closes it themselves.
document.querySelector("[data-close-drawer]")?.click();
}
async function deletePlan(plan) {
const name = plan.plan?.fields?.["0.1"] || "Untitled plan";
if (!confirm(`Delete "${name}"? This cannot be undone.`)) return;
try {
await api(`/api/plans/${plan.id}`, { method: "DELETE" });
} catch (error) {
showToast(error.message, "fail");
return;
}
showToast("Plan deleted.");
if (plan.id === remotePlanId) resetToBlankPlan();
await loadPlans();
}
function wireToolbar() {
document.getElementById("btn-toggle-fids").addEventListener("click", (e) => {
+31 -14
View File
@@ -1,25 +1,42 @@
// Shared teaching-layer collapse memory for every page that uses `.why-panel` (`<details>`
// with a `data-why` id): the planner and the cupping session view. One localStorage key across
// both so a user's dismissals carry over between pages.
const WHY_COLLAPSED_KEY = "roastPlannerWhyCollapsed.v1";
// both so a user's choices carry over between pages.
//
// Panels are collapsed by default (see index.html/cupping.html) so the form isn't wall-to-wall
// explanatory text on first load; this tracks which ones a user has explicitly opened, rather
// than which they've closed, so it stays meaningful against a closed-by-default baseline.
const WHY_EXPANDED_KEY = "roastPlannerWhyExpanded.v1";
export function wireWhyPanels() {
let collapsed;
let expanded;
try {
collapsed = new Set(JSON.parse(localStorage.getItem(WHY_COLLAPSED_KEY)) || []);
expanded = new Set(JSON.parse(localStorage.getItem(WHY_EXPANDED_KEY)) || []);
} catch {
collapsed = new Set();
expanded = new Set();
}
for (const panel of document.querySelectorAll(".why-panel")) {
if (collapsed.has(panel.dataset.why)) panel.open = false;
const persist = () => {
try {
localStorage.setItem(WHY_EXPANDED_KEY, JSON.stringify([...expanded]));
} catch {
/* unavailable storage */
}
};
const panels = [...document.querySelectorAll(".why-panel")];
for (const panel of panels) {
if (expanded.has(panel.dataset.why)) panel.open = true;
panel.addEventListener("toggle", () => {
if (panel.open) collapsed.delete(panel.dataset.why);
else collapsed.add(panel.dataset.why);
try {
localStorage.setItem(WHY_COLLAPSED_KEY, JSON.stringify([...collapsed]));
} catch {
/* unavailable storage */
}
if (panel.open) expanded.add(panel.dataset.why);
else expanded.delete(panel.dataset.why);
persist();
});
}
document.getElementById("btn-expand-why")?.addEventListener("click", () => {
for (const panel of panels) {
panel.open = true;
expanded.add(panel.dataset.why);
}
persist();
});
}