let monthlyChart = null; function buildChart(rows) { const chartWrap = document.getElementById('chart-wrap'); const noDataMsg = document.getElementById('chart-no-data'); if (rows.length === 0) { chartWrap.style.display = 'none'; noDataMsg.style.display = 'block'; return; } chartWrap.style.display = 'block'; noDataMsg.style.display = 'none'; // Show up to last 24 months, oldest → newest for the chart const display = [...rows].reverse().slice(-24); const labels = display.map(r => r.month_label); const data = display.map(r => r.total_eggs); const ctx = document.getElementById('monthly-chart').getContext('2d'); if (monthlyChart) monthlyChart.destroy(); monthlyChart = new Chart(ctx, { type: 'bar', data: { labels, datasets: [{ data, backgroundColor: 'rgba(61,107,79,0.75)', borderColor: '#3d6b4f', borderWidth: 1, borderRadius: 4, }], }, options: { responsive: true, plugins: { legend: { display: false }, tooltip: { callbacks: { label: ctx => ` ${ctx.parsed.y} eggs`, }, }, }, scales: { y: { beginAtZero: true, suggestedMax: 10, ticks: { stepSize: 1, precision: 0 }, }, }, }, }); } async function loadSummary() { const msg = document.getElementById('msg'); const tbody = document.getElementById('summary-body'); try { const rows = await API.get('/api/stats/monthly'); buildChart(rows); if (rows.length === 0) { tbody.innerHTML = 'No data yet.'; return; } tbody.innerHTML = rows.map(r => ` ${r.month_label} ${r.total_eggs} ${r.days_logged} ${r.avg_eggs_per_day ?? '—'} ${r.flock_at_month_end ?? '—'} ${r.avg_eggs_per_hen_per_day ?? '—'} ${fmtMoney(r.feed_cost)} ${fmtMoneyFull(r.cost_per_egg)} ${fmtMoney(r.cost_per_dozen)} `).join(''); } catch (err) { showMessage(msg, `Failed to load summary: ${err.message}`, 'error'); } } document.addEventListener('DOMContentLoaded', loadSummary); // ── CSV Export ──────────────────────────────────────────────────────────────── function csvEscape(str) { return `"${String(str == null ? '' : str).replace(/"/g, '""')}"`; } async function exportCSV() { const msg = document.getElementById('msg'); try { const [eggsData, flockAll, feedData] = await Promise.all([ API.get('/api/eggs'), API.get('/api/flock'), API.get('/api/feed'), ]); if (eggsData.length === 0 && flockAll.length === 0 && feedData.length === 0) { showMessage(msg, 'No data to export.', 'error'); return; } // Sort flock ascending for effective-count lookup const flockSorted = [...flockAll].sort((a, b) => a.date.localeCompare(b.date)); function effectiveFlockAt(dateStr) { let result = null; for (const f of flockSorted) { if (f.date <= dateStr) result = f; else break; } return result; } // Date-keyed lookups const eggsByDate = Object.fromEntries(eggsData.map(e => [e.date, e])); const flockByDate = Object.fromEntries(flockAll.map(f => [f.date, f])); const feedByDate = {}; for (const f of feedData) { if (!feedByDate[f.date]) feedByDate[f.date] = []; feedByDate[f.date].push(f); } // Union of all dates const allDates = [...new Set([ ...Object.keys(eggsByDate), ...Object.keys(flockByDate), ...Object.keys(feedByDate), ])].sort((a, b) => b.localeCompare(a)); const csvRows = [[ 'Date', 'Eggs Collected', 'Egg Notes', 'Flock Size', 'Flock Notes', 'Feed Bags', 'Feed Price/Bag', 'Feed Total', 'Feed Notes', ]]; for (const dateStr of allDates) { const egg = eggsByDate[dateStr]; const feeds = feedByDate[dateStr] || []; const flock = effectiveFlockAt(dateStr); const flockChg = flockByDate[dateStr]; const flockCount = flock ? flock.chicken_count : ''; const flockNotes = flockChg ? csvEscape(flockChg.notes) : ''; if (feeds.length === 0) { csvRows.push([ dateStr, egg ? egg.eggs : '', egg ? csvEscape(egg.notes) : '', flockCount, flockNotes, '', '', '', '', ]); } else { feeds.forEach((feed, i) => { const total = (parseFloat(feed.bags) * parseFloat(feed.price_per_bag)).toFixed(2); csvRows.push([ dateStr, i === 0 && egg ? egg.eggs : '', i === 0 && egg ? csvEscape(egg.notes) : '', i === 0 ? flockCount : '', i === 0 ? flockNotes : '', parseFloat(feed.bags), parseFloat(feed.price_per_bag), total, csvEscape(feed.notes), ]); }); } } const csv = csvRows.map(r => r.join(',')).join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const now = new Date(); const fileDate = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`; a.download = `egg-tracker-${fileDate}.csv`; a.click(); URL.revokeObjectURL(url); } catch (err) { showMessage(msg, `Export failed: ${err.message}`, 'error'); } } // ── CSV Import ──────────────────────────────────────────────────────────────── function parseCSVText(text) { const rows = []; const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n'); for (const line of lines) { if (!line.trim()) continue; const fields = []; let field = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const ch = line[i]; if (inQuotes) { if (ch === '"' && line[i + 1] === '"') { field += '"'; i++; } else if (ch === '"') { inQuotes = false; } else { field += ch; } } else if (ch === '"') { inQuotes = true; } else if (ch === ',') { fields.push(field); field = ''; } else { field += ch; } } fields.push(field); rows.push(fields); } return rows; } async function handleImportFile(input) { const file = input.files[0]; if (!file) return; input.value = ''; // allow re-selecting the same file const msg = document.getElementById('msg'); let text; try { text = await file.text(); } catch (err) { showMessage(msg, `Could not read file: ${err.message}`, 'error'); return; } const rows = parseCSVText(text); if (rows.length < 2) { showMessage(msg, 'CSV has no data rows.', 'error'); return; } const headers = rows[0].map(h => h.trim()); const expected = [ 'Date', 'Eggs Collected', 'Egg Notes', 'Flock Size', 'Flock Notes', 'Feed Bags', 'Feed Price/Bag', 'Feed Total', 'Feed Notes', ]; const missing = expected.filter(h => !headers.includes(h)); if (missing.length > 0) { showMessage(msg, `CSV is missing columns: ${missing.join(', ')}`, 'error'); return; } const idx = Object.fromEntries(headers.map((h, i) => [h, i])); // Sort ascending so flock-change detection is chronologically correct const dataRows = rows.slice(1) .filter(r => r.length > 1) .sort((a, b) => (a[idx['Date']] || '').localeCompare(b[idx['Date']] || '')); let eggsCreated = 0, eggsSkipped = 0; let flockCreated = 0; let feedCreated = 0; let errors = 0; let lastFlockSize = null; let lastFlockDate = null; for (const row of dataRows) { const get = col => (row[idx[col]] ?? '').trim(); const date = get('Date'); const eggsStr = get('Eggs Collected'); const eggNotes = get('Egg Notes'); const flockStr = get('Flock Size'); const flockNotes = get('Flock Notes'); const bagsStr = get('Feed Bags'); const priceStr = get('Feed Price/Bag'); const feedNotes = get('Feed Notes'); if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) continue; // ── Egg entry ──────────────────────────────────────────────────────── if (eggsStr !== '' && !isNaN(parseInt(eggsStr, 10))) { try { await API.post('/api/eggs', { date, eggs: parseInt(eggsStr, 10), notes: eggNotes || null, }); eggsCreated++; } catch (err) { if (err.message.toLowerCase().includes('already exists')) { eggsSkipped++; } else { errors++; } } } // ── Flock entry ────────────────────────────────────────────────────── if (flockStr !== '' && !isNaN(parseInt(flockStr, 10))) { const flockSize = parseInt(flockStr, 10); const sizeChanged = flockSize !== lastFlockSize; const hasNotes = flockNotes !== ''; if (date !== lastFlockDate && (sizeChanged || hasNotes)) { try { await API.post('/api/flock', { date, chicken_count: flockSize, notes: flockNotes || null, }); flockCreated++; lastFlockSize = flockSize; lastFlockDate = date; } catch (err) { errors++; } } } // ── Feed entry ─────────────────────────────────────────────────────── if (bagsStr !== '' && priceStr !== '' && !isNaN(parseFloat(bagsStr)) && !isNaN(parseFloat(priceStr))) { try { await API.post('/api/feed', { date, bags: parseFloat(bagsStr), price_per_bag: parseFloat(priceStr), notes: feedNotes || null, }); feedCreated++; } catch (err) { errors++; } } } const parts = []; if (eggsCreated) parts.push(`${eggsCreated} egg ${eggsCreated === 1 ? 'entry' : 'entries'}`); if (eggsSkipped) parts.push(`${eggsSkipped} skipped (duplicate date)`); if (flockCreated) parts.push(`${flockCreated} flock ${flockCreated === 1 ? 'change' : 'changes'}`); if (feedCreated) parts.push(`${feedCreated} feed ${feedCreated === 1 ? 'purchase' : 'purchases'}`); const summary = parts.length ? `Imported: ${parts.join(', ')}.` : 'Nothing new to import.'; const errNote = errors > 0 ? ` (${errors} row${errors === 1 ? '' : 's'} failed)` : ''; showMessage(msg, summary + errNote, errors > 0 ? 'error' : 'success'); loadSummary(); }