312 lines
11 KiB
TypeScript
312 lines
11 KiB
TypeScript
import React, { useState } from "react";
|
|
import { View, ScrollView, Pressable, ActivityIndicator } from "react-native";
|
|
import { api } from "@/lib/api";
|
|
import { Text } from "@/components/ui/text";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { useSirouRouter } from "@sirou/react-native";
|
|
import { AppRoutes } from "@/lib/routes";
|
|
import {
|
|
Plus,
|
|
Send,
|
|
History as HistoryIcon,
|
|
Briefcase,
|
|
ChevronRight,
|
|
Clock,
|
|
DollarSign,
|
|
FileText,
|
|
} from "@/lib/icons";
|
|
import { ScreenWrapper } from "@/components/ScreenWrapper";
|
|
import { ShadowWrapper } from "@/components/ShadowWrapper";
|
|
import { StandardHeader } from "@/components/StandardHeader";
|
|
import { useAuthStore } from "@/lib/auth-store";
|
|
|
|
export default function HomeScreen() {
|
|
const [activeFilter, setActiveFilter] = useState("All");
|
|
const [stats, setStats] = useState({
|
|
total: 0,
|
|
paid: 0,
|
|
pending: 0,
|
|
overdue: 0,
|
|
totalRevenue: 0,
|
|
});
|
|
const [invoices, setInvoices] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const nav = useSirouRouter<AppRoutes>();
|
|
|
|
React.useEffect(() => {
|
|
fetchStats();
|
|
}, []);
|
|
|
|
React.useEffect(() => {
|
|
fetchInvoices();
|
|
}, [activeFilter]);
|
|
|
|
const fetchStats = async () => {
|
|
const { isAuthenticated } = useAuthStore.getState();
|
|
if (!isAuthenticated) return;
|
|
|
|
try {
|
|
const data = await api.invoices.stats();
|
|
setStats(data);
|
|
} catch (e) {
|
|
console.error("[HomeScreen] Failed to fetch stats:", e);
|
|
}
|
|
};
|
|
|
|
const fetchInvoices = async () => {
|
|
const { isAuthenticated } = useAuthStore.getState();
|
|
if (!isAuthenticated) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const statusParam =
|
|
activeFilter === "All" ? undefined : activeFilter.toUpperCase();
|
|
const response = await api.invoices.getAll({
|
|
query: {
|
|
limit: 5,
|
|
status: statusParam,
|
|
},
|
|
});
|
|
setInvoices(response.data || []);
|
|
} catch (e) {
|
|
console.error("[HomeScreen] Failed to fetch invoices:", e);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<StandardHeader />
|
|
|
|
<ScrollView
|
|
showsVerticalScrollIndicator={false}
|
|
contentContainerStyle={{
|
|
paddingHorizontal: 20,
|
|
paddingTop: 10,
|
|
paddingBottom: 150,
|
|
}}
|
|
>
|
|
{/* Balance Card Section */}
|
|
<View className="mb-4">
|
|
<ShadowWrapper level="lg">
|
|
<Card className="overflow-hidden rounded-[10px] border-0 bg-primary">
|
|
<View className="p-4 relative">
|
|
<View
|
|
className="absolute -top-10 -right-10 w-48 h-48 bg-white/10 rounded-full"
|
|
style={{ transform: [{ scale: 1.5 }] }}
|
|
/>
|
|
|
|
<Text className="text-white/60 text-[14px] font-semibold">
|
|
Available Balance
|
|
</Text>
|
|
<View className="mt-2 flex-row items-baseline">
|
|
<Text className="text-white text-2xl font-medium">$</Text>
|
|
<Text className="ml-1 text-4xl font-bold text-white">
|
|
{stats.total.toLocaleString()}
|
|
</Text>
|
|
</View>
|
|
|
|
<View className="mt-4 flex-row gap-4">
|
|
<View className="flex-1 bg-white/15 rounded-[10px] p-2 border border-white/10 backdrop-blur-xl">
|
|
<View className="flex-row items-center gap-2">
|
|
<View className="p-1.5 bg-white/20 rounded-lg">
|
|
<Clock color="white" size={12} strokeWidth={2.5} />
|
|
</View>
|
|
<Text className="text-white text-[12px] font-semibold">
|
|
Pending
|
|
</Text>
|
|
</View>
|
|
<Text className="text-white font-bold text-xl mt-2">
|
|
${stats.pending.toLocaleString()}
|
|
</Text>
|
|
</View>
|
|
<View className="flex-1 bg-white/15 rounded-[10px] p-2 border border-white/10 backdrop-blur-xl">
|
|
<View className="flex-row items-center gap-2">
|
|
<View className="p-1.5 bg-white/20 rounded-lg">
|
|
<DollarSign color="white" size={12} strokeWidth={2.5} />
|
|
</View>
|
|
<Text className="text-white text-[12px] font-semibold">
|
|
Income
|
|
</Text>
|
|
</View>
|
|
<Text className="text-white font-bold text-xl mt-2">
|
|
${stats.totalRevenue.toLocaleString()}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Card>
|
|
</ShadowWrapper>
|
|
</View>
|
|
|
|
{/* Circular Quick Actions Section */}
|
|
<View className="mb-4 flex-row justify-around items-center px-2">
|
|
<QuickAction
|
|
icon={<Briefcase color="#000" size={20} strokeWidth={1.5} />}
|
|
label="Company"
|
|
onPress={() => nav.go("company")}
|
|
/>
|
|
<QuickAction
|
|
icon={<Send color="#000" size={20} strokeWidth={1.5} />}
|
|
label="Send"
|
|
onPress={() => nav.go("(tabs)/proforma")}
|
|
/>
|
|
<QuickAction
|
|
icon={<HistoryIcon color="#000" size={20} strokeWidth={1.5} />}
|
|
label="History"
|
|
onPress={() => nav.go("history")}
|
|
/>
|
|
<QuickAction
|
|
icon={<Plus color="#000" size={20} strokeWidth={1.5} />}
|
|
label="Create Proforma"
|
|
onPress={() => nav.go("proforma/create")}
|
|
/>
|
|
</View>
|
|
|
|
{/* Recent Activity Header */}
|
|
<View className="mb-4 flex-row justify-between items-center">
|
|
<Text variant="h4" className="text-foreground tracking-tight">
|
|
Recent Activity
|
|
</Text>
|
|
<Pressable
|
|
onPress={() => nav.go("history")}
|
|
className="px-4 py-2 rounded-full"
|
|
>
|
|
<Text className="text-primary font-bold text-xs">View all</Text>
|
|
</Pressable>
|
|
</View>
|
|
|
|
{/* Filters */}
|
|
<View className="mb-6">
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
contentContainerStyle={{ gap: 8 }}
|
|
>
|
|
{["All", "Draft", "Pending", "Paid", "Overdue", "Cancelled"].map(
|
|
(filter) => (
|
|
<Pressable
|
|
key={filter}
|
|
onPress={() => setActiveFilter(filter)}
|
|
className={`rounded-[4px] px-4 py-1.5 ${
|
|
activeFilter === filter
|
|
? "bg-primary"
|
|
: "bg-card border border-border"
|
|
}`}
|
|
>
|
|
<Text
|
|
className={`text-xs font-bold ${
|
|
activeFilter === filter
|
|
? "text-white"
|
|
: "text-muted-foreground"
|
|
}`}
|
|
>
|
|
{filter}
|
|
</Text>
|
|
</Pressable>
|
|
),
|
|
)}
|
|
</ScrollView>
|
|
</View>
|
|
|
|
{/* Transactions List */}
|
|
<View className="gap-2">
|
|
{loading ? (
|
|
<ActivityIndicator color="#ea580c" className="py-20" />
|
|
) : invoices.length > 0 ? (
|
|
invoices.map((inv) => (
|
|
<Pressable
|
|
key={inv.id}
|
|
onPress={() => nav.go("invoices/[id]", { id: inv.id })}
|
|
>
|
|
<ShadowWrapper level="xs">
|
|
<Card className="overflow-hidden rounded-[6px] bg-card">
|
|
<CardContent className="flex-row items-center py-3 px-2">
|
|
<View className="bg-secondary/40 rounded-[6px] p-2 mr-2 border border-border/10">
|
|
<FileText
|
|
className="text-muted-foreground"
|
|
size={22}
|
|
strokeWidth={2}
|
|
/>
|
|
</View>
|
|
<View className="flex-1 mt-[-20px]">
|
|
<Text
|
|
variant="p"
|
|
className="text-foreground font-semibold"
|
|
>
|
|
{inv.customerName}
|
|
</Text>
|
|
<Text
|
|
variant="muted"
|
|
className="mt-1 text-[11px] font-medium opacity-70"
|
|
>
|
|
{new Date(inv.issueDate).toLocaleDateString()} ·
|
|
Proforma
|
|
</Text>
|
|
</View>
|
|
<View className="items-end mt-[-20px]">
|
|
<Text
|
|
variant="p"
|
|
className="text-foreground font-semibold"
|
|
>
|
|
${Number(inv.amount).toLocaleString()}
|
|
</Text>
|
|
<View
|
|
className={`mt-1 rounded-[5px] px-3 py-1 border border-border/50 ${
|
|
inv.status === "PAID"
|
|
? "bg-emerald-500/30 text-emerald-600"
|
|
: inv.status === "PENDING"
|
|
? "bg-amber-500/30 text-amber-600"
|
|
: inv.status === "DRAFT"
|
|
? "bg-secondary text-muted-foreground"
|
|
: "bg-red-500/30 text-red-600"
|
|
}`}
|
|
>
|
|
<Text className="text-[9px] font-semibold uppercase tracking-widest">
|
|
{inv.status}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</CardContent>
|
|
</Card>
|
|
</ShadowWrapper>
|
|
</Pressable>
|
|
))
|
|
) : (
|
|
<View className="py-20 items-center">
|
|
<Text variant="muted">No transactions found</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</ScrollView>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|
|
|
|
function QuickAction({
|
|
icon,
|
|
label,
|
|
onPress,
|
|
}: {
|
|
icon: React.ReactNode;
|
|
label: string;
|
|
onPress?: () => void;
|
|
}) {
|
|
return (
|
|
<View className="items-center">
|
|
<ShadowWrapper>
|
|
<Pressable
|
|
onPress={onPress}
|
|
className="h-12 w-12 rounded-full bg-background items-center justify-center mb-2"
|
|
>
|
|
{icon}
|
|
</Pressable>
|
|
</ShadowWrapper>
|
|
<Text className="text-foreground text-[12px] font-semibold tracking-tight opacity-90">
|
|
{label}
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|