Amba-Agent-App/app/(root)/(tabs)/history.tsx
2026-01-16 00:22:35 +03:00

578 lines
20 KiB
TypeScript

import React from "react";
import { View, Text, ScrollView, Image, TouchableOpacity } from "react-native";
import ScreenWrapper from "~/components/ui/ScreenWrapper";
import { Input } from "~/components/ui/input";
import { Icons } from "~/assets/icons";
import TopBar from "~/components/ui/topBar";
import BottomSheet from "~/components/ui/bottomSheet";
type TransactionStatus = "success" | "pending" | "failed";
type AgentTransaction = {
id: string;
clientName: string;
amount: string;
currency: string;
status: TransactionStatus;
date: string;
time: string;
method: "telebirr" | "chapa" | "bank" | "cash";
reference?: string;
};
const MOCK_TRANSACTIONS: AgentTransaction[] = [
{
id: "tx-1",
clientName: "Abebe Kebede",
amount: "4,500",
currency: "ETB",
status: "success",
date: "Today",
time: "09:32 AM",
method: "telebirr",
reference: "REF-938201",
},
{
id: "tx-2",
clientName: "Sara Alemu",
amount: "250",
currency: "USD",
status: "pending",
date: "Today",
time: "01:15 PM",
method: "chapa",
reference: "REF-938202",
},
{
id: "tx-3",
clientName: "Hope Community Fund",
amount: "32,000",
currency: "ETB",
status: "success",
date: "Yesterday",
time: "05:47 PM",
method: "bank",
reference: "REF-937812",
},
{
id: "tx-4",
clientName: "Kidus Tadesse",
amount: "900",
currency: "ETB",
status: "failed",
date: "Mon, 06 Jan",
time: "10:02 AM",
method: "telebirr",
reference: "REF-937111",
},
{
id: "tx-5",
clientName: "Blue Nile Traders",
amount: "12,750",
currency: "ETB",
status: "success",
date: "Sun, 05 Jan",
time: "03:28 PM",
method: "cash",
reference: "REF-936444",
},
];
const getStatusPillClasses = (status: TransactionStatus) => {
switch (status) {
case "success":
return "bg-green-100 text-green-700";
case "pending":
return "bg-yellow-100 text-yellow-700";
case "failed":
default:
return "bg-red-100 text-red-700";
}
};
export default function AgentTab() {
const [search, setSearch] = React.useState("");
const [statusFilter, setStatusFilter] = React.useState<
"all" | TransactionStatus
>("all");
const [methodFilter, setMethodFilter] = React.useState<
"all" | "telebirr" | "chapa" | "bank" | "cash"
>("all");
const [selectedTx, setSelectedTx] = React.useState<AgentTransaction | null>(
null
);
const [filterSheetVisible, setFilterSheetVisible] = React.useState(false);
const normalizedSearch = search.trim().toLowerCase();
const filteredTransactions = React.useMemo(() => {
return MOCK_TRANSACTIONS.filter((tx) => {
const matchesStatus =
statusFilter === "all" || tx.status === statusFilter;
const matchesMethod =
methodFilter === "all" || tx.method === methodFilter;
const matchesSearch =
!normalizedSearch ||
tx.clientName.toLowerCase().includes(normalizedSearch) ||
(tx.reference && tx.reference.toLowerCase().includes(normalizedSearch));
return matchesStatus && matchesMethod && matchesSearch;
});
}, [statusFilter, methodFilter, normalizedSearch]);
const {
todayTotalEtb,
weekTotalEtb,
totalTxns,
successCount,
pendingCount,
failedCount,
} = React.useMemo(() => {
const parseAmount = (amount: string) =>
parseInt(amount.replace(/,/g, ""), 10) || 0;
let todayTotal = 0;
let weekTotal = 0;
let success = 0;
let pending = 0;
let failed = 0;
MOCK_TRANSACTIONS.forEach((tx) => {
if (tx.status === "success") success += 1;
if (tx.status === "pending") pending += 1;
if (tx.status === "failed") failed += 1;
if (tx.currency === "ETB") {
const val = parseAmount(tx.amount);
weekTotal += val;
if (tx.date === "Today") {
todayTotal += val;
}
}
});
return {
todayTotalEtb: todayTotal,
weekTotalEtb: weekTotal,
totalTxns: MOCK_TRANSACTIONS.length,
successCount: success,
pendingCount: pending,
failedCount: failed,
};
}, []);
const groupedTransactions = React.useMemo(() => {
const groups: Record<string, AgentTransaction[]> = {};
filteredTransactions.forEach((tx) => {
if (!groups[tx.date]) {
groups[tx.date] = [];
}
groups[tx.date].push(tx);
});
return Object.entries(groups);
}, [filteredTransactions]);
return (
<ScreenWrapper edges={[]}>
<View className="flex-1 bg-white">
<TopBar />
<ScrollView
className="flex-1"
contentContainerStyle={{ paddingHorizontal: 20, paddingBottom: 70 }}
showsVerticalScrollIndicator={false}
>
<View className="mb-4">
<Text className="text-xl font-dmsans-bold text-primary mb-1">
Transactions
</Text>
<Text className="text-sm font-dmsans text-gray-500">
All the transactions you have processed. UI-only reporting &
tracking.
</Text>
<Text className="text-[10px] font-dmsans text-gray-400 mt-0.5">
Last updated: Just now (mock data)
</Text>
</View>
{/* Search / filter bar */}
<View className="mb-3">
<Input
value={search}
onChangeText={setSearch}
placeholderText="Search by client or reference"
containerClassName="w-full"
borderClassName="border-[#D9DBE9] bg-white"
placeholderColor="#7E7E7E"
textClassName="text-[#000] text-sm"
rightIcon={
<TouchableOpacity
className="p-1"
onPress={() => setFilterSheetVisible(true)}
>
<Image
source={Icons.filterIcon}
style={{ width: 25, height: 25, tintColor: "#0F7B4A" }}
resizeMode="contain"
/>
</TouchableOpacity>
}
/>
</View>
{/* Optional summary row (dummy numbers) */}
<View className="flex-row bg-primary/5 rounded-2xl px-4 py-3 mb-4">
<View className="flex-1 mr-2">
<Text className="text-[11px] font-dmsans text-gray-500 mb-1">
Today
</Text>
<Text className="text-sm font-dmsans-bold text-gray-900">
ETB {todayTotalEtb.toLocaleString("en-ET")}
</Text>
</View>
<View className="flex-1 mr-2">
<Text className="text-[11px] font-dmsans text-gray-500 mb-1">
This week
</Text>
<Text className="text-sm font-dmsans-bold text-gray-900">
ETB {weekTotalEtb.toLocaleString("en-ET")}
</Text>
</View>
<View className="flex-1">
<Text className="text-[11px] font-dmsans text-gray-500 mb-1">
Total txns
</Text>
<Text className="text-sm font-dmsans-bold text-gray-900">
{totalTxns}
</Text>
</View>
</View>
<Text className="text-[10px] font-dmsans text-gray-400 mb-3">
Success: {successCount} · Pending: {pendingCount} · Failed:{" "}
{failedCount}
</Text>
{/* Transactions list */}
{groupedTransactions.map(([dateLabel, txs]) => (
<View key={dateLabel} className="mb-2">
<Text className="text-[11px] font-dmsans text-gray-400 mb-1 mt-1">
{dateLabel}
</Text>
{txs.map((tx) => {
const pillClasses = getStatusPillClasses(tx.status);
return (
<TouchableOpacity
key={tx.id}
activeOpacity={0.9}
onPress={() => setSelectedTx(tx)}
>
<View
className="bg-white rounded-3xl mb-3 border border-gray-100"
style={{
shadowColor: "#000",
shadowOpacity: 0.02,
shadowRadius: 40,
shadowOffset: { width: 0, height: 8 },
elevation: 2,
}}
>
<View className="px-4 py-3">
<View className="flex-row items-center justify-between mb-1.5">
<Text className="text-base font-dmsans-bold text-gray-900">
{tx.currency} {tx.amount}
</Text>
<View
className={`px-2 py-0.5 rounded-full ${pillClasses}`}
>
<Text className="text-[10px] font-dmsans-medium">
{tx.status === "success"
? "Success"
: tx.status === "pending"
? "Pending"
: "Failed"}
</Text>
</View>
</View>
<View className="flex-row items-center justify-between mb-1">
<Text className="text-sm font-dmsans text-gray-800">
{tx.clientName}
</Text>
<Text className="text-[11px] font-dmsans text-gray-400">
{tx.time}
</Text>
</View>
<View className="flex-row items-center justify-between mt-1">
<View className="flex-row items-center">
<View className="w-7 h-7 rounded-full bg-primary/10 items-center justify-center mr-2">
<Image
source={Icons.bottomTransferIcon}
style={{
width: 16,
height: 16,
tintColor: "#0F7B4A",
}}
resizeMode="contain"
/>
</View>
<Text className="text-[11px] font-dmsans text-gray-500">
{tx.method === "telebirr"
? "Telebirr"
: tx.method === "chapa"
? "Chapa"
: tx.method === "bank"
? "Bank transfer"
: "Cash"}
</Text>
</View>
{tx.reference && (
<Text className="text-[11px] font-dmsans text-gray-400">
{tx.reference}
</Text>
)}
</View>
</View>
</View>
</TouchableOpacity>
);
})}
</View>
))}
{groupedTransactions.length === 0 && (
<View className="mt-10 items-center">
<Text className="text-sm font-dmsans text-gray-400">
No transactions match your filters.
</Text>
</View>
)}
</ScrollView>
</View>
{/* Filter BottomSheet */}
<BottomSheet
visible={filterSheetVisible}
onClose={() => setFilterSheetVisible(false)}
maxHeightRatio={0.5}
>
<View className="w-full px-5 pt-4 pb-6">
<Text className="text-base font-dmsans-bold text-primary mb-3 text-center">
Filters
</Text>
{/* Status filter chips */}
<Text className="text-[11px] font-dmsans text-gray-500 mb-1">
Status
</Text>
<View className="flex-row flex-wrap mb-3">
{["all", "success", "pending", "failed"].map((status) => {
const isActive = statusFilter === status;
const label =
status === "all"
? "All"
: status === "success"
? "Success"
: status === "pending"
? "Pending"
: "Failed";
return (
<TouchableOpacity
key={status}
className={`px-3 py-1 mr-2 mb-2 rounded-full border ${
isActive
? "bg-primary border-primary"
: "bg-white border-gray-200"
}`}
onPress={() =>
setStatusFilter(status as "all" | TransactionStatus)
}
>
<Text
className={`text-[11px] font-dmsans-medium ${
isActive ? "text-white" : "text-gray-600"
}`}
>
{label}
</Text>
</TouchableOpacity>
);
})}
</View>
{/* Method filter chips */}
<Text className="text-[11px] font-dmsans text-gray-500 mb-1">
Method
</Text>
<View className="flex-row flex-wrap mb-3">
{["all", "telebirr", "chapa", "bank", "cash"].map((method) => {
const isActive = methodFilter === method;
const label =
method === "all"
? "All methods"
: method === "telebirr"
? "Telebirr"
: method === "chapa"
? "Chapa"
: method === "bank"
? "Bank"
: "Cash";
return (
<TouchableOpacity
key={method}
className={`px-3 py-1 mr-2 mb-2 rounded-full border ${
isActive
? "bg-primary/10 border-primary"
: "bg-white border-gray-200"
}`}
onPress={() =>
setMethodFilter(
method as "all" | "telebirr" | "chapa" | "bank" | "cash"
)
}
>
<Text
className={`text-[11px] font-dmsans-medium ${
isActive ? "text-primary" : "text-gray-600"
}`}
>
{label}
</Text>
</TouchableOpacity>
);
})}
</View>
<View className="flex-row gap-3 mt-1">
<TouchableOpacity
activeOpacity={0.8}
className="flex-1 bg-white border border-[#E5E7EB] rounded-2xl py-3 items-center justify-center"
onPress={() => {
setStatusFilter("all");
setMethodFilter("all");
}}
>
<Text className="text-sm font-dmsans-medium text-gray-800">
Reset
</Text>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={0.8}
className="flex-1 bg-primary rounded-2xl py-3 items-center justify-center"
onPress={() => setFilterSheetVisible(false)}
>
<Text className="text-sm font-dmsans-medium text-white">
Apply
</Text>
</TouchableOpacity>
</View>
</View>
</BottomSheet>
<BottomSheet
visible={!!selectedTx}
onClose={() => setSelectedTx(null)}
maxHeightRatio={0.6}
>
{selectedTx && (
<View className="w-full px-5 pt-4 pb-6">
<Text className="text-base font-dmsans-bold text-primary mb-2 text-center">
Transaction Details
</Text>
<Text className="text-2xl font-dmsans-bold text-gray-900 text-center mb-1">
{selectedTx.currency} {selectedTx.amount}
</Text>
<View
className={`self-center px-3 py-1 rounded-full ${getStatusPillClasses(
selectedTx.status
)} mb-4`}
>
<Text className="text-[11px] font-dmsans-medium">
{selectedTx.status === "success"
? "Success"
: selectedTx.status === "pending"
? "Pending"
: "Failed"}
</Text>
</View>
<View className="space-y-2 mb-5">
<View className="flex-row justify-between">
<Text className="text-[12px] font-dmsans text-gray-500">
Client
</Text>
<Text className="text-[12px] font-dmsans-medium text-gray-900">
{selectedTx.clientName}
</Text>
</View>
<View className="flex-row justify-between">
<Text className="text-[12px] font-dmsans text-gray-500">
Date & time
</Text>
<Text className="text-[12px] font-dmsans-medium text-gray-900">
{selectedTx.date} · {selectedTx.time}
</Text>
</View>
<View className="flex-row justify-between items-center">
<Text className="text-[12px] font-dmsans text-gray-500">
Method
</Text>
<View className="flex-row items-center">
<View className="w-6 h-6 rounded-full bg-primary/10 items-center justify-center mr-1.5">
<Image
source={Icons.bottomTransferIcon}
style={{ width: 14, height: 14, tintColor: "#0F7B4A" }}
resizeMode="contain"
/>
</View>
<Text className="text-[12px] font-dmsans-medium text-gray-900">
{selectedTx.method === "telebirr"
? "Telebirr"
: selectedTx.method === "chapa"
? "Chapa"
: selectedTx.method === "bank"
? "Bank transfer"
: "Cash"}
</Text>
</View>
</View>
{selectedTx.reference && (
<View className="flex-row justify-between">
<Text className="text-[12px] font-dmsans text-gray-500">
Reference
</Text>
<Text className="text-[12px] font-dmsans-medium text-gray-900">
{selectedTx.reference}
</Text>
</View>
)}
</View>
<View className="flex-row gap-3 mt-2">
<TouchableOpacity
activeOpacity={0.8}
className="flex-1 bg-white border border-[#E5E7EB] rounded-2xl py-3 items-center justify-center"
>
<Text className="text-sm font-dmsans-medium text-gray-800">
Repeat Payment
</Text>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={0.8}
className="flex-1 bg-primary rounded-2xl py-3 items-center justify-center"
>
<Text className="text-sm font-dmsans-medium text-white">
Share Receipt
</Text>
</TouchableOpacity>
</View>
</View>
)}
</BottomSheet>
</ScreenWrapper>
);
}