Yaltopia-Tickets-App/app/(tabs)/index.tsx
2026-03-11 22:48:53 +03:00

355 lines
12 KiB
TypeScript

import React, { useState } from "react";
import {
View,
ScrollView,
Pressable,
ActivityIndicator,
useColorScheme,
} from "react-native";
import { api } from "@/lib/api";
import { Text } from "@/components/ui/text";
import { EmptyState } from "@/components/EmptyState";
import { Card, CardContent } from "@/components/ui/card";
import { useSirouRouter } from "@sirou/react-native";
import { AppRoutes } from "@/lib/routes";
import {
Plus,
History as HistoryIcon,
Briefcase,
ChevronRight,
Clock,
DollarSign,
FileText,
ScanLine,
} 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);
}
};
const colorScheme = useColorScheme();
const isDark = colorScheme === "dark";
return (
<ScreenWrapper className="bg-background">
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{
paddingTop: 10,
paddingBottom: 150,
}}
>
<StandardHeader />
{/* Balance Card Section */}
<View className="px-[16px] pt-6">
<View className="mb-4">
<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>
</View>
{/* Circular Quick Actions Section */}
<View className="mb-4 flex-row justify-around items-center px-2">
<QuickAction
icon={
<Briefcase
color={isDark ? "#f1f5f9" : "#0f172a"}
size={20}
strokeWidth={1.5}
/>
}
label="Company"
onPress={() => nav.go("company")}
/>
<QuickAction
icon={
<ScanLine
color={isDark ? "#f1f5f9" : "#0f172a"}
size={20}
strokeWidth={1.5}
/>
}
label="Scan SMS"
onPress={() => nav.go("sms-scan")}
/>
<QuickAction
icon={
<Plus
color={isDark ? "#f1f5f9" : "#0f172a"}
size={20}
strokeWidth={1.5}
/>
}
label="Create Proforma"
onPress={() => nav.go("proforma/create")}
/>
<QuickAction
icon={
<HistoryIcon
color={isDark ? "#f1f5f9" : "#0f172a"}
size={20}
strokeWidth={1.5}
/>
}
label="History"
onPress={() => nav.go("history")}
/>
</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 ${
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-10">
<EmptyState
title="No transactions yet"
description="Your recent activity will show up here once you create and send invoices."
hint="Create a proforma invoice to get started."
actionLabel="Create Proforma"
onActionPress={() => nav.go("proforma/create")}
previewLines={3}
/>
</View>
)}
</View>
</View>
</ScrollView>
</ScreenWrapper>
);
}
function QuickAction({
icon,
label,
onPress,
}: {
icon: React.ReactNode;
label: string;
onPress?: () => void;
}) {
return (
<View className="pt-2 items-center w-[75px]">
<Pressable
onPress={onPress}
className="h-12 w-12 rounded-full bg-card border border-border/20 items-center justify-center flex-shrink-0"
>
{icon}
</Pressable>
<Text
variant="p"
className="flex-1 text-foreground text-[12px] font-bold tracking-tight text-center leading-4"
>
{label}
</Text>
</View>
);
}