Fix: Complete Dashboard page implementation
This commit is contained in:
@@ -1,11 +1,333 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTransactionStore } from '../stores/transactionStore';
|
||||
import { BarChart, LineChart, PieChart } from '../components/Charts';
|
||||
import { getDefaultConverter } from '@brazil-swift-ops/utils';
|
||||
import type { Transaction } from '@brazil-swift-ops/types';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { transactions, results } = useTransactionStore();
|
||||
const converter = getDefaultConverter();
|
||||
|
||||
// Calculate statistics
|
||||
const stats = useMemo(() => {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const weekAgo = new Date(today);
|
||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||
const monthAgo = new Date(today);
|
||||
monthAgo.setDate(monthAgo.getDate() - 30);
|
||||
|
||||
const todayTransactions = transactions.filter((txn) => txn.createdAt >= today);
|
||||
const weekTransactions = transactions.filter((txn) => txn.createdAt >= weekAgo);
|
||||
const monthTransactions = transactions.filter((txn) => txn.createdAt >= monthAgo);
|
||||
|
||||
const totalUsdEquivalent = transactions.reduce((sum, txn) => {
|
||||
const usd = txn.usdEquivalent || converter.convert(txn.amount, txn.currency, 'USD');
|
||||
return sum + usd;
|
||||
}, 0);
|
||||
|
||||
const reportingRequired = transactions.filter((txn) => {
|
||||
const usd = txn.usdEquivalent || converter.convert(txn.amount, txn.currency, 'USD');
|
||||
return usd >= 10000;
|
||||
}).length;
|
||||
|
||||
const pendingCount = transactions.filter((txn) => txn.status === 'pending').length;
|
||||
const approvedCount = transactions.filter((txn) => txn.status === 'approved').length;
|
||||
const heldCount = transactions.filter((txn) => txn.status === 'held').length;
|
||||
const escalatedCount = transactions.filter((txn) => txn.status === 'escalated').length;
|
||||
|
||||
// Currency breakdown
|
||||
const currencyMap = new Map<string, number>();
|
||||
transactions.forEach((txn) => {
|
||||
const usd = txn.usdEquivalent || converter.convert(txn.amount, txn.currency, 'USD');
|
||||
currencyMap.set(txn.currency, (currencyMap.get(txn.currency) || 0) + usd);
|
||||
});
|
||||
|
||||
// Transaction volume over time (last 7 days)
|
||||
const volumeData = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const date = new Date(today);
|
||||
date.setDate(date.getDate() - i);
|
||||
const dayTransactions = transactions.filter((txn) => {
|
||||
const txnDate = new Date(txn.createdAt);
|
||||
return (
|
||||
txnDate.getFullYear() === date.getFullYear() &&
|
||||
txnDate.getMonth() === date.getMonth() &&
|
||||
txnDate.getDate() === date.getDate()
|
||||
);
|
||||
});
|
||||
const dayVolume = dayTransactions.reduce((sum, txn) => {
|
||||
const usd = txn.usdEquivalent || converter.convert(txn.amount, txn.currency, 'USD');
|
||||
return sum + usd;
|
||||
}, 0);
|
||||
volumeData.push({
|
||||
date: date.toISOString().split('T')[0],
|
||||
value: dayVolume,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
total: transactions.length,
|
||||
today: todayTransactions.length,
|
||||
week: weekTransactions.length,
|
||||
month: monthTransactions.length,
|
||||
totalUsdEquivalent,
|
||||
reportingRequired,
|
||||
pendingCount,
|
||||
approvedCount,
|
||||
heldCount,
|
||||
escalatedCount,
|
||||
currencyBreakdown: Array.from(currencyMap.entries()).map(([currency, amount]) => ({
|
||||
label: currency,
|
||||
value: amount,
|
||||
})),
|
||||
statusDistribution: [
|
||||
{ label: 'Approved', value: approvedCount, color: '#10b981' },
|
||||
{ label: 'Pending', value: pendingCount, color: '#f59e0b' },
|
||||
{ label: 'Held', value: heldCount, color: '#ef4444' },
|
||||
{ label: 'Escalated', value: escalatedCount, color: '#8b5cf6' },
|
||||
].filter((item) => item.value > 0),
|
||||
volumeData,
|
||||
};
|
||||
}, [transactions, converter]);
|
||||
|
||||
// Recent activity (last 10 transactions)
|
||||
const recentActivity = useMemo(() => {
|
||||
return [...transactions]
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
||||
.slice(0, 10)
|
||||
.map((txn) => {
|
||||
const result = results.get(txn.id);
|
||||
const usd = txn.usdEquivalent || converter.convert(txn.amount, txn.currency, 'USD');
|
||||
return {
|
||||
...txn,
|
||||
usd,
|
||||
decision: result?.overallDecision || 'Pending',
|
||||
severity: result?.overallSeverity || 'Info',
|
||||
};
|
||||
});
|
||||
}, [transactions, results, converter]);
|
||||
|
||||
// Compliance status
|
||||
const complianceStatus = useMemo(() => {
|
||||
const totalWithResults = transactions.filter((txn) => results.has(txn.id)).length;
|
||||
const compliant = transactions.filter((txn) => {
|
||||
const result = results.get(txn.id);
|
||||
return result?.overallDecision === 'Allow';
|
||||
}).length;
|
||||
|
||||
return {
|
||||
totalEvaluated: totalWithResults,
|
||||
compliant,
|
||||
complianceRate: totalWithResults > 0 ? (compliant / totalWithResults) * 100 : 0,
|
||||
};
|
||||
}, [transactions, results]);
|
||||
|
||||
return (
|
||||
<div className="px-4 py-6 sm:px-0">
|
||||
<div className="border-4 border-dashed border-gray-200 rounded-lg h-96 p-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
|
||||
<p className="text-gray-600">Brazil SWIFT Operations Platform</p>
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
|
||||
<p className="mt-1 text-sm text-gray-600">Brazil SWIFT Operations Platform Overview</p>
|
||||
</div>
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Total Transactions</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mt-1">{stats.total}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{stats.today} today, {stats.week} this week
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-blue-100 rounded-full">
|
||||
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Total Volume (USD)</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mt-1">
|
||||
${(stats.totalUsdEquivalent / 1000000).toFixed(2)}M
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{stats.month} transactions this month
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-green-100 rounded-full">
|
||||
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Reporting Required</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mt-1">{stats.reportingRequired}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">≥ USD 10,000</p>
|
||||
</div>
|
||||
<div className="p-3 bg-yellow-100 rounded-full">
|
||||
<svg className="w-8 h-8 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Pending Approvals</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mt-1">{stats.pendingCount}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{stats.escalatedCount} escalated
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-red-100 rounded-full">
|
||||
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
{stats.volumeData.length > 0 && (
|
||||
<LineChart
|
||||
data={stats.volumeData}
|
||||
title="Transaction Volume (Last 7 Days)"
|
||||
/>
|
||||
)}
|
||||
{stats.statusDistribution.length > 0 && (
|
||||
<PieChart
|
||||
data={stats.statusDistribution}
|
||||
title="Transaction Status Distribution"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{stats.currencyBreakdown.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<BarChart
|
||||
data={stats.currencyBreakdown}
|
||||
title="Volume by Currency (USD Equivalent)"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recent Activity and Compliance Status */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Recent Activity */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-lg font-semibold">Recent Activity</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{recentActivity.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{recentActivity.map((txn) => (
|
||||
<div
|
||||
key={txn.id}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded-md hover:bg-gray-100 transition"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-medium text-gray-900">{txn.id}</span>
|
||||
<span
|
||||
className={`text-xs px-2 py-1 rounded ${
|
||||
txn.decision === 'Allow'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: txn.decision === 'Hold'
|
||||
? 'bg-yellow-100 text-yellow-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{txn.decision}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
{txn.direction} • {txn.currency} {txn.amount.toLocaleString()} • ${txn.usd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{new Date(txn.createdAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<p>No recent transactions</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Compliance Status */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-lg font-semibold">Compliance Status</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-600">Compliance Rate</span>
|
||||
<span className="text-sm font-semibold text-gray-900">
|
||||
{complianceStatus.complianceRate.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-green-600 h-2 rounded-full transition-all"
|
||||
style={{ width: `${complianceStatus.complianceRate}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-gray-200">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Total Evaluated</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-1">
|
||||
{complianceStatus.totalEvaluated}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Compliant</p>
|
||||
<p className="text-2xl font-bold text-green-600 mt-1">
|
||||
{complianceStatus.compliant}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-gray-200">
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
<div className="w-3 h-3 bg-green-500 rounded-full" />
|
||||
<span className="text-gray-600">BCB Reporting: Compliant</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-sm mt-2">
|
||||
<div className="w-3 h-3 bg-green-500 rounded-full" />
|
||||
<span className="text-gray-600">AML Checks: Active</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-sm mt-2">
|
||||
<div className="w-3 h-3 bg-yellow-500 rounded-full" />
|
||||
<span className="text-gray-600">FX Contracts: Monitoring</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user