import React from "react"; import { View, Text, FlatList, Image, ScrollView, TouchableOpacity, } from "react-native"; import { router } from "expo-router"; import { ROUTES } from "~/lib/routes"; import TransactionCard from "~/components/ui/transactionCard"; import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; import { useTransactions } from "~/lib/hooks/useTransactions"; import ScreenWrapper from "~/components/ui/ScreenWrapper"; import { Icons } from "~/assets/icons"; import { Input } from "~/components/ui/input"; import Skeleton from "~/components/ui/skeleton"; import BottomSheet from "~/components/ui/bottomSheet"; import ModalToast from "~/components/ui/toast"; import { useTranslation } from "react-i18next"; import BackButton from "~/components/ui/backButton"; type TransactionTypeFilter = "all" | "incoming" | "outgoing"; interface DateRange { startDate: Date | null; endDate: Date | null; } const daysOfWeek = ["S", "M", "T", "W", "T", "F", "S"]; const stripTime = (date: Date) => { const d = new Date(date); d.setHours(0, 0, 0, 0); return d; }; const isSameDay = (a: Date | null, b: Date | null) => { if (!a || !b) return false; return ( a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate() ); }; const SimpleCalendarRange: React.FC<{ value: DateRange; onChange: (range: DateRange) => void; }> = ({ value, onChange }) => { const initialBase = value.startDate || new Date(); const [currentMonth, setCurrentMonth] = React.useState( () => new Date(initialBase.getFullYear(), initialBase.getMonth(), 1) ); const monthLabel = currentMonth.toLocaleString("default", { month: "short", year: "numeric", }); const days: (Date | null)[] = React.useMemo(() => { const year = currentMonth.getFullYear(); const month = currentMonth.getMonth(); const firstDayOfMonth = new Date(year, month, 1); const firstWeekday = firstDayOfMonth.getDay(); const numDays = new Date(year, month + 1, 0).getDate(); const arr: (Date | null)[] = []; for (let i = 0; i < firstWeekday; i++) { arr.push(null); } for (let d = 1; d <= numDays; d++) { arr.push(new Date(year, month, d)); } return arr; }, [currentMonth]); const handleSelectDay = (day: Date) => { const { startDate, endDate } = value; if (!startDate || (startDate && endDate)) { onChange({ startDate: stripTime(day), endDate: null }); return; } const start = stripTime(startDate); const selected = stripTime(day); if (selected < start) { onChange({ startDate: selected, endDate: start }); } else { onChange({ startDate: start, endDate: selected }); } }; const inRange = (day: Date) => { const { startDate, endDate } = value; if (!startDate || !endDate) return false; const d = stripTime(day); const s = stripTime(startDate); const e = stripTime(endDate); return d > s && d < e; }; const goMonth = (offset: number) => { setCurrentMonth( (prev) => new Date(prev.getFullYear(), prev.getMonth() + offset, 1) ); }; return ( goMonth(-1)} className="w-10 h-10 rounded-full bg-white items-center justify-center" > {"<"} {monthLabel} goMonth(1)} className="w-10 h-10 rounded-full bg-white items-center justify-center" > {">"} {daysOfWeek.map((d, idx) => ( {d} ))} {days.map((day, index) => { if (!day) { return ( ); } const selectedStart = isSameDay(day, value.startDate); const selectedEnd = isSameDay(day, value.endDate); const between = inRange(day); const isSelected = selectedStart || selectedEnd; const bgColor = isSelected ? "#0F7B4A" : between ? "rgba(15,123,74,0.08)" : "transparent"; const textColor = isSelected ? "#ffffff" : "#111827"; return ( handleSelectDay(day)} style={{ width: `${100 / 7}%`, aspectRatio: 1 }} className="items-center justify-center" > {day.getDate()} ); })} ); }; export default function History() { const { user } = useAuthWithProfile(); const { transactions, loading: transactionsLoading, error: transactionsError, } = useTransactions(user?.uid); const { t } = useTranslation(); const [searchQuery, setSearchQuery] = React.useState(""); const [filterVisible, setFilterVisible] = React.useState(false); const [dateRange, setDateRange] = React.useState({ startDate: null, endDate: null, }); const [typeFilter, setTypeFilter] = React.useState("all"); const [toastVisible, setToastVisible] = React.useState(false); const [toastTitle, setToastTitle] = React.useState(""); const [toastDescription, setToastDescription] = React.useState< string | undefined >(undefined); const [toastVariant, setToastVariant] = React.useState< "success" | "error" | "warning" | "info" >("info"); const toastTimeoutRef = React.useRef | null>( null ); const showToast = ( title: string, description?: string, variant: "success" | "error" | "warning" | "info" = "info" ) => { if (toastTimeoutRef.current) { clearTimeout(toastTimeoutRef.current); } setToastTitle(title); setToastDescription(description); setToastVariant(variant); setToastVisible(true); toastTimeoutRef.current = setTimeout(() => { setToastVisible(false); toastTimeoutRef.current = null; }, 2500); }; React.useEffect(() => { return () => { if (toastTimeoutRef.current) { clearTimeout(toastTimeoutRef.current); } }; }, []); React.useEffect(() => { if (transactionsError) { showToast( t("history.toastErrorTitle"), transactionsError || t("history.errorTitle"), "error" ); } }, [transactionsError, t]); const filteredTransactions = React.useMemo(() => { let data = transactions || []; const query = searchQuery.trim().toLowerCase(); if (query) { data = data.filter((t) => { const parts: string[] = []; if ((t as any).recipientName) parts.push(String((t as any).recipientName)); if ((t as any).senderName) parts.push(String((t as any).senderName)); if ((t as any).note) parts.push(String((t as any).note)); parts.push(String(t.type)); const haystack = parts.join(" ").toLowerCase(); return haystack.includes(query); }); } if (dateRange.startDate || dateRange.endDate) { const start = dateRange.startDate ? stripTime(dateRange.startDate) : null; const end = dateRange.endDate ? stripTime(dateRange.endDate) : null; data = data.filter((t) => { const created = t.createdAt instanceof Date ? stripTime(t.createdAt) : stripTime(new Date(t.createdAt)); if (start && created < start) return false; if (end && created > end) return false; return true; }); } if (typeFilter !== "all") { data = data.filter((t) => { const txType = t.type; const isOutgoing = txType === "send" || txType === "cash_out"; const isIncoming = txType === "receive" || txType === "add_cash"; if (typeFilter === "outgoing") return isOutgoing; if (typeFilter === "incoming") return isIncoming; return true; }); } return data; }, [transactions, searchQuery, dateRange, typeFilter]); return ( {transactionsLoading ? ( {t("history.title")} {t("history.subtitle")} {Array.from({ length: 5 }).map((_, index) => ( ))} ) : transactionsError ? ( {t("history.title")} {t("history.subtitle")} {t("history.errorTitle")} {transactionsError} ) : ( <> item.id} contentContainerStyle={{ paddingBottom: 30 }} ListHeaderComponent={ <> {t("history.title")} {t("history.subtitle")} setFilterVisible(true)} className="p-1" > } /> } ListEmptyComponent={ {t("history.emptyTitle")} {t("history.emptySubtitle")} } renderItem={({ item: transaction }) => ( { router.push({ pathname: ROUTES.TRANSACTION_DETAIL, params: { transactionId: transaction.id, amount: transaction.amount.toString(), type: transaction.type, recipientName: transaction.type === "send" ? transaction.recipientName : transaction.type === "receive" ? transaction.senderName : transaction.type === "add_cash" ? "Card" : "Bank", date: transaction.createdAt.toISOString(), status: transaction.status, //@ts-ignore note: transaction?.note || "", fromHistory: "true", }, }); }} /> )} /> )} setFilterVisible(false)} maxHeightRatio={0.9} > {t("history.filterTitle")} {t("history.filterSubtitle")} {/* Date range */} {t("history.dateRangeLabel")} {t("history.fromLabel")} {dateRange.startDate ? dateRange.startDate.toLocaleDateString() : t("history.selectStart")} {t("history.toLabel")} {dateRange.endDate ? dateRange.endDate.toLocaleDateString() : t("history.selectEnd")} setDateRange({ startDate: null, endDate: null })} > {t("history.clearDates")} {/* Type filter */} {t("history.typeLabel")} {( [ { label: t("history.typeAll"), value: "all" }, { label: t("history.typeIncoming"), value: "incoming" }, { label: t("history.typeOutgoing"), value: "outgoing" }, ] as { label: string; value: TransactionTypeFilter }[] ).map((option) => { const active = typeFilter === option.value; return ( setTypeFilter(option.value)} className={`px-4 py-2 rounded-[4px] mr-2 border ${ active ? "bg-primary border-primary" : "bg-white border-gray-200" }`} > {option.label} ); })} setFilterVisible(false)} className="h-11 rounded-3xl bg-[#FFB668] items-center justify-center" > {t("history.applyFilters")} ); }