matiru

Blockchain-Based Supply Chain Transparency for Agricultural Produce
commit a0efb5e4dd1f1b5404aeefb297400f68e86f2b52
parent 15f5de3b58e534a9b125436f0ce189ffc22feac0
Author: maydayv7 <maydayv7@gmail.com>
Date:   Sat,  4 Oct 2025 20:16:59 +0530

feat: Fix user registration

Also improve search page

Diffstat:
MREADME.md | 21+++++++++------------
Dbackend/createUsers.js | 34----------------------------------
Abackend/initUsers.js | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rbackend/addToWallet.js -> backend/initWallet.js | 0
Mbackend/package-lock.json | 311++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mbackend/package.json | 7++++---
Mbackend/server.js | 13+++++--------
Mchaincode/index.js | 47++++++++++++++++++++++++++---------------------
Mfrontend/components/Scanner.js | 2+-
Mfrontend/package-lock.json | 41+++++++++++++++++++++++++++++++++++++++++
Mfrontend/package.json | 1+
Mfrontend/screens/Search.js | 560++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
12 files changed, 956 insertions(+), 195 deletions(-)

diff --git a/README.md b/README.md @@ -23,8 +23,7 @@ npm install ``` git clone https://github.com/hyperledger/fabric-samples.git curl -sSLO https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh && chmod +x install-fabric.sh -# Install Fabric binaries and Docker images -./install-fabric.sh +./install-fabric.sh # Install Fabric binaries and Docker images ``` Ensure the `config` directory is present inside `fabric-samples` @@ -33,8 +32,7 @@ Ensure the `config` directory is present inside `fabric-samples` ``` cd fabric-samples/test-network -# 'test-network' with CA enabled and channel "mychannel" -./network.sh up createChannel -ca +./network.sh up createChannel -ca -c CHANNEL_NAME ``` After initial setup, you can use the following commands: @@ -47,18 +45,17 @@ After initial setup, you can use the following commands: 4. Deploy Chaincode: ``` -# Chaincode name "produce" -./network.sh deployCC -ccn produce -ccp /path/to/CHAINCODE -ccl javascript +./network.sh deployCC -c CHANNEL_NAME -ccn CHAINCODE_NAME -ccp /path/to/chaincode -ccl javascript ``` 5. Sample Wallet Creation: ``` # From fabric-samples/test-network -cp organizations/peerOrganizations/org1.example.com/connection-org1.json /path/to/MATIRU/backend/connection-org1.json -cd /path/to/MATIRU/backend +cp organizations/peerOrganizations/org1.example.com/connection-org1.json /path/to/matiru/backend/connection-org1.json +cd /path/to/matiru/backend mkdir -p wallet -node addToWallet.js org1 /path/to/fabric-samples/TEST-NETWORK/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp ./wallet USER_NAME +node initWallet.js org1 /path/to/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp ./wallet USER_NAME ``` 6. The `.env` file must be created in `backend` like so: @@ -74,8 +71,8 @@ TOKEN_EXPIRES_IN=24h # Fabric Connection CCP_PATH=connection-org1.json WALLET_PATH=./wallet -CHANNEL=mychannel -CHAINCODE=produce +CHANNEL=CHANNEL_NAME +CHAINCODE=CHAINCODE_NAME IDENTITY=USER_NAME AS_LOCALHOST=true ``` @@ -86,7 +83,7 @@ Make sure the port is not blocked by a firewall ``` npm install -npm run create-users +npm run init npm start ``` diff --git a/backend/createUsers.js b/backend/createUsers.js @@ -1,34 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const bcrypt = require("bcryptjs"); - -const users = [ - { id: "farmer1", username: "farmer1", password: "password", role: "Farmer" }, - { - id: "distributor1", - username: "dist1", - password: "password", - role: "Distributor", - }, - { id: "retailer1", username: "ret1", password: "password", role: "Retailer" }, - { - id: "inspector1", - username: "insp1", - password: "password", - role: "Inspector", - }, -]; - -(async () => { - const out = users.map((u) => ({ - id: u.id, - username: u.username, - passwordHash: bcrypt.hashSync(u.password, 10), - role: u.role, - })); - const dest = path.join(__dirname, "users.json"); - fs.writeFileSync(dest, JSON.stringify(out, null, 2)); - console.log("users.json written to", dest); - console.log("Demo accounts: (username / password)"); - out.forEach((u) => console.log(`${u.username} / password (role: ${u.role})`)); -})(); diff --git a/backend/initUsers.js b/backend/initUsers.js @@ -0,0 +1,114 @@ +"use strict"; +require("dotenv").config(); +const fs = require("fs"); +const path = require("path"); +const bcrypt = require("bcryptjs"); +const { Gateway, Wallets } = require("fabric-network"); + +// Demo Users +const users = [ + { + id: "farmer1", + username: "farmer1", + password: "password", + role: "Farmer", + name: "Farmer One", + location: "Village A", + certification: ["Organic Certified"], + }, + { + id: "distributor1", + username: "dist1", + password: "password", + role: "Distributor", + name: "Distributor One", + location: "City Warehouse", + }, + { + id: "retailer1", + username: "ret1", + password: "password", + role: "Retailer", + name: "Retailer One", + location: "Market B", + }, + { + id: "inspector1", + username: "insp1", + password: "password", + role: "Inspector", + name: "Inspector One", + location: "Lab C", + }, +]; + +async function seedBackendUsers() { + const out = users.map((u) => ({ + id: u.id, + username: u.username, + passwordHash: bcrypt.hashSync(u.password, 10), + role: u.role, + })); + const dest = path.join(__dirname, "users.json"); + fs.writeFileSync(dest, JSON.stringify(out, null, 2)); + console.log("users.json written to", dest); + console.log("Demo accounts: (username / password)"); + out.forEach((u) => console.log(`${u.username} / password (role: ${u.role})`)); +} + +async function seedBlockchainUsers() { + try { + const ccpPath = path.resolve(__dirname, process.env.CCP_PATH); + const ccp = JSON.parse(fs.readFileSync(ccpPath, "utf8")); + + const walletPath = path.resolve(__dirname, process.env.WALLET_PATH); + const wallet = await Wallets.newFileSystemWallet(walletPath); + + const identity = await wallet.get(process.env.IDENTITY); + if (!identity) { + console.error( + `Identity "${process.env.IDENTITY}" not found in wallet: ${walletPath}` + ); + console.error("Run initWallet.js first"); + return; + } + + const gateway = new Gateway(); + await gateway.connect(ccp, { + wallet, + identity: process.env.IDENTITY, + discovery: { + enabled: true, + asLocalhost: process.env.AS_LOCALHOST === "true", + }, + }); + + const network = await gateway.getNetwork(process.env.CHANNEL); + const contract = network.getContract(process.env.CHAINCODE); + + for (const u of users) { + const userKey = `${u.role.toUpperCase()}-${u.id}`; + try { + await contract.evaluateTransaction("getUserDetails", userKey); + console.log(`User ${userKey} already exists, skipping`); + } catch { + console.log(`Registering ${u.role}: ${u.id}`); + await contract.submitTransaction( + "registerUser", + u.role, + JSON.stringify(u) + ); + } + } + + console.log("Blockchain users seeded successfully"); + gateway.disconnect(); + } catch (err) { + console.error("Error seeding blockchain users:", err); + } +} + +(async () => { + await seedBackendUsers(); + await seedBlockchainUsers(); +})(); diff --git a/backend/addToWallet.js b/backend/initWallet.js diff --git a/backend/package-lock.json b/backend/package-lock.json @@ -17,7 +17,8 @@ "fs-extra": "^11.3.2", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.1", - "multer": "^2.0.2" + "multer": "^2.0.2", + "nodemon": "^3.1.10" } }, "node_modules/@grpc/grpc-js": { @@ -161,6 +162,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -190,6 +204,12 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -217,6 +237,18 @@ "bcrypt": "bin/bcrypt" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bn.js": { "version": "4.12.2", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", @@ -243,6 +275,28 @@ "node": ">=18" } }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", @@ -318,6 +372,30 @@ "node": "*" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -371,6 +449,12 @@ "node": ">= 0.8" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -726,6 +810,18 @@ "node": ">=10.13.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", @@ -832,6 +928,20 @@ "node": ">=14.14" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -887,6 +997,18 @@ "node": ">= 0.4" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -905,6 +1027,15 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1002,6 +1133,12 @@ "node": ">=0.10.0" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "license": "ISC" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1026,6 +1163,27 @@ "node": ">= 0.10" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1035,6 +1193,27 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -1240,6 +1419,18 @@ "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", "license": "MIT" }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -1460,6 +1651,43 @@ "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", "license": "MIT" }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1530,6 +1758,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pkcs11js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkcs11js/-/pkcs11js-1.3.1.tgz", @@ -1597,6 +1837,12 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "license": "MIT" + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -1666,6 +1912,18 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -1850,6 +2108,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sjcl": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.8.tgz", @@ -1920,6 +2190,30 @@ "node": ">=8" } }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1929,6 +2223,15 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1949,6 +2252,12 @@ "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", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "7.13.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", diff --git a/backend/package.json b/backend/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "main": "server.js", "scripts": { - "start": "node server.js", - "create-users": "node createUsers.js" + "start": "npx nodemon server.js", + "init": "node initUsers.js" }, "dependencies": { "bcryptjs": "^3.0.2", @@ -16,6 +16,7 @@ "fs-extra": "^11.3.2", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.1", - "multer": "^2.0.2" + "multer": "^2.0.2", + "nodemon": "^3.1.10" } } diff --git a/backend/server.js b/backend/server.js @@ -39,11 +39,9 @@ const AS_LOCALHOST = process.env.AS_LOCALHOST === "true"; const JWT_SECRET = process.env.JWT_SECRET; async function getContract() { - if (!CCP_PATH || !WALLET_PATH || !CHANNEL || !CHAINCODE || !IDENTITY) { - throw new Error( - "Missing Fabric environment variables (CCP_PATH/WALLET_PATH/CHANNEL/CHAINCODE/IDENTITY)" - ); - } + if (!CCP_PATH || !WALLET_PATH || !CHANNEL || !CHAINCODE || !IDENTITY) + throw new Error("Missing Fabric environment variables"); + const ccp = JSON.parse(fs.readFileSync(path.resolve(CCP_PATH), "utf8")); const wallet = await Wallets.newFileSystemWallet(path.resolve(WALLET_PATH)); const gateway = new Gateway(); @@ -92,7 +90,7 @@ app.post("/api/auth/login", bodyParser.json(), async (req, res) => { if (!fs.existsSync(usersPath)) return res .status(500) - .json({ error: "users.json missing -> run 'npm run create-users'" }); + .json({ error: "users.json missing -> run 'npm run init'" }); const users = JSON.parse(fs.readFileSync(usersPath, "utf8")); const user = users.find((u) => u.username === username); if (!user) return res.status(401).json({ error: "invalid credentials" }); @@ -115,9 +113,8 @@ app.post( authenticateMiddleware, upload.single("image"), (req, res) => { - if (!req.file) { + if (!req.file) return res.status(400).json({ error: "no image file uploaded" }); - } try { const fileUrl = `${req.protocol}://${req.get("host")}/uploads/${ req.file.filename diff --git a/chaincode/index.js b/chaincode/index.js @@ -3,7 +3,7 @@ const { Contract } = require("fabric-contract-api"); class ProduceContract extends Contract { - // Helper: throw if missing + // Utilities async _getState(ctx, id) { const data = await ctx.stub.getState(id); if (!data || data.length === 0) { @@ -16,7 +16,7 @@ class ProduceContract extends Contract { await ctx.stub.putState(id, Buffer.from(JSON.stringify(obj))); } - // Helper: deterministic timestamp from tx context + // Deterministic timestamp from tx context _txTimestampISO(ctx) { const ts = ctx.stub.getTxTimestamp(); // ts.seconds may be a Long object in some environments @@ -29,7 +29,7 @@ class ProduceContract extends Contract { return new Date(millis).toISOString(); } - // Utility: create deterministic action item (uses tx timestamp) + // Deterministic action item _actionItem(ctx, action, location, owner, meta) { return { timestamp: this._txTimestampISO(ctx), @@ -40,13 +40,20 @@ class ProduceContract extends Contract { }; } - // Initialize ledger (optional) + // Initialize ledger async initLedger(ctx) { console.info("Ledger initialized"); } + // Functions // 1. registerProduce(farmerId, details) async registerProduce(ctx, farmerId, detailsStr) { + const farmerKey = `FARMER-${farmerId}`; + const farmerState = await ctx.stub.getState(farmerKey); + if (!farmerState || farmerState.length === 0) { + throw new Error(`Farmer ${farmerId} is not registered`); + } + const details = JSON.parse(detailsStr || "{}"); const txId = ctx.stub.getTxID(); const now = this._txTimestampISO(ctx); @@ -83,18 +90,10 @@ class ProduceContract extends Contract { await this._putState(ctx, id, produce); - // Update farmer profile (simple) - const farmerKey = `FARMER-${farmerId}`; - let farmer = {}; - const farmerState = await ctx.stub.getState(farmerKey); - if (farmerState && farmerState.length) - farmer = JSON.parse(farmerState.toString()); - farmer.role = "Farmer"; - farmer.id = farmerId; - farmer.name = farmer.name || details.farmerName || ""; + const farmer = JSON.parse(farmerState.toString()); farmer.registeredProduce = farmer.registeredProduce || []; farmer.registeredProduce.push(id); - await ctx.stub.putState(farmerKey, Buffer.from(JSON.stringify(farmer))); + await this._putState(ctx, farmerKey, farmer); return produce; } @@ -258,7 +257,7 @@ class ProduceContract extends Contract { let resultAssetId = produceId; if (qty < produce.qty) { - // partial -> split then assign child to new owner + // Partial Transfer const splitRes = await this.splitProduce( ctx, produceId, @@ -285,7 +284,7 @@ class ProduceContract extends Contract { await this._putState(ctx, child.id, child); resultAssetId = child.id; } else { - // full transfer + // Full Transfer produce.currentOwner = newOwnerId; produce.actionHistory.push( this._actionItem(ctx, "SALE", produce.currentLocation, newOwnerId, { @@ -386,21 +385,27 @@ class ProduceContract extends Contract { return results; } - // Governance functions + // Governance Functions async registerUser(ctx, role, detailsStr) { const details = JSON.parse(detailsStr || "{}"); const id = details.id || `USER-${ctx.stub.getTxID()}`; const key = `${role.toUpperCase()}-${id}`; + const user = { role, id, name: details.name || "", location: details.location || "", walletId: details.walletId || "", - registeredProduce: details.registeredProduce || [], - ownedProduce: details.ownedProduce || [], - inventory: details.inventory || [], + registeredProduce: [], + ownedProduce: [], + inventory: [], }; + + if (role.toUpperCase() === "FARMER") { + user.certification = details.certification || []; + } + await this._putState(ctx, key, user); return user; } @@ -412,7 +417,7 @@ class ProduceContract extends Contract { return JSON.parse(data.toString()); } - async updateUser(ctx, userKey, role, detailsStr) { + async updateUser(ctx, userKey, detailsStr) { const data = await ctx.stub.getState(userKey); if (!data || data.length === 0) throw new Error(`User ${userKey} not found`); diff --git a/frontend/components/Scanner.js b/frontend/components/Scanner.js @@ -67,7 +67,7 @@ export default function Scanner({ value, onChange }) { ) : null ) : ( <TextInput - style={styles.input} + style={[styles.input, { marginTop: 10 }]} placeholder="Enter Produce ID" value={value} onChangeText={onChange} diff --git a/frontend/package-lock.json b/frontend/package-lock.json @@ -36,6 +36,7 @@ "react-dom": "19.1.0", "react-native": "0.81.4", "react-native-gesture-handler": "~2.28.0", + "react-native-picker-select": "^9.3.1", "react-native-qrcode-svg": "^6.3.15", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", @@ -3030,6 +3031,20 @@ "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, + "node_modules/@react-native-picker/picker": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.2.tgz", + "integrity": "sha512-2zyFdW4jgHjF+NeuDZ4nl3hJ+8suey69bI3yljqhNyowfklW2NwNrdDUaJ2iwtPCpk2pt7834aPF8TI6iyZRhA==", + "license": "MIT", + "peer": true, + "workspaces": [ + "example" + ], + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.81.4", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.4.tgz", @@ -7084,6 +7099,19 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isobject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", + "integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==", + "license": "MIT" + }, "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", @@ -8683,6 +8711,19 @@ "react-native": "*" } }, + "node_modules/react-native-picker-select": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/react-native-picker-select/-/react-native-picker-select-9.3.1.tgz", + "integrity": "sha512-o621HcsKJfJkpYeP/PZQiZTKbf8W7FT08niLFL0v1pGkIQyak5IfzfinV2t+/l1vktGwAH2Tt29LrP/Hc5fk3A==", + "license": "MIT", + "dependencies": { + "lodash.isequal": "^4.5.0", + "lodash.isobject": "^3.0.2" + }, + "peerDependencies": { + "@react-native-picker/picker": "^2.4.0" + } + }, "node_modules/react-native-qrcode-svg": { "version": "6.3.15", "resolved": "https://registry.npmjs.org/react-native-qrcode-svg/-/react-native-qrcode-svg-6.3.15.tgz", diff --git a/frontend/package.json b/frontend/package.json @@ -34,6 +34,7 @@ "react-dom": "19.1.0", "react-native": "0.81.4", "react-native-gesture-handler": "~2.28.0", + "react-native-picker-select": "^9.3.1", "react-native-qrcode-svg": "^6.3.15", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", diff --git a/frontend/screens/Search.js b/frontend/screens/Search.js @@ -1,5 +1,6 @@ import { useState } from "react"; import { + ActivityIndicator, Alert, Image, ScrollView, @@ -10,43 +11,71 @@ import { View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +import RNPickerSelect from "react-native-picker-select"; import { MaterialCommunityIcons } from "@expo/vector-icons"; import ScreenHeader from "../components/ScreenHeader"; import Scanner from "../components/Scanner"; import { API_BASE } from "../config"; import styles, { colors } from "../styles"; +const DetailRow = ({ icon, label, value }) => ( + <View style={local.detailItem}> + <MaterialCommunityIcons + name={icon} + size={20} + color={colors.darkGreen} + style={{ marginRight: 12 }} + /> + <View style={{ flex: 1 }}> + <Text style={local.detailLabel}>{label}</Text> + <Text style={local.detailValue}>{value}</Text> + </View> + </View> +); + export default function SearchScreen({ navigation }) { const [tab, setTab] = useState("Produce"); - const [input, setInput] = useState(""); - const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); + const [result, setResult] = useState(null); + + const [produceId, setProduceId] = useState(""); + const [userRole, setUserRole] = useState("FARMER"); + const [userId, setUserId] = useState(""); const fetchResult = async () => { - if (!input.trim()) { - return Alert.alert("Enter a valid ID"); + const isProduceSearch = tab === "Produce"; + const searchInput = isProduceSearch ? produceId : userId; + if (!searchInput.trim()) { + return Alert.alert("Invalid Input", "Please enter a valid ID to search."); } + try { setLoading(true); setResult(null); let url = ""; - if (tab === "Produce") url = `${API_BASE}/getProduce/${input.trim()}`; - if (tab === "User") url = `${API_BASE}/getUser/${input.trim()}`; + if (isProduceSearch) { + url = `${API_BASE}/getProduce/${produceId.trim()}`; + } else { + const userKey = `${userRole}-${userId.trim()}`; + url = `${API_BASE}/getUser/${userKey}`; + } const res = await fetch(url); const data = await res.json(); - if (!res.ok) throw new Error(data.error || `Server ${res.status}`); + if (!res.ok) + throw new Error(data.error || `Server returned status ${res.status}`); setResult(data); } catch (err) { Alert.alert("Error", err.message); + setResult(null); } finally { setLoading(false); } }; const renderTimeline = (produce) => ( - <View style={{ marginTop: 14 }}> + <View> <Text style={local.subtitle}>Journey Timeline</Text> {produce.actionHistory?.map((a, idx) => ( <View key={idx} style={local.timelineItem}> @@ -55,28 +84,35 @@ export default function SearchScreen({ navigation }) { a.action === "REGISTER" ? "sprout" : a.action === "MOVE" - ? "truck" + ? "truck-fast" : a.action === "SALE" - ? "cash" + ? "swap-horizontal-bold" : a.action === "INSPECT" ? "check-decagram" : "close-circle" } - size={22} + size={24} color={ a.action === "REMOVED" - ? "red" + ? colors.danger : a.action === "SALE" - ? "orange" + ? colors.midGreen : colors.darkGreen } /> - <View style={{ marginLeft: 8 }}> - <Text style={{ fontWeight: "600" }}> - {a.action} at {a.currentLocation || "N/A"} + <View style={{ marginLeft: 12, flex: 1 }}> + <Text style={{ fontWeight: "600", textTransform: "capitalize" }}> + {a.action.toLowerCase()} + </Text> + <Text style={local.small}> + Location: {a.currentLocation || "N/A"} + </Text> + <Text style={local.small}> + {a.action === "INSPECT" ? "Inspected by" : "Actor"}:{" "} + {a.currentOwner} </Text> <Text style={local.small}> - Owner: {a.currentOwner} | Time: {a.timestamp} + {new Date(a.timestamp).toLocaleString()} </Text> </View> </View> @@ -86,35 +122,118 @@ export default function SearchScreen({ navigation }) { const renderProduce = (produce) => ( <View style={local.card}> - {produce.imageUrl && ( - <Image - source={{ uri: produce.imageUrl }} - style={{ - width: "100%", - height: 200, - borderRadius: 10, - marginBottom: 12, - }} - resizeMode="cover" + <View style={local.header}> + <Text style={local.refNoText}>Ref. No.</Text> + <Text style={local.refNoId}>{produce.id}</Text> + </View> + <View style={local.splitRow}> + <View style={{ flex: 1, marginRight: 8 }}> + <Text style={local.detailLabel}>Name</Text> + <Text style={local.title}>{produce.cropType}</Text> + </View> + <View style={{ flex: 1, marginLeft: 8 }}> + <Text style={local.detailLabel}>Type / Quality</Text> + <Text style={local.title}>{produce.quality || "N/A"}</Text> + </View> + </View> + <View style={local.mainContentRow}> + {produce.imageUrl ? ( + <Image + source={{ uri: produce.imageUrl }} + style={local.produceImage} + resizeMode="cover" + /> + ) : ( + <View style={[local.produceImage, local.imagePlaceholder]}> + <MaterialCommunityIcons + name="image-off" + size={40} + color={colors.midGreen} + /> + </View> + )} + <View style={local.detailsContainer}> + <DetailRow + icon="weight-kilogram" + label="Quantity" + value={`${produce.qty} ${produce.qtyUnit}`} + /> + <DetailRow + icon="cash" + label="Price" + value={`$${produce.pricePerUnit} / ${produce.qtyUnit}`} + /> + <DetailRow + icon="account-circle-outline" + label="Current Owner" + value={produce.currentOwner} + /> + </View> + </View> + <View style={local.splitRow}> + <DetailRow + icon="calendar-arrow-left" + label="Date of Harvest" + value={new Date(produce.harvestDate).toLocaleDateString()} /> - )} - <Text style={local.title}>{produce.cropType}</Text> - <Text> - Quantity: {produce.qty} {produce.qtyUnit} - </Text> - <Text>Price Per Unit: {produce.pricePerUnit}</Text> - <Text>Quality: {produce.quality}</Text> - <Text>Status: {produce.status}</Text> - <Text>Owner: {produce.currentOwner}</Text> - <Text>Location: {produce.currentLocation}</Text> - <Text>Storage: {produce.storageConditions}</Text> - <Text>Harvest Date: {produce.harvestDate}</Text> - <Text>Expiry Date: {produce.expiryDate}</Text> - <Text>Available: {produce.isAvailable ? "Yes" : "No"}</Text> + <DetailRow + icon="calendar-arrow-right" + label="Date of Expiry" + value={new Date(produce.expiryDate).toLocaleDateString()} + /> + </View> + <View style={local.badgesRow}> + <View style={local.certContainer}> + <MaterialCommunityIcons + name="shield-check" + size={20} + color={colors.darkGreen} + /> + <Text style={local.certText}>Organic Certified</Text> + </View> + <View style={local.statusContainer}> + <Text style={local.statusText}>{produce.status}</Text> + </View> + </View> {renderTimeline(produce)} </View> ); + const renderUser = (user) => ( + <View style={local.card}> + <View style={local.userHeader}> + <View style={local.userAvatar}> + <MaterialCommunityIcons + name="account-circle" + size={40} + color={colors.darkGreen} + /> + </View> + <View> + <Text style={local.title}>{user.name || "Unnamed User"}</Text> + <View style={local.roleBadge}> + <Text style={local.roleText}>{user.role}</Text> + </View> + </View> + </View> + <Text style={local.subtitle}>User Details</Text> + <DetailRow icon="identifier" label="User ID" value={user.id} /> + {user.location && ( + <DetailRow icon="map-marker" label="Location" value={user.location} /> + )} + {user.walletId && ( + <DetailRow icon="wallet" label="Wallet ID" value={user.walletId} /> + )} + {user.certification && ( + <DetailRow + icon="certificate" + label="Certifications" + value={user.certification.join(", ")} + /> + )} + </View> + ); + return ( <SafeAreaView style={styles.container}> <ScreenHeader @@ -124,86 +243,120 @@ export default function SearchScreen({ navigation }) { showBack={true} /> - <Text style={{ marginBottom: 12, color: colors.darkGreen }}> - Search by Produce ID or User Key - </Text> - - <View style={{ flexDirection: "row", justifyContent: "space-around" }}> - {["Produce", "User"].map((t) => ( - <TouchableOpacity - key={t} - style={{ - flexDirection: "row", - alignItems: "center", - paddingVertical: 10, - paddingHorizontal: 14, - borderRadius: 12, - backgroundColor: tab === t ? colors.darkGreen : colors.lightGreen, - }} - onPress={() => { - setTab(t); - setResult(null); - setInput(""); - }} - > - <MaterialCommunityIcons - name={t === "Produce" ? "leaf" : "account-badge"} - size={20} - color={tab === t ? "white" : colors.darkGreen} + <View style={local.searchContainer}> + <Text + style={{ + marginBottom: 12, + color: colors.darkGreen, + textAlign: "center", + }} + > + Search by Produce ID or User Key + </Text> + <View style={{ flexDirection: "row", justifyContent: "space-around" }}> + {["Produce", "User"].map((t) => ( + <TouchableOpacity + key={t} + style={[ + local.tabButton, + { + backgroundColor: + tab === t ? colors.darkGreen : colors.lightGreen, + }, + ]} + onPress={() => { + setTab(t); + setResult(null); + setProduceId(""); + setUserId(""); + setUserRole("FARMER"); + }} + > + <MaterialCommunityIcons + name={t === "Produce" ? "leaf" : "account-badge"} + size={20} + color={tab === t ? "white" : colors.darkGreen} + /> + <Text + style={[ + local.tabText, + { color: tab === t ? "white" : colors.darkGreen }, + ]} + > + {t} + </Text> + </TouchableOpacity> + ))} + </View> + + {tab === "Produce" ? ( + <Scanner value={produceId} onChange={setProduceId} /> + ) : ( + <View> + <Text style={[local.detailLabel, { marginTop: 20 }]}> + 1. Select Role + </Text> + <RNPickerSelect + onValueChange={(value) => setUserRole(value)} + items={[ + { label: "Farmer", value: "FARMER" }, + { label: "Distributor", value: "DISTRIBUTOR" }, + { label: "Retailer", value: "RETAILER" }, + { label: "Inspector", value: "INSPECTOR" }, + ]} + style={pickerSelectStyles} + value={userRole} + useNativeAndroidPickerStyle={false} + placeholder={{}} /> <Text - style={{ - color: tab === t ? "white" : colors.darkGreen, - marginLeft: 6, - fontWeight: "600", - }} + style={[local.detailLabel, { marginTop: 20, marginBottom: 6 }]} > - {t} + 2. Enter User ID </Text> - </TouchableOpacity> - ))} - </View> + <TextInput + style={styles.input} + placeholder="e.g. farmer1, distributor_xyz" + value={userId} + onChangeText={setUserId} + /> + </View> + )} - {tab === "Produce" ? ( - <Scanner value={input} onChange={setInput} /> - ) : ( - <TextInput - style={[styles.input, { marginTop: 20 }]} - placeholder={`Enter ${tab} ID`} - value={input} - onChangeText={setInput} - /> - )} + <TouchableOpacity + style={[styles.primaryButton, { marginTop: 20 }]} + onPress={fetchResult} + disabled={loading} + > + {loading ? ( + <ActivityIndicator color="white" /> + ) : ( + <Text style={styles.buttonText}>Search</Text> + )} + </TouchableOpacity> + </View> - <TouchableOpacity style={styles.primaryButton} onPress={fetchResult}> - <Text style={styles.buttonText}>Search</Text> - </TouchableOpacity> + <ScrollView contentContainerStyle={{ paddingHorizontal: 4 }}> + {loading && ( + <ActivityIndicator + size="large" + color={colors.darkGreen} + style={{ marginTop: 30 }} + /> + )} - <ScrollView style={{ marginTop: 20, paddingHorizontal: 12 }}> - {loading && <Text>Loading...</Text>} {!loading && result && (tab === "Produce" && result.produce ? ( renderProduce(result.produce) ) : tab === "User" && result.user ? ( + renderUser(result.user) + ) : ( <View style={local.card}> - <Text style={local.title}>{result.user.role} Profile</Text> - <Text>ID: {result.user.id}</Text> - <Text>Name: {result.user.name}</Text> - {result.user.location && ( - <Text>Location: {result.user.location}</Text> - )} - {result.user.walletId && ( - <Text>Wallet: {result.user.walletId}</Text> - )} - {result.user.certification && ( - <Text> - Certifications: {result.user.certification.join(", ")} - </Text> - )} + <Text style={{ textAlign: "center", fontWeight: "500" }}> + No results found for the provided ID. + </Text> </View> - ) : ( - <Text>No results found.</Text> ))} </ScrollView> </SafeAreaView> @@ -211,25 +364,202 @@ export default function SearchScreen({ navigation }) { } const local = StyleSheet.create({ + searchContainer: { + padding: 16, + backgroundColor: "white", + borderRadius: 16, + marginBottom: 10, + elevation: 4, + shadowColor: "#000", + shadowOpacity: 0.1, + shadowOffset: { width: 0, height: 2 }, + shadowRadius: 8, + }, + tabButton: { + flex: 1, + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + paddingVertical: 10, + borderRadius: 12, + marginHorizontal: 5, + }, + tabText: { + marginLeft: 6, + fontWeight: "600", + }, card: { - borderWidth: 1, - borderColor: "#ddd", - borderRadius: 10, - padding: 14, + backgroundColor: "white", + borderRadius: 16, + padding: 16, + marginVertical: 10, + shadowColor: "#000", + shadowOpacity: 0.1, + shadowOffset: { width: 0, height: 4 }, + shadowRadius: 10, + elevation: 5, + }, + header: { + borderBottomWidth: 1, + borderBottomColor: colors.lightGreen, + paddingBottom: 8, + marginBottom: 12, + }, + refNoText: { + color: "#999", + fontSize: 12, + }, + refNoId: { + color: colors.darkGreen, + fontSize: 14, + fontWeight: "500", + }, + splitRow: { + flexDirection: "row", + justifyContent: "space-between", marginBottom: 16, - backgroundColor: "#fafafa", + }, + mainContentRow: { + flexDirection: "row", + marginBottom: 16, + alignItems: "center", + }, + produceImage: { + width: 120, + height: 120, + borderRadius: 12, + }, + imagePlaceholder: { + backgroundColor: colors.cream, + alignItems: "center", + justifyContent: "center", + borderWidth: 1, + borderColor: colors.lightGreen, + }, + detailsContainer: { + flex: 1, + marginLeft: 16, + height: "100%", + justifyContent: "space-around", + }, + detailItem: { + flexDirection: "row", + alignItems: "center", + marginVertical: 8, + }, + detailLabel: { + color: "#666", + fontSize: 12, + marginBottom: 2, + }, + detailValue: { + color: "#000", + fontSize: 15, + fontWeight: "600", }, title: { fontWeight: "700", - marginBottom: 6, fontSize: 18, color: colors.darkGreen, }, - subtitle: { fontWeight: "600", marginTop: 6, marginBottom: 4, fontSize: 16 }, + badgesRow: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + marginVertical: 8, + paddingVertical: 12, + borderTopWidth: 1, + borderTopColor: colors.lightGreen, + }, + certContainer: { + flexDirection: "row", + alignItems: "center", + backgroundColor: colors.lightGreen, + paddingVertical: 8, + paddingHorizontal: 12, + borderRadius: 20, + }, + certText: { + color: colors.darkGreen, + fontWeight: "600", + marginLeft: 6, + }, + statusContainer: { + backgroundColor: colors.accent, + paddingVertical: 8, + paddingHorizontal: 16, + borderRadius: 8, + }, + statusText: { + color: colors.darkGreen, + fontWeight: "bold", + fontSize: 14, + }, + subtitle: { + fontWeight: "600", + marginTop: 12, + marginBottom: 8, + fontSize: 16, + color: colors.darkGreen, + }, timelineItem: { flexDirection: "row", alignItems: "center", - marginVertical: 6, + marginVertical: 8, + paddingLeft: 4, }, small: { fontSize: 12, color: "#555" }, + userHeader: { + flexDirection: "row", + alignItems: "center", + paddingBottom: 10, + marginBottom: 10, + borderBottomWidth: 1, + borderBottomColor: colors.lightGreen, + }, + userAvatar: { + width: 60, + height: 60, + borderRadius: 30, + backgroundColor: colors.lightGreen, + justifyContent: "center", + alignItems: "center", + marginRight: 16, + }, + roleBadge: { + backgroundColor: colors.midGreen, + borderRadius: 6, + paddingHorizontal: 8, + paddingVertical: 4, + alignSelf: "flex-start", + marginTop: 4, + }, + roleText: { + color: "white", + fontSize: 12, + fontWeight: "bold", + }, +}); + +const pickerSelectStyles = StyleSheet.create({ + inputIOS: { + ...styles.input, + height: 48, + justifyContent: "center", + paddingVertical: 12, + paddingHorizontal: 10, + fontSize: 16, + }, + inputAndroid: { + ...styles.input, + height: 48, + justifyContent: "center", + paddingVertical: 12, + paddingHorizontal: 10, + fontSize: 16, + }, + iconContainer: { + top: 18, + right: 15, + }, });