mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-03 22:23:17 +00:00
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
175 lines
4.4 KiB
TypeScript
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',
|
|
},
|
|
});
|