From 999b602c6b7acf89cef61da74c9cf69ed4ad2a13 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Feb 2026 16:41:33 +0000 Subject: [PATCH 1/2] Add cattle feed lot monitoring system Full-featured web application for managing 10 feed lots (500 head capacity each) with real-time tracking of cattle status, alerts, and reporting. Features: - Dashboard with summary cards and visual lot capacity bars - Individual lot detail views with occupancy stats - Cattle management: add, edit, move between lots, change status - Status tracking: active, processing, deceased, medical care, pregnant - Alert system with toast notifications for status changes, deaths, processing moves, medical care, pregnancy, and capacity warnings - Days-on-feed tracking per head with distribution reports - Searchable/filterable cattle table across all lots - Capacity, status breakdown, and days-on-feed reports - Data persistence via localStorage - Demo data seeded for all 10 lots on first load - Responsive design for desktop and mobile https://claude.ai/code/session_01GsMe6FU1te6H5zU6m9Gj9H --- css/styles.css | 772 +++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 222 +++++++++++++- js/alerts.js | 189 ++++++++++++ js/app.js | 624 +++++++++++++++++++++++++++++++++++++++ js/store.js | 346 ++++++++++++++++++++++ 5 files changed, 2140 insertions(+), 13 deletions(-) create mode 100644 css/styles.css create mode 100644 js/alerts.js create mode 100644 js/app.js create mode 100644 js/store.js diff --git a/css/styles.css b/css/styles.css new file mode 100644 index 0000000..003e397 --- /dev/null +++ b/css/styles.css @@ -0,0 +1,772 @@ +/* === Reset & Base === */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + --color-bg: #f4f6f9; + --color-surface: #ffffff; + --color-primary: #2c6e49; + --color-primary-light: #4c956c; + --color-primary-dark: #1b4332; + --color-accent: #d4a843; + --color-danger: #c0392b; + --color-warning: #e67e22; + --color-info: #2980b9; + --color-medical: #8e44ad; + --color-pregnant: #e91e90; + --color-text: #2d3436; + --color-text-light: #636e72; + --color-border: #dfe6e9; + --radius: 8px; + --shadow: 0 2px 8px rgba(0,0,0,0.08); + --shadow-lg: 0 4px 16px rgba(0,0,0,0.12); + --transition: 0.2s ease; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + background: var(--color-bg); + color: var(--color-text); + line-height: 1.5; + min-height: 100vh; +} + +/* === Header === */ +header { + background: var(--color-primary-dark); + color: white; + padding: 16px 24px; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: var(--shadow); +} + +header h1 { + font-size: 1.4rem; + font-weight: 700; + letter-spacing: -0.02em; +} + +.subtitle { + font-size: 0.85rem; + opacity: 0.8; + margin-left: 4px; +} + +.header-right { + display: flex; + align-items: center; + gap: 12px; +} + +/* === Navigation === */ +nav#main-nav { + background: var(--color-surface); + border-bottom: 1px solid var(--color-border); + display: flex; + gap: 0; + padding: 0 24px; + overflow-x: auto; +} + +.nav-tab { + padding: 12px 20px; + border: none; + background: none; + font-size: 0.9rem; + font-weight: 500; + color: var(--color-text-light); + cursor: pointer; + border-bottom: 3px solid transparent; + transition: var(--transition); + white-space: nowrap; +} + +.nav-tab:hover { + color: var(--color-primary); + background: rgba(44, 110, 73, 0.05); +} + +.nav-tab.active { + color: var(--color-primary); + border-bottom-color: var(--color-primary); +} + +/* === Buttons === */ +.btn { + padding: 8px 16px; + border: 1px solid var(--color-border); + border-radius: var(--radius); + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: var(--transition); + background: var(--color-surface); + color: var(--color-text); + display: inline-flex; + align-items: center; + gap: 6px; +} + +.btn:hover { + box-shadow: var(--shadow); +} + +.btn-primary { + background: var(--color-primary); + color: white; + border-color: var(--color-primary); +} + +.btn-primary:hover { + background: var(--color-primary-light); +} + +.btn-secondary { + background: var(--color-surface); + color: var(--color-text); +} + +.btn-danger { + background: var(--color-danger); + color: white; + border-color: var(--color-danger); +} + +.btn-sm { + padding: 4px 10px; + font-size: 0.8rem; +} + +.btn-icon { + background: transparent; + border: none; + color: white; + font-size: 1.2rem; + position: relative; + padding: 6px; + cursor: pointer; +} + +.badge { + position: absolute; + top: -2px; + right: -4px; + background: var(--color-danger); + color: white; + font-size: 0.7rem; + font-weight: 700; + padding: 1px 5px; + border-radius: 10px; + min-width: 18px; + text-align: center; +} + +.hidden { + display: none !important; +} + +/* === Views === */ +.view { + display: none; + padding: 24px; + max-width: 1400px; + margin: 0 auto; +} + +.view.active { + display: block; +} + +/* === Summary Cards === */ +.summary-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.summary-card { + background: var(--color-surface); + border-radius: var(--radius); + padding: 20px; + box-shadow: var(--shadow); + text-align: center; + border-top: 4px solid var(--color-primary); +} + +.summary-card.card-danger { border-top-color: var(--color-danger); } +.summary-card.card-warning { border-top-color: var(--color-warning); } +.summary-card.card-info { border-top-color: var(--color-info); } +.summary-card.card-medical { border-top-color: var(--color-medical); } +.summary-card.card-pregnant { border-top-color: var(--color-pregnant); } +.summary-card.card-accent { border-top-color: var(--color-accent); } + +.summary-card .card-value { + font-size: 2rem; + font-weight: 700; + color: var(--color-primary-dark); + line-height: 1.2; +} + +.summary-card .card-label { + font-size: 0.8rem; + color: var(--color-text-light); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-top: 4px; +} + +/* === Lots Grid === */ +.lots-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 16px; +} + +.lot-card { + background: var(--color-surface); + border-radius: var(--radius); + box-shadow: var(--shadow); + overflow: hidden; + cursor: pointer; + transition: var(--transition); +} + +.lot-card:hover { + box-shadow: var(--shadow-lg); + transform: translateY(-2px); +} + +.lot-card-header { + padding: 16px 20px; + background: var(--color-primary); + color: white; + display: flex; + justify-content: space-between; + align-items: center; +} + +.lot-card-header h3 { + font-size: 1.1rem; +} + +.lot-card-header .lot-count { + font-size: 0.85rem; + opacity: 0.9; +} + +.lot-card-body { + padding: 16px 20px; +} + +.capacity-bar { + height: 12px; + background: var(--color-border); + border-radius: 6px; + overflow: hidden; + margin-bottom: 12px; +} + +.capacity-fill { + height: 100%; + border-radius: 6px; + transition: width 0.4s ease; + background: var(--color-primary-light); +} + +.capacity-fill.high { + background: var(--color-warning); +} + +.capacity-fill.full { + background: var(--color-danger); +} + +.lot-stats { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + font-size: 0.85rem; +} + +.lot-stat { + display: flex; + justify-content: space-between; + padding: 4px 0; + border-bottom: 1px solid var(--color-border); +} + +.lot-stat .stat-label { + color: var(--color-text-light); +} + +.lot-stat .stat-value { + font-weight: 600; +} + +/* === Lot Detail === */ +.lot-selector { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; +} + +.lot-selector select { + padding: 8px 12px; + border-radius: var(--radius); + border: 1px solid var(--color-border); + font-size: 0.9rem; +} + +.lot-detail-header { + background: var(--color-surface); + border-radius: var(--radius); + padding: 20px; + box-shadow: var(--shadow); + margin-bottom: 20px; +} + +.lot-detail-header h2 { + margin-bottom: 12px; +} + +.lot-detail-stats { + display: flex; + gap: 24px; + flex-wrap: wrap; +} + +.lot-detail-stat { + text-align: center; +} + +.lot-detail-stat .value { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-primary-dark); +} + +.lot-detail-stat .label { + font-size: 0.8rem; + color: var(--color-text-light); + text-transform: uppercase; +} + +/* === Tables === */ +.data-table { + width: 100%; + background: var(--color-surface); + border-radius: var(--radius); + box-shadow: var(--shadow); + border-collapse: collapse; + overflow: hidden; +} + +.data-table th { + background: var(--color-primary-dark); + color: white; + padding: 12px 16px; + text-align: left; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 600; + cursor: pointer; + user-select: none; + white-space: nowrap; +} + +.data-table th:hover { + background: var(--color-primary); +} + +.data-table td { + padding: 10px 16px; + border-bottom: 1px solid var(--color-border); + font-size: 0.875rem; +} + +.data-table tr:last-child td { + border-bottom: none; +} + +.data-table tr:hover td { + background: rgba(44, 110, 73, 0.03); +} + +/* === Status Badges === */ +.status-badge { + display: inline-block; + padding: 3px 10px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.status-active { background: #d4edda; color: #155724; } +.status-processing { background: #fff3cd; color: #856404; } +.status-deceased { background: #f8d7da; color: #721c24; } +.status-medical { background: #e8daef; color: #6c3483; } +.status-pregnant { background: #fdedfa; color: #c2185b; } + +/* === Toolbar === */ +.toolbar { + display: flex; + gap: 12px; + margin-bottom: 16px; + flex-wrap: wrap; + align-items: center; +} + +.toolbar input[type="text"] { + flex: 1; + min-width: 200px; + padding: 8px 14px; + border: 1px solid var(--color-border); + border-radius: var(--radius); + font-size: 0.9rem; +} + +.toolbar select { + padding: 8px 12px; + border: 1px solid var(--color-border); + border-radius: var(--radius); + font-size: 0.9rem; +} + +/* === Alerts === */ +.alert-item { + background: var(--color-surface); + border-radius: var(--radius); + padding: 16px 20px; + margin-bottom: 8px; + box-shadow: var(--shadow); + display: flex; + align-items: flex-start; + gap: 14px; + border-left: 4px solid var(--color-border); + transition: var(--transition); +} + +.alert-item.unread { + border-left-color: var(--color-primary); + background: #f0faf4; +} + +.alert-item.alert-processing { border-left-color: var(--color-warning); } +.alert-item.alert-processing.unread { background: #fef9f0; } +.alert-item.alert-death { border-left-color: var(--color-danger); } +.alert-item.alert-death.unread { background: #fef0f0; } +.alert-item.alert-medical { border-left-color: var(--color-medical); } +.alert-item.alert-medical.unread { background: #f8f0fe; } +.alert-item.alert-pregnancy { border-left-color: var(--color-pregnant); } +.alert-item.alert-pregnancy.unread { background: #fef0fa; } +.alert-item.alert-capacity { border-left-color: var(--color-info); } +.alert-item.alert-capacity.unread { background: #f0f6fe; } + +.alert-icon { + font-size: 1.5rem; + line-height: 1; + flex-shrink: 0; +} + +.alert-body { + flex: 1; +} + +.alert-body .alert-message { + font-size: 0.9rem; + margin-bottom: 4px; +} + +.alert-body .alert-time { + font-size: 0.75rem; + color: var(--color-text-light); +} + +.alert-actions { + flex-shrink: 0; +} + +.empty-state { + text-align: center; + padding: 60px 20px; + color: var(--color-text-light); +} + +.empty-state .empty-icon { + font-size: 3rem; + margin-bottom: 12px; +} + +.empty-state p { + font-size: 1rem; +} + +/* === Reports === */ +.report-section { + background: var(--color-surface); + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 24px; + margin-bottom: 20px; +} + +.report-section h2 { + font-size: 1.1rem; + margin-bottom: 16px; + color: var(--color-primary-dark); +} + +.report-table { + width: 100%; + border-collapse: collapse; +} + +.report-table th { + text-align: left; + padding: 10px 14px; + border-bottom: 2px solid var(--color-primary); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-light); +} + +.report-table td { + padding: 10px 14px; + border-bottom: 1px solid var(--color-border); + font-size: 0.875rem; +} + +.report-table tr:last-child td { + border-bottom: none; +} + +.report-table tfoot td { + font-weight: 700; + border-top: 2px solid var(--color-primary-dark); + color: var(--color-primary-dark); +} + +/* === Modals === */ +.modal { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 20px; +} + +.modal.hidden { + display: none !important; +} + +.modal-content { + background: var(--color-surface); + border-radius: var(--radius); + width: 100%; + max-width: 560px; + max-height: 90vh; + overflow-y: auto; + box-shadow: var(--shadow-lg); +} + +.modal-sm { + max-width: 420px; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid var(--color-border); +} + +.modal-header h2 { + font-size: 1.1rem; +} + +.modal-close { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: var(--color-text-light); + padding: 4px; + line-height: 1; +} + +.modal-close:hover { + color: var(--color-danger); +} + +/* === Forms === */ +form { + padding: 20px; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.form-group { + margin-bottom: 16px; +} + +.form-group label { + display: block; + font-size: 0.8rem; + font-weight: 600; + color: var(--color-text-light); + margin-bottom: 4px; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 8px 12px; + border: 1px solid var(--color-border); + border-radius: var(--radius); + font-size: 0.9rem; + font-family: inherit; + transition: var(--transition); +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(44, 110, 73, 0.15); +} + +.form-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 8px; +} + +/* === Toast Notifications === */ +#toast-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 2000; + display: flex; + flex-direction: column; + gap: 8px; +} + +.toast { + background: var(--color-surface); + border-radius: var(--radius); + padding: 14px 20px; + box-shadow: var(--shadow-lg); + min-width: 280px; + max-width: 400px; + border-left: 4px solid var(--color-primary); + animation: slideIn 0.3s ease; + display: flex; + align-items: center; + gap: 10px; +} + +.toast.toast-danger { border-left-color: var(--color-danger); } +.toast.toast-warning { border-left-color: var(--color-warning); } +.toast.toast-info { border-left-color: var(--color-info); } + +.toast-icon { font-size: 1.2rem; } +.toast-message { flex: 1; font-size: 0.875rem; } + +.toast-close { + background: none; + border: none; + cursor: pointer; + color: var(--color-text-light); + font-size: 1.1rem; + padding: 2px; +} + +@keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +@keyframes slideOut { + from { transform: translateX(0); opacity: 1; } + to { transform: translateX(100%); opacity: 0; } +} + +/* === Action buttons in tables === */ +.action-btns { + display: flex; + gap: 4px; +} + +.action-btns .btn-sm { + white-space: nowrap; +} + +/* === Responsive === */ +@media (max-width: 768px) { + header { + flex-direction: column; + gap: 10px; + text-align: center; + } + + .form-row { + grid-template-columns: 1fr; + } + + .lots-grid { + grid-template-columns: 1fr; + } + + .summary-cards { + grid-template-columns: repeat(2, 1fr); + } + + .toolbar { + flex-direction: column; + } + + .data-table { + display: block; + overflow-x: auto; + } +} + +@media (max-width: 480px) { + .summary-cards { + grid-template-columns: 1fr; + } + + nav#main-nav { + padding: 0 8px; + } + + .nav-tab { + padding: 10px 12px; + font-size: 0.8rem; + } +} diff --git a/index.html b/index.html index 9bc31e6..a824742 100755 --- a/index.html +++ b/index.html @@ -1,13 +1,209 @@ ---- -layout: presentation ---- - -{% for post in site.posts reversed %} - {% include slide.html %} -
-{% endfor %} -{% unless site.simple-slideshow %} -{% if site.overview %} -
-{% endif %} -{% endunless %} + + + + + + Cattle Feed Lot Monitor + + + +
+
+

Cattle Feed Lot Monitor

+ 10 Lots · 5,000 Head Capacity +
+
+ + +
+
+ + + + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+

Lot Capacity Summary

+
+
+
+

Status Breakdown

+
+
+
+

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 '
' + + '
' + value + '
' + + '
' + label + '
'; + } + + 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 += '
' + + '

' + lot.name + '

' + + '' + stats.total + ' / ' + lot.capacity + '
' + + '
' + + '
' + + '
' + + 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 = '
' + + '

' + lot.name + '

' + + '
' + + '
' + + lotDetailStat(stats.total, 'Occupied') + + lotDetailStat(stats.available, 'Available') + + lotDetailStat(pct + '%', 'Capacity') + + lotDetailStat(stats.active, 'Active') + + lotDetailStat(stats.medical, 'Medical') + + lotDetailStat(stats.pregnant, 'Pregnant') + + lotDetailStat(stats.processing, 'Processing') + + lotDetailStat(stats.deceased, 'Deceased') + + '
'; + + // 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 '
🐄

No cattle found.

'; + } + + var html = ''; + html += ''; + if (!hideLotColumn) html += ''; + html += ''; + html += ''; + + cattle.forEach(function (c) { + var dof = Store.getDaysOnFeed(c.dateAdded); + html += ''; + html += ''; + if (!hideLotColumn) html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + }); + + html += '
Tag #LotBreedWeight (lbs)StatusDays on FeedDate On FeedActions
' + escapeHtml(c.tagNumber) + 'Lot ' + c.lotId + '' + escapeHtml(c.breed || '-') + '' + (c.weight ? c.weight.toLocaleString() : '-') + '' + c.status + '' + dof + ' days' + formatDate(c.dateAdded) + ''; + 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 = '
🔔

No alerts yet.

'; + 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 = 'LotCapacityOccupiedAvailable% 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 = 'LotActiveMedicalPregnantProcessingDeceased'; + 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 FeedHead 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 + }; +})(); From 126880e24bfb262a6d6edb6a44e1162bba9d3d66 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Feb 2026 19:13:21 +0000 Subject: [PATCH 2/2] Add .nojekyll to fix GitHub Pages deployment GitHub Pages was attempting Jekyll build which conflicts with the plain HTML/CSS/JS cattle monitoring app. The .nojekyll file tells GitHub Pages to serve static files directly. https://claude.ai/code/session_01GsMe6FU1te6H5zU6m9Gj9H --- .nojekyll | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .nojekyll diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29