proposal-system/mobile/src/screens/admin/ProposalQueueScreen.tsx

155 lines
4 KiB
TypeScript
Raw Normal View History

import React, { useState } from 'react';
import {
View,
StyleSheet,
FlatList,
RefreshControl,
} from 'react-native';
import { Searchbar, Chip, FAB } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useSelector } from 'react-redux';
import { selectUser } from '../../store/slices/authSlice';
import { usePaginatedList } from '../../hooks/usePaginatedList';
import { proposalsApi, type ProposalListItem } from '../../lib/api/proposals';
import { ProposalCard } from '../../components/ProposalCard';
import { EmptyState } from '../../components/EmptyState';
import { PROPOSAL_STATUSES } from '../../constants';
import type { ProposalsStackParamList } from '../../navigation/types';
type Nav = NativeStackNavigationProp<ProposalsStackParamList>;
export function ProposalQueueScreen() {
const navigation = useNavigation<Nav>();
const user = useSelector(selectUser);
const isAdmin = user?.role === 'Admin' || user?.role === 'SysAdmin';
const [statusFilter, setStatusFilter] = useState<string | null>(null);
const {
rows,
search,
setSearch,
loading,
refreshing,
reload,
} = usePaginatedList<ProposalListItem>(
(params) =>
proposalsApi.getAll({
search: params.search as string | undefined,
page: params.page as number | undefined,
pageSize: params.pageSize as number | undefined,
status: (params.status as string) || undefined,
}),
{ status: statusFilter },
);
const handlePress = (proposal: ProposalListItem) => {
if (isAdmin) {
navigation.navigate('ProposalWorkspace', { proposalId: proposal.id });
} else {
navigation.navigate('ProposalDetail', { proposalId: proposal.id });
}
};
return (
<View style={styles.container}>
<Searchbar
placeholder="Search proposals..."
onChangeText={setSearch}
value={search}
style={styles.search}
/>
<View style={styles.filters}>
<Chip
selected={!statusFilter}
onPress={() => setStatusFilter(null)}
compact
style={styles.chip}
>
All
</Chip>
{PROPOSAL_STATUSES.map((s) => (
<Chip
key={s}
selected={statusFilter === s}
onPress={() =>
setStatusFilter(statusFilter === s ? null : s)
}
compact
style={styles.chip}
>
{s === 'InReview' ? 'In Review' : s}
</Chip>
))}
</View>
<FlatList
data={rows}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<ProposalCard proposal={item} onPress={() => handlePress(item)} />
)}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={reload} />
}
contentContainerStyle={rows.length === 0 ? styles.empty : styles.list}
ListEmptyComponent={
!loading ? (
<EmptyState
icon="file-search-outline"
title="No proposals found"
message={
search
? 'Try a different search term'
: 'No proposals match the current filter'
}
/>
) : null
}
/>
{!isAdmin && (
<FAB
icon="plus"
style={styles.fab}
onPress={() => navigation.navigate('NewProposal')}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FAFAFA',
},
search: {
margin: 12,
elevation: 1,
},
filters: {
flexDirection: 'row',
paddingHorizontal: 12,
paddingBottom: 8,
gap: 6,
flexWrap: 'wrap',
},
chip: {
marginBottom: 4,
},
list: {
paddingBottom: 80,
},
empty: {
flex: 1,
},
fab: {
position: 'absolute',
right: 16,
bottom: 16,
backgroundColor: '#1565C0',
},
});