proposal-system/mobile/src/screens/dispatcher/DashboardScreen.tsx

176 lines
4.3 KiB
TypeScript
Raw Normal View History

import React from 'react';
import {
View,
StyleSheet,
ScrollView,
RefreshControl,
} from 'react-native';
import { Card, Text, FAB } from 'react-native-paper';
import { useQuery } from '@tanstack/react-query';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useAuth } from '../../hooks/useAuth';
import { proposalsApi } from '../../lib/api/proposals';
import { ProposalCard } from '../../components/ProposalCard';
import { EmptyState } from '../../components/EmptyState';
import { QUERY_KEYS } from '../../constants/queryKeys';
import type { ProposalsStackParamList } from '../../navigation/types';
type Nav = NativeStackNavigationProp<ProposalsStackParamList>;
export function DashboardScreen() {
const { user } = useAuth();
const navigation = useNavigation<Nav>();
const statsQuery = useQuery({
queryKey: [QUERY_KEYS.dashboard, 'stats'],
queryFn: () => proposalsApi.getStats(),
});
const proposalsQuery = useQuery({
queryKey: [QUERY_KEYS.proposals, 'recent'],
queryFn: () => proposalsApi.getAll({ pageSize: 5 }),
});
const refreshing = statsQuery.isRefetching || proposalsQuery.isRefetching;
const onRefresh = () => {
statsQuery.refetch();
proposalsQuery.refetch();
};
const stats = statsQuery.data;
const proposals = proposalsQuery.data?.items ?? [];
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView
contentContainerStyle={styles.scroll}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
<Text variant="headlineSmall" style={styles.welcome}>
Welcome, {user?.displayName?.split(' ')[0] || 'there'}
</Text>
{stats && (
<View style={styles.statsRow}>
<StatCard label="Total" value={stats.totalCount} color="#1565C0" />
<StatCard
label="In Review"
value={stats.inReviewCount}
color="#E65100"
/>
<StatCard
label="Approved"
value={stats.approvedCount}
color="#2E7D32"
/>
<StatCard label="Sent" value={stats.sentCount} color="#7B1FA2" />
</View>
)}
<Text variant="titleMedium" style={styles.sectionTitle}>
Recent Proposals
</Text>
{proposals.length === 0 && !proposalsQuery.isLoading ? (
<EmptyState
icon="file-document-outline"
title="No proposals yet"
message="Tap + to submit your first proposal"
/>
) : (
proposals.map((p) => (
<ProposalCard
key={p.id}
proposal={p}
onPress={() =>
navigation.navigate('ProposalDetail', { proposalId: p.id })
}
/>
))
)}
</ScrollView>
<FAB
icon="plus"
style={styles.fab}
onPress={() => navigation.navigate('NewProposal')}
/>
</SafeAreaView>
);
}
function StatCard({
label,
value,
color,
}: {
label: string;
value: number;
color: string;
}) {
return (
<Card style={styles.statCard} mode="elevated">
<Card.Content style={styles.statContent}>
<Text variant="headlineMedium" style={[styles.statValue, { color }]}>
{value}
</Text>
<Text variant="bodySmall" style={styles.statLabel}>
{label}
</Text>
</Card.Content>
</Card>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FAFAFA',
},
scroll: {
paddingBottom: 80,
},
welcome: {
fontWeight: '700',
paddingHorizontal: 16,
paddingTop: 16,
paddingBottom: 12,
},
statsRow: {
flexDirection: 'row',
paddingHorizontal: 12,
gap: 8,
marginBottom: 16,
},
statCard: {
flex: 1,
},
statContent: {
alignItems: 'center',
paddingVertical: 8,
},
statValue: {
fontWeight: '700',
},
statLabel: {
color: '#757575',
marginTop: 2,
},
sectionTitle: {
fontWeight: '600',
paddingHorizontal: 16,
paddingVertical: 8,
},
fab: {
position: 'absolute',
right: 16,
bottom: 16,
backgroundColor: '#1565C0',
},
});