From a61ffed9cdde1c0196c17f5745c456f0f1b03577 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 5 May 2025 13:01:42 +0700 Subject: [PATCH] penambahan fungsi implementasi data --- backend/controller/diagnosaController.js | 102 ++++++ backend/controller/gejalaController.js | 2 +- backend/controller/hamaController.js | 13 +- backend/controller/penyakitController.js | 9 +- backend/image_hama/hama-1746382661292.jpg | Bin 0 -> 6187 bytes .../image_penyakit/penyakit-1746385635429.jpg | Bin 0 -> 7806 bytes .../migrations/20250318213037-create-hama.js | 7 + ...50504181953-add-nilai_pakar-to-penyakit.js | 14 + backend/models/hama.js | 4 + backend/models/penyakit.js | 4 + backend/routes/diagnosaRoutes.js | 42 +++ backend/routes/hamaRoutes.js | 6 + backend/routes/penyakitRoutes.js | 6 + frontend/lib/admin/admin_page.dart | 157 ++++++++-- frontend/lib/admin/edit_hama_page.dart | 129 ++++++-- frontend/lib/admin/edit_penyakit_page.dart | 46 +++ frontend/lib/admin/edit_rule_page.dart | 123 ++++---- frontend/lib/admin/hama_page.dart | 155 ++-------- frontend/lib/admin/penyakit_page.dart | 33 ++ frontend/lib/admin/rule_page.dart | 279 +++++++++-------- frontend/lib/admin/tambah_hama_page.dart | 16 +- frontend/lib/admin/tambah_penyakit_page.dart | 14 +- frontend/lib/admin/tambah_rule_page.dart | 205 ++++++------ frontend/lib/api_services/api_services.dart | 65 +++- frontend/lib/user/detail_hama_page.dart | 29 +- frontend/lib/user/detail_penyakit_page.dart | 292 +++++++++++++----- frontend/lib/user/penyakit_page.dart | 2 +- 27 files changed, 1184 insertions(+), 570 deletions(-) create mode 100644 backend/controller/diagnosaController.js create mode 100644 backend/image_hama/hama-1746382661292.jpg create mode 100644 backend/image_penyakit/penyakit-1746385635429.jpg create mode 100644 backend/migrations/20250504181953-add-nilai_pakar-to-penyakit.js create mode 100644 backend/routes/diagnosaRoutes.js diff --git a/backend/controller/diagnosaController.js b/backend/controller/diagnosaController.js new file mode 100644 index 0000000..e190b2b --- /dev/null +++ b/backend/controller/diagnosaController.js @@ -0,0 +1,102 @@ +const { Rule_penyakit, Rule_hama, Gejala, Penyakit, Hama } = require('../models'); + +exports.diagnosa = async (req, res) => { + const { gejala } = req.body; // array of id_gejala + + if (!gejala || !Array.isArray(gejala)) { + return res.status(400).json({ message: 'Gejala harus berupa array' }); + } + + try { + // ===================== Penyakit ===================== + const allPenyakitRules = await Rule_penyakit.findAll({ + where: { + id_gejala: gejala, + }, + include: [ + { + model: Penyakit, + as: 'penyakit', + }, + ], + }); + + const penyakitScores = {}; + + allPenyakitRules.forEach(rule => { + const idPenyakit = rule.id_penyakit; + const nilaiPakarGejala = rule.nilai_pakar; // P(E|H) + const nilaiPakarPenyakit = rule.penyakit.nilai_pakar; // P(H) + + if (!penyakitScores[idPenyakit]) { + // === Menginisialisasi: P(E|H) * P(H) === + penyakitScores[idPenyakit] = { + penyakit: rule.penyakit.nama, + total: nilaiPakarGejala * nilaiPakarPenyakit, // ← Rumus Bayes awal + }; + } else { + // === Mengalikan P(E|H) berikutnya (jika diasumsikan independen) === + penyakitScores[idPenyakit].total *= nilaiPakarGejala; + } + }); + + // ===================== Hama ===================== + const allHamaRules = await Rule_hama.findAll({ + where: { + id_gejala: gejala, + }, + include: [ + { + model: Hama, + as: 'hama', + }, + ], + }); + + const hamaScores = {}; + + allHamaRules.forEach(rule => { + const idHama = rule.id_hama; + const nilaiPakarGejala = rule.nilai_pakar; // P(E|H) + const nilaiPakarHama = rule.hama.nilai_pakar; // P(H) + + if (!hamaScores[idHama]) { + // === Menginisialisasi: P(E|H) * P(H) === + hamaScores[idHama] = { + hama: rule.hama.nama, + total: nilaiPakarGejala * nilaiPakarHama, // ← Rumus Bayes awal + }; + } else { + // === Mengalikan P(E|H) berikutnya === + hamaScores[idHama].total *= nilaiPakarGejala; + } + }); + + // ===================== Normalisasi (opsional) ===================== + const totalPenyakit = Object.values(penyakitScores).reduce((acc, cur) => acc + cur.total, 0); + const totalHama = Object.values(hamaScores).reduce((acc, cur) => acc + cur.total, 0); + + const normalizedPenyakit = Object.values(penyakitScores).map(p => ({ + ...p, + probabilitas: (p.total / totalPenyakit) || 0, // Probabilitas akhir + })); + + const normalizedHama = Object.values(hamaScores).map(h => ({ + ...h, + probabilitas: (h.total / totalHama) || 0, + })); + + // Sorting + const sortedPenyakit = normalizedPenyakit.sort((a, b) => b.probabilitas - a.probabilitas); + const sortedHama = normalizedHama.sort((a, b) => b.probabilitas - a.probabilitas); + + res.json({ + penyakit: sortedPenyakit, + hama: sortedHama, + }); + + } catch (error) { + console.error('Error dalam perhitungan Bayes:', error); + res.status(500).json({ message: 'Terjadi kesalahan dalam proses diagnosa' }); + } +}; diff --git a/backend/controller/gejalaController.js b/backend/controller/gejalaController.js index 5f18990..1a39be0 100644 --- a/backend/controller/gejalaController.js +++ b/backend/controller/gejalaController.js @@ -4,7 +4,7 @@ const {Gejala} = require('../models'); exports.getAllGejala = async (req, res) => { try { const gejala = await Gejala.findAll({ - attributes: ['id', 'nama'] + attributes: ['id', 'nama', 'kode'] }); res.status(200).json(gejala); } catch (error) { diff --git a/backend/controller/hamaController.js b/backend/controller/hamaController.js index 14e3308..28f04fc 100644 --- a/backend/controller/hamaController.js +++ b/backend/controller/hamaController.js @@ -6,7 +6,7 @@ const fs = require('fs'); exports.getAllHama = async (req, res) => { try { const dataHama = await Hama.findAll({ - attributes: ['id', 'nama' , 'deskripsi' , 'penanganan', 'foto'] + attributes: ['id', 'nama' , 'deskripsi' , 'penanganan', 'foto', 'kode', 'nilai_pakar'] }); res.status(200).json({ message: 'Data hama berhasil diambil', data: dataHama }); } catch (error) { @@ -51,7 +51,7 @@ exports.getHamaById = async (req, res) => { // Pastikan sudah import 'Hama' model dan multer middleware sebelumnya exports.createHama = async (req, res) => { try { - const { nama, deskripsi, penanganan } = req.body; + const { nama, deskripsi, penanganan, nilai_pakar } = req.body; const file = req.file; // Cek kode terakhir @@ -71,10 +71,11 @@ exports.createHama = async (req, res) => { const newHama = await Hama.create({ kode: newKode, nama, - kategori: 'hama', // Default kategori + kategori: 'hama', deskripsi, penanganan, - foto: fotoPath, // ⬅️ Masukkan nama file ke database + foto: fotoPath, + nilai_pakar }); res.status(201).json({ message: 'Hama berhasil ditambahkan', data: newHama }); @@ -88,7 +89,7 @@ exports.createHama = async (req, res) => { exports.updateHama = async (req, res) => { try { const { id } = req.params; - const { nama, kategori, deskripsi, penanganan } = req.body; + const { nama, kategori, deskripsi, penanganan, nilai_pakar, } = req.body; const hama = await Hama.findByPk(id); if (!hama) { @@ -101,7 +102,7 @@ exports.updateHama = async (req, res) => { foto = req.file.filename; } - await hama.update({ nama, kategori, deskripsi, penanganan, foto }); + await hama.update({ nama, kategori, deskripsi, penanganan, foto, nilai_pakar }); res.status(200).json({ message: 'Hama berhasil diperbarui', data: hama }); } catch (error) { diff --git a/backend/controller/penyakitController.js b/backend/controller/penyakitController.js index 390ecec..1ebd6a3 100644 --- a/backend/controller/penyakitController.js +++ b/backend/controller/penyakitController.js @@ -6,7 +6,7 @@ const fs = require('fs'); exports.getAllPenyakit = async (req, res) => { try { const dataPenyakit = await Penyakit.findAll({ - attributes: ['id', 'nama' , 'deskripsi' , 'penanganan' , 'foto'] + attributes: ['id', 'nama' , 'deskripsi' , 'penanganan' , 'foto', 'kode', 'nilai_pakar'] }); res.status(200).json({ message: 'Data penyakit berhasil diambil', data: dataPenyakit }); } catch (error) { @@ -50,7 +50,7 @@ exports.getPenyakitById = async (req, res) => { // 🔹 Fungsi untuk menambahkan penyakit baru (kode otomatis & kategori default) exports.createPenyakit = async (req, res) => { try { - const { nama, deskripsi, penanganan } = req.body; + const { nama, deskripsi, penanganan, nilai_pakar } = req.body; const file = req.file; // Cek kode terakhir @@ -74,6 +74,7 @@ exports.createPenyakit = async (req, res) => { deskripsi, penanganan, foto: fotoPath, + nilai_pakar }); res.status(201).json({ message: 'Penyakit berhasil ditambahkan', data: newPenyakit }); @@ -86,7 +87,7 @@ exports.createPenyakit = async (req, res) => { exports.updatePenyakit = async (req, res) => { try { const { id } = req.params; - const { nama, kategori, deskripsi, penanganan } = req.body; + const { nama, kategori, deskripsi, penanganan, nilai_pakar } = req.body; const penyakit = await Penyakit.findByPk(id); if (!penyakit) { @@ -99,7 +100,7 @@ exports.updatePenyakit = async (req, res) => { foto = req.file.filename; } - await penyakit.update({ nama, kategori, deskripsi, penanganan, foto }); + await penyakit.update({ nama, kategori, deskripsi, penanganan, foto, nilai_pakar }); res.status(200).json({ message: 'Penyakit berhasil diperbarui', data: penyakit }); } catch (error) { diff --git a/backend/image_hama/hama-1746382661292.jpg b/backend/image_hama/hama-1746382661292.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fa920a58f6ad2fd51ae53f849ed817071f6d5752 GIT binary patch literal 6187 zcmZvfWl-Erkca<^yR*1u(Gc7rSa1vOp2giIc!JB~yg`EoSS)COV8L13-8E!`hX6q? zZ{1zpUDf@jrn-B&`~AP@k6{x0BY6^I0|F)&F;Nyz`*|0AOR zbiv?%EhQa0BPBHp4LLaz9}^1)CpR}Y1%rU70G9|m7dIye2M31$pMZjhh?)3yb4pRFsC(>n!8@rkvA;yku5Hk+>4U>Wnc~Kt( ziM}<&BBLC80%U{U$M)`dl~7i32SdIgV??8<#nPyH zYFSP=`wkrp=OWY3ES~zB4YMyc(Rg;p&z%w(;0vwQ`W5+Z+w?h13DrMi22FkU4#bNN z|EkuD>W`!Mb_l;Wo4!JnQ`b@3fS)q_C{M$d?(>_UC@w4`_~dU(+~juL3d!QAY-M&8 z-_5%#P6ZW9P)#(MaVi4M1zV>L;68QU#&OmbZnr+5Qw5|5HI&T8+kfq>duv!J!F*_@ zkgaF3bXTKE7NzB6)cHb3s2|Y@=@`}RGi%5da|~eV2nV;_G#XbamP9*>t?^j5FDfxQ z2bxCd7A0iD)k=>j(s^e2nu#WJ8(=>;>N43&zbn8ss@Ebw%LLi zQ;1gaZ6y=C;+0soiR;57@S%>z9{#g)m>ocN`8?5_Fg7rp3X4;7XK8d7 zX5Fl9oG*|`t>Q+a{NBjbpn){NK)35G0Z*BAxUn7y6jU1XdOK9!TS!Fu<0L}YaU+%* z^ewgzY)0q=Efw7oBy5O=v?7NT1aP zRTlXW;+|R4$mMt2NP&9VI4m3(dgZ(xfuTBVu*QU z#8w%(Y7B?d+9h;63Q9b{UX7KMfj_LL%6RwzHX_iO;i^^sCtNo{fsqo7%buxXG*@da zlgwM@Dc3~B{7V5aY-JT!j3P1D!of@C7S=7&{ycSzQKIpJPNwAv2z>(Jhyf}}11W~N zU@_2ix29A|2hc(>Lbd z`J;@(gbUX+J*`}H<3|}{nq!I;IH@Qqh^vWp%c4?wS-O+uM_Uk3%Gx|Rr0`6+XG8?q zF%pC>`%2(72B|vAMVcc_Mlgm((IDYqnX$Xu9h)!p7a$Lxo|6&lpgnX^n} zd_HwfnX_ss|IXF=_=V z;jU9-~>VU6~CC&&OBL*l~Ad|s3w83WP?&%9- zTX$CUmBkCUvbl5pt+gA-!|aj|PvMJc>yev5q2xJ^$HnDDSjL6tJk;Q&IBNC2+$SO_ zrs5)RApNY-Y&$=05+aHS#c+>2ol7dtZHXA!*Eh0wdRBBH|-?zvV-m$w_S zd)F{4^LDRDu5@3*Rud-j_Sum9fyA&foX(ZM8}*8&VhV` zRZOei^bz$rG+Wt?{WGQnz@!v(2g&+}<_`<6VLPTg!7G1x>ypSuC#Yk&DC)5u>Y&g4 zeYE0ct3mP7j2$A>`09S%J^9Kwjla6pp3Fv`-b(fxn4h$HO=5gvGG#ABN6={+i;q09 zW}1f3r`EQTQRY}KjT7PNr<;&!r5K*|Qq;#_&0dvuVVK;zpt^}VZ$F~IY!AQxbFp6d`K^+A)V*v#7GbFqh7%wo(?7{OASDz z7BwZT)cvM1Ti1q4CG{~v-4BHzyLqNA%*U8k_&6dl<>xP12R2k2RW?klvjJI7fOx>P z5|k~EFTYom7gT5^$4|7Mg4}F}?6cwjc>)@Iv}WWAwZ0D?vM5J$f3!Jhyr^q3ao9mE zNQ~(kzoPG|tT?7l%Giw=emN)-hA?b%bjsxzs$JF=O2;j4E?*1NFG|#Hk|S}qUZXy@of&p$ATeg@1nH{|Jw78<=8!fT|nvrhcpjd;Xxvop z#~7PR2^BXUot)d3Huvg3Zxmh^jD~aI|J3lx{dBf=D!jEcbeG42Tr~kZ!$l;RciqB& zvso3=v#~nhxgVf&_P5zqb8y-y-Pl=w;e4P*J^?3XAI=LJG-z3sp6x8f!TA6VRWi74U~a|C{I&fccM#Kw)-Ao!iij#q33AOqE7xiAN{9iWI?z zCd4W1J}^J2!4uh@j65BX2180S*532|?IJqf^k{p-nyz!Sw)lBA$FJtEA-!{)Tj10)(h`uj% z!}(%YApv!c6WX((Hq68ACyd+uu=<*o@VJ~O%CtY*>c*E%k|9zV?foK7ml$d|1ma7r zVGoHw8sgKQFK6XESdp}KQe;>3zWW_VD2|urRU99jo?kVgx@}rqZi^6rp5mQ5O?ohB;E3@=CbP!okz*UNQ34g z_LD`{yF=zIFYa%7pd;09F0Ty34jj4LFoH~iL)kpOCNe-RrzHk{CClU8Y^Mp=iohNX zS%T3&#hOIlz$No~vA@6Tc$7xXLMk5ykByxps=cU_>f!7ymj-b58B`^;^=!>n-DCfLc1w1q}Dl{8Juc$ zE!v!+cUSM^ToNq$9yQ`7StP_^!_!*3J5eYG-(l0!?$4%9L(YbvsardpB)L?1(q%62 zm|(P(cgr%;=qnPNC+p}(sA;QKu`3jKSDowI@+08D;gP^zkim<;T8S#T7RI3Q1XP&T zkr&&HZqN+j#$A#S@CZR4%*i)$6ETVCow{QUdFeX!S(E$Ez~Ls)YOiqI(9FkC!n$+W zn@K)F3~zev-2Lu>CqRz}?B&)%rklDF;7`f4_Qmy)^zpGGROsx=Oif(|pOlxB^sadm zp?M)*kqn)BQ66*i-jaqPcs$?^wsp-|LxI1Om(0cb5Y&VWE4t>`9${Dg;NPxlUUYJfX!W%IRrgXj&(t-i)1o0$c`L7cY}JyFuN)`sG_E`)EINu6;S4 zH)MXps(KA6^hRGGH^x;~Q89VWJ(y96W6*L&p5dd3e$IO&hJ7i3@w+zOuhrZ7jVO1H z(FVf>tgB?%QRt;c;nvEfw=72|8(2N8$~d73o^l!ugw3nb|npNx%BT zi&uM`3+<3?r52~p%>Hyd)bR!}Lu|Z~?er5g?~n9S4v9p>ng?zO>Zd2EoZ%aT?blr1 z;V(DCihrJkQ?Lfpw?NaVjgCG9%?M3Nn|+EL+1%&M9w2_x2nqAqS(S?|Ou482!*9uC-{G6uQ20cyee&W$)2YVIy~V-~@uvLpyQ9!@soIzBU)+@R(v& z#a*b4oeed$N-)2-(kIq?l{+xz?KtM<4CNZdawsx?;X(%PSn(e?i7&kF8rvj1oAq_) zYqyM^!ptrdU-P?TUz58Ilps7QgUR6AXUE(nBjz1*Y4GcdI)hsgjj*?1x{%ZM6|qBl zj54rSh<}WbR!*?edeim8>X&&r-d0@3#d`BJF1wosmpdkvGAImKEks!u9StcG3Z`}5 zCDduO^{40CrYvosMy;3)hYP`+HaXMJHSv#&8kt$!+q(Q7lvjVKeZdG&UIZ_g1&Emc zDOU)!6Ky5DYVW?aTzBr`+jl8Uz)bC%UbS5OW!5MwP#h5SHn{KW>)k)db}1O6th>k< zjd>DsD~vODG&&70o7jm)+h8P!G=MuX-C1!dUIPzQh%S`W(8{l=%kQ?_KhhKt7#Ko)M9(=o zLPGV!ob%dxK&>SG^^S@)q3yv3Bsf_{;R!f4TgmK>$%os#(~z*x2zSQw_^d|zntf*U zC0XHgmcFNiKfmJy$(K(e6v&S}5K5agW8WK zsl&3@o&dRN%|o$cjHzm@2RsZUc^FpgA#6F>F|IH>@a`E8y+?S&lZN) z?%PsV&7W12S~)+mawt5e5^%pvH3{;ek_}2KIcLA63#3@RtKfIFsXqE=*i_SHh>`tu z#viut7@-wKU=zoR5Mug!&>S9v0@V`mKrF?$wA)-iITy8Scd4gnzMFNvd|AoklX}#VFGbe-SP3@N9uK+2w_qa zs+V}dM~!#2v&y(R3pf)5dj@+Cj_NaT!0dT|*m}_3!Rt8Smv+QcrHtx7c; zm*oY;6&oZ(9|zJC_#>*$F_O2{4RYsk6y{x%>|LQ75m7%L2~gIA_@5?{gg z8IkeN17gW)w%$~PRn&26rLg3C96po)%f|f`c)+>=|@?clg1*LU&H~8H8 zGRT7N=2e2-I43I|uf-m=Po8mu*U}IVBCPVHleL)XQX(TLc^Yl{Sap%b*<^dH_xK4g zRa5yz(W+jm&0{UgUc)Xc_;p_LnV5ZiD_V}RGGrJz_y}V-AwP_0(h8Q2H6CXV1S?#` zlOx1~PFmK~UXANL6Qg9lfo7MC(W2*0Jm~XaZ)x|98AGbjd$M-vN83Z?3M3_SM_C5r zbe+ei$WIE4ZUG0h^t7O20 zCS$j5720ePc8`^3`{Ig`AGF6hLV9fvZ+a+{Mqe`qXKv0)R9ikjY3H3jUHy35pc82fIVjmd<2D+}rKwazerFg~AI*u#K z+9LCj?Gc*B6%Cgc&%TQi!&B#5*1wtzvD$L2GL1J%46m-D{z3c$-wy7_>a%bN&UW+e z)`ZUsymK53r4-tj+}c);D8vCkO91Aw?av!7c`4l9M^d@c61GorA)O)mC1pp^IOOY< zd(k6&U2`YWSZP?FjwjcwS9;(z3%#MPw1(k{(S2Cxy{2eM|fvn$JV<(+M zo-#}-eC-{DsM0nVBQOMRc{To|-YhSGR)NbT?=QVU(%0~w>~}KU?Mpwk1G<## z_?fVCHktQ`ZHK%_c4ZFGyy;>xE#*DTA%rK)1uJ^2~4vk;N$? zPlz8zxO@8sJ-5^^|xT>fZ9Iow?+M(mcGhC%V#DPyXEmI8*=1HC>9;$qE>S9}68x9(gFxz)WAo~fjD z_bR22wl${9|8eQ0<#IvWQAQJ$M1PCGlK8*lq001-)@E?Hx zfsO%00|79xu>VCRi2*8}%qS(d+5J~b1qA#P{-5f9{C{-O|3z3y0BHXO1o)5Se*mJ9kTQYzd*zYuwDe1MC}l6QZM zh5bHavm&e|iu-j-Zi&`*M_19jlz1?xTdog-+nNfR3H6v# z$Yoy$jNXoWi!%GUIete}RAY%xP^T{79+YqmojW5tOexdn2!e#WG_dTo&Oi(+T!sRg zBt{gTt*#HtW;P|_P-q>LWps>~DIwiYGhROz+jM>>xx@W&v9~4$cS*vcIvMhcH2xD> zg+X0L8$GNdR>YEk3^c}$G$Z4DFW|Bi&;Lb2EbVvQ;*fsvRH=D@01IMU<+!>x?19{DyR3rCS>JY!jC=AJFOBqQCagd zCCD??Cw#4JHnc`F*jT{CdmsDuom5!)5#tH+1iYDT+)ac(rIwOr=bvK>dYIK z^)XL_WPVH-_~aS_A#|neesGNc=&t-)#ikb5H6a|x5*SW;Zb?N75TdT~Z+T!6rxUpd z6AH)(V%znYEIxP^+bbNsFx)=v#MjM|LGTsN3GOK`2{w~R!uvHZDXvUCpxzY)K{waW zPhOd;%TzbtVC0sR>u0<&B%dN@8^@`8&s~;B(WT_U=9U8sv={}|PY2LP$R033m0zrv zoDs#1Frh#kW?dW#E1^kEaSp!#QYDsARey$;g~N&4Inc6o)d!WTG48nBh*vwG0E{>C z?}3@5m)_2r#eVxKO)-^&d1ZMkmw9McAFA&LYvrBryL`I<4%^-l$kcW8je=PgP7Jx> z>qk?34KPH(0cqIqvY_y!P@JIB?sTnAy=*|{A zF6dQJf66drF9TVk#?pTn?e2~n&w?bgPTpa|3nxdgsvtRUJ#Zld5t>=x*Ldm;eJB~{ z`eFOX^wfz%F) z&l_x<-QAd~ke_UjqjAPP@Hg%26JTj&>L)@qno{{fc*p9+Yi(Jh+THd?2k6GhyhU(| zu2uwBF@BKWVbm<+^Mo$-3NIC7<`r<7p2rfHzgvY;ORlQc;is6{s2-QDx1qMLJ9+FeRHJPb8EaB|wa<>IQG z!Ge18z2nsBJxXl+*d}FoUxpal>*EMutBLxjNgKtlAkn4zn@^H>iZKxk#F7!-x$Sf; zR1CKZ+sw;wjcrk<$>Mk3X;A4+Y_17ShN?P;a$k1B#hf+NH=4(p7`v2RjmkARRQUkUhwl{X}#=@{1=?qoGS=FF;{ivSflnp}c zg|>o-^KNE)r=MmFKJjNI+LC%_p%r9CPB zIG}u|(LId;hz4YzIvgdG`V=yDnGtHj4=|AEQ1uC zA=Nlxuee7S(n9o&it8dIijVrdE=f*CE%pT^IT)ktF>c|$Bf&8Wg0Bw z(d!aDtg}b&sigjps@+vRCskl{(qU5-*cyNAQ6$!NSmpa`cqb-qtU@y$O}sk9JN5UK zdbPt`n(~Xq3R!RvL&#Cb6X4*a^2{s7igjli;2E4Ip;R@Sr>&0r*sYcSOo=aPr(x;i z>=?V}+=O-s!gy2mnrgD#g#z`Wkc>x2sXUp8ahWJ~_<`c?Em_A043n%i_M=CJzTcv( z^tvMt6Hd))t3($0W}qpr!o&H+Y3ih5kz_tFL_Ohg)$!Z3C=Vh2ELq}DNYdEVH>ar! z6V-2=7BXE8nc=U|waU}*5~wYPi*dWcPg8Q@MDs&dx}3EdQyFR>ttf1?y}sjK8`siT z$ZG*(@v96z`P0dghB`ZARDkDI!{hm$zx9+1+?Xdk;?yo-Kc*;w!5hh22qUBuC-~>p z2PVrLid&#mw6-tQtLo$uJ?c@(8NbADIr~fkqfv{8X$*}Xg)rV)KsOm(Z3$d|$nLwToCC17^Hj+@cV|&O!Hq$8b`_=h0sp@a$v)$*K%?wCXvGEmk!AOy6DDMRu zY)!PflKa`wbAdAlSkO<51} zS3eO|u*t~x-gMVdbT*S#n_GijUo)cK17A>VDfR-mnfr)CPrj>E7G*5WyI95ih4-*W zzgDBvA+zLDo#^bOjwkcJ+)JVnfn^mq(s<9h3$fJE@$L9MK?EEw z3b@M6KSfUttQ>zpOKL8Vk3;#BfjDTx*Q_e*_7PL>-mUo>aFS)O<&TUVM-DEf1u|QZ zj+&Ik9SM|Kn`0d5?6w3wYMv4`WVaRx%~y}s=}j3(O*q7pxrf(;gmSDC;5oObolY~V z8GdwV;&(w{nTNbUTWJv+Q@D+3mzHtOBigt0sgQim0&_`zn~v47qo;xTO$LjJnISa; z(#7rIW(x587)dts;D@gi9yGYWu$2hpdBJvoB&jcoCtC6e<_R7E+=X?m<9l}hc;)@s z|K0_+E1w{%#wwJ9hn)-_7t=2|W94(U1{q#PxQoUfoUaJl6O--2OMr#6{cJNEz4_>%j>=msW z_a~m_e@JCZemG98h~f3{_>80{kb71;f>;@HCiRwk%^CUP&UfC&IhEZYOC5tRz6Jty zP!y`D(5vR?pUTDH8d!z2o@n9oAw(Khx!Rn*8&OzQ*7@e{9S8TE`wbm7h{H)}ulNA#)uKXOhIM`CIAjd9$cl5K76#f*N+e_lRvdj zR+*z1h>(o>F2zQE)xcvNO#CJXi4zMV7LrXzpOw>iOyrXqQ$EyS-G|s(O?(cQ49QqO z%3yo{jngYZvclyM(N=_PpY+Gk!2mping&Kv*N~b4>QjoUHeul-P#;Dw@p~ z=p<8?x6|a*oK*1zCnT#1?9a;OmG;wn7O7=h=3)a{*nYnW`s*BHh-CUZs#z@?)l3{} zt;ltM*fX~T&(EzAV|dBa0{*2gqBVD`jxz46+TxbXNfR+sna&CL3xicQV4g;o(DC%x zQ?ilgb?fiRoACV%5){z5q&Lsr6vnp*Df{s4#FHg8<30C&Y=?RKGCVPLRZCu5acKXu zoygah8zRw9fMwe7cRLnYO8vhRj%9zVnW__^*f*Im=lv8rHlyd~CG9?x1m%YTaxtyD z15?6s6s^9@jr;0RZ;$vSLrj-;F}*Z^*7QTKQg%PC5Zeza%x0fX_}4kMzH4it>b=Am z!9FSOE1MSHIsf%`fCcU(^)9DWJ8D1@$~-o3M_RbdBDzC$J8FV{1XuE}eY+@H5b^RE z9pf~0{tfO-rE0VcTT;TLD2d?M%>5Ify^c*y&^3dr8+99t&7$NG=qOXIrf`_sE%rls z4_ksW!aA(^?(vzxBFP<*PGJRQ=~9kja-6pXCc->;6;%6tpY}U84L=nD`pKA=O1{=0 z;b^aJBs4;>@u7TE3&iX6a&}%zIHBu6upy@3QNvYFo=4;*{{eR=Z%k7&(4o!4OLfZk zG`?~1b3Hfg8sVz8c>i`+HZqP0hT}FNMAXYij-iK7AmMY|$mQlM4;x9y=aRG3u`Ast zHDWk@T^v<$(XA)ctmm+hdk;;Pkqq}nv)N57xerAO{d(IQWf3|#H&vvRIFCHvtoh}3+Taw4Tq9lEB4{i zQ2ZX~>gB(@lTSZQ%Z=jy348)z=3X)&4t3r9 z;jP3h;j$Ej`4KrbsN|tj9w!ezM=ujAC;!bgqS2Y)CU6wVhuad}H>PV-rF%4~-1n&o zOCr*QUQX#dfWdue(wJzp7--igq}kq2%Acht5QAuQg+b>lD5BNpJht?eq&2 zjhu-zILaDDEJ1W(at$t8s5>Rav?)JnrrqJ6QHZjcd+f)nQ9=nn0hoAZD#eD&k*WSC z*>B)seZ+h<>>MuJMLGTJ-!(t)o{=5R9EPeGN|ek1HmSgtr3EFZzFg1B{O@>1@o2Ah znwdjAoKpHrNxpxf(@6D3{$L6+J&b!c>Wi3tr`=ly2hvXz+!Y{OkIc_aQ-_-)50NK7 z57zLaw=Jj1J=#Ukb|_<_+SQzz#10&RV%FW@(Uk*)-Si6YabBA;X{Ue0$M*)6FqcS&Pn$2 z>4ZjZyNbZ$tRp$Jr?}?JZcS$}yDPJ>asG^Acf1f$b@$JaI6yoBG^XNk072S6Z6bfQUacV$J`aSUbZq=Ho!3-QHsxWY%Q}w$y;$ z^+03RuG;q2W7V4{0RIy}dJ$iS z`j-hgSAN52Uu>P6C^!R3TMxNb_|g2sB-M%Td?QU^nYc1b6Y+freWKCH*KoAu&2Qy- ztmM+Kc)M(_uH&HN)DRsSs}ZlwFFKE^3-ee3m$r2+i}02Z$y658o&K?NMTZOC{vtRH z_O|}A%c(CjlC$wA4-%8DU^9@n%hkL$E%Ov=+R_RI-yEjN+YD@*;xO4espiO6>J)}OttGj;Es=9`?-kcW;@ zz~@EwXuRM1$}#I>2ijMKYQ>^z#f`F2r4@lJU7)IEXJ~W8_IVLu8kq>@&}4QbC}3zm zKZKh4xas6xZzt$9V8eShp&1LCNka1pAfj?*Tue`Ql0;_{`l&>VtE;CB{EZpnGv~M2 z_}LI*+JQM`pzVg8uJx|Ze@aCHdQ>ggKEd0rp0v4#ez|RYLZK~gC+QO-E6Z5PoH@uo z<8$2m>y*Ma4+ibNeqT9_f!~6+lBoYlryQHFTj=%QVnVYCYLfem6!))YLYuPP$53K9 z*g5s=H{r=J&AIJI{Vf*AUoZXN2L~h*7FgfIjEwEQ5#^}aza5(H{zH$O(Uu?NfBPJY zJAmAIgT6e^L?#uP6+8DaRFCSjR{DMUb7a5?nQvMn7bLw=TDdi{DQCQMyvkiQsCdpo zA{aXQ;B`4yg1z3n=R9eEjRiv=Nf|cSv_}*Z2Y3H~rh$!_zFKyZ-9%4YOjG?9snKj> zHa)3A>O(7S*JZecb;cBTzQ#)y{#B`A6Ao}`fLpq*c-O*j6o5$Rvh^QnYGhWOZ~US? zdC1$nNL00=dOMDm~`I4Ju?W+qj4>gZT`HuMMgaG(wH_z&oB8n zOHMsm4aGGOywu_W>P%wwCa)l}@5X``{9;ko((M`2ImJ!GkLc+P7AI_2|9)7L`z{qw z#pR=0!|3{XzhS*sWUau3k5N#gW7KNStIks8#2O>w-NVQgFHywO1u{=I_K2;-ZG`}< znetZ8`TLz?C(pNIRc)A2ob@z=-Ga(@6BwcH6DxAwP-t+>wRawNO3WY&vWhFU;-;)J2XD;>j=cP3`HrQd0l&P*7UA{X*l_ef zFx$pG1eW+-BRSQgro6pmedTQnQBND(|Fz#8M-#*ESJpc1^GJnImnzC6bXM~i-C9|( zX2peSGYA5e@B~l|b`jIKGi2z%9QI#^IQ#@GvD7#BpXVY%qguI>AUSkTfD=2z zB13QB3-0Du!4mXjZm;!~^qidh39n88MJw{wU>7<^o(O*5i2N&R`v`PD^>}fPYKXhWbM^&05>;o~2&{o*H?>3h;}czU zCyEj|^+ktu;OG#?ZgsT%_S z=Y8~e1hq=hA}LH^Hr)tHveFxG$242|xMiuGyDEuTWSVMMgcPxBb4GBE^0U%(Ggx*c z7Y-oo&Dao%O3|Q{)u6#_dX1T~2PhqPf7d8?oi`^$a1rDp)1OFb^vqdGBt3OFozMjy zbhaP<&0R_aBn|NN~H|^v5N^ zD7$XE?%D?tF4A#0OR1@*tp+5$wX=EIiiBhmR+=n#ge{t0(avn)cz>6!8GR{$ zr5~ow>h3E%H$58>B3W_Q&$Y7F&mMMm_K?xzAElFaD}g%pjcA79M|lV|(Xkqii2OWQ zaJG0O)u^95Brku9_C;R%z_afqb9IyTk86qvNY_Kf=frQ;z)vunJdD|U3mZy(smVSm zmlATT#Qs@1npGkk9dDLTvIzGMN|F$f^P(q!zHZE5`2honI%0}zK{i7LjF31v%wBT% zBNte?lTGV$oE479Jpoe7Ic;RHvo1dYs+{5!#67ocj~$PNaiW<&64O@c z2BjpZn}6nP6BeQx&$_m+F|4l$grWh{+@_*IW- zZ}F}co?W$ry4)_&muW^ka4MT>N_~H~@{kN?vuBe~M4tUi7mNMB=7GIZKvq{BJ8NcB zqM2G9J7!hm$k=C#7{%-MlE-E1&@>)7-kB%?d|JY|o}+h;W=9GqRsfJXfPro66+5@p z;J?M%joQG5_e6!jTIytphszc_+dP6>f$1(fKBC?c(z$xkGQ1hB#Z|%*6%-nrsjL+a zEYyX-(-sDT`688d1#e}lJ>8Oqn~4i0tpxG=vQA>0d2=|iRgrC4rl}AEE`(1wrO`Bw z6tFEaHlnAQtKFMD1IM(C#B?hqjDNNiYDOV0?u_fDI%=;Yt=ekjH(G|F1eVyDhQ~%T r*6qkg$Akjx5fO9LyaPR*2gCwAFVNQVqgtcC { + await queryInterface.addColumn('penyakit', 'nilai_pakar', { + type: Sequelize.FLOAT, + allowNull: true, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('penyakit', 'nilai_pakar'); + } +}; diff --git a/backend/models/hama.js b/backend/models/hama.js index de4727c..b647d70 100644 --- a/backend/models/hama.js +++ b/backend/models/hama.js @@ -31,6 +31,10 @@ module.exports = (sequelize) => { foto: { type: DataTypes.STRING, allowNull: false, + }, + nilai_pakar: { + type: DataTypes.FLOAT, + allowNull: true } }, { diff --git a/backend/models/penyakit.js b/backend/models/penyakit.js index 92c9bbf..abc6e57 100644 --- a/backend/models/penyakit.js +++ b/backend/models/penyakit.js @@ -31,6 +31,10 @@ module.exports =(sequelize) => { foto: { type: DataTypes.STRING, allowNull: false, + }, + nilai_pakar: { + type: DataTypes.FLOAT, + allowNull: true } }, { diff --git a/backend/routes/diagnosaRoutes.js b/backend/routes/diagnosaRoutes.js new file mode 100644 index 0000000..d0cee5b --- /dev/null +++ b/backend/routes/diagnosaRoutes.js @@ -0,0 +1,42 @@ +const express = require('express'); +const router = express.Router(); +const diagnosaController = require('../controller/diagnosaController'); + +/** + * @swagger + * /api/diagnosa/bayes: + * post: + * summary: Melakukan diagnosa penyakit dan hama menggunakan Teorema Bayes + * tags: [Diagnosa] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * gejala: + * type: array + * items: + * type: integer + * example: [1, 2, 3] + * responses: + * 200: + * description: Hasil diagnosa berhasil dikembalikan + * content: + * application/json: + * schema: + * type: object + * properties: + * penyakit: + * type: object + * hama: + * type: object + * 400: + * description: Permintaan tidak valid + * 500: + * description: Terjadi kesalahan pada server + */ +router.post('/bayes', diagnosaController.diagnosaBayes); + +module.exports = router; diff --git a/backend/routes/hamaRoutes.js b/backend/routes/hamaRoutes.js index e09e941..bf834c6 100644 --- a/backend/routes/hamaRoutes.js +++ b/backend/routes/hamaRoutes.js @@ -68,6 +68,9 @@ router.get('/:id/image', hamaController.getHamaById); * type: string * format: binary * description: Foto hama (JPG, JPEG, PNG, GIF) + * nilai_pakar: + * type: number + * format: float * responses: * 201: * description: Hama berhasil ditambahkan @@ -105,6 +108,9 @@ router.post('/', uploadHamaGambar.single('foto'), hamaController.createHama); * foto: * type: string * format: binary + * nilai_pakar: + * type: number + * format: float * responses: * 200: * description: Hama berhasil diperbarui diff --git a/backend/routes/penyakitRoutes.js b/backend/routes/penyakitRoutes.js index 3859dd7..645cd3d 100644 --- a/backend/routes/penyakitRoutes.js +++ b/backend/routes/penyakitRoutes.js @@ -69,6 +69,9 @@ router.get('/:id/image', penyakitController.getPenyakitById); * type: string * format: binary * description: Foto penyakit (JPG, JPEG, PNG, GIF) + * nilai_pakar: + * type: number + * format: float * responses: * 201: * description: Hama berhasil ditambahkan @@ -106,6 +109,9 @@ router.post('/', uploadPenyakitGambar.single('foto'), penyakitController.createP * foto: * type: string * format: binary + * nilai_pakar: + * type: number + * format: float * responses: * 200: * description: penyakit berhasil diperbarui diff --git a/frontend/lib/admin/admin_page.dart b/frontend/lib/admin/admin_page.dart index 69206ea..e8b8028 100644 --- a/frontend/lib/admin/admin_page.dart +++ b/frontend/lib/admin/admin_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'dart:async'; import 'hama_page.dart'; import 'penyakit_page.dart'; import 'gejala_page.dart'; @@ -6,7 +7,65 @@ import 'rule_page.dart'; import 'package:frontend/api_services/api_services.dart'; import 'package:frontend/user/login_page.dart'; -class AdminPage extends StatelessWidget { +class AdminPage extends StatefulWidget { + @override + _AdminPageState createState() => _AdminPageState(); +} + +class _AdminPageState extends State { + // Data counters + int userCount = 0; + int diagnosisCount = 0; + int diseaseCount = 0; + int pestCount = 0; + bool isLoading = true; + + @override + void initState() { + super.initState(); + _loadDashboardData(); + } + + // Method untuk memuat data dashboard dari API + Future _loadDashboardData() async { + try { + setState(() { + isLoading = true; + }); + + print("Fetching users with role 'user'..."); + + // Mengambil jumlah user dengan role 'user' + final userList = await ApiService().getUsers(role: 'user'); + if (userList != null && userList.isNotEmpty) { + userCount = userList.length; + print("Jumlah user: $userCount"); + } else { + print("Tidak ada user dengan role 'user'."); + } + + print("Fetching data penyakit..."); + // Mengambil data penyakit menggunakan fungsi yang sudah ada + final penyakitList = await ApiService().getPenyakit(); + diseaseCount = penyakitList.length; + print("Jumlah penyakit: $diseaseCount"); + + print("Fetching data hama..."); + // Mengambil data hama menggunakan fungsi yang sudah ada + final hamaList = await ApiService().getHama(); + pestCount = hamaList.length; + print("Jumlah hama: $pestCount"); + + } catch (e) { + print("Error loading dashboard data: $e"); + } finally { + setState(() { + isLoading = false; + }); + } +} + + Future _logout(BuildContext context) async { await ApiService.logoutUser(); Navigator.pushReplacement( @@ -18,7 +77,10 @@ class AdminPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('Admin Dashboard')), + appBar: AppBar( + title: Text('Admin Dashboard'), + backgroundColor: Color(0xFF9DC08D), + ), drawer: Drawer( child: Container( color: Color(0xFFFFFFFF), @@ -80,41 +142,61 @@ class AdminPage extends StatelessWidget { body: Padding( padding: const EdgeInsets.all(16.0), child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Selamat datang Admin!', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - ), - SizedBox(height: 24), - Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildCard('Jumlah User', '10'), - _buildCard('Jumlah Diagnosa', '25'), - ], - ), - SizedBox(height: 16), // Spasi antar baris - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildCard('Penyakit', '15'), - _buildCard('Hama', '15'), - ], - ), - ], - ), - ], - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Selamat datang Admin!', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + SizedBox(height: 24), + isLoading + ? Center( + child: CircularProgressIndicator(color: Color(0xFF9DC08D)), + ) + : Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildCard( + 'Jumlah User', + userCount.toString(), + Icons.people, + ), + _buildCard( + 'Jumlah Diagnosa', + diagnosisCount.toString(), + Icons.assignment, + ), + ], + ), + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildCard( + 'Penyakit', + diseaseCount.toString(), + Icons.sick, + ), + _buildCard( + 'Hama', + pestCount.toString(), + Icons.bug_report, + ), + ], + ), + ], + ), + ], + ), ), ), ); } - Widget _buildCard(String title, String count) { + Widget _buildCard(String title, String count, IconData icon) { return Card( elevation: 4, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), @@ -125,13 +207,22 @@ class AdminPage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(icon, size: 40, color: Color(0xFF9DC08D)), + SizedBox(height: 10), Text( title, textAlign: TextAlign.center, style: TextStyle(fontWeight: FontWeight.bold), ), SizedBox(height: 10), - Text(count, style: TextStyle(fontSize: 20, color: Colors.green)), + Text( + count, + style: TextStyle( + fontSize: 24, + color: Color(0xFF9DC08D), + fontWeight: FontWeight.bold, + ), + ), ], ), ), diff --git a/frontend/lib/admin/edit_hama_page.dart b/frontend/lib/admin/edit_hama_page.dart index 9391e23..c9476bb 100644 --- a/frontend/lib/admin/edit_hama_page.dart +++ b/frontend/lib/admin/edit_hama_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:frontend/api_services/api_services.dart'; -import 'image_utilities.dart'; // Import file baru +import 'image_utilities.dart'; import 'dart:io'; import 'dart:typed_data'; import 'package:image_picker/image_picker.dart'; @@ -11,6 +11,7 @@ class EditHamaPage extends StatefulWidget { final String namaAwal; final String deskripsiAwal; final String penangananAwal; + final double nilai_pakar; final String gambarUrl; final VoidCallback onHamaUpdated; @@ -20,6 +21,7 @@ class EditHamaPage extends StatefulWidget { required this.namaAwal, required this.deskripsiAwal, required this.penangananAwal, + required this.nilai_pakar, required this.gambarUrl, required this.onHamaUpdated, }) : super(key: key); @@ -32,6 +34,7 @@ class _EditHamaPageState extends State { final TextEditingController _namaController = TextEditingController(); final TextEditingController _deskripsiController = TextEditingController(); final TextEditingController _penangananController = TextEditingController(); + final TextEditingController _nilaiPakarController = TextEditingController(); final ApiService apiService = ApiService(); final ImagePicker _picker = ImagePicker(); @@ -41,6 +44,8 @@ class _EditHamaPageState extends State { String? _errorMessage; bool _isImageLoading = false; Uint8List? _currentImageBytes; + // Default value for nilai_pakar to prevent empty string issues + double _currentNilaiPakar = 0.0; @override void initState() { @@ -49,6 +54,10 @@ class _EditHamaPageState extends State { _deskripsiController.text = widget.deskripsiAwal; _penangananController.text = widget.penangananAwal; + // Ensure nilai_pakar is properly initialized + _currentNilaiPakar = widget.nilai_pakar; + _nilaiPakarController.text = widget.nilai_pakar.toString(); + // Load existing image _loadExistingImage(); } @@ -89,9 +98,25 @@ class _EditHamaPageState extends State { _namaController.dispose(); _deskripsiController.dispose(); _penangananController.dispose(); + _nilaiPakarController.dispose(); super.dispose(); } + // Validate and parse nilai_pakar input + double _parseNilaiPakar() { + if (_nilaiPakarController.text.isEmpty) { + return _currentNilaiPakar; // Return current value if field is empty + } + + try { + String input = _nilaiPakarController.text.trim().replaceAll(',', '.'); + return double.parse(input); + } catch (e) { + print("Error parsing nilai_pakar: $e"); + return _currentNilaiPakar; // Return current value if parsing fails + } + } + Future _updateHama() async { try { setState(() { @@ -99,12 +124,18 @@ class _EditHamaPageState extends State { _errorMessage = null; }); + // Get nilai_pakar value with safety check + double nilaiPakar = _parseNilaiPakar(); + + print("Updating hama with nilai_pakar: $nilaiPakar"); + await apiService.updateHama( widget.idHama, _namaController.text, _deskripsiController.text, _penangananController.text, _pickedFile, + nilaiPakar, ); setState(() { @@ -257,41 +288,71 @@ class _EditHamaPageState extends State { maxLines: 3, ), SizedBox(height: 20), - Text( - 'Foto Hama', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - SizedBox(height: 8), - _buildImagePreview(), - SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton.icon( - onPressed: _pickImage, - icon: Icon(Icons.photo_library), - label: Text('Pilih Gambar'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - ), - ), - ], - ), - - ElevatedButton( - onPressed: _updateHama, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green[300], + TextField( + controller: _nilaiPakarController, + decoration: InputDecoration( + labelText: 'Nilai Pakar', + hintText: 'Contoh: 0.5', ), - child: Text( - 'Simpan Perubahan', - style: TextStyle(color: Colors.black), + keyboardType: TextInputType.numberWithOptions(decimal: true), + onChanged: (value) { + // Validate as user types (optional) + try { + if (value.isNotEmpty) { + double.parse(value.replaceAll(',', '.')); + } + } catch (e) { + // Could show validation error here + } + }, + ), + SizedBox(height: 20), + Text( + 'Foto Hama', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, ), ), + SizedBox(height: 8), + _buildImagePreview(), + SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton.icon( + onPressed: _pickImage, + icon: Icon(Icons.photo_library), + label: Text('Pilih Gambar'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + ), + ], + ), + SizedBox(height: 20), + if (_isLoading) + CircularProgressIndicator() + else + ElevatedButton( + onPressed: _updateHama, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[300], + ), + child: Text( + 'Simpan Perubahan', + style: TextStyle(color: Colors.black), + ), + ), + if (_errorMessage != null) + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: Text( + _errorMessage!, + style: TextStyle(color: Colors.red), + ), + ), ], ), ), @@ -301,4 +362,4 @@ class _EditHamaPageState extends State { ), ); } -} +} \ No newline at end of file diff --git a/frontend/lib/admin/edit_penyakit_page.dart b/frontend/lib/admin/edit_penyakit_page.dart index f605fdf..00647c5 100644 --- a/frontend/lib/admin/edit_penyakit_page.dart +++ b/frontend/lib/admin/edit_penyakit_page.dart @@ -12,6 +12,7 @@ class EditPenyakitPage extends StatefulWidget { final String namaAwal; final String deskripsiAwal; final String penangananAwal; + final double nilai_pakar; final String gambarUrl; final VoidCallback onPenyakitUpdated; @@ -21,6 +22,7 @@ class EditPenyakitPage extends StatefulWidget { required this.namaAwal, required this.deskripsiAwal, required this.penangananAwal, + required this.nilai_pakar, required this.gambarUrl, required this.onPenyakitUpdated, }) : super(key: key); @@ -33,6 +35,7 @@ class _EditPenyakitPageState extends State { final TextEditingController _namaController = TextEditingController(); final TextEditingController _deskripsiController = TextEditingController(); final TextEditingController _penangananController = TextEditingController(); + final TextEditingController _nilaiPakarController = TextEditingController(); final ApiService apiService = ApiService(); final ImagePicker _picker = ImagePicker(); @@ -42,6 +45,7 @@ class _EditPenyakitPageState extends State { String? _errorMessage; bool _isImageLoading = false; Uint8List? _currentImageBytes; + double _currentNilaiPakar = 0.0; @override void initState() { @@ -49,6 +53,8 @@ class _EditPenyakitPageState extends State { _namaController.text = widget.namaAwal; _deskripsiController.text = widget.deskripsiAwal; _penangananController.text = widget.penangananAwal; + _currentNilaiPakar = widget.nilai_pakar; + _nilaiPakarController.text = widget.nilai_pakar.toString(); _loadExistingImage(); } @@ -91,6 +97,21 @@ class _EditPenyakitPageState extends State { super.dispose(); } + // Validate and parse nilai_pakar input + double _parseNilaiPakar() { + if (_nilaiPakarController.text.isEmpty) { + return _currentNilaiPakar; // Return current value if field is empty + } + + try { + String input = _nilaiPakarController.text.trim().replaceAll(',', '.'); + return double.parse(input); + } catch (e) { + print("Error parsing nilai_pakar: $e"); + return _currentNilaiPakar; // Return current value if parsing fails + } + } + Future _updatePenyakit() async { try { setState(() { @@ -98,12 +119,18 @@ class _EditPenyakitPageState extends State { _errorMessage = null; }); + // Get nilai_pakar value with safety check + double nilaiPakar = _parseNilaiPakar(); + + print("Updating hama with nilai_pakar: $nilaiPakar"); + await apiService.updatePenyakit( widget.idPenyakit, _namaController.text, _deskripsiController.text, _penangananController.text, _pickedFile, + nilaiPakar, ); setState(() { @@ -260,6 +287,25 @@ class _EditPenyakitPageState extends State { maxLines: 3, ), SizedBox(height: 20), + TextField( + controller: _nilaiPakarController, + decoration: InputDecoration( + labelText: 'Nilai Pakar', + hintText: 'Contoh: 0.5', + ), + keyboardType: TextInputType.numberWithOptions(decimal: true), + onChanged: (value) { + // Validate as user types (optional) + try { + if (value.isNotEmpty) { + double.parse(value.replaceAll(',', '.')); + } + } catch (e) { + // Could show validation error here + } + }, + ), + SizedBox(height: 20), Text( 'Foto Penyakit', style: TextStyle( diff --git a/frontend/lib/admin/edit_rule_page.dart b/frontend/lib/admin/edit_rule_page.dart index 7678974..77c8e08 100644 --- a/frontend/lib/admin/edit_rule_page.dart +++ b/frontend/lib/admin/edit_rule_page.dart @@ -13,6 +13,8 @@ class EditRulePage extends StatefulWidget { final List nilaiPakarList; final int? selectedHamaId; final int? selectedPenyakitId; + final bool showHamaOnly; // Tambahkan ini + final bool showPenyakitOnly; // Tambahkan ini const EditRulePage({ Key? key, @@ -21,6 +23,8 @@ class EditRulePage extends StatefulWidget { required this.selectedRuleIds, required this.selectedGejalaIds, required this.nilaiPakarList, + this.showHamaOnly = false, // Tambahkan default value + this.showPenyakitOnly = false, this.selectedHamaId, this.selectedPenyakitId, }) : super(key: key); @@ -37,6 +41,9 @@ class _EditRulePageState extends State { bool isLoading = true; + bool showHamaOnly = false; + bool showPenyakitOnly = false; + final api = ApiService(); // Deklarasi variabel untuk menampung data dari API @@ -154,6 +161,8 @@ class _EditRulePageState extends State { @override void initState() { super.initState(); + showHamaOnly = widget.showHamaOnly; + showPenyakitOnly = widget.showPenyakitOnly; fetchData(); // Panggil fetchData saat halaman dibuka pertama kali // Inisialisasi dari widget parent @@ -250,62 +259,68 @@ class _EditRulePageState extends State { child: ListView( children: [ // Pilih Hama - Text("Pilih Hama"), - DropdownButton( - isExpanded: true, - value: selectedHamaId, - hint: Text('Pilih Hama'), - items: - hamaList.isNotEmpty - ? hamaList.map>((hama) { - return DropdownMenuItem( - value: hama['id'], - child: Text(hama['nama']), - ); - }).toList() - : [ - DropdownMenuItem( - value: null, - child: Text("Data tidak tersedia"), - ), - ], - onChanged: (value) { - setState(() { - selectedHamaId = value; - }); - }, - ), - SizedBox(height: 16), + if (!showPenyakitOnly) ...[ + Text("Pilih Hama"), + DropdownButton( + isExpanded: true, + value: selectedHamaId, + hint: Text('Pilih Hama'), + items: + hamaList.isNotEmpty + ? hamaList.map>(( + hama, + ) { + return DropdownMenuItem( + value: hama['id'], + child: Text(hama['nama']), + ); + }).toList() + : [ + DropdownMenuItem( + value: null, + child: Text("Data tidak tersedia"), + ), + ], + onChanged: (value) { + setState(() { + selectedHamaId = value; + }); + }, + ), + SizedBox(height: 16), + ], // Pilih Penyakit - Text("Pilih Penyakit"), - DropdownButton( - isExpanded: true, - value: selectedPenyakitId, - hint: Text('Pilih Penyakit'), - items: - penyakitList.isNotEmpty - ? penyakitList.map>(( - penyakit, - ) { - return DropdownMenuItem( - value: penyakit['id'], - child: Text(penyakit['nama']), - ); - }).toList() - : [ - DropdownMenuItem( - value: null, - child: Text("Data tidak tersedia"), - ), - ], - onChanged: (value) { - setState(() { - selectedPenyakitId = value; - }); - }, - ), - SizedBox(height: 16), + if (!showHamaOnly) ...[ + Text("Pilih Penyakit"), + DropdownButton( + isExpanded: true, + value: selectedPenyakitId, + hint: Text('Pilih Penyakit'), + items: + penyakitList.isNotEmpty + ? penyakitList.map>(( + penyakit, + ) { + return DropdownMenuItem( + value: penyakit['id'], + child: Text(penyakit['nama']), + ); + }).toList() + : [ + DropdownMenuItem( + value: null, + child: Text("Data tidak tersedia"), + ), + ], + onChanged: (value) { + setState(() { + selectedPenyakitId = value; + }); + }, + ), + SizedBox(height: 16), + ], // Pilih Gejala dan Nilai Pakar Text("Pilih Gejala"), diff --git a/frontend/lib/admin/hama_page.dart b/frontend/lib/admin/hama_page.dart index 8b5dec9..7abee0a 100644 --- a/frontend/lib/admin/hama_page.dart +++ b/frontend/lib/admin/hama_page.dart @@ -67,128 +67,6 @@ class _HamaPageState extends State { ); } - // void _tambahHama() { - // TextEditingController namaController = TextEditingController(); - // TextEditingController penangananController = TextEditingController(); - // TextEditingController deskripsiController = TextEditingController(); - - // showDialog( - // context: context, - // builder: (context) { - // return AlertDialog( - // title: Text('Tambah Hama Baru'), - // content: Column( - // mainAxisSize: MainAxisSize.min, - // children: [ - // TextField( - // controller: namaController, - // decoration: InputDecoration(labelText: 'Nama'), - // ), - // TextField( - // controller: deskripsiController, - // decoration: InputDecoration(labelText: 'Deskripsi'), - // ), - // TextField( - // controller: penangananController, - // decoration: InputDecoration(labelText: 'Penanganan'), - // ), - // ], - // ), - // actions: [ - // TextButton( - // onPressed: () => Navigator.pop(context), - // child: Text('Batal', style: TextStyle(color: Colors.black)), - // ), - // ElevatedButton( - // onPressed: () async { - // if (namaController.text.isNotEmpty && - // deskripsiController.text.isNotEmpty && - // penangananController.text.isNotEmpty) { - // try { - // await apiService.createHama( - // namaController.text, - // deskripsiController.text, - // penangananController.text, - // ); - // _fetchHama(); - // Navigator.pop(context); - // } catch (e) { - // print("Error adding hama: $e"); - // } - // } - // }, - // child: Text('Simpan', style: TextStyle(color: Colors.black)), - // ), - // ], - // ); - // }, - // ).then((_) { - // namaController.dispose(); - // deskripsiController.dispose(); - // penangananController.dispose(); - // }); - // } - - // void showEditDialog(BuildContext context, Map hama) { - // final TextEditingController editNamaController = TextEditingController( - // text: hama['nama'] ?? '', - // ); - // final TextEditingController editDeskripsiController = TextEditingController( - // text: hama['deskripsi'] ?? '', - // ); - // final TextEditingController editPenangananController = - // TextEditingController(text: hama['penanganan'] ?? ''); - - // showDialog( - // context: context, - // builder: (context) { - // return AlertDialog( - // title: Text('Edit Hama'), - // content: Column( - // mainAxisSize: MainAxisSize.min, - // children: [ - // TextField( - // controller: editNamaController, - // decoration: InputDecoration(labelText: 'Nama'), - // ), - // TextField( - // controller: editDeskripsiController, - // decoration: InputDecoration(labelText: 'Deskripsi'), - // ), - // TextField( - // controller: editPenangananController, - // decoration: InputDecoration(labelText: 'Penanganan'), - // ), - // ], - // ), - // actions: [ - // TextButton( - // onPressed: () => Navigator.pop(context), - // child: Text('Batal'), - // ), - // ElevatedButton( - // onPressed: () async { - // try { - // await apiService.updateHama( - // hama['id'], - // editNamaController.text, - // editDeskripsiController.text, - // editPenangananController.text, - // ); - // _fetchHama(); - // Navigator.pop(context); - // } catch (e) { - // print("Error updating hama: $e"); - // } - // }, - // child: Text('Simpan', style: TextStyle(color: Colors.black)), - // ), - // ], - // ); - // }, - // ); - // } - //pagination int currentPage = 0; int rowsPerPage = 10; @@ -283,6 +161,38 @@ class _HamaPageState extends State { color: Color(0xFF9DC08D), ), onPressed: () { + // Parse nilai_pakar dengan aman + double nilaiPakar = 0.0; + if (hama['nilai_pakar'] != null) { + // Coba parse jika string + if (hama['nilai_pakar'] is String) { + try { + String nilaiStr = + hama['nilai_pakar'] + .toString() + .trim(); + if (nilaiStr.isNotEmpty) { + nilaiPakar = double.parse( + nilaiStr.replaceAll(',', '.'), + ); + } + } catch (e) { + print( + "Error parsing nilai_pakar: $e", + ); + } + } + // Langsung gunakan jika sudah double + else if (hama['nilai_pakar'] + is double) { + nilaiPakar = hama['nilai_pakar']; + } + // Jika int, konversi ke double + else if (hama['nilai_pakar'] is int) { + nilaiPakar = + hama['nilai_pakar'].toDouble(); + } + } Navigator.push( context, MaterialPageRoute( @@ -296,6 +206,7 @@ class _HamaPageState extends State { penangananAwal: hama['penanganan'] ?? '', gambarUrl: hama['foto'] ?? '', + nilai_pakar: nilaiPakar, onHamaUpdated: _fetchHama, // fungsi untuk refresh list setelah update ), diff --git a/frontend/lib/admin/penyakit_page.dart b/frontend/lib/admin/penyakit_page.dart index 66923b3..337e03b 100644 --- a/frontend/lib/admin/penyakit_page.dart +++ b/frontend/lib/admin/penyakit_page.dart @@ -162,6 +162,38 @@ class _PenyakitPageState extends State { color: Color(0xFF9DC08D), ), onPressed: () { + // Parse nilai_pakar dengan aman + double nilaiPakar = 0.0; + if (penyakit['nilai_pakar'] != null) { + // Coba parse jika string + if (penyakit['nilai_pakar'] is String) { + try { + String nilaiStr = + penyakit['nilai_pakar'] + .toString() + .trim(); + if (nilaiStr.isNotEmpty) { + nilaiPakar = double.parse( + nilaiStr.replaceAll(',', '.'), + ); + } + } catch (e) { + print( + "Error parsing nilai_pakar: $e", + ); + } + } + // Langsung gunakan jika sudah double + else if (penyakit['nilai_pakar'] + is double) { + nilaiPakar = penyakit['nilai_pakar']; + } + // Jika int, konversi ke double + else if (penyakit['nilai_pakar'] is int) { + nilaiPakar = + penyakit['nilai_pakar'].toDouble(); + } + } Navigator.push( context, MaterialPageRoute( @@ -176,6 +208,7 @@ class _PenyakitPageState extends State { penyakit['penanganan'] ?? '', gambarUrl: penyakit['foto'] ?? '', + nilai_pakar: nilaiPakar, onPenyakitUpdated: _fetchPenyakit, // fungsi untuk refresh list setelah update ), diff --git a/frontend/lib/admin/rule_page.dart b/frontend/lib/admin/rule_page.dart index 2f8f7bf..d5e88b2 100644 --- a/frontend/lib/admin/rule_page.dart +++ b/frontend/lib/admin/rule_page.dart @@ -176,141 +176,176 @@ class _RulePageState extends State { padding: const EdgeInsets.all(16.0), child: Column( children: [ - Align( - alignment: Alignment.centerRight, - child: ElevatedButton.icon( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: - (context) => TambahRulePage( - isEditing: false, // Menandakan mode tambah - isEditingHama: - true, // Atur sesuai dengan jenis rule - selectedRuleIds: [], - selectedGejalaIds: [], - nilaiPakarList: [], - selectedHamaId: null, // Hanya jika rule hama - selectedPenyakitId: - null, // Hanya jika rule penyakit - ), - ), - ).then((_) => fetchRules()); - }, - icon: Icon(Icons.add), - label: Text("Tambah Rule"), - ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Button untuk tambah rule hama + ElevatedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TambahRulePage( + isEditing: false, + isEditingHama: true, // Menandakan ini adalah rule hama + selectedRuleIds: [], + selectedGejalaIds: [], + nilaiPakarList: [], + selectedHamaId: null, + selectedPenyakitId: null, + showHamaOnly: true, // Parameter baru untuk menampilkan hanya dropdown hama + ), + ), + ).then((_) => fetchRules()); + }, + icon: Icon(Icons.bug_report), + label: Text("Tambah Rule Hama"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + ), + SizedBox(width: 10), + // Button untuk tambah rule penyakit + ElevatedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TambahRulePage( + isEditing: false, + isEditingHama: false, // Menandakan ini adalah rule penyakit + selectedRuleIds: [], + selectedGejalaIds: [], + nilaiPakarList: [], + selectedHamaId: null, + selectedPenyakitId: null, + showPenyakitOnly: true, // Parameter baru untuk menampilkan hanya dropdown penyakit + ), + ), + ).then((_) => fetchRules()); + }, + icon: Icon(Icons.healing), + label: Text("Tambah Rule Penyakit"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + ), + ], ), const SizedBox(height: 16), isLoading ? const Center(child: CircularProgressIndicator()) : Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: DataTable( - columns: const [ - DataColumn(label: Text('No')), - DataColumn(label: Text('Hama / Penyakit')), - DataColumn(label: Text('Gejala')), - DataColumn(label: Text('nilai pakar')), - DataColumn(label: Text('Aksi')), - ], - rows: List.generate(rules.length, (index) { - final rule = rules[index]; + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + columns: const [ + DataColumn(label: Text('No')), + DataColumn(label: Text('Hama / Penyakit')), + DataColumn(label: Text('Gejala')), + DataColumn(label: Text('nilai pakar')), + DataColumn(label: Text('Aksi')), + ], + rows: List.generate(rules.length, (index) { + final rule = rules[index]; - final namaKategori = - rule['id_penyakit'] != null - ? rule['nama_penyakit'] ?? '-' - : rule['nama_hama'] ?? '-'; + final namaKategori = + rule['id_penyakit'] != null + ? rule['nama_penyakit'] ?? '-' + : rule['nama_hama'] ?? '-'; - final isPenyakit = rule['id_penyakit'] != null; + final isPenyakit = rule['id_penyakit'] != null; - return DataRow( - cells: [ - DataCell(Text((index + 1).toString())), - DataCell(Text(namaKategori)), - DataCell(Text(rule['nama_gejala'] ?? '-')), - DataCell( - Text(rule['nilai_pakar']?.toString() ?? '-'), - ), - DataCell( - Row( - children: [ - IconButton( - icon: const Icon( - Icons.edit, - color: Colors.orange, - ), - onPressed: () { - if (rule != null && - rule['id'] != null && - rule['id_gejala'] != null && - rule['nilai_pakar'] != null) { - Navigator.push( - context, - MaterialPageRoute( - builder: - (context) => EditRulePage( - isEditing: true, - isEditingHama: true, - selectedRuleIds: [ - rule['id'] as int, - ], - selectedGejalaIds: [ - rule['id_gejala'] as int, - ], - nilaiPakarList: [ - (rule['nilai_pakar'] as num) - .toDouble(), - ], - selectedHamaId: - rule['id_hama'] - as int?, - selectedPenyakitId: rule['id_penyakit'] as int?, // Tambahkan type cast ke int? - ), - ), - ); - } else { - // Tampilkan pesan error jika data rule tidak lengkap - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text( - "Data rule tidak lengkap atau tidak valid", - ), - backgroundColor: Colors.red, - ), - ); - - // Debug info - print("Rule data: $rule"); - } - }, - ), - - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.red, - ), - onPressed: () { - deleteRule(rule); - }, - ), - ], + return DataRow( + cells: [ + DataCell(Text((index + 1).toString())), + DataCell(Text(namaKategori)), + DataCell(Text(rule['nama_gejala'] ?? '-')), + DataCell( + Text(rule['nilai_pakar']?.toString() ?? '-'), ), - ), - ], - ); - }), + DataCell( + Row( + children: [ + IconButton( + icon: const Icon( + Icons.edit, + color: Colors.orange, + ), + onPressed: () { + if (rule != null && + rule['id'] != null && + rule['id_gejala'] != null && + rule['nilai_pakar'] != null) { + // Tentukan jenis rule untuk editing + final bool editingHama = rule['id_hama'] != null; + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EditRulePage( + isEditing: true, + isEditingHama: editingHama, + selectedRuleIds: [ + rule['id'] as int, + ], + selectedGejalaIds: [ + rule['id_gejala'] as int, + ], + nilaiPakarList: [ + (rule['nilai_pakar'] as num) + .toDouble(), + ], + selectedHamaId: + rule['id_hama'] as int?, + selectedPenyakitId: rule['id_penyakit'] as int?, + // Tambahkan parameter untuk menentukan dropdown yang ditampilkan + showHamaOnly: editingHama, + showPenyakitOnly: !editingHama, + ), + ), + ).then((_) => fetchRules()); + } else { + // Tampilkan pesan error jika data rule tidak lengkap + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + "Data rule tidak lengkap atau tidak valid", + ), + backgroundColor: Colors.red, + ), + ); + + // Debug info + print("Rule data: $rule"); + } + }, + ), + IconButton( + icon: const Icon( + Icons.delete, + color: Colors.red, + ), + onPressed: () { + deleteRule(rule); + }, + ), + ], + ), + ), + ], + ); + }), + ), ), ), - ), ], ), ), ); } -} +} \ No newline at end of file diff --git a/frontend/lib/admin/tambah_hama_page.dart b/frontend/lib/admin/tambah_hama_page.dart index 7566ad7..8d9605e 100644 --- a/frontend/lib/admin/tambah_hama_page.dart +++ b/frontend/lib/admin/tambah_hama_page.dart @@ -19,6 +19,7 @@ class _TambahHamaPageState extends State { final TextEditingController namaController = TextEditingController(); final TextEditingController deskripsiController = TextEditingController(); final TextEditingController penangananController = TextEditingController(); + final TextEditingController nilaiPakarController = TextEditingController(); final ApiService apiService = ApiService(); final ImagePicker _picker = ImagePicker(); File? _imageFile; @@ -32,19 +33,24 @@ class _TambahHamaPageState extends State { namaController.dispose(); deskripsiController.dispose(); penangananController.dispose(); + nilaiPakarController.dispose(); super.dispose(); } Future _simpanHama() async { if (namaController.text.isNotEmpty && deskripsiController.text.isNotEmpty && - penangananController.text.isNotEmpty) { + penangananController.text.isNotEmpty && + nilaiPakarController.text.isNotEmpty) { try { + String nilaiInput = nilaiPakarController.text.replaceAll(',', '.'); + double nilaiPakar = double.parse(nilaiInput); await apiService.createHama( namaController.text, deskripsiController.text, penangananController.text, - _pickedFile, + _pickedFile, + nilaiPakar, ); widget.onHamaAdded(); Navigator.pop(context); @@ -136,6 +142,12 @@ class _TambahHamaPageState extends State { maxLines: 3, ), SizedBox(height: 15), + TextField( + controller: nilaiPakarController, + decoration: InputDecoration(labelText: 'Nilai Pakar'), + maxLines: 3, + ), + SizedBox(height: 15), Text('Foto'), (_webImage != null) ? Image.memory( diff --git a/frontend/lib/admin/tambah_penyakit_page.dart b/frontend/lib/admin/tambah_penyakit_page.dart index d908fe4..51f6daa 100644 --- a/frontend/lib/admin/tambah_penyakit_page.dart +++ b/frontend/lib/admin/tambah_penyakit_page.dart @@ -18,6 +18,7 @@ class _TambahPenyakitPageState extends State { final TextEditingController namaController = TextEditingController(); final TextEditingController deskripsiController = TextEditingController(); final TextEditingController penangananController = TextEditingController(); + final TextEditingController nilaiPakarController = TextEditingController(); final ApiService apiService = ApiService(); final ImagePicker _picker = ImagePicker(); File? _imageFile; @@ -31,19 +32,24 @@ class _TambahPenyakitPageState extends State { namaController.dispose(); deskripsiController.dispose(); penangananController.dispose(); + nilaiPakarController.dispose(); super.dispose(); } Future _simpanPenyakit() async { if (namaController.text.isNotEmpty && deskripsiController.text.isNotEmpty && - penangananController.text.isNotEmpty) { + penangananController.text.isNotEmpty && + nilaiPakarController.text.isNotEmpty) { try { + String nilaiInput = nilaiPakarController.text.replaceAll(',', '.'); + double nilaiPakar = double.parse(nilaiInput); await apiService.createPenyakit( namaController.text, deskripsiController.text, penangananController.text, _pickedFile, + nilaiPakar, ); widget.onPenyakitAdded(); Navigator.pop(context); @@ -135,6 +141,12 @@ class _TambahPenyakitPageState extends State { maxLines: 3, ), SizedBox(height: 15), + TextField( + controller: nilaiPakarController, + decoration: InputDecoration(labelText: 'nilai pakar'), + maxLines: 3, + ), + SizedBox(height: 15), (_webImage != null) ? Image.memory( _webImage!, diff --git a/frontend/lib/admin/tambah_rule_page.dart b/frontend/lib/admin/tambah_rule_page.dart index 539538b..64e6327 100644 --- a/frontend/lib/admin/tambah_rule_page.dart +++ b/frontend/lib/admin/tambah_rule_page.dart @@ -13,6 +13,8 @@ class TambahRulePage extends StatefulWidget { final List nilaiPakarList; final int? selectedHamaId; final int? selectedPenyakitId; + final bool showHamaOnly; // Tambahkan ini + final bool showPenyakitOnly; const TambahRulePage({ Key? key, @@ -23,6 +25,8 @@ class TambahRulePage extends StatefulWidget { required this.nilaiPakarList, this.selectedHamaId, this.selectedPenyakitId, + this.showHamaOnly = false, // Tambahkan default value + this.showPenyakitOnly = false, }) : super(key: key); } @@ -31,12 +35,16 @@ class _TambahRulePageState extends State { int? selectedPenyakitId; List selectedGejalaIds = [null]; List nilaiPakarList = [0.5]; - List selectedRuleIds = []; // List paralel dengan selectedGejalaIds dan nilaiPakarList + List selectedRuleIds = + []; // List paralel dengan selectedGejalaIds dan nilaiPakarList bool isEditing = true; // atau false jika sedang edit penyakit bool isLoading = true; + bool showHamaOnly = false; + bool showPenyakitOnly = false; + final api = ApiService(); // Deklarasi variabel untuk menampung data dari API @@ -119,6 +127,8 @@ class _TambahRulePageState extends State { @override void initState() { super.initState(); + showHamaOnly = widget.showHamaOnly; + showPenyakitOnly = widget.showPenyakitOnly; fetchData(); // Panggil fetchData saat halaman dibuka pertama kali } @@ -173,57 +183,6 @@ class _TambahRulePageState extends State { } } - // void updateRules() async { - // if (selectedPenyakitId == null && selectedHamaId == null) { - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar(content: Text("Pilih minimal satu: Penyakit atau Hama")), - // ); - // return; - // } - - // try { - // for (int i = 0; i < selectedGejalaIds.length; i++) { - // final idRule = selectedRuleIds[i]; - // final idGejala = selectedGejalaIds[i]; - // final nilai = nilaiPakarList[i]; - - // if (idRule != null && idGejala != null) { - // http.Response response; - - // if (selectedPenyakitId != null) { - // response = await ApiService.updateRulePenyakit( - // id: idRule, - // idGejala: idGejala, - // idPenyakit: selectedPenyakitId, - // nilaiPakar: nilai, - // ); - // } else { - // response = await ApiService.updateRuleHama( - // id: idRule, - // idGejala: idGejala, - // idHama: selectedHamaId, - // nilaiPakar: nilai, - // ); - // } - - // if (response.statusCode != 200 && response.statusCode != 201) { - // throw Exception("Gagal mengupdate rule"); - // } - // } - // } - - // ScaffoldMessenger.of( - // context, - // ).showSnackBar(SnackBar(content: Text("Data berhasil diperbarui"))); - // Navigator.pop(context); - // } catch (e) { - // print('Gagal memperbarui data: $e'); - // ScaffoldMessenger.of( - // context, - // ).showSnackBar(SnackBar(content: Text("Gagal memperbarui data"))); - // } - // } - @override Widget build(BuildContext context) { return Scaffold( @@ -243,62 +202,68 @@ class _TambahRulePageState extends State { child: ListView( children: [ // Pilih Hama - Text("Pilih Hama"), - DropdownButton( - isExpanded: true, - value: selectedHamaId, - hint: Text('Pilih Hama'), - items: - hamaList.isNotEmpty - ? hamaList.map>((hama) { - return DropdownMenuItem( - value: hama['id'], - child: Text(hama['nama']), - ); - }).toList() - : [ - DropdownMenuItem( - value: null, - child: Text("Data tidak tersedia"), - ), - ], - onChanged: (value) { - setState(() { - selectedHamaId = value; - }); - }, - ), - SizedBox(height: 16), + if (!showPenyakitOnly) ...[ + Text("Pilih Hama"), + DropdownButton( + isExpanded: true, + value: selectedHamaId, + hint: Text('Pilih Hama'), + items: + hamaList.isNotEmpty + ? hamaList.map>(( + hama, + ) { + return DropdownMenuItem( + value: hama['id'], + child: Text(hama['nama']), + ); + }).toList() + : [ + DropdownMenuItem( + value: null, + child: Text("Data tidak tersedia"), + ), + ], + onChanged: (value) { + setState(() { + selectedHamaId = value; + }); + }, + ), + SizedBox(height: 16), + ], // Pilih Penyakit - Text("Pilih Penyakit"), - DropdownButton( - isExpanded: true, - value: selectedPenyakitId, - hint: Text('Pilih Penyakit'), - items: - penyakitList.isNotEmpty - ? penyakitList.map>(( - penyakit, - ) { - return DropdownMenuItem( - value: penyakit['id'], - child: Text(penyakit['nama']), - ); - }).toList() - : [ - DropdownMenuItem( - value: null, - child: Text("Data tidak tersedia"), - ), - ], - onChanged: (value) { - setState(() { - selectedPenyakitId = value; - }); - }, - ), - SizedBox(height: 16), + if (!showHamaOnly) ...[ + Text("Pilih Penyakit"), + DropdownButton( + isExpanded: true, + value: selectedPenyakitId, + hint: Text('Pilih Penyakit'), + items: + penyakitList.isNotEmpty + ? penyakitList.map>(( + penyakit, + ) { + return DropdownMenuItem( + value: penyakit['id'], + child: Text(penyakit['nama']), + ); + }).toList() + : [ + DropdownMenuItem( + value: null, + child: Text("Data tidak tersedia"), + ), + ], + onChanged: (value) { + setState(() { + selectedPenyakitId = value; + }); + }, + ), + SizedBox(height: 16), + ], // Pilih Gejala dan Nilai Pakar Text("Pilih Gejala"), @@ -398,7 +363,33 @@ class _TambahRulePageState extends State { // Tombol untuk menambah rule ElevatedButton( onPressed: () { - // Panggil fungsi saveRules untuk menyimpan data + // Cek duplikasi gejala + final uniqueGejala = selectedGejalaIds.toSet(); + if (uniqueGejala.length != + selectedGejalaIds.length) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Terdapat gejala yang sama, harap pilih gejala yang berbeda.', + ), + ), + ); + return; // Gagal simpan + } + + // Cek apakah semua nilai gejala sudah dipilih (tidak null) + if (selectedGejalaIds.contains(null)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Harap lengkapi semua pilihan gejala.', + ), + ), + ); + return; + } + + // Panggil fungsi saveRules jika valid saveRules(); }, child: Text('Tambah Rule'), diff --git a/frontend/lib/api_services/api_services.dart b/frontend/lib/api_services/api_services.dart index 477b4bb..e9ef531 100644 --- a/frontend/lib/api_services/api_services.dart +++ b/frontend/lib/api_services/api_services.dart @@ -13,6 +13,7 @@ class ApiService { static const String penyakitUrl = 'http://localhost:5000/api/penyakit'; static const String rulesPenyakitUrl ='http://localhost:5000/api/rules_penyakit'; static const String rulesHamaUrl = 'http://localhost:5000/api/rules_hama'; + static const String userUrl = 'http://localhost:5000/api/users'; static const Duration timeout = Duration(seconds: 15); // Fungsi Login (dengan perbaikan) @@ -123,7 +124,6 @@ class ApiService { } // Ambil semua hama - // Get all hama Future>> getHama() async { try { final response = await http.get(Uri.parse(hamaUrl)).timeout(timeout); @@ -251,6 +251,7 @@ class ApiService { String deskripsi, String penanganan, XFile? pickedFile, + double nilai_pakar ) async { try { var uri = Uri.parse(hamaUrl); @@ -259,6 +260,7 @@ class ApiService { request.fields['nama'] = nama; request.fields['deskripsi'] = deskripsi; request.fields['penanganan'] = penanganan; + request.fields['nilai_pakar'] = nilai_pakar.toString(); print('Mengirim request ke: $uri'); print('Dengan fields: ${request.fields}'); @@ -330,6 +332,7 @@ class ApiService { String deskripsi, String penanganan, XFile? pickedFile, + double nilai_pakar ) async { try { var uri = Uri.parse('$hamaUrl/$id'); @@ -339,6 +342,7 @@ class ApiService { request.fields['nama'] = nama; request.fields['deskripsi'] = deskripsi; request.fields['penanganan'] = penanganan; + request.fields['nilai_pakar'] = nilai_pakar.toString(); // Log untuk debugging print('Mengirim request ke: $uri'); @@ -524,12 +528,33 @@ class ApiService { } } + Future getPenyakitImageBytesByFilename(String filename) async { + try { + final url = Uri.parse('http://localhost:5000/image_penyakit/$filename'); + print('Fetching image from: $url'); + final response = await http.get(url); + + if (response.statusCode == 200) { + return response.bodyBytes; + } else { + print('Failed to fetch image. Status: ${response.statusCode}'); + print('Response body: ${response.body}'); + return null; + } + } catch (e) { + print('Error fetching image by filename: $e'); + return null; + } +} + + // Tambah penyakit baru (kode otomatis) Future> createPenyakit( String nama, String deskripsi, String penanganan, XFile? pickedFile, + double nilai_pakar ) async { try { var uri = Uri.parse(penyakitUrl); @@ -538,6 +563,7 @@ class ApiService { request.fields['nama'] = nama; request.fields['deskripsi'] = deskripsi; request.fields['penanganan'] = penanganan; + request.fields['nilai_pakar'] = nilai_pakar.toString(); print('Mengirim request ke: $uri'); print('Dengan fields: ${request.fields}'); @@ -609,6 +635,7 @@ class ApiService { String deskripsi, String penanganan, XFile? pickedFile, + double nilai_pakar ) async { try { var uri = Uri.parse('$penyakitUrl/$id'); @@ -618,6 +645,7 @@ class ApiService { request.fields['nama'] = nama; request.fields['deskripsi'] = deskripsi; request.fields['penanganan'] = penanganan; + request.fields['nilai_pakar'] = nilai_pakar.toString(); // Log untuk debugging print('Mengirim request ke: $uri'); @@ -889,4 +917,39 @@ class ApiService { final response = await http.delete(Uri.parse('$rulesHamaUrl/$id')); return response; } + + //get users + Future>> getUsers({String? role}) async { + try { + String url = ApiService.userUrl; + if (role != null) { + url += '?role=$role'; + } + + final response = await http.get(Uri.parse(url)); + + if (response.statusCode == 200) { + final List responseData = jsonDecode(response.body); + + // Filter berdasarkan role jika perlu + if (role != null) { + // Mengambil user dengan role yang sesuai + final filteredData = responseData.where((user) => user['role'] == role).toList(); + return List>.from(filteredData); + } + + // Jika tidak ada filter role, kembalikan semua data + return List>.from(responseData); + } else { + throw Exception("Gagal mengambil data user: ${response.statusCode}"); + } + } catch (e) { + print("Error getUsers: $e"); + throw Exception("Gagal mengambil data user"); + } } + + +} + + diff --git a/frontend/lib/user/detail_hama_page.dart b/frontend/lib/user/detail_hama_page.dart index 3b6690e..2ff5f8c 100644 --- a/frontend/lib/user/detail_hama_page.dart +++ b/frontend/lib/user/detail_hama_page.dart @@ -57,23 +57,40 @@ class _DetailHamaPageState extends State { // Widget untuk menampilkan gambar dengan penanganan error yang lebih baik Widget _buildImageWidget(String? filename) { if (filename == null || filename.isEmpty) { - return Text("Tidak ada gambar tersedia"); + return _buildPlaceholderImage( + "Tidak ada gambar tersedia", + Icons.image_not_supported, + ); } return FutureBuilder( future: ApiService().getHamaImageBytesByFilename(filename), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { - return CircularProgressIndicator(); + return SizedBox( + height: 200, + width: double.infinity, + child: Center(child: CircularProgressIndicator()), + ); } else if (snapshot.hasError || snapshot.data == null) { - return Text("Gagal memuat gambar"); + return _buildPlaceholderImage( + "Gagal memuat gambar", + Icons.broken_image, + ); } else { - return Image.memory(snapshot.data!, fit: BoxFit.cover); + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.memory( + snapshot.data!, + height: 200, + width: double.infinity, + fit: BoxFit.contain, // untuk memastikan proporsional & penuh + ), + ); } }, ); -} - + } // Widget untuk placeholder gambar Widget _buildPlaceholderImage(String message, IconData icon) { diff --git a/frontend/lib/user/detail_penyakit_page.dart b/frontend/lib/user/detail_penyakit_page.dart index 3a110b9..90e1ecf 100644 --- a/frontend/lib/user/detail_penyakit_page.dart +++ b/frontend/lib/user/detail_penyakit_page.dart @@ -1,9 +1,122 @@ import 'package:flutter/material.dart'; +import 'package:frontend/api_services/api_services.dart'; +import 'dart:typed_data'; -class DetailPenyakitPage extends StatelessWidget { - final Map detailPenyakit; +class DetailPenyakitPage extends StatefulWidget { + final Map DetailPenyakit; + final int? penyakitId; - const DetailPenyakitPage({required this.detailPenyakit}); + const DetailPenyakitPage({Key? key, required this.DetailPenyakit, this.penyakitId}) + : super(key: key); + + @override + _DetailPenyakitPageState createState() => _DetailPenyakitPageState(); +} + +class _DetailPenyakitPageState extends State { + late Future> _detailPenyakitFuture; + late Map _currentDetailPenyakit; + + @override + void initState() { + super.initState(); + _currentDetailPenyakit = widget.DetailPenyakit; + + // Jika hamaId tersedia, fetch data terbaru dari API + if (widget.penyakitId != null) { + _detailPenyakitFuture = _fetchDetailPenyakit(widget.penyakitId!); + } else { + // Jika tidak ada ID, gunakan data yang sudah diberikan + _detailPenyakitFuture = Future.value(widget.DetailPenyakit); + } + } + + Future> _fetchDetailPenyakit(int id) async { + try { + final detailData = await ApiService().getPenyakitById(id); + setState(() { + _currentDetailPenyakit = detailData; + }); + return detailData; + } catch (e) { + print('Error fetching detail penyakit: $e'); + // Jika gagal fetch, gunakan data yang sudah ada + return widget.DetailPenyakit; + } + } + + // Fungsi untuk memvalidasi URL gambar + bool _isValidImageUrl(String? url) { + if (url == null || url.isEmpty) return false; + + // Periksa apakah URL berakhir dengan ekstensi gambar yang umum + final validExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp']; + return validExtensions.any((ext) => url.toLowerCase().endsWith(ext)); + } + + // Widget untuk menampilkan gambar dengan penanganan error yang lebih baik + Widget _buildImageWidget(String? filename) { + if (filename == null || filename.isEmpty) { + return _buildPlaceholderImage( + "Tidak ada gambar tersedia", + Icons.image_not_supported, + ); + } + + return FutureBuilder( + future: ApiService().getPenyakitImageBytesByFilename(filename), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return SizedBox( + height: 200, + width: double.infinity, + child: Center(child: CircularProgressIndicator()), + ); + } else if (snapshot.hasError || snapshot.data == null) { + return _buildPlaceholderImage( + "Gagal memuat gambar", + Icons.broken_image, + ); + } else { + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.memory( + snapshot.data!, + height: 200, + width: double.infinity, + fit: BoxFit.contain, // untuk memastikan proporsional & penuh + ), + ); + } + }, + ); + } + + // Widget untuk placeholder gambar + Widget _buildPlaceholderImage(String message, IconData icon) { + return Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 64, color: Colors.grey[600]), + SizedBox(height: 8), + Text( + message, + style: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } @override Widget build(BuildContext context) { @@ -11,96 +124,123 @@ class DetailPenyakitPage extends StatelessWidget { backgroundColor: Color(0xFF9DC08D), appBar: AppBar( backgroundColor: Color(0xFF9DC08D), - title: Text( - "Detail Penyakit", - style: TextStyle(color: Colors.white), - ), + title: Text("Detail Penyakit", style: TextStyle(color: Colors.white)), leading: IconButton( icon: Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.of(context).pop(), ), ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - if (detailPenyakit["gambar"] != null && detailPenyakit["gambar"]!.isNotEmpty) - ClipRRect( + body: FutureBuilder>( + future: _detailPenyakitFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center( + child: CircularProgressIndicator(color: Colors.white), + ); + } + + if (snapshot.hasError) { + print('Error: ${snapshot.error}'); + // Tampilkan data yang sudah ada jika terjadi error + return _buildDetailContent(_currentDetailPenyakit); + } + print("Snapshot data runtimeType: ${snapshot.data.runtimeType}"); + print("Snapshot data content: ${snapshot.data}"); + + // Jika berhasil fetch data baru, tampilkan data tersebut + final detailData = snapshot.data ?? _currentDetailPenyakit; + return _buildDetailContent(detailData); + }, + ), + ); + } + + Widget _buildDetailContent(Map detailData) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + // Tampilkan foto dari database dengan penanganan error yang lebih baik + _buildImageWidget(detailData["foto"]), + SizedBox(height: 16), + + // Card Nama Hama + SizedBox( + width: double.infinity, + child: Card( + elevation: 6, + shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - child: Image.asset( - detailPenyakit["gambar"]!, - height: 200, - width: 200, // Biar gambar full lebar - fit: BoxFit.cover, - ), ), - SizedBox(height: 16), - - // Card Nama Penyakit - SizedBox( - width: double.infinity, // Bikin card full lebar - child: Card( - elevation: 6, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Nama Penyakit:", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Nama Penyakit:", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, ), - SizedBox(height: 8), - Text( - detailPenyakit["nama"] ?? "Nama penyakit tidak tersedia", - style: TextStyle(fontSize: 16), - ), - ], - ), + ), + SizedBox(height: 8), + Text( + detailData["nama"] ?? "Nama hama tidak tersedia", + style: TextStyle(fontSize: 16), + ), + ], ), ), ), - SizedBox(height: 16), + ), + SizedBox(height: 16), - // Card Deskripsi + Penanganan - SizedBox( - width: double.infinity, // Bikin card full lebar - child: Card( - elevation: 6, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Deskripsi:", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + // Card Deskripsi + Penanganan + SizedBox( + width: double.infinity, + child: Card( + elevation: 6, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Deskripsi:", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, ), - SizedBox(height: 8), - Text( - detailPenyakit["deskripsi"] ?? "Deskripsi tidak tersedia", - style: TextStyle(fontSize: 16), + ), + SizedBox(height: 8), + Text( + detailData["deskripsi"] ?? "Deskripsi tidak tersedia", + style: TextStyle(fontSize: 16), + ), + SizedBox(height: 16), + Text( + "Penanganan:", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, ), - SizedBox(height: 16), - Text( - "Penanganan:", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - SizedBox(height: 8), - Text( - detailPenyakit["penanganan"] ?? "Penanganan tidak tersedia", - style: TextStyle(fontSize: 16), - ), - ], - ), + ), + SizedBox(height: 8), + Text( + detailData["penanganan"] ?? "Penanganan tidak tersedia", + style: TextStyle(fontSize: 16), + ), + ], ), ), ), - ], - ), + ), + ], ), ), ); diff --git a/frontend/lib/user/penyakit_page.dart b/frontend/lib/user/penyakit_page.dart index bbfb8d2..2df119f 100644 --- a/frontend/lib/user/penyakit_page.dart +++ b/frontend/lib/user/penyakit_page.dart @@ -68,7 +68,7 @@ class _PenyakitPageState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => DetailPenyakitPage(detailPenyakit: penyakit), + builder: (context) => DetailPenyakitPage(DetailPenyakit: penyakit), ), ); },