import React, { useState, useEffect, useMemo, useCallback } from 'react';
import styled from 'styled-components';
import { motion, AnimatePresence } from 'framer-motion';
import { Search, Filter, X, RefreshCw, AlertCircle, TrendingUp, TrendingDown, ArrowUpDown } from 'lucide-react';
import {
ResponsiveContainer,
BarChart,
Bar,
LineChart,
Line,
PieChart,
Pie,
Cell,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
} from 'recharts';
import { toast } from 'react-hot-toast'; // Assuming you use react-hot-toast
// Assume 'api' is configured and imported from your api setup
// import api from '../api';
// Assume 'user' object is available from a context or props
// const { user } = useAuth();
// Styled Components (as provided in the prompt)
const colors = {
primary: '#667eea',
primaryDark: '#5a67d8',
secondary: '#764ba2',
success: '#48bb78',
error: '#ff6b6b',
warning: '#f6ad55',
text: '#1a202c',
textSecondary: '#4a5568',
textMuted: '#a0aec0',
background: 'rgba(255, 255, 255, 0.95)',
border: 'rgba(102, 126, 234, 0.2)',
borderHover: '#667eea',
glass: 'rgba(255, 255, 255, 0.95)',
glassLight: 'rgba(255, 255, 255, 0.8)',
shadow: 'rgba(0, 0, 0, 0.1)',
shadowHover: 'rgba(102, 126, 234, 0.1)'
};
const gradients = {
primary: `linear-gradient(135deg, ${colors.primary}, ${colors.secondary})`,
};
const spacing = {
sm: '8px',
md: '12px',
lg: '16px',
xl: '20px',
xxl: '24px',
xxxl: '32px'
};
const Container = styled(motion.div)`
padding: ${spacing.xxxl};
max-width: 1600px;
margin: 0 auto;
h2 {
font-size: 32px;
font-weight: 800;
color: ${colors.text};
margin-bottom: ${spacing.xxxl};
background: ${gradients.primary};
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-align: center;
}
@media (max-width: 768px) { padding: ${spacing.xl}; }
`;
const FilterSection = styled(motion.div)`
background: ${colors.glass};
backdrop-filter: blur(20px);
border-radius: 20px;
padding: ${spacing.xxl};
margin-bottom: ${spacing.xxxl};
box-shadow: 0 8px 32px ${colors.shadow};
border: 1px solid rgba(255, 255, 255, 0.2);
`;
const SearchContainer = styled.div`
position: relative;
margin-bottom: ${spacing.xl};
svg {
position: absolute;
left: ${spacing.lg};
top: 50%;
transform: translateY(-50%);
color: ${colors.textMuted};
}
`;
const SearchInput = styled.input`
width: 100%;
padding: ${spacing.lg} ${spacing.lg} ${spacing.lg} 48px;
border: 2px solid ${colors.border};
border-radius: 12px;
font-size: 16px;
background: ${colors.glassLight};
&:focus {
outline: none;
border-color: ${colors.borderHover};
box-shadow: 0 0 0 4px ${colors.shadowHover};
}
`;
const FiltersGrid = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: ${spacing.lg};
margin-bottom: ${spacing.xl};
`;
const FilterSelect = styled.select`
padding: ${spacing.lg};
border: 2px solid ${colors.border};
border-radius: 12px;
font-size: 16px;
background: ${colors.glassLight};
cursor: pointer;
&:focus {
outline: none;
border-color: ${colors.borderHover};
box-shadow: 0 0 0 4px ${colors.shadowHover};
}
`;
const DateInput = styled.input`
padding: ${spacing.lg};
border: 2px solid ${colors.border};
border-radius: 12px;
font-size: 16px;
background: ${colors.glassLight};
color: ${colors.textSecondary};
cursor: pointer;
&:focus {
outline: none;
border-color: ${colors.borderHover};
box-shadow: 0 0 0 4px ${colors.shadowHover};
}
`;
const ButtonGroup = styled.div`
display: flex;
gap: ${spacing.md};
justify-content: flex-end;
margin-top: ${spacing.xl};
`;
const Button = styled(motion.button)`
display: flex;
align-items: center;
gap: ${spacing.sm};
padding: ${spacing.md} ${spacing.xl};
border-radius: 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.3s ease;
${props => props.variant === 'primary' ? `
background: ${gradients.primary};
color: white;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
&:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
}
` : `
background: transparent;
color: ${colors.textSecondary};
border-color: #e2e8f0;
&:hover:not(:disabled) {
background: #f7fafc;
border-color: #cbd5e0;
}
`}
&:disabled { opacity: 0.6; cursor: not-allowed; }
`;
const AnalyticsGrid = styled(motion.div)`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: ${spacing.xxl};
margin-bottom: ${spacing.xxxl};
`;
const ChartContainer = styled.div`
background: ${colors.glass};
backdrop-filter: blur(20px);
border-radius: 20px;
padding: ${spacing.xl};
box-shadow: 0 8px 32px ${colors.shadow};
border: 1px solid rgba(255, 255, 255, 0.2);
h3 {
font-size: 18px;
font-weight: 600;
color: ${colors.text};
margin-bottom: ${spacing.xl};
}
`;
const TableContainer = styled.div`
background: ${colors.glass};
backdrop-filter: blur(20px);
border-radius: 20px;
overflow: hidden;
box-shadow: 0 8px 32px ${colors.shadow};
`;
const Table = styled.table`
width: 100%;
border-collapse: separate;
border-spacing: 0;
th {
background: ${gradients.primary};
color: white;
padding: ${spacing.lg};
text-align: left;
font-weight: 600;
font-size: 14px;
cursor: pointer;
white-space: nowrap;
&:hover { background: ${colors.primaryDark}; }
}
td {
padding: ${spacing.lg};
color: ${colors.textSecondary};
border-bottom: 1px solid rgba(226, 232, 240, 0.8);
}
tr:last-child td { border-bottom: none; }
tr:hover { background: ${colors.shadowHover}; }
`;
const StatusBadge = styled.span`
padding: 6px ${spacing.md};
border-radius: 20px;
font-size: 12px;
font-weight: 600;
${({ status }) => {
switch (status) {
case 'SUCCESS': return `background-color: ${colors.success}20; color: ${colors.success};`;
case 'PENDING': return `background-color: ${colors.warning}20; color: ${colors.warning};`;
case 'FAILED': return `background-color: ${colors.error}20; color: ${colors.error};`;
default: return `background-color: ${colors.textMuted}20; color: ${colors.textMuted};`;
}
}}
`;
const TypeBadge = styled.span`
padding: 6px ${spacing.md};
border-radius: 20px;
font-size: 12px;
font-weight: 600;
${({ type }) => {
switch (type) {
case 'DEPOSIT': return `background-color: ${colors.success}20; color: ${colors.success};`;
case 'WITHDRAW': return `background-color: ${colors.error}20; color: ${colors.error};`;
case 'TRANSFER': return `background-color: ${colors.primary}20; color: ${colors.primary};`;
default: return `background-color: ${colors.textMuted}20; color: ${colors.textMuted};`;
}
}}
`;
const LoadingSpinner = styled(motion.div)`
display: flex; justify-content: center; padding: 64px;
svg { animation: spin 1s linear infinite; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
`;
const ErrorMessage = styled(motion.div)`
display: flex; flex-direction: column; align-items: center; gap: ${spacing.lg};
padding: 64px; text-align: center; color: ${colors.error};
`;
const EmptyState = styled(motion.div)`
display: flex; flex-direction: column; align-items: center; gap: ${spacing.lg};
padding: 64px; text-align: center; color: ${colors.textMuted};
`;
// Mock user for demonstration. In a real app, this would come from an auth context.
const user = { id: 1, name: 'Kishan' };
// Mock API for demonstration
const api = {
get: async (url) => {
console.log(`Fetching ${url}...`);
await new Promise(res => setTimeout(res, 500)); // Simulate network delay
if (url.startsWith('/accounts/user/')) {
return { data: { message: "Accounts retrieved successfully", data: [ { id: 1, accountNumber: "98765432155", accountType: "SAVINGS", balance: 1450.01, userDTO: { id: 1, name: "kishan" } }, { id: 52, accountNumber: "98765432188", accountType: "SALARY", balance: 25000.00, userDTO: { id: 1, name: "kishan" } } ] }};
}
if (url.startsWith('/transactions/user/')) {
return { data: { message: "Transactions fetched successfully", data: [ { id: 1, fromAccount: { id: 52, accountNumber: "98765432188" }, toAccount: { id: 1, accountNumber: "98765432155" }, type: "TRANSFER", amount: 500.00, description: "Monthly savings", timestamp: "2025-07-01T10:00:00Z", status: "SUCCESS" }, { id: 2, toAccount: { id: 52, accountNumber: "98765432188" }, type: "DEPOSIT", amount: 25000.00, description: "July Salary", timestamp: "2025-07-01T09:00:00Z", status: "SUCCESS" }, { id: 3, fromAccount: { id: 1, accountNumber: "98765432155" }, type: "WITHDRAW", amount: 150.00, description: "Grocery Shopping", timestamp: "2025-07-02T18:30:00Z", status: "SUCCESS" }, { id: 4, fromAccount: { id: 1, accountNumber: "98765432155" }, type: "WITHDRAW", amount: 80.00, description: "Dinner with friends", timestamp: "2025-06-15T20:00:00Z", status: "SUCCESS" }, { id: 5, toAccount: { id: 1, accountNumber: "98765432155" }, type: "DEPOSIT", amount: 1000.00, description: "Freelance Payment", timestamp: "2025-06-20T14:00:00Z", status: "SUCCESS" } ] }};
}
return { data: { data: [] } };
}
};
const TransactionAnalytics = () => {
const [transactions, setTransactions] = useState([]);
const [userAccounts, setUserAccounts] = useState([]);
const [loading, setLoading] = useState(true);
const [accountsLoading, setAccountsLoading] = useState(true);
const [error, setError] = useState(null);
const [filters, setFilters] = useState({
searchTerm: '',
account: 'all',
type: 'all',
startDate: '',
endDate: '',
});
const [sortConfig, setSortConfig] = useState({ key: 'timestamp', direction: 'desc' });
// --- DATA FETCHING ---
const fetchUserAccounts = useCallback(async () => {
if (!user?.id) return;
try {
setAccountsLoading(true);
const response = await api.get(`/accounts/user/${user.id}`);
setUserAccounts(response.data.data);
} catch (err) {
console.error('Error fetching user accounts:', err);
toast.error('Failed to load user accounts');
} finally {
setAccountsLoading(false);
}
}, []); // user.id is static in this example
const fetchTransactions = useCallback(async () => {
if (!user?.id) return;
try {
setLoading(true);
setError(null);
const response = await api.get(`/transactions/user/${user.id}`);
setTransactions(response.data.data);
} catch (err) {
setError('Failed to load transactions. Please try again.');
console.error('Error fetching transactions:', err);
} finally {
setLoading(false);
}
}, []); // user.id is static
const handleRefresh = () => {
toast.success('Refreshing data...');
fetchUserAccounts();
fetchTransactions();
}
useEffect(() => {
handleRefresh();
}, [fetchUserAccounts, fetchTransactions]);
// --- FILTERING & SORTING ---
const filteredAndSortedTransactions = useMemo(() => {
let filtered = transactions.filter(t => {
const transactionDate = new Date(t.timestamp);
const startDate = filters.startDate ? new Date(filters.startDate) : null;
const endDate = filters.endDate ? new Date(filters.endDate) : null;
if(startDate) startDate.setHours(0,0,0,0);
if(endDate) endDate.setHours(23,59,59,999);
const inDateRange = (!startDate || transactionDate >= startDate) && (!endDate || transactionDate <= endDate);
const inSearch = t.description.toLowerCase().includes(filters.searchTerm.toLowerCase());
const inAccount = filters.account === 'all' || t.fromAccount?.accountNumber === filters.account || t.toAccount?.accountNumber === filters.account;
const inType = filters.type === 'all' || t.type === filters.type;
return inDateRange && inSearch && inAccount && inType;
});
return filtered.sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? 1 : -1;
}
return 0;
});
}, [transactions, filters, sortConfig]);
const handleSort = (key) => {
setSortConfig(prev => ({
key,
direction: prev.key === key && prev.direction === 'asc' ? 'desc' : 'asc'
}));
};
const handleFilterChange = (e) => {
const { name, value } = e.target;
setFilters(prev => ({ ...prev, [name]: value }));
};
const resetFilters = () => {
setFilters({ searchTerm: '', account: 'all', type: 'all', startDate: '', endDate: '' });
setSortConfig({ key: 'timestamp', direction: 'desc' });
toast.success("Filters Reset");
};
// --- CHART DATA PROCESSING ---
const analyticsData = useMemo(() => {
const incomeVsExpense = [
{ name: 'Income', amount: filteredAndSortedTransactions.filter(t => t.type === 'DEPOSIT').reduce((sum, t) => sum + t.amount, 0) },
{ name: 'Expenses', amount: filteredAndSortedTransactions.filter(t => ['WITHDRAW', 'TRANSFER'].includes(t.type)).reduce((sum, t) => sum + t.amount, 0) }
];
const typeDistribution = Object.entries(
filteredAndSortedTransactions.reduce((acc, t) => {
acc[t.type] = (acc[t.type] || 0) + 1;
return acc;
}, {})
).map(([name, value]) => ({ name, value }));
const trendData = filteredAndSortedTransactions
.map(t => ({ ...t, date: new Date(t.timestamp).toISOString().split('T')[0] }))
.reduce((acc, t) => {
if (!acc[t.date]) acc[t.date] = { date: t.date, amount: 0 };
acc[t.date].amount += t.amount;
return acc;
}, {});
const sortedTrendData = Object.values(trendData).sort((a,b) => new Date(a.date) - new Date(b.date));
return { incomeVsExpense, typeDistribution, trendData: sortedTrendData };
}, [filteredAndSortedTransactions]);
const PIE_COLORS = {
DEPOSIT: colors.success,
WITHDRAW: colors.error,
TRANSFER: colors.primary
};
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div style={{ background: 'white', padding: '10px', border: `1px solid ${colors.border}`, borderRadius: '10px' }}>
<p style={{ fontWeight: 600, color: colors.text }}>{`Date: ${label}`}</p>
<p style={{ color: colors.primary }}>{`Amount: $${payload[0].value.toFixed(2)}`}</p>
</div>
);
}
return null;
};
// --- RENDER LOGIC ---
if (error) {
return (
<Container>
<ErrorMessage initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}>
<AlertCircle size={48} />
<h3>Oops! Something went wrong.</h3>
<p>{error}</p>
<Button variant="primary" onClick={handleRefresh}><RefreshCw size={16} /> Try Again</Button>
</ErrorMessage>
</Container>
);
}
return (
<Container initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5 }}>
<h2>Transaction Analytics</h2>
<FilterSection initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }}>
<SearchContainer>
<Search size={20} />
<SearchInput
type="text"
name="searchTerm"
placeholder="Search by description..."
value={filters.searchTerm}
onChange={handleFilterChange}
/>
</SearchContainer>
<FiltersGrid>
<FilterSelect name="account" value={filters.account} onChange={handleFilterChange} disabled={accountsLoading}>
<option value="all">All Accounts</option>
{userAccounts.map(acc => (
<option key={acc.id} value={acc.accountNumber}>
{acc.accountType} - {acc.accountNumber}
</option>
))}
</FilterSelect>
<FilterSelect name="type" value={filters.type} onChange={handleFilterChange}>
<option value="all">All Types</option>
<option value="DEPOSIT">Deposit</option>
<option value="WITHDRAW">Withdraw</option>
<option value="TRANSFER">Transfer</option>
</FilterSelect>
<DateInput type="date" name="startDate" value={filters.startDate} onChange={handleFilterChange} />
<DateInput type="date" name="endDate" value={filters.endDate} onChange={handleFilterChange} />
</FiltersGrid>
<ButtonGroup>
<Button onClick={resetFilters} whileTap={{ scale: 0.95 }}>
<X size={16} /> Reset
</Button>
<Button variant="primary" onClick={handleRefresh} disabled={loading || accountsLoading} whileTap={{ scale: 0.95 }}>
<RefreshCw size={16} /> Refresh Data
</Button>
</ButtonGroup>
</FilterSection>
{loading ? (
<LoadingSpinner><RefreshCw size={48} /></LoadingSpinner>
) : (
<>
<AnalyticsGrid initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.4 }}>
<ChartContainer>
<h3>Income vs. Expense</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={analyticsData.incomeVsExpense} margin={{ top: 5, right: 20, left: -10, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke={colors.border}/>
<XAxis dataKey="name" stroke={colors.textSecondary} />
<YAxis stroke={colors.textSecondary}/>
<Tooltip cursor={{fill: 'rgba(102, 126, 234, 0.1)'}}/>
<Legend />
<Bar dataKey="amount" name="Amount">
{analyticsData.incomeVsExpense.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.name === 'Income' ? colors.success : colors.error} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</ChartContainer>
<ChartContainer>
<h3>Transaction Trend</h3>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={analyticsData.trendData} margin={{ top: 5, right: 20, left: -10, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke={colors.border} />
<XAxis dataKey="date" stroke={colors.textSecondary} tickFormatter={(str) => new Date(str).toLocaleDateString('en-US', {month: 'short', day: 'numeric'})} />
<YAxis stroke={colors.textSecondary}/>
<Tooltip content={<CustomTooltip />}/>
<Legend />
<Line type="monotone" dataKey="amount" stroke={colors.primary} strokeWidth={2} dot={{ r: 4 }} activeDot={{ r: 8 }} name="Total Amount"/>
</LineChart>
</ResponsiveContainer>
</ChartContainer>
<ChartContainer>
<h3>Transaction by Type</h3>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie data={analyticsData.typeDistribution} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={100} label>
{analyticsData.typeDistribution.map((entry, index) => (
<Cell key={`cell-${index}`} fill={PIE_COLORS[entry.name]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
</ChartContainer>
</AnalyticsGrid>
<AnimatePresence>
{filteredAndSortedTransactions.length > 0 ? (
<TableContainer initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.6 }}>
<Table>
<thead>
<tr>
<th onClick={() => handleSort('timestamp')}>Date <ArrowUpDown size={14} /></th>
<th onClick={() => handleSort('description')}>Description <ArrowUpDown size={14} /></th>
<th>From</th>
<th>To</th>
<th onClick={() => handleSort('type')}>Type <ArrowUpDown size={14} /></th>
<th onClick={() => handleSort('amount')}>Amount <ArrowUpDown size={14} /></th>
<th onClick={() => handleSort('status')}>Status <ArrowUpDown size={14} /></th>
</tr>
</thead>
<tbody>
{filteredAndSortedTransactions.map(t => (
<motion.tr key={t.id} initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
<td>{new Date(t.timestamp).toLocaleDateString()}</td>
<td>{t.description}</td>
<td>{t.fromAccount?.accountNumber || 'N/A'}</td>
<td>{t.toAccount?.accountNumber || 'N/A'}</td>
<td><TypeBadge type={t.type}>{t.type}</TypeBadge></td>
<td style={{ color: ['DEPOSIT'].includes(t.type) ? colors.success : colors.error, fontWeight: '600' }}>
{['DEPOSIT'].includes(t.type) ? '+' : '-'} ${t.amount.toFixed(2)}
</td>
<td><StatusBadge status={t.status}>{t.status}</StatusBadge></td>
</motion.tr>
))}
</tbody>
</Table>
</TableContainer>
) : (
<EmptyState initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.6 }}>
<Filter size={48} />
<h3>No Transactions Found</h3>
<p>Try adjusting your filters or click Refresh to fetch the latest data.</p>
</EmptyState>
)}
</AnimatePresence>
</>
)}
</Container>
);
};
export default TransactionAnalytics;
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)