commit cfdfdeb8cbd4fe64ddbe1b7fdd113540a0136ff3
parent 2eb1c86e7c7efe40424f7e3f2518722df0767ca1
Author: maydayv7 <maydayv7@gmail.com>
Date: Tue, 7 Oct 2025 22:43:54 +0530
fix: Performance Improvements + Style Changes
Diffstat:
12 files changed, 475 insertions(+), 563 deletions(-)
diff --git a/backend/server.js b/backend/server.js
@@ -18,6 +18,7 @@ app.use(morgan("dev"));
app.use("/uploads", express.static("uploads"));
const UPLOAD_DIR = path.join(__dirname, "uploads");
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR);
+
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, UPLOAD_DIR);
@@ -32,6 +33,7 @@ const upload = multer({ storage });
// Fabric Network
const { CHANNEL, CHAINCODE, AS_LOCALHOST, JWT_SECRET, WALLET_PATH } =
process.env;
+
const orgConfig = {
Org1: {
ccpPath: process.env.CCP_PATH_ORG1,
@@ -51,31 +53,44 @@ const orgConfig = {
},
};
-async function getContract(org) {
- const config = orgConfig[org];
- if (!config) throw new Error(`Configuration for ${org} not found.`);
- if (
- !config.ccpPath ||
- !config.identity ||
- !WALLET_PATH ||
- !CHANNEL ||
- !CHAINCODE
- )
- throw new Error(`Missing Fabric environment variables for ${org}`);
-
- const ccp = JSON.parse(fs.readFileSync(path.resolve(config.ccpPath), "utf8"));
+const gateways = {};
+async function initializeGateways() {
+ console.log("Initializing Fabric Gateways...");
const wallet = await Wallets.newFileSystemWallet(path.resolve(WALLET_PATH));
- const gateway = new Gateway();
- await gateway.connect(ccp, {
- wallet,
- identity: config.identity,
- discovery: { enabled: true, asLocalhost: AS_LOCALHOST === "true" },
- });
+ for (const org of Object.keys(orgConfig)) {
+ const config = orgConfig[org];
+ if (!config || !config.ccpPath || !config.identity) {
+ console.warn(`Skipping gateway for ${org}: configuration missing.`);
+ continue;
+ }
+
+ const ccp = JSON.parse(
+ fs.readFileSync(path.resolve(config.ccpPath), "utf8")
+ );
+ const gateway = new Gateway();
+
+ try {
+ await gateway.connect(ccp, {
+ wallet,
+ identity: config.identity,
+ discovery: { enabled: true, asLocalhost: AS_LOCALHOST === "true" },
+ });
+ gateways[org] = gateway;
+ console.log(`Gateway for ${org} initialized successfully.`);
+ } catch (error) {
+ console.error(`Failed to initialize gateway for ${org}:`, error);
+ }
+ }
+}
+
+async function getContract(org) {
+ const gateway = gateways[org];
+ if (!gateway || !gateway.getNetwork)
+ throw new Error(`Gateway for ${org} is not available or not connected.`);
const network = await gateway.getNetwork(CHANNEL);
- const contract = network.getContract(CHAINCODE);
- return { contract, gateway };
+ return network.getContract(CHAINCODE);
}
// Middleware
@@ -94,8 +109,10 @@ function authenticateMiddleware(req, res, next) {
const header = req.headers["authorization"];
if (!header)
return res.status(401).json({ error: "missing authorization header" });
+
const token = header.split(" ")[1];
if (!token) return res.status(401).json({ error: "missing token" });
+
jwt.verify(token, JWT_SECRET, (err, payload) => {
if (err) return res.status(403).json({ error: "invalid token" });
req.user = payload;
@@ -108,34 +125,39 @@ function authenticateMiddleware(req, res, next) {
});
}
+const asyncHandler = (fn) => (req, res, next) =>
+ Promise.resolve(fn(req, res, next)).catch(next);
+
// User Authentication
-// Prototype: Uses backend/users.json
-app.post("/api/auth/login", express.json(), async (req, res) => {
- try {
+app.post(
+ "/api/auth/login",
+ express.json(),
+ asyncHandler(async (req, res) => {
const { username, password } = req.body || {};
if (!username || !password)
return res.status(400).json({ error: "username and password required" });
+
const usersPath = path.join(__dirname, "users.json");
if (!fs.existsSync(usersPath))
return res
.status(500)
.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" });
+
const ok = await bcrypt.compare(password, user.passwordHash);
if (!ok) return res.status(401).json({ error: "invalid credentials" });
+
const token = jwt.sign(
{ id: user.id, role: user.role, username: user.username, org: user.org },
JWT_SECRET,
{ expiresIn: process.env.TOKEN_EXPIRES_IN || "1h" }
);
res.json({ token, ...user });
- } catch (err) {
- console.error("Auth error:", err);
- res.status(500).json({ error: err.message });
- }
-});
+ })
+);
app.post(
"/api/uploadImage",
@@ -144,95 +166,80 @@ app.post(
(req, res) => {
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
- }`;
- res.json({ url: fileUrl });
- } catch (err) {
- console.error("Image Upload error:", err);
- res.status(500).json({ error: "failed to process image upload" });
- }
+
+ const fileUrl = `${req.protocol}://${req.get("host")}/uploads/${
+ req.file.filename
+ }`;
+ res.json({ url: fileUrl });
}
);
// Functions
const router = express.Router();
-router.post("/registerUser", async (req, res) => {
- try {
+router.post(
+ "/registerUser",
+ asyncHandler(async (req, res) => {
const { role, details } = req.body;
- const { contract, gateway } = await getContract(req.user.org);
+ const contract = await getContract(req.user.org);
const result = await contract.submitTransaction(
"registerUser",
role,
JSON.stringify(details)
);
- await gateway.disconnect();
return res.json({ success: true, user: JSON.parse(result.toString()) });
- } catch (err) {
- console.error("registerUser error:", err);
- return res.status(500).json({ error: err.message });
- }
-});
+ })
+);
-router.post("/registerProduce", async (req, res) => {
- try {
+router.post(
+ "/registerProduce",
+ asyncHandler(async (req, res) => {
const { farmerId, details } = req.body;
- const { contract, gateway } = await getContract(req.user.org);
+ const contract = await getContract(req.user.org);
const result = await contract.submitTransaction(
"registerProduce",
farmerId,
JSON.stringify(details)
);
- await gateway.disconnect();
return res.json({ success: true, produce: JSON.parse(result.toString()) });
- } catch (err) {
- console.error("registerProduce error:", err);
- return res.status(500).json({ error: err.message });
- }
-});
+ })
+);
-router.post("/updateLocation", async (req, res) => {
- try {
+router.post(
+ "/updateLocation",
+ asyncHandler(async (req, res) => {
const { produceId, actorId, newLocation } = req.body;
- const { contract, gateway } = await getContract(req.user.org);
+ const contract = await getContract(req.user.org);
const result = await contract.submitTransaction(
"updateLocation",
produceId,
actorId,
newLocation
);
- await gateway.disconnect();
return res.json({ success: true, produce: JSON.parse(result.toString()) });
- } catch (err) {
- console.error("updateLocation error:", err);
- return res.status(500).json({ error: err.message });
- }
-});
+ })
+);
-router.post("/inspectProduce", async (req, res) => {
- try {
+router.post(
+ "/inspectProduce",
+ asyncHandler(async (req, res) => {
const { produceId, inspectorId, qualityUpdate } = req.body;
- const { contract, gateway } = await getContract(req.user.org);
+ const contract = await getContract(req.user.org);
const result = await contract.submitTransaction(
"inspectProduce",
produceId,
inspectorId,
JSON.stringify(qualityUpdate)
);
- await gateway.disconnect();
return res.json({ success: true, produce: JSON.parse(result.toString()) });
- } catch (err) {
- console.error("inspectProduce error:", err);
- return res.status(500).json({ error: err.message });
- }
-});
+ })
+);
-router.post("/transferOwnership", async (req, res) => {
- try {
+router.post(
+ "/transferOwnership",
+ asyncHandler(async (req, res) => {
const { produceId, newOwnerId, qty, salePrice } = req.body;
- const { contract, gateway } = await getContract(req.user.org);
+ const contract = await getContract(req.user.org);
const result = await contract.submitTransaction(
"transferOwnership",
produceId,
@@ -240,36 +247,30 @@ router.post("/transferOwnership", async (req, res) => {
"" + qty,
"" + salePrice
);
- await gateway.disconnect();
return res.json({ success: true, result: JSON.parse(result.toString()) });
- } catch (err) {
- console.error("transferOwnership error:", err);
- return res.status(500).json({ error: err.message });
- }
-});
+ })
+);
-router.post("/updateDetails", async (req, res) => {
- try {
+router.post(
+ "/updateDetails",
+ asyncHandler(async (req, res) => {
const { produceId, actorId, details } = req.body;
- const { contract, gateway } = await getContract(req.user.org);
+ const contract = await getContract(req.user.org);
const result = await contract.submitTransaction(
"updateDetails",
produceId,
actorId,
JSON.stringify(details)
);
- await gateway.disconnect();
return res.json({ success: true, produce: JSON.parse(result.toString()) });
- } catch (err) {
- console.error("updateDetails error:", err);
- return res.status(500).json({ error: err.message });
- }
-});
+ })
+);
-router.post("/markAsUnavailable", async (req, res) => {
- try {
+router.post(
+ "/markAsUnavailable",
+ asyncHandler(async (req, res) => {
const { produceId, actorId, reason, newStatus } = req.body;
- const { contract, gateway } = await getContract(req.user.org);
+ const contract = await getContract(req.user.org);
const result = await contract.submitTransaction(
"markAsUnavailable",
produceId,
@@ -277,34 +278,28 @@ router.post("/markAsUnavailable", async (req, res) => {
reason || "",
newStatus || "Removed"
);
- await gateway.disconnect();
return res.json({ success: true, produce: JSON.parse(result.toString()) });
- } catch (err) {
- console.error("markAsUnavailable error:", err);
- return res.status(500).json({ error: err.message });
- }
-});
+ })
+);
-router.post("/splitProduce", async (req, res) => {
- try {
+router.post(
+ "/splitProduce",
+ asyncHandler(async (req, res) => {
const { produceId, qty, ownerId } = req.body;
- const { contract, gateway } = await getContract(req.user.org);
+ const contract = await getContract(req.user.org);
const result = await contract.submitTransaction(
"splitProduce",
produceId,
"" + qty,
ownerId
);
- await gateway.disconnect();
return res.json({ success: true, split: JSON.parse(result.toString()) });
- } catch (err) {
- console.error("splitProduce error:", err);
- return res.status(500).json({ error: err.message });
- }
-});
+ })
+);
-router.post("/recordPayment", async (req, res) => {
- try {
+router.post(
+ "/recordPayment",
+ asyncHandler(async (req, res) => {
const {
produceId,
transactionId,
@@ -312,7 +307,7 @@ router.post("/recordPayment", async (req, res) => {
paymentMethod,
paymentRef,
} = req.body;
- const { contract, gateway } = await getContract(req.user.org);
+ const contract = await getContract(req.user.org);
const result = await contract.submitTransaction(
"recordPayment",
produceId,
@@ -321,64 +316,70 @@ router.post("/recordPayment", async (req, res) => {
paymentMethod,
paymentRef || ""
);
- await gateway.disconnect();
return res.json({ success: true, produce: JSON.parse(result.toString()) });
- } catch (err) {
- console.error("recordPayment error:", err);
- return res.status(500).json({ error: err.message });
- }
-});
+ })
+);
// Queries
-
-router.get("/getProduce/:id", async (req, res) => {
- try {
- const { contract, gateway } = await getContract(req.user.org);
+router.get(
+ "/getProduce/:id",
+ asyncHandler(async (req, res) => {
+ const contract = await getContract(req.user.org);
const result = await contract.evaluateTransaction(
"getProduceById",
req.params.id
);
- await gateway.disconnect();
return res.json({ success: true, produce: JSON.parse(result.toString()) });
- } catch (err) {
- console.error("getProduceById error:", err);
- return res.status(500).json({ error: err.message });
- }
-});
+ })
+);
-router.get("/getProduceByOwner/:ownerId", async (req, res) => {
- try {
- const { contract, gateway } = await getContract(req.user.org);
+router.get(
+ "/getProduceByOwner/:ownerId",
+ asyncHandler(async (req, res) => {
+ const contract = await getContract(req.user.org);
const result = await contract.evaluateTransaction(
"getProduceByOwner",
req.params.ownerId
);
- await gateway.disconnect();
return res.json({ success: true, produces: JSON.parse(result.toString()) });
- } catch (err) {
- console.error("getProduceByOwner error:", err);
- return res.status(500).json({ error: err.message });
- }
-});
+ })
+);
-router.get("/getUser/:userKey", async (req, res) => {
- try {
- const { contract, gateway } = await getContract(req.user.org);
+router.get(
+ "/getUser/:userKey",
+ asyncHandler(async (req, res) => {
+ const contract = await getContract(req.user.org);
const result = await contract.evaluateTransaction(
"getUserDetails",
req.params.userKey
);
- await gateway.disconnect();
return res.json({ success: true, user: JSON.parse(result.toString()) });
- } catch (err) {
- console.error("getUserDetails error:", err);
- return res.status(500).json({ error: err.message });
- }
-});
+ })
+);
app.use("/api", authenticateMiddleware, express.json(), router);
+function errorHandler(err, req, res, next) {
+ console.error(err);
+
+ const userMessage =
+ err.message.includes("DiscoveryService") ||
+ err.message.includes("CommitError")
+ ? "Blockchain transaction failed. Please try again."
+ : err.message;
+
+ res.status(500).json({ error: userMessage });
+}
+app.use(errorHandler);
+
const PORT = process.env.PORT || 4000;
-app.listen(PORT, "0.0.0.0", () =>
- console.log(`Server listening on port ${PORT}`)
-);
+initializeGateways()
+ .then(() => {
+ app.listen(PORT, "0.0.0.0", () =>
+ console.log(`Server listening on port ${PORT}`)
+ );
+ })
+ .catch((e) => {
+ console.error("Server failed to start:", e);
+ process.exit(1);
+ });
diff --git a/fabric/chaincode/index.js b/fabric/chaincode/index.js
@@ -1,5 +1,4 @@
"use strict";
-
const { Contract } = require("fabric-contract-api");
class ProduceContract extends Contract {
@@ -77,6 +76,29 @@ class ProduceContract extends Contract {
}
}
+ _validateInput(data, requiredFields) {
+ for (const field of requiredFields) {
+ if (
+ data[field] === undefined ||
+ data[field] === null ||
+ data[field] === ""
+ ) {
+ throw new Error(`Validation Error: Field '${field}' is required`);
+ }
+ }
+ if (data.qty && (typeof data.qty !== "number" || data.qty <= 0))
+ throw new Error("Validation Error: 'qty' must be a positive number");
+
+ if (
+ data.pricePerUnit &&
+ (typeof data.pricePerUnit !== "number" || data.pricePerUnit < 0)
+ ) {
+ throw new Error(
+ "Validation Error: 'pricePerUnit' must be a non-negative number"
+ );
+ }
+ }
+
// Initialize ledger
async initLedger(ctx) {
console.info("Ledger initialized");
@@ -86,36 +108,43 @@ class ProduceContract extends Contract {
// Produce Registration
async registerProduce(ctx, farmerId, detailsStr) {
const clientMspId = ctx.clientIdentity.getMSPID();
- if (clientMspId !== "Org1MSP") {
+ if (clientMspId !== "Org1MSP")
throw new Error(
`Client from ${clientMspId} is not authorized to register produce. Only Org1MSP is allowed.`
);
- }
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 farmer = JSON.parse(farmerState.toString());
+ const farmer = JSON.parse(farmerState.toString());
const details = JSON.parse(detailsStr || "{}");
+
+ this._validateInput(details, [
+ "cropType",
+ "qty",
+ "qtyUnit",
+ "pricePerUnit",
+ "harvestDate",
+ ]);
+
const txId = ctx.stub.getTxID();
const now = this._txTimestampISO(ctx);
-
const id = `PRODUCE-${txId}`;
const produce = {
id,
parentId: null,
children: [],
- qty: details.qty || 0,
- qtyUnit: details.qtyUnit || "KG",
- pricePerUnit: details.pricePerUnit || 0,
- totalPrice: (details.pricePerUnit || 0) * (details.qty || 0),
+ qty: details.qty,
+ qtyUnit: details.qtyUnit,
+ pricePerUnit: details.pricePerUnit,
+ totalPrice: details.pricePerUnit * details.qty,
currentOwner: farmerId,
currentLocation: details.location || "",
actionHistory: [],
saleHistory: [],
- cropType: details.cropType || "",
+ cropType: details.cropType,
harvestDate: details.harvestDate || now,
quality: details.quality || null,
expiryDate: details.expiryDate || null,
@@ -132,16 +161,18 @@ class ProduceContract extends Contract {
note: details.note || "",
})
);
+
await this._putState(ctx, id, produce);
const farmerObj = farmer;
farmerObj.registeredProduce = farmerObj.registeredProduce || [];
if (!farmerObj.registeredProduce.includes(id))
farmerObj.registeredProduce.push(id);
+
farmerObj.ownedProduce = farmerObj.ownedProduce || [];
if (!farmerObj.ownedProduce.includes(id)) farmerObj.ownedProduce.push(id);
- await this._putState(ctx, farmerKey, farmerObj);
+ await this._putState(ctx, farmerKey, farmerObj);
return produce;
}
@@ -167,6 +198,7 @@ class ProduceContract extends Contract {
produce.isAvailable = false;
produce.notAvailableReason = reason || "";
produce.status = newStatus || "Removed";
+
produce.actionHistory.push(
this._actionItem(ctx, "REMOVED", produce.currentLocation, actorId, {
reason,
@@ -179,11 +211,10 @@ class ProduceContract extends Contract {
// Produce Inspection
async inspectProduce(ctx, produceId, inspectorId, qualityUpdateStr) {
const clientMspId = ctx.clientIdentity.getMSPID();
- if (clientMspId !== "Org4MSP") {
+ if (clientMspId !== "Org4MSP")
throw new Error(
`Client from ${clientMspId} is not authorized to inspect produce. Only Org4MSP is allowed.`
);
- }
const qualityUpdate = JSON.parse(qualityUpdateStr || "{}");
const produce = await this._getState(ctx, produceId);
@@ -228,7 +259,6 @@ class ProduceContract extends Contract {
await this._putState(ctx, inspectorKey, inspectorObj);
}
}
-
return produce;
}
@@ -249,7 +279,6 @@ class ProduceContract extends Contract {
produce.certification = details.certification;
produce.totalPrice = (produce.pricePerUnit || 0) * (produce.qty || 0);
-
produce.actionHistory.push(
this._actionItem(
ctx,
@@ -284,6 +313,7 @@ class ProduceContract extends Contract {
child.qty = qty;
child.totalPrice = (child.pricePerUnit || 0) * qty;
child.actionHistory = [];
+ child.saleHistory = [];
child.actionHistory.push(
this._actionItem(ctx, "SPLIT", produce.currentLocation, ownerId, { qty })
);
@@ -309,7 +339,6 @@ class ProduceContract extends Contract {
childId
);
}
-
return { parent: produce, child };
}
@@ -362,7 +391,6 @@ class ProduceContract extends Contract {
child.id
);
}
-
const newOwnerRes = await this._getUserById(ctx, newOwnerId);
if (newOwnerRes) {
await this._addOwnedProduceToUser(
@@ -402,7 +430,6 @@ class ProduceContract extends Contract {
produceId
);
}
-
const newOwnerRes = await this._getUserById(ctx, newOwnerId);
if (newOwnerRes) {
await this._addOwnedProduceToUser(
@@ -413,7 +440,6 @@ class ProduceContract extends Contract {
);
}
}
-
return { newAssetId: resultAssetId };
}
@@ -428,8 +454,8 @@ class ProduceContract extends Contract {
) {
const produce = await this._getState(ctx, produceId);
const now = this._txTimestampISO(ctx);
-
produce.saleHistory = produce.saleHistory || [];
+
if (produce.saleHistory.length === 0) {
produce.saleHistory.push({
timestamp: now,
@@ -464,7 +490,6 @@ class ProduceContract extends Contract {
}
)
);
-
await this._putState(ctx, produceId, produce);
return produce;
}
@@ -558,9 +583,11 @@ class ProduceContract extends Contract {
const data = await ctx.stub.getState(userKey);
if (!data || data.length === 0)
throw new Error(`User ${userKey} not found`);
+
const user = JSON.parse(data.toString());
const details = JSON.parse(detailsStr || "{}");
Object.assign(user, details);
+
await this._putState(ctx, userKey, user);
return user;
}
diff --git a/frontend/components/ImageUploader.js b/frontend/components/ImageUploader.js
@@ -1,15 +1,15 @@
import { useState, forwardRef, useImperativeHandle } from "react";
import {
+ ActivityIndicator,
+ Alert,
Button,
Image,
- View,
Platform,
- ActivityIndicator,
Text,
- Alert,
+ View,
} from "react-native";
import { launchImageLibraryAsync } from "expo-image-picker";
-import { API_BASE } from "../config";
+import { api } from "../services/api";
import { colors } from "../styles";
const ImageUploader = forwardRef(({ token }, ref) => {
@@ -20,7 +20,6 @@ const ImageUploader = forwardRef(({ token }, ref) => {
upload: async () => {
if (!imageAsset) return null;
setIsUploading(true);
-
try {
const imageUrl = await handleUpload();
return imageUrl;
@@ -31,7 +30,6 @@ const ImageUploader = forwardRef(({ token }, ref) => {
setIsUploading(false);
}
},
-
reset: () => {
setImageAsset(null);
setIsUploading(false);
@@ -44,13 +42,11 @@ const ImageUploader = forwardRef(({ token }, ref) => {
quality: 0.7,
mediaTypes: "Images",
});
-
if (!result.canceled) setImageAsset(result.assets[0]);
};
const handleUpload = async () => {
const data = new FormData();
-
if (Platform.OS === "web") {
const response = await fetch(imageAsset.uri);
const blob = await response.blob();
@@ -66,17 +62,7 @@ const ImageUploader = forwardRef(({ token }, ref) => {
`image-${Date.now()}.jpg`,
});
}
-
- const res = await fetch(`${API_BASE}/uploadImage`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${token}`,
- },
- body: data,
- });
-
- const responseData = await res.json();
- if (!res.ok) throw new Error(responseData.error || "Image upload failed");
+ const responseData = await api.uploadImage(data, token);
return responseData.url;
};
diff --git a/frontend/components/TransferOwnershipForm.js b/frontend/components/TransferOwnershipForm.js
@@ -0,0 +1,55 @@
+import { useState } from "react";
+import { View, TextInput, TouchableOpacity, Text } from "react-native";
+import Scanner from "./Scanner";
+import styles from "../styles";
+
+export default function TransferOwnershipForm({ onSubmit }) {
+ const [produceId, setProduceId] = useState("");
+ const [newOwnerId, setNewOwnerId] = useState("");
+ const [qty, setQty] = useState("");
+ const [salePrice, setSalePrice] = useState("");
+
+ const handleSubmit = () => {
+ onSubmit({
+ produceId,
+ newOwnerId,
+ qty,
+ salePrice,
+ onComplete: () => {
+ setProduceId("");
+ setNewOwnerId("");
+ setQty("");
+ setSalePrice("");
+ },
+ });
+ };
+
+ return (
+ <View>
+ <Scanner value={produceId} onChange={setProduceId} />
+ <TextInput
+ style={styles.input}
+ placeholder="New Owner ID"
+ value={newOwnerId}
+ onChangeText={setNewOwnerId}
+ />
+ <TextInput
+ style={styles.input}
+ placeholder="Quantity"
+ keyboardType="numeric"
+ value={qty}
+ onChangeText={setQty}
+ />
+ <TextInput
+ style={styles.input}
+ placeholder="Sale Price"
+ keyboardType="numeric"
+ value={salePrice}
+ onChangeText={setSalePrice}
+ />
+ <TouchableOpacity style={styles.primaryButton} onPress={handleSubmit}>
+ <Text style={styles.buttonText}>Transfer Ownership</Text>
+ </TouchableOpacity>
+ </View>
+ );
+}
diff --git a/frontend/screens/Distributor.js b/frontend/screens/Distributor.js
@@ -8,11 +8,14 @@ import {
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
+
import ScreenHeader from "../components/ScreenHeader";
import ActionButton from "../components/ActionButton";
import Scanner from "../components/Scanner";
import LocationPicker from "../components/LocationPicker";
-import { API_BASE } from "../config";
+import TransferOwnershipForm from "../components/TransferOwnershipForm";
+
+import { api } from "../services/api";
import styles from "../styles";
import { AuthContext } from "../AuthContext";
@@ -20,22 +23,12 @@ export default function DistributorScreen({ navigation, route }) {
const { user } = useContext(AuthContext);
const userId = route.params?.userId || user?.id;
const token = user?.token;
- const [active, setActive] = useState(null);
- // Common
+ const [active, setActive] = useState(null);
const [produceId, setProduceId] = useState("");
const [location, setLocation] = useState("");
-
- // transferOwnership
- const [newOwnerId, setNewOwnerId] = useState("");
- const [qty, setQty] = useState("");
- const [salePrice, setSalePrice] = useState("");
-
- // markAsUnavailable
const [reason, setReason] = useState("");
const [newStatus, setNewStatus] = useState("");
-
- // updateStorageConditions
const [storageConditions, setStorageConditions] = useState("");
const resetCommon = () => {
@@ -44,20 +37,14 @@ export default function DistributorScreen({ navigation, route }) {
const updateLocation = async () => {
try {
- const res = await fetch(`${API_BASE}/updateLocation`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({
+ await api.updateLocation(
+ {
produceId,
actorId: userId,
newLocation: location,
- }),
- });
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Error");
+ },
+ token
+ );
Alert.alert("Location Updated", "Location updated successfully");
resetCommon();
setLocation("");
@@ -66,29 +53,30 @@ export default function DistributorScreen({ navigation, route }) {
}
};
- const transferOwnership = async () => {
+ const transferOwnership = async ({
+ produceId,
+ newOwnerId,
+ qty,
+ salePrice,
+ onComplete,
+ }) => {
+ if (!produceId || !newOwnerId || !qty)
+ return Alert.alert(
+ "Error",
+ "Please provide Produce ID, New Owner ID, and Quantity."
+ );
try {
- const res = await fetch(`${API_BASE}/transferOwnership`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({
+ await api.transferOwnership(
+ {
produceId,
newOwnerId,
qty: parseFloat(qty),
salePrice: parseFloat(salePrice),
- }),
- });
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Error");
+ },
+ token
+ );
Alert.alert("Transferred", "Ownership transferred successfully");
- // reset fields
- resetCommon();
- setNewOwnerId("");
- setQty("");
- setSalePrice("");
+ onComplete();
} catch (err) {
Alert.alert("Error", err.message);
}
@@ -96,17 +84,11 @@ export default function DistributorScreen({ navigation, route }) {
const markAsUnavailable = async () => {
try {
- const res = await fetch(`${API_BASE}/markAsUnavailable`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({ produceId, actorId: userId, reason, newStatus }),
- });
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Error");
- Alert.alert("Marked Unavailable", "Produce marked unavailable");
+ await api.markAsUnavailable(
+ { produceId, actorId: userId, reason, newStatus },
+ token
+ );
+ Alert.alert("Marked Unavailable", "Produce marked as unavailable");
resetCommon();
setReason("");
setNewStatus("");
@@ -117,13 +99,8 @@ export default function DistributorScreen({ navigation, route }) {
const updateStorageConditions = async () => {
try {
- const res = await fetch(`${API_BASE}/updateDetails`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({
+ await api.updateDetails(
+ {
produceId,
actorId: userId,
details: {
@@ -131,11 +108,10 @@ export default function DistributorScreen({ navigation, route }) {
? storageConditions.split(",").map((c) => c.trim())
: [],
},
- }),
- });
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Error");
- Alert.alert("Updated Storage Conditions", "Storage conditions updated");
+ },
+ token
+ );
+ Alert.alert("Updated", "Storage conditions updated successfully");
resetCommon();
setStorageConditions("");
} catch (err) {
@@ -169,7 +145,7 @@ export default function DistributorScreen({ navigation, route }) {
/>
<ActionButton
icon="thermometer"
- text="Update Storage Conditions"
+ text="Update Storage"
onPress={() => setActive("storage")}
/>
</View>
@@ -188,35 +164,7 @@ export default function DistributorScreen({ navigation, route }) {
)}
{active === "transfer" && (
- <View>
- <Scanner value={produceId} onChange={setProduceId} />
- <TextInput
- style={styles.input}
- placeholder="New Owner ID"
- value={newOwnerId}
- onChangeText={setNewOwnerId}
- />
- <TextInput
- style={styles.input}
- placeholder="Quantity"
- keyboardType="numeric"
- value={qty}
- onChangeText={setQty}
- />
- <TextInput
- style={styles.input}
- placeholder="Sale Price"
- keyboardType="numeric"
- value={salePrice}
- onChangeText={setSalePrice}
- />
- <TouchableOpacity
- style={styles.primaryButton}
- onPress={transferOwnership}
- >
- <Text style={styles.buttonText}>Transfer Ownership</Text>
- </TouchableOpacity>
- </View>
+ <TransferOwnershipForm onSubmit={transferOwnership} />
)}
{active === "remove" && (
@@ -230,7 +178,7 @@ export default function DistributorScreen({ navigation, route }) {
/>
<TextInput
style={styles.input}
- placeholder="New Status (Removed/Missing...)"
+ placeholder="New Status (e.g., Removed, Missing)"
value={newStatus}
onChangeText={setNewStatus}
/>
@@ -248,7 +196,7 @@ export default function DistributorScreen({ navigation, route }) {
<Scanner value={produceId} onChange={setProduceId} />
<TextInput
style={styles.input}
- placeholder="Enter Storage Conditions (comma separated)"
+ placeholder="Storage Conditions (comma separated)"
value={storageConditions}
onChangeText={setStorageConditions}
/>
diff --git a/frontend/screens/Farmer.js b/frontend/screens/Farmer.js
@@ -8,6 +8,7 @@ import {
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
+
import ScreenHeader from "../components/ScreenHeader";
import ActionButton from "../components/ActionButton";
import Scanner from "../components/Scanner";
@@ -15,7 +16,9 @@ import LocationPicker from "../components/LocationPicker";
import QRModal from "../components/QRModal";
import ImageUploader from "../components/ImageUploader";
import DatePicker from "../components/DatePicker";
-import { API_BASE } from "../config";
+import TransferOwnershipForm from "../components/TransferOwnershipForm";
+
+import { api } from "../services/api";
import styles from "../styles";
import { AuthContext } from "../AuthContext";
@@ -27,14 +30,12 @@ export default function FarmerScreen({ navigation, route }) {
const [active, setActive] = useState(null);
const imageUploaderRef = useRef(null);
- // Common
const [produceId, setProduceId] = useState("");
const [location, setLocation] = useState("");
const [pricePerUnit, setPricePerUnit] = useState("");
const [storageConditions, setStorageConditions] = useState("");
const [certification, setCertification] = useState("");
- // registerProduce
const [cropType, setCropType] = useState("");
const [qty, setQty] = useState("");
const [qtyUnit, setQtyUnit] = useState("KG");
@@ -42,19 +43,11 @@ export default function FarmerScreen({ navigation, route }) {
const [quality, setQuality] = useState("");
const [expiryDate, setExpiryDate] = useState("");
- // splitProduce
const [splitQty, setSplitQty] = useState("");
- // transferOwnership
- const [newOwnerId, setNewOwnerId] = useState("");
- const [salePrice, setSalePrice] = useState("");
- const [transferQty, setTransferQty] = useState("");
-
- // QR Modal
const [qrVisible, setQrVisible] = useState(false);
const [lastProduceId, setLastProduceId] = useState(null);
- // Reset
const resetRegisterForm = () => {
setCropType("");
setQty("");
@@ -73,18 +66,11 @@ export default function FarmerScreen({ navigation, route }) {
setProduceId("");
};
- // Functions
const registerProduce = async () => {
try {
const imageUrl = await imageUploaderRef.current?.upload();
-
- const res = await fetch(`${API_BASE}/registerProduce`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({
+ const { produce } = await api.registerProduce(
+ {
farmerId: userId,
details: {
imageUrl,
@@ -102,39 +88,23 @@ export default function FarmerScreen({ navigation, route }) {
? certification.split(",").map((c) => c.trim())
: [],
location,
- farmerName: user?.username || "",
},
- }),
- });
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Error");
-
- const produced = data.produce;
- setLastProduceId(produced.id);
+ },
+ token
+ );
+ setLastProduceId(produce.id);
setQrVisible(true);
-
- console.info("Registered", "Produce registered successfully");
resetRegisterForm();
- resetCommon();
} catch (err) {
Alert.alert("Error", err.message);
}
};
const updateDetails = async () => {
- if (!produceId) {
- Alert.alert("Error", "Please enter Produce ID");
- return;
- }
-
+ if (!produceId) return Alert.alert("Error", "Please enter Produce ID");
try {
- const res = await fetch(`${API_BASE}/updateDetails`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({
+ await api.updateDetails(
+ {
produceId,
actorId: userId,
details: {
@@ -146,42 +116,31 @@ export default function FarmerScreen({ navigation, route }) {
? certification.split(",").map((c) => c.trim())
: undefined,
},
- }),
- });
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Error");
-
- Alert.alert("Updated", "Produce details updated");
+ },
+ token
+ );
+ Alert.alert("Updated", "Produce details updated successfully");
resetCommon();
setPricePerUnit("");
setStorageConditions("");
+ setCertification("");
} catch (err) {
Alert.alert("Error", err.message);
}
};
const splitProduce = async () => {
- if (!produceId || !splitQty) {
- Alert.alert("Error", "Please enter Produce ID quantity to split");
- return;
- }
-
+ if (!produceId || !splitQty)
+ return Alert.alert("Error", "Please enter Produce ID and quantity");
try {
- const res = await fetch(`${API_BASE}/splitProduce`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({
+ await api.splitProduce(
+ {
produceId,
qty: parseFloat(splitQty),
ownerId: userId,
- }),
- });
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Error");
-
+ },
+ token
+ );
Alert.alert("Split", "Produce split successfully");
setSplitQty("");
resetCommon();
@@ -191,27 +150,17 @@ export default function FarmerScreen({ navigation, route }) {
};
const updateLocation = async () => {
- if (!produceId || !location) {
- Alert.alert("Error", "Please enter Produce ID and pick a location");
- return;
- }
-
+ if (!produceId || !location)
+ return Alert.alert("Error", "Please enter Produce ID and location");
try {
- const res = await fetch(`${API_BASE}/updateLocation`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({
+ await api.updateLocation(
+ {
produceId,
actorId: userId,
newLocation: location,
- }),
- });
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Error");
-
+ },
+ token
+ );
Alert.alert("Moved", "Location updated successfully");
resetCommon();
setLocation("");
@@ -220,35 +169,30 @@ export default function FarmerScreen({ navigation, route }) {
}
};
- const transferOwnership = async () => {
- if (!produceId || !newOwnerId || !transferQty) {
- Alert.alert(
+ const transferOwnership = async ({
+ produceId,
+ newOwnerId,
+ qty,
+ salePrice,
+ onComplete,
+ }) => {
+ if (!produceId || !newOwnerId || !qty)
+ return Alert.alert(
"Error",
- "Please enter Produce ID, New Owner ID and Quantity"
+ "Please provide Produce ID, New Owner ID, and Quantity."
);
- return;
- }
try {
- const res = await fetch(`${API_BASE}/transferOwnership`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({
+ await api.transferOwnership(
+ {
produceId,
newOwnerId,
- qty: parseFloat(transferQty),
+ qty: parseFloat(qty),
salePrice: salePrice ? parseFloat(salePrice) : 0,
- }),
- });
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Error");
+ },
+ token
+ );
Alert.alert("Transferred", "Ownership transferred successfully");
- resetCommon();
- setNewOwnerId("");
- setTransferQty("");
- setSalePrice("");
+ onComplete();
} catch (err) {
Alert.alert("Error", err.message);
}
@@ -421,35 +365,7 @@ export default function FarmerScreen({ navigation, route }) {
)}
{active === "transfer" && (
- <View>
- <Scanner value={produceId} onChange={setProduceId} />
- <TextInput
- style={styles.input}
- placeholder="New Owner ID"
- value={newOwnerId}
- onChangeText={setNewOwnerId}
- />
- <TextInput
- style={styles.input}
- placeholder="Quantity"
- keyboardType="numeric"
- value={transferQty}
- onChangeText={setTransferQty}
- />
- <TextInput
- style={styles.input}
- placeholder="Sale Price"
- keyboardType="numeric"
- value={salePrice}
- onChangeText={setSalePrice}
- />
- <TouchableOpacity
- style={styles.primaryButton}
- onPress={transferOwnership}
- >
- <Text style={styles.buttonText}>Transfer Ownership</Text>
- </TouchableOpacity>
- </View>
+ <TransferOwnershipForm onSubmit={transferOwnership} />
)}
</ScrollView>
diff --git a/frontend/screens/Inspector.js b/frontend/screens/Inspector.js
@@ -9,11 +9,13 @@ import {
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
+
import ScreenHeader from "../components/ScreenHeader";
import ActionButton from "../components/ActionButton";
import Scanner from "../components/Scanner";
import DatePicker from "../components/DatePicker";
-import { API_BASE } from "../config";
+
+import { api } from "../services/api";
import styles, { colors } from "../styles";
import { AuthContext } from "../AuthContext";
@@ -21,8 +23,8 @@ export default function InspectorScreen({ navigation, route }) {
const { user } = useContext(AuthContext);
const userId = route.params?.userId || user?.id;
const token = user?.token;
- const [active, setActive] = useState(null);
+ const [active, setActive] = useState(null);
const [produceId, setProduceId] = useState("");
const [quality, setQuality] = useState("");
const [expiryDate, setExpiryDate] = useState("");
@@ -38,10 +40,7 @@ export default function InspectorScreen({ navigation, route }) {
};
const inspectProduce = async () => {
- if (!produceId) {
- Alert.alert("Error", "Enter a Produce ID");
- return;
- }
+ if (!produceId) return Alert.alert("Error", "Enter a Produce ID");
try {
const body = {
@@ -55,17 +54,7 @@ export default function InspectorScreen({ navigation, route }) {
},
};
- const res = await fetch(`${API_BASE}/inspectProduce`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify(body),
- });
-
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Error");
+ await api.inspectProduce(body, token);
Alert.alert(
markFailed ? "Marked as Failed" : "Inspection Recorded",
@@ -125,6 +114,7 @@ export default function InspectorScreen({ navigation, route }) {
thumbColor={markFailed ? colors.danger : colors.gray}
/>
</View>
+
{markFailed && (
<TextInput
style={styles.input}
@@ -133,6 +123,7 @@ export default function InspectorScreen({ navigation, route }) {
onChangeText={setReason}
/>
)}
+
<TouchableOpacity
style={[
styles.primaryButton,
diff --git a/frontend/screens/Inventory.js b/frontend/screens/Inventory.js
@@ -8,10 +8,12 @@ import {
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
+
import ScreenHeader from "../components/ScreenHeader";
import ProduceCard from "../components/ProduceCard";
+
import { AuthContext } from "../AuthContext";
-import { API_BASE } from "../config";
+import { api } from "../services/api";
import styles, { colors } from "../styles";
export default function InventoryScreen({ navigation, route }) {
@@ -25,17 +27,6 @@ export default function InventoryScreen({ navigation, route }) {
fetchInventory();
}, []);
- const fetchUserDetails = async (userKey) => {
- try {
- const res = await fetch(`${API_BASE}/getUser/${userKey}`);
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Error fetching user");
- return data.user;
- } catch (err) {
- throw err;
- }
- };
-
const fetchInventory = async () => {
try {
setLoading(true);
@@ -43,24 +34,22 @@ export default function InventoryScreen({ navigation, route }) {
if (role.toUpperCase() === "INSPECTOR") {
const userKey = `INSPECTOR-${routeUserId}`;
- const userDetails = await fetchUserDetails(userKey);
+ const { user: userDetails } = await api.getUserDetails(userKey);
const inspected = userDetails.inspectedProduce || [];
const items = [];
+
for (const pid of inspected) {
try {
- const resp = await fetch(`${API_BASE}/getProduce/${pid}`);
- const data = await resp.json();
- if (resp.ok && data.produce) items.push(data.produce);
+ const { produce } = await api.getProduceById(pid);
+ if (produce) items.push(produce);
} catch (e) {
// Ignore failure for single produce
}
}
setProduces(items);
} else {
- const res = await fetch(`${API_BASE}/getProduceByOwner/${routeUserId}`);
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Error fetching inventory");
- setProduces(data.produces || []);
+ const { produces } = await api.getProduceByOwner(routeUserId);
+ setProduces(produces || []);
}
} catch (err) {
Alert.alert("Error", err.message);
@@ -93,7 +82,6 @@ export default function InventoryScreen({ navigation, route }) {
) : (
produces.map((p) => <ProduceCard key={p.id} produce={p} />)
)}
-
<TouchableOpacity
style={styles.secondaryButton}
onPress={fetchInventory}
diff --git a/frontend/screens/Login.js b/frontend/screens/Login.js
@@ -2,9 +2,10 @@ import { useState, useContext } from "react";
import { Text, TextInput, TouchableOpacity, Alert } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { MaterialCommunityIcons } from "@expo/vector-icons";
+
import styles, { colors } from "../styles";
import { AuthContext } from "../AuthContext";
-import { API_BASE } from "../config";
+import { api } from "../services/api";
export default function LoginScreen({ route, navigation }) {
const { role } = route.params || {};
@@ -17,29 +18,24 @@ export default function LoginScreen({ route, navigation }) {
Alert.alert("Error", "Enter username and password");
return;
}
+
try {
- const res = await fetch(`${API_BASE}/auth/login`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ username, password }),
- });
- if (!res.ok) {
- const err = await res.json().catch(() => ({}));
- throw new Error(err.error || `Server ${res.status}`);
- }
- const data = await res.json();
+ const data = await api.login({ username, password });
+
if (role && data.role !== role) {
return Alert.alert(
"Error",
`Account role mismatch. Expected ${role}, got ${data.role}`
);
}
+
await login({
token: data.token,
id: data.id,
role: data.role,
username: data.username,
});
+
navigation.replace(data.role, { userId: data.id });
} catch (err) {
Alert.alert("Login failed", err.message);
@@ -49,7 +45,6 @@ export default function LoginScreen({ route, navigation }) {
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>Login as {role}</Text>
-
<TextInput
style={styles.input}
placeholder="Username"
@@ -64,7 +59,6 @@ export default function LoginScreen({ route, navigation }) {
value={password}
onChangeText={setPassword}
/>
-
<TouchableOpacity
style={[
styles.bigButton,
diff --git a/frontend/screens/Retailer.js b/frontend/screens/Retailer.js
@@ -8,11 +8,14 @@ import {
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
+
import ScreenHeader from "../components/ScreenHeader";
import ActionButton from "../components/ActionButton";
import Scanner from "../components/Scanner";
import LocationPicker from "../components/LocationPicker";
-import { API_BASE } from "../config";
+import TransferOwnershipForm from "../components/TransferOwnershipForm";
+
+import { api } from "../services/api";
import styles from "../styles";
import { AuthContext } from "../AuthContext";
@@ -20,37 +23,27 @@ export default function RetailerScreen({ navigation, route }) {
const { user } = useContext(AuthContext);
const userId = route.params?.userId || user?.id;
const token = user?.token;
- const [active, setActive] = useState(null);
+ const [active, setActive] = useState(null);
const [produceId, setProduceId] = useState("");
const [location, setLocation] = useState("");
const [pricePerUnit, setPricePerUnit] = useState("");
const [storageConditions, setStorageConditions] = useState("");
- const [newOwnerId, setNewOwnerId] = useState("");
- const [qty, setQty] = useState("");
- const [salePrice, setSalePrice] = useState("");
-
const resetCommon = () => {
setProduceId("");
};
const updateLocation = async () => {
try {
- const res = await fetch(`${API_BASE}/updateLocation`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({
+ await api.updateLocation(
+ {
produceId,
actorId: userId,
newLocation: location,
- }),
- });
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Error");
+ },
+ token
+ );
Alert.alert("Location Updated", "Location updated successfully");
resetCommon();
setLocation("");
@@ -59,28 +52,30 @@ export default function RetailerScreen({ navigation, route }) {
}
};
- const transferOwnership = async () => {
+ const transferOwnership = async ({
+ produceId,
+ newOwnerId,
+ qty,
+ salePrice,
+ onComplete,
+ }) => {
+ if (!produceId || !newOwnerId || !qty)
+ return Alert.alert(
+ "Error",
+ "Please provide Produce ID, New Owner ID, and Quantity."
+ );
try {
- const res = await fetch(`${API_BASE}/transferOwnership`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({
+ await api.transferOwnership(
+ {
produceId,
newOwnerId,
qty: parseFloat(qty),
salePrice: parseFloat(salePrice),
- }),
- });
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Error");
+ },
+ token
+ );
Alert.alert("Transferred", "Sale / Transfer successful");
- resetCommon();
- setNewOwnerId("");
- setQty("");
- setSalePrice("");
+ onComplete();
} catch (err) {
Alert.alert("Error", err.message);
}
@@ -88,13 +83,8 @@ export default function RetailerScreen({ navigation, route }) {
const updateDetails = async () => {
try {
- const res = await fetch(`${API_BASE}/updateDetails`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({
+ await api.updateDetails(
+ {
produceId,
actorId: userId,
details: {
@@ -103,11 +93,10 @@ export default function RetailerScreen({ navigation, route }) {
? storageConditions.split(",").map((c) => c.trim())
: [],
},
- }),
- });
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Error");
- Alert.alert("Updated Details", "Produce details updated");
+ },
+ token
+ );
+ Alert.alert("Updated Details", "Produce details updated successfully");
resetCommon();
setPricePerUnit("");
setStorageConditions("");
@@ -156,35 +145,7 @@ export default function RetailerScreen({ navigation, route }) {
)}
{active === "transfer" && (
- <View>
- <Scanner value={produceId} onChange={setProduceId} />
- <TextInput
- style={styles.input}
- placeholder="New Owner ID"
- value={newOwnerId}
- onChangeText={setNewOwnerId}
- />
- <TextInput
- style={styles.input}
- placeholder="Quantity"
- keyboardType="numeric"
- value={qty}
- onChangeText={setQty}
- />
- <TextInput
- style={styles.input}
- placeholder="Sale Price"
- keyboardType="numeric"
- value={salePrice}
- onChangeText={setSalePrice}
- />
- <TouchableOpacity
- style={styles.primaryButton}
- onPress={transferOwnership}
- >
- <Text style={styles.buttonText}>Confirm Sale</Text>
- </TouchableOpacity>
- </View>
+ <TransferOwnershipForm onSubmit={transferOwnership} />
)}
{active === "update" && (
diff --git a/frontend/screens/Search.js b/frontend/screens/Search.js
@@ -15,20 +15,20 @@ import {
} 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 ProduceCard from "../components/ProduceCard";
import DetailRow from "../components/DetailRow";
-import { API_BASE } from "../config";
+
+import { api } from "../services/api";
import styles, { colors } from "../styles";
export default function SearchScreen({ navigation }) {
const insets = useSafeAreaInsets();
-
const [tab, setTab] = useState("Produce");
const [loading, setLoading] = useState(false);
const [result, setResult] = useState(null);
-
const [produceId, setProduceId] = useState("");
const [userRole, setUserRole] = useState("FARMER");
const [userId, setUserId] = useState("");
@@ -42,18 +42,13 @@ export default function SearchScreen({ navigation }) {
try {
setLoading(true);
setResult(null);
-
- let url = "";
- if (isProduceSearch) url = `${API_BASE}/getProduce/${produceId.trim()}`;
- else {
+ let data;
+ if (isProduceSearch) {
+ data = await api.getProduceById(produceId.trim());
+ } else {
const userKey = `${userRole}-${userId.trim()}`;
- url = `${API_BASE}/getUser/${userKey}`;
+ data = await api.getUserDetails(userKey);
}
-
- const res = await fetch(url);
- const data = await res.json();
- if (!res.ok)
- throw new Error(data.error || `Server returned ${res.status}`);
setResult(data);
} catch (err) {
Alert.alert("Error", err.message);
@@ -161,7 +156,6 @@ export default function SearchScreen({ navigation }) {
hideSearchButton={true}
showBack={true}
/>
-
<View style={local.searchContainer}>
<Text
style={{
@@ -172,7 +166,6 @@ export default function SearchScreen({ navigation }) {
>
Search for information about any produce or user in the supply chain
</Text>
-
<View
style={{ flexDirection: "row", justifyContent: "space-around" }}
>
@@ -210,7 +203,6 @@ export default function SearchScreen({ navigation }) {
</TouchableOpacity>
))}
</View>
-
{tab === "Produce" ? (
<Scanner value={produceId} onChange={setProduceId} />
) : (
@@ -244,7 +236,6 @@ export default function SearchScreen({ navigation }) {
/>
</View>
)}
-
<TouchableOpacity
style={[styles.primaryButton, { marginTop: 20 }]}
onPress={fetchResult}
@@ -257,7 +248,6 @@ export default function SearchScreen({ navigation }) {
)}
</TouchableOpacity>
</View>
-
{loading && (
<ActivityIndicator
size="large"
@@ -265,7 +255,6 @@ export default function SearchScreen({ navigation }) {
style={{ marginTop: 30 }}
/>
)}
-
{!loading && result && (
<>
{tab === "Produce" && result.produce ? (
diff --git a/frontend/services/api.js b/frontend/services/api.js
@@ -0,0 +1,56 @@
+import { API_BASE } from "../config";
+
+const request = async (endpoint, options = {}) => {
+ const { body, token, ...customConfig } = options;
+
+ const headers = { "Content-Type": "application/json" };
+ if (token) headers.Authorization = `Bearer ${token}`;
+
+ const config = {
+ method: body ? "POST" : "GET",
+ ...customConfig,
+ headers: { ...headers, ...customConfig.headers },
+ };
+ if (body) config.body = JSON.stringify(body);
+
+ const response = await fetch(`${API_BASE}${endpoint}`, config);
+ const data = await response.json();
+ if (!response.ok) throw new Error(data.error || "An API error occurred");
+ return data;
+};
+
+const uploadImage = (formData, token) => {
+ return fetch(`${API_BASE}/uploadImage`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ body: formData,
+ }).then(async (res) => {
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.error || "Image upload failed");
+ return data;
+ });
+};
+
+export const api = {
+ login: (credentials) => request("/auth/login", { body: credentials }),
+ uploadImage,
+ registerProduce: (data, token) =>
+ request("/registerProduce", { body: data, token }),
+ updateLocation: (data, token) =>
+ request("/updateLocation", { body: data, token }),
+ transferOwnership: (data, token) =>
+ request("/transferOwnership", { body: data, token }),
+ inspectProduce: (data, token) =>
+ request("/inspectProduce", { body: data, token }),
+ updateDetails: (data, token) =>
+ request("/updateDetails", { body: data, token }),
+ markAsUnavailable: (data, token) =>
+ request("/markAsUnavailable", { body: data, token }),
+ splitProduce: (data, token) =>
+ request("/splitProduce", { body: data, token }),
+ getProduceById: (id) => request(`/getProduce/${id}`),
+ getProduceByOwner: (ownerId) => request(`/getProduceByOwner/${ownerId}`),
+ getUserDetails: (userKey) => request(`/getUser/${userKey}`),
+};