109 lines
3.7 KiB
TypeScript
109 lines
3.7 KiB
TypeScript
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,
|
|
},
|
|
});
|