const express = require('express'); const cors = require('cors'); const bodyParser = require('body-parser'); const Database = require('better-sqlite3'); const path = require('path'); const fs = require('fs'); require('dotenv').config(); const app = express(); const PORT = process.env.PORT || 3001; const DB_DIR = path.join(__dirname, 'database'); const DB_PATH = path.join(DB_DIR, 'gardentrack.db'); // Ensure database directory exists if (!fs.existsSync(DB_DIR)) { fs.mkdirSync(DB_DIR, { recursive: true }); console.log('Created database directory'); } // Middleware app.use(cors()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); // Database connection and initialization let db; try { db = new Database(DB_PATH); console.log('Connected to SQLite database at:', DB_PATH); initializeDatabase(); } catch (err) { console.error('Error opening database:', err.message); process.exit(1); } // Initialize database tables function initializeDatabase() { console.log('Initializing database tables...'); try { // Plants table db.exec(` CREATE TABLE IF NOT EXISTS plants ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, variety TEXT NOT NULL, purchase_location TEXT NOT NULL, seedling_age INTEGER NOT NULL, seedling_height REAL NOT NULL, planting_date DATE NOT NULL, health_status TEXT DEFAULT 'good' CHECK(health_status IN ('good', 'needs-attention', 'dead')), current_height REAL, photo_url TEXT, current_photo_url TEXT, notes TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); console.log('Plants table ready'); // Plant observations table db.exec(` CREATE TABLE IF NOT EXISTS plant_observations ( id INTEGER PRIMARY KEY AUTOINCREMENT, plant_id INTEGER NOT NULL, date DATE NOT NULL, title TEXT NOT NULL, observation TEXT NOT NULL, weather_conditions TEXT, temperature REAL, photo_url TEXT, tags TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (plant_id) REFERENCES plants (id) ON DELETE CASCADE ) `); console.log('Plant observations table ready'); // Plant history table db.exec(` CREATE TABLE IF NOT EXISTS plant_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, plant_id INTEGER NOT NULL, year INTEGER NOT NULL, blooming_date DATE, fruiting_date DATE, harvest_date DATE, current_height REAL, current_photo_url TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (plant_id) REFERENCES plants (id) ON DELETE CASCADE ) `); console.log('Plant history table ready'); // Harvest records table db.exec(` CREATE TABLE IF NOT EXISTS harvest_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, plant_id INTEGER NOT NULL, date DATE NOT NULL, quantity REAL NOT NULL, unit TEXT NOT NULL, notes TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (plant_id) REFERENCES plants (id) ON DELETE CASCADE ) `); console.log('Harvest records table ready'); // Maintenance records table db.exec(` CREATE TABLE IF NOT EXISTS maintenance_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, plant_id INTEGER NOT NULL, date DATE NOT NULL, type TEXT NOT NULL CHECK(type IN ('chemical', 'fertilizer', 'watering', 'pruning', 'transplanting', 'other')), description TEXT NOT NULL, amount TEXT, is_planned BOOLEAN DEFAULT 0, is_completed BOOLEAN DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (plant_id) REFERENCES plants (id) ON DELETE CASCADE ) `); console.log('Maintenance records table ready'); // Tasks table db.exec(` CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, plant_id INTEGER, title TEXT NOT NULL, description TEXT, deadline DATE NOT NULL, completed BOOLEAN DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (plant_id) REFERENCES plants (id) ON DELETE SET NULL ) `); console.log('Tasks table ready'); // Insert sample data if tables are empty const plantCount = db.prepare('SELECT COUNT(*) as count FROM plants').get(); if (plantCount.count === 0) { console.log('Inserting sample data...'); insertSampleData(); } else { console.log('Database already contains data'); } } catch (err) { console.error('Error initializing database:', err); process.exit(1); } } // Insert sample data function insertSampleData() { try { // Sample plants const insertPlant = db.prepare(` INSERT INTO plants (type, variety, purchase_location, seedling_age, seedling_height, planting_date, health_status, current_height, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `); insertPlant.run('tree', 'Apple - Honeycrisp', 'Local Nursery', 12, 45.5, '2022-04-15', 'good', 180.2, 'Growing well, good fruit production'); insertPlant.run('shrub', 'Blueberry - Bluecrop', 'Garden Center', 8, 25.0, '2023-03-20', 'needs-attention', 65.8, 'Leaves showing slight discoloration'); insertPlant.run('herb', 'Basil - Sweet Genovese', 'Online Store', 3, 8.2, '2024-05-10', 'good', 22.5, 'Producing well, regular harvests'); // Sample tasks const insertTask = db.prepare(` INSERT INTO tasks (plant_id, title, description, deadline, completed) VALUES (?, ?, ?, ?, ?) `); insertTask.run(1, 'Apply fertilizer', 'Apply spring fertilizer to apple trees', '2024-03-15', 0); insertTask.run(2, 'Prune blueberry bushes', 'Annual pruning before spring growth', '2024-02-28', 1); insertTask.run(3, 'Harvest basil', 'Regular harvest to encourage growth', '2024-06-01', 0); // Sample maintenance records const insertMaintenance = db.prepare(` INSERT INTO maintenance_records (plant_id, date, type, description, amount, is_planned, is_completed) VALUES (?, ?, ?, ?, ?, ?, ?) `); insertMaintenance.run(1, '2024-01-10', 'pruning', 'Winter pruning - removed dead branches', null, 0, 1); insertMaintenance.run(2, '2024-01-05', 'fertilizer', 'Applied organic compost', '2 cups', 0, 1); insertMaintenance.run(3, '2024-05-15', 'watering', 'Deep watering during dry spell', '1 gallon', 0, 1); // Sample harvest records const insertHarvest = db.prepare(` INSERT INTO harvest_records (plant_id, date, quantity, unit, notes) VALUES (?, ?, ?, ?, ?) `); insertHarvest.run(1, '2023-09-15', 25, 'lbs', 'Excellent harvest, apples were sweet and crisp'); insertHarvest.run(2, '2023-07-20', 3, 'cups', 'First harvest of the season, berries were plump and sweet'); insertHarvest.run(3, '2024-05-25', 0.5, 'cups', 'Fresh basil for cooking, very aromatic'); // Sample observations const insertObservation = db.prepare(` INSERT INTO plant_observations (plant_id, date, title, observation, weather_conditions, temperature, tags) VALUES (?, ?, ?, ?, ?, ?, ?) `); insertObservation.run(1, '2024-01-15', 'Winter dormancy check', 'Apple tree showing normal winter dormancy. Buds are tight and healthy looking. No signs of pest damage on bark.', 'Clear, cold', 2.5, 'dormancy,health-check,winter'); insertObservation.run(2, '2024-01-20', 'Pruning completed', 'Finished annual pruning of blueberry bushes. Removed about 20% of old wood and opened up center for better air circulation.', 'Overcast', 8.0, 'pruning,maintenance'); insertObservation.run(3, '2024-05-12', 'First true leaves', 'Basil seedlings showing first set of true leaves. Growth is vigorous and color is deep green. Ready for transplanting soon.', 'Sunny', 22.0, 'growth,seedling,transplant-ready'); console.log('Sample data inserted successfully!'); } catch (err) { console.error('Error inserting sample data:', err); } } // Routes // Plant Observations app.get('/api/observations', (req, res) => { try { const stmt = db.prepare('SELECT * FROM plant_observations ORDER BY date DESC'); const rows = stmt.all(); res.json(rows.map(row => ({ id: row.id, plantId: row.plant_id, date: row.date, title: row.title, observation: row.observation, weatherConditions: row.weather_conditions, temperature: row.temperature, photoUrl: row.photo_url, tags: row.tags, createdAt: row.created_at, updatedAt: row.updated_at }))); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/api/observations', (req, res) => { try { const { plantId, date, title, observation, weatherConditions, temperature, photoUrl, tags } = req.body; const stmt = db.prepare(` INSERT INTO plant_observations (plant_id, date, title, observation, weather_conditions, temperature, photo_url, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); const result = stmt.run(plantId, date, title, observation, weatherConditions, temperature, photoUrl, tags); const getStmt = db.prepare('SELECT * FROM plant_observations WHERE id = ?'); const row = getStmt.get(result.lastInsertRowid); res.json({ id: row.id, plantId: row.plant_id, date: row.date, title: row.title, observation: row.observation, weatherConditions: row.weather_conditions, temperature: row.temperature, photoUrl: row.photo_url, tags: row.tags, createdAt: row.created_at, updatedAt: row.updated_at }); } catch (err) { res.status(400).json({ error: err.message }); } }); app.put('/api/observations/:id', (req, res) => { try { const { plantId, date, title, observation, weatherConditions, temperature, photoUrl, tags } = req.body; const stmt = db.prepare(` UPDATE plant_observations SET plant_id = ?, date = ?, title = ?, observation = ?, weather_conditions = ?, temperature = ?, photo_url = ?, tags = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `); stmt.run(plantId, date, title, observation, weatherConditions, temperature, photoUrl, tags, req.params.id); const getStmt = db.prepare('SELECT * FROM plant_observations WHERE id = ?'); const row = getStmt.get(req.params.id); res.json({ id: row.id, plantId: row.plant_id, date: row.date, title: row.title, observation: row.observation, weatherConditions: row.weather_conditions, temperature: row.temperature, photoUrl: row.photo_url, tags: row.tags, createdAt: row.created_at, updatedAt: row.updated_at }); } catch (err) { res.status(400).json({ error: err.message }); } }); app.delete('/api/observations/:id', (req, res) => { try { const stmt = db.prepare('DELETE FROM plant_observations WHERE id = ?'); stmt.run(req.params.id); res.json({ message: 'Observation deleted successfully' }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Plants app.get('/api/plants', (req, res) => { try { const stmt = db.prepare('SELECT * FROM plants ORDER BY created_at DESC'); const rows = stmt.all(); res.json(rows.map(row => ({ id: row.id, type: row.type, variety: row.variety, purchaseLocation: row.purchase_location, seedlingAge: row.seedling_age, seedlingHeight: row.seedling_height, plantingDate: row.planting_date, healthStatus: row.health_status, currentHeight: row.current_height, photoUrl: row.photo_url, currentPhotoUrl: row.current_photo_url, notes: row.notes, createdAt: row.created_at, updatedAt: row.updated_at }))); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/api/plants', (req, res) => { try { const { type, variety, purchaseLocation, seedlingAge, seedlingHeight, plantingDate, healthStatus, currentHeight, photoUrl, notes } = req.body; const stmt = db.prepare(` INSERT INTO plants (type, variety, purchase_location, seedling_age, seedling_height, planting_date, health_status, current_height, photo_url, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); const result = stmt.run(type, variety, purchaseLocation, seedlingAge, seedlingHeight, plantingDate, healthStatus, currentHeight, photoUrl, notes); // Get the created plant const getStmt = db.prepare('SELECT * FROM plants WHERE id = ?'); const row = getStmt.get(result.lastInsertRowid); res.json({ id: row.id, type: row.type, variety: row.variety, purchaseLocation: row.purchase_location, seedlingAge: row.seedling_age, seedlingHeight: row.seedling_height, plantingDate: row.planting_date, healthStatus: row.health_status, currentHeight: row.current_height, photoUrl: row.photo_url, currentPhotoUrl: row.current_photo_url, notes: row.notes, createdAt: row.created_at, updatedAt: row.updated_at }); } catch (err) { res.status(400).json({ error: err.message }); } }); app.put('/api/plants/:id', (req, res) => { try { const { type, variety, purchaseLocation, seedlingAge, seedlingHeight, plantingDate, healthStatus, currentHeight, photoUrl, notes } = req.body; const stmt = db.prepare(` UPDATE plants SET type = ?, variety = ?, purchase_location = ?, seedling_age = ?, seedling_height = ?, planting_date = ?, health_status = ?, current_height = ?, photo_url = ?, notes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `); stmt.run(type, variety, purchaseLocation, seedlingAge, seedlingHeight, plantingDate, healthStatus, currentHeight, photoUrl, notes, req.params.id); // Get the updated plant const getStmt = db.prepare('SELECT * FROM plants WHERE id = ?'); const row = getStmt.get(req.params.id); res.json({ id: row.id, type: row.type, variety: row.variety, purchaseLocation: row.purchase_location, seedlingAge: row.seedling_age, seedlingHeight: row.seedling_height, plantingDate: row.planting_date, healthStatus: row.health_status, currentHeight: row.current_height, photoUrl: row.photo_url, currentPhotoUrl: row.current_photo_url, notes: row.notes, createdAt: row.created_at, updatedAt: row.updated_at }); } catch (err) { res.status(400).json({ error: err.message }); } }); app.delete('/api/plants/:id', (req, res) => { try { const stmt = db.prepare('DELETE FROM plants WHERE id = ?'); stmt.run(req.params.id); res.json({ message: 'Plant deleted successfully' }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Tasks app.get('/api/tasks', (req, res) => { try { const stmt = db.prepare('SELECT * FROM tasks ORDER BY deadline ASC'); const rows = stmt.all(); res.json(rows.map(row => ({ id: row.id, plantId: row.plant_id, title: row.title, description: row.description, deadline: row.deadline, completed: Boolean(row.completed), createdAt: row.created_at, updatedAt: row.updated_at }))); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/api/tasks', (req, res) => { try { const { plantId, title, description, deadline, completed = false } = req.body; const stmt = db.prepare(` INSERT INTO tasks (plant_id, title, description, deadline, completed) VALUES (?, ?, ?, ?, ?) `); const result = stmt.run(plantId, title, description, deadline, completed ? 1 : 0); const getStmt = db.prepare('SELECT * FROM tasks WHERE id = ?'); const row = getStmt.get(result.lastInsertRowid); res.json({ id: row.id, plantId: row.plant_id, title: row.title, description: row.description, deadline: row.deadline, completed: Boolean(row.completed), createdAt: row.created_at, updatedAt: row.updated_at }); } catch (err) { res.status(400).json({ error: err.message }); } }); app.put('/api/tasks/:id', (req, res) => { try { const { plantId, title, description, deadline, completed } = req.body; const stmt = db.prepare(` UPDATE tasks SET plant_id = ?, title = ?, description = ?, deadline = ?, completed = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `); stmt.run(plantId, title, description, deadline, completed ? 1 : 0, req.params.id); const getStmt = db.prepare('SELECT * FROM tasks WHERE id = ?'); const row = getStmt.get(req.params.id); res.json({ id: row.id, plantId: row.plant_id, title: row.title, description: row.description, deadline: row.deadline, completed: Boolean(row.completed), createdAt: row.created_at, updatedAt: row.updated_at }); } catch (err) { res.status(400).json({ error: err.message }); } }); app.delete('/api/tasks/:id', (req, res) => { try { const stmt = db.prepare('DELETE FROM tasks WHERE id = ?'); stmt.run(req.params.id); res.json({ message: 'Task deleted successfully' }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Maintenance Records app.get('/api/maintenance', (req, res) => { try { const stmt = db.prepare('SELECT * FROM maintenance_records ORDER BY date DESC'); const rows = stmt.all(); res.json(rows.map(row => ({ id: row.id, plantId: row.plant_id, date: row.date, type: row.type, description: row.description, amount: row.amount, isPlanned: Boolean(row.is_planned), isCompleted: Boolean(row.is_completed), createdAt: row.created_at, updatedAt: row.updated_at }))); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/api/maintenance', (req, res) => { try { const { plantId, date, type, description, amount, isPlanned, isCompleted } = req.body; const stmt = db.prepare(` INSERT INTO maintenance_records (plant_id, date, type, description, amount, is_planned, is_completed) VALUES (?, ?, ?, ?, ?, ?, ?) `); const result = stmt.run(plantId, date, type, description, amount, isPlanned ? 1 : 0, isCompleted ? 1 : 0); const getStmt = db.prepare('SELECT * FROM maintenance_records WHERE id = ?'); const row = getStmt.get(result.lastInsertRowid); res.json({ id: row.id, plantId: row.plant_id, date: row.date, type: row.type, description: row.description, amount: row.amount, isPlanned: Boolean(row.is_planned), isCompleted: Boolean(row.is_completed), createdAt: row.created_at, updatedAt: row.updated_at }); } catch (err) { res.status(400).json({ error: err.message }); } }); // Harvest Records app.get('/api/harvests', (req, res) => { try { const stmt = db.prepare('SELECT * FROM harvest_records ORDER BY date DESC'); const rows = stmt.all(); res.json(rows.map(row => ({ id: row.id, plantId: row.plant_id, date: row.date, quantity: row.quantity, unit: row.unit, notes: row.notes, createdAt: row.created_at, updatedAt: row.updated_at }))); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/api/harvests', (req, res) => { try { const { plantId, date, quantity, unit, notes } = req.body; const stmt = db.prepare(` INSERT INTO harvest_records (plant_id, date, quantity, unit, notes) VALUES (?, ?, ?, ?, ?) `); const result = stmt.run(plantId, date, quantity, unit, notes); const getStmt = db.prepare('SELECT * FROM harvest_records WHERE id = ?'); const row = getStmt.get(result.lastInsertRowid); res.json({ id: row.id, plantId: row.plant_id, date: row.date, quantity: row.quantity, unit: row.unit, notes: row.notes, createdAt: row.created_at, updatedAt: row.updated_at }); } catch (err) { res.status(400).json({ error: err.message }); } }); // Health check app.get('/api/health', (req, res) => { res.json({ status: 'OK', message: 'GardenTrack API is running' }); }); // Additional endpoints for enhanced functionality // Get plant by ID app.get('/api/plants/:id', (req, res) => { try { const stmt = db.prepare('SELECT * FROM plants WHERE id = ?'); const row = stmt.get(req.params.id); if (!row) { res.status(404).json({ error: 'Plant not found' }); return; } res.json({ id: row.id, type: row.type, variety: row.variety, purchaseLocation: row.purchase_location, seedlingAge: row.seedling_age, seedlingHeight: row.seedling_height, plantingDate: row.planting_date, healthStatus: row.health_status, currentHeight: row.current_height, photoUrl: row.photo_url, currentPhotoUrl: row.current_photo_url, notes: row.notes, createdAt: row.created_at, updatedAt: row.updated_at }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Search plants app.get('/api/plants/search', (req, res) => { try { const query = req.query.q; if (!query) { res.status(400).json({ error: 'Search query is required' }); return; } const stmt = db.prepare(` SELECT * FROM plants WHERE variety LIKE ? OR type LIKE ? OR notes LIKE ? ORDER BY created_at DESC `); const searchTerm = `%${query}%`; const rows = stmt.all(searchTerm, searchTerm, searchTerm); res.json(rows.map(row => ({ id: row.id, type: row.type, variety: row.variety, purchaseLocation: row.purchase_location, seedlingAge: row.seedling_age, seedlingHeight: row.seedling_height, plantingDate: row.planting_date, healthStatus: row.health_status, currentHeight: row.current_height, photoUrl: row.photo_url, currentPhotoUrl: row.current_photo_url, notes: row.notes, createdAt: row.created_at, updatedAt: row.updated_at }))); } catch (err) { res.status(500).json({ error: err.message }); } }); // Get upcoming tasks app.get('/api/tasks/upcoming', (req, res) => { try { const days = parseInt(req.query.days) || 7; const endDate = new Date(); endDate.setDate(endDate.getDate() + days); const stmt = db.prepare('SELECT * FROM tasks WHERE completed = 0 AND deadline <= ? ORDER BY deadline ASC'); const rows = stmt.all(endDate.toISOString().split('T')[0]); res.json(rows.map(row => ({ id: row.id, plantId: row.plant_id, title: row.title, description: row.description, deadline: row.deadline, completed: Boolean(row.completed), createdAt: row.created_at, updatedAt: row.updated_at }))); } catch (err) { res.status(500).json({ error: err.message }); } }); // Get overdue tasks app.get('/api/tasks/overdue', (req, res) => { try { const today = new Date().toISOString().split('T')[0]; const stmt = db.prepare('SELECT * FROM tasks WHERE completed = 0 AND deadline < ? ORDER BY deadline ASC'); const rows = stmt.all(today); res.json(rows.map(row => ({ id: row.id, plantId: row.plant_id, title: row.title, description: row.description, deadline: row.deadline, completed: Boolean(row.completed), createdAt: row.created_at, updatedAt: row.updated_at }))); } catch (err) { res.status(500).json({ error: err.message }); } }); // Dashboard summary app.get('/api/dashboard/summary', (req, res) => { try { const results = { totalPlants: db.prepare('SELECT COUNT(*) as count FROM plants').get().count, healthyPlants: db.prepare('SELECT COUNT(*) as count FROM plants WHERE health_status = "good"').get().count, plantsNeedingAttention: db.prepare('SELECT COUNT(*) as count FROM plants WHERE health_status = "needs-attention"').get().count, totalTasks: db.prepare('SELECT COUNT(*) as count FROM tasks').get().count, completedTasks: db.prepare('SELECT COUNT(*) as count FROM tasks WHERE completed = 1').get().count, upcomingTasks: db.prepare('SELECT COUNT(*) as count FROM tasks WHERE completed = 0 AND deadline >= date("now")').get().count, totalHarvests: db.prepare('SELECT COUNT(*) as count FROM harvest_records').get().count, recentMaintenanceCount: db.prepare('SELECT COUNT(*) as count FROM maintenance_records WHERE date >= date("now", "-30 days")').get().count }; res.json(results); } catch (err) { res.status(500).json({ error: err.message }); } }); // Error handling middleware app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: 'Something went wrong!' }); }); // Start server app.listen(PORT, () => { console.log(`GardenTrack API server running on port ${PORT}`); }); // Graceful shutdown process.on('SIGINT', () => { if (db) { db.close(); console.log('Database connection closed.'); } process.exit(0); });