242 lines
8.4 KiB
TypeScript
242 lines
8.4 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { View, ScrollView, Pressable, ActivityIndicator } from "react-native";
|
|
import { useSirouRouter } from "@sirou/react-native";
|
|
import { AppRoutes } from "@/lib/routes";
|
|
import { Stack, useLocalSearchParams } from "expo-router";
|
|
import { Text } from "@/components/ui/text";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card } from "@/components/ui/card";
|
|
import {
|
|
FileText,
|
|
Calendar,
|
|
Share2,
|
|
Download,
|
|
ArrowLeft,
|
|
ExternalLink,
|
|
} from "@/lib/icons";
|
|
import { ScreenWrapper } from "@/components/ScreenWrapper";
|
|
import { ShadowWrapper } from "@/components/ShadowWrapper";
|
|
import { StandardHeader } from "@/components/StandardHeader";
|
|
import { api } from "@/lib/api";
|
|
import { toast } from "@/lib/toast-store";
|
|
|
|
export default function InvoiceDetailScreen() {
|
|
const nav = useSirouRouter<AppRoutes>();
|
|
const { id } = useLocalSearchParams();
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [invoice, setInvoice] = useState<any>(null);
|
|
|
|
useEffect(() => {
|
|
fetchInvoice();
|
|
}, [id]);
|
|
|
|
const fetchInvoice = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await api.invoices.getById({ params: { id: id as string } });
|
|
setInvoice(data);
|
|
} catch (error: any) {
|
|
console.error("[InvoiceDetail] Error:", error);
|
|
toast.error("Error", "Failed to load invoice details");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<Stack.Screen options={{ headerShown: false }} />
|
|
<StandardHeader title="Invoice Details" showBack />
|
|
<View className="flex-1 justify-center items-center">
|
|
<ActivityIndicator color="#ea580c" size="large" />
|
|
</View>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|
|
|
|
if (!invoice) {
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<Stack.Screen options={{ headerShown: false }} />
|
|
<StandardHeader title="Invoice Details" showBack />
|
|
<View className="flex-1 justify-center items-center">
|
|
<Text variant="muted">Invoice not found</Text>
|
|
</View>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<Stack.Screen options={{ headerShown: false }} />
|
|
<StandardHeader title="Invoice Details" showBack />
|
|
|
|
<ScrollView
|
|
className="flex-1"
|
|
contentContainerStyle={{ padding: 16, paddingBottom: 120 }}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
{/* Status Hero Card */}
|
|
<Card className="mb-4 overflow-hidden rounded-[6px] border-0 bg-primary">
|
|
<View className="p-5">
|
|
<View className="flex-row items-center justify-between mb-3">
|
|
<View className="bg-white/20 p-1.5 rounded-[6px]">
|
|
<FileText color="white" size={16} strokeWidth={2.5} />
|
|
</View>
|
|
<View
|
|
className={`rounded-[6px] px-3 py-1 ${invoice.status === "PAID" ? "bg-emerald-500/20" : "bg-white/15"}`}
|
|
>
|
|
<Text
|
|
className={`text-[10px] font-bold ${invoice.status === "PAID" ? "text-emerald-400" : "text-white"}`}
|
|
>
|
|
{invoice.status || "Pending"}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<Text variant="small" className="text-white/70 mb-0.5">
|
|
Total Amount
|
|
</Text>
|
|
<Text variant="h3" className="text-white font-bold mb-3">
|
|
${Number(invoice.amount).toLocaleString()}
|
|
</Text>
|
|
|
|
<View className="flex-row items-center gap-3 border-t border-white/40 pt-3">
|
|
<View className="flex-row items-center gap-1.5">
|
|
<Calendar color="rgba(255,255,255,0.9)" size={12} />
|
|
<Text className="text-white/90 text-xs font-semibold">
|
|
Due {new Date(invoice.dueDate).toLocaleDateString()}
|
|
</Text>
|
|
</View>
|
|
<View className="h-3 w-[1px] bg-white/60" />
|
|
<Text className="text-white/90 text-xs font-semibold">
|
|
#{invoice.invoiceNumber || id}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</Card>
|
|
|
|
{/* Recipient & Category — inline info strip */}
|
|
<Card className="bg-card rounded-[6px] mb-4">
|
|
<View className="flex-row px-4 py-2">
|
|
<View className="flex-1 flex-row items-center">
|
|
<View className="flex-col">
|
|
<Text className="text-foreground text-xs opacity-60">
|
|
Recipient
|
|
</Text>
|
|
<Text
|
|
variant="p"
|
|
className="text-foreground font-semibold"
|
|
numberOfLines={1}
|
|
>
|
|
{invoice.customerName || "—"}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<View className="w-[1px] bg-border/70 mx-3" />
|
|
<View className="flex-1 flex-row items-center">
|
|
<View className="flex-col">
|
|
<Text className="text-foreground text-xs opacity-60">
|
|
Category
|
|
</Text>
|
|
<Text
|
|
variant="p"
|
|
className="text-foreground font-semibold"
|
|
numberOfLines={1}
|
|
>
|
|
General
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Card>
|
|
|
|
{/* Items / Billing Summary */}
|
|
<Card className="mb-4 bg-card rounded-[6px]">
|
|
<View className="p-4">
|
|
<View className="flex-row items-center gap-2 mb-2">
|
|
<Text
|
|
variant="small"
|
|
className="font-bold opacity-60 uppercase text-[10px] tracking-widest"
|
|
>
|
|
Billing Summary
|
|
</Text>
|
|
</View>
|
|
|
|
<View className="flex-row justify-between py-3 border-b border-border/70">
|
|
<View className="flex-1 pr-4">
|
|
<Text
|
|
variant="p"
|
|
className="text-foreground font-semibold text-sm"
|
|
>
|
|
Subtotal
|
|
</Text>
|
|
</View>
|
|
<Text variant="p" className="text-foreground font-bold text-sm">
|
|
$
|
|
{(
|
|
Number(invoice.amount) - (Number(invoice.taxAmount) || 0)
|
|
).toLocaleString()}
|
|
</Text>
|
|
</View>
|
|
|
|
{Number(invoice.taxAmount) > 0 && (
|
|
<View className="flex-row justify-between py-3 border-b border-border/70">
|
|
<View className="flex-1 pr-4">
|
|
<Text
|
|
variant="p"
|
|
className="text-foreground font-semibold text-sm"
|
|
>
|
|
Tax
|
|
</Text>
|
|
</View>
|
|
<Text variant="p" className="text-foreground font-bold text-sm">
|
|
+ ${Number(invoice.taxAmount).toLocaleString()}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
<View className="mt-3 pt-3 flex-row justify-between items-center border-t border-border/70">
|
|
<Text variant="muted" className="font-semibold text-sm">
|
|
Total Balance
|
|
</Text>
|
|
<Text
|
|
variant="h3"
|
|
className="text-foreground font-semibold text-xl tracking-tight"
|
|
>
|
|
${Number(invoice.amount).toLocaleString()}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</Card>
|
|
|
|
{/* Actions */}
|
|
<View className="flex-row gap-3">
|
|
<Button
|
|
className=" flex-1 mb-4 h-11 rounded-[6px] bg-primary shadow-lg shadow-primary/30"
|
|
onPress={() => {}}
|
|
>
|
|
<Share2 color="#ffffff" size={14} strokeWidth={2.5} />
|
|
<Text className="ml-2 text-white text-[11px] font-bold uppercase tracking-widest">
|
|
Share
|
|
</Text>
|
|
</Button>
|
|
<ShadowWrapper>
|
|
<Button
|
|
className=" flex-1 mb-4 h-11 rounded-[6px] bg-card border border-border"
|
|
onPress={() => {}}
|
|
>
|
|
<Download color="#0f172a" size={14} strokeWidth={2.5} />
|
|
<Text className="ml-2 text-foreground text-[11px] font-bold uppercase tracking-widest">
|
|
PDF
|
|
</Text>
|
|
</Button>
|
|
</ShadowWrapper>
|
|
</View>
|
|
</ScrollView>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|