matiru

Blockchain-Based Supply Chain Transparency for Agricultural Produce

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});