897 lines
31 KiB
TypeScript
897 lines
31 KiB
TypeScript
import React, { useState, useCallback } from "react";
|
||
import {
|
||
View,
|
||
ScrollView,
|
||
ActivityIndicator,
|
||
Linking,
|
||
useColorScheme,
|
||
Pressable,
|
||
Modal,
|
||
Dimensions,
|
||
} from "react-native";
|
||
import { useSirouRouter } from "@sirou/react-native";
|
||
import { AppRoutes } from "@/lib/routes";
|
||
import { Stack, useLocalSearchParams, useFocusEffect } from "expo-router";
|
||
import { Text } from "@/components/ui/text";
|
||
import {
|
||
Calendar,
|
||
Share2,
|
||
Download,
|
||
Trash2,
|
||
Package,
|
||
Clock,
|
||
User,
|
||
Hash,
|
||
AlertCircle,
|
||
Mail,
|
||
MessageSquare,
|
||
X,
|
||
MoreVertical,
|
||
FileText,
|
||
Receipt,
|
||
CreditCard,
|
||
ChevronRight,
|
||
Check,
|
||
Edit,
|
||
} from "@/lib/icons";
|
||
import { ScreenWrapper } from "@/components/ScreenWrapper";
|
||
import { StandardHeader } from "@/components/StandardHeader";
|
||
import { api, BASE_URL } from "@/lib/api";
|
||
import { toast } from "@/lib/toast-store";
|
||
import { useAuthStore } from "@/lib/auth-store";
|
||
import { ActionModal } from "@/components/ActionModal";
|
||
import { UploadIcon } from "lucide-react-native";
|
||
|
||
const { height: SCREEN_HEIGHT } = Dimensions.get("window");
|
||
|
||
export default function InvoiceDetailScreen() {
|
||
const nav = useSirouRouter<AppRoutes>();
|
||
const { id } = useLocalSearchParams();
|
||
const colorScheme = useColorScheme();
|
||
const isDark = colorScheme === "dark";
|
||
|
||
const [loading, setLoading] = useState(true);
|
||
const [invoice, setInvoice] = useState<any>(null);
|
||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||
const [showShareSheet, setShowShareSheet] = useState(false);
|
||
const [showMoreSheet, setShowMoreSheet] = useState(false);
|
||
const [sharing, setSharing] = useState(false);
|
||
const [activeTab, setActiveTab] = useState<"details" | "activity">("details");
|
||
|
||
useFocusEffect(
|
||
useCallback(() => {
|
||
fetchInvoice();
|
||
}, [id]),
|
||
);
|
||
|
||
const fetchInvoice = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const invoiceId = Array.isArray(id) ? id[0] : id;
|
||
if (!invoiceId) throw new Error("No ID provided");
|
||
|
||
const data = await api.invoices.getById({ params: { id: invoiceId } });
|
||
setInvoice(data);
|
||
} catch (error: any) {
|
||
console.error("[InvoiceDetail] Error:", error);
|
||
toast.error("Error", "Failed to load invoice details");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleGetPdf = async () => {
|
||
try {
|
||
const { token } = useAuthStore.getState();
|
||
const pdfUrl = `${BASE_URL}invoices/${id}/pdf?token=${token}`;
|
||
await Linking.openURL(pdfUrl);
|
||
} catch (error) {
|
||
console.error("[InvoiceDetail] PDF Error:", error);
|
||
toast.error("Error", "Failed to open PDF");
|
||
}
|
||
};
|
||
|
||
const handleShare = async (channel: "email" | "sms") => {
|
||
try {
|
||
setSharing(true);
|
||
const invoiceId = Array.isArray(id) ? id[0] : id;
|
||
await api.invoices.shareLink({
|
||
body: { invoiceId, channel },
|
||
});
|
||
toast.success(
|
||
"Sent",
|
||
`Invoice shared via ${channel === "email" ? "email" : "SMS"}`,
|
||
);
|
||
setShowShareSheet(false);
|
||
} catch (err: any) {
|
||
toast.error("Error", err?.message || "Failed to share invoice");
|
||
} finally {
|
||
setSharing(false);
|
||
}
|
||
};
|
||
|
||
const handleDelete = () => setShowDeleteModal(true);
|
||
|
||
const confirmDelete = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const invoiceId = Array.isArray(id) ? id[0] : id;
|
||
await api.invoices.delete({
|
||
params: { id: invoiceId as string },
|
||
});
|
||
toast.success("Success", "Invoice deleted successfully");
|
||
setShowDeleteModal(false);
|
||
nav.back();
|
||
} catch (error) {
|
||
console.error("[InvoiceDetail] Delete Error:", error);
|
||
toast.error("Error", "Failed to delete invoice");
|
||
setShowDeleteModal(false);
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<ScreenWrapper className="bg-background">
|
||
<Stack.Screen options={{ headerShown: false }} />
|
||
<StandardHeader title="Invoice" 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" showBack />
|
||
<View className="flex-1 justify-center items-center">
|
||
<AlertCircle size={48} color="#ef4444" className="mb-4" />
|
||
<Text variant="h4" className="mb-1">
|
||
Invoice Not Found
|
||
</Text>
|
||
<Text variant="muted">
|
||
The requested document could not be retrieved.
|
||
</Text>
|
||
</View>
|
||
</ScreenWrapper>
|
||
);
|
||
}
|
||
|
||
// Robust data extraction
|
||
const originalData = invoice.scannedData?.originalData || {};
|
||
const items =
|
||
(invoice.items?.length > 0 ? invoice.items : originalData.items) || [];
|
||
|
||
const taxAmountValue = Number(
|
||
typeof invoice.taxAmount === "object"
|
||
? invoice.taxAmount?.value
|
||
: invoice.taxAmount || originalData.taxAmount || 0,
|
||
);
|
||
|
||
const discountValue = Number(
|
||
typeof invoice.discountAmount === "object"
|
||
? invoice.discountAmount?.value
|
||
: invoice.discountAmount || originalData.discountAmount || 0,
|
||
);
|
||
|
||
let amountValue = Number(
|
||
typeof invoice.amount === "object" ? invoice.amount.value : invoice.amount,
|
||
);
|
||
|
||
if (items.length > 0) {
|
||
const itemsTotal = items.reduce(
|
||
(acc: number, item: any) =>
|
||
acc + (Number(item.total?.value || item.total) || 0),
|
||
0,
|
||
);
|
||
if (
|
||
itemsTotal > 0 &&
|
||
(amountValue === taxAmountValue || amountValue < itemsTotal)
|
||
) {
|
||
amountValue = itemsTotal + taxAmountValue - discountValue;
|
||
}
|
||
}
|
||
|
||
const subtotalValue = amountValue - taxAmountValue + discountValue;
|
||
const discountPercent =
|
||
subtotalValue > 0 ? Math.round((discountValue / subtotalValue) * 100) : 0;
|
||
|
||
const status = (invoice.status || "PENDING").toUpperCase();
|
||
const isPaid = status === "PAID";
|
||
const statusLabel: Record<string, string> = {
|
||
PAID: "Paid",
|
||
PENDING: "Pending",
|
||
DRAFT: "Draft",
|
||
CANCELLED: "Cancelled",
|
||
};
|
||
|
||
const customerName = (
|
||
invoice.customerName?.replace("Customer Name: ", "") || "Walking Client"
|
||
).trim();
|
||
|
||
const paymentDate = invoice.dueDate
|
||
? new Date(invoice.dueDate)
|
||
: invoice.issueDate
|
||
? new Date(invoice.issueDate)
|
||
: new Date(invoice.createdAt);
|
||
|
||
const formatLongDate = (d: Date) =>
|
||
d.toLocaleDateString("en-US", {
|
||
day: "numeric",
|
||
month: "short",
|
||
year: "numeric",
|
||
});
|
||
|
||
return (
|
||
<ScreenWrapper className="bg-background">
|
||
<Stack.Screen options={{ headerShown: false }} />
|
||
<StandardHeader
|
||
title="Invoice Details"
|
||
showBack
|
||
right={
|
||
<Pressable
|
||
onPress={() => setShowMoreSheet(true)}
|
||
className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border"
|
||
>
|
||
<MoreVertical color={isDark ? "#f1f5f9" : "#0f172a"} size={18} />
|
||
</Pressable>
|
||
}
|
||
/>
|
||
|
||
<ScrollView
|
||
className="flex-1"
|
||
contentContainerStyle={{ paddingBottom: 24 }}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
{/* Hero Card — illustration overflows the top */}
|
||
<View className="px-5 pt-3">
|
||
<View
|
||
className="items-center"
|
||
style={{ marginTop: 8, marginBottom: -40, zIndex: 2 }}
|
||
>
|
||
<View style={{ width: 110, height: 92 }}>
|
||
<View
|
||
style={{
|
||
position: "absolute",
|
||
top: 6,
|
||
left: 22,
|
||
width: 72,
|
||
height: 84,
|
||
borderRadius: 10,
|
||
backgroundColor: "#E46212",
|
||
transform: [{ rotate: "8deg" }],
|
||
opacity: 0.92,
|
||
}}
|
||
/>
|
||
<View
|
||
style={{
|
||
position: "absolute",
|
||
top: 2,
|
||
left: 8,
|
||
width: 72,
|
||
height: 84,
|
||
borderRadius: 10,
|
||
backgroundColor: "#0f172a",
|
||
transform: [{ rotate: "-6deg" }],
|
||
opacity: 0.95,
|
||
}}
|
||
/>
|
||
<View
|
||
style={{
|
||
position: "absolute",
|
||
top: 4,
|
||
left: 22,
|
||
width: 72,
|
||
height: 84,
|
||
borderRadius: 10,
|
||
backgroundColor: isDark ? "#1F1F1F" : "#ffffff",
|
||
borderWidth: 1,
|
||
borderColor: isDark ? "rgba(255,255,255,0.08)" : "#EDD5D1",
|
||
alignItems: "center",
|
||
paddingTop: 12,
|
||
}}
|
||
>
|
||
<Receipt
|
||
size={22}
|
||
color={isDark ? "#f1f5f9" : "#251615"}
|
||
strokeWidth={1.6}
|
||
/>
|
||
<View
|
||
style={{
|
||
marginTop: 8,
|
||
width: 36,
|
||
height: 3,
|
||
borderRadius: 2,
|
||
backgroundColor: isDark
|
||
? "rgba(255,255,255,0.18)"
|
||
: "rgba(0,0,0,0.08)",
|
||
}}
|
||
/>
|
||
<View
|
||
style={{
|
||
marginTop: 4,
|
||
width: 22,
|
||
height: 3,
|
||
borderRadius: 2,
|
||
backgroundColor: isDark
|
||
? "rgba(255,255,255,0.12)"
|
||
: "rgba(0,0,0,0.06)",
|
||
}}
|
||
/>
|
||
<View
|
||
style={{
|
||
position: "absolute",
|
||
bottom: 10,
|
||
right: -6,
|
||
width: 22,
|
||
height: 26,
|
||
borderRadius: 5,
|
||
backgroundColor: "#E46212",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
}}
|
||
>
|
||
<Text className="text-white font-sans-black text-[12px]">
|
||
$
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
<View
|
||
className="rounded-[14px] pt-14 pb-6 px-6 items-center bg-primary/5"
|
||
style={{
|
||
borderWidth: 1,
|
||
borderColor: isDark
|
||
? "rgba(255,255,255,0.06)"
|
||
: "rgba(0,0,0,0.05)",
|
||
}}
|
||
>
|
||
<Text className="text-foreground text-[34px] font-sans-black tracking-tight leading-tight">
|
||
{Number(amountValue).toLocaleString("en-US", {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
})}{" "}
|
||
<Text className="text-foreground text-[20px] font-sans-bold">
|
||
{invoice.currency || "USD"}
|
||
</Text>
|
||
</Text>
|
||
<Text className="text-muted-foreground text-[11px] font-sans-medium mt-1">
|
||
No.{" "}
|
||
{invoice.invoiceNumber ||
|
||
`INV${(invoice.id || "").slice(0, 8).toUpperCase()}`}
|
||
</Text>
|
||
|
||
<View className="w-full mt-5 gap-3">
|
||
<View className="flex-row justify-between items-center">
|
||
<Text className="text-muted-foreground text-[12px] font-sans-medium">
|
||
Status
|
||
</Text>
|
||
<View
|
||
className="px-2.5 py-1 rounded-[4px] flex-row items-center gap-1.5"
|
||
style={{
|
||
backgroundColor: isPaid
|
||
? "rgba(34, 197, 94, 0.12)"
|
||
: "rgba(234, 179, 8, 0.12)",
|
||
}}
|
||
>
|
||
{isPaid ? (
|
||
<Check size={11} color="#16a34a" strokeWidth={3} />
|
||
) : (
|
||
<Clock size={11} color="#ca8a04" strokeWidth={2.5} />
|
||
)}
|
||
<Text
|
||
className="text-[10px] font-sans-bold tracking-widest"
|
||
style={{
|
||
color: isPaid ? "#16a34a" : "#ca8a04",
|
||
}}
|
||
>
|
||
{statusLabel[status] || "Pending"}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
<View className="flex-row justify-between items-center">
|
||
<Text className="text-muted-foreground text-[12px] font-sans-medium">
|
||
{isPaid ? "Payment Date" : "Due Date"}
|
||
</Text>
|
||
<Text className="text-foreground text-[12px] font-sans-bold">
|
||
{isPaid
|
||
? `Paid at ${formatLongDate(paymentDate)}`
|
||
: formatLongDate(paymentDate)}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* Tabs */}
|
||
<View className="px-5 pt-5">
|
||
<View className="flex-row gap-6 border-b border-border">
|
||
<Pressable
|
||
onPress={() => setActiveTab("details")}
|
||
className="pb-2.5"
|
||
>
|
||
<Text
|
||
className={`text-[14px] font-sans-bold ${
|
||
activeTab === "details"
|
||
? "text-foreground"
|
||
: "text-muted-foreground"
|
||
}`}
|
||
>
|
||
Details
|
||
</Text>
|
||
{activeTab === "details" && (
|
||
<View className="absolute -bottom-px left-0 right-0 h-0.5 bg-foreground" />
|
||
)}
|
||
</Pressable>
|
||
<Pressable
|
||
onPress={() => setActiveTab("activity")}
|
||
className="pb-2.5"
|
||
>
|
||
<Text
|
||
className={`text-[14px] font-sans-bold ${
|
||
activeTab === "activity"
|
||
? "text-foreground"
|
||
: "text-muted-foreground"
|
||
}`}
|
||
>
|
||
Activity Log
|
||
</Text>
|
||
{activeTab === "activity" && (
|
||
<View className="absolute -bottom-px left-0 right-0 h-0.5 bg-foreground" />
|
||
)}
|
||
</Pressable>
|
||
</View>
|
||
</View>
|
||
|
||
{activeTab === "details" ? (
|
||
<View className="px-5 pt-5">
|
||
<View className="gap-5">
|
||
<View>
|
||
<Text className="text-[11px] font-sans-bold tracking-widest text-muted-foreground mb-1.5">
|
||
Billed to
|
||
</Text>
|
||
<Text className="text-foreground text-[14px] font-sans-bold">
|
||
{invoice.customerEmail || "—"}
|
||
</Text>
|
||
</View>
|
||
|
||
<View>
|
||
<Text className="text-[11px] font-sans-bold tracking-widest text-muted-foreground mb-1.5">
|
||
Billing details
|
||
</Text>
|
||
<Text className="text-foreground text-[14px] font-sans-bold">
|
||
{customerName}
|
||
</Text>
|
||
</View>
|
||
|
||
<View>
|
||
<Text className="text-[11px] font-sans-bold tracking-widest text-muted-foreground mb-1.5">
|
||
Invoice Number
|
||
</Text>
|
||
<Text className="text-foreground text-[14px] font-sans-bold">
|
||
{invoice.invoiceNumber ||
|
||
`INV${(invoice.id || "").slice(0, 8).toUpperCase()}`}
|
||
</Text>
|
||
</View>
|
||
|
||
<View>
|
||
<Text className="text-[11px] font-sans-bold tracking-widest text-muted-foreground mb-1.5">
|
||
Notes
|
||
</Text>
|
||
<Text
|
||
className="text-foreground text-[14px] font-sans-bold"
|
||
numberOfLines={1}
|
||
>
|
||
{invoice.notes || "-"}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{items.length > 0 ? (
|
||
<View className="mt-8">
|
||
<Text className="text-foreground text-[16px] font-sans-black tracking-tight mb-3">
|
||
Product
|
||
</Text>
|
||
<View>
|
||
{items.map((item: any, idx: number) => {
|
||
const qty = Number(
|
||
item.quantity?.value || item.quantity || 1,
|
||
);
|
||
const unitPrice = Number(
|
||
item.unitPrice?.value || item.unitPrice || 0,
|
||
);
|
||
const lineTotal = Number(
|
||
item.total?.value || item.total || qty * unitPrice,
|
||
);
|
||
return (
|
||
<View
|
||
key={idx}
|
||
className={`flex-row items-center gap-3 py-3 ${
|
||
idx < items.length - 1 ? "border-b border-border" : ""
|
||
}`}
|
||
>
|
||
<View className="h-12 w-12 rounded-[8px] bg-muted items-center justify-center overflow-hidden">
|
||
<Package
|
||
size={20}
|
||
color="#94a3b8"
|
||
strokeWidth={1.5}
|
||
/>
|
||
</View>
|
||
<View className="flex-1">
|
||
<Text
|
||
className="text-foreground text-[14px] font-sans-bold"
|
||
numberOfLines={1}
|
||
>
|
||
{item.description || `Item ${idx + 1}`}
|
||
</Text>
|
||
<Text className="text-muted-foreground text-[12px] font-sans-medium mt-0.5">
|
||
{qty} ×{" "}
|
||
{unitPrice.toLocaleString("en-US", {
|
||
minimumFractionDigits: 2,
|
||
})}{" "}
|
||
{invoice.currency || "USD"}
|
||
</Text>
|
||
</View>
|
||
<View className="items-end">
|
||
<Text className="text-foreground text-[14px] font-sans-bold">
|
||
{lineTotal.toLocaleString("en-US", {
|
||
minimumFractionDigits: 2,
|
||
})}{" "}
|
||
{invoice.currency || "USD"}
|
||
</Text>
|
||
</View>
|
||
<Pressable hitSlop={8} className="px-1">
|
||
<MoreVertical size={16} color="#94a3b8" />
|
||
</Pressable>
|
||
</View>
|
||
);
|
||
})}
|
||
</View>
|
||
</View>
|
||
) : null}
|
||
|
||
<View className="mt-6 gap-2.5">
|
||
<View className="flex-row justify-between items-center">
|
||
<Text className="text-muted-foreground text-[14px] font-sans-medium">
|
||
Subtotal
|
||
</Text>
|
||
<Text className="text-foreground text-[14px] font-sans-bold">
|
||
{subtotalValue.toLocaleString("en-US", {
|
||
minimumFractionDigits: 2,
|
||
})}{" "}
|
||
{invoice.currency || "USD"}
|
||
</Text>
|
||
</View>
|
||
{discountValue > 0 ? (
|
||
<View className="flex-row justify-between items-center">
|
||
<Text className="text-muted-foreground text-[14px] font-sans-medium">
|
||
Discount {discountPercent > 0 ? `${discountPercent}%` : ""}
|
||
</Text>
|
||
<Text className="text-foreground text-[14px] font-sans-bold">
|
||
-{" "}
|
||
{discountValue.toLocaleString("en-US", {
|
||
minimumFractionDigits: 2,
|
||
})}{" "}
|
||
{invoice.currency || "USD"}
|
||
</Text>
|
||
</View>
|
||
) : null}
|
||
{taxAmountValue > 0 ? (
|
||
<View className="flex-row justify-between items-center">
|
||
<Text className="text-muted-foreground text-[14px] font-sans-medium">
|
||
Tax
|
||
</Text>
|
||
<Text className="text-foreground text-[14px] font-sans-bold">
|
||
+{" "}
|
||
{taxAmountValue.toLocaleString("en-US", {
|
||
minimumFractionDigits: 2,
|
||
})}{" "}
|
||
{invoice.currency || "USD"}
|
||
</Text>
|
||
</View>
|
||
) : null}
|
||
<View className="flex-row justify-between items-center pt-2 border-t border-border">
|
||
<Text className="text-foreground text-[15px] font-sans-black">
|
||
Total
|
||
</Text>
|
||
<Text className="text-foreground text-[15px] font-sans-black">
|
||
{amountValue.toLocaleString("en-US", {
|
||
minimumFractionDigits: 2,
|
||
})}{" "}
|
||
{invoice.currency || "USD"}
|
||
</Text>
|
||
</View>
|
||
<View className="flex-row justify-between items-center">
|
||
<Text className="text-muted-foreground text-[13px] font-sans-medium">
|
||
Amount due
|
||
</Text>
|
||
<Text className="text-foreground text-[15px] font-sans-black">
|
||
{amountValue.toLocaleString("en-US", {
|
||
minimumFractionDigits: 2,
|
||
})}{" "}
|
||
{invoice.currency || "USD"}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
) : (
|
||
<View className="px-5 pt-5">
|
||
{items.length > 0 ? (
|
||
<View>
|
||
<Text className="text-foreground text-[16px] font-sans-black tracking-tight mb-3">
|
||
Items
|
||
</Text>
|
||
<View>
|
||
{items.map((item: any, idx: number) => {
|
||
const qty = Number(
|
||
item.quantity?.value || item.quantity || 1,
|
||
);
|
||
const unitPrice = Number(
|
||
item.unitPrice?.value || item.unitPrice || 0,
|
||
);
|
||
const lineTotal = Number(
|
||
item.total?.value || item.total || qty * unitPrice,
|
||
);
|
||
return (
|
||
<View
|
||
key={idx}
|
||
className={`flex-row items-center gap-3 py-3 ${
|
||
idx < items.length - 1 ? "border-b border-border" : ""
|
||
}`}
|
||
>
|
||
<View className="h-12 w-12 rounded-[8px] bg-muted items-center justify-center overflow-hidden">
|
||
<Package
|
||
size={20}
|
||
color="#94a3b8"
|
||
strokeWidth={1.5}
|
||
/>
|
||
</View>
|
||
<View className="flex-1">
|
||
<Text
|
||
className="text-foreground text-[14px] font-sans-bold"
|
||
numberOfLines={1}
|
||
>
|
||
{item.description || `Item ${idx + 1}`}
|
||
</Text>
|
||
<Text className="text-muted-foreground text-[12px] font-sans-medium mt-0.5">
|
||
{qty} ×{" "}
|
||
{unitPrice.toLocaleString("en-US", {
|
||
minimumFractionDigits: 2,
|
||
})}{" "}
|
||
{invoice.currency || "USD"}
|
||
</Text>
|
||
</View>
|
||
<Text className="text-foreground text-[14px] font-sans-bold">
|
||
{lineTotal.toLocaleString("en-US", {
|
||
minimumFractionDigits: 2,
|
||
})}{" "}
|
||
{invoice.currency || "USD"}
|
||
</Text>
|
||
</View>
|
||
);
|
||
})}
|
||
</View>
|
||
</View>
|
||
) : (
|
||
<View className="px-5 pt-12 items-center">
|
||
<View className="h-14 w-14 rounded-full bg-muted items-center justify-center mb-3">
|
||
<Clock size={22} color="#94a3b8" />
|
||
</View>
|
||
<Text className="text-foreground text-[14px] font-sans-bold mb-1">
|
||
No activity yet
|
||
</Text>
|
||
<Text className="text-muted-foreground text-[12px] font-sans-medium text-center">
|
||
Events related to this invoice will show up here.
|
||
</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
)}
|
||
</ScrollView>
|
||
|
||
<View
|
||
className="flex-row gap-3 px-5 py-3 border-t border-border"
|
||
style={{ backgroundColor: isDark ? "#0a0505" : "#ffffff" }}
|
||
>
|
||
<Pressable
|
||
onPress={() => setShowShareSheet(true)}
|
||
className="flex-1 h-12 rounded-[8px] border border-border items-center justify-center flex-row gap-2 bg-card"
|
||
>
|
||
<Share2 color="#0f172a" size={16} strokeWidth={2.5} />
|
||
<Text className="text-foreground text-[13px] font-sans-bold">
|
||
Send Invoice
|
||
</Text>
|
||
</Pressable>
|
||
<Pressable
|
||
onPress={handleGetPdf}
|
||
className="flex-1 h-12 rounded-[8px] border border-border items-center justify-center flex-row gap-2 bg-card"
|
||
>
|
||
<UploadIcon color="#0f172a" size={16} strokeWidth={2.5} />
|
||
<Text className="text-foreground text-[13px] font-sans-bold">
|
||
Export
|
||
</Text>
|
||
</Pressable>
|
||
</View>
|
||
|
||
<Modal
|
||
visible={showShareSheet}
|
||
transparent
|
||
animationType="slide"
|
||
onRequestClose={() => setShowShareSheet(false)}
|
||
>
|
||
<Pressable
|
||
className="flex-1 bg-black/40"
|
||
onPress={() => setShowShareSheet(false)}
|
||
>
|
||
<View className="flex-1 justify-end">
|
||
<Pressable
|
||
className="bg-card rounded-t-[36px] overflow-hidden border-t-[3px] border-border/20"
|
||
style={{ maxHeight: SCREEN_HEIGHT * 0.8 }}
|
||
onPress={(e) => e.stopPropagation()}
|
||
>
|
||
<View className="px-6 pb-4 pt-4 flex-row justify-between items-center">
|
||
<View className="w-10" />
|
||
<Text className="text-foreground font-sans-bold text-[18px]">
|
||
Share
|
||
</Text>
|
||
<Pressable
|
||
onPress={() => setShowShareSheet(false)}
|
||
className="h-8 w-8 bg-secondary/80 rounded-full items-center justify-center border border-border/10"
|
||
>
|
||
<X
|
||
size={14}
|
||
color={isDark ? "#f1f5f9" : "#0f172a"}
|
||
strokeWidth={2.5}
|
||
/>
|
||
</Pressable>
|
||
</View>
|
||
|
||
<ScrollView
|
||
className="px-5"
|
||
showsVerticalScrollIndicator={false}
|
||
contentContainerStyle={{ paddingBottom: 40 }}
|
||
>
|
||
<ActionOption
|
||
icon={<Mail color="#E46212" size={18} strokeWidth={2} />}
|
||
label="Send as Email"
|
||
description="Public accessible shortened link via yaltopia.com"
|
||
onPress={() => handleShare("email")}
|
||
loading={sharing}
|
||
/>
|
||
<ActionOption
|
||
icon={
|
||
<MessageSquare color="#E46212" size={18} strokeWidth={2} />
|
||
}
|
||
label="Send as SMS"
|
||
description="Public accessible shortened link via yaltopia.com"
|
||
onPress={() => handleShare("sms")}
|
||
loading={sharing}
|
||
/>
|
||
</ScrollView>
|
||
</Pressable>
|
||
</View>
|
||
</Pressable>
|
||
</Modal>
|
||
|
||
<Modal
|
||
visible={showMoreSheet}
|
||
transparent
|
||
animationType="slide"
|
||
onRequestClose={() => setShowMoreSheet(false)}
|
||
>
|
||
<Pressable
|
||
className="flex-1 bg-black/40"
|
||
onPress={() => setShowMoreSheet(false)}
|
||
>
|
||
<View className="flex-1 justify-end">
|
||
<Pressable
|
||
className="bg-card rounded-t-[36px] overflow-hidden border-t-[3px] border-border/20"
|
||
style={{ maxHeight: SCREEN_HEIGHT * 0.5 }}
|
||
onPress={(e) => e.stopPropagation()}
|
||
>
|
||
<View className="px-6 pb-4 pt-4 flex-row justify-between items-center">
|
||
<View className="w-10" />
|
||
<Text className="text-foreground font-sans-bold text-[18px]">
|
||
Invoice
|
||
</Text>
|
||
<Pressable
|
||
onPress={() => setShowMoreSheet(false)}
|
||
className="h-8 w-8 bg-secondary/80 rounded-full items-center justify-center border border-border/10"
|
||
>
|
||
<X
|
||
size={14}
|
||
color={isDark ? "#f1f5f9" : "#0f172a"}
|
||
strokeWidth={2.5}
|
||
/>
|
||
</Pressable>
|
||
</View>
|
||
|
||
<ScrollView
|
||
className="px-5"
|
||
showsVerticalScrollIndicator={false}
|
||
contentContainerStyle={{ paddingBottom: 40 }}
|
||
>
|
||
<ActionOption
|
||
icon={<Edit color="#E46212" size={18} strokeWidth={2} />}
|
||
label="Edit Invoice"
|
||
description="Update details, items, or dates"
|
||
onPress={() => {
|
||
setShowMoreSheet(false);
|
||
nav.go("invoices/edit", { id: invoice.id });
|
||
}}
|
||
/>
|
||
<ActionOption
|
||
icon={<Trash2 color="#ef4444" size={18} strokeWidth={2} />}
|
||
label="Delete Invoice"
|
||
description="Permanently remove this record"
|
||
onPress={() => {
|
||
setShowMoreSheet(false);
|
||
setShowDeleteModal(true);
|
||
}}
|
||
danger
|
||
/>
|
||
</ScrollView>
|
||
</Pressable>
|
||
</View>
|
||
</Pressable>
|
||
</Modal>
|
||
|
||
<ActionModal
|
||
visible={showDeleteModal}
|
||
onClose={() => setShowDeleteModal(false)}
|
||
onConfirm={confirmDelete}
|
||
title="Delete Invoice"
|
||
description="Are you sure you want to delete this invoice? This will remove all associated data and cannot be recovered."
|
||
confirmText="Delete"
|
||
confirmVariant="destructive"
|
||
icon={Trash2}
|
||
iconColor="#ef4444"
|
||
loading={loading}
|
||
/>
|
||
</ScreenWrapper>
|
||
);
|
||
}
|
||
|
||
function ActionOption({
|
||
icon,
|
||
label,
|
||
description,
|
||
onPress,
|
||
danger,
|
||
loading,
|
||
}: {
|
||
icon: React.ReactNode;
|
||
label: string;
|
||
description: string;
|
||
onPress?: () => void;
|
||
danger?: boolean;
|
||
loading?: boolean;
|
||
}) {
|
||
return (
|
||
<Pressable
|
||
onPress={onPress}
|
||
disabled={loading}
|
||
className="flex-row items-center gap-3.5 p-4 mb-2 rounded-[6px] border border-border bg-card"
|
||
>
|
||
<View className="h-9 w-9 rounded-full bg-primary/10 items-center justify-center">
|
||
{loading ? <ActivityIndicator color="#E46212" size="small" /> : icon}
|
||
</View>
|
||
<View className="flex-1">
|
||
<Text
|
||
className={`font-sans-bold text-sm ${danger ? "text-red-500" : "text-foreground"}`}
|
||
>
|
||
{label}
|
||
</Text>
|
||
<Text className="text-muted-foreground text-[11px] font-sans-medium mt-px">
|
||
{description}
|
||
</Text>
|
||
</View>
|
||
</Pressable>
|
||
);
|
||
}
|