Add mobile expenses and receipt OCR
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { RefreshControl, StyleSheet, Text, View } from "react-native";
|
||||
|
||||
import { AppBackground } from "@/components/AppBackground";
|
||||
import { LoadingScreen } from "@/components/LoadingScreen";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { StatCard } from "@/components/StatCard";
|
||||
import { TabPage } from "@/components/TabPage";
|
||||
import { TabScrollView } from "@/components/TabScrollView";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { spacing } from "@/constants/theme";
|
||||
import { useAppTheme } from "@/contexts/ThemeContext";
|
||||
import { formatCurrency } from "@/lib/format";
|
||||
import { formatTrpcErrorMessage } from "@/lib/trpc-errors";
|
||||
import { api } from "@/lib/trpc";
|
||||
|
||||
export default function ReportsScreen() {
|
||||
const { colors } = useAppTheme();
|
||||
const statsQuery = api.dashboard.getStats.useQuery();
|
||||
const expensesQuery = api.expenses.getAll.useQuery();
|
||||
const summaryQuery = api.timeEntries.getSummary.useQuery();
|
||||
|
||||
if (statsQuery.isLoading || expensesQuery.isLoading || summaryQuery.isLoading) {
|
||||
return <LoadingScreen message="Loading reports…" />;
|
||||
}
|
||||
|
||||
if (statsQuery.error) {
|
||||
return (
|
||||
<AppBackground>
|
||||
<TabPage>
|
||||
<Text style={{ color: colors.mutedForeground, padding: spacing.lg }}>
|
||||
{formatTrpcErrorMessage(statsQuery.error)}
|
||||
</Text>
|
||||
</TabPage>
|
||||
</AppBackground>
|
||||
);
|
||||
}
|
||||
|
||||
const stats = statsQuery.data!;
|
||||
const expenseTotal = (expensesQuery.data ?? []).reduce((sum, e) => sum + e.amount, 0);
|
||||
const summary = summaryQuery.data;
|
||||
|
||||
return (
|
||||
<AppBackground>
|
||||
<TabPage>
|
||||
<TabScrollView
|
||||
header={<PageHeader title="Reports" subtitle="Business performance snapshot" />}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={statsQuery.isRefetching}
|
||||
onRefresh={() => {
|
||||
void statsQuery.refetch();
|
||||
void expensesQuery.refetch();
|
||||
void summaryQuery.refetch();
|
||||
}}
|
||||
tintColor={colors.primary}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<View style={styles.grid}>
|
||||
<StatCard label="Revenue" value={formatCurrency(stats.totalRevenue)} />
|
||||
<StatCard label="Pending" value={formatCurrency(stats.pendingAmount)} />
|
||||
<StatCard label="Expenses" value={formatCurrency(expenseTotal)} />
|
||||
<StatCard
|
||||
label="Billable hours"
|
||||
value={summary ? summary.totalHours.toFixed(1) : "0"}
|
||||
hint={summary ? `${summary.count} entries` : undefined}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Card title="Invoice status">
|
||||
{(stats.statusChartData ?? []).map((item) => (
|
||||
<View key={item.status} style={styles.statusRow}>
|
||||
<Text style={{ color: colors.foreground }}>{item.name}</Text>
|
||||
<Text style={{ color: colors.mutedForeground }}>
|
||||
{item.count} · {formatCurrency(item.value)}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</Card>
|
||||
|
||||
<Card title="Revenue trend (6 mo)">
|
||||
{stats.revenueChartData.map((point) => (
|
||||
<View key={point.month} style={styles.statusRow}>
|
||||
<Text style={{ color: colors.foreground }}>{point.monthLabel}</Text>
|
||||
<Text style={{ color: colors.mutedForeground }}>
|
||||
{formatCurrency(point.revenue)}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</Card>
|
||||
</TabScrollView>
|
||||
</TabPage>
|
||||
</AppBackground>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
grid: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: spacing.md,
|
||||
},
|
||||
statusRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user