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'); const multer = require('multer'); 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 uploads directory exists const UPLOADS_DIR = path.join(__dirname, 'uploads'); if (!fs.existsSync(UPLOADS_DIR)) { fs.mkdirSync(UPLOADS_DIR, { recursive: true }); console.log('Created uploads directory'); } // 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 })); // Serve uploaded files statically app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); // Configure multer for file uploads const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, UPLOADS_DIR); }, filename: function (req, file, cb) { // Generate unique filename with timestamp const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); const extension = path.extname(file.originalname); cb(null, 'plant-' + uniqueSuffix + extension); } }); const fileFilter = (req, file, cb) => { // Accept only image files if (file.mimetype.startsWith('image/')) { cb(null, true); } else { cb(new Error('Only image files are allowed!'), false); } }; const upload = multer({ storage: storage, fileFilter: fileFilter, limits: { fileSize: 5 * 1024 * 1024 // 5MB limit } }); // 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'); // Fertilizers table db.exec(` CREATE TABLE IF NOT EXISTS fertilizers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, brand TEXT, type TEXT NOT NULL CHECK(type IN ('organic', 'synthetic', 'liquid', 'granular', 'slow-release')), npk_ratio TEXT, description TEXT, application_rate TEXT, frequency TEXT, season TEXT, notes TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); console.log('Fertilizers table ready'); // Chemicals table db.exec(` CREATE TABLE IF NOT EXISTS chemicals ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, brand TEXT, type TEXT NOT NULL CHECK(type IN ('pesticide', 'herbicide', 'fungicide', 'insecticide', 'miticide')), active_ingredient TEXT, concentration TEXT, target_pests TEXT, application_method TEXT, safety_period INTEGER, notes TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); console.log('Chemicals 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, fertilizer_id INTEGER, chemical_id INTEGER, 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, FOREIGN KEY (fertilizer_id) REFERENCES fertilizers (id) ON DELETE SET NULL, FOREIGN KEY (chemical_id) REFERENCES chemicals (id) ON DELETE SET NULL ) `); console.log('Maintenance records table ready'); // Tasks table db.exec(` CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, plant_id INTEGER, type TEXT DEFAULT 'general' CHECK(type IN ('general', 'fertilizer', 'chemical', 'watering', 'pruning', 'transplanting', 'harvesting', 'other')), title TEXT NOT NULL, description TEXT, deadline DATE NOT NULL, completed BOOLEAN DEFAULT 0, fertilizer_id INTEGER, chemical_id INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (plant_id) REFERENCES plants (id) ON DELETE SET NULL, FOREIGN KEY (fertilizer_id) REFERENCES fertilizers (id) ON DELETE SET NULL, FOREIGN KEY (chemical_id) REFERENCES chemicals (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 fertilizers const insertFertilizer = db.prepare(` INSERT INTO fertilizers (name, brand, type, npk_ratio, description, application_rate, frequency, season, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `); insertFertilizer.run('All-Purpose Garden Fertilizer', 'Miracle-Gro', 'synthetic', '10-10-10', 'Balanced fertilizer for general garden use', '1 tablespoon per gallon', 'Every 2 weeks', 'Spring-Summer', 'Good for most plants'); insertFertilizer.run('Organic Compost', 'Local Farm', 'organic', '3-2-2', 'Natural organic matter for soil improvement', '2-3 inches layer', 'Twice yearly', 'Spring-Fall', 'Improves soil structure'); insertFertilizer.run('Bone Meal', 'Espoma', 'organic', '3-15-0', 'Slow-release phosphorus for root development', '1-2 tablespoons per plant', 'Once per season', 'Spring', 'Great for flowering plants'); insertFertilizer.run('Liquid Kelp', 'Neptune\'s Harvest', 'liquid', '0-0-1', 'Seaweed extract for plant health', '1 tablespoon per gallon', 'Monthly', 'All seasons', 'Boosts plant immunity'); // Sample chemicals const insertChemical = db.prepare(` INSERT INTO chemicals (name, brand, type, active_ingredient, concentration, target_pests, application_method, safety_period, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `); insertChemical.run('Neem Oil', 'Garden Safe', 'insecticide', 'Azadirachtin', '0.9%', 'Aphids, whiteflies, spider mites', 'Foliar spray', 1, 'Organic option, safe for beneficial insects'); insertChemical.run('Copper Fungicide', 'Bonide', 'fungicide', 'Copper sulfate', '8%', 'Blight, rust, mildew', 'Foliar spray', 7, 'Use in early morning or evening'); insertChemical.run('Bt Spray', 'Safer Brand', 'pesticide', 'Bacillus thuringiensis', '0.5%', 'Caterpillars, larvae', 'Foliar spray', 0, 'Organic, targets specific pests'); insertChemical.run('Systemic Insecticide', 'Bayer', 'insecticide', 'Imidacloprid', '1.47%', 'Aphids, scale, thrips', 'Soil drench', 21, 'Long-lasting protection'); // 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, fertilizer_id, chemical_id, is_planned, is_completed) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `); insertMaintenance.run(1, '2024-01-10', 'pruning', 'Winter pruning - removed dead branches', null, null, null, 0, 1); insertMaintenance.run(2, '2024-01-05', 'fertilizer', 'Applied organic compost', '2 cups', 2, null, 0, 1); insertMaintenance.run(3, '2024-05-15', 'watering', 'Deep watering during dry spell', '1 gallon', null, null, 0, 1); insertMaintenance.run(1, '2024-03-20', 'chemical', 'Applied neem oil for aphid prevention', '2 tablespoons per gallon', null, 1, 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 // Photo upload endpoint app.post('/api/upload-photo', upload.single('photo'), (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'No file uploaded' }); } // Return the file path relative to the server const photoUrl = `/uploads/${req.file.filename}`; res.json({ photoUrl }); } catch (error) { res.status(500).json({ error: error.message }); } }); // 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 t.*, f.name as fertilizer_name, c.name as chemical_name FROM tasks t LEFT JOIN fertilizers f ON t.fertilizer_id = f.id LEFT JOIN chemicals c ON t.chemical_id = c.id ORDER BY t.deadline ASC `); const rows = stmt.all(); res.json(rows.map(row => ({ id: row.id, plantId: row.plant_id, type: row.type, title: row.title, description: row.description, deadline: row.deadline, completed: Boolean(row.completed), fertilizerId: row.fertilizer_id, chemicalId: row.chemical_id, fertilizerName: row.fertilizer_name, chemicalName: row.chemical_name, 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, type, title, description, deadline, completed = false, fertilizerId, chemicalId } = req.body; const stmt = db.prepare(` INSERT INTO tasks (plant_id, type, title, description, deadline, completed, fertilizer_id, chemical_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); const result = stmt.run(plantId, type, title, description, deadline, completed ? 1 : 0, fertilizerId || null, chemicalId || null); const getStmt = db.prepare(` SELECT t.*, f.name as fertilizer_name, c.name as chemical_name FROM tasks t LEFT JOIN fertilizers f ON t.fertilizer_id = f.id LEFT JOIN chemicals c ON t.chemical_id = c.id WHERE t.id = ? `); const row = getStmt.get(result.lastInsertRowid); res.json({ id: row.id, plantId: row.plant_id, type: row.type, title: row.title, description: row.description, deadline: row.deadline, completed: Boolean(row.completed), fertilizerId: row.fertilizer_id, chemicalId: row.chemical_id, fertilizerName: row.fertilizer_name, chemicalName: row.chemical_name, 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, type, title, description, deadline, completed, fertilizerId, chemicalId } = req.body; const stmt = db.prepare(` UPDATE tasks SET plant_id = ?, type = ?, title = ?, description = ?, deadline = ?, completed = ?, fertilizer_id = ?, chemical_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `); stmt.run(plantId, type, title, description, deadline, completed ? 1 : 0, fertilizerId || null, chemicalId || null, req.params.id); const getStmt = db.prepare(` SELECT t.*, f.name as fertilizer_name, c.name as chemical_name FROM tasks t LEFT JOIN fertilizers f ON t.fertilizer_id = f.id LEFT JOIN chemicals c ON t.chemical_id = c.id WHERE t.id = ? `); const row = getStmt.get(req.params.id); res.json({ id: row.id, plantId: row.plant_id, type: row.type, title: row.title, description: row.description, deadline: row.deadline, completed: Boolean(row.completed), fertilizerId: row.fertilizer_id, chemicalId: row.chemical_id, fertilizerName: row.fertilizer_name, chemicalName: row.chemical_name, 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 mr.*, f.name as fertilizer_name, c.name as chemical_name FROM maintenance_records mr LEFT JOIN fertilizers f ON mr.fertilizer_id = f.id LEFT JOIN chemicals c ON mr.chemical_id = c.id ORDER BY mr.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, fertilizerId: row.fertilizer_id, chemicalId: row.chemical_id, fertilizerName: row.fertilizer_name, chemicalName: row.chemical_name, 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, fertilizerId, chemicalId, isPlanned, isCompleted } = req.body; const stmt = db.prepare(` INSERT INTO maintenance_records (plant_id, date, type, description, amount, fertilizer_id, chemical_id, is_planned, is_completed) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `); const result = stmt.run(plantId, date, type, description, amount, fertilizerId || null, chemicalId || null, isPlanned ? 1 : 0, isCompleted ? 1 : 0); const getStmt = db.prepare(` SELECT mr.*, f.name as fertilizer_name, c.name as chemical_name FROM maintenance_records mr LEFT JOIN fertilizers f ON mr.fertilizer_id = f.id LEFT JOIN chemicals c ON mr.chemical_id = c.id WHERE mr.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, fertilizerId: row.fertilizer_id, chemicalId: row.chemical_id, fertilizerName: row.fertilizer_name, chemicalName: row.chemical_name, 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' }); }); // Fertilizers app.get('/api/fertilizers', (req, res) => { try { const stmt = db.prepare('SELECT * FROM fertilizers ORDER BY name ASC'); const rows = stmt.all(); res.json(rows.map(row => ({ id: row.id, name: row.name, brand: row.brand, type: row.type, npkRatio: row.npk_ratio, description: row.description, applicationRate: row.application_rate, frequency: row.frequency, season: row.season, notes: row.notes, createdAt: row.created_at, updatedAt: row.updated_at }))); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/api/fertilizers', (req, res) => { try { const { name, brand, type, npkRatio, description, applicationRate, frequency, season, notes } = req.body; const stmt = db.prepare(` INSERT INTO fertilizers (name, brand, type, npk_ratio, description, application_rate, frequency, season, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `); const result = stmt.run(name, brand, type, npkRatio, description, applicationRate, frequency, season, notes); const getStmt = db.prepare('SELECT * FROM fertilizers WHERE id = ?'); const row = getStmt.get(result.lastInsertRowid); res.json({ id: row.id, name: row.name, brand: row.brand, type: row.type, npkRatio: row.npk_ratio, description: row.description, applicationRate: row.application_rate, frequency: row.frequency, season: row.season, notes: row.notes, createdAt: row.created_at, updatedAt: row.updated_at }); } catch (err) { res.status(400).json({ error: err.message }); } }); // Chemicals app.get('/api/chemicals', (req, res) => { try { const stmt = db.prepare('SELECT * FROM chemicals ORDER BY name ASC'); const rows = stmt.all(); res.json(rows.map(row => ({ id: row.id, name: row.name, brand: row.brand, type: row.type, activeIngredient: row.active_ingredient, concentration: row.concentration, targetPests: row.target_pests, applicationMethod: row.application_method, safetyPeriod: row.safety_period, notes: row.notes, createdAt: row.created_at, updatedAt: row.updated_at }))); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/api/chemicals', (req, res) => { try { const { name, brand, type, activeIngredient, concentration, targetPests, applicationMethod, safetyPeriod, notes } = req.body; const stmt = db.prepare(` INSERT INTO chemicals (name, brand, type, active_ingredient, concentration, target_pests, application_method, safety_period, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `); const result = stmt.run(name, brand, type, activeIngredient, concentration, targetPests, applicationMethod, safetyPeriod, notes); const getStmt = db.prepare('SELECT * FROM chemicals WHERE id = ?'); const row = getStmt.get(result.lastInsertRowid); res.json({ id: row.id, name: row.name, brand: row.brand, type: row.type, activeIngredient: row.active_ingredient, concentration: row.concentration, targetPests: row.target_pests, applicationMethod: row.application_method, safetyPeriod: row.safety_period, notes: row.notes, createdAt: row.created_at, updatedAt: row.updated_at }); } catch (err) { res.status(400).json({ error: err.message }); } }); // 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); });