summaryrefslogtreecommitdiff
path: root/apps/mobile/app/(tabs)/tasks.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'apps/mobile/app/(tabs)/tasks.tsx')
-rw-r--r--apps/mobile/app/(tabs)/tasks.tsx231
1 files changed, 231 insertions, 0 deletions
diff --git a/apps/mobile/app/(tabs)/tasks.tsx b/apps/mobile/app/(tabs)/tasks.tsx
new file mode 100644
index 0000000..5aac710
--- /dev/null
+++ b/apps/mobile/app/(tabs)/tasks.tsx
@@ -0,0 +1,231 @@
+import React, { useState, useCallback, useMemo } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ SectionList,
+ RefreshControl,
+ useColorScheme,
+ TextInput,
+ TouchableOpacity,
+} from 'react-native';
+import { useRouter } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { Colors } from '../../constants/Colors';
+import { useTasks, groupTasksByStatus } from '../../hooks/useTasks';
+import { TaskListItem } from '../../components/TaskListItem';
+import { TaskListSkeleton } from '../../components/TaskListSkeleton';
+import { EmptyState } from '../../components/EmptyState';
+import type { TaskSummary } from '../../lib/api';
+
+interface Section {
+ title: string;
+ data: TaskSummary[];
+}
+
+export default function TasksScreen() {
+ const colorScheme = useColorScheme() ?? 'light';
+ const colors = Colors[colorScheme];
+ const router = useRouter();
+ const [searchQuery, setSearchQuery] = useState('');
+
+ const { data: tasks, isLoading, isError, refetch, isRefetching } = useTasks();
+
+ // Group and filter tasks
+ const sections = useMemo(() => {
+ if (!tasks) return [];
+
+ // Filter by search query
+ const filteredTasks = searchQuery
+ ? tasks.filter(
+ (task) =>
+ task.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ task.contractName?.toLowerCase().includes(searchQuery.toLowerCase())
+ )
+ : tasks;
+
+ // Group by status
+ const groups = groupTasksByStatus(filteredTasks);
+
+ const result: Section[] = [];
+
+ if (groups.running.length > 0) {
+ result.push({ title: 'Running', data: groups.running });
+ }
+ if (groups.blocked.length > 0) {
+ result.push({ title: 'Needs Attention', data: groups.blocked });
+ }
+ if (groups.pending.length > 0) {
+ result.push({ title: 'Pending', data: groups.pending });
+ }
+ if (groups.completed.length > 0) {
+ result.push({ title: 'Completed', data: groups.completed });
+ }
+
+ return result;
+ }, [tasks, searchQuery]);
+
+ const handleTaskPress = useCallback(
+ (task: TaskSummary) => {
+ router.push(`/task/${task.id}`);
+ },
+ [router]
+ );
+
+ const handleRefresh = useCallback(() => {
+ refetch();
+ }, [refetch]);
+
+ const clearSearch = useCallback(() => {
+ setSearchQuery('');
+ }, []);
+
+ // Render loading state
+ if (isLoading) {
+ return (
+ <View style={[styles.container, { backgroundColor: colors.background }]}>
+ <TaskListSkeleton />
+ </View>
+ );
+ }
+
+ // Render error state
+ if (isError) {
+ return (
+ <View style={[styles.container, { backgroundColor: colors.background }]}>
+ <EmptyState
+ icon="alert-circle-outline"
+ title="Failed to load tasks"
+ message="Pull down to retry"
+ />
+ </View>
+ );
+ }
+
+ // Render empty state
+ if (!tasks || tasks.length === 0) {
+ return (
+ <View style={[styles.container, { backgroundColor: colors.background }]}>
+ <EmptyState
+ icon="cube-outline"
+ title="No tasks yet"
+ message="Tasks created from contracts will appear here"
+ />
+ </View>
+ );
+ }
+
+ return (
+ <View style={[styles.container, { backgroundColor: colors.background }]}>
+ {/* Search bar */}
+ <View
+ style={[
+ styles.searchContainer,
+ {
+ backgroundColor: colors.secondaryBackground,
+ borderColor: colors.border,
+ },
+ ]}
+ >
+ <Ionicons name="search" size={18} color={colors.secondaryText} />
+ <TextInput
+ style={[styles.searchInput, { color: colors.text }]}
+ placeholder="Search tasks..."
+ placeholderTextColor={colors.secondaryText}
+ value={searchQuery}
+ onChangeText={setSearchQuery}
+ autoCapitalize="none"
+ autoCorrect={false}
+ />
+ {searchQuery.length > 0 && (
+ <TouchableOpacity onPress={clearSearch}>
+ <Ionicons name="close-circle" size={18} color={colors.secondaryText} />
+ </TouchableOpacity>
+ )}
+ </View>
+
+ {/* Task list */}
+ <SectionList
+ sections={sections}
+ keyExtractor={(item) => item.id}
+ renderItem={({ item }) => (
+ <TaskListItem task={item} onPress={handleTaskPress} />
+ )}
+ renderSectionHeader={({ section }) => (
+ <View
+ style={[
+ styles.sectionHeader,
+ { backgroundColor: colors.secondaryBackground },
+ ]}
+ >
+ <Text style={[styles.sectionTitle, { color: colors.text }]}>
+ {section.title}
+ </Text>
+ <Text style={[styles.sectionCount, { color: colors.secondaryText }]}>
+ {section.data.length}
+ </Text>
+ </View>
+ )}
+ refreshControl={
+ <RefreshControl
+ refreshing={isRefetching}
+ onRefresh={handleRefresh}
+ tintColor={colors.tint}
+ />
+ }
+ ListEmptyComponent={
+ searchQuery ? (
+ <EmptyState
+ icon="search-outline"
+ title="No matching tasks"
+ message={`No tasks match "${searchQuery}"`}
+ />
+ ) : null
+ }
+ stickySectionHeadersEnabled
+ contentContainerStyle={sections.length === 0 ? styles.emptyContent : undefined}
+ />
+ </View>
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ searchContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ margin: 16,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ borderRadius: 10,
+ borderWidth: StyleSheet.hairlineWidth,
+ gap: 8,
+ },
+ searchInput: {
+ flex: 1,
+ fontSize: 16,
+ padding: 0,
+ },
+ sectionHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ },
+ sectionTitle: {
+ fontSize: 14,
+ fontWeight: '600',
+ textTransform: 'uppercase',
+ letterSpacing: 0.5,
+ },
+ sectionCount: {
+ fontSize: 14,
+ fontWeight: '500',
+ },
+ emptyContent: {
+ flex: 1,
+ },
+});