CityTrack / User /src /screens /issues /MyIssuesScreen.tsx
0xarchit's picture
User app beta v1 complete
71638d4
import React, { useEffect, useState, useCallback } from "react";
import {
View,
Text,
StyleSheet,
FlatList,
RefreshControl,
TouchableOpacity,
ActivityIndicator,
} from "react-native";
import { useNavigation, useFocusEffect } from "@react-navigation/native";
import { LinearGradient } from "expo-linear-gradient";
import { Ionicons } from "@expo/vector-icons";
import { Card } from "../../components/ui/Card";
import { IssueCard } from "../../components/issues/IssueCard";
import { issueService } from "../../services/issueService";
import { cacheService } from "../../services/cacheService";
import { useAuth } from "../../context/AuthContext";
import { colors, spacing, typography, borderRadius } from "../../theme";
import { Issue } from "../../types";
const ITEMS_PER_PAGE = 10;
export function MyIssuesScreen() {
const navigation = useNavigation<any>();
const { user, isDevMode } = useAuth();
const [issues, setIssues] = useState<Issue[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [filter, setFilter] = useState<string | null>(null);
useFocusEffect(
useCallback(() => {
loadIssues();
}, [filter, user?.id]),
);
const loadIssues = async () => {
setLoading(true);
await cacheService.clearCache();
await fetchIssues(1, true);
};
const fetchIssues = async (pageNum: number, reset: boolean = false) => {
try {
const response = await issueService.listIssues(
pageNum,
ITEMS_PER_PAGE,
filter || undefined,
isDevMode ? undefined : user?.id,
);
const filtered = response.items;
if (reset) {
setIssues(filtered);
await cacheService.setIssuesCache(filtered);
} else {
const newIssues = [...issues, ...filtered];
setIssues(newIssues);
await cacheService.setIssuesCache(newIssues);
}
setHasMore(response.items.length === ITEMS_PER_PAGE);
setPage(pageNum);
} catch (error) {
console.error("Failed to fetch issues:", error);
} finally {
setLoading(false);
setRefreshing(false);
}
};
const handleRefresh = () => {
setRefreshing(true);
fetchIssues(1, true);
};
const handleForceRefresh = async () => {
setLoading(true);
await cacheService.clearCache();
await fetchIssues(1, true);
};
const handleLoadMore = () => {
if (hasMore && !loading) {
fetchIssues(page + 1);
}
};
const handleIssuePress = (issue: Issue) => {
navigation.navigate("IssueDetail", { issueId: issue.id });
};
const filters = [
{ key: null, label: "All" },
{ key: "reported", label: "Reported" },
{ key: "assigned", label: "Assigned" },
{ key: "in_progress", label: "In Progress" },
{ key: "pending_verification", label: "Review" },
{ key: "resolved", label: "Resolved" },
];
const renderHeader = () => (
<View style={styles.header}>
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<Ionicons name="arrow-back" size={24} color={colors.text.primary} />
</TouchableOpacity>
<Text style={styles.title}>My Reports</Text>
<TouchableOpacity
style={styles.refreshButton}
onPress={handleForceRefresh}
>
<Ionicons name="refresh" size={24} color={colors.primary.main} />
</TouchableOpacity>
</View>
);
const renderFilters = () => (
<View style={styles.filters}>
{filters.map((f) => (
<TouchableOpacity
key={f.key || "all"}
style={[styles.filterButton, filter === f.key && styles.filterActive]}
onPress={() => setFilter(f.key)}
>
<Text
style={[
styles.filterText,
filter === f.key && styles.filterTextActive,
]}
>
{f.label}
</Text>
</TouchableOpacity>
))}
</View>
);
const renderEmpty = () => (
<Card style={styles.emptyCard}>
<Ionicons
name="documents-outline"
size={48}
color={colors.text.tertiary}
/>
<Text style={styles.emptyTitle}>No Reports Found</Text>
<Text style={styles.emptyText}>
{filter
? "No issues match this filter"
: "You haven't reported any issues yet"}
</Text>
</Card>
);
const renderFooter = () => {
if (!hasMore) return null;
return (
<View style={styles.footer}>
<ActivityIndicator size="small" color={colors.primary.main} />
</View>
);
};
return (
<LinearGradient
colors={[colors.background.primary, colors.background.secondary]}
style={styles.container}
>
<FlatList
data={issues}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<IssueCard issue={item} onPress={() => handleIssuePress(item)} />
)}
ListHeaderComponent={
<>
{renderHeader()}
{renderFilters()}
</>
}
ListEmptyComponent={!loading ? renderEmpty : null}
ListFooterComponent={renderFooter}
contentContainerStyle={styles.listContent}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
tintColor={colors.primary.main}
/>
}
onEndReached={handleLoadMore}
onEndReachedThreshold={0.5}
showsVerticalScrollIndicator={false}
/>
</LinearGradient>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
listContent: {
padding: spacing.lg,
paddingTop: spacing.xxl * 2,
},
header: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginBottom: spacing.lg,
},
backButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: colors.background.tertiary,
alignItems: "center",
justifyContent: "center",
},
refreshButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: colors.background.tertiary,
alignItems: "center",
justifyContent: "center",
},
title: {
...typography.h2,
color: colors.text.primary,
},
filters: {
flexDirection: "row",
marginBottom: spacing.lg,
gap: spacing.sm,
},
filterButton: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
backgroundColor: colors.background.tertiary,
},
filterActive: {
backgroundColor: colors.primary.main,
},
filterText: {
color: colors.text.secondary,
fontSize: 14,
},
filterTextActive: {
color: colors.text.primary,
fontWeight: "600",
},
emptyCard: {
alignItems: "center",
padding: spacing.xl,
},
emptyTitle: {
...typography.h3,
color: colors.text.primary,
marginBottom: spacing.sm,
marginTop: spacing.md,
},
emptyText: {
...typography.body,
color: colors.text.secondary,
textAlign: "center",
},
footer: {
paddingVertical: spacing.lg,
alignItems: "center",
},
});