<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>Oyster Farm Manager</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f8f9fa;
color: #333;
overflow-x: hidden;
}
.app-container {
max-width: 800px;
margin: 0 auto;
padding-bottom: 70px;
}
/* Header */
.header {
background: linear-gradient(135deg, #2c5aa0 0%, #1e3d72 100%);
color: white;
padding: 20px;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.header h1 { font-size: 24px; margin-bottom: 4px; }
.header-subtitle { opacity: 0.9; font-size: 14px; }
/* Tabs */
.tabs {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
display: flex;
border-top: 1px solid #e9ecef;
z-index: 100;
box-shadow: 0 -2px 8px rgba(0,0,0,0.1);
}
.tab {
flex: 1;
padding: 12px;
text-align: center;
border: none;
background: white;
cursor: pointer;
font-size: 20px;
transition: all 0.2s;
}
.tab.active {
background: #e8f0fe;
border-top: 3px solid #2c5aa0;
}
.tab-label {
display: block;
font-size: 11px;
margin-top: 4px;
color: #666;
}
.tab.active .tab-label { color: #2c5aa0; font-weight: 600; }
/* Content Sections */
.content { display: none; padding: 20px; }
.content.active { display: block; }
/* Metric Cards */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.metric-card {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
border-left: 4px solid #2c5aa0;
}
.metric-label {
font-size: 11px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.metric-value {
font-size: 28px;
font-weight: 800;
color: #2c5aa0;
}
/* Panel */
.panel {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
margin-bottom: 20px;
overflow: hidden;
}
.panel-header {
background: #f8f9fa;
padding: 16px 20px;
border-bottom: 1px solid #e9ecef;
font-size: 18px;
font-weight: 700;
}
.panel-body { padding: 20px; }
/* Activity List */
.activity-item {
padding: 12px 0;
border-bottom: 1px solid #f1f3f4;
}
.activity-item:last-child { border-bottom: none; }
.activity-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.site-badge {
padding: 3px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: 800;
color: white;
}
.badge-stamford { background: #34a853; }
.badge-westport { background: #4285f4; }
.badge-pond { background: #fbbc04; color: #333; }
.activity-location { font-weight: 700; color: #2c5aa0; }
.activity-details { font-size: 14px; color: #666; }
/* Form */
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 700;
margin-bottom: 8px;
color: #333;
}
.form-input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 16px;
}
.form-input:focus {
outline: none;
border-color: #2c5aa0;
box-shadow: 0 0 0 3px rgba(44, 90, 160, 0.1);
}
textarea.form-input {
min-height: 80px;
resize: vertical;
}
/* Buttons */
.btn {
padding: 14px 20px;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
width: 100%;
margin-top: 8px;
}
.btn-primary {
background: #2c5aa0;
color: white;
}
.btn-primary:active {
background: #1e3d72;
transform: scale(0.98);
}
.btn-secondary {
background: #e9ecef;
color: #333;
}
.btn-success {
background: #34a853;
color: white;
}
.btn-gps {
background: #34a853;
color: white;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
/* Chip Selector */
.chip-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.chip {
padding: 8px 16px;
background: #e9ecef;
border: 2px solid #e9ecef;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.chip.active {
background: #2c5aa0;
color: white;
border-color: #2c5aa0;
}
/* Clone Template Section */
.template-section {
background: #fff3cd;
border: 2px solid #ffc107;
border-radius: 8px;
padding: 12px;
margin-bottom: 20px;
}
.template-title {
font-size: 14px;
font-weight: 700;
color: #856404;
margin-bottom: 8px;
}
.template-item {
background: white;
padding: 10px;
border-radius: 6px;
margin-bottom: 8px;
cursor: pointer;
border-left: 3px solid #ffc107;
}
.template-item:hover {
background: #f8f9fa;
}
/* Loading */
.loading {
text-align: center;
padding: 40px;
color: #666;
}
/* Success Message */
.success-banner {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 16px;
display: none;
}
.success-banner.show {
display: block;
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Map Placeholder */
.map-container {
width: 100%;
height: 400px;
background: #e9ecef;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #666;
font-size: 14px;
}
</style>
</head>
<body>
<div class="app-container">
<!-- Header -->
<div class="header">
<h1>Oyster Farm Manager</h1>
<div class="header-subtitle">Real-time farm operations</div>
</div>
<!-- Dashboard Content -->
<div id="dashboard" class="content active">
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-label">Total Oysters</div>
<div class="metric-value" id="totalOysters">0</div>
</div>
<div class="metric-card">
<div class="metric-label">Active Locations</div>
<div class="metric-value" id="activeLocations">0</div>
</div>
<div class="metric-card">
<div class="metric-label">Today's Activity</div>
<div class="metric-value" id="todayActivity">0</div>
</div>
</div>
<div class="panel">
<div class="panel-header">Recent Activity</div>
<div class="panel-body">
<div id="activityList" class="loading">Loading activities...</div>
</div>
</div>
</div>
<!-- Activity Form Content -->
<div id="activityForm" class="content">
<div class="success-banner" id="successBanner">
Activity logged successfully!
</div>
<div id="templateSection" class="template-section" style="display:none;">
<div class="template-title">Clone from recent:</div>
<div id="templateList"></div>
</div>
<div class="panel">
<div class="panel-body">
<div class="form-group">
<label class="form-label">Site</label>
<div class="chip-group">
<button class="chip active" data-site="Stamford" onclick="selectSite('Stamford')">Stamford</button>
<button class="chip" data-site="Westport" onclick="selectSite('Westport')">Westport</button>
<button class="chip" data-site="Pond" onclick="selectSite('Pond')">Pond</button>
</div>
</div>
<div class="form-group">
<label class="form-label">Location ID *</label>
<input type="text" id="locationId" class="form-input" placeholder="e.g., X6, ST-A-042">
</div>
<div class="form-group">
<label class="form-label">Activity</label>
<div class="chip-group">
<button class="chip active" data-activity="Deployed" onclick="selectActivity('Deployed')">Deployed</button>
<button class="chip" data-activity="Tumbled" onclick="selectActivity('Tumbled')">Tumbled</button>
<button class="chip" data-activity="Flipped" onclick="selectActivity('Flipped')">Flipped</button>
<button class="chip" data-activity="Transplanted" onclick="selectActivity('Transplanted')">Transplanted</button>
<button class="chip" data-activity="Harvested" onclick="selectActivity('Harvested')">Harvested</button>
</div>
</div>
<div class="form-group">
<label class="form-label">Oyster Count *</label>
<input type="number" id="oysterCount" class="form-input" placeholder="50000">
</div>
<div class="form-group" id="bagCountGroup" style="display:none;">
<label class="form-label">Bag Count</label>
<input type="number" id="bagCount" class="form-input" placeholder="Number of bags">
</div>
<div class="form-group">
<label class="form-label">Size Class</label>
<div class="chip-group">
<button class="chip active" data-size="Seed" onclick="selectSize('Seed')">Seed</button>
<button class="chip" data-size="Cocktail" onclick="selectSize('Cocktail')">Cocktail</button>
<button class="chip" data-size="Select" onclick="selectSize('Select')">Select</button>
<button class="chip" data-size="Choice" onclick="selectSize('Choice')">Choice</button>
<button class="chip" data-size="Market" onclick="selectSize('Market')">Market</button>
</div>
</div>
<div class="form-group">
<label class="form-label">Crop Year</label>
<div class="chip-group">
<button class="chip" data-year="2023" onclick="selectYear('2023')">2023</button>
<button class="chip" data-year="2024" onclick="selectYear('2024')">2024</button>
<button class="chip active" data-year="2025" onclick="selectYear('2025')">2025</button>
</div>
</div>
<div class="form-group">
<label class="form-label">GPS Coordinates</label>
<input type="text" id="gpsCoords" class="form-input" placeholder="Will be captured automatically" readonly>
<button class="btn btn-gps" onclick="getLocation()">
<span>📍</span> Get Current Location
</button>
</div>
<div class="form-group">
<label class="form-label">Notes</label>
<textarea id="notes" class="form-input" placeholder="Optional notes..."></textarea>
</div>
<button class="btn btn-primary" onclick="submitActivity()">Log Activity</button>
<button class="btn btn-secondary" onclick="showTemplates()">Show Recent Templates</button>
</div>
</div>
</div>
<!-- Map Content -->
<div id="map" class="content">
<div class="panel">
<div class="panel-header">Location Map</div>
<div class="panel-body">
<div class="map-container">
Map will display GPS locations here<br>
<small>(Requires Google Maps API - coming in Phase 2)</small>
</div>
</div>
</div>
</div>
<!-- Settings Content -->
<div id="settings" class="content">
<div class="panel">
<div class="panel-header">Settings</div>
<div class="panel-body">
<p style="color: #666; margin-bottom: 16px;">App Version 1.0.0</p>
<button class="btn btn-secondary" onclick="clearAllData()">Clear Test Data</button>
<button class="btn btn-primary" onclick="exportData()">Export Data (CSV)</button>
</div>
</div>
</div>
</div>
<!-- Bottom Navigation -->
<div class="tabs">
<button class="tab active" onclick="switchTab('dashboard')">
📊
<span class="tab-label">Dashboard</span>
</button>
<button class="tab" onclick="switchTab('activityForm')">
📝
<span class="tab-label">Log</span>
</button>
<button class="tab" onclick="switchTab('map')">
🗺️
<span class="tab-label">Map</span>
</button>
<button class="tab" onclick="switchTab('settings')">
⚙️
<span class="tab-label">Settings</span>
</button>
</div>
<!-- Firebase SDK -->
<script type="module">
import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js';
import { getFirestore, collection, addDoc, getDocs, query, orderBy, limit, serverTimestamp } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js';
import { getAuth, signInAnonymously } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-auth.js';
// Your Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyBIwNfJnKPseBg5qKMgjlg6w_m6IBkjlwQ",
authDomain: "oyster-farm-app-4bb00.firebaseapp.com",
projectId: "oyster-farm-app-4bb00",
storageBucket: "oyster-farm-app-4bb00.firebasestorage.app",
messagingSenderId: "833644603673",
appId: "1:833644603673:web:1f5f0b6431d06d3cdffd88"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const auth = getAuth(app);
// Sign in anonymously
signInAnonymously(auth).then(() => {
console.log('Signed in to Firebase');
loadDashboard();
});
// Make Firebase available globally
window.db = db;
window.firebaseModules = { collection, addDoc, getDocs, query, orderBy, limit, serverTimestamp };
// Load dashboard data
async function loadDashboard() {
try {
const q = query(collection(db, 'activities'), orderBy('timestamp', 'desc'), limit(20));
const querySnapshot = await getDocs(q);
let totalOysters = 0;
let locations = new Set();
let todayCount = 0;
let activities = [];
const today = new Date().setHours(0,0,0,0);
querySnapshot.forEach((doc) => {
const data = doc.data();
activities.push(data);
totalOysters += data.oysterCount || 0;
locations.add(data.locationId);
if (data.timestamp) {
const actDate = data.timestamp.toDate().setHours(0,0,0,0);
if (actDate === today) todayCount++;
}
});
document.getElementById('totalOysters').textContent = totalOysters.toLocaleString();
document.getElementById('activeLocations').textContent = locations.size;
document.getElementById('todayActivity').textContent = todayCount;
renderActivityList(activities);
} catch (error) {
console.error('Error loading dashboard:', error);
document.getElementById('activityList').innerHTML = '<div style="color:#c00;">Error loading data. Check console.</div>';
}
}
function renderActivityList(activities) {
const container = document.getElementById('activityList');
if (activities.length === 0) {
container.innerHTML = '<div style="color:#666;">No activities logged yet. Start by logging your first activity!</div>';
return;
}
container.innerHTML = activities.slice(0, 10).map(act => `
<div class="activity-item">
<div class="activity-header">
<span class="site-badge badge-${act.site.toLowerCase()}">${act.site}</span>
<span class="activity-location">${act.locationId}</span>
</div>
<div class="activity-details">
${act.activity} — ${(act.oysterCount || 0).toLocaleString()} oysters
${act.sizeClass ? ` — ${act.sizeClass}` : ''}
${act.notes ? ` — ${act.notes}` : ''}
</div>
</div>
`).join('');
}
window.loadDashboard = loadDashboard;
</script>
<script>
// App State
let formData = {
site: 'Stamford',
locationId: '',
activity: 'Deployed',
oysterCount: '',
bagCount: '',
sizeClass: 'Seed',
cropYear: '2025',
gpsCoords: '',
notes: ''
};
let recentActivities = [];
// Tab Switching
function switchTab(tabName) {
document.querySelectorAll('.content').forEach(c => c.classList.remove('active'));
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.getElementById(tabName).classList.add('active');
event.currentTarget.classList.add('active');
if (tabName === 'dashboard') {
loadDashboard();
}
}
// Form Selectors
function selectSite(site) {
formData.site = site;
document.querySelectorAll('[data-site]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.site === site);
});
// Show/hide bag count for Pond
document.getElementById('bagCountGroup').style.display = site === 'Pond' ? 'block' : 'none';
}
function selectActivity(activity) {
formData.activity = activity;
document.querySelectorAll('[data-activity]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.activity === activity);
});
}
function selectSize(size) {
formData.sizeClass = size;
document.querySelectorAll('[data-size]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.size === size);
});
}
function selectYear(year) {
formData.cropYear = year;
document.querySelectorAll('[data-year]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.year === year);
});
}
// Get GPS Location
function getLocation() {
if (!navigator.geolocation) {
alert('Geolocation is not supported by your browser');
return;
}
const btn = event.currentTarget;
btn.textContent = 'Getting location...';
btn.disabled = true;
navigator.geolocation.getCurrentPosition(
(position) => {
const coords = `${position.coords.latitude.toFixed(6)}, ${position.coords.longitude.toFixed(6)}`;
document.getElementById('gpsCoords').value = coords;
formData.gpsCoords = coords;
btn.innerHTML = '<span>✅</span> Location Captured';
setTimeout(() => {
btn.innerHTML = '<span>📍</span> Get Current Location';
btn.disabled = false;
}, 2000);
},
(error) => {
alert('Error getting location: ' + error.message);
btn.innerHTML = '<span>📍</span> Get Current Location';
btn.disabled = false;
},
{ enableHighAccuracy: true }
);
}
// Submit Activity
async function submitActivity() {
formData.locationId = document.getElementById('locationId').value;
formData.oysterCount = document.getElementById('oysterCount').value;
formData.bagCount = document.getElementById('bagCount').value;
formData.notes = document.getElementById('notes').value;
formData.gpsCoords = document.getElementById('gpsCoords').value;
if (!formData.locationId || !formData.oysterCount) {
alert('Please fill in Location ID and Oyster Count');
return;
}
try {
const { collection, addDoc, serverTimestamp } = window.firebaseModules;
await addDoc(collection(window.db, 'activities'), {
site: formData.site,
locationId: formData.locationId,
activity: formData.activity,
oysterCount: parseInt(formData.oysterCount) || 0,
bagCount: parseInt(formData.bagCount) || 0,
sizeClass: formData.sizeClass,
cropYear: formData.cropYear,
gpsCoords: formData.gpsCoords,
notes: formData.notes,
timestamp: serverTimestamp(),
hash: Date.now() + '-' + formData.locationId
});
// Show success
const banner = document.getElementById('successBanner');
banner.classList.add('show');
setTimeout(() => banner.classList.remove('show'), 3000);
// Show clone option
if (confirm('Activity logged! Clone this entry for next location?')) {
cloneEntry();
} else {
resetForm();
}
loadDashboard();
} catch (error) {
console.error('Error submitting:', error);
alert('Error logging activity: ' + error.message);
}
}
// Clone Entry
function cloneEntry() {
// Keep all data except location-specific fields
document.getElementById('locationId').value = '';
document.getElementById('gpsCoords').value = '';
document.getElementById('notes').value = '';
document.getElementById('locationId').focus();
}
// Reset Form
function resetForm() {
document.getElementById('locationId').value = '';
document.getElementById('oysterCount').value = '';
document.getElementById('bagCount').value = '';
document.getElementById('gpsCoords').value = '';
document.getElementById('notes').value = '';
selectSite('Stamford');
selectActivity('Deployed');
selectSize('Seed');
selectYear('2025');
}
// Show Templates
async function showTemplates() {
const section = document.getElementById('templateSection');
const list = document.getElementById('templateList');
try {
const { collection, getDocs, query, orderBy, limit } = window.firebaseModules;
const q = query(collection(window.db, 'activities'), orderBy('timestamp', 'desc'), limit(5));
const snapshot = await getDocs(q);
if (snapshot.empty) {
alert('No recent activities to clone from');
return;
}
list.innerHTML = '';
snapshot.forEach(doc => {
const data = doc.data();
const div = document.createElement('div');
div.className = 'template-item';
div.innerHTML = `
<strong>${data.locationId}</strong> - ${data.sizeClass} (${(data.oysterCount || 0).toLocaleString()})
<br><small>${data.activity} • ${data.cropYear}</small>
`;
div.onclick = () => useTemplate(data);
list.appendChild(div);
});
section.style.display = 'block';
} catch (error) {
console.error('Error loading templates:', error);
alert('Error loading templates');
}
}
function useTemplate(data) {
selectSite(data.site);
selectActivity(data.activity);
selectSize(data.sizeClass);
selectYear(data.cropYear);
document.getElementById('oysterCount').value = data.oysterCount || '';
document.getElementById('bagCount').value = data.bagCount || '';
document.getElementById('locationId').value = '';
document.getElementById('gpsCoords').value = '';
document.getElementById('notes').value = '';
document.getElementById('templateSection').style.display = 'none';
document.getElementById('locationId').focus();
}
// Settings Functions
function clearAllData() {
if (confirm('This will delete all test activities. Are you sure?')) {
// Would implement Firestore delete here
alert('Feature coming soon - for now, delete manually in Firebase Console');
}
}
function exportData() {
alert('Export feature coming in Phase 2');
}
</script>
</body>
</html>