-
My Bean Journey
-
+
+
+
Days on Feed Distribution
+
+
+
-
-
+
+
-
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
-
-
+
+
+
+
+
+
diff --git a/js/alerts.js b/js/alerts.js
new file mode 100644
index 0000000..e576206
--- /dev/null
+++ b/js/alerts.js
@@ -0,0 +1,189 @@
+/**
+ * Cattle Feed Lot Monitor - Alert System
+ * Handles alert generation and toast notifications.
+ */
+const AlertSystem = (function () {
+
+ const ALERT_ICONS = {
+ processing: '⚠️', // warning
+ death: '❌', // red X
+ medical: '💊', // syringe
+ pregnancy: '💕', // heart
+ capacity: '📦', // package
+ move: '🚚' // truck
+ };
+
+ function getIcon(type) {
+ return ALERT_ICONS[type] || '🔔';
+ }
+
+ /**
+ * Create an alert when cattle status changes
+ */
+ function onStatusChange(animal, oldStatus, newStatus) {
+ const lotName = 'Lot ' + animal.lotId;
+ let type, message;
+
+ switch (newStatus) {
+ case 'processing':
+ type = 'processing';
+ message = `ALERT: Tag #${animal.tagNumber} moved to PROCESSING from ${lotName}. Was ${oldStatus}.`;
+ break;
+ case 'deceased':
+ type = 'death';
+ message = `ALERT: Tag #${animal.tagNumber} reported DECEASED in ${lotName}. Was ${oldStatus}.`;
+ break;
+ case 'medical':
+ type = 'medical';
+ message = `Tag #${animal.tagNumber} placed in MEDICAL CARE in ${lotName}.`;
+ break;
+ case 'pregnant':
+ type = 'pregnancy';
+ message = `Pregnancy confirmed for Tag #${animal.tagNumber} in ${lotName}.`;
+ break;
+ case 'active':
+ type = 'move';
+ message = `Tag #${animal.tagNumber} returned to ACTIVE status in ${lotName}. Was ${oldStatus}.`;
+ break;
+ default:
+ type = 'move';
+ message = `Tag #${animal.tagNumber} status changed to ${newStatus} in ${lotName}.`;
+ }
+
+ const alert = Store.addAlert({
+ type,
+ lotId: animal.lotId,
+ cattleId: animal.id,
+ tagNumber: animal.tagNumber,
+ message
+ });
+
+ showToast(message, type);
+ updateBadge();
+ return alert;
+ }
+
+ /**
+ * Create an alert when cattle is moved between lots
+ */
+ function onCattleMoved(animal, oldLotId, newLotId) {
+ const message = `Tag #${animal.tagNumber} moved from Lot ${oldLotId} to Lot ${newLotId}.`;
+ const alert = Store.addAlert({
+ type: 'move',
+ lotId: newLotId,
+ cattleId: animal.id,
+ tagNumber: animal.tagNumber,
+ message
+ });
+ showToast(message, 'info');
+ updateBadge();
+ return alert;
+ }
+
+ /**
+ * Check lot capacity and alert if near full
+ */
+ function checkCapacity(lotId) {
+ const stats = Store.getLotStats(lotId);
+ const pct = (stats.total / Store.LOT_CAPACITY) * 100;
+
+ if (pct >= 95) {
+ const message = `Lot ${lotId} is at ${pct.toFixed(0)}% capacity (${stats.total}/${Store.LOT_CAPACITY}). Nearly FULL!`;
+ Store.addAlert({
+ type: 'capacity',
+ lotId,
+ message
+ });
+ showToast(message, 'warning');
+ updateBadge();
+ } else if (pct >= 85) {
+ const message = `Lot ${lotId} is at ${pct.toFixed(0)}% capacity (${stats.total}/${Store.LOT_CAPACITY}).`;
+ Store.addAlert({
+ type: 'capacity',
+ lotId,
+ message
+ });
+ updateBadge();
+ }
+ }
+
+ /**
+ * Show toast notification
+ */
+ function showToast(message, type) {
+ const container = document.getElementById('toast-container');
+ const toast = document.createElement('div');
+ toast.className = 'toast toast-' + (type === 'death' ? 'danger' : type === 'processing' ? 'warning' : 'info');
+
+ const icon = document.createElement('span');
+ icon.className = 'toast-icon';
+ icon.innerHTML = getIcon(type);
+
+ const msg = document.createElement('span');
+ msg.className = 'toast-message';
+ msg.textContent = message;
+
+ const close = document.createElement('button');
+ close.className = 'toast-close';
+ close.innerHTML = '×';
+ close.addEventListener('click', () => {
+ toast.style.animation = 'slideOut 0.3s ease forwards';
+ setTimeout(() => toast.remove(), 300);
+ });
+
+ toast.appendChild(icon);
+ toast.appendChild(msg);
+ toast.appendChild(close);
+ container.appendChild(toast);
+
+ // Auto-dismiss after 6 seconds
+ setTimeout(() => {
+ if (toast.parentNode) {
+ toast.style.animation = 'slideOut 0.3s ease forwards';
+ setTimeout(() => toast.remove(), 300);
+ }
+ }, 6000);
+ }
+
+ /**
+ * Update the alert badge count in the header
+ */
+ function updateBadge() {
+ const badge = document.getElementById('alert-badge');
+ const count = Store.getUnreadCount();
+ if (count > 0) {
+ badge.textContent = count > 99 ? '99+' : count;
+ badge.classList.remove('hidden');
+ } else {
+ badge.classList.add('hidden');
+ }
+ }
+
+ /**
+ * Format a timestamp for display
+ */
+ function formatTime(isoString) {
+ const d = new Date(isoString);
+ const now = new Date();
+ const diffMs = now - d;
+ const diffMins = Math.floor(diffMs / 60000);
+ const diffHours = Math.floor(diffMs / 3600000);
+ const diffDays = Math.floor(diffMs / 86400000);
+
+ if (diffMins < 1) return 'Just now';
+ if (diffMins < 60) return diffMins + 'm ago';
+ if (diffHours < 24) return diffHours + 'h ago';
+ if (diffDays < 7) return diffDays + 'd ago';
+ return d.toLocaleDateString();
+ }
+
+ return {
+ onStatusChange,
+ onCattleMoved,
+ checkCapacity,
+ showToast,
+ updateBadge,
+ formatTime,
+ getIcon
+ };
+})();
diff --git a/js/app.js b/js/app.js
new file mode 100644
index 0000000..e94477a
--- /dev/null
+++ b/js/app.js
@@ -0,0 +1,624 @@
+/**
+ * Cattle Feed Lot Monitor - Main Application
+ * Handles UI rendering, navigation, and user interactions.
+ */
+(function () {
+ 'use strict';
+
+ // --- Initialize ---
+ document.addEventListener('DOMContentLoaded', function () {
+ if (!Store.isInitialized()) {
+ Store.seedDemoData();
+ }
+ initNavigation();
+ initModals();
+ initEventHandlers();
+ renderDashboard();
+ AlertSystem.updateBadge();
+ populateLotDropdowns();
+ });
+
+ // --- Navigation ---
+ function initNavigation() {
+ document.querySelectorAll('.nav-tab').forEach(function (tab) {
+ tab.addEventListener('click', function () {
+ var view = this.getAttribute('data-view');
+ switchView(view);
+ });
+ });
+ }
+
+ function switchView(viewName) {
+ document.querySelectorAll('.nav-tab').forEach(function (t) { t.classList.remove('active'); });
+ document.querySelectorAll('.view').forEach(function (v) { v.classList.remove('active'); });
+
+ var tab = document.querySelector('.nav-tab[data-view="' + viewName + '"]');
+ var view = document.getElementById('view-' + viewName);
+ if (tab) tab.classList.add('active');
+ if (view) view.classList.add('active');
+
+ switch (viewName) {
+ case 'dashboard': renderDashboard(); break;
+ case 'lots': renderLotDetail(); break;
+ case 'cattle': renderCattleTable(); break;
+ case 'alerts': renderAlerts(); break;
+ case 'reports': renderReports(); break;
+ }
+ }
+
+ // --- Dashboard ---
+ function renderDashboard() {
+ renderSummaryCards();
+ renderLotsGrid();
+ }
+
+ function renderSummaryCards() {
+ var stats = Store.getOverallStats();
+ var container = document.getElementById('summary-cards');
+ container.innerHTML = [
+ summaryCard(stats.totalOccupied.toLocaleString(), 'Total Head On Feed', ''),
+ summaryCard(stats.totalAvailable.toLocaleString(), 'Available Openings', 'card-info'),
+ summaryCard(stats.active.toLocaleString(), 'Active', ''),
+ summaryCard(stats.processing.toLocaleString(), 'Processing', 'card-warning'),
+ summaryCard(stats.deceased.toLocaleString(), 'Deceased', 'card-danger'),
+ summaryCard(stats.medical.toLocaleString(), 'Medical Care', 'card-medical'),
+ summaryCard(stats.pregnant.toLocaleString(), 'Pregnant', 'card-pregnant')
+ ].join('');
+ }
+
+ function summaryCard(value, label, extraClass) {
+ return '';
+ }
+
+ function renderLotsGrid() {
+ var container = document.getElementById('lots-grid');
+ var lots = Store.getLots();
+ var html = '';
+ lots.forEach(function (lot) {
+ var stats = Store.getLotStats(lot.id);
+ var pct = (stats.total / lot.capacity) * 100;
+ var fillClass = pct >= 95 ? 'full' : pct >= 80 ? 'high' : '';
+
+ html += '
' +
+ '' +
+ '
' +
+ '
' +
+ '
' +
+ lotStatItem('Active', stats.active) +
+ lotStatItem('Available', stats.available) +
+ lotStatItem('Medical', stats.medical) +
+ lotStatItem('Pregnant', stats.pregnant) +
+ lotStatItem('Processing', stats.processing) +
+ lotStatItem('Deceased', stats.deceased) +
+ '
';
+ });
+ container.innerHTML = html;
+
+ // Click handler to navigate to lot detail
+ container.querySelectorAll('.lot-card').forEach(function (card) {
+ card.addEventListener('click', function () {
+ var lotId = parseInt(this.getAttribute('data-lot-id'), 10);
+ document.getElementById('lot-select').value = lotId;
+ switchView('lots');
+ });
+ });
+ }
+
+ function lotStatItem(label, value) {
+ return '
' + label + '' + value + '
';
+ }
+
+ // --- Lot Detail ---
+ function renderLotDetail() {
+ var lotId = parseInt(document.getElementById('lot-select').value, 10) || 1;
+ var stats = Store.getLotStats(lotId);
+ var cattle = Store.getCattleByLot(lotId);
+ var lot = Store.getLots().find(function (l) { return l.id === lotId; });
+ var pct = ((stats.total / lot.capacity) * 100).toFixed(1);
+
+ var content = document.getElementById('lot-detail-content');
+
+ var headerHtml = '';
+
+ // Cattle in this lot that are still occupying space
+ var activeCattle = cattle.filter(function (c) {
+ return c.status === 'active' || c.status === 'medical' || c.status === 'pregnant';
+ });
+
+ var tableHtml = buildCattleTable(activeCattle, true);
+
+ content.innerHTML = headerHtml + tableHtml;
+ attachTableActions(content);
+ }
+
+ function lotDetailStat(value, label) {
+ return '
' + value + '
' + label + '
';
+ }
+
+ // --- All Cattle Table ---
+ function renderCattleTable() {
+ var cattle = Store.getAllCattle();
+ var search = document.getElementById('cattle-search').value.toLowerCase();
+ var statusFilter = document.getElementById('cattle-status-filter').value;
+ var lotFilter = document.getElementById('cattle-lot-filter').value;
+
+ if (search) {
+ cattle = cattle.filter(function (c) {
+ return c.tagNumber.toLowerCase().indexOf(search) !== -1 ||
+ c.breed.toLowerCase().indexOf(search) !== -1 ||
+ ('lot ' + c.lotId).indexOf(search) !== -1;
+ });
+ }
+
+ if (statusFilter !== 'all') {
+ cattle = cattle.filter(function (c) { return c.status === statusFilter; });
+ }
+
+ if (lotFilter !== 'all') {
+ cattle = cattle.filter(function (c) { return c.lotId === parseInt(lotFilter, 10); });
+ }
+
+ // Sort by lot, then tag
+ cattle.sort(function (a, b) {
+ if (a.lotId !== b.lotId) return a.lotId - b.lotId;
+ return a.tagNumber.localeCompare(b.tagNumber);
+ });
+
+ var container = document.getElementById('cattle-table-container');
+ container.innerHTML = buildCattleTable(cattle, false);
+ attachTableActions(container);
+ }
+
+ function buildCattleTable(cattle, hideLotColumn) {
+ if (cattle.length === 0) {
+ return '
';
+ }
+
+ var html = '
';
+ html += '| Tag # | ';
+ if (!hideLotColumn) html += 'Lot | ';
+ html += 'Breed | Weight (lbs) | Status | Days on Feed | Date On Feed | Actions | ';
+ html += '
';
+
+ cattle.forEach(function (c) {
+ var dof = Store.getDaysOnFeed(c.dateAdded);
+ html += '';
+ html += '| ' + escapeHtml(c.tagNumber) + ' | ';
+ if (!hideLotColumn) html += 'Lot ' + c.lotId + ' | ';
+ html += '' + escapeHtml(c.breed || '-') + ' | ';
+ html += '' + (c.weight ? c.weight.toLocaleString() : '-') + ' | ';
+ html += '' + c.status + ' | ';
+ html += '' + dof + ' days | ';
+ html += '' + formatDate(c.dateAdded) + ' | ';
+ html += '';
+ html += ' ';
+ html += ' ';
+ html += '';
+ html += ' |
';
+ });
+
+ html += '
';
+ return html;
+ }
+
+ function attachTableActions(container) {
+ container.querySelectorAll('.btn-edit').forEach(function (btn) {
+ btn.addEventListener('click', function (e) {
+ e.stopPropagation();
+ openEditCattle(this.getAttribute('data-id'));
+ });
+ });
+ container.querySelectorAll('.btn-status').forEach(function (btn) {
+ btn.addEventListener('click', function (e) {
+ e.stopPropagation();
+ openChangeStatus(this.getAttribute('data-id'));
+ });
+ });
+ container.querySelectorAll('.btn-move').forEach(function (btn) {
+ btn.addEventListener('click', function (e) {
+ e.stopPropagation();
+ openMoveCattle(this.getAttribute('data-id'));
+ });
+ });
+ }
+
+ // --- Alerts View ---
+ function renderAlerts() {
+ var alerts = Store.getAlerts();
+ var container = document.getElementById('alerts-list');
+
+ if (alerts.length === 0) {
+ container.innerHTML = '
';
+ return;
+ }
+
+ var html = '';
+ alerts.forEach(function (alert) {
+ var unread = !alert.read ? 'unread' : '';
+ var typeClass = 'alert-' + alert.type;
+ html += '
';
+ html += '
' + AlertSystem.getIcon(alert.type) + '';
+ html += '
';
+ html += '
' + escapeHtml(alert.message) + '
';
+ html += '
' + AlertSystem.formatTime(alert.timestamp) + '
';
+ html += '
';
+ if (!alert.read) {
+ html += '
';
+ }
+ html += '
';
+ });
+
+ container.innerHTML = html;
+
+ container.querySelectorAll('.btn-mark-read').forEach(function (btn) {
+ btn.addEventListener('click', function () {
+ Store.markAlertRead(this.getAttribute('data-alert-id'));
+ renderAlerts();
+ AlertSystem.updateBadge();
+ });
+ });
+ }
+
+ // --- Reports View ---
+ function renderReports() {
+ renderCapacityReport();
+ renderStatusReport();
+ renderDaysOnFeedReport();
+ }
+
+ function renderCapacityReport() {
+ var table = document.getElementById('capacity-report');
+ var lots = Store.getLots();
+ var html = '
| Lot | Capacity | Occupied | Available | % Full |
';
+ var totalOccupied = 0;
+ var totalAvailable = 0;
+ var totalCapacity = 0;
+
+ lots.forEach(function (lot) {
+ var stats = Store.getLotStats(lot.id);
+ var pct = ((stats.total / lot.capacity) * 100).toFixed(1);
+ totalOccupied += stats.total;
+ totalAvailable += stats.available;
+ totalCapacity += lot.capacity;
+ html += '| ' + lot.name + ' | ' + lot.capacity + ' | ' + stats.total + ' | ' + stats.available + ' | ' + pct + '% |
';
+ });
+
+ var totalPct = ((totalOccupied / totalCapacity) * 100).toFixed(1);
+ html += '| TOTAL | ' + totalCapacity.toLocaleString() + ' | ' + totalOccupied.toLocaleString() + ' | ' + totalAvailable.toLocaleString() + ' | ' + totalPct + '% |
';
+ table.innerHTML = html;
+ }
+
+ function renderStatusReport() {
+ var table = document.getElementById('status-report');
+ var lots = Store.getLots();
+ var html = '
| Lot | Active | Medical | Pregnant | Processing | Deceased |
';
+ var totals = { active: 0, medical: 0, pregnant: 0, processing: 0, deceased: 0 };
+
+ lots.forEach(function (lot) {
+ var stats = Store.getLotStats(lot.id);
+ totals.active += stats.active;
+ totals.medical += stats.medical;
+ totals.pregnant += stats.pregnant;
+ totals.processing += stats.processing;
+ totals.deceased += stats.deceased;
+ html += '| ' + lot.name + ' | ' + stats.active + ' | ' + stats.medical + ' | ' + stats.pregnant + ' | ' + stats.processing + ' | ' + stats.deceased + ' |
';
+ });
+
+ html += '| TOTAL | ' + totals.active + ' | ' + totals.medical + ' | ' + totals.pregnant + ' | ' + totals.processing + ' | ' + totals.deceased + ' |
';
+ table.innerHTML = html;
+ }
+
+ function renderDaysOnFeedReport() {
+ var table = document.getElementById('dof-report');
+ var cattle = Store.getAllCattle().filter(function (c) {
+ return c.status === 'active' || c.status === 'medical' || c.status === 'pregnant';
+ });
+
+ var buckets = {
+ '0-30 days': 0,
+ '31-60 days': 0,
+ '61-90 days': 0,
+ '91-120 days': 0,
+ '121-150 days': 0,
+ '151-180 days': 0,
+ '180+ days': 0
+ };
+
+ cattle.forEach(function (c) {
+ var dof = Store.getDaysOnFeed(c.dateAdded);
+ if (dof <= 30) buckets['0-30 days']++;
+ else if (dof <= 60) buckets['31-60 days']++;
+ else if (dof <= 90) buckets['61-90 days']++;
+ else if (dof <= 120) buckets['91-120 days']++;
+ else if (dof <= 150) buckets['121-150 days']++;
+ else if (dof <= 180) buckets['151-180 days']++;
+ else buckets['180+ days']++;
+ });
+
+ var html = '
| Days on Feed | Head Count | % of Active Herd |
';
+ var total = cattle.length;
+
+ Object.keys(buckets).forEach(function (key) {
+ var count = buckets[key];
+ var pct = total > 0 ? ((count / total) * 100).toFixed(1) : '0.0';
+ html += '| ' + key + ' | ' + count + ' | ' + pct + '% |
';
+ });
+
+ html += '| TOTAL | ' + total + ' | 100% |
';
+ table.innerHTML = html;
+ }
+
+ // --- Modals ---
+ function initModals() {
+ // Close modal on X or Cancel
+ document.querySelectorAll('.modal-close, .modal-cancel').forEach(function (btn) {
+ btn.addEventListener('click', function () {
+ this.closest('.modal').classList.add('hidden');
+ });
+ });
+
+ // Close modal on backdrop click
+ document.querySelectorAll('.modal').forEach(function (modal) {
+ modal.addEventListener('click', function (e) {
+ if (e.target === this) this.classList.add('hidden');
+ });
+ });
+ }
+
+ function openAddCattle() {
+ document.getElementById('modal-cattle-title').textContent = 'Add Cattle';
+ var form = document.getElementById('form-cattle');
+ form.reset();
+ document.getElementById('cattle-id').value = '';
+ document.getElementById('cattle-date-added').value = new Date().toISOString().split('T')[0];
+ populateFormLotDropdown('cattle-lot');
+ document.getElementById('modal-cattle').classList.remove('hidden');
+ }
+
+ function openEditCattle(id) {
+ var animal = Store.getCattleById(id);
+ if (!animal) return;
+
+ document.getElementById('modal-cattle-title').textContent = 'Edit Cattle - ' + animal.tagNumber;
+ document.getElementById('cattle-id').value = animal.id;
+ document.getElementById('cattle-tag').value = animal.tagNumber;
+ document.getElementById('cattle-breed').value = animal.breed || '';
+ document.getElementById('cattle-weight').value = animal.weight || '';
+ document.getElementById('cattle-date-added').value = animal.dateAdded;
+ document.getElementById('cattle-status').value = animal.status;
+ document.getElementById('cattle-notes').value = animal.notes || '';
+ populateFormLotDropdown('cattle-lot');
+ document.getElementById('cattle-lot').value = animal.lotId;
+ document.getElementById('modal-cattle').classList.remove('hidden');
+ }
+
+ function openChangeStatus(id) {
+ var animal = Store.getCattleById(id);
+ if (!animal) return;
+
+ document.getElementById('status-cattle-id').value = animal.id;
+ document.getElementById('status-cattle-info').textContent = 'Tag #' + animal.tagNumber + ' in Lot ' + animal.lotId + ' (currently ' + animal.status + ')';
+ document.getElementById('new-status').value = animal.status;
+ document.getElementById('status-notes').value = '';
+ document.getElementById('modal-status').classList.remove('hidden');
+ }
+
+ function openMoveCattle(id) {
+ var animal = Store.getCattleById(id);
+ if (!animal) return;
+
+ document.getElementById('move-cattle-id').value = animal.id;
+ document.getElementById('move-cattle-info').textContent = 'Tag #' + animal.tagNumber + ' currently in Lot ' + animal.lotId;
+
+ var select = document.getElementById('move-target-lot');
+ select.innerHTML = '';
+ Store.getLots().forEach(function (lot) {
+ if (lot.id !== animal.lotId) {
+ var stats = Store.getLotStats(lot.id);
+ var opt = document.createElement('option');
+ opt.value = lot.id;
+ opt.textContent = lot.name + ' (' + stats.available + ' available)';
+ select.appendChild(opt);
+ }
+ });
+
+ document.getElementById('modal-move').classList.remove('hidden');
+ }
+
+ // --- Event Handlers ---
+ function initEventHandlers() {
+ // Add cattle button
+ document.getElementById('btn-add-cattle').addEventListener('click', openAddCattle);
+
+ // Alerts button navigates to alerts view
+ document.getElementById('btn-alerts').addEventListener('click', function () {
+ switchView('alerts');
+ });
+
+ // Add/Edit cattle form
+ document.getElementById('form-cattle').addEventListener('submit', function (e) {
+ e.preventDefault();
+ var id = document.getElementById('cattle-id').value;
+ var data = {
+ tagNumber: document.getElementById('cattle-tag').value,
+ lotId: document.getElementById('cattle-lot').value,
+ breed: document.getElementById('cattle-breed').value,
+ weight: document.getElementById('cattle-weight').value,
+ dateAdded: document.getElementById('cattle-date-added').value,
+ status: document.getElementById('cattle-status').value,
+ notes: document.getElementById('cattle-notes').value
+ };
+
+ if (id) {
+ // Edit existing
+ var oldAnimal = Store.getCattleById(id);
+ var oldStatus = oldAnimal.status;
+ Store.updateCattle(id, data);
+ if (data.status !== oldStatus) {
+ var updated = Store.getCattleById(id);
+ AlertSystem.onStatusChange(updated, oldStatus, data.status);
+ }
+ } else {
+ // Add new
+ var lotStats = Store.getLotStats(parseInt(data.lotId, 10));
+ if (lotStats.available <= 0) {
+ AlertSystem.showToast('Lot ' + data.lotId + ' is FULL. Cannot add cattle.', 'death');
+ return;
+ }
+ var newAnimal = Store.addCattle(data);
+ if (data.status !== 'active') {
+ AlertSystem.onStatusChange(newAnimal, 'new', data.status);
+ }
+ AlertSystem.checkCapacity(parseInt(data.lotId, 10));
+ }
+
+ document.getElementById('modal-cattle').classList.add('hidden');
+ refreshCurrentView();
+ });
+
+ // Change status form
+ document.getElementById('form-status').addEventListener('submit', function (e) {
+ e.preventDefault();
+ var id = document.getElementById('status-cattle-id').value;
+ var newStatus = document.getElementById('new-status').value;
+ var notes = document.getElementById('status-notes').value;
+
+ var result = Store.changeStatus(id, newStatus, notes);
+ if (result) {
+ AlertSystem.onStatusChange(result.animal, result.oldStatus, result.newStatus);
+ AlertSystem.checkCapacity(result.animal.lotId);
+ }
+
+ document.getElementById('modal-status').classList.add('hidden');
+ refreshCurrentView();
+ });
+
+ // Move cattle form
+ document.getElementById('form-move').addEventListener('submit', function (e) {
+ e.preventDefault();
+ var id = document.getElementById('move-cattle-id').value;
+ var newLotId = document.getElementById('move-target-lot').value;
+
+ var result = Store.moveCattle(id, newLotId);
+ if (result) {
+ AlertSystem.onCattleMoved(result.animal, result.oldLotId, result.newLotId);
+ AlertSystem.checkCapacity(result.newLotId);
+ }
+
+ document.getElementById('modal-move').classList.add('hidden');
+ refreshCurrentView();
+ });
+
+ // Mark all alerts read
+ document.getElementById('btn-mark-all-read').addEventListener('click', function () {
+ Store.markAllAlertsRead();
+ renderAlerts();
+ AlertSystem.updateBadge();
+ });
+
+ // Clear all alerts
+ document.getElementById('btn-clear-alerts').addEventListener('click', function () {
+ if (confirm('Clear all alerts? This cannot be undone.')) {
+ Store.clearAlerts();
+ renderAlerts();
+ AlertSystem.updateBadge();
+ }
+ });
+
+ // Lot selector change
+ document.getElementById('lot-select').addEventListener('change', renderLotDetail);
+
+ // Cattle search and filters
+ document.getElementById('cattle-search').addEventListener('input', debounce(renderCattleTable, 300));
+ document.getElementById('cattle-status-filter').addEventListener('change', renderCattleTable);
+ document.getElementById('cattle-lot-filter').addEventListener('change', renderCattleTable);
+ }
+
+ // --- Helpers ---
+ function populateLotDropdowns() {
+ var lots = Store.getLots();
+
+ // Lot select in Lot Detail view
+ var lotSelect = document.getElementById('lot-select');
+ lotSelect.innerHTML = '';
+ lots.forEach(function (lot) {
+ var opt = document.createElement('option');
+ opt.value = lot.id;
+ opt.textContent = lot.name;
+ lotSelect.appendChild(opt);
+ });
+
+ // Lot filter in All Cattle view
+ var lotFilter = document.getElementById('cattle-lot-filter');
+ var firstOpt = lotFilter.querySelector('option[value="all"]');
+ lotFilter.innerHTML = '';
+ lotFilter.appendChild(firstOpt || createOption('all', 'All Lots'));
+ lots.forEach(function (lot) {
+ lotFilter.appendChild(createOption(lot.id, lot.name));
+ });
+ }
+
+ function populateFormLotDropdown(selectId) {
+ var select = document.getElementById(selectId);
+ select.innerHTML = '';
+ Store.getLots().forEach(function (lot) {
+ var stats = Store.getLotStats(lot.id);
+ var opt = document.createElement('option');
+ opt.value = lot.id;
+ opt.textContent = lot.name + ' (' + stats.available + ' available)';
+ select.appendChild(opt);
+ });
+ }
+
+ function createOption(value, text) {
+ var opt = document.createElement('option');
+ opt.value = value;
+ opt.textContent = text;
+ return opt;
+ }
+
+ function refreshCurrentView() {
+ var activeTab = document.querySelector('.nav-tab.active');
+ if (activeTab) {
+ switchView(activeTab.getAttribute('data-view'));
+ }
+ AlertSystem.updateBadge();
+ }
+
+ function escapeHtml(str) {
+ var div = document.createElement('div');
+ div.textContent = str;
+ return div.innerHTML;
+ }
+
+ function formatDate(dateStr) {
+ if (!dateStr) return '-';
+ var d = new Date(dateStr + 'T00:00:00');
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
+ }
+
+ function debounce(fn, delay) {
+ var timer;
+ return function () {
+ clearTimeout(timer);
+ timer = setTimeout(fn, delay);
+ };
+ }
+})();
diff --git a/js/store.js b/js/store.js
new file mode 100644
index 0000000..cb9cc95
--- /dev/null
+++ b/js/store.js
@@ -0,0 +1,346 @@
+/**
+ * Cattle Feed Lot Monitor - Data Store
+ * Manages all data persistence using localStorage.
+ */
+const Store = (function () {
+ const LOTS_KEY = 'cflm_lots';
+ const CATTLE_KEY = 'cflm_cattle';
+ const ALERTS_KEY = 'cflm_alerts';
+ const INITIALIZED_KEY = 'cflm_initialized';
+
+ const LOT_COUNT = 10;
+ const LOT_CAPACITY = 500;
+
+ // --- Helpers ---
+ function generateId() {
+ return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
+ }
+
+ function save(key, data) {
+ localStorage.setItem(key, JSON.stringify(data));
+ }
+
+ function load(key) {
+ const raw = localStorage.getItem(key);
+ return raw ? JSON.parse(raw) : null;
+ }
+
+ // --- Lots ---
+ function getDefaultLots() {
+ const lots = [];
+ for (let i = 1; i <= LOT_COUNT; i++) {
+ lots.push({
+ id: i,
+ name: 'Lot ' + i,
+ capacity: LOT_CAPACITY
+ });
+ }
+ return lots;
+ }
+
+ function getLots() {
+ return load(LOTS_KEY) || getDefaultLots();
+ }
+
+ function saveLots(lots) {
+ save(LOTS_KEY, lots);
+ }
+
+ // --- Cattle ---
+ function getAllCattle() {
+ return load(CATTLE_KEY) || [];
+ }
+
+ function saveCattle(cattle) {
+ save(CATTLE_KEY, cattle);
+ }
+
+ function getCattleById(id) {
+ return getAllCattle().find(c => c.id === id) || null;
+ }
+
+ function getCattleByLot(lotId) {
+ return getAllCattle().filter(c => c.lotId === lotId);
+ }
+
+ function getActiveCattleByLot(lotId) {
+ return getAllCattle().filter(c =>
+ c.lotId === lotId && (c.status === 'active' || c.status === 'medical' || c.status === 'pregnant')
+ );
+ }
+
+ function addCattle(data) {
+ const cattle = getAllCattle();
+ const record = {
+ id: generateId(),
+ tagNumber: data.tagNumber.trim(),
+ lotId: parseInt(data.lotId, 10),
+ breed: (data.breed || '').trim(),
+ weight: data.weight ? parseInt(data.weight, 10) : null,
+ dateAdded: data.dateAdded,
+ status: data.status || 'active',
+ statusDate: new Date().toISOString(),
+ notes: (data.notes || '').trim(),
+ history: [{
+ date: new Date().toISOString(),
+ action: 'added',
+ status: data.status || 'active',
+ notes: 'Entered lot'
+ }]
+ };
+ cattle.push(record);
+ saveCattle(cattle);
+ return record;
+ }
+
+ function updateCattle(id, updates) {
+ const cattle = getAllCattle();
+ const idx = cattle.findIndex(c => c.id === id);
+ if (idx === -1) return null;
+ Object.assign(cattle[idx], updates);
+ saveCattle(cattle);
+ return cattle[idx];
+ }
+
+ function changeStatus(id, newStatus, notes) {
+ const cattle = getAllCattle();
+ const idx = cattle.findIndex(c => c.id === id);
+ if (idx === -1) return null;
+
+ const oldStatus = cattle[idx].status;
+ cattle[idx].status = newStatus;
+ cattle[idx].statusDate = new Date().toISOString();
+ if (notes) cattle[idx].notes = notes;
+
+ cattle[idx].history = cattle[idx].history || [];
+ cattle[idx].history.push({
+ date: new Date().toISOString(),
+ action: 'status_change',
+ from: oldStatus,
+ to: newStatus,
+ notes: notes || ''
+ });
+
+ saveCattle(cattle);
+ return { animal: cattle[idx], oldStatus, newStatus };
+ }
+
+ function moveCattle(id, newLotId) {
+ const cattle = getAllCattle();
+ const idx = cattle.findIndex(c => c.id === id);
+ if (idx === -1) return null;
+
+ const oldLotId = cattle[idx].lotId;
+ cattle[idx].lotId = parseInt(newLotId, 10);
+
+ cattle[idx].history = cattle[idx].history || [];
+ cattle[idx].history.push({
+ date: new Date().toISOString(),
+ action: 'moved',
+ from: 'Lot ' + oldLotId,
+ to: 'Lot ' + newLotId,
+ notes: ''
+ });
+
+ saveCattle(cattle);
+ return { animal: cattle[idx], oldLotId, newLotId: parseInt(newLotId, 10) };
+ }
+
+ function deleteCattle(id) {
+ const cattle = getAllCattle().filter(c => c.id !== id);
+ saveCattle(cattle);
+ }
+
+ // --- Alerts ---
+ function getAlerts() {
+ return load(ALERTS_KEY) || [];
+ }
+
+ function saveAlerts(alerts) {
+ save(ALERTS_KEY, alerts);
+ }
+
+ function addAlert(alert) {
+ const alerts = getAlerts();
+ const record = {
+ id: generateId(),
+ type: alert.type,
+ lotId: alert.lotId || null,
+ cattleId: alert.cattleId || null,
+ tagNumber: alert.tagNumber || '',
+ message: alert.message,
+ timestamp: new Date().toISOString(),
+ read: false
+ };
+ alerts.unshift(record);
+ saveAlerts(alerts);
+ return record;
+ }
+
+ function markAlertRead(id) {
+ const alerts = getAlerts();
+ const alert = alerts.find(a => a.id === id);
+ if (alert) {
+ alert.read = true;
+ saveAlerts(alerts);
+ }
+ }
+
+ function markAllAlertsRead() {
+ const alerts = getAlerts();
+ alerts.forEach(a => a.read = true);
+ saveAlerts(alerts);
+ }
+
+ function clearAlerts() {
+ saveAlerts([]);
+ }
+
+ function getUnreadCount() {
+ return getAlerts().filter(a => !a.read).length;
+ }
+
+ // --- Statistics ---
+ function getLotStats(lotId) {
+ const cattle = getCattleByLot(lotId);
+ const occupying = cattle.filter(c =>
+ c.status === 'active' || c.status === 'medical' || c.status === 'pregnant'
+ );
+ return {
+ total: occupying.length,
+ available: LOT_CAPACITY - occupying.length,
+ active: cattle.filter(c => c.status === 'active').length,
+ processing: cattle.filter(c => c.status === 'processing').length,
+ deceased: cattle.filter(c => c.status === 'deceased').length,
+ medical: cattle.filter(c => c.status === 'medical').length,
+ pregnant: cattle.filter(c => c.status === 'pregnant').length
+ };
+ }
+
+ function getOverallStats() {
+ const cattle = getAllCattle();
+ const occupying = cattle.filter(c =>
+ c.status === 'active' || c.status === 'medical' || c.status === 'pregnant'
+ );
+ return {
+ totalCapacity: LOT_COUNT * LOT_CAPACITY,
+ totalOccupied: occupying.length,
+ totalAvailable: (LOT_COUNT * LOT_CAPACITY) - occupying.length,
+ active: cattle.filter(c => c.status === 'active').length,
+ processing: cattle.filter(c => c.status === 'processing').length,
+ deceased: cattle.filter(c => c.status === 'deceased').length,
+ medical: cattle.filter(c => c.status === 'medical').length,
+ pregnant: cattle.filter(c => c.status === 'pregnant').length,
+ totalCattle: cattle.length
+ };
+ }
+
+ function getDaysOnFeed(dateAdded) {
+ const start = new Date(dateAdded);
+ const now = new Date();
+ const diff = now - start;
+ return Math.floor(diff / (1000 * 60 * 60 * 24));
+ }
+
+ // --- Initialization / Seed Data ---
+ function isInitialized() {
+ return localStorage.getItem(INITIALIZED_KEY) === 'true';
+ }
+
+ function markInitialized() {
+ localStorage.setItem(INITIALIZED_KEY, 'true');
+ }
+
+ function resetAll() {
+ localStorage.removeItem(LOTS_KEY);
+ localStorage.removeItem(CATTLE_KEY);
+ localStorage.removeItem(ALERTS_KEY);
+ localStorage.removeItem(INITIALIZED_KEY);
+ }
+
+ function seedDemoData() {
+ const lots = getDefaultLots();
+ saveLots(lots);
+
+ const breeds = ['Angus', 'Hereford', 'Charolais', 'Simmental', 'Limousin', 'Red Angus', 'Brahman', 'Shorthorn'];
+ const cattle = [];
+ const now = new Date();
+
+ for (let lotId = 1; lotId <= LOT_COUNT; lotId++) {
+ const headCount = 40 + Math.floor(Math.random() * 80); // 40-119 per lot
+ for (let j = 0; j < headCount; j++) {
+ const daysAgo = Math.floor(Math.random() * 180) + 10; // 10-190 days ago
+ const dateAdded = new Date(now.getTime() - daysAgo * 86400000);
+ const breed = breeds[Math.floor(Math.random() * breeds.length)];
+ const weight = 600 + Math.floor(Math.random() * 700); // 600-1300 lbs
+
+ // Mostly active, some with other statuses
+ let status = 'active';
+ const roll = Math.random();
+ if (roll < 0.03) status = 'medical';
+ else if (roll < 0.06) status = 'pregnant';
+ else if (roll < 0.08) status = 'processing';
+ else if (roll < 0.09) status = 'deceased';
+
+ const tagNum = 'T' + String(lotId).padStart(2, '0') + '-' + String(j + 1).padStart(4, '0');
+
+ cattle.push({
+ id: generateId(),
+ tagNumber: tagNum,
+ lotId: lotId,
+ breed: breed,
+ weight: weight,
+ dateAdded: dateAdded.toISOString().split('T')[0],
+ status: status,
+ statusDate: dateAdded.toISOString(),
+ notes: '',
+ history: [{
+ date: dateAdded.toISOString(),
+ action: 'added',
+ status: status,
+ notes: 'Initial entry'
+ }]
+ });
+ }
+ }
+
+ saveCattle(cattle);
+
+ // Seed a few alerts
+ const alerts = [
+ { type: 'processing', lotId: 2, tagNumber: cattle.find(c => c.lotId === 2 && c.status === 'processing')?.tagNumber || 'T02-0005', message: 'Head moved to processing from Lot 2', timestamp: new Date(now - 3600000).toISOString(), read: false, id: generateId() },
+ { type: 'medical', lotId: 5, tagNumber: cattle.find(c => c.lotId === 5 && c.status === 'medical')?.tagNumber || 'T05-0012', message: 'Head placed in medical care in Lot 5', timestamp: new Date(now - 7200000).toISOString(), read: false, id: generateId() },
+ { type: 'pregnancy', lotId: 3, tagNumber: cattle.find(c => c.lotId === 3 && c.status === 'pregnant')?.tagNumber || 'T03-0008', message: 'Pregnancy confirmed for head in Lot 3', timestamp: new Date(now - 14400000).toISOString(), read: true, id: generateId() },
+ ];
+ saveAlerts(alerts);
+ markInitialized();
+ }
+
+ return {
+ LOT_COUNT,
+ LOT_CAPACITY,
+ getLots,
+ getAllCattle,
+ getCattleById,
+ getCattleByLot,
+ getActiveCattleByLot,
+ addCattle,
+ updateCattle,
+ changeStatus,
+ moveCattle,
+ deleteCattle,
+ getAlerts,
+ addAlert,
+ markAlertRead,
+ markAllAlertsRead,
+ clearAlerts,
+ getUnreadCount,
+ getLotStats,
+ getOverallStats,
+ getDaysOnFeed,
+ isInitialized,
+ seedDemoData,
+ resetAll,
+ generateId
+ };
+})();