Yaltopia-Tickets-App/app/invoices/[id].tsx
2026-06-17 15:16:40 +03:00

906 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useCallback } from "react";
import {
View,
ScrollView,
ActivityIndicator,
Linking,
useColorScheme,
Pressable,
Modal,
Dimensions,
Image,
} 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,
CreditCard,
ChevronRight,
Check,
Edit,
Camera,
ArrowUpRight,
} 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 { EmptyState } from "@/components/EmptyState";
import { WebView } from "react-native-webview";
import { UploadIcon } from "lucide-react-native";
import ticketImage from "@/assets/ticket.png";
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" | "items" | "image">(
"details",
);
const [showImageFullScreen, setShowImageFullScreen] = useState(false);
const [imageLoading, setImageLoading] = useState(false);
const token = useAuthStore((state) => state.token);
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
: invoice.scannedData?.items?.length > 0
? invoice.scannedData.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",
};
// Scanned image URL — try several common fields
const scannedImageRaw =
invoice.scannedData?.imageUrl ||
invoice.scannedData?.image ||
invoice.scannedData?.imagePath ||
invoice.scannedData?.originalData?.imageUrl ||
invoice.imageUrl ||
invoice.imagePath ||
invoice.receiptPath ||
null;
const scannedImageUrl = scannedImageRaw
? scannedImageRaw.startsWith("http")
? scannedImageRaw
: `${BASE_URL}${scannedImageRaw.replace(/^\//, "")}`
: null;
const hasScannedImage = Boolean(invoice?.isScanned && scannedImageUrl);
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 paidDate = invoice.paidDate ? new Date(invoice.paidDate) : null;
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={{ marginBottom: -60, zIndex: 2 }}
>
<Image
source={ticketImage}
style={{ width: 150, height: 150 }}
resizeMode="contain"
/>
</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">
Due Date
</Text>
<Text className="text-foreground text-[12px] font-sans-bold">
{formatLongDate(paymentDate)}
</Text>
</View>
{paidDate && (
<View className="flex-row justify-between items-center">
<Text className="text-muted-foreground text-[12px] font-sans-medium">
Paid Date
</Text>
<View className="flex-row items-center gap-1.5">
<Check size={11} color="#16a34a" strokeWidth={3} />
<Text className="text-foreground text-[12px] font-sans-bold">
{formatLongDate(paidDate)}
</Text>
</View>
</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("items")} className="pb-2.5">
<Text
className={`text-[14px] font-sans-bold ${
activeTab === "items"
? "text-foreground"
: "text-muted-foreground"
}`}
>
Items
</Text>
{activeTab === "items" && (
<View className="absolute -bottom-px left-0 right-0 h-0.5 bg-foreground" />
)}
</Pressable>
{hasScannedImage && (
<Pressable
onPress={() => setActiveTab("image")}
className="pb-2.5"
>
<Text
className={`text-[14px] font-sans-bold ${
activeTab === "image"
? "text-foreground"
: "text-muted-foreground"
}`}
>
Image
</Text>
{activeTab === "image" && (
<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>
{invoice.notes && (
<View>
<Text className="text-foreground text-sm font-sans-bold mb-2">
Note
</Text>
<View className="rounded-[10px] bg-muted p-4">
<Text className="text-foreground font-sans-medium text-[13px] leading-5">
{invoice.notes}
</Text>
</View>
</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 || "No item"}
</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>
) : activeTab === "image" ? (
<View className="px-5 pt-5">
{hasScannedImage ? (
<View>
<Text className="text-foreground text-[16px] font-sans-black tracking-tight mb-3">
Scanned Document
</Text>
<Pressable
onPress={() => setShowImageFullScreen(true)}
className="rounded-[10px] overflow-hidden border border-border bg-card active:opacity-80"
>
<WebView
source={{ uri: scannedImageUrl || "" }}
style={{
width: "100%",
height: 360,
backgroundColor: isDark ? "#1F1F1F" : "#ffffff",
}}
originWhitelist={["*"]}
mixedContentMode="always"
scalesPageToFit
onLoadStart={() => setImageLoading(true)}
onLoadEnd={() => setImageLoading(false)}
onError={() => {
setImageLoading(false);
toast.error(
"Image Error",
"Failed to load scanned image.",
);
}}
renderError={() => (
<View className="flex-1 items-center justify-center p-4">
<Camera size={28} color="#94a3b8" />
<Text className="text-muted-foreground text-[12px] font-sans-medium mt-2 text-center">
Failed to load image
</Text>
</View>
)}
/>
{imageLoading && (
<View
className="absolute inset-0 items-center justify-center bg-card/40"
pointerEvents="none"
>
<ActivityIndicator color="#ea580c" size="large" />
</View>
)}
<View className="absolute bottom-3 right-3 h-9 w-9 rounded-full bg-black/60 items-center justify-center flex-row">
<ArrowUpRight size={16} color="#ffffff" strokeWidth={2.5} />
</View>
</Pressable>
<Text className="text-muted-foreground text-[11px] font-sans-medium mt-2 text-center">
Tap to view full screen
</Text>
</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">
<Camera size={22} color="#94a3b8" />
</View>
<Text className="text-foreground text-[14px] font-sans-bold mb-1">
No image available
</Text>
<Text className="text-muted-foreground text-[12px] font-sans-medium text-center">
The scanned image could not be found.
</Text>
</View>
)}
</View>
) : null}
</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>
{/* Full screen image viewer */}
<Modal
visible={showImageFullScreen}
transparent
animationType="fade"
onRequestClose={() => setShowImageFullScreen(false)}
>
<Pressable
className="flex-1 bg-black"
onPress={() => setShowImageFullScreen(false)}
>
<View className="flex-1">
{scannedImageUrl && (
<WebView
source={{ uri: scannedImageUrl }}
style={{ flex: 1, backgroundColor: "#000000" }}
originWhitelist={["*"]}
mixedContentMode="always"
scalesPageToFit
/>
)}
</View>
<Pressable
onPress={() => setShowImageFullScreen(false)}
className="absolute top-12 right-5 h-10 w-10 rounded-full bg-black/60 items-center justify-center border border-white/20"
>
<X size={18} color="#ffffff" strokeWidth={2.5} />
</Pressable>
</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>
);
}