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();
});