578 lines
20 KiB
TypeScript
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>
|
|
);
|
|
}
|