matiru

Blockchain-Based Supply Chain Transparency for Agricultural Produce
commit 89a8abcab81f22f210b5deba1c4d7bfa8b5b1a01
parent 57e5e0226cec4442fe3987b398279e7bf7ecc8b1
Author: maydayv7 <maydayv7@gmail.com>
Date:   Thu,  2 Oct 2025 19:26:41 +0530

feat(frontend): Implement fake login + Populate acc. to schema

Diffstat:
Mbackend/server.js | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mfrontend/App.js | 60++++++++++++++++++++++--------------------------------------
Afrontend/components/ActionButton.js | 12++++++++++++
Mfrontend/components/LocationPicker.js | 61+++++++++++++++++++++++++++++++++++++------------------------
Dfrontend/components/QRScanner.js | 146-------------------------------------------------------------------------------
Afrontend/components/Scanner.js | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afrontend/components/ScreenHeader.js | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Dfrontend/components/SearchNav.js | 45---------------------------------------------
Mfrontend/screens/Distributor.js | 214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mfrontend/screens/Farmer.js | 187+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mfrontend/screens/Inspector.js | 187++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mfrontend/screens/Login.js | 73+++++++++++++++++++++++++++++++++++++++++++------------------------------
Mfrontend/screens/Retailer.js | 174++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mfrontend/screens/Search.js | 206++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mfrontend/styles.js | 50++++++++++++++++++++++++--------------------------
15 files changed, 1025 insertions(+), 599 deletions(-)

diff --git a/backend/server.js b/backend/server.js @@ -106,6 +106,61 @@ app.post("/api/transferOwnership", async (req, res) => { } }); +app.post("/api/updateDetails", async (req, res) => { + try { + const { produceId, actorId, details } = req.body; + const { contract, gateway } = await getContract(); + 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 }); + } +}); + +app.post("/api/markAsUnavailable", async (req, res) => { + try { + const { produceId, actorId, reason, newStatus } = req.body; + const { contract, gateway } = await getContract(); + const result = await contract.submitTransaction( + "markAsUnavailable", + produceId, + actorId, + 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 }); + } +}); + +app.post("/api/splitProduce", async (req, res) => { + try { + const { produceId, qty, ownerId } = req.body; + const { contract, gateway } = await getContract(); + 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 }); + } +}); + app.post("/api/recordPayment", async (req, res) => { try { const { diff --git a/frontend/App.js b/frontend/App.js @@ -1,9 +1,9 @@ import { ImageBackground, ScrollView, + StatusBar, Text, TouchableOpacity, - StatusBar, View, } from "react-native"; import { NavigationContainer } from "@react-navigation/native"; @@ -12,6 +12,7 @@ import { SafeAreaView } from "react-native-safe-area-context"; import { MaterialCommunityIcons } from "@expo/vector-icons"; import styles, { colors } from "./styles"; +import LoginScreen from "./screens/Login"; import FarmerScreen from "./screens/Farmer"; import DistributorScreen from "./screens/Distributor"; import RetailerScreen from "./screens/Retailer"; @@ -42,49 +43,31 @@ function HomeScreen({ navigation }) { styles.bigButton, { backgroundColor: "#444", marginBottom: 30 }, ]} - onPress={() => navigation.navigate("Search")} + onPress={() => + navigation.navigate("Search", { fromRole: "Consumer" }) + } > <MaterialCommunityIcons name="magnify" size={24} color="white" /> <Text style={styles.bigButtonText}>Global Search</Text> </TouchableOpacity> - <Text style={styles.subtitle}>Continue As</Text> + <Text style={styles.subtitle}> Continue As </Text> - <TouchableOpacity - style={styles.bigButton} - onPress={() => navigation.navigate("Farmer")} - > - <MaterialCommunityIcons name="tractor" size={24} color="white" /> - <Text style={styles.bigButtonText}>Farmer</Text> - </TouchableOpacity> - - <TouchableOpacity - style={styles.bigButton} - onPress={() => navigation.navigate("Distributor")} - > - <MaterialCommunityIcons name="truck" size={24} color="white" /> - <Text style={styles.bigButtonText}>Distributor</Text> - </TouchableOpacity> - - <TouchableOpacity - style={styles.bigButton} - onPress={() => navigation.navigate("Retailer")} - > - <MaterialCommunityIcons name="store" size={24} color="white" /> - <Text style={styles.bigButtonText}>Retailer</Text> - </TouchableOpacity> - - <TouchableOpacity - style={styles.bigButton} - onPress={() => navigation.navigate("Inspector")} - > - <MaterialCommunityIcons - name="shield-check" - size={24} - color="white" - /> - <Text style={styles.bigButtonText}>Inspector</Text> - </TouchableOpacity> + {[ + { role: "Farmer", icon: "tractor" }, + { role: "Distributor", icon: "truck" }, + { role: "Retailer", icon: "store" }, + { role: "Inspector", icon: "shield-check" }, + ].map((r) => ( + <TouchableOpacity + key={r.role} + style={styles.bigButton} + onPress={() => navigation.navigate("Login", { role: r.role })} + > + <MaterialCommunityIcons name={r.icon} size={24} color="white" /> + <Text style={styles.bigButtonText}>{r.role}</Text> + </TouchableOpacity> + ))} </View> <View style={styles.bottomContent}> @@ -101,6 +84,7 @@ export default function App() { <NavigationContainer> <Stack.Navigator screenOptions={{ headerShown: false }}> <Stack.Screen name="Home" component={HomeScreen} /> + <Stack.Screen name="Login" component={LoginScreen} /> <Stack.Screen name="Farmer" component={FarmerScreen} /> <Stack.Screen name="Distributor" component={DistributorScreen} /> <Stack.Screen name="Retailer" component={RetailerScreen} /> diff --git a/frontend/components/ActionButton.js b/frontend/components/ActionButton.js @@ -0,0 +1,12 @@ +import { TouchableOpacity, Text } from "react-native"; +import { MaterialCommunityIcons } from "@expo/vector-icons"; +import styles from "../styles"; + +export default function ActionButton({ icon, text, onPress }) { + return ( + <TouchableOpacity style={styles.actionButton} onPress={onPress}> + <MaterialCommunityIcons name={icon} size={20} color="white" /> + <Text style={styles.actionText}>{text}</Text> + </TouchableOpacity> + ); +} diff --git a/frontend/components/LocationPicker.js b/frontend/components/LocationPicker.js @@ -3,52 +3,65 @@ import { View, TextInput, Button, Text, Alert } from "react-native"; import * as Location from "expo-location"; import { colors } from "../styles"; -export default function LocationPicker({ onPicked }) { - const [manual, setManual] = useState(""); - const [place, setPlace] = useState(""); +export default function LocationPicker({ value, onChange }) { + const [coords, setCoords] = useState(null); - const pickGPS = async () => { + const pickLocation = async () => { try { const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== "granted") { - return Alert.alert("Permission denied", "Enable location services."); - } - const pos = await Location.getCurrentPositionAsync({}); - const geo = await Location.reverseGeocodeAsync(pos.coords); - if (geo.length > 0) { - const name = `${geo[0].city || ""} ${geo[0].name || ""}`; - setPlace(name); - onPicked(name); + Alert.alert("Permission denied", "Enable location services."); + return; } + + const loc = await Location.getCurrentPositionAsync({}); + setCoords( + `Lat ${loc.coords.latitude.toFixed(4)}, Lng ${loc.coords.longitude.toFixed(4)}` + ); + + const [address] = await Location.reverseGeocodeAsync({ + latitude: loc.coords.latitude, + longitude: loc.coords.longitude, + }); + + const placeName = address + ? `${address.name || ""} ${address.street || ""}, ${address.city || ""}, ${address.region || ""}, ${address.country || ""}`.trim() + : ""; + + onChange( + placeName || + `${loc.coords.latitude.toFixed(4)}, ${loc.coords.longitude.toFixed(4)}` + ); } catch (err) { Alert.alert("Error", err.message); } }; return ( - <View style={{ marginTop: 10 }}> + <View style={{ marginVertical: 8 }}> <Button title="Use Current Location" color={colors.darkGreen} - onPress={pickGPS} + onPress={pickLocation} /> <TextInput style={{ borderWidth: 1, borderColor: colors.darkGreen, - borderRadius: 8, - padding: 10, - marginTop: 10, + borderRadius: 10, + padding: 12, + marginTop: 8, backgroundColor: "white", }} - placeholder="Or enter place manually" - value={manual} - onChangeText={(t) => { - setManual(t); - onPicked(t); - }} + placeholder="Enter Location Name" + value={value} + onChangeText={onChange} /> - {place ? <Text style={{ marginTop: 6 }}>📍 {place}</Text> : null} + {coords && ( + <Text style={{ marginTop: 6, color: colors.darkGreen }}> + 📍 {coords} + </Text> + )} </View> ); } diff --git a/frontend/components/QRScanner.js b/frontend/components/QRScanner.js @@ -1,146 +0,0 @@ -import { useState, useEffect } from "react"; -import { - View, - Text, - Button, - StyleSheet, - Dimensions, - TextInput, - TouchableOpacity, - Alert, -} from "react-native"; -import { Camera, CameraView } from "expo-camera"; -import { colors } from "../styles"; - -const { width } = Dimensions.get("window"); -const SCAN_AREA_SIZE = width * 0.72; - -export default function Scanner({ - onScanned, - placeholder = "Enter ID manually", -}) { - const [hasPermission, setHasPermission] = useState(null); - const [useCamera, setUseCamera] = useState(false); - const [scanned, setScanned] = useState(false); - const [manualId, setManualId] = useState(""); - - useEffect(() => { - (async () => { - const { status } = await Camera.requestCameraPermissionsAsync(); - setHasPermission(status === "granted"); - })(); - }, []); - - const handleBarCodeScanned = ({ data }) => { - setScanned(true); - onScanned(data); - }; - - return ( - <View style={{ marginVertical: 8 }}> - <Button - title={useCamera ? "Use Manual Entry" : "Scan QR Code"} - color={colors.darkGreen} - onPress={() => { - setUseCamera(!useCamera); - setScanned(false); - setManualId(""); - }} - /> - - {useCamera ? ( - <> - {hasPermission === null && ( - <Text>Requesting camera permission...</Text> - )} - {hasPermission === false && <Text>No access to camera</Text>} - {hasPermission && ( - <View style={{ height: 360, marginTop: 10 }}> - <CameraView - style={{ flex: 1 }} - onBarcodeScanned={scanned ? undefined : handleBarCodeScanned} - barcodeScannerSettings={{ barcodeTypes: ["qr"] }} - /> - - {/* Overlay */} - <View style={scannerStyles.overlay}> - <View style={scannerStyles.topBottomOverlay} /> - <View style={scannerStyles.middleRow}> - <View style={scannerStyles.sideOverlay} /> - <View style={scannerStyles.scanArea} /> - <View style={scannerStyles.sideOverlay} /> - </View> - <View style={scannerStyles.topBottomOverlay} /> - </View> - - {scanned && ( - <View style={{ marginTop: 8 }}> - <Button - title="Scan again" - color={colors.midGreen} - onPress={() => setScanned(false)} - /> - </View> - )} - </View> - )} - </> - ) : ( - <View style={{ marginTop: 8 }}> - <TextInput - placeholder={placeholder} - value={manualId} - onChangeText={setManualId} - style={{ - borderWidth: 1, - borderColor: colors.cream, - borderRadius: 10, - padding: 10, - backgroundColor: "#fff", - }} - /> - <TouchableOpacity - style={{ - backgroundColor: colors.midGreen, - padding: 12, - borderRadius: 10, - marginTop: 8, - alignItems: "center", - }} - onPress={() => { - if (!manualId) { - Alert.alert("Error", "ID cannot be empty"); - return; - } - onScanned(manualId); - }} - > - <Text style={{ color: "#fff", fontWeight: "700" }}>Submit</Text> - </TouchableOpacity> - </View> - )} - </View> - ); -} - -const scannerStyles = StyleSheet.create({ - overlay: { - ...StyleSheet.absoluteFillObject, - justifyContent: "center", - alignItems: "center", - }, - topBottomOverlay: { - flex: 1, - width: "100%", - backgroundColor: "rgba(0,0,0,0.5)", - }, - middleRow: { flexDirection: "row" }, - sideOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.5)" }, - scanArea: { - width: SCAN_AREA_SIZE, - height: SCAN_AREA_SIZE, - borderWidth: 2, - borderColor: colors.midGreen, - borderRadius: 12, - }, -}); diff --git a/frontend/components/Scanner.js b/frontend/components/Scanner.js @@ -0,0 +1,104 @@ +import { useState, useEffect } from "react"; +import { + Alert, + Button, + Dimensions, + StyleSheet, + TextInput, + View, +} from "react-native"; +import { CameraView, Camera } from "expo-camera"; +import styles, { colors } from "../styles"; + +const { width } = Dimensions.get("window"); +const SCAN_AREA_SIZE = width * 0.7; + +export default function Scanner({ value, onChange }) { + const [hasPermission, setHasPermission] = useState(null); + const [useCamera, setUseCamera] = useState(false); + const [scanned, setScanned] = useState(false); + + useEffect(() => { + (async () => { + const { status } = await Camera.requestCameraPermissionsAsync(); + setHasPermission(status === "granted"); + })(); + }, []); + + const handleScanned = ({ data }) => { + if (!data) { + Alert.alert("Scan Failed", "No data found in QR code."); + return; + } + + setScanned(true); + onChange(data); + setUseCamera(false); + }; + + return ( + <View style={{ marginVertical: 8 }}> + <Button + title={useCamera ? "Use Manual Entry" : "Scan QR Code"} + color={colors.darkGreen} + onPress={() => { + setUseCamera(!useCamera); + setScanned(false); + }} + /> + {useCamera ? ( + !scanned ? ( + <View style={{ height: SCAN_AREA_SIZE * 1.4, marginTop: 10 }}> + <CameraView + style={{ flex: 1 }} + onBarcodeScanned={scanned ? undefined : handleScanned} + barcodeScannerSettings={{ barcodeTypes: ["qr"] }} + /> + <View style={cameraStyles.overlay}> + <View style={cameraStyles.topBottomOverlay} /> + <View style={cameraStyles.middleRow}> + <View style={cameraStyles.sideOverlay} /> + <View style={cameraStyles.scanArea} /> + <View style={cameraStyles.sideOverlay} /> + </View> + <View style={cameraStyles.topBottomOverlay} /> + </View> + </View> + ) : null + ) : ( + <TextInput + style={styles.input} + placeholder="Enter Produce ID" + value={value} + onChangeText={onChange} + /> + )} + </View> + ); +} + +const cameraStyles = StyleSheet.create({ + overlay: { + ...StyleSheet.absoluteFillObject, + justifyContent: "center", + alignItems: "center", + }, + topBottomOverlay: { + flex: 1, + width: "100%", + backgroundColor: "rgba(0,0,0,0.5)", + }, + middleRow: { + flexDirection: "row", + }, + sideOverlay: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.5)", + }, + scanArea: { + width: SCAN_AREA_SIZE, + height: SCAN_AREA_SIZE, + borderWidth: 2, + borderColor: colors.midGreen, + }, +}); diff --git a/frontend/components/ScreenHeader.js b/frontend/components/ScreenHeader.js @@ -0,0 +1,50 @@ +import { View, Text, TouchableOpacity } from "react-native"; +import { MaterialCommunityIcons } from "@expo/vector-icons"; +import styles, { colors } from "../styles"; + +export default function ScreenHeader({ + title, + navigation, + role, + showBack, + hideSearchButton, +}) { + return ( + <View + style={{ + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: 16, + }} + > + {showBack ? ( + <TouchableOpacity onPress={() => navigation.goBack()}> + <MaterialCommunityIcons + name="arrow-left" + size={28} + color={colors.darkGreen} + /> + </TouchableOpacity> + ) : ( + <View style={{ width: 28 }} /> // spacer + )} + + <Text style={styles.title}>{title}</Text> + + {!hideSearchButton ? ( + <TouchableOpacity + onPress={() => navigation.navigate("Search", { fromRole: role })} + > + <MaterialCommunityIcons + name="magnify" + size={28} + color={colors.darkGreen} + /> + </TouchableOpacity> + ) : ( + <View style={{ width: 28 }} /> + )} + </View> + ); +} diff --git a/frontend/components/SearchNav.js b/frontend/components/SearchNav.js @@ -1,45 +0,0 @@ -import { View, TouchableOpacity, Text } from "react-native"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { useNavigation } from "@react-navigation/native"; -import { colors } from "../styles"; - -export default function SearchNav() { - const navigation = useNavigation(); - return ( - <View - style={{ - flexDirection: "row", - justifyContent: "space-between", - marginVertical: 12, - }} - > - <TouchableOpacity - style={{ - flexDirection: "row", - alignItems: "center", - backgroundColor: colors.darkGreen, - padding: 12, - borderRadius: 8, - }} - onPress={() => navigation.navigate("Search")} - > - <MaterialCommunityIcons name="magnify" color="white" size={20} /> - <Text style={{ color: "white", marginLeft: 6 }}>Go to Search</Text> - </TouchableOpacity> - - <TouchableOpacity - style={{ - flexDirection: "row", - alignItems: "center", - backgroundColor: colors.midGreen, - padding: 12, - borderRadius: 8, - }} - onPress={() => navigation.goBack()} - > - <MaterialCommunityIcons name="arrow-left" color="white" size={20} /> - <Text style={{ color: "white", marginLeft: 6 }}>Back</Text> - </TouchableOpacity> - </View> - ); -} diff --git a/frontend/screens/Distributor.js b/frontend/screens/Distributor.js @@ -1,27 +1,39 @@ -// screens/DistributorScreen.js -import React, { useState } from "react"; +import { useState } from "react"; import { + Alert, ScrollView, - View, Text, TextInput, TouchableOpacity, - Alert, + View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { API_BASE } from "../config"; -import styles, { colors } from "../styles"; -import QRScanner from "../components/QRScanner"; +import ScreenHeader from "../components/ScreenHeader"; +import ActionButton from "../components/ActionButton"; +import Scanner from "../components/Scanner"; import LocationPicker from "../components/LocationPicker"; -import SearchNav from "../components/SearchNav"; +import { API_BASE } from "../config"; +import styles from "../styles"; -export default function DistributorScreen() { +export default function DistributorScreen({ navigation, route }) { + const { userId } = route.params; const [active, setActive] = useState(null); - const distributorId = "dist1"; + + // Common const [produceId, setProduceId] = useState(""); - const [location, setLocation] = useState(null); - const [newOwner, setNewOwner] = 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 updateLocation = async () => { try { @@ -30,14 +42,15 @@ export default function DistributorScreen() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ produceId, - actorId: distributorId, + actorId: userId, newLocation: location, }), }); const data = await res.json(); - Alert.alert("Updated", JSON.stringify(data)); - } catch (e) { - Alert.alert("Error", e.message); + if (!res.ok) throw new Error(data.error || "Error"); + Alert.alert("Location Updated", JSON.stringify(data.produce, null, 2)); + } catch (err) { + Alert.alert("Error", err.message); } }; @@ -48,72 +61,175 @@ export default function DistributorScreen() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ produceId, - newOwnerId: newOwner, - qty: 1, - salePrice: 100, + newOwnerId, + qty: parseFloat(qty), + salePrice: parseFloat(salePrice), }), }); const data = await res.json(); - Alert.alert("Transferred", JSON.stringify(data)); - } catch (e) { - Alert.alert("Error", e.message); + if (!res.ok) throw new Error(data.error || "Error"); + Alert.alert("Transferred", JSON.stringify(data.result, null, 2)); + } catch (err) { + Alert.alert("Error", err.message); + } + }; + + const markAsUnavailable = async () => { + try { + const res = await fetch(`${API_BASE}/markAsUnavailable`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + 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", JSON.stringify(data.produce, null, 2)); + } catch (err) { + Alert.alert("Error", err.message); + } + }; + + const updateStorageConditions = async () => { + try { + const res = await fetch(`${API_BASE}/updateStorageConditions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + produceId, + actorId: userId, + storageConditions: storageConditions.split(","), + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Error"); + Alert.alert( + "Updated Storage Conditions", + JSON.stringify(data.produce, null, 2) + ); + } catch (err) { + Alert.alert("Error", err.message); } }; return ( <SafeAreaView style={styles.container}> + <ScreenHeader + title="Distributor Dashboard" + navigation={navigation} + role="Distributor" + /> <ScrollView> - <Text style={styles.title}>🚚 Distributor Dashboard</Text> - <SearchNav /> - <View style={styles.actionGrid}> - <TouchableOpacity - style={styles.actionButton} + <ActionButton + icon="map-marker" + text="Update Location" onPress={() => setActive("location")} - > - <MaterialCommunityIcons name="map-marker" size={20} color="white" /> - <Text style={styles.actionText}>Update Location</Text> - </TouchableOpacity> - <TouchableOpacity - style={styles.actionButton} + /> + <ActionButton + icon="cash" + text="Transfer Ownership" onPress={() => setActive("transfer")} - > - <MaterialCommunityIcons - name="swap-horizontal" - size={20} - color="white" - /> - <Text style={styles.actionText}>Transfer</Text> - </TouchableOpacity> + /> + <ActionButton + icon="cancel" + text="Mark Unavailable" + onPress={() => setActive("remove")} + /> + <ActionButton + icon="thermometer" + text="Update Storage Conditions" + onPress={() => setActive("storage")} + /> </View> {active === "location" && ( <View> - <QRScanner onScanned={setProduceId} placeholder="Produce ID" /> - <LocationPicker onPicked={setLocation} /> + <Scanner value={produceId} onChange={setProduceId} /> + <LocationPicker value={location} onChange={setLocation} /> <TouchableOpacity style={styles.primaryButton} onPress={updateLocation} > - <Text style={styles.buttonText}>Update</Text> + <Text style={styles.buttonText}>Update Location</Text> </TouchableOpacity> </View> )} {active === "transfer" && ( <View> - <QRScanner onScanned={setProduceId} placeholder="Produce ID" /> + <Scanner value={produceId} onChange={setProduceId} /> <TextInput style={styles.input} placeholder="New Owner ID" - value={newOwner} - onChangeText={setNewOwner} + 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</Text> + <Text style={styles.buttonText}>Transfer Ownership</Text> + </TouchableOpacity> + </View> + )} + + {active === "remove" && ( + <View> + <Scanner value={produceId} onChange={setProduceId} /> + <TextInput + style={styles.input} + placeholder="Reason" + value={reason} + onChangeText={setReason} + /> + <TextInput + style={styles.input} + placeholder="New Status (Removed/Missing...)" + value={newStatus} + onChangeText={setNewStatus} + /> + <TouchableOpacity + style={styles.primaryButton} + onPress={markAsUnavailable} + > + <Text style={styles.buttonText}>Mark as Unavailable</Text> + </TouchableOpacity> + </View> + )} + + {active === "storage" && ( + <View> + <Scanner value={produceId} onChange={setProduceId} /> + <TextInput + style={styles.input} + placeholder="Enter Storage Conditions (comma separated)" + value={storageConditions} + onChangeText={setStorageConditions} + /> + <TouchableOpacity + style={styles.primaryButton} + onPress={updateStorageConditions} + > + <Text style={styles.buttonText}>Update Storage Conditions</Text> </TouchableOpacity> </View> )} diff --git a/frontend/screens/Farmer.js b/frontend/screens/Farmer.js @@ -1,37 +1,39 @@ import { useState } from "react"; import { + Alert, ScrollView, - View, Text, TextInput, TouchableOpacity, - Alert, + View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { API_BASE } from "../config"; -import styles, { colors } from "../styles"; -import QRScanner from "../components/QRScanner"; +import ScreenHeader from "../components/ScreenHeader"; +import ActionButton from "../components/ActionButton"; +import Scanner from "../components/Scanner"; import LocationPicker from "../components/LocationPicker"; -import SearchNav from "../components/SearchNav"; +import { API_BASE } from "../config"; +import styles from "../styles"; -export default function FarmerScreen() { +export default function FarmerScreen({ navigation, route }) { + const { userId } = route.params; const [active, setActive] = useState(null); - const farmerId = "farmer1"; + // Common + const [produceId, setProduceId] = useState(""); + const [location, setLocation] = useState(""); + + // registerProduce const [cropType, setCropType] = useState(""); const [qty, setQty] = useState(""); const [qtyUnit, setQtyUnit] = useState("KG"); + const [pricePerUnit, setPricePerUnit] = useState(""); const [harvestDate, setHarvestDate] = useState(""); const [quality, setQuality] = useState(""); const [expiryDate, setExpiryDate] = useState(""); const [storageConditions, setStorageConditions] = useState(""); - const [location, setLocation] = useState(null); - const [produceId, setProduceId] = useState(""); - const [pricePerUnit, setPricePerUnit] = useState(""); - const [storageUpdate, setStorageUpdate] = useState(""); - const [splitId, setSplitId] = useState(""); + // splitProduce const [splitQty, setSplitQty] = useState(""); const registerProduce = async () => { @@ -40,23 +42,25 @@ export default function FarmerScreen() { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - farmerId, + farmerId: userId, details: { cropType, - qty, + qty: parseFloat(qty), qtyUnit, + pricePerUnit: parseFloat(pricePerUnit), harvestDate, quality, expiryDate, - storageConditions, - currentLocation: location || "Unknown", + storageConditions: storageConditions.split(","), + location, }, }), }); const data = await res.json(); - Alert.alert("Success", JSON.stringify(data)); - } catch (e) { - Alert.alert("Error", e.message); + if (!res.ok) throw new Error(data.error || "Error"); + Alert.alert("Registered", JSON.stringify(data.produce, null, 2)); + } catch (err) { + Alert.alert("Error", err.message); } }; @@ -67,14 +71,18 @@ export default function FarmerScreen() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ produceId, - actorId: farmerId, - details: { pricePerUnit, storageConditions: storageUpdate }, + actorId: userId, + details: { + pricePerUnit: parseFloat(pricePerUnit), + storageConditions: storageConditions.split(","), + }, }), }); const data = await res.json(); - Alert.alert("Updated", JSON.stringify(data)); - } catch (e) { - Alert.alert("Error", e.message); + if (!res.ok) throw new Error(data.error || "Error"); + Alert.alert("Updated", JSON.stringify(data.produce, null, 2)); + } catch (err) { + Alert.alert("Error", err.message); } }; @@ -84,50 +92,67 @@ export default function FarmerScreen() { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - produceId: splitId, - qty: splitQty, - OwnerId: farmerId, + produceId, + qty: parseFloat(splitQty), + ownerId: userId, }), }); const data = await res.json(); - Alert.alert("Split Done", JSON.stringify(data)); - } catch (e) { - Alert.alert("Error", e.message); + if (!res.ok) throw new Error(data.error || "Error"); + Alert.alert("Split", JSON.stringify(data.result, null, 2)); + } catch (err) { + Alert.alert("Error", err.message); + } + }; + + const updateLocation = async () => { + try { + const res = await fetch(`${API_BASE}/updateLocation`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + produceId, + actorId: userId, + newLocation: location, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Error"); + Alert.alert("Moved", JSON.stringify(data.produce, null, 2)); + } catch (err) { + Alert.alert("Error", err.message); } }; return ( <SafeAreaView style={styles.container}> + <ScreenHeader + title="Farmer Dashboard" + navigation={navigation} + role="Farmer" + /> <ScrollView> - <Text style={styles.title}>👨‍🌾 Farmer Dashboard</Text> - <SearchNav /> - <View style={styles.actionGrid}> - <TouchableOpacity - style={styles.actionButton} + <ActionButton + icon="plus" + text="Register Produce" onPress={() => setActive("register")} - > - <MaterialCommunityIcons name="plus-box" size={20} color="white" /> - <Text style={styles.actionText}>Register</Text> - </TouchableOpacity> - <TouchableOpacity - style={styles.actionButton} + /> + <ActionButton + icon="update" + text="Update Details" onPress={() => setActive("update")} - > - <MaterialCommunityIcons name="pencil" size={20} color="white" /> - <Text style={styles.actionText}>Update</Text> - </TouchableOpacity> - <TouchableOpacity - style={styles.actionButton} + /> + <ActionButton + icon="call-split" + text="Split Produce" onPress={() => setActive("split")} - > - <MaterialCommunityIcons - name="content-cut" - size={20} - color="white" - /> - <Text style={styles.actionText}>Split</Text> - </TouchableOpacity> + /> + <ActionButton + icon="map-marker" + text="Update Location" + onPress={() => setActive("location")} + /> </View> {active === "register" && ( @@ -153,7 +178,14 @@ export default function FarmerScreen() { /> <TextInput style={styles.input} - placeholder="Harvest Date" + placeholder="Price Per Unit" + keyboardType="numeric" + value={pricePerUnit} + onChangeText={setPricePerUnit} + /> + <TextInput + style={styles.input} + placeholder="Harvest Date (YYYY-MM-DD)" value={harvestDate} onChangeText={setHarvestDate} /> @@ -165,57 +197,57 @@ export default function FarmerScreen() { /> <TextInput style={styles.input} - placeholder="Expiry Date" + placeholder="Expiry Date (YYYY-MM-DD)" value={expiryDate} onChangeText={setExpiryDate} /> <TextInput style={styles.input} - placeholder="Storage Conditions" + placeholder="Storage Conditions (comma separated)" value={storageConditions} onChangeText={setStorageConditions} /> - <LocationPicker onPicked={setLocation} /> + <LocationPicker value={location} onChange={setLocation} /> <TouchableOpacity style={styles.primaryButton} onPress={registerProduce} > - <Text style={styles.buttonText}>Submit</Text> + <Text style={styles.buttonText}>Submit Registration</Text> </TouchableOpacity> </View> )} {active === "update" && ( <View> - <QRScanner onScanned={setProduceId} placeholder="Produce ID" /> + <Scanner value={produceId} onChange={setProduceId} /> <TextInput style={styles.input} - placeholder="Price/Unit" + placeholder="New Price Per Unit" keyboardType="numeric" value={pricePerUnit} onChangeText={setPricePerUnit} /> <TextInput style={styles.input} - placeholder="Storage Conditions" - value={storageUpdate} - onChangeText={setStorageUpdate} + placeholder="Storage Conditions (comma separated)" + value={storageConditions} + onChangeText={setStorageConditions} /> <TouchableOpacity style={styles.primaryButton} onPress={updateDetails} > - <Text style={styles.buttonText}>Update</Text> + <Text style={styles.buttonText}>Update Details</Text> </TouchableOpacity> </View> )} {active === "split" && ( <View> - <QRScanner onScanned={setSplitId} placeholder="Produce ID" /> + <Scanner value={produceId} onChange={setProduceId} /> <TextInput style={styles.input} - placeholder="Qty to Split" + placeholder="Quantity to Split" keyboardType="numeric" value={splitQty} onChangeText={setSplitQty} @@ -224,7 +256,20 @@ export default function FarmerScreen() { style={styles.primaryButton} onPress={splitProduce} > - <Text style={styles.buttonText}>Split</Text> + <Text style={styles.buttonText}>Split Produce</Text> + </TouchableOpacity> + </View> + )} + + {active === "location" && ( + <View> + <Scanner value={produceId} onChange={setProduceId} /> + <LocationPicker value={location} onChange={setLocation} /> + <TouchableOpacity + style={styles.primaryButton} + onPress={updateLocation} + > + <Text style={styles.buttonText}>Update Location</Text> </TouchableOpacity> </View> )} diff --git a/frontend/screens/Inspector.js b/frontend/screens/Inspector.js @@ -1,28 +1,39 @@ -// screens/InspectorScreen.js -import React, { useState } from "react"; +import { useState } from "react"; import { + Alert, ScrollView, - View, Text, TextInput, TouchableOpacity, - Alert, + View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; +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 styles, { colors } from "../styles"; -import QRScanner from "../components/QRScanner"; -import SearchNav from "../components/SearchNav"; +import styles from "../styles"; -export default function InspectorScreen() { +export default function InspectorScreen({ navigation, route }) { + const { userId } = route.params; const [active, setActive] = useState(null); - const inspectorId = "insp1"; + + // Common const [produceId, setProduceId] = useState(""); + const [location, setLocation] = useState(""); + + // inspectProduce const [quality, setQuality] = useState(""); const [expiryDate, setExpiryDate] = useState(""); + const [storageConditions, setStorageConditions] = useState(""); + const [failed, setFailed] = useState(false); const [reason, setReason] = useState(""); + // markAsUnavailable + const [unavailReason, setUnavailReason] = useState(""); + const [newStatus, setNewStatus] = useState(""); + const inspectProduce = async () => { try { const res = await fetch(`${API_BASE}/inspectProduce`, { @@ -30,105 +41,173 @@ export default function InspectorScreen() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ produceId, - inspectorId, - qualityUpdate: { quality, expiryDate }, + inspectorId: userId, + qualityUpdate: { + quality, + expiryDate, + storageConditions: storageConditions.split(","), + failed, + reason, + }, }), }); const data = await res.json(); - Alert.alert("Inspected", JSON.stringify(data)); - } catch (e) { - Alert.alert("Error", e.message); + if (!res.ok) throw new Error(data.error || "Error"); + Alert.alert("Inspection Complete", JSON.stringify(data.produce, null, 2)); + } catch (err) { + Alert.alert("Error", err.message); } }; - const markUnavailable = async () => { + const updateLocation = async () => { + try { + const res = await fetch(`${API_BASE}/updateLocation`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + produceId, + actorId: userId, + newLocation: location, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Error"); + Alert.alert("Location Updated", JSON.stringify(data.produce, null, 2)); + } catch (err) { + Alert.alert("Error", err.message); + } + }; + + const markAsUnavailable = async () => { try { const res = await fetch(`${API_BASE}/markAsUnavailable`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ produceId, - actorId: inspectorId, - reason, - newStatus: "Removed", + actorId: userId, + reason: unavailReason, + newStatus, }), }); const data = await res.json(); - Alert.alert("Marked", JSON.stringify(data)); - } catch (e) { - Alert.alert("Error", e.message); + if (!res.ok) throw new Error(data.error || "Error"); + Alert.alert("Marked Unavailable", JSON.stringify(data.produce, null, 2)); + } catch (err) { + Alert.alert("Error", err.message); } }; return ( <SafeAreaView style={styles.container}> + <ScreenHeader + title="Inspector Dashboard" + navigation={navigation} + role="Inspector" + /> <ScrollView> - <Text style={styles.title}>🕵️ Inspector Dashboard</Text> - <SearchNav /> - <View style={styles.actionGrid}> - <TouchableOpacity - style={styles.actionButton} + <ActionButton + icon="check-decagram" + text="Inspect Produce" onPress={() => setActive("inspect")} - > - <MaterialCommunityIcons - name="check-decagram" - size={20} - color="white" - /> - <Text style={styles.actionText}>Inspect</Text> - </TouchableOpacity> - <TouchableOpacity - style={styles.actionButton} + /> + <ActionButton + icon="map-marker" + text="Update Location" + onPress={() => setActive("location")} + /> + <ActionButton + icon="cancel" + text="Mark Unavailable" onPress={() => setActive("remove")} - > - <MaterialCommunityIcons - name="close-octagon" - size={20} - color="white" - /> - <Text style={styles.actionText}>Mark Unavailable</Text> - </TouchableOpacity> + /> </View> {active === "inspect" && ( <View> - <QRScanner onScanned={setProduceId} placeholder="Produce ID" /> + <Scanner value={produceId} onChange={setProduceId} /> <TextInput style={styles.input} - placeholder="Quality" + placeholder="Quality (e.g., Grade A)" value={quality} onChangeText={setQuality} /> <TextInput style={styles.input} - placeholder="Expiry Date" + placeholder="Expiry Date (YYYY-MM-DD)" value={expiryDate} onChangeText={setExpiryDate} /> + <TextInput + style={styles.input} + placeholder="Storage Conditions (comma separated)" + value={storageConditions} + onChangeText={setStorageConditions} + /> + <TextInput + style={styles.input} + placeholder="Reason (if failed)" + value={reason} + onChangeText={setReason} + /> + <TouchableOpacity + style={[ + styles.primaryButton, + { + backgroundColor: failed + ? "red" + : styles.primaryButton.backgroundColor, + }, + ]} + onPress={() => setFailed(!failed)} + > + <Text style={styles.buttonText}> + {failed ? "Mark as Passed" : "Mark as Failed"} + </Text> + </TouchableOpacity> <TouchableOpacity style={styles.primaryButton} onPress={inspectProduce} > - <Text style={styles.buttonText}>Inspect</Text> + <Text style={styles.buttonText}>Submit Inspection</Text> + </TouchableOpacity> + </View> + )} + + {active === "location" && ( + <View> + <Scanner value={produceId} onChange={setProduceId} /> + <LocationPicker value={location} onChange={setLocation} /> + <TouchableOpacity + style={styles.primaryButton} + onPress={updateLocation} + > + <Text style={styles.buttonText}>Update Location</Text> </TouchableOpacity> </View> )} {active === "remove" && ( <View> - <QRScanner onScanned={setProduceId} placeholder="Produce ID" /> + <Scanner value={produceId} onChange={setProduceId} /> <TextInput style={styles.input} placeholder="Reason" - value={reason} - onChangeText={setReason} + value={unavailReason} + onChangeText={setUnavailReason} + /> + <TextInput + style={styles.input} + placeholder="New Status (Failed Inspection, Removed...)" + value={newStatus} + onChangeText={setNewStatus} /> <TouchableOpacity style={styles.primaryButton} - onPress={markUnavailable} + onPress={markAsUnavailable} > - <Text style={styles.buttonText}>Remove</Text> + <Text style={styles.buttonText}>Mark as Unavailable</Text> </TouchableOpacity> </View> )} diff --git a/frontend/screens/Login.js b/frontend/screens/Login.js @@ -1,37 +1,50 @@ -import { View, Text, TouchableOpacity, Image } from "react-native"; +import { useState } 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"; -export default function LoginScreen({ navigation }) { - const roles = ["Farmer", "Distributor", "Retailer", "Inspector", "Search"]; +export default function LoginScreen({ route, navigation }) { + const { role } = route.params; + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + + const handleLogin = () => { + if (!username || !password) { + Alert.alert("Error", "Enter username and password"); + return; + } + navigation.replace(role, { userId: `${role.toLowerCase()}1` }); + }; return ( - <View style={styles.container}> - <View style={styles.headerBar}> - <Text style={styles.headerTitle}>Matiru.</Text> - </View> + <SafeAreaView style={styles.container}> + <Text style={styles.title}>Login as {role}</Text> + + <TextInput + style={styles.input} + placeholder="Username" + value={username} + onChangeText={setUsername} + /> + <TextInput + style={styles.input} + placeholder="Password" + secureTextEntry + value={password} + onChangeText={setPassword} + /> - <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}> - {/* <Image - source={require("../assets/logo.png")} - style={{ - width: 180, - height: 280, - resizeMode: "cover", - borderRadius: 12, - }} - /> */} - <View style={{ marginTop: 20, width: "90%", alignItems: "center" }}> - {roles.map((r) => ( - <TouchableOpacity - key={r} - style={[styles.button, { backgroundColor: colors.midGreen }]} - onPress={() => navigation.navigate(r)} - > - <Text style={styles.buttonText}>{r}</Text> - </TouchableOpacity> - ))} - </View> - </View> - </View> + <TouchableOpacity + style={[ + styles.bigButton, + { backgroundColor: colors.darkGreen, width: "100%" }, + ]} + onPress={handleLogin} + > + <MaterialCommunityIcons name="login" size={24} color="white" /> + <Text style={styles.bigButtonText}>Login</Text> + </TouchableOpacity> + </SafeAreaView> ); } diff --git a/frontend/screens/Retailer.js b/frontend/screens/Retailer.js @@ -1,26 +1,36 @@ -// screens/RetailerScreen.js -import React, { useState } from "react"; +import { useState } from "react"; import { + Alert, ScrollView, - View, Text, TextInput, TouchableOpacity, - Alert, + View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import { MaterialCommunityIcons } from "@expo/vector-icons"; -import { API_BASE } from "../config"; -import styles, { colors } from "../styles"; -import QRScanner from "../components/QRScanner"; +import ScreenHeader from "../components/ScreenHeader"; +import ActionButton from "../components/ActionButton"; +import Scanner from "../components/Scanner"; import LocationPicker from "../components/LocationPicker"; -import SearchNav from "../components/SearchNav"; +import { API_BASE } from "../config"; +import styles from "../styles"; -export default function RetailerScreen() { +export default function RetailerScreen({ navigation, route }) { + const { userId } = route.params; const [active, setActive] = useState(null); - const retailerId = "ret1"; + + // Common const [produceId, setProduceId] = useState(""); - const [location, setLocation] = useState(null); + 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(""); const updateLocation = async () => { try { @@ -29,77 +39,149 @@ export default function RetailerScreen() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ produceId, - actorId: retailerId, + actorId: userId, newLocation: location, }), }); const data = await res.json(); - Alert.alert("Updated", JSON.stringify(data)); - } catch (e) { - Alert.alert("Error", e.message); + if (!res.ok) throw new Error(data.error || "Error"); + Alert.alert("Location Updated", JSON.stringify(data.produce, null, 2)); + } catch (err) { + Alert.alert("Error", err.message); } }; - const markSale = async () => { + const transferOwnership = async () => { try { const res = await fetch(`${API_BASE}/transferOwnership`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ produceId, - newOwnerId: "consumer", - qty: 1, - salePrice: 200, + newOwnerId, + qty: parseFloat(qty), + salePrice: parseFloat(salePrice), + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Error"); + Alert.alert("Sale Recorded", JSON.stringify(data.result, null, 2)); + } catch (err) { + Alert.alert("Error", err.message); + } + }; + + const markAsUnavailable = async () => { + try { + const res = await fetch(`${API_BASE}/markAsUnavailable`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + produceId, + actorId: userId, + reason, + newStatus, }), }); const data = await res.json(); - Alert.alert("Sold", JSON.stringify(data)); - } catch (e) { - Alert.alert("Error", e.message); + if (!res.ok) throw new Error(data.error || "Error"); + Alert.alert("Marked Unavailable", JSON.stringify(data.produce, null, 2)); + } catch (err) { + Alert.alert("Error", err.message); } }; return ( <SafeAreaView style={styles.container}> + <ScreenHeader + title="Retailer Dashboard" + navigation={navigation} + role="Retailer" + /> <ScrollView> - <Text style={styles.title}>🏬 Retailer Dashboard</Text> - <SearchNav /> - <View style={styles.actionGrid}> - <TouchableOpacity - style={styles.actionButton} + <ActionButton + icon="map-marker" + text="Update Location" onPress={() => setActive("location")} - > - <MaterialCommunityIcons name="map-marker" size={20} color="white" /> - <Text style={styles.actionText}>Update Location</Text> - </TouchableOpacity> - <TouchableOpacity - style={styles.actionButton} - onPress={() => setActive("sale")} - > - <MaterialCommunityIcons name="cart" size={20} color="white" /> - <Text style={styles.actionText}>Mark Sale</Text> - </TouchableOpacity> + /> + <ActionButton + icon="cash-register" + text="Record Sale" + onPress={() => setActive("transfer")} + /> + <ActionButton + icon="cancel" + text="Mark Unavailable" + onPress={() => setActive("remove")} + /> </View> {active === "location" && ( <View> - <QRScanner onScanned={setProduceId} placeholder="Produce ID" /> - <LocationPicker onPicked={setLocation} /> + <Scanner value={produceId} onChange={setProduceId} /> + <LocationPicker value={location} onChange={setLocation} /> <TouchableOpacity style={styles.primaryButton} onPress={updateLocation} > - <Text style={styles.buttonText}>Update</Text> + <Text style={styles.buttonText}>Update Location</Text> + </TouchableOpacity> + </View> + )} + + {active === "transfer" && ( + <View> + <Scanner value={produceId} onChange={setProduceId} /> + <TextInput + style={styles.input} + placeholder="Customer/User ID" + value={newOwnerId} + onChangeText={setNewOwnerId} + /> + <TextInput + style={styles.input} + placeholder="Quantity Sold" + 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}>Record Sale</Text> </TouchableOpacity> </View> )} - {active === "sale" && ( + {active === "remove" && ( <View> - <QRScanner onScanned={setProduceId} placeholder="Produce ID" /> - <TouchableOpacity style={styles.primaryButton} onPress={markSale}> - <Text style={styles.buttonText}>Sell</Text> + <Scanner value={produceId} onChange={setProduceId} /> + <TextInput + style={styles.input} + placeholder="Reason" + value={reason} + onChangeText={setReason} + /> + <TextInput + style={styles.input} + placeholder="New Status (Expired, Spoiled...)" + value={newStatus} + onChangeText={setNewStatus} + /> + <TouchableOpacity + style={styles.primaryButton} + onPress={markAsUnavailable} + > + <Text style={styles.buttonText}>Mark as Unavailable</Text> </TouchableOpacity> </View> )} diff --git a/frontend/screens/Search.js b/frontend/screens/Search.js @@ -5,106 +5,172 @@ */ import { useState } from "react"; -import { View, Text, TouchableOpacity, Alert, ScrollView } from "react-native"; -import styles, { colors } from "../styles"; +import { + Alert, + ScrollView, + Text, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import ScreenHeader from "../components/ScreenHeader"; +import Scanner from "../components/Scanner"; import { API_BASE } from "../config"; -import Scanner from "../components/QRScanner"; +import { MaterialCommunityIcons } from "@expo/vector-icons"; +import styles, { colors } from "../styles"; -export default function SearchScreen() { - const [mode, setMode] = useState("produce"); // produce | owner | user - const [queryId, setQueryId] = useState(""); - const [result, setResult] = useState(null); +const tabs = [ + { key: "produce", label: "Produce", icon: "leaf" }, + { key: "owner", label: "Owner", icon: "account" }, + { key: "user", label: "User", icon: "account-badge" }, +]; - const searchProduce = async (id) => { - try { - const res = await fetch(`${API_BASE}/getProduce/${id}`); - if (!res.ok) throw new Error("Not found"); - const data = await res.json(); - setResult({ type: "produce", data: data.produce }); - } catch (err) { - Alert.alert("Error", err.message); - setResult(null); - } - }; - - const searchOwner = async (id) => { - try { - const res = await fetch(`${API_BASE}/getOwner/${id}`); - const data = await res.json(); - setResult({ type: "owner", data: data.produces }); - } catch (err) { - Alert.alert("Error", err.message); - setResult(null); - } - }; +export default function SearchScreen({ navigation }) { + const [activeTab, setActiveTab] = useState("produce"); + const [produceId, setProduceId] = useState(""); + const [ownerId, setOwnerId] = useState(""); + const [userKey, setUserKey] = useState(""); + const [result, setResult] = useState(null); - const getUser = async (id) => { + const fetchResult = async (type, id) => { try { - const res = await fetch(`${API_BASE}/getUser/${id}`); + if (!id) { + Alert.alert("Error", "Please enter an ID"); + return; + } + const url = + type === "produce" + ? `${API_BASE}/getProduce/${id}` + : type === "owner" + ? `${API_BASE}/getProduceByOwner/${id}` + : `${API_BASE}/getUser/${id}`; + const res = await fetch(url); + if (!res.ok) throw new Error(`Server ${res.status}`); const data = await res.json(); - setResult({ type: "user", data: data.user }); + setResult(data); } catch (err) { Alert.alert("Error", err.message); - setResult(null); } }; - const handleSearch = (id) => { - if (!id) return Alert.alert("Error", "ID required"); - setQueryId(id); - if (mode === "produce") searchProduce(id); - if (mode === "owner") searchOwner(id); - if (mode === "user") getUser(id); - }; - return ( - <ScrollView style={styles.container}> - <Text style={styles.title}>Global Search</Text> + <SafeAreaView style={styles.container}> + <ScreenHeader + title="Global Search" + navigation={navigation} + role="Search" + showBack={true} + hideSearchButton={true} + /> + + <Text + style={{ + marginVertical: 12, + color: colors.darkGreen, + textAlign: "center", + }} + > + To view information, select a tab and enter the ID + </Text> <View style={{ flexDirection: "row", justifyContent: "space-around", - marginVertical: 10, + marginBottom: 16, }} > - {["produce", "owner", "user"].map((m) => ( + {tabs.map((t) => ( <TouchableOpacity - key={m} - style={[ - styles.actionButton, - { - backgroundColor: - mode === m ? colors.midGreen : colors.lightGreen, - width: "30%", - }, - ]} + key={t.key} + style={{ + flexDirection: "row", + alignItems: "center", + paddingVertical: 10, + paddingHorizontal: 14, + borderRadius: 12, + backgroundColor: + activeTab === t.key ? colors.darkGreen : colors.lightGreen, + }} onPress={() => { - setMode(m); + setActiveTab(t.key); setResult(null); }} > - <Text style={styles.actionText}>{m.toUpperCase()}</Text> + <MaterialCommunityIcons + name={t.icon} + size={20} + color={activeTab === t.key ? "white" : colors.darkGreen} + /> + <Text + style={{ + color: activeTab === t.key ? "white" : colors.darkGreen, + marginLeft: 6, + fontWeight: "600", + }} + > + {t.label} + </Text> </TouchableOpacity> ))} </View> - <Scanner onScanned={handleSearch} placeholder={`Enter ${mode} ID`} /> + <ScrollView> + {activeTab === "produce" ? ( + <Scanner value={produceId} onChange={setProduceId} /> + ) : activeTab === "owner" ? ( + <TextInput + style={styles.input} + placeholder="Enter Owner ID" + value={ownerId} + onChangeText={setOwnerId} + /> + ) : ( + <TextInput + style={styles.input} + placeholder="Enter User Key" + value={userKey} + onChangeText={setUserKey} + /> + )} - <View style={{ marginTop: 20 }}> - <Text style={{ fontWeight: "700", color: colors.darkGreen }}> - Result - </Text> - {result ? ( - <View style={styles.card}> - <Text style={{ color: colors.darkGreen }}> - {JSON.stringify(result.data, null, 2)} - </Text> + <TouchableOpacity + style={styles.primaryButton} + onPress={() => + fetchResult( + activeTab, + activeTab === "produce" + ? produceId + : activeTab === "owner" + ? ownerId + : userKey + ) + } + > + <Text style={styles.buttonText}> + Search {activeTab.charAt(0).toUpperCase() + activeTab.slice(1)} + </Text> + </TouchableOpacity> + + {result && ( + <View + style={{ + marginTop: 20, + padding: 10, + backgroundColor: "white", + borderRadius: 10, + }} + > + <Text style={styles.subtitle}>Search Results:</Text> + <ScrollView horizontal> + <Text style={{ fontSize: 12 }}> + {JSON.stringify(result, null, 2)} + </Text> + </ScrollView> </View> - ) : ( - <Text style={{ color: "#666", marginTop: 8 }}>No results</Text> )} - </View> - </ScrollView> + </ScrollView> + </SafeAreaView> ); } diff --git a/frontend/styles.js b/frontend/styles.js @@ -15,12 +15,12 @@ export default StyleSheet.create({ flex: 1, backgroundColor: colors.cream, padding: 16, - paddingTop: StatusBar.currentHeight || 0, // safe top padding + paddingTop: StatusBar.currentHeight || 0, }, bg: { flex: 1, - width: width, - height: height, + width, + height, resizeMode: "cover", }, overlay: { @@ -50,6 +50,19 @@ export default StyleSheet.create({ marginBottom: 20, textAlign: "center", }, + brand: { + color: colors.accent, + fontSize: 40, + fontWeight: "bold", + textAlign: "center", + }, + title: { + fontSize: 24, + fontWeight: "bold", + color: colors.darkGreen, + marginBottom: 16, + textAlign: "center", + }, bigButton: { flexDirection: "row", alignItems: "center", @@ -64,7 +77,7 @@ export default StyleSheet.create({ shadowOpacity: 0.2, shadowOffset: { width: 0, height: 3 }, shadowRadius: 5, - elevation: 4, // Android shadow + elevation: 4, }, bigButtonText: { color: "white", @@ -72,19 +85,6 @@ export default StyleSheet.create({ fontWeight: "600", marginLeft: 10, }, - brand: { - color: colors.accent, - fontSize: 40, - fontWeight: "bold", - textAlign: "center", - }, - title: { - fontSize: 24, - fontWeight: "bold", - color: colors.darkGreen, - marginBottom: 16, - textAlign: "center", - }, actionGrid: { flexDirection: "row", flexWrap: "wrap", @@ -95,11 +95,11 @@ export default StyleSheet.create({ flexDirection: "row", alignItems: "center", backgroundColor: colors.midGreen, - paddingVertical: 14, + paddingVertical: 16, paddingHorizontal: 20, margin: 8, borderRadius: 12, - flexBasis: "40%", + flexBasis: "40%", // 2 per row justifyContent: "center", shadowColor: "#000", shadowOpacity: 0.15, @@ -123,19 +123,17 @@ export default StyleSheet.create({ backgroundColor: "white", fontSize: 16, }, - button: { - padding: 16, - borderRadius: 12, - alignItems: "center", - marginTop: 12, - backgroundColor: colors.darkGreen, - }, primaryButton: { backgroundColor: colors.darkGreen, paddingVertical: 16, borderRadius: 14, alignItems: "center", marginVertical: 12, + shadowColor: "#000", + shadowOpacity: 0.2, + shadowOffset: { width: 0, height: 2 }, + shadowRadius: 4, + elevation: 3, }, buttonText: { color: "white",