Files
snowspeeder 59645e59b5 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.
2026-07-30 14:47:15 -04:00

73 lines
2.6 KiB
JavaScript

// Browser-safe. The allowlist of every scalar worksheet field, and a blank plan factory.
export const FIELD_IDS = [
"0.1", "0.2", "0.3", "0.4",
"1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7",
"2.1", "2.3", "2.4", "2.5",
"3.1", "3.2",
"4.1", "4.2", "4.3",
"5.1", "5.2", "5.3", "5.4", "5.5", "5.6",
"6.4",
];
export function blankPlan() {
const fields = {};
for (const id of FIELD_IDS) fields[id] = "";
fields["1.6"] = "0";
fields["2.1"] = "single";
return {
schemaVersion: 1,
fields,
blendComponents: [
{ cultivar: "", group: "", process: "", sharePct: "", fcAnchor: "" },
{ cultivar: "", group: "", process: "", sharePct: "", fcAnchor: "" },
],
temps: {
charge: { time: "0:00", tempC: "", tempF: "" },
tp: { time: "", tempC: "", tempF: "" },
yellow: { time: "", tempC: "", tempF: "" },
fc: { time: "", tempC: "", tempF: "" },
drop: { time: "", tempC: "", tempF: "" },
},
ror: {
tpToYellow: { value: "", ok: false, note: "" },
yellowToFc: { value: "", ok: false, note: "" },
fcToDrop: { value: "", ok: false, note: "" },
},
actuators: [
{ time: "0:00", heatPct: "", fanPct: "", expectedBt: "", why: "" },
{ time: "", heatPct: "", fanPct: "", expectedBt: "", why: "" },
{ time: "", heatPct: "", fanPct: "", expectedBt: "", why: "" },
],
planActual: {
charge: { planTime: "0:00", planBt: "", actualTime: "0:00", actualBt: "", ror: "", note: "" },
tp: { planTime: "", planBt: "", actualTime: "", actualBt: "", ror: "", note: "" },
yellow: { planTime: "", planBt: "", actualTime: "", actualBt: "", ror: "", note: "" },
fc: { planTime: "", planBt: "", actualTime: "", actualBt: "", ror: "", note: "" },
drop: { planTime: "", planBt: "", actualTime: "", actualBt: "", ror: "", note: "" },
},
afterRoast: {
greenIn: "", out: "", weightLossPct: "", vsTarget: "", actualDtrPct: "",
colour: "", restedDays: "", brewRatio: "", method: "",
cupNotes: "", cupNotesLine2: "", oneChange: "", disproof: "",
},
sanityOverride: { drying: null, maillard: null, dtr: null, ceiling: null },
reference: null,
prefill: null,
inventory: { lotId: "", lotLabel: "", consumed: null },
};
}
/** Keep only known FIELD_IDS, coerce every value to a string. */
export function sanitizeFieldPatch(patch) {
const out = {};
if (!patch || typeof patch !== "object") return out;
for (const id of FIELD_IDS) {
if (Object.prototype.hasOwnProperty.call(patch, id) && patch[id] !== null && patch[id] !== undefined) {
out[id] = String(patch[id]);
}
}
return out;
}