feat: Expo scaffold + auth screens + core screens (#39-#46, #48)

This commit is contained in:
Nemo
2026-03-23 18:40:04 +08:00
commit 745321e5bd
45 changed files with 10557 additions and 0 deletions

65
app/(app)/dashboard.tsx Normal file
View File

@@ -0,0 +1,65 @@
import { View, Text, ScrollView, RefreshControl, ActivityIndicator } from 'react-native';
import { useQuery } from '@tanstack/react-query';
import { api } from '../../services/api';
import { useAuthStore } from '../../stores/authStore';
function KpiCard({ label, value, color }: { label: string; value: string | number; color: string }) {
return (
<View className="bg-white rounded-2xl p-4 flex-1 mx-1 shadow-sm border border-gray-100">
<Text className="text-gray-500 text-xs mb-1">{label}</Text>
<Text className={`text-2xl font-bold`} style={{ color }}>{value}</Text>
</View>
);
}
export default function DashboardScreen() {
const { user } = useAuthStore();
const { data, isLoading, refetch, isRefetching } = useQuery({
queryKey: ['dashboard'],
queryFn: () => api.get('/api/v1/dashboard/summary').then(r => r.data),
});
return (
<ScrollView
className="flex-1 bg-gray-50"
refreshControl={<RefreshControl refreshing={isRefetching} onRefresh={refetch} />}
>
<View className="px-4 pt-14 pb-4 bg-primary">
<Text className="text-white text-sm opacity-80">Welcome back,</Text>
<Text className="text-white text-xl font-bold">{user?.firstName ?? 'Field Staff'}</Text>
</View>
{isLoading ? (
<View className="flex-1 items-center justify-center py-20">
<ActivityIndicator color="#2563EB" />
</View>
) : (
<View className="px-3 py-4">
<Text className="text-gray-700 font-semibold mb-3 px-1">Overview</Text>
<View className="flex-row mb-3">
<KpiCard label="Total Clients" value={data?.totalClients ?? 0} color="#2563EB" />
<KpiCard label="Active Subs" value={data?.activeSubscriptions ?? 0} color="#16A34A" />
</View>
<View className="flex-row mb-6">
<KpiCard label="Overdue" value={data?.overdueInvoices ?? 0} color="#DC2626" />
<KpiCard label="Today Collections" value={`${(data?.todayCollections ?? 0).toLocaleString()}`} color="#D97706" />
</View>
<Text className="text-gray-700 font-semibold mb-3 px-1">Recent Tickets</Text>
{(data?.recentTickets ?? []).length === 0 ? (
<View className="bg-white rounded-2xl p-6 items-center border border-gray-100">
<Text className="text-gray-400">No recent tickets</Text>
</View>
) : (
(data?.recentTickets ?? []).map((t: any) => (
<View key={t.id} className="bg-white rounded-xl p-4 mb-2 border border-gray-100">
<Text className="font-medium text-gray-900">{t.subject}</Text>
<Text className="text-gray-500 text-sm mt-1">{t.clientName} · {t.status}</Text>
</View>
))
)}
</View>
)}
</ScrollView>
);
}