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
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:
+11
-1
@@ -1706,6 +1706,11 @@ body.show-fids .field[data-fid]::after {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
.plan-list-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 6px;
|
||||
}
|
||||
.plan-list-item {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
@@ -1719,6 +1724,10 @@ body.show-fids .field[data-fid]::after {
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.plan-list-delete {
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
}
|
||||
.plan-list-item:hover,
|
||||
.plan-list-item.active {
|
||||
border-color: var(--ember);
|
||||
@@ -2603,7 +2612,8 @@ body.show-fids .field[data-fid]::after {
|
||||
flex: 1 1 auto;
|
||||
justify-content: center;
|
||||
}
|
||||
.header-actions #btn-print {
|
||||
.header-actions #btn-print,
|
||||
.header-actions #btn-expand-why {
|
||||
flex: 0 0 auto;
|
||||
min-height: 44px;
|
||||
padding: 8px 16px;
|
||||
|
||||
+6
-3
@@ -88,6 +88,9 @@
|
||||
><span class="dot"></span
|
||||
><span class="autosave-text">Not saved yet</span></span
|
||||
>
|
||||
<button id="btn-expand-why" type="button" class="ghost-btn">
|
||||
Expand all "why" notes
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="drawer-overlay hidden" id="drawer-overlay"></div>
|
||||
@@ -160,7 +163,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<details class="why-panel" data-why="cup-scores" open>
|
||||
<details class="why-panel" data-why="cup-scores">
|
||||
<summary>Why the scale starts at 6</summary>
|
||||
<p>
|
||||
The 6–10 range is a specialty-grading convention:
|
||||
@@ -186,7 +189,7 @@
|
||||
<section class="panel-card" id="cup-flavors">
|
||||
<div class="panel-head"><h2>Flavors</h2></div>
|
||||
<div class="panel-body">
|
||||
<details class="why-panel" data-why="cup-flavors" open>
|
||||
<details class="why-panel" data-why="cup-flavors">
|
||||
<summary>Tag what you can point at</summary>
|
||||
<p>
|
||||
A tag is only useful if you could defend it to
|
||||
@@ -206,7 +209,7 @@
|
||||
<section class="panel-card" id="cup-notes">
|
||||
<div class="panel-head"><h2>Notes</h2></div>
|
||||
<div class="panel-body">
|
||||
<details class="why-panel" data-why="cup-notes" open>
|
||||
<details class="why-panel" data-why="cup-notes">
|
||||
<summary>Notes outlive scores</summary>
|
||||
<p>
|
||||
Numbers compare roasts; sentences explain them. Write
|
||||
|
||||
+10
-7
@@ -129,6 +129,9 @@
|
||||
><span class="dot"></span
|
||||
><span class="autosave-text">Not saved yet</span></span
|
||||
>
|
||||
<button id="btn-expand-why" type="button" class="ghost-btn">
|
||||
Expand all "why" notes
|
||||
</button>
|
||||
<button id="btn-print" type="button" class="primary-btn">
|
||||
Print
|
||||
</button>
|
||||
@@ -372,7 +375,7 @@
|
||||
<section class="panel-card" id="sec-coffee">
|
||||
<div class="panel-head"><h2>The Coffee</h2></div>
|
||||
<div class="panel-body">
|
||||
<details class="why-panel" data-why="sec-coffee" open>
|
||||
<details class="why-panel" data-why="sec-coffee">
|
||||
<summary>Why cultivar sets the clock</summary>
|
||||
<p>
|
||||
A roast plan is three durations laid end to end: drying +
|
||||
@@ -543,7 +546,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body" id="blend-body">
|
||||
<details class="why-panel" data-why="sec-blend" open>
|
||||
<details class="why-panel" data-why="sec-blend">
|
||||
<summary>Why blends combine times, never temperatures</summary>
|
||||
<p>
|
||||
The drum has one heat setting and one clock — you cannot
|
||||
@@ -615,7 +618,7 @@
|
||||
<section class="panel-card" id="sec-process">
|
||||
<div class="panel-head"><h2>Roast Target</h2></div>
|
||||
<div class="panel-body">
|
||||
<details class="why-panel" data-why="sec-process" open>
|
||||
<details class="why-panel" data-why="sec-process">
|
||||
<summary>Why processing moves development, not first crack</summary>
|
||||
<p>
|
||||
Processing changed the chemistry that browning starts
|
||||
@@ -743,7 +746,7 @@
|
||||
<section class="panel-card" id="sec-bean">
|
||||
<div class="panel-head"><h2>Bean Condition</h2></div>
|
||||
<div class="panel-body">
|
||||
<details class="why-panel" data-why="sec-bean" open>
|
||||
<details class="why-panel" data-why="sec-bean">
|
||||
<summary>Why bean condition is worth seconds, not a new plan</summary>
|
||||
<p>
|
||||
Moisture, density and screen size change how much energy the
|
||||
@@ -826,7 +829,7 @@
|
||||
<section class="panel-card" id="sec-machine">
|
||||
<div class="panel-head"><h2>Machine Plan</h2></div>
|
||||
<div class="panel-body">
|
||||
<details class="why-panel" data-why="sec-machine" open>
|
||||
<details class="why-panel" data-why="sec-machine">
|
||||
<summary>
|
||||
Why the rate of rise must fall, and never touch zero
|
||||
</summary>
|
||||
@@ -994,7 +997,7 @@
|
||||
<span class="phase-chip">during the roast</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<details class="why-panel" data-why="sec-roastlog" open>
|
||||
<details class="why-panel" data-why="sec-roastlog">
|
||||
<summary>Record what you observed, never what you planned</summary>
|
||||
<p>
|
||||
A planned milestone written into the log as though it
|
||||
@@ -1136,7 +1139,7 @@
|
||||
<span class="phase-chip">after cupping</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<details class="why-panel" data-why="sec-after" open>
|
||||
<details class="why-panel" data-why="sec-after">
|
||||
<summary>One change per batch — arithmetic, not patience</summary>
|
||||
<p>
|
||||
Move two variables and there are four possible
|
||||
|
||||
+42
-6
@@ -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
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user