Add green-bean inventory and cupping features
Ports inventory management and SCA-style cupping scoring from hope_roaster: lot tracking with audit-logged consumption, cupping sessions with server-authoritative scoring and a live radar chart, and links from the planner (draw-from-lot, open-cupping-session). Also fixes the Blend/Single-origin toggle layout, replaces tooltips with an in-context "why" teaching layer, and stages the planner UI into pre-roast vs. post-roast phases.
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Cupping — Roast Planner</title>
|
||||
<link rel="stylesheet" href="/app.css" />
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
|
||||
<meta name="theme-color" content="#A8481A" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
<nav class="side-nav" id="side-nav" aria-label="Main navigation">
|
||||
<div class="side-nav-brand">
|
||||
<span class="brand-mark" aria-hidden="true">◐</span>
|
||||
<span class="brand-name">Roast Planner</span>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<a class="nav-item" href="/app"
|
||||
><span class="nav-icon" aria-hidden="true">◐</span
|
||||
><span class="nav-label">Planner</span></a
|
||||
>
|
||||
<a class="nav-item" href="/account#your-plans"
|
||||
><span class="nav-icon" aria-hidden="true">▤</span
|
||||
><span class="nav-label">Plans</span></a
|
||||
>
|
||||
<a class="nav-item" href="/inventory"
|
||||
><span class="nav-icon" aria-hidden="true">▥</span
|
||||
><span class="nav-label">Inventory</span></a
|
||||
>
|
||||
<a class="nav-item" href="/cupping" aria-current="page"
|
||||
><span class="nav-icon" aria-hidden="true">◒</span
|
||||
><span class="nav-label">Cupping</span></a
|
||||
>
|
||||
<a class="nav-item" href="/account"
|
||||
><span class="nav-icon" aria-hidden="true">◔</span
|
||||
><span class="nav-label">Account</span></a
|
||||
>
|
||||
<a class="nav-item hidden" id="nav-admin" href="/admin"
|
||||
><span class="nav-icon" aria-hidden="true">⚙</span
|
||||
><span class="nav-label">Admin</span></a
|
||||
>
|
||||
</div>
|
||||
<div class="nav-spacer"></div>
|
||||
<button
|
||||
class="icon-btn nav-collapse-toggle"
|
||||
type="button"
|
||||
id="nav-collapse"
|
||||
aria-label="Collapse navigation"
|
||||
title="Collapse navigation"
|
||||
>
|
||||
«
|
||||
</button>
|
||||
<div class="nav-user">
|
||||
<div class="nav-user-avatar" id="nav-user-avatar" aria-hidden="true"></div>
|
||||
<div class="nav-user-detail">
|
||||
<span class="nav-user-email" id="account-email"></span>
|
||||
<button class="nav-user-logout" type="button" id="btn-logout">
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="app-workspace" id="app-workspace">
|
||||
<header class="app-header">
|
||||
<div class="header-row-top">
|
||||
<button
|
||||
class="icon-btn nav-hamburger"
|
||||
type="button"
|
||||
id="nav-hamburger"
|
||||
aria-label="Open navigation"
|
||||
aria-expanded="false"
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
<div class="brand">
|
||||
<div class="brand-text">
|
||||
<h1 id="cupping-title">Cupping</h1>
|
||||
<p class="brand-sub" id="cupping-subtitle">
|
||||
Tasting and scoring sessions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions hidden" id="session-autosave-wrap">
|
||||
<span class="autosave-chip" id="cupping-autosave-status"
|
||||
><span class="dot"></span
|
||||
><span class="autosave-text">Not saved yet</span></span
|
||||
>
|
||||
</div>
|
||||
</header>
|
||||
<div class="drawer-overlay hidden" id="drawer-overlay"></div>
|
||||
|
||||
<main class="page-content wide" id="cupping-list-view">
|
||||
<section class="panel-card" id="cupping-sessions-list">
|
||||
<div class="panel-head"><h2>Cupping sessions</h2></div>
|
||||
<div class="panel-body">
|
||||
<div class="table-wrap">
|
||||
<table class="data-table" id="sessions-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Coffee</th>
|
||||
<th class="num">Score</th>
|
||||
<th class="num">Cups</th>
|
||||
<th>Flavors</th>
|
||||
<th>Updated</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sessions-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel-card" id="new-session-card">
|
||||
<div class="panel-head"><h2>New session</h2></div>
|
||||
<div class="panel-body">
|
||||
<form id="new-session-form" class="inline-form">
|
||||
<label class="field"
|
||||
><span class="field-label">Roast (optional)</span>
|
||||
<select class="field-input" id="new-session-plan">
|
||||
<option value="">— no linked roast —</option>
|
||||
</select></label
|
||||
>
|
||||
<label class="field"
|
||||
><span class="field-label">Cups</span
|
||||
><input
|
||||
class="field-input"
|
||||
type="number"
|
||||
id="new-session-cups"
|
||||
min="1"
|
||||
max="12"
|
||||
value="5"
|
||||
/></label>
|
||||
<button class="primary-btn" type="submit">
|
||||
Start cupping
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<main class="page-content wide hidden" id="cupping-session-view">
|
||||
<div class="workspace-grid">
|
||||
<div class="form-col">
|
||||
<section class="panel-card" id="cup-scores">
|
||||
<div class="panel-head">
|
||||
<h2>Scores</h2>
|
||||
<label class="field" style="margin:0">
|
||||
<span class="field-label">Cups</span>
|
||||
<input
|
||||
class="field-input sm"
|
||||
type="number"
|
||||
id="cup-count"
|
||||
min="1"
|
||||
max="12"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<details class="why-panel" data-why="cup-scores" open>
|
||||
<summary>Why the scale starts at 6</summary>
|
||||
<p>
|
||||
The 6–10 range is a specialty-grading convention:
|
||||
anything below specialty grade simply isn't scored, so
|
||||
6 is the floor of the scale, not "zero". Score in
|
||||
quarter-point steps and leave an attribute unscored
|
||||
(—) rather than guessing — an unscored attribute
|
||||
contributes nothing, which is honest; a guessed 7.5
|
||||
pollutes every comparison you make later. The tick
|
||||
rows count cups, not quality: five clean cups out of
|
||||
five is full marks even if the coffee is merely good.
|
||||
</p>
|
||||
</details>
|
||||
<div id="cup-score-rows"></div>
|
||||
<div id="cup-tick-rows"></div>
|
||||
<details class="cupping-defects">
|
||||
<summary>Defects</summary>
|
||||
<div id="cup-defect-rows"></div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<summary>Tag what you can point at</summary>
|
||||
<p>
|
||||
A tag is only useful if you could defend it to
|
||||
another taster with the cup in front of you. Two or
|
||||
three confident descriptors beat a dozen hopeful ones
|
||||
— the list is capped at 32, but a session that needs
|
||||
10 is already suspect.
|
||||
</p>
|
||||
</details>
|
||||
<p class="field-note hidden" id="flavor-limit-note">
|
||||
32-tag limit reached.
|
||||
</p>
|
||||
<div id="flavor-families"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<summary>Notes outlive scores</summary>
|
||||
<p>
|
||||
Numbers compare roasts; sentences explain them. Write
|
||||
what you'd want to read before roasting this coffee
|
||||
again — texture, where it fell apart as it cooled,
|
||||
what you'd change.
|
||||
</p>
|
||||
</details>
|
||||
<textarea
|
||||
class="field-input"
|
||||
id="cup-notes-text"
|
||||
maxlength="4000"
|
||||
rows="5"
|
||||
aria-label="Cupping notes"
|
||||
></textarea>
|
||||
<p class="field-note"><span id="cup-notes-count">0</span>/4000</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside class="dock">
|
||||
<div class="dock-card">
|
||||
<div class="dock-card-head"><h2>Score</h2></div>
|
||||
<div class="stat-tile" style="box-shadow:none;border:none;padding:0">
|
||||
<div class="stat-tile-label">Total score</div>
|
||||
<div class="stat-tile-value" id="cup-total">0.00</div>
|
||||
</div>
|
||||
<p class="dock-note">Recomputed by the server on every save.</p>
|
||||
<div class="chip-group" id="flavor-chips"></div>
|
||||
</div>
|
||||
<div class="dock-card">
|
||||
<div class="dock-card-head"><h2>Radar</h2></div>
|
||||
<svg
|
||||
id="cup-radar"
|
||||
viewBox="0 0 240 240"
|
||||
role="img"
|
||||
aria-label="Cupping score radar chart"
|
||||
></svg>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Runs synchronously at parse time (before the deferred module script below) so opening
|
||||
// /cupping?session=… never flashes the empty sessions list first while cupping.js's own
|
||||
// async init (which awaits the auth check) is still getting underway.
|
||||
if (new URLSearchParams(location.search).get("session")) {
|
||||
document.getElementById("cupping-list-view").classList.add("hidden");
|
||||
document.getElementById("cupping-session-view").classList.remove("hidden");
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="/js/cupping.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user