diff --git a/backend/database/gardentrack.db b/backend/database/gardentrack.db
index fb5fd06..6318f49 100644
Binary files a/backend/database/gardentrack.db and b/backend/database/gardentrack.db differ
diff --git a/backend/server.js b/backend/server.js
index a04a48e..58f5c3f 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -137,6 +137,7 @@ function initializeDatabase() {
)
`);
console.log('Chemicals table ready');
+
// Plant observations table
db.exec(`
CREATE TABLE IF NOT EXISTS plant_observations (
@@ -156,6 +157,42 @@ function initializeDatabase() {
`);
console.log('Plant observations table ready');
+ // Harvest processing table
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS harvest_processing (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ harvest_id INTEGER NOT NULL,
+ process_type TEXT NOT NULL CHECK(process_type IN ('fresh', 'frozen', 'jam', 'dried', 'canned', 'juice', 'sauce', 'pickled', 'other')),
+ quantity REAL NOT NULL,
+ unit TEXT NOT NULL,
+ process_date DATE NOT NULL,
+ expiry_date DATE,
+ storage_location TEXT,
+ notes TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (harvest_id) REFERENCES harvest_records (id) ON DELETE CASCADE
+ )
+ `);
+ console.log('Harvest processing table ready');
+
+ // Harvest stock table
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS harvest_stock (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ processing_id INTEGER NOT NULL,
+ current_quantity REAL NOT NULL,
+ unit TEXT NOT NULL,
+ last_updated DATE NOT NULL,
+ status TEXT DEFAULT 'available' CHECK(status IN ('available', 'consumed', 'expired', 'spoiled')),
+ notes TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (processing_id) REFERENCES harvest_processing (id) ON DELETE CASCADE
+ )
+ `);
+ console.log('Harvest stock table ready');
+
// Plant history table
db.exec(`
CREATE TABLE IF NOT EXISTS plant_history (
@@ -784,7 +821,7 @@ app.post('/api/harvests', (req, res) => {
const result = stmt.run(plantId, date, quantity, unit, notes);
- const getStmt = db.prepare('SELECT * FROM harvest_records WHERE id = ?');
+ const getStmt = db.prepare('SELECT * FROM harvest_records WHERE id = ? ORDER BY date DESC');
const row = getStmt.get(result.lastInsertRowid);
res.json({
@@ -802,6 +839,156 @@ app.post('/api/harvests', (req, res) => {
}
});
+// Harvest Processing
+app.get('/api/harvest-processing', (req, res) => {
+ try {
+ const stmt = db.prepare(`
+ SELECT hp.*, hr.plant_id, hr.date as harvest_date
+ FROM harvest_processing hp
+ JOIN harvest_records hr ON hp.harvest_id = hr.id
+ ORDER BY hp.process_date DESC
+ `);
+ const rows = stmt.all();
+
+ res.json(rows.map(row => ({
+ id: row.id,
+ harvestId: row.harvest_id,
+ plantId: row.plant_id,
+ harvestDate: row.harvest_date,
+ processType: row.process_type,
+ quantity: row.quantity,
+ unit: row.unit,
+ processDate: row.process_date,
+ expiryDate: row.expiry_date,
+ storageLocation: row.storage_location,
+ notes: row.notes,
+ createdAt: row.created_at,
+ updatedAt: row.updated_at
+ })));
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+app.post('/api/harvest-processing', (req, res) => {
+ try {
+ const { harvestId, processType, quantity, unit, processDate, expiryDate, storageLocation, notes } = req.body;
+
+ const stmt = db.prepare(`
+ INSERT INTO harvest_processing (harvest_id, process_type, quantity, unit, process_date, expiry_date, storage_location, notes)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ `);
+
+ const result = stmt.run(harvestId, processType, quantity, unit, processDate, expiryDate, storageLocation, notes);
+
+ // Create corresponding stock entry
+ const stockStmt = db.prepare(`
+ INSERT INTO harvest_stock (processing_id, current_quantity, unit, last_updated, status)
+ VALUES (?, ?, ?, ?, 'available')
+ `);
+ stockStmt.run(result.lastInsertRowid, quantity, unit, processDate);
+
+ const getStmt = db.prepare(`
+ SELECT hp.*, hr.plant_id, hr.date as harvest_date
+ FROM harvest_processing hp
+ JOIN harvest_records hr ON hp.harvest_id = hr.id
+ WHERE hp.id = ?
+ `);
+ const row = getStmt.get(result.lastInsertRowid);
+
+ res.json({
+ id: row.id,
+ harvestId: row.harvest_id,
+ plantId: row.plant_id,
+ harvestDate: row.harvest_date,
+ processType: row.process_type,
+ quantity: row.quantity,
+ unit: row.unit,
+ processDate: row.process_date,
+ expiryDate: row.expiry_date,
+ storageLocation: row.storage_location,
+ notes: row.notes,
+ createdAt: row.created_at,
+ updatedAt: row.updated_at
+ });
+ } catch (err) {
+ res.status(400).json({ error: err.message });
+ }
+});
+
+// Harvest Stock
+app.get('/api/harvest-stock', (req, res) => {
+ try {
+ const stmt = db.prepare(`
+ SELECT hs.*, hp.process_type, hp.harvest_id, hr.plant_id, hr.date as harvest_date
+ FROM harvest_stock hs
+ JOIN harvest_processing hp ON hs.processing_id = hp.id
+ JOIN harvest_records hr ON hp.harvest_id = hr.id
+ ORDER BY hs.last_updated DESC
+ `);
+ const rows = stmt.all();
+
+ res.json(rows.map(row => ({
+ id: row.id,
+ processingId: row.processing_id,
+ harvestId: row.harvest_id,
+ plantId: row.plant_id,
+ harvestDate: row.harvest_date,
+ processType: row.process_type,
+ currentQuantity: row.current_quantity,
+ unit: row.unit,
+ lastUpdated: row.last_updated,
+ status: row.status,
+ notes: row.notes,
+ createdAt: row.created_at,
+ updatedAt: row.updated_at
+ })));
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+app.put('/api/harvest-stock/:id', (req, res) => {
+ try {
+ const { currentQuantity, status, notes } = req.body;
+
+ const stmt = db.prepare(`
+ UPDATE harvest_stock SET current_quantity = ?, status = ?, notes = ?,
+ last_updated = date('now'), updated_at = CURRENT_TIMESTAMP
+ WHERE id = ?
+ `);
+
+ stmt.run(currentQuantity, status, notes, req.params.id);
+
+ const getStmt = db.prepare(`
+ SELECT hs.*, hp.process_type, hp.harvest_id, hr.plant_id, hr.date as harvest_date
+ FROM harvest_stock hs
+ JOIN harvest_processing hp ON hs.processing_id = hp.id
+ JOIN harvest_records hr ON hp.harvest_id = hr.id
+ WHERE hs.id = ?
+ `);
+ const row = getStmt.get(req.params.id);
+
+ res.json({
+ id: row.id,
+ processingId: row.processing_id,
+ harvestId: row.harvest_id,
+ plantId: row.plant_id,
+ harvestDate: row.harvest_date,
+ processType: row.process_type,
+ currentQuantity: row.current_quantity,
+ unit: row.unit,
+ lastUpdated: row.last_updated,
+ status: row.status,
+ 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' });
diff --git a/backend/uploads/photo_5247041613312685274_y.jpg b/backend/uploads/photo_5247041613312685274_y.jpg
new file mode 100644
index 0000000..dc12251
Binary files /dev/null and b/backend/uploads/photo_5247041613312685274_y.jpg differ
diff --git a/backend/uploads/photo_5247041613312685275_y.jpg b/backend/uploads/photo_5247041613312685275_y.jpg
new file mode 100644
index 0000000..62bacbe
Binary files /dev/null and b/backend/uploads/photo_5247041613312685275_y.jpg differ
diff --git a/backend/uploads/photo_5271893247570083300_y.jpg b/backend/uploads/photo_5271893247570083300_y.jpg
new file mode 100644
index 0000000..9f710a6
Binary files /dev/null and b/backend/uploads/photo_5271893247570083300_y.jpg differ
diff --git a/backend/uploads/plant-1753960452405-724160168.png b/backend/uploads/plant-1753960452405-724160168.png
new file mode 100644
index 0000000..393297a
Binary files /dev/null and b/backend/uploads/plant-1753960452405-724160168.png differ
diff --git a/backend/uploads/plant-1753994802271-447475482.png b/backend/uploads/plant-1753994802271-447475482.png
new file mode 100644
index 0000000..393297a
Binary files /dev/null and b/backend/uploads/plant-1753994802271-447475482.png differ
diff --git a/backend/uploads/plant-1754921088782-970283539.png b/backend/uploads/plant-1754921088782-970283539.png
new file mode 100644
index 0000000..7ddb0ae
Binary files /dev/null and b/backend/uploads/plant-1754921088782-970283539.png differ
diff --git a/backend/uploads/plant-1756392282774-773870385.jpg b/backend/uploads/plant-1756392282774-773870385.jpg
new file mode 100644
index 0000000..83c9f01
Binary files /dev/null and b/backend/uploads/plant-1756392282774-773870385.jpg differ
diff --git a/backend/uploads/plant-1756392409168-924448707.jpg b/backend/uploads/plant-1756392409168-924448707.jpg
new file mode 100644
index 0000000..a293f59
Binary files /dev/null and b/backend/uploads/plant-1756392409168-924448707.jpg differ
diff --git a/backend/uploads/plant-1756400176200-257499030.png b/backend/uploads/plant-1756400176200-257499030.png
new file mode 100644
index 0000000..21e481a
Binary files /dev/null and b/backend/uploads/plant-1756400176200-257499030.png differ
diff --git a/backend/uploads/plant-1756401137797-166836683.png b/backend/uploads/plant-1756401137797-166836683.png
new file mode 100644
index 0000000..21e481a
Binary files /dev/null and b/backend/uploads/plant-1756401137797-166836683.png differ
diff --git a/backend/uploads/plant-1756927577450-332932988.jpg b/backend/uploads/plant-1756927577450-332932988.jpg
new file mode 100644
index 0000000..bab2c20
Binary files /dev/null and b/backend/uploads/plant-1756927577450-332932988.jpg differ
diff --git a/package-lock.json b/package-lock.json
index 77f4ab0..eb2e37f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,13 +1,14 @@
{
"name": "vite-react-typescript-starter",
- "version": "0.0.0",
+ "version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vite-react-typescript-starter",
- "version": "0.0.0",
+ "version": "0.2.0",
"dependencies": {
+ "framer-motion": "^12.23.12",
"lucide-react": "^0.344.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
@@ -18,12 +19,10 @@
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.18",
- "concurrently": "^8.2.2",
"eslint": "^9.9.1",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.11",
"globals": "^15.9.0",
- "nodemon": "^3.0.2",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"typescript": "^5.5.3",
@@ -292,16 +291,6 @@
"@babel/core": "^7.0.0-0"
}
},
- "node_modules/@babel/runtime": {
- "version": "7.28.2",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
- "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/@babel/template": {
"version": "7.25.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz",
@@ -1831,120 +1820,6 @@
"node": ">= 6"
}
},
- "node_modules/cliui": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
- "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "string-width": "^4.2.0",
- "strip-ansi": "^6.0.1",
- "wrap-ansi": "^7.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/cliui/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cliui/node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/cliui/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/cliui/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/cliui/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/cliui/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cliui/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cliui/node_modules/wrap-ansi": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -1975,126 +1850,6 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
- "node_modules/concurrently": {
- "version": "8.2.2",
- "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
- "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chalk": "^4.1.2",
- "date-fns": "^2.30.0",
- "lodash": "^4.17.21",
- "rxjs": "^7.8.1",
- "shell-quote": "^1.8.1",
- "spawn-command": "0.0.2",
- "supports-color": "^8.1.1",
- "tree-kill": "^1.2.2",
- "yargs": "^17.7.2"
- },
- "bin": {
- "conc": "dist/bin/concurrently.js",
- "concurrently": "dist/bin/concurrently.js"
- },
- "engines": {
- "node": "^14.13.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
- }
- },
- "node_modules/concurrently/node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/concurrently/node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/concurrently/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/concurrently/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/concurrently/node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/concurrently/node_modules/supports-color": {
- "version": "8.1.1",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
- },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -2133,23 +1888,6 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true
},
- "node_modules/date-fns": {
- "version": "2.30.0",
- "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
- "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.21.0"
- },
- "engines": {
- "node": ">=0.11"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/date-fns"
- }
- },
"node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
@@ -2652,6 +2390,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
+ "node_modules/framer-motion": {
+ "version": "12.23.12",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz",
+ "integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.23.12",
+ "motion-utils": "^12.23.6",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2684,16 +2449,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/get-caller-file": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
- "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": "6.* || 8.* || >= 10.*"
- }
- },
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -2798,13 +2553,6 @@
"node": ">= 4"
}
},
- "node_modules/ignore-by-default": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
- "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -3040,13 +2788,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -3124,6 +2865,21 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/motion-dom": {
+ "version": "12.23.12",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz",
+ "integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.23.6"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.23.6",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
+ "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -3171,48 +2927,6 @@
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
"dev": true
},
- "node_modules/nodemon": {
- "version": "3.1.10",
- "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
- "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chokidar": "^3.5.2",
- "debug": "^4",
- "ignore-by-default": "^1.0.1",
- "minimatch": "^3.1.2",
- "pstree.remy": "^1.1.8",
- "semver": "^7.5.3",
- "simple-update-notifier": "^2.0.0",
- "supports-color": "^5.5.0",
- "touch": "^3.1.0",
- "undefsafe": "^2.0.5"
- },
- "bin": {
- "nodemon": "bin/nodemon.js"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/nodemon"
- }
- },
- "node_modules/nodemon/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -3548,13 +3262,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/pstree.remy": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
- "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3637,16 +3344,6 @@
"node": ">=8.10.0"
}
},
- "node_modules/require-directory": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
- "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -3741,16 +3438,6 @@
"queue-microtask": "^1.2.2"
}
},
- "node_modules/rxjs": {
- "version": "7.8.2",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
- "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.1.0"
- }
- },
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@@ -3789,19 +3476,6 @@
"node": ">=8"
}
},
- "node_modules/shell-quote": {
- "version": "1.8.3",
- "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
- "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -3814,32 +3488,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/simple-update-notifier": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
- "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "semver": "^7.5.3"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/simple-update-notifier/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -3849,12 +3497,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/spawn-command": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
- "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
- "dev": true
- },
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -4094,26 +3736,6 @@
"node": ">=8.0"
}
},
- "node_modules/touch": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
- "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "nodetouch": "bin/nodetouch.js"
- }
- },
- "node_modules/tree-kill": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
- "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "tree-kill": "cli.js"
- }
- },
"node_modules/ts-api-utils": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
@@ -4136,7 +3758,6 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
"license": "0BSD"
},
"node_modules/type-check": {
@@ -4187,13 +3808,6 @@
}
}
},
- "node_modules/undefsafe": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
- "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/update-browserslist-db": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
@@ -4443,16 +4057,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/y18n": {
- "version": "5.0.8",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
- "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -4471,80 +4075,6 @@
"node": ">= 14"
}
},
- "node_modules/yargs": {
- "version": "17.7.2",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
- "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cliui": "^8.0.1",
- "escalade": "^3.1.1",
- "get-caller-file": "^2.0.5",
- "require-directory": "^2.1.1",
- "string-width": "^4.2.3",
- "y18n": "^5.0.5",
- "yargs-parser": "^21.1.1"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/yargs-parser": {
- "version": "21.1.1",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
- "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/yargs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/yargs/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index ac7a059..7117958 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "vite-react-typescript-starter",
"private": true,
- "version": "0.2.0",
+ "version": "0.3.0",
"type": "module",
"scripts": {
"dev": "vite --mode development",
@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "framer-motion": "^12.23.12",
"lucide-react": "^0.344.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
diff --git a/src/App.tsx b/src/App.tsx
index 72d7c42..5e34ac7 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
-import {Sprout, Calendar, CheckSquare, Activity, BookOpen, Beaker, FlaskConical} from 'lucide-react';
+import { Sprout, Calendar, CheckSquare, Activity, BookOpen, Beaker, FlaskConical, Archive } from 'lucide-react';
import { Plant, Task, MaintenanceRecord, HarvestRecord, PlantObservation } from './types';
import Dashboard from './components/Dashboard';
import PlantRegistry from './components/PlantRegistry';
@@ -8,6 +8,7 @@ import MaintenanceLog from './components/MaintenanceLog';
import ObservationJournal from './components/ObservationJournal';
import FertilizerRegistry from './components/FertilizerRegistry';
import ChemicalRegistry from './components/ChemicalRegistry';
+import HarvestStockManager from './components/HarvestStockManager';
import { apiService } from './services/api';
function App() {
@@ -56,10 +57,11 @@ function App() {
{ id: 'dashboard', label: 'Панель', icon: Activity },
{ id: 'plants', label: 'Реестр растений', icon: Sprout },
{ id: 'tasks', label: 'План работ', icon: CheckSquare },
- { id: 'maintenance', label: 'Журнал', icon: Calendar },
+ { id: 'maintenance', label: 'Выполненные работы', icon: Calendar },
{ id: 'observations', label: 'Дневник', icon: BookOpen },
{ id: 'fertilizers', label: 'Удобрения', icon: FlaskConical },
{ id: 'chemicals', label: 'Химикаты', icon: Beaker },
+ { id: 'stock', label: 'Заготовки', icon: Archive },
];
if (loading) {
@@ -74,109 +76,129 @@ function App() {
}
return (
-
-
-
-
-
+
+ {/* Left Sidebar */}
+
+
+ {/* Logo */}
+
-
GardenTrack
+ GardenTrack
-
-
-
- {activeTab === 'dashboard' && (
-
- )}
- {activeTab === 'plants' && (
-
- )}
- {activeTab === 'tasks' && (
-
- )}
- {activeTab === 'maintenance' && (
-
- )}
- {activeTab === 'observations' && (
-
- )}
- {activeTab === 'fertilizers' && (
-
- )}
- {activeTab === 'chemicals' && (
-
- )}
-
+ {/* Main Content */}
+
+ {/* Mobile Header */}
+
- {/* Mobile Navigation */}
-
-
- {navItems.map((item) => {
- const Icon = item.icon;
- return (
-
- );
- })}
+ {/* Main Content Area */}
+
+ {activeTab === 'dashboard' && (
+
+ )}
+ {activeTab === 'plants' && (
+
+ )}
+ {activeTab === 'tasks' && (
+
+ )}
+ {activeTab === 'maintenance' && (
+
+ )}
+ {activeTab === 'observations' && (
+
+ )}
+ {activeTab === 'fertilizers' && (
+
+ )}
+ {activeTab === 'chemicals' && (
+
+ )}
+ {activeTab === 'stock' && (
+
+ )}
+
-
-
+
+ {/* Mobile Navigation */}
+
+
+ {navItems.map((item) => {
+ const Icon = item.icon;
+ return (
+
+ );
+ })}
+
+
+
);
}
+
export default App;
\ No newline at end of file
diff --git a/src/components/ChemicalRegistry.tsx b/src/components/ChemicalRegistry.tsx
index 92f3c05..fbd80e2 100644
--- a/src/components/ChemicalRegistry.tsx
+++ b/src/components/ChemicalRegistry.tsx
@@ -231,7 +231,7 @@ const ChemicalRegistry: React.FC = () => {
onClick={() => setShowForm(true)}
className="mt-4 text-green-600 hover:text-green-700 transition-colors"
>
- Add your first chemical →
+ Добавте первую запись о химикате →
)}
diff --git a/src/components/FertilizerRegistry.tsx b/src/components/FertilizerRegistry.tsx
index d42bdf8..ee9de46 100644
--- a/src/components/FertilizerRegistry.tsx
+++ b/src/components/FertilizerRegistry.tsx
@@ -231,7 +231,7 @@ const FertilizerRegistry: React.FC = () => {
onClick={() => setShowForm(true)}
className="mt-4 text-green-600 hover:text-green-700 transition-colors"
>
- Add your first fertilizer →
+ Добавте первую запись о удобрении →
)}
diff --git a/src/components/HarvestProcessingModal.tsx b/src/components/HarvestProcessingModal.tsx
new file mode 100644
index 0000000..a1a879a
--- /dev/null
+++ b/src/components/HarvestProcessingModal.tsx
@@ -0,0 +1,246 @@
+import React, { useState } from 'react';
+import { HarvestRecord, Plant, HarvestProcessing } from '../types';
+import { X } from 'lucide-react';
+import { apiService } from '../services/api';
+
+interface HarvestProcessingModalProps {
+ isOpen: boolean;
+ harvest: HarvestRecord;
+ plant: Plant;
+ onClose: () => void;
+ onProcessingAdded: () => void;
+}
+
+export const processTypes = [
+ { value: 'fresh', label: 'Fresh/Raw', color: 'bg-green-100 text-green-800' },
+ { value: 'frozen', label: 'Frozen', color: 'bg-blue-100 text-blue-800' },
+ { value: 'jam', label: 'Jam/Preserves', color: 'bg-purple-100 text-purple-800' },
+ { value: 'dried', label: 'Dried', color: 'bg-yellow-100 text-yellow-800' },
+ { value: 'canned', label: 'Canned', color: 'bg-orange-100 text-orange-800' },
+ { value: 'juice', label: 'Juice', color: 'bg-pink-100 text-pink-800' },
+ { value: 'sauce', label: 'Sauce', color: 'bg-red-100 text-red-800' },
+ { value: 'pickled', label: 'Pickled', color: 'bg-indigo-100 text-indigo-800' },
+ { value: 'other', label: 'Other', color: 'bg-gray-100 text-gray-800' }
+];
+
+export default function HarvestProcessingModal({
+ isOpen,
+ harvest,
+ plant,
+ onClose,
+ onProcessingAdded
+}: HarvestProcessingModalProps) {
+ const [formData, setFormData] = useState({
+ processType: 'fresh' as HarvestProcessing['processType'],
+ quantity: harvest.quantity.toString(),
+ unit: harvest.unit,
+ processDate: new Date().toISOString().split('T')[0],
+ expiryDate: '',
+ storageLocation: '',
+ notes: ''
+ });
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ try {
+ await apiService.createHarvestProcessing({
+ harvestId: harvest.id,
+ processType: formData.processType,
+ quantity: parseFloat(formData.quantity),
+ unit: formData.unit,
+ processDate: formData.processDate,
+ expiryDate: formData.expiryDate || undefined,
+ storageLocation: formData.storageLocation || undefined,
+ notes: formData.notes || undefined
+ });
+
+ onProcessingAdded();
+ onClose();
+ } catch (error) {
+ console.error('Error creating harvest processing:', error);
+ alert('Failed to create harvest processing record');
+ }
+ };
+
+ const handleChange = (e: React.ChangeEvent
) => {
+ const { name, value } = e.target;
+
+ if (name === 'unit') {
+ // Check if the new unit requires integer values
+ const countableUnits = ['pieces', 'jars', 'bottles', 'bags'];
+ const requiresInteger = countableUnits.includes(value);
+
+ setFormData(prev => ({
+ ...prev,
+ [name]: value,
+ // Convert quantity to integer if switching to a countable unit
+ quantity: requiresInteger ? Math.floor(parseFloat(prev.quantity) || 0).toString() : prev.quantity
+ }));
+ } else {
+ setFormData(prev => ({ ...prev, [name]: value }));
+ }
+ };
+
+ if (!isOpen) return null;
+
+ // Determine if current unit requires integer values
+ const countableUnits = ['pieces', 'jars', 'bottles', 'bags'];
+ const requiresInteger = countableUnits.includes(formData.unit);
+
+ return (
+
+
+
+
+
Обработка урожая
+
+ {plant.variety} - {harvest.quantity} {harvest.unit} собрано {new Date(harvest.date).toLocaleDateString()}
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/HarvestStockManager.tsx b/src/components/HarvestStockManager.tsx
new file mode 100644
index 0000000..61c4bce
--- /dev/null
+++ b/src/components/HarvestStockManager.tsx
@@ -0,0 +1,305 @@
+import { useState, useEffect } from 'react';
+import { Plant } from '../types';
+import { Package, Edit, X, Clock, MapPin, AlertTriangle } from 'lucide-react';
+import { apiService } from '../services/api';
+import { processTypes } from "./HarvestProcessingModal";
+
+interface HarvestStockManagerProps {
+ plants: Plant[];
+}
+
+export default function HarvestStockManager({ plants }: HarvestStockManagerProps) {
+ const [stocks, setStocks] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [editingStock, setEditingStock] = useState(null);
+ const [filterStatus, setFilterStatus] = useState('all');
+ const [filterPlant, setFilterPlant] = useState('all');
+
+ useEffect(() => {
+ loadStocks();
+ }, []);
+
+ const loadStocks = async () => {
+ try {
+ setLoading(true);
+ const data = await apiService.getHarvestStock();
+ setStocks(data);
+ } catch (error) {
+ console.error('Error loading harvest stock:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const filteredStocks = stocks.filter(stock => {
+ const matchesStatus = filterStatus === 'all' || stock.status === filterStatus;
+ const matchesPlant = filterPlant === 'all' || stock.plantId.toString() === filterPlant;
+ return matchesStatus && matchesPlant;
+ });
+
+ const handleUpdateStock = async (stockId: number, updates: any) => {
+ try {
+ const updatedStock = await apiService.updateHarvestStock(stockId, updates);
+ setStocks(stocks.map(s => s.id === stockId ? updatedStock : s));
+ setEditingStock(null);
+ } catch (error) {
+ console.error('Error updating stock:', error);
+ }
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'available': return 'bg-green-100 text-green-800';
+ case 'consumed': return 'bg-gray-100 text-gray-800';
+ case 'expired': return 'bg-red-100 text-red-800';
+ case 'spoiled': return 'bg-orange-100 text-orange-800';
+ default: return 'bg-gray-100 text-gray-800';
+ }
+ };
+
+ const getProcessTypeColor = (type: string) => {
+ switch (type) {
+ case 'fresh': return 'bg-green-100 text-green-800';
+ case 'frozen': return 'bg-blue-100 text-blue-800';
+ case 'jam': return 'bg-purple-100 text-purple-800';
+ case 'dried': return 'bg-yellow-100 text-yellow-800';
+ case 'canned': return 'bg-orange-100 text-orange-800';
+ case 'juice': return 'bg-pink-100 text-pink-800';
+ case 'sauce': return 'bg-red-100 text-red-800';
+ case 'pickled': return 'bg-indigo-100 text-indigo-800';
+ default: return 'bg-gray-100 text-gray-800';
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
Управление запасами
+
Отслеживайте свои запасы обработанного урожая
+
+
+ {/* Filters */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Stock Grid */}
+
+ {filteredStocks.map((stock) => {
+ const plant = plants.find(p => p.id === stock.plantId);
+ const isExpiringSoon = stock.expiryDate &&
+ new Date(stock.expiryDate) <= new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
+
+ return (
+
+
+
+
+
+
+
{plant?.variety}
+
+ Собрано: {new Date(stock.harvestDate).toLocaleDateString()}
+
+
+
+
+
+
+
+
+ Тип:
+
+ {processTypes.find(t => t.value === stock.processType)?.label}
+
+
+
+
+ Количество:
+
+ {stock.currentQuantity} {stock.unit}
+
+
+
+
+ Состояние:
+
+ {stock.status}
+
+
+
+ {stock.storageLocation && (
+
+
+ {stock.storageLocation}
+
+ )}
+
+ {stock.expiryDate && (
+
+
+
+ Срок годности: {new Date(stock.expiryDate).toLocaleDateString()}
+
+ {isExpiringSoon &&
}
+
+ )}
+
+
+ Обновлено: {new Date(stock.lastUpdated).toLocaleDateString()}
+
+
+ {stock.notes && (
+
+ )}
+
+
+
+ );
+ })}
+
+
+ {filteredStocks.length === 0 && (
+
+
No harvest stock found.
+
Process some harvests to see stock here.
+
+ )}
+
+ {/* Edit Stock Modal */}
+ {editingStock && (
+
+
+
+
Изменить запасы
+
+
+
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ImageUpload.tsx b/src/components/ImageUpload.tsx
new file mode 100644
index 0000000..28fd767
--- /dev/null
+++ b/src/components/ImageUpload.tsx
@@ -0,0 +1,157 @@
+import React, { useState, useRef } from 'react';
+import { Upload, Image, X } from 'lucide-react';
+
+interface ImageUploadProps {
+ currentImageUrl?: string;
+ onImageChange: (imageUrl: string) => void;
+ onFileSelect: (file: File) => void;
+ uploading?: boolean;
+ className?: string;
+}
+
+export default function ImageUpload({
+ currentImageUrl,
+ onImageChange,
+ onFileSelect,
+ uploading = false,
+ className = ""
+}: ImageUploadProps) {
+ const [isDragOver, setIsDragOver] = useState(false);
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [previewUrl, setPreviewUrl] = useState(null);
+ const fileInputRef = useRef(null);
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragOver(true);
+ };
+
+ const handleDragLeave = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragOver(false);
+ };
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragOver(false);
+
+ const files = e.dataTransfer.files;
+ if (files.length > 0) {
+ const file = files[0];
+ if (file.type.startsWith('image/')) {
+ handleFileSelection(file);
+ } else {
+ alert('Please select an image file');
+ }
+ }
+ };
+
+ const handleFileInputChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ handleFileSelection(file);
+ }
+ };
+
+ const handleFileSelection = (file: File) => {
+ setSelectedFile(file);
+ onFileSelect(file);
+
+ // Create preview URL
+ const url = URL.createObjectURL(file);
+ setPreviewUrl(url);
+ };
+
+ const handleRemoveImage = () => {
+ setSelectedFile(null);
+ setPreviewUrl(null);
+ onImageChange('');
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ const displayImageUrl = previewUrl || currentImageUrl;
+
+ return (
+
+ {/* Drag and Drop Area */}
+
fileInputRef.current?.click()}
+ className={`
+ relative border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-all
+ ${isDragOver
+ ? 'border-green-400 bg-green-50'
+ : 'border-gray-300 hover:border-green-400 hover:bg-green-50'
+ }
+ ${uploading ? 'opacity-50 pointer-events-none' : ''}
+ `}
+ >
+
+
+ {uploading ? (
+
+
+
Изображение загружается...
+
+ ) : (
+
+
+
+
+ {isDragOver ? 'Перетащите изображение сюда' : 'Перетащите изображение или нажмите, чтобы выбрать'}
+
+
PNG, JPG, GIF до 5MB
+
+
+ )}
+
+
+ {/* Image Preview */}
+ {displayImageUrl && (
+
+
)
{
+ console.log('Image failed to load:', displayImageUrl);
+ (e.target as HTMLImageElement).style.display = 'none';
+ }}
+ />
+
+
+ )}
+
+ {/* File Info */}
+ {selectedFile && (
+
+
+
+
+ Выбран файл: {selectedFile.name} ({(selectedFile.size / 1024 / 1024).toFixed(2)} MB)
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/MaintenanceLog.tsx b/src/components/MaintenanceLog.tsx
index 869acbf..ef69910 100644
--- a/src/components/MaintenanceLog.tsx
+++ b/src/components/MaintenanceLog.tsx
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
-import { Plant, MaintenanceRecord } from '../types';
+import { Plant, MaintenanceRecord, Maintenances } from '../types';
import { Plus, Calendar, Search, Scissors, Droplets, Beaker, Sprout } from 'lucide-react';
import MaintenanceForm from './MaintenanceForm';
import { apiService } from '../services/api';
@@ -146,9 +146,9 @@ const MaintenanceLog: React.FC = ({
-
{record.type}
+
{Maintenances[record.type]}
- {record.type.replace('-', ' ')}
+ {Maintenances[record.type]}
{record.fertilizerName && (
diff --git a/src/components/ObservationForm.tsx b/src/components/ObservationForm.tsx
index bad7d50..09f0ee4 100644
--- a/src/components/ObservationForm.tsx
+++ b/src/components/ObservationForm.tsx
@@ -1,6 +1,8 @@
import React, { useState, useEffect } from 'react';
import {Plant, PlantObservation, PlantTitles} from '../types';
import { X } from 'lucide-react';
+import ImageUpload from './ImageUpload';
+import { apiService } from '../services/api';
interface ObservationFormProps {
observation?: PlantObservation | null;
@@ -21,6 +23,9 @@ const ObservationForm: React.FC = ({ observation, plants,
tags: ''
});
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [uploading, setUploading] = useState(false);
+
useEffect(() => {
if (observation) {
setFormData({
@@ -36,14 +41,33 @@ const ObservationForm: React.FC = ({ observation, plants,
}
}, [observation]);
- const handleSubmit = (e: React.FormEvent) => {
+ const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
+
+ let finalPhotoUrl = formData.photoUrl;
+
+ // Upload image if a file is selected
+ if (selectedFile) {
+ try {
+ setUploading(true);
+ const result = await apiService.uploadPhoto(selectedFile);
+ finalPhotoUrl = result.photoUrl;
+ } catch (error) {
+ console.error('Error uploading photo:', error);
+ alert('Failed to upload photo. Please try again.');
+ setUploading(false);
+ return;
+ } finally {
+ setUploading(false);
+ }
+ }
+
onSave({
...formData,
plantId: parseInt(formData.plantId),
temperature: formData.temperature ? parseFloat(formData.temperature) : undefined,
weatherConditions: formData.weatherConditions || undefined,
- photoUrl: formData.photoUrl || undefined,
+ photoUrl: finalPhotoUrl || undefined,
tags: formData.tags || undefined
});
};
@@ -53,6 +77,14 @@ const ObservationForm: React.FC = ({ observation, plants,
setFormData(prev => ({ ...prev, [name]: value }));
};
+ const handleFileSelect = (file: File) => {
+ setSelectedFile(file);
+ };
+
+ const handleImageChange = (imageUrl: string) => {
+ setFormData(prev => ({ ...prev, photoUrl: imageUrl }));
+ };
+
return (
@@ -173,17 +205,14 @@ const ObservationForm: React.FC = ({ observation, plants,
-
@@ -214,10 +243,11 @@ const ObservationForm: React.FC
= ({ observation, plants,
Отмена
diff --git a/src/components/ObservationJournal.tsx b/src/components/ObservationJournal.tsx
index 00064e0..6551853 100644
--- a/src/components/ObservationJournal.tsx
+++ b/src/components/ObservationJournal.tsx
@@ -3,6 +3,7 @@ import { Plant, PlantObservation } from '../types';
import { Plus, Search, Edit, Trash2, Thermometer, Cloud, Tag } from 'lucide-react';
import ObservationForm from './ObservationForm';
import { apiService } from '../services/api';
+import { ImagePreview } from "./utils/ImagePreview";
interface ObservationJournalProps {
observations: PlantObservation[];
@@ -20,16 +21,23 @@ const ObservationJournal: React.FC = ({
const [searchTerm, setSearchTerm] = useState('');
const [filterPlant, setFilterPlant] = useState('all');
const [filterTag, setFilterTag] = useState('all');
+ const [filterYear, setFilterYear] = useState('all');
+ const [filterMonth, setFilterMonth] = useState('all');
const filteredObservations = observations.filter(observation => {
const plant = plants.find(p => p.id === observation.plantId);
+ const observationDate = new Date(observation.date);
+
const matchesSearch = observation.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
observation.observation.toLowerCase().includes(searchTerm.toLowerCase()) ||
(plant?.variety.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesPlant = filterPlant === 'all' || observation.plantId.toString() === filterPlant;
const matchesTag = filterTag === 'all' || (observation.tags && observation.tags.includes(filterTag));
-
- return matchesSearch && matchesPlant && matchesTag;
+
+ const matchesYear = filterYear === 'all' || observationDate.getFullYear().toString() === filterYear;
+ const matchesMonth = filterMonth === 'all' || (observationDate.getMonth() + 1).toString() === filterMonth;
+
+ return matchesSearch && matchesPlant && matchesTag && matchesYear && matchesMonth;
}).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
const allTags = Array.from(new Set(
@@ -38,6 +46,27 @@ const ObservationJournal: React.FC = ({
.flatMap(obs => obs.tags!.split(',').map(tag => tag.trim()))
)).sort();
+ // Get available years from observations
+ const availableYears = Array.from(new Set(
+ observations.map(obs => new Date(obs.date).getFullYear())
+ )).sort((a, b) => b - a);
+
+ // Month options
+ const months = [
+ { value: '1', label: 'Январь' },
+ { value: '2', label: 'Февраль' },
+ { value: '3', label: 'Март' },
+ { value: '4', label: 'Апрель' },
+ { value: '5', label: 'Май' },
+ { value: '6', label: 'Июнь' },
+ { value: '7', label: 'Июль' },
+ { value: '8', label: 'Август' },
+ { value: '9', label: 'Сентябрь' },
+ { value: '10', label: 'Октябрь' },
+ { value: '11', label: 'Ноябрь' },
+ { value: '12', label: 'Декабрь' }
+ ];
+
const handleSaveObservation = async (observationData: Omit) => {
try {
if (editingObservation) {
@@ -87,7 +116,7 @@ const ObservationJournal: React.FC = ({
{/* Filters */}
-
+
Поиск
@@ -101,7 +130,39 @@ const ObservationJournal: React.FC = ({
/>
-
+
+
+ Год
+
+
+
+
+ Месяц
+
+
+
Растения
-
-
-
- Фотография
-
-
- {/* Photo Upload Section */}
-
-
-
-
-
- Choose Photo
-
-
- {selectedFile && (
-
- )}
-
-
- {selectedFile && (
-
- Selected: {selectedFile.name}
-
- )}
-
- {/* Manual URL input as fallback */}
-
- Or enter photo URL manually:
-
-
-
-
-
-
- {/* Photo Preview */}
- {formData.photoUrl && (
-
-
Photo Preview
-
)
{
- console.log('Image failed to load: ' + e, formData.photoUrl);
- }}
- />
-
- )}
-
+
+
+ Plant Photo
+
+
@@ -347,10 +280,11 @@ const PlantForm: React.FC
= ({ plant, onSave, onCancel }) => {
Отмена
diff --git a/src/components/PlantRegistry.tsx b/src/components/PlantRegistry.tsx
index 1c02d81..d65cc5c 100644
--- a/src/components/PlantRegistry.tsx
+++ b/src/components/PlantRegistry.tsx
@@ -1,10 +1,12 @@
import React, { useState } from 'react';
import {Plant, HarvestRecord, PlantTitles, Conditions, HarvestUnits} from '../types';
-import { Plus, Search, Edit, Trash2, Calendar, Award, BarChart3, Grid3X3, List } from 'lucide-react';
+import { Plus, Search, Edit, Trash2, Calendar, Award, BarChart3, Grid3X3, List, Package } from 'lucide-react';
import PlantForm from './PlantForm';
import HarvestForm from './HarvestForm';
import PlantStatistics from './PlantStatistics';
import { apiService } from '../services/api';
+import HarvestProcessingModal from './HarvestProcessingModal';
+import { ImagePreview } from "./utils/ImagePreview.tsx";
interface PlantRegistryProps {
plants: Plant[];
@@ -14,6 +16,9 @@ interface PlantRegistryProps {
maintenanceRecords: any[];
}
+//const APP_MODE = import.meta.env.VITE_ENV;
+//const API_BASE = APP_MODE == "development" ? import.meta.env.VITE_API_URL : "";
+
const PlantRegistry: React.FC
= ({
plants,
onPlantsChange,
@@ -27,6 +32,8 @@ const PlantRegistry: React.FC = ({
const [editingPlant, setEditingPlant] = useState(null);
const [selectedPlantForHarvest, setSelectedPlantForHarvest] = useState(null);
const [selectedPlantForStats, setSelectedPlantForStats] = useState(null);
+ const [showProcessingModal, setShowProcessingModal] = useState(false);
+ const [selectedHarvestForProcessing, setSelectedHarvestForProcessing] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const [filterType, setFilterType] = useState('all');
const [filterStatus, setFilterStatus] = useState('all');
@@ -210,7 +217,7 @@ const PlantRegistry: React.FC = ({
setShowStatistics(true);
}}
className="p-1 text-purple-600 hover:text-purple-700 transition-colors"
- title="Посмотреть Статистику"
+ title="Статистика"
>
@@ -220,10 +227,25 @@ const PlantRegistry: React.FC = ({
setShowHarvestForm(true);
}}
className="p-1 text-green-600 hover:text-green-700 transition-colors"
- title="Добавить урожай"
+ title="Урожай"
>
+