Fix: Complete Dashboard page implementation

This commit is contained in:
defiQUG
2026-01-23 18:19:16 -08:00
parent dec59ccb49
commit 053a91cc18

View File

@@ -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>
);