summaryrefslogblamecommitdiff
path: root/apps/mobile/app/(tabs)/tasks.tsx
blob: 5aac7103bc6b6c479f2f3ab49cdef74749727646 (plain) (tree)






































































































































































































































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