Files
roast_command_center/public/js/lot-picker.js
T

152 lines
4.9 KiB
JavaScript

import { api, protectedFetch } from "./api.js?v=20260730-release2";
import { showToast } from "./toast.js?v=20260730-release2";
let lots = [];
function findLot(id) {
return lots.find((l) => l.id === id);
}
/** Wires the "From inventory lot" picker (in The Coffee) and the "Draw from lot" action
* (in Roast Log — Plan vs. Actual). `getRemotePlanId` is a function since the planner's
* remotePlanId is module-local and can change after a sync. */
export function initLotPicker({ state, recompute, flushCurrentPlan, getRemotePlanId }) {
const select = document.getElementById("lot-select");
const note = document.getElementById("lot-picker-note");
const consumeRow = document.getElementById("inventory-consume");
const consumeLabel = document.getElementById("inventory-consume-label");
const consumeBtn = document.getElementById("btn-consume");
function renderOptions() {
const current = state.plan.inventory.lotId;
select.replaceChildren(
Object.assign(document.createElement("option"), { value: "", textContent: "— none —" }),
...lots
.filter((l) => !l.archived)
.map((l) =>
Object.assign(document.createElement("option"), {
value: l.id,
textContent: `${l.origin}${l.variety ? ` — ${l.variety}` : ""} (${Math.round(l.remainingWeightG)} g remaining)`,
}),
),
);
if (current && !findLot(current)) {
// The plan references a lot that's now archived/deleted — keep it selectable so the
// stored value still displays instead of silently reverting to "— none —".
select.append(
Object.assign(document.createElement("option"), {
value: current,
textContent: state.plan.inventory.lotLabel || "Unavailable lot",
disabled: true,
}),
);
}
select.value = current || "";
}
async function loadLots() {
try {
const body = await api("/api/inventory");
lots = body.lots;
} catch {
lots = [];
}
renderOptions();
updateConsumeRow();
}
function updateConsumeRow() {
const lotId = state.plan.inventory.lotId;
if (!lotId) {
consumeRow.classList.add("hidden");
return;
}
consumeRow.classList.remove("hidden");
const consumed = state.plan.inventory.consumed;
if (consumed) {
consumeLabel.textContent = `✓ ${Math.round(consumed.weightG)} g drawn from ${consumed.lotLabel} · ${new Date(consumed.atIso).toLocaleDateString()}`;
consumeBtn.classList.add("hidden");
return;
}
consumeBtn.classList.remove("hidden");
const weightG = Number(state.plan.fields["0.4"]);
const lot = findLot(lotId);
const lotLabel = lot ? lot.origin : state.plan.inventory.lotLabel || "this lot";
if (Number.isFinite(weightG) && weightG > 0) {
consumeLabel.textContent = `Charging will draw ${weightG} g from ${lotLabel}.`;
consumeBtn.disabled = false;
consumeBtn.removeAttribute("title");
} else {
consumeLabel.textContent = `Set "Green in" (field 0.4) to draw weight from ${lotLabel}.`;
consumeBtn.disabled = true;
consumeBtn.title = "Enter a Green in weight on The Coffee first";
}
}
select.addEventListener("change", () => {
const lotId = select.value;
state.plan.inventory.lotId = lotId;
const lot = findLot(lotId);
state.plan.inventory.lotLabel = lot
? `${lot.origin}${lot.variety ? ` — ${lot.variety}` : ""}`
: "";
if (lot) {
note.textContent = `${Math.round(lot.remainingWeightG)} g remaining in this lot.`;
const nameInput = document.querySelector('[name="0.1"]');
if (nameInput && !nameInput.value) {
nameInput.value = lot.origin;
state.plan.fields["0.1"] = lot.origin;
}
} else {
note.textContent =
"Optional — link a lot and the Roast Log can draw the green weight down when you charge.";
}
updateConsumeRow();
recompute();
});
consumeBtn.addEventListener("click", async () => {
consumeBtn.disabled = true;
try {
if (!(await flushCurrentPlan())) {
showToast("Could not sync the plan. Try again.", "fail");
return;
}
const roastPlanId = getRemotePlanId();
if (!roastPlanId) {
showToast("Sync the plan first, then draw from the lot.", "fail");
return;
}
const weightG = Number(state.plan.fields["0.4"]);
const response = await protectedFetch(
`/api/inventory/${state.plan.inventory.lotId}/consume`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ weightG, roastPlanId }),
},
);
const body = await response.json().catch(() => ({}));
if (!response.ok && body.code !== "already_consumed") {
showToast(body.error || "Could not draw from the lot.", "fail");
return;
}
state.plan.inventory.consumed = {
lotId: state.plan.inventory.lotId,
lotLabel: state.plan.inventory.lotLabel,
weightG,
atIso: new Date().toISOString(),
};
updateConsumeRow();
recompute();
showToast(`${weightG} g drawn from lot.`);
await loadLots();
} finally {
consumeBtn.disabled = false;
}
});
loadLots();
return { updateConsumeRow, renderOptions };
}