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:
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,
+ },
});