let feedData = []; let otherData = []; async function loadBudget() { const msg = document.getElementById('msg'); try { const [stats, purchases, otherPurchases] = await Promise.all([ API.get('/api/stats/budget'), API.get('/api/feed'), API.get('/api/other'), ]); // All-time stats document.getElementById('b-cost-total').textContent = fmtMoney(stats.total_feed_cost); document.getElementById('b-other-total').textContent = fmtMoney(stats.total_other_cost); document.getElementById('b-eggs-total').textContent = stats.total_eggs_alltime; document.getElementById('b-cpe').textContent = fmtMoneyFull(stats.cost_per_egg); document.getElementById('b-cpd').textContent = fmtMoney(stats.cost_per_dozen); // Last 30 days document.getElementById('b-cost-30d').textContent = fmtMoney(stats.total_feed_cost_30d); document.getElementById('b-other-30d').textContent = fmtMoney(stats.total_other_cost_30d); document.getElementById('b-eggs-30d').textContent = stats.total_eggs_30d; document.getElementById('b-cpe-30d').textContent = fmtMoneyFull(stats.cost_per_egg_30d); document.getElementById('b-cpd-30d').textContent = fmtMoney(stats.cost_per_dozen_30d); feedData = purchases; otherData = otherPurchases; renderTable(); } catch (err) { showMessage(msg, `Failed to load budget data: ${err.message}`, 'error'); } } function renderTable() { const tbody = document.getElementById('purchase-body'); const tfoot = document.getElementById('purchase-foot'); const combined = [ ...feedData.map(e => ({ ...e, _type: 'feed' })), ...otherData.map(e => ({ ...e, _type: 'other' })), ].sort((a, b) => { if (b.date !== a.date) return b.date.localeCompare(a.date); return b.created_at.localeCompare(a.created_at); }); if (combined.length === 0) { tbody.innerHTML = 'No purchases logged yet.'; tfoot.innerHTML = ''; return; } tbody.innerHTML = combined.map(e => { if (e._type === 'feed') { const total = (parseFloat(e.bags) * parseFloat(e.price_per_bag)).toFixed(2); return ` ${fmtDate(e.date)} Feed ${parseFloat(e.bags)} bags @ ${fmtMoney(e.price_per_bag)}/bag ${fmtMoney(total)} ${e.notes || ''} `; } else { return ` ${fmtDate(e.date)} Other — ${fmtMoney(e.total)} ${e.notes || ''} `; } }).join(''); const feedTotal = feedData.reduce((sum, e) => sum + parseFloat(e.bags) * parseFloat(e.price_per_bag), 0); const otherTotal = otherData.reduce((sum, e) => sum + parseFloat(e.total), 0); const grandTotal = feedTotal + otherTotal; tfoot.innerHTML = ` Total ${fmtMoney(grandTotal)} ${combined.length} purchase${combined.length === 1 ? '' : 's'} `; } // ── Feed edit / delete ──────────────────────────────────────────────────────── function startEditFeed(id) { const entry = feedData.find(e => e.id === id); const row = document.querySelector(`tr[data-id="${id}"][data-type="feed"]`); row.innerHTML = ` Feed bags @ /bag — `; } async function saveEditFeed(id) { const msg = document.getElementById('msg'); const row = document.querySelector(`tr[data-id="${id}"][data-type="feed"]`); const [dateInput, bagsInput, priceInput, notesInput] = row.querySelectorAll('input'); try { const updated = await API.put(`/api/feed/${id}`, { date: dateInput.value, bags: parseFloat(bagsInput.value), price_per_bag: parseFloat(priceInput.value), notes: notesInput.value.trim() || null, }); feedData[feedData.findIndex(e => e.id === id)] = updated; renderTable(); loadBudget(); showMessage(msg, 'Purchase updated.'); } catch (err) { showMessage(msg, `Error: ${err.message}`, 'error'); } } async function deleteFeed(id) { if (!confirm('Delete this purchase?')) return; const msg = document.getElementById('msg'); try { await API.del(`/api/feed/${id}`); feedData = feedData.filter(e => e.id !== id); renderTable(); loadBudget(); showMessage(msg, 'Purchase deleted.'); } catch (err) { showMessage(msg, `Error: ${err.message}`, 'error'); } } // ── Other edit / delete ─────────────────────────────────────────────────────── function startEditOther(id) { const entry = otherData.find(e => e.id === id); const row = document.querySelector(`tr[data-id="${id}"][data-type="other"]`); row.innerHTML = ` Other — `; } async function saveEditOther(id) { const msg = document.getElementById('msg'); const row = document.querySelector(`tr[data-id="${id}"][data-type="other"]`); const [dateInput, totalInput, notesInput] = row.querySelectorAll('input'); try { const updated = await API.put(`/api/other/${id}`, { date: dateInput.value, total: parseFloat(totalInput.value), notes: notesInput.value.trim() || null, }); otherData[otherData.findIndex(e => e.id === id)] = updated; renderTable(); loadBudget(); showMessage(msg, 'Purchase updated.'); } catch (err) { showMessage(msg, `Error: ${err.message}`, 'error'); } } async function deleteOther(id) { if (!confirm('Delete this purchase?')) return; const msg = document.getElementById('msg'); try { await API.del(`/api/other/${id}`); otherData = otherData.filter(e => e.id !== id); renderTable(); loadBudget(); showMessage(msg, 'Purchase deleted.'); } catch (err) { showMessage(msg, `Error: ${err.message}`, 'error'); } } // ── Init ────────────────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { const feedForm = document.getElementById('feed-form'); const otherForm = document.getElementById('other-form'); const msg = document.getElementById('msg'); const bagsInput = document.getElementById('bags'); const priceInput = document.getElementById('price'); const totalDisplay = document.getElementById('total-display'); setToday(document.getElementById('date')); setToday(document.getElementById('other-date')); // Live total calculation for feed form function updateTotal() { const bags = parseFloat(bagsInput.value) || 0; const price = parseFloat(priceInput.value) || 0; totalDisplay.value = bags && price ? fmtMoney(bags * price) : ''; } bagsInput.addEventListener('input', updateTotal); priceInput.addEventListener('input', updateTotal); feedForm.addEventListener('submit', async (e) => { e.preventDefault(); const data = { date: document.getElementById('date').value, bags: parseFloat(bagsInput.value), price_per_bag: parseFloat(priceInput.value), notes: document.getElementById('notes').value.trim() || null, }; try { await API.post('/api/feed', data); showMessage(msg, 'Feed purchase saved!'); feedForm.reset(); totalDisplay.value = ''; setToday(document.getElementById('date')); loadBudget(); } catch (err) { showMessage(msg, `Error: ${err.message}`, 'error'); } }); otherForm.addEventListener('submit', async (e) => { e.preventDefault(); const data = { date: document.getElementById('other-date').value, total: parseFloat(document.getElementById('other-total').value), notes: document.getElementById('other-notes').value.trim() || null, }; try { await API.post('/api/other', data); showMessage(msg, 'Purchase saved!'); otherForm.reset(); setToday(document.getElementById('other-date')); loadBudget(); } catch (err) { showMessage(msg, `Error: ${err.message}`, 'error'); } }); loadBudget(); });