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:
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",