From 12d0f3708d3b7374871601b4685981810e9b4c6a Mon Sep 17 00:00:00 2001 From: anibilag Date: Sun, 10 Aug 2025 23:46:53 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=87=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F=200.2.0=20=D0=94=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA=D0=B0,=20=D0=B2=D0=B8?= =?UTF-8?q?=D0=B4=20=D1=80=D0=B0=D1=81=D1=82=D0=B5=D0=BD=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA=D0=B0?= =?UTF-8?q?=D0=BC=20=D0=B8=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D0=B5?= =?UTF-8?q?=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/database/gardentrack.db | Bin 32768 -> 49152 bytes backend/package-lock.json | 143 ++++++++- backend/package.json | 3 +- backend/scripts/init-db.js | 89 +++++- backend/server.js | 341 ++++++++++++++++++++-- package.json | 2 +- src/App.tsx | 14 +- src/components/ChemicalForm.tsx | 244 ++++++++++++++++ src/components/ChemicalRegistry.tsx | 254 ++++++++++++++++ src/components/Dashboard.tsx | 14 +- src/components/FertilizerForm.tsx | 243 ++++++++++++++++ src/components/FertilizerRegistry.tsx | 254 ++++++++++++++++ src/components/HarvestForm.tsx | 28 +- src/components/MaintenanceForm.tsx | 104 ++++++- src/components/MaintenanceLog.tsx | 18 +- src/components/ObservationForm.tsx | 6 +- src/components/ObservationJournal.tsx | 2 +- src/components/PlantForm.tsx | 337 +++++++++++++-------- src/components/PlantRegistry.tsx | 253 ++++++++++++++-- src/components/PlantStatistics.tsx | 33 ++- src/components/TaskForm.tsx | 402 ++++++++++++++++++-------- src/components/TaskPlanner.tsx | 72 ++++- src/components/words/FruitsWord.tsx | 22 ++ src/services/api.ts | 84 +++++- src/types/index.ts | 118 +++++++- 25 files changed, 2710 insertions(+), 370 deletions(-) create mode 100644 src/components/ChemicalForm.tsx create mode 100644 src/components/ChemicalRegistry.tsx create mode 100644 src/components/FertilizerForm.tsx create mode 100644 src/components/FertilizerRegistry.tsx create mode 100644 src/components/words/FruitsWord.tsx diff --git a/backend/database/gardentrack.db b/backend/database/gardentrack.db index 0691ddff748c6439bd4a9f017a58a2f540f85c77..fb5fd06334326cabd96b955063dac52e1335cd3a 100644 GIT binary patch literal 49152 zcmeHQYit`=b|&?r7Y-#;j$l2!l8jwX0$X5zRLRDP^K$-eQJ@7VGzEeJ1@fmr|Fu9LXtBkj z-?_tikQzF6vWD&CGO;~#pXYq%+~?eT&g!=pREx6>-6$v)ON0)D!r{>4jD

Z{klk z{y4u*JhVA4_&4l(Zua=i&}i}9c1qm)TT1g#?{7PQ*YQc)$E`iB6fA!dfqRcYM`wp1 z;>yWLr0>|V@YNHRvaa#GVqD~=l`(i$H*#jfYv=4rI<=N&YpI!qG;4@tL;G0mr{>uF z(pvgddWD@?nO{t;oMWfc=UD3O+VcDo%2-SS`H#iu+mJ(vL3(Bjer5oFIFOfPC)43g&a^s?6sd~}0rR@*t2+#(9{M}RuS7rh2675;z%^Izbz-CFd z(1i2c$Yk|m4$1@G8w70eLWvuSRW^8oYBW;`OWABJH#C}Mtz4GoqsM5$Rd#|KB) zAmqI6zHF3>o9>HRG+bV(^@b4x@n@XiyfRP!(uVL!6}; zo7~C}HI1sFlu8<4rA(OuxAJ-;m!`78t!?BrRK(RrOOLF3H88p3Nq1|cZ(tyNEhDsK zgBzBrsZVpm^gXuKX^Jo6e$o})*i?#YmNdoOE}~a}63~mLUMQiRXkem=`pxpE9ORKW}(#ty7AI$RX+sMv!y(nWw^$zW)C34^Df7l|4Y2Ep;V` zK?~C#ce?1CDBi~8Qgj+kW`7qILq#JL0tgs{N@XLPhaO}!-EPUv5}KUnG@79(o76$s z6W_g)95!cK&PR-y^w>e?pY}NdzPU5&?;TM8HL$a`-@R=-}yOxa-I~5U&G5z_08QEs~=Y1!_!Zz*Q+9h<{fbLA^v`m*!zGO1kZ1QUjzmb(>@l9#RB1SDvnJND#_S%!llI*R4Mly2=$#l z9gZA17zu?cryOE_36WfCPRh@P9G@e?K$?>V_n5zxGpvqJ?v>~O|_&yP&4b|6$DqN|40Cji;j9#u`a((wQ zgK&1Lzs8?;2m@CA^Xf}bnfLG?aO*zt4+-&u5aQ9;R5U&z`2P<(wE8MI70m+wzKQ&# z(vZ1F`y`8xPmd?2<72KEouKh#W#S>yWI;RHS|IIA5WiD>7iC`;q~9R}pW33TXQ1VF zD347|Cy!6ZlRnCwpvvR>K>4zYUg97q|9%biA3~PTIeo+{K=dZMjT@r<30$hL0q3^} zD=65zd!VOo;Vs}uy}=9B>)kfZ$7@R0QvD^hDhQ7h2I@&IYlqqv6pe%s06yp)pO_vW zuV-O=5?obMU0~sa!-5{f!q1>Lq@Pz&≤8C$)}|Xth=5Z4xKIxsLvH_X<=Wbx>WG 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 delta 1422 zcma)5Pi)&%825{lILT@|g(jvMQ(qQUS=X8+c0$*3U~Lt35>X2@lK=^|PW#rF89O{X z9YIJ<%PMw2g6VPPFe+{9Kn#&_;KGSJoir{W{z$uQ5)u;P)WgK*G+im`p^;uJe}3=x z{r-Hf)#6*Nz$eFhC641Fu=al8Ya{w`iF?lePK1&Wyw8a{;;kMnb~ECHcY^oZ`S=?_ z{z^t*zl3ur((V!dMrYP(SWpxNZA^Ja)gqqZUUEBs!qls!iAo7qCML@z+$rHxF_sRU zX7L++Y*SF!AMo6{kz%Qv11uBHv!}nvvuokAeRHJ>p1Dvi$A+#HLxQ5~=xQ(f{njuF zvtwI>EE77#UJ0FGZ9d61x87l?+b2ZhZKq*-xKetpG7@7S-9E{i?mPge_&mI)!+IC| z+_9k#B0)hJ9!49}?8PrLD8ydi(+4nr_JdfDElI`pF*0;aD*V_Jm`&-ynRCA-M zb{6Ny#tVh=PL6jInX_VkNRo7>2kVf#F=TO4e2(3;Uu4!Qv z&s`?O!_%w8c2A^=uUb44|u%Pc$2(s_eI*&|CB_Fb}?PkJ5Fe!sLP1Y|CYlp^#8 zozQ>Upi9KYQy_@Yh+CtL>ZH{mRYGYKSWK?M%OZ8^{m?*Hb2`{@;eRg7_@>~CgnFiB zzDMZMsK70JH;WQ^a2%Wf$ICDt7M^KPm(XU9M+upz*R3}RusHA7q`62&js5!{kRNgKKk}n?@4~OL5)JSQ>Re(zhWLI|w+!2J`EM3bd=o8nmVbzs z9&qxf@|g4hhUO<}wZca^l+C6iy3Tjwz*RrJ(h4ZRS|=G7NI`DTtGWEq=)E2Q)H6fr zsICWIrVW^lYBvv)xN~*ti>*W5T(y^Xu{D^vqhY^=qFhv+^%?u#Hvbb^7kNKLe9@uU afL#RpS#)*ia1FY>1`Zwd{aO2bA^tC8Wr;Ze diff --git a/backend/package-lock.json b/backend/package-lock.json index d9d32ff..7ef559e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,7 +13,8 @@ "body-parser": "^1.20.2", "cors": "^2.8.5", "dotenv": "^16.3.1", - "express": "^4.18.2" + "express": "^4.18.2", + "multer": "^1.4.5-lts.1" }, "devDependencies": { "nodemon": "^3.0.1" @@ -46,6 +47,12 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -195,6 +202,23 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -265,6 +289,51 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -301,6 +370,12 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -834,6 +909,12 @@ "node": ">=0.12.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -937,6 +1018,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -949,6 +1042,25 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -1136,6 +1248,12 @@ "node": ">=10" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1484,6 +1602,14 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -1606,6 +1732,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -1651,6 +1783,15 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } } } } diff --git a/backend/package.json b/backend/package.json index 6a10511..1f8e5d2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,7 +14,8 @@ "better-sqlite3": "^8.7.0", "cors": "^2.8.5", "body-parser": "^1.20.2", - "dotenv": "^16.3.1" + "dotenv": "^16.3.1", + "multer": "^1.4.5-lts.1" }, "devDependencies": { "nodemon": "^3.0.1" diff --git a/backend/scripts/init-db.js b/backend/scripts/init-db.js index 8f2c1d5..31f95b7 100644 --- a/backend/scripts/init-db.js +++ b/backend/scripts/init-db.js @@ -41,6 +41,42 @@ try { ) `); + // 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 + ) + `); + + // 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 + ) + `); + // Plant history table db.exec(` CREATE TABLE IF NOT EXISTS plant_history ( @@ -70,7 +106,6 @@ try { ) `); - // Maintenance records table db.exec(` CREATE TABLE IF NOT EXISTS maintenance_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -79,9 +114,13 @@ try { 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, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (plant_id) REFERENCES plants (id) ON DELETE CASCADE + 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 ) `); @@ -104,7 +143,29 @@ try { // Insert sample data console.log('Inserting sample data...'); - + + // 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 (purchase_location, seedling_age, type, variety, seedling_height, planting_date, current_height, health_status, photo_url, current_photo_url, notes) @@ -120,20 +181,22 @@ try { 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); + + insertTask.run(1, 'fertilizer', 'Apply spring fertilizer', 'Apply all-purpose fertilizer to apple trees for spring growth', '2024-03-15', 0, 1, null); + insertTask.run(2, 'pruning', 'Prune blueberry bushes', 'Annual pruning before spring growth', '2024-02-28', 1, null, null); + insertTask.run(3, 'harvesting', 'Harvest basil', 'Regular harvest to encourage growth', '2024-06-01', 0, null, null); + insertTask.run(1, 'chemical', 'Apply neem oil treatment', 'Preventive neem oil application for pest control', '2024-04-01', 0, null, 1); // Sample maintenance records const insertMaintenance = db.prepare(` - INSERT INTO maintenance_records (plant_id, date, type, description, amount) - VALUES (?, ?, ?, ?, ?) + INSERT INTO maintenance_records (plant_id, date, type, description, amount, fertilizer_id, chemical_id) + VALUES (?, ?, ?, ?, ?, ?, ?) `); - - insertMaintenance.run(1, '2024-01-10', 'pruning', 'Winter pruning - removed dead branches', null); - insertMaintenance.run(2, '2024-01-05', 'fertilizer', 'Applied organic compost', '2 cups'); - insertMaintenance.run(3, '2024-05-15', 'watering', 'Deep watering during dry spell', '1 gallon'); + + insertMaintenance.run(1, '2024-01-10', 'pruning', 'Winter pruning - removed dead branches', null, null, null); + insertMaintenance.run(2, '2024-01-05', 'fertilizer', 'Applied organic compost', '2 cups', 2, null); + insertMaintenance.run(3, '2024-05-15', 'watering', 'Deep watering during dry spell', '1 gallon', null, null); + insertMaintenance.run(1, '2024-03-20', 'chemical', 'Applied neem oil for aphid prevention', '2 tablespoons per gallon', null, 1); // Sample harvest records const insertHarvest = db.prepare(` diff --git a/backend/server.js b/backend/server.js index e703afe..a04a48e 100644 --- a/backend/server.js +++ b/backend/server.js @@ -4,6 +4,7 @@ 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(); @@ -11,6 +12,13 @@ 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 }); @@ -22,6 +30,39 @@ 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 { @@ -59,6 +100,43 @@ function initializeDatabase() { `); 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 ( @@ -121,11 +199,15 @@ function initializeDatabase() { 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 (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'); @@ -135,13 +217,18 @@ function initializeDatabase() { 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 (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'); @@ -164,6 +251,28 @@ function initializeDatabase() { // 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) @@ -186,13 +295,14 @@ function insertSampleData() { // Sample maintenance records const insertMaintenance = db.prepare(` - INSERT INTO maintenance_records (plant_id, date, type, description, amount, is_planned, is_completed) - VALUES (?, ?, ?, ?, ?, ?, ?) + 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, 0, 1); - insertMaintenance.run(2, '2024-01-05', 'fertilizer', 'Applied organic compost', '2 cups', 0, 1); - insertMaintenance.run(3, '2024-05-15', 'watering', 'Deep watering during dry spell', '1 gallon', 0, 1); + 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(` @@ -221,6 +331,21 @@ function insertSampleData() { // 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 { @@ -433,16 +558,27 @@ app.delete('/api/plants/:id', (req, res) => { // Tasks app.get('/api/tasks', (req, res) => { try { - const stmt = db.prepare('SELECT * FROM tasks ORDER BY deadline ASC'); + 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 }))); @@ -453,25 +589,36 @@ app.get('/api/tasks', (req, res) => { app.post('/api/tasks', (req, res) => { try { - const { plantId, title, description, deadline, completed = false } = req.body; + const { plantId, type, title, description, deadline, completed = false, fertilizerId, chemicalId } = req.body; const stmt = db.prepare(` - INSERT INTO tasks (plant_id, title, description, deadline, completed) - VALUES (?, ?, ?, ?, ?) + INSERT INTO tasks (plant_id, type, title, description, deadline, completed, fertilizer_id, chemical_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); - const result = stmt.run(plantId, title, description, deadline, completed ? 1 : 0); + const result = stmt.run(plantId, type, title, description, deadline, completed ? 1 : 0, fertilizerId || null, chemicalId || null); - const getStmt = db.prepare('SELECT * FROM tasks WHERE 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(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 }); @@ -482,26 +629,37 @@ app.post('/api/tasks', (req, res) => { app.put('/api/tasks/:id', (req, res) => { try { - const { plantId, title, description, deadline, completed } = req.body; + const { plantId, type, title, description, deadline, completed, fertilizerId, chemicalId } = req.body; const stmt = db.prepare(` - UPDATE tasks SET plant_id = ?, title = ?, description = ?, - deadline = ?, completed = ?, updated_at = CURRENT_TIMESTAMP + UPDATE tasks SET plant_id = ?, type = ?, title = ?, description = ?, + deadline = ?, completed = ?, fertilizer_id = ?, chemical_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `); - stmt.run(plantId, title, description, deadline, completed ? 1 : 0, req.params.id); + stmt.run(plantId, type, title, description, deadline, completed ? 1 : 0, fertilizerId || null, chemicalId || null, req.params.id); - const getStmt = db.prepare('SELECT * FROM tasks WHERE 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 }); @@ -523,7 +681,13 @@ app.delete('/api/tasks/:id', (req, res) => { // Maintenance Records app.get('/api/maintenance', (req, res) => { try { - const stmt = db.prepare('SELECT * FROM maintenance_records ORDER BY date DESC'); + 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 => ({ @@ -533,6 +697,10 @@ app.get('/api/maintenance', (req, res) => { 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, @@ -545,16 +713,22 @@ app.get('/api/maintenance', (req, res) => { app.post('/api/maintenance', (req, res) => { try { - const { plantId, date, type, description, amount, isPlanned, isCompleted } = req.body; + 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, is_planned, is_completed) - VALUES (?, ?, ?, ?, ?, ?, ?) + 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, isPlanned ? 1 : 0, isCompleted ? 1 : 0); + const result = stmt.run(plantId, date, type, description, amount, fertilizerId || null, chemicalId || null, isPlanned ? 1 : 0, isCompleted ? 1 : 0); - const getStmt = db.prepare('SELECT * FROM maintenance_records WHERE id = ?'); + 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({ @@ -564,6 +738,10 @@ app.post('/api/maintenance', (req, res) => { 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, @@ -629,6 +807,121 @@ 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 diff --git a/package.json b/package.json index 9cac8f2..ac7a059 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vite-react-typescript-starter", "private": true, - "version": "0.1.0", + "version": "0.2.0", "type": "module", "scripts": { "dev": "vite --mode development", diff --git a/src/App.tsx b/src/App.tsx index 329414b..72d7c42 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,13 @@ -import React, { useState, useEffect } from 'react'; -import { Sprout, Calendar, CheckSquare, Activity, Plus, BookOpen } from 'lucide-react'; +import { useState, useEffect } from 'react'; +import {Sprout, Calendar, CheckSquare, Activity, BookOpen, Beaker, FlaskConical} from 'lucide-react'; import { Plant, Task, MaintenanceRecord, HarvestRecord, PlantObservation } from './types'; import Dashboard from './components/Dashboard'; import PlantRegistry from './components/PlantRegistry'; import TaskPlanner from './components/TaskPlanner'; import MaintenanceLog from './components/MaintenanceLog'; import ObservationJournal from './components/ObservationJournal'; +import FertilizerRegistry from './components/FertilizerRegistry'; +import ChemicalRegistry from './components/ChemicalRegistry'; import { apiService } from './services/api'; function App() { @@ -56,6 +58,8 @@ function App() { { id: 'tasks', label: 'План работ', icon: CheckSquare }, { id: 'maintenance', label: 'Журнал', icon: Calendar }, { id: 'observations', label: 'Дневник', icon: BookOpen }, + { id: 'fertilizers', label: 'Удобрения', icon: FlaskConical }, + { id: 'chemicals', label: 'Химикаты', icon: Beaker }, ]; if (loading) { @@ -141,6 +145,12 @@ function App() { onObservationsChange={setObservations} /> )} + {activeTab === 'fertilizers' && ( + + )} + {activeTab === 'chemicals' && ( + + )} {/* Mobile Navigation */} diff --git a/src/components/ChemicalForm.tsx b/src/components/ChemicalForm.tsx new file mode 100644 index 0000000..89f7e7f --- /dev/null +++ b/src/components/ChemicalForm.tsx @@ -0,0 +1,244 @@ +import React, { useState, useEffect } from 'react'; +import { Chemical } from '../types'; +import { X } from 'lucide-react'; + +interface ChemicalFormProps { + chemical?: Chemical | null; + onSave: (chemical: Omit) => void; + onCancel: () => void; +} + +const ChemicalForm: React.FC = ({ chemical, onSave, onCancel }) => { + const [formData, setFormData] = useState({ + name: '', + brand: '', + type: 'pesticide' as Chemical['type'], + activeIngredient: '', + concentration: '', + targetPests: '', + applicationMethod: '', + safetyPeriod: '', + notes: '' + }); + + useEffect(() => { + if (chemical) { + setFormData({ + name: chemical.name, + brand: chemical.brand || '', + type: chemical.type, + activeIngredient: chemical.activeIngredient || '', + concentration: chemical.concentration || '', + targetPests: chemical.targetPests || '', + applicationMethod: chemical.applicationMethod || '', + safetyPeriod: chemical.safetyPeriod?.toString() || '', + notes: chemical.notes || '' + }); + } + }, [chemical]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSave({ + ...formData, + brand: formData.brand || undefined, + activeIngredient: formData.activeIngredient || undefined, + concentration: formData.concentration || undefined, + targetPests: formData.targetPests || undefined, + applicationMethod: formData.applicationMethod || undefined, + safetyPeriod: formData.safetyPeriod ? parseInt(formData.safetyPeriod) : undefined, + notes: formData.notes || undefined + }); + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + + return ( +

+
+
+

+ {chemical ? 'Сохранить' : 'Добавить'} +

+ +
+ +
+
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ +