matiru

Blockchain-Based Supply Chain Transparency for Agricultural Produce
commit cfdfdeb8cbd4fe64ddbe1b7fdd113540a0136ff3
parent 2eb1c86e7c7efe40424f7e3f2518722df0767ca1
Author: maydayv7 <maydayv7@gmail.com>
Date:   Tue,  7 Oct 2025 22:43:54 +0530

fix: Performance Improvements + Style Changes

Diffstat:
Mbackend/server.js | 301++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mfabric/chaincode/index.js | 69++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mfrontend/components/ImageUploader.js | 24+++++-------------------
Afrontend/components/TransferOwnershipForm.js | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mfrontend/screens/Distributor.js | 138+++++++++++++++++++++++++------------------------------------------------------
Mfrontend/screens/Farmer.js | 184++++++++++++++++++++++---------------------------------------------------------
Mfrontend/screens/Inspector.js | 25++++++++-----------------
Mfrontend/screens/Inventory.js | 30+++++++++---------------------
Mfrontend/screens/Login.js | 20+++++++-------------
Mfrontend/screens/Retailer.js | 109++++++++++++++++++++++++++-----------------------------------------------------
Mfrontend/screens/Search.js | 27++++++++-------------------
Afrontend/services/api.js | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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}`), +};