matiru
Blockchain-Based Supply Chain Transparency for Agricultural Producegit clone https://github.com/maydayv7/matiru
frontend/screens/Search.js (11529B)
1import { useState } from "react";
2import {
3 ActivityIndicator,
4 Alert,
5 ScrollView,
6 StyleSheet,
7 Text,
8 TextInput,
9 TouchableOpacity,
10 View,
11} from "react-native";
12import {
13 SafeAreaView,
14 useSafeAreaInsets,
15} from "react-native-safe-area-context";
16import RNPickerSelect from "react-native-picker-select";
17import { MaterialCommunityIcons } from "@expo/vector-icons";
18
19import ScreenHeader from "../components/ScreenHeader";
20import Scanner from "../components/Scanner";
21import ProduceCard from "../components/ProduceCard";
22import DetailRow from "../components/DetailRow";
23
24import { api } from "../services/api";
25import styles, { colors } from "../styles";
26
27export default function SearchScreen({ navigation }) {
28 const insets = useSafeAreaInsets();
29 const [tab, setTab] = useState("Produce");
30 const [loading, setLoading] = useState(false);
31 const [result, setResult] = useState(null);
32 const [produceId, setProduceId] = useState("");
33 const [userRole, setUserRole] = useState("FARMER");
34 const [userId, setUserId] = useState("");
35
36 const fetchResult = async () => {
37 const isProduceSearch = tab === "Produce";
38 const searchInput = isProduceSearch ? produceId : userId;
39 if (!searchInput.trim())
40 return Alert.alert("Invalid Input", "Please enter a valid ID to search");
41
42 try {
43 setLoading(true);
44 setResult(null);
45 let data;
46 if (isProduceSearch) {
47 data = await api.getProduceById(produceId.trim());
48 } else {
49 const userKey = `${userRole}-${userId.trim()}`;
50 data = await api.getUserDetails(userKey);
51 }
52 setResult(data);
53 } catch (err) {
54 Alert.alert("Error", err.message);
55 setResult(null);
56 } finally {
57 setLoading(false);
58 }
59 };
60
61 const renderTimeline = (produce) => (
62 <View>
63 <Text style={local.subtitle}>Journey Timeline</Text>
64 {produce.actionHistory
65 ?.slice()
66 .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
67 .map((a, idx) => (
68 <View key={idx} style={local.timelineItem}>
69 <MaterialCommunityIcons
70 name={
71 a.action === "REGISTER"
72 ? "sprout"
73 : a.action === "MOVE"
74 ? "truck-fast"
75 : a.action === "SALE"
76 ? "swap-horizontal-bold"
77 : a.action === "INSPECT"
78 ? "check-decagram"
79 : a.action === "SPLIT"
80 ? "call-split"
81 : a.action === "UPDATED"
82 ? "pencil"
83 : "close-circle"
84 }
85 size={24}
86 color={
87 a.action === "REMOVED"
88 ? colors.danger
89 : a.action === "SALE"
90 ? colors.midGreen
91 : colors.darkGreen
92 }
93 style={{ marginRight: 12 }}
94 />
95 <View style={{ flex: 1 }}>
96 <Text style={{ fontWeight: "600", textTransform: "capitalize" }}>
97 {a.action.toLowerCase()}
98 </Text>
99 <Text style={local.small}>
100 {a.action === "SALE" ? "To" : "By"}: {a.currentOwner}
101 </Text>
102 <Text style={local.small}>
103 On: {new Date(a.timestamp).toLocaleString()}
104 </Text>
105 <Text style={local.small}>
106 Location: {a.currentLocation || "N/A"}
107 </Text>
108 </View>
109 </View>
110 ))}
111 </View>
112 );
113
114 const renderUser = (user) => (
115 <View style={local.card}>
116 <View style={local.userHeader}>
117 <View style={local.userAvatar}>
118 <MaterialCommunityIcons
119 name="account-circle"
120 size={40}
121 color={colors.darkGreen}
122 />
123 </View>
124 <View>
125 <Text style={local.title}>{user.name || "Unnamed User"}</Text>
126 <View style={local.roleBadge}>
127 <Text style={local.roleText}>{user.role}</Text>
128 </View>
129 </View>
130 </View>
131 <Text style={local.subtitle}>User Details</Text>
132 <DetailRow icon="identifier" label="User ID" value={user.id} />
133 {user.location && (
134 <DetailRow icon="map-marker" label="Location" value={user.location} />
135 )}
136 {user.walletId && (
137 <DetailRow icon="wallet" label="Wallet ID" value={user.walletId} />
138 )}
139 {user.certification?.length > 0 && (
140 <DetailRow
141 icon="certificate"
142 label="Certifications"
143 value={user.certification.join(", ")}
144 />
145 )}
146 </View>
147 );
148
149 return (
150 <SafeAreaView style={styles.container} edges={["top", "left", "right"]}>
151 <ScrollView
152 style={{ flex: 1 }}
153 contentContainerStyle={{
154 flexGrow: 1,
155 paddingBottom: insets.bottom,
156 }}
157 keyboardShouldPersistTaps="handled"
158 >
159 <ScreenHeader
160 title="Global Search"
161 navigation={navigation}
162 hideSearchButton={true}
163 hideNotificationsButton={true}
164 showBack={true}
165 />
166 <View style={local.searchContainer}>
167 <Text style={local.searchDescription}>
168 Search for information about any produce or user in the supply
169 chain.
170 </Text>
171 <View style={local.tabContainer}>
172 {["Produce", "User"].map((t) => (
173 <TouchableOpacity
174 key={t}
175 style={[
176 local.tabButton,
177 {
178 backgroundColor:
179 tab === t ? colors.darkGreen : colors.lightGreen,
180 },
181 ]}
182 onPress={() => {
183 setTab(t);
184 setResult(null);
185 setProduceId("");
186 setUserId("");
187 setUserRole("FARMER");
188 }}
189 >
190 <MaterialCommunityIcons
191 name={t === "Produce" ? "leaf" : "account-badge"}
192 size={20}
193 color={tab === t ? "white" : colors.darkGreen}
194 />
195 <Text
196 style={[
197 local.tabText,
198 { color: tab === t ? "white" : colors.darkGreen },
199 ]}
200 >
201 {t}
202 </Text>
203 </TouchableOpacity>
204 ))}
205 </View>
206 {tab === "Produce" ? (
207 <Scanner value={produceId} onChange={setProduceId} />
208 ) : (
209 <View>
210 <Text style={local.detailLabel}>1. Select Role</Text>
211 <RNPickerSelect
212 onValueChange={(value) => setUserRole(value)}
213 items={[
214 { label: "Farmer", value: "FARMER" },
215 { label: "Distributor", value: "DISTRIBUTOR" },
216 { label: "Retailer", value: "RETAILER" },
217 { label: "Inspector", value: "INSPECTOR" },
218 ]}
219 style={pickerSelectStyles}
220 value={userRole}
221 useNativeAndroidPickerStyle={false}
222 placeholder={{}}
223 />
224 <Text style={[local.detailLabel, { marginTop: 12 }]}>
225 2. Enter User ID
226 </Text>
227 <TextInput
228 style={styles.input}
229 placeholder="e.g. farmer1, dist1"
230 value={userId}
231 onChangeText={setUserId}
232 />
233 </View>
234 )}
235 <TouchableOpacity
236 style={[styles.primaryButton, { marginTop: 16 }]}
237 onPress={fetchResult}
238 disabled={loading}
239 >
240 {loading ? (
241 <ActivityIndicator color="white" />
242 ) : (
243 <Text style={styles.buttonText}>Search</Text>
244 )}
245 </TouchableOpacity>
246 </View>
247 {loading && (
248 <ActivityIndicator
249 size="large"
250 color={colors.darkGreen}
251 style={{ marginTop: 30 }}
252 />
253 )}
254 {!loading && result && (
255 <View style={{ marginTop: 16 }}>
256 {tab === "Produce" && result.produce ? (
257 <>
258 <ProduceCard produce={result.produce} />
259 {renderTimeline(result.produce)}
260 </>
261 ) : tab === "User" && result.user ? (
262 renderUser(result.user)
263 ) : (
264 <View style={local.card}>
265 <Text style={{ textAlign: "center", fontWeight: "500" }}>
266 No results found for the provided ID.
267 </Text>
268 </View>
269 )}
270 </View>
271 )}
272 </ScrollView>
273 </SafeAreaView>
274 );
275}
276
277const local = StyleSheet.create({
278 searchContainer: {
279 padding: 16,
280 backgroundColor: "white",
281 borderRadius: 16,
282 margin: 12,
283 elevation: 4,
284 shadowColor: "#000",
285 shadowOpacity: 0.1,
286 shadowOffset: { width: 0, height: 2 },
287 shadowRadius: 8,
288 },
289 searchDescription: {
290 marginBottom: 16,
291 color: colors.gray,
292 textAlign: "center",
293 fontSize: 15,
294 },
295 tabContainer: {
296 flexDirection: "row",
297 justifyContent: "space-around",
298 marginBottom: 10,
299 },
300 tabButton: {
301 flex: 1,
302 flexDirection: "row",
303 alignItems: "center",
304 justifyContent: "center",
305 paddingVertical: 10,
306 borderRadius: 12,
307 marginHorizontal: 5,
308 },
309 tabText: { marginLeft: 6, fontWeight: "600" },
310 card: {
311 backgroundColor: "white",
312 borderRadius: 16,
313 padding: 16,
314 marginHorizontal: 12,
315 marginVertical: 10,
316 shadowColor: "#000",
317 shadowOpacity: 0.1,
318 shadowOffset: { width: 0, height: 4 },
319 shadowRadius: 10,
320 elevation: 5,
321 },
322 title: {
323 fontWeight: "700",
324 fontSize: 18,
325 color: colors.darkGreen,
326 },
327 subtitle: {
328 fontWeight: "600",
329 marginTop: 16,
330 marginBottom: 8,
331 fontSize: 16,
332 color: colors.darkGreen,
333 paddingHorizontal: 12,
334 },
335 timelineItem: {
336 flexDirection: "row",
337 paddingVertical: 12,
338 marginHorizontal: 12,
339 borderBottomWidth: 1,
340 borderBottomColor: colors.lightGreen,
341 },
342 small: { fontSize: 13, color: "#555", marginTop: 2 },
343 userHeader: {
344 flexDirection: "row",
345 alignItems: "center",
346 paddingBottom: 10,
347 marginBottom: 10,
348 borderBottomWidth: 1,
349 borderBottomColor: colors.lightGreen,
350 },
351 userAvatar: {
352 width: 60,
353 height: 60,
354 borderRadius: 30,
355 backgroundColor: colors.lightGreen,
356 justifyContent: "center",
357 alignItems: "center",
358 marginRight: 16,
359 },
360 roleBadge: {
361 backgroundColor: colors.midGreen,
362 borderRadius: 6,
363 paddingHorizontal: 8,
364 paddingVertical: 4,
365 alignSelf: "flex-start",
366 marginTop: 4,
367 },
368 roleText: { color: "white", fontSize: 12, fontWeight: "bold" },
369 detailLabel: {
370 color: colors.gray,
371 fontSize: 14,
372 fontWeight: "600",
373 marginBottom: 4,
374 },
375});
376
377const pickerSelectStyles = StyleSheet.create({
378 inputIOS: {
379 ...styles.input,
380 height: 48,
381 justifyContent: "center",
382 paddingVertical: 12,
383 paddingHorizontal: 10,
384 fontSize: 16,
385 },
386 inputAndroid: {
387 ...styles.input,
388 height: 48,
389 justifyContent: "center",
390 paddingVertical: 12,
391 paddingHorizontal: 10,
392 fontSize: 16,
393 },
394 iconContainer: {
395 top: 18,
396 right: 15,
397 },
398});