proposal-system/mobile/src/screens/dispatcher/DashboardScreen.tsx
Adam Moussa 686b42330c Fix React version mismatch, auth fallback, and UI polish
Pin React to 19.2.3 (exact version RN 0.85.3's bundled renderer
requires — 19.2.6 caused "Cannot read property 'default' of
undefined" crash at getPaperRenderer).

Auth: fall back to parsing user info from Cognito ID token when
backend API is unreachable, removing the hard dependency on
localhost:5000 for credential login.

UI fixes:
- Safe area insets on Settings screen (top edge was missing)
- Separate refresh indicator from filter-triggered loading on
  proposal queue (chips no longer trigger pull-to-refresh animation)
- Dashboard welcome shows first name instead of full email
- Dashboard/AdminDashboard remove duplicate top safe area when
  navigation header already provides it
- Service category selector uses wrapping chips instead of
  cramped SegmentedButtons
- Add ErrorBoundary to App root for crash visibility
2026-05-20 13:34:08 -04:00

175 lines
4.4 KiB
TypeScript

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?.includes('@') ? user.displayName.split('@')[0] : 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',
},
});