const express = require('express'); const cors = require('cors'); const bodyParser = require('body-parser'); const sqlite3 = require('sqlite3').verbose(); 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 const db = new sqlite3.Database(DB_PATH, (err) => { if (err) { console.error('Error opening database:', err.message); process.exit(1); } else { console.log('Connected to SQLite database at:', DB_PATH); initializeDatabase(); } }); // Initialize database tables function initializeDatabase() { console.log('Initializing database tables...'); db.serialize(() => { // Plants table db.run(` 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 ) `, (err) => { if (err) console.error('Error creating plants table:', err); else console.log('Plants table ready'); }); // Plant history table db.run(` 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 ) `, (err) => { if (err) console.error('Error creating plant_history table:', err); else console.log('Plant history table ready'); }); // Harvest records table db.run(` 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 ) `, (err) => { if (err) console.error('Error creating harvest_records table:', err); else console.log('Harvest records table ready'); }); // Maintenance records table db.run(` 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 ) `, (err) => { if (err) console.error('Error creating maintenance_records table:', err); else console.log('Maintenance records table ready'); }); // Tasks table db.run(` 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 ) `, (err) => { if (err) console.error('Error creating tasks table:', err); else console.log('Tasks table ready'); }); // Insert sample data if tables are empty db.get('SELECT COUNT(*) as count FROM plants', (err, row) => { if (err) { console.error('Error checking plants table:', err); return; } if (row.count === 0) { console.log('Inserting sample data...'); insertSampleData(); } else { console.log('Database already contains data'); } }); }); } // Insert sample data function insertSampleData() { // Sample plants const plantSql = `INSERT INTO plants (type, variety, purchase_location, seedling_age, seedling_height, planting_date, health_status, current_height, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`; db.run(plantSql, ['tree', 'Apple - Honeycrisp', 'Local Nursery', 12, 45.5, '2022-04-15', 'good', 180.2, 'Growing well, good fruit production']); db.run(plantSql, ['shrub', 'Blueberry - Bluecrop', 'Garden Center', 8, 25.0, '2023-03-20', 'needs-attention', 65.8, 'Leaves showing slight discoloration']); db.run(plantSql, ['herb', 'Basil - Sweet Genovese', 'Online Store', 3, 8.2, '2024-05-10', 'good', 22.5, 'Producing well, regular harvests']); // Sample tasks const taskSql = `INSERT INTO tasks (plant_id, title, description, deadline, completed) VALUES (?, ?, ?, ?, ?)`; db.run(taskSql, [1, 'Apply fertilizer', 'Apply spring fertilizer to apple trees', '2024-03-15', 0]); db.run(taskSql, [2, 'Prune blueberry bushes', 'Annual pruning before spring growth', '2024-02-28', 1]); db.run(taskSql, [3, 'Harvest basil', 'Regular harvest to encourage growth', '2024-06-01', 0]); // Sample maintenance records const maintenanceSql = `INSERT INTO maintenance_records (plant_id, date, type, description, amount, is_planned, is_completed) VALUES (?, ?, ?, ?, ?, ?, ?)`; db.run(maintenanceSql, [1, '2024-01-10', 'pruning', 'Winter pruning - removed dead branches', null, 0, 1]); db.run(maintenanceSql, [2, '2024-01-05', 'fertilizer', 'Applied organic compost', '2 cups', 0, 1]); db.run(maintenanceSql, [3, '2024-05-15', 'watering', 'Deep watering during dry spell', '1 gallon', 0, 1]); // Sample harvest records const harvestSql = `INSERT INTO harvest_records (plant_id, date, quantity, unit, notes) VALUES (?, ?, ?, ?, ?)`; db.run(harvestSql, [1, '2023-09-15', 25, 'lbs', 'Excellent harvest, apples were sweet and crisp']); db.run(harvestSql, [2, '2023-07-20', 3, 'cups', 'First harvest of the season, berries were plump and sweet']); db.run(harvestSql, [3, '2024-05-25', 0.5, 'cups', 'Fresh basil for cooking, very aromatic']); console.log('Sample data inserted successfully!'); } // Routes // Plants app.get('/api/plants', (req, res) => { const sql = 'SELECT * FROM plants ORDER BY created_at DESC'; db.all(sql, [], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } 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 }))); }); }); app.post('/api/plants', (req, res) => { const { type, variety, purchaseLocation, seedlingAge, seedlingHeight, plantingDate, healthStatus, currentHeight, photoUrl, notes } = req.body; const sql = `INSERT INTO plants (type, variety, purchase_location, seedling_age, seedling_height, planting_date, health_status, current_height, photo_url, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; db.run(sql, [type, variety, purchaseLocation, seedlingAge, seedlingHeight, plantingDate, healthStatus, currentHeight, photoUrl, notes], function(err) { if (err) { res.status(400).json({ error: err.message }); return; } // Get the created plant db.get('SELECT * FROM plants WHERE id = ?', [this.lastID], (err, row) => { if (err) { res.status(500).json({ error: err.message }); 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 }); }); }); }); app.put('/api/plants/:id', (req, res) => { const { type, variety, purchaseLocation, seedlingAge, seedlingHeight, plantingDate, healthStatus, currentHeight, photoUrl, notes } = req.body; const sql = `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 = ?`; db.run(sql, [type, variety, purchaseLocation, seedlingAge, seedlingHeight, plantingDate, healthStatus, currentHeight, photoUrl, notes, req.params.id], function(err) { if (err) { res.status(400).json({ error: err.message }); return; } // Get the updated plant db.get('SELECT * FROM plants WHERE id = ?', [req.params.id], (err, row) => { if (err) { res.status(500).json({ error: err.message }); 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 }); }); }); }); app.delete('/api/plants/:id', (req, res) => { db.run('DELETE FROM plants WHERE id = ?', [req.params.id], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ message: 'Plant deleted successfully' }); }); }); // Tasks app.get('/api/tasks', (req, res) => { const sql = 'SELECT * FROM tasks ORDER BY deadline ASC'; db.all(sql, [], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } 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 }))); }); }); app.post('/api/tasks', (req, res) => { const { plantId, title, description, deadline, completed = false } = req.body; const sql = `INSERT INTO tasks (plant_id, title, description, deadline, completed) VALUES (?, ?, ?, ?, ?)`; db.run(sql, [plantId, title, description, deadline, completed ? 1 : 0], function(err) { if (err) { res.status(400).json({ error: err.message }); return; } db.get('SELECT * FROM tasks WHERE id = ?', [this.lastID], (err, row) => { if (err) { res.status(500).json({ error: err.message }); return; } 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 }); }); }); }); app.put('/api/tasks/:id', (req, res) => { const { plantId, title, description, deadline, completed } = req.body; const sql = `UPDATE tasks SET plant_id = ?, title = ?, description = ?, deadline = ?, completed = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`; db.run(sql, [plantId, title, description, deadline, completed ? 1 : 0, req.params.id], function(err) { if (err) { res.status(400).json({ error: err.message }); return; } db.get('SELECT * FROM tasks WHERE id = ?', [req.params.id], (err, row) => { if (err) { res.status(500).json({ error: err.message }); return; } 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 }); }); }); }); app.delete('/api/tasks/:id', (req, res) => { db.run('DELETE FROM tasks WHERE id = ?', [req.params.id], function(err) { if (err) { res.status(500).json({ error: err.message }); return; } res.json({ message: 'Task deleted successfully' }); }); }); // Maintenance Records app.get('/api/maintenance', (req, res) => { const sql = 'SELECT * FROM maintenance_records ORDER BY date DESC'; db.all(sql, [], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } 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 }))); }); }); app.post('/api/maintenance', (req, res) => { const { plantId, date, type, description, amount, isPlanned, isCompleted } = req.body; const sql = `INSERT INTO maintenance_records (plant_id, date, type, description, amount, is_planned, is_completed) VALUES (?, ?, ?, ?, ?, ?, ?)`; db.run(sql, [plantId, date, type, description, amount, isPlanned ? 1 : 0, isCompleted ? 1 : 0], function(err) { if (err) { res.status(400).json({ error: err.message }); return; } db.get('SELECT * FROM maintenance_records WHERE id = ?', [this.lastID], (err, row) => { if (err) { res.status(500).json({ error: err.message }); return; } 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 }); }); }); }); // Harvest Records app.get('/api/harvests', (req, res) => { const sql = 'SELECT * FROM harvest_records ORDER BY date DESC'; db.all(sql, [], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } 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 }))); }); }); app.post('/api/harvests', (req, res) => { const { plantId, date, quantity, unit, notes } = req.body; const sql = `INSERT INTO harvest_records (plant_id, date, quantity, unit, notes) VALUES (?, ?, ?, ?, ?)`; db.run(sql, [plantId, date, quantity, unit, notes], function(err) { if (err) { res.status(400).json({ error: err.message }); return; } db.get('SELECT * FROM harvest_records WHERE id = ?', [this.lastID], (err, row) => { if (err) { res.status(500).json({ error: err.message }); return; } 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 }); }); }); }); // 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) => { const sql = 'SELECT * FROM plants WHERE id = ?'; db.get(sql, [req.params.id], (err, row) => { if (err) { res.status(500).json({ error: err.message }); return; } 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 }); }); }); // Search plants app.get('/api/plants/search', (req, res) => { const query = req.query.q; if (!query) { res.status(400).json({ error: 'Search query is required' }); return; } const sql = `SELECT * FROM plants WHERE variety LIKE ? OR type LIKE ? OR notes LIKE ? ORDER BY created_at DESC`; const searchTerm = `%${query}%`; db.all(sql, [searchTerm, searchTerm, searchTerm], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } 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 }))); }); }); // Get upcoming tasks app.get('/api/tasks/upcoming', (req, res) => { const days = parseInt(req.query.days) || 7; const endDate = new Date(); endDate.setDate(endDate.getDate() + days); const sql = 'SELECT * FROM tasks WHERE completed = 0 AND deadline <= ? ORDER BY deadline ASC'; db.all(sql, [endDate.toISOString().split('T')[0]], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } 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 }))); }); }); // Get overdue tasks app.get('/api/tasks/overdue', (req, res) => { const today = new Date().toISOString().split('T')[0]; const sql = 'SELECT * FROM tasks WHERE completed = 0 AND deadline < ? ORDER BY deadline ASC'; db.all(sql, [today], (err, rows) => { if (err) { res.status(500).json({ error: err.message }); return; } 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 }))); }); }); // Dashboard summary app.get('/api/dashboard/summary', (req, res) => { const queries = { totalPlants: 'SELECT COUNT(*) as count FROM plants', healthyPlants: 'SELECT COUNT(*) as count FROM plants WHERE health_status = "good"', plantsNeedingAttention: 'SELECT COUNT(*) as count FROM plants WHERE health_status = "needs-attention"', totalTasks: 'SELECT COUNT(*) as count FROM tasks', completedTasks: 'SELECT COUNT(*) as count FROM tasks WHERE completed = 1', upcomingTasks: 'SELECT COUNT(*) as count FROM tasks WHERE completed = 0 AND deadline >= date("now")', totalHarvests: 'SELECT COUNT(*) as count FROM harvest_records', recentMaintenanceCount: 'SELECT COUNT(*) as count FROM maintenance_records WHERE date >= date("now", "-30 days")' }; const results = {}; const queryKeys = Object.keys(queries); let completed = 0; queryKeys.forEach(key => { db.get(queries[key], [], (err, row) => { if (err) { console.error(`Error in ${key} query:`, err); results[key] = 0; } else { results[key] = row.count; } completed++; if (completed === queryKeys.length) { res.json(results); } }); }); }); // 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', () => { db.close((err) => { if (err) { console.error(err.message); } console.log('Database connection closed.'); process.exit(0); }); });