209 lines
6.7 KiB
TypeScript
209 lines
6.7 KiB
TypeScript
import React, { useCallback } from 'react';
|
|
import { View, Text, StyleSheet, SafeAreaView, ScrollView } from 'react-native';
|
|
import { useFocusEffect } from 'expo-router';
|
|
import { Header } from '@/components/Header';
|
|
import { useStats } from '@/hooks/useStats';
|
|
import { useSettings, type ThemeColors } from '@/theme';
|
|
|
|
export default function StatsScreen() {
|
|
const { theme } = useSettings();
|
|
const stats = useStats();
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
return () => {};
|
|
}, [])
|
|
);
|
|
|
|
const maxDaily = Math.max(1, ...stats.daily.map((d) => d.count));
|
|
|
|
return (
|
|
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
|
<Header title="Stats" showLogo={true} />
|
|
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
|
|
<View style={[styles.card, { backgroundColor: theme.card }]}>
|
|
<Text style={[styles.cardTitle, { color: theme.textMuted }]}>Overview</Text>
|
|
<View style={styles.summaryGrid}>
|
|
<View style={styles.summaryItem}>
|
|
<Text style={[styles.summaryValue, { color: theme.accent }]}>{stats.totalCompleted}</Text>
|
|
<Text style={[styles.summaryLabel, { color: theme.textMuted }]}>Completed</Text>
|
|
</View>
|
|
<View style={styles.summaryItem}>
|
|
<Text style={[styles.summaryValue, { color: theme.text }]}>{stats.completedLast7}</Text>
|
|
<Text style={[styles.summaryLabel, { color: theme.textMuted }]}>Last 7 days</Text>
|
|
</View>
|
|
<View style={styles.summaryItem}>
|
|
<Text style={[styles.summaryValue, { color: theme.text }]}>{stats.completedLast30}</Text>
|
|
<Text style={[styles.summaryLabel, { color: theme.textMuted }]}>Last 30 days</Text>
|
|
</View>
|
|
<View style={styles.summaryItem}>
|
|
<Text style={[styles.summaryValue, { color: theme.text }]}>{stats.completionRate}%</Text>
|
|
<Text style={[styles.summaryLabel, { color: theme.textMuted }]}>Success rate</Text>
|
|
</View>
|
|
<View style={styles.summaryItem}>
|
|
<Text style={[styles.summaryValue, { color: theme.text }]}>{stats.currentStreak}</Text>
|
|
<Text style={[styles.summaryLabel, { color: theme.textMuted }]}>Day streak</Text>
|
|
</View>
|
|
<View style={styles.summaryItem}>
|
|
<Text style={[styles.summaryValue, { color: stats.overdueCount > 0 ? theme.accent : theme.text }]}>
|
|
{stats.overdueCount}
|
|
</Text>
|
|
<Text style={[styles.summaryLabel, { color: theme.textMuted }]}>Overdue</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={[styles.card, { backgroundColor: theme.card }]}>
|
|
<Text style={[styles.cardTitle, { color: theme.textMuted }]}>Last 7 days</Text>
|
|
<View style={styles.chartRow}>
|
|
{stats.daily.map((d, i) => {
|
|
const height = (d.count / maxDaily) * BASE_HEIGHT;
|
|
return (
|
|
<View key={i} style={styles.chartCol}>
|
|
<View style={styles.chartBarTrack}>
|
|
<View
|
|
style={[
|
|
styles.chartBar,
|
|
{ height: Math.max(4, height), backgroundColor: d.count > 0 ? theme.accent : theme.cardAlt },
|
|
]}
|
|
/>
|
|
</View>
|
|
<Text style={[styles.chartLabel, { color: theme.textFaint }]}>{d.label}</Text>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
</View>
|
|
|
|
<View style={[styles.card, { backgroundColor: theme.card }]}>
|
|
<Text style={[styles.cardTitle, { color: theme.textMuted }]}>By tags</Text>
|
|
{stats.byCategory.length === 0 ? (
|
|
<Text style={[styles.emptyText, { color: theme.textFaint }]}>No completed tasks yet</Text>
|
|
) : (
|
|
stats.byCategory.map((c) => (
|
|
<View key={c.id} style={styles.row}>
|
|
<View style={[styles.dot, { backgroundColor: c.color }]} />
|
|
<Text style={[styles.rowLabel, { color: theme.text }]}>{c.name}</Text>
|
|
<Text style={[styles.rowValue, { color: theme.textMuted }]}>{c.count}</Text>
|
|
</View>
|
|
))
|
|
)}
|
|
</View>
|
|
|
|
<View style={[styles.card, { backgroundColor: theme.card }]}>
|
|
<Text style={[styles.cardTitle, { color: theme.textMuted }]}>By priority</Text>
|
|
{stats.byPriority.length === 0 ? (
|
|
<Text style={[styles.emptyText, { color: theme.textFaint }]}>No completed tasks yet</Text>
|
|
) : (
|
|
stats.byPriority.map((p) => (
|
|
<View key={p.priority} style={styles.row}>
|
|
<View style={[styles.dot, { backgroundColor: priorityColor(theme, p.priority) }]} />
|
|
<Text style={[styles.rowLabel, { color: theme.text }]}>{p.label}</Text>
|
|
<Text style={[styles.rowValue, { color: theme.textMuted }]}>{p.count}</Text>
|
|
</View>
|
|
))
|
|
)}
|
|
</View>
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const BASE_HEIGHT = 90;
|
|
|
|
function priorityColor(theme: ThemeColors, priority: string): string {
|
|
switch (priority) {
|
|
case 'critical':
|
|
return theme.dangerStrong;
|
|
case 'high':
|
|
return theme.warning;
|
|
case 'low':
|
|
case 'medium':
|
|
return theme.success;
|
|
default:
|
|
return theme.textMuted;
|
|
}
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
},
|
|
content: {
|
|
padding: 16,
|
|
paddingBottom: 32,
|
|
gap: 12,
|
|
},
|
|
card: {
|
|
borderRadius: 16,
|
|
padding: 16,
|
|
},
|
|
cardTitle: {
|
|
fontSize: 13,
|
|
fontWeight: '600',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.5,
|
|
marginBottom: 14,
|
|
},
|
|
summaryGrid: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
},
|
|
summaryItem: {
|
|
width: '33.33%',
|
|
marginBottom: 16,
|
|
},
|
|
summaryValue: {
|
|
fontSize: 26,
|
|
fontWeight: '700',
|
|
},
|
|
summaryLabel: {
|
|
fontSize: 12,
|
|
marginTop: 2,
|
|
},
|
|
chartRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-end',
|
|
gap: 8,
|
|
},
|
|
chartCol: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
},
|
|
chartBarTrack: {
|
|
height: BASE_HEIGHT,
|
|
justifyContent: 'flex-end',
|
|
width: '100%',
|
|
},
|
|
chartBar: {
|
|
width: '100%',
|
|
borderRadius: 6,
|
|
minHeight: 4,
|
|
},
|
|
chartLabel: {
|
|
fontSize: 11,
|
|
marginTop: 6,
|
|
},
|
|
row: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingVertical: 8,
|
|
},
|
|
dot: {
|
|
width: 10,
|
|
height: 10,
|
|
borderRadius: 5,
|
|
marginRight: 10,
|
|
},
|
|
rowLabel: {
|
|
flex: 1,
|
|
fontSize: 15,
|
|
},
|
|
rowValue: {
|
|
fontSize: 15,
|
|
fontWeight: '600',
|
|
},
|
|
emptyText: {
|
|
fontSize: 14,
|
|
},
|
|
}); |