2025-07-28 22:22:46 +03:00

673 lines
22 KiB
JavaScript

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);
});
});