${UNITS.map(u => `${esc(UNIT_LABEL[u])} `).join("")}
D \u270E N/A ${notAvail(l)?`
Not available \u2013 the supplier had none today, so no invoice line is expected
`:""} ${awaiting(l)?`
Weight to follow \u2013 leave the qty blank, the office will add it
`:""} ${note?`
Note: ${esc(note)}
`:""} ${custom&&!bought(l)?`
\u00D7 `:""} ${bought(l)?`
+ Another ${esc(p)} line (different supplier or price) `:""}
`;}const colHead = `
Product Qty Unit Price Supplier Direct Note None
`;function renderMain(){ const m = $("#main"); if(view === "mine" && !query){ viewSup = ""; renderMine(); return; } if(query) viewSup = ""; const childrenOf = (g,p) => [...LM.values()].filter(l => l.extra && l.parent === key(g,p)); const block = (g,p,showGroup,_i,custom) => rowHtml(g,p,showGroup,null,custom) + childrenOf(g,p).map(l => rowHtml(l.g,l.p,showGroup,l.id)).join(""); if(query){ const q = query.toLowerCase(); let html = `
Results for \u201C${esc(query)}\u201D ` + colHead, n = 0; SECTIONS.forEach(s => { s.items.forEach(p => { if(prodMatch(p,q)){ html += block(s.group,p,true); n++; } }); customOf(s.group).forEach(p => { if(prodMatch(p,q)){ html += block(s.group,p,true,null,true); n++; } }); }); [...LM.values()].filter(l => l.extra && !l.parent && prodMatch(l.p,q)).forEach(l => { html += rowHtml(l.g,l.p,true,l.id); n++; }); if(!n) html += `
Nothing called \u201C${esc(query)}\u201D on the sheet.+ Add \u201C${esc(query)}\u201D to today
`; m.innerHTML = html; return; } const sv = SUPPLIER_VIEWS.find(x => x.id === view); if(sv){ viewSup = sv.name; let html = `
${esc(sv.name)} what you usually buy from them ` + colHead; sv.items.forEach(p => { const g = GROUP_OF.get(p); if(g) html += block(g,p,true); }); customOf("sup:"+sv.name).forEach(p => html += block("Extra",p,false,null,true)); html += `
+ Add something not on this list `; html += `
These are the same lines as in the main sections \u2013 filling one in here is the same as filling it in there. The supplier is set to ${esc(sv.name)} for you.
`; m.innerHTML = html; return; } viewSup = ""; const s = SECTIONS.find(x => x.id === view); let html = `
${esc(s.name)} ${s.contact?`${esc(s.contact)} `:""}` + colHead; s.items.forEach(p => html += block(s.group,p,false)); customOf(s.group).forEach(p => html += block(s.group,p,false,null,true)); [...LM.values()].filter(l => l.extra && !l.parent && l.g === s.group).forEach(l => html += rowHtml(l.g,l.p,false,l.id)); html += `
+ Add something not on the list `; html += `
The supplier is filled in for you \u2013 type over it if you bought the item from someone else. Tap D for Direct (Brismark). Set the Unit to how you bought it \u2013 box, kg, carton, tray and so on.
`; m.innerHTML = html;}function sortedLines(){ return [...LM.values()].filter(l => hasData(l) && (onList(l) || String(l.pr||"").trim())) .map(l => { const supplier = (l.sup||"").trim() || (isMarket(l.g) || l.g === "Extra" ? "" : l.g); return {...l, unit: l.u || "", supplier, agent: isDirectSupplier(supplier) ? "Direct supplier" : (l.d ? "Brismark" : "IGA")}; }) .sort((a,b) => (a.agent.localeCompare(b.agent)) || a.supplier.localeCompare(b.supplier) || a.p.localeCompare(b.p));}function renderMine(){ const lines = sortedLines(); let html = `
My list for ${esc(niceDate(date))} `; if(!lines.length){ $("#main").innerHTML = html + `
Nothing entered yet. Pick a section at the top and type a quantity next to what you bought.
`; return; } html += day.fileSavedAt ? `
Sent at ${new Date(day.fileSavedAt).toLocaleTimeString("en-AU",{hour:"numeric",minute:"2-digit"})}. If you change anything, send it again.
` : `
When you've finished, tap Send / save file at the bottom, then choose SharePoint or Mail.
`; const newsup = lines.filter(l => l.newsup); if(newsup.length){ html += `
New supplier \u00B7 ${newsup.length} line${newsup.length>1?"s":""} Supplier Product Qty Price Note `; newsup.forEach(l => { const pr = num(l.pr); html += `${esc(l.supplier)} ${esc(l.p)} ${esc(l.q)} ${pr!==null?"$"+pr.toFixed(2):"\u2013"} ${esc(l.n||"")} `; }); html += `
We have not bought from these before \u2013 the office will confirm who invoices them.
`; } const na = lines.filter(l => notAvail(l)); if(na.length){ html += `
Not available \u00B7 ${na.length} line${na.length>1?"s":""} Supplier Product Note `; na.forEach(l => { html += `${esc(l.supplier)||"\u2013"} ${esc(l.p)} none today ${esc(l.n||"")} `; }); html += `
The supplier had none of these, so the office should not expect an invoice line.
`; } const pend = lines.filter(l => awaiting(l) && !notAvail(l)); if(pend.length){ html += `
Waiting on weight \u00B7 ${pend.length} line${pend.length>1?"s":""} Supplier Product Qty Price Note `; pend.forEach(l => { html += `${esc(l.supplier)||'No supplier '} ${esc(l.p)} weigh later $${(num(l.pr)||0).toFixed(2)}/kg ${esc(l.n||"")} `; }); html += `
These go to the office without a weight. The office adds the kilos once the warehouse has weighed them.
`; } ["Brismark","IGA","Direct supplier"].forEach(a => { const ls = lines.filter(l => l.agent === a && !awaiting(l) && !notAvail(l) && !l.newsup); if(!ls.length) return; html += `
${a}${a==="Brismark"?" (Direct)":""}${a==="Direct supplier"?" \u2013 invoices us themselves":""} · ${ls.length} line${ls.length>1?"s":""} Supplier Product Qty Price Note `; ls.forEach(l => { const pr = num(l.pr); html += `${esc(l.supplier)||'No supplier '} ${esc(l.p)} ${esc(l.q)||'No qty '}${l.u?" "+esc(l.u):""} ${pr!==null?"$"+pr.toFixed(2)+(l.u?"/"+esc(l.u):""):'No price '} ${esc(l.n||"")} `; }); html += `
`; }); html += `
${canSave() ? `${day.fileSavedAt?"Send again":"Send / save file"} ` : ""}Copy list
`; html += `
`; $("#main").innerHTML = html;}function renderAll(){ const di = $("#dateInput"); if(di.value !== date) di.value = date; di.max = todayIso(); $("#dayLabel").textContent = fromIso(date).toLocaleDateString("en-AU",{weekday:"long"}); $("#today").hidden = date === todayIso(); $("#next").disabled = date >= todayIso(); $("#next").style.opacity = date >= todayIso() ? .35 : 1; renderMain(); updateCounts();}/* ---------- interactions ---------- */function getLine(k){ let l = LM.get(k); if(!l){ const i = k.indexOf("|"); l = {id:k, g:k.slice(0,i), p:k.slice(i+1), q:"", pr:"", sup:"", d:false, u:"", n:""}; LM.set(k,l); } return l;}$("#main").addEventListener("input", e => { const inp = e.target.closest("input[data-f]"); if(!inp) return; const row = inp.closest(".item"); const l = getLine(row.dataset.k); l[inp.dataset.f] = inp.value; if(inp.dataset.f === "sup") openSug(inp); if(l.na && (inp.dataset.f === "q" || inp.dataset.f === "pr") && inp.value.trim()){ l.na = false; inp.closest(".item").classList.remove("na"); } if(inp.dataset.f !== "sup" && inp.value.trim() && !String(l.sup||"").trim() && viewSup) l.sup = viewSup; if(inp.dataset.f === "pr" && !isMarket(l.g) && l.g !== "Extra") rememberPrice(l.g, l.p, inp.value); const was = row.classList.contains("bought"), is = bought(l); row.classList.toggle("bought", is); row.classList.toggle("pending", awaiting(l)); if(is !== was){ const btn = row.querySelector('button[data-a="again"]'); if(is && !btn){ const nb = document.createElement("button"); nb.className = "againBtn"; nb.dataset.a = "again"; nb.textContent = "+ Another " + l.p + " line (different supplier or price)"; row.appendChild(nb); } else if(!is && btn && !l.extra){ btn.remove(); } } commit();});$("#main").addEventListener("change", e => { const inp = e.target.closest('input[data-f="sup"]'); if(inp){ const c = canonical(inp.value); const l0 = getLine(inp.closest(".item").dataset.k); l0.newsup = !!String(c).trim() && !isKnownAgent(c); if(c !== inp.value){ inp.value = c; const l = getLine(inp.closest(".item").dataset.k); l.sup = c; commit(); } rememberAgent(c); closeSug(); return; } const sel = e.target.closest('select[data-f="u"]'); if(!sel) return; const l = getLine(sel.closest(".item").dataset.k); l.u = sel.value; l.kg = (sel.value === "kg"); sel.classList.toggle("set", !!l.u); commit(); renderMain();});$("#main").addEventListener("focusin", e => { const inp = e.target.closest('input[data-f="sup"]'); if(inp) openSug(inp); else closeSug();});$("#main").addEventListener("focusout", e => { const inp = e.target.closest('input[data-f="sup"]'); if(inp) setTimeout(() => { if(document.activeElement !== inp) closeSug(); }, 120);});document.addEventListener("scroll", closeSug, true);$("#main").addEventListener("keydown", e => { if(e.key === "Escape"){ closeSug(); return; } if(e.key !== "Enter") return; const inp = e.target.closest("input[data-f]"); if(!inp) return; e.preventDefault(); if(inp.dataset.f === "sup"){ const first = sugBox && sugBox.querySelector(".sugItem"); if(first){ inp.value = first.textContent; inp.dispatchEvent(new Event("input",{bubbles:true})); inp.dispatchEvent(new Event("change",{bubbles:true})); closeSug(); inp.blur(); return; } } closeSug(); const all = [...document.querySelectorAll("#main input[data-f]")]; const i = all.indexOf(inp); if(all[i+1]) all[i+1].focus(); else inp.blur();});let noteKey = null, addD = false;$("#main").addEventListener("click", e => { const b = e.target.closest("button[data-a]"); if(!b) return; const a = b.dataset.a; if(a === "d"){ const row = b.closest(".item"); const l = getLine(row.dataset.k); l.d = !l.d; b.setAttribute("aria-pressed", l.d); commit(); } else if(a === "drop"){ const row = b.closest(".item"); const id = row.dataset.k; const i = id.indexOf("|"); forgetProduct(id.slice(0,i), id.slice(i+1)); LM.delete(id); commit(); renderMain(); } else if(a === "na"){ const row = b.closest(".item"); const l = getLine(row.dataset.k); l.na = !l.na; if(l.na){ l.q = ""; l.pr = ""; if(!String(l.sup||"").trim() && viewSup) l.sup = viewSup; } commit(); renderMain(); } else if(a === "again"){ const src = getLine(b.closest(".item").dataset.k); const nl = {id:nextId(), g:src.g, p:src.p, q:"", pr:"", sup:"", d:false, u:src.u, n:"", extra:true, parent:key(src.g,src.p)}; LM.set(nl.id, nl); commit(); renderMain(); const el = document.querySelector(`.item[data-k="${nl.id}"] input[data-f="q"]`); if(el) el.focus(); } else if(a === "n"){ noteKey = b.closest(".item").dataset.k; const l = getLine(noteKey); $("#noteTitle").textContent = "Note for " + l.p; $("#noteText").value = l.n || ""; $("#noteDlg").showModal(); setTimeout(()=>$("#noteText").focus(),50); } else if(a === "add"){ openAdd(query); } else if(a === "csv"){ saveCsv(); } else if(a === "copy"){ copyList(); }});$("#noteCancel").onclick = () => $("#noteDlg").close();$("#noteSave").onclick = () => { const l = getLine(noteKey); l.n = $("#noteText").value.trim(); $("#noteDlg").close(); commit(); renderMain(); };function openAdd(name){ $("#aName").value = name || ""; $("#aQty").value = ""; $("#aPrice").value = ""; $("#aSup").value = ""; $("#aNote").value = ""; $("#aUnit").innerHTML = UNITS.map(u => `
${esc(UNIT_LABEL[u])} `).join(""); $("#aUnit").value = ""; addD = false; $("#aD").setAttribute("aria-pressed","false"); $("#addDlg").showModal(); setTimeout(()=>$(name?"#aQty":"#aName").focus(),50);}$("#aD").onclick = () => { addD = !addD; $("#aD").setAttribute("aria-pressed", addD); };$("#addCancel").onclick = () => $("#addDlg").close();$("#addSave").onclick = () => { const p = $("#aName").value.trim(); if(!p){ $("#aName").focus(); return; } let g = "Extra"; if(!query && view !== "mine"){ const s = SECTIONS.find(x => x.id === view); if(s) g = s.group; const sv = SUPPLIER_VIEWS.find(x => x.id === view); if(sv && !$("#aSup").value.trim()) $("#aSup").value = sv.name; } const l = {id:nextId(), g, p, q:$("#aQty").value.trim(), pr:$("#aPrice").value.trim(), sup:canonical($("#aSup").value.trim()), d:addD, u:$("#aUnit").value, newsup: !isKnownAgent(canonical($("#aSup").value.trim())), n:($("#aNote").value.trim() || "Added at market"), extra:true}; if(!isMarket(g) && g !== "Extra") l.sup = ""; LM.set(l.id, l); rememberAgent(l.sup); rememberProduct(g, p); if(!isMarket(g) && g !== "Extra") rememberPrice(g, p, l.pr); $("#addDlg").close(); query = ""; $("#search").value = ""; commit(); renderMain();};function csvText(){ const q = v => `"${String(v??"").replace(/"/g,'""')}"`; const rows = [["Date","Buying Agent","Supplier","Product","Qty","Unit","Price","Notes"]] .concat(sortedLines().map(l => [date, l.agent, l.supplier, l.p, l.q, l.u || "", num(l.pr) ?? "", ((l.newsup ? "NEW SUPPLIER - INVOICING ROUTE TO CONFIRM. " : "") + (notAvail(l) ? "NOT AVAILABLE" + (l.n?" \u2013 "+l.n:"") : awaiting(l) ? "WEIGHT TO FOLLOW" + (l.n?" \u2013 "+l.n:"") : (l.n||""))).trim()])); return rows.map(r => r.map(q).join(",")).join("\r\n");}const standalone = !(window.claude && window.claude.use);function canSave(){ return !!dl || standalone; }function csvFile(){ return new File([csvText()], `Buy_Sheet_${date}.csv`, {type:"text/csv"});}function markSaved(how){ day.fileSavedAt = Date.now(); day.savedHow = how; LS.set("mbs:day:"+date, day); renderMine();}// iOS Safari cannot download from a locally-opened file, so try the Share sheet first.async function shareFile(){ try{ const f = csvFile(); if(navigator.canShare && navigator.canShare({files:[f]})){ await navigator.share({files:[f], title:`Buy Sheet ${date}`}); markSaved("shared"); return true; } }catch(e){ if(e && e.name === "AbortError") return true; } return false;}function anchorDownload(){ return new Promise(res => { try{ const url = URL.createObjectURL(new Blob([csvText()],{type:"text/csv"})); const a = document.createElement("a"); a.href = url; a.download = `Buy_Sheet_${date}.csv`; a.rel = "noopener"; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => { URL.revokeObjectURL(url); res(true); }, 300); }catch(e){ res(false); } });}async function saveCsv(){ if(standalone){ if(await shareFile()) return; const ok = await anchorDownload(); // On iOS a download from a local file fails silently, so never claim success here. showFallback(ok ? "If nothing appeared in Files, copy the list below instead." : "This iPad can't save a file from here. Copy the list below instead."); return; } if(!dl) return; try{ await dl.save({filename:`Buy_Sheet_${date}.csv`, data:new Blob([csvText()],{type:"text/csv"})}); markSaved("downloaded"); }catch(e){ if(e && (e.code === "declined" || e.code === "rate_limited")) return; dl = null; renderMine(); }}function showFallback(msg){ const box = document.getElementById("fallback"); if(box){ box.hidden = false; box.querySelector(".fbMsg").textContent = msg; box.scrollIntoView({block:"nearest"}); }}async function copyList(){ try{ await navigator.clipboard.writeText(csvText()); setStatus("List copied \u2013 now paste it into an email"); markSaved("copied"); }catch(e){ showFallback("Press and hold in the box below, choose Select All, then Copy."); const t = document.querySelector("#fallback textarea"); if(t){ t.focus(); t.select(); } }}["#chips","#chips2"].forEach(sel => $(sel).addEventListener("click", e => { const b = e.target.closest(".chip"); if(!b) return; view = b.dataset.v; query = ""; $("#search").value = ""; renderMain(); updateCounts(); window.scrollTo(0,0);}));let st = null;$("#search").addEventListener("input", e => { clearTimeout(st); st = setTimeout(() => { query = e.target.value.trim(); renderMain(); updateCounts(); }, 150); });$("#listBtn").onclick = () => { query = ""; $("#search").value = ""; if(view === "mine"){ view = LS.get("mbs:lastView") || SECTIONS[0].id; } else { LS.set("mbs:lastView", view); view = "mine"; } renderMain(); updateCounts(); window.scrollTo(0,0);};const shift = n => { const d = fromIso(date); d.setDate(d.getDate()+n); const s = iso(d); if(s <= todayIso()) loadDay(s); };$("#prev").onclick = () => shift(-1);$("#next").onclick = () => shift(1);$("#today").onclick = () => loadDay(todayIso());$("#dateInput").addEventListener("change", e => { const v = e.target.value; if(!v){ e.target.value = date; return; } if(v > todayIso()){ e.target.value = date; return; } closeSug(); loadDay(v);});/* ---------- start ---------- */agentsList();loadDay(date);if(window.claude && window.claude.use){ window.claude.use("downloads").then(x => { dl = x; if(view === "mine" && !query) renderMine(); });}})();