Implement critical Week 1 tasks
- ✅ Dashboard page: Full implementation with statistics, charts, recent activity, compliance status - ✅ Toast integration: ToastProvider and useToast hook integrated throughout app - ✅ Navigation icons: Added react-icons with icons for all menu items - ✅ Active state styling: Navigation highlights current page - ✅ User menu: Added user menu component with dropdown in header - ✅ Error handling: Replaced console.error with toast notifications - ✅ Success feedback: Added success toasts for all user actions All critical Week 1 tasks completed!
This commit is contained in:
@@ -10,15 +10,16 @@
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@brazil-swift-ops/audit": "workspace:*",
|
||||
"@brazil-swift-ops/iso20022": "workspace:*",
|
||||
"@brazil-swift-ops/risk-models": "workspace:*",
|
||||
"@brazil-swift-ops/rules-engine": "workspace:*",
|
||||
"@brazil-swift-ops/treasury": "workspace:*",
|
||||
"@brazil-swift-ops/types": "workspace:*",
|
||||
"@brazil-swift-ops/utils": "workspace:*",
|
||||
"@brazil-swift-ops/rules-engine": "workspace:*",
|
||||
"@brazil-swift-ops/iso20022": "workspace:*",
|
||||
"@brazil-swift-ops/treasury": "workspace:*",
|
||||
"@brazil-swift-ops/risk-models": "workspace:*",
|
||||
"@brazil-swift-ops/audit": "workspace:*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
|
||||
@@ -1,65 +1,88 @@
|
||||
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
|
||||
import { BrowserRouter, Routes, Route, Link, useLocation } from 'react-router-dom';
|
||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
import { ToastProvider } from './components/ToastProvider';
|
||||
import UserMenu from './components/UserMenu';
|
||||
import DashboardPage from './pages/DashboardPage';
|
||||
import TransactionsPage from './pages/TransactionsPage';
|
||||
import TreasuryPage from './pages/TreasuryPage';
|
||||
import ReportsPage from './pages/ReportsPage';
|
||||
import { FiHome, FiFileText, FiDollarSign, FiBarChart2, FiBell } from 'react-icons/fi';
|
||||
|
||||
function Navigation() {
|
||||
const location = useLocation();
|
||||
|
||||
const isActive = (path: string) => location.pathname === path;
|
||||
|
||||
const navItems = [
|
||||
{ path: '/', label: 'Dashboard', icon: FiHome },
|
||||
{ path: '/transactions', label: 'Transactions', icon: FiFileText },
|
||||
{ path: '/treasury', label: 'Treasury', icon: FiDollarSign },
|
||||
{ path: '/reports', label: 'Reports', icon: FiBarChart2 },
|
||||
];
|
||||
|
||||
return (
|
||||
<nav className="bg-white shadow-sm border-b">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<h1 className="text-xl font-bold text-gray-900">
|
||||
Brazil SWIFT Operations
|
||||
</h1>
|
||||
</div>
|
||||
<div className="hidden sm:ml-6 sm:flex sm:space-x-1">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const active = isActive(item.path);
|
||||
return (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={`inline-flex items-center px-4 py-2 border-b-2 text-sm font-medium transition ${
|
||||
active
|
||||
? 'border-blue-500 text-blue-700 bg-blue-50'
|
||||
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-5 h-5 mr-2" />
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<button className="relative p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-md transition">
|
||||
<FiBell className="w-5 h-5" />
|
||||
<span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-red-500 ring-2 ring-white" />
|
||||
</button>
|
||||
<UserMenu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<BrowserRouter>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<nav className="bg-white shadow-sm border-b">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<h1 className="text-xl font-bold text-gray-900">
|
||||
Brazil SWIFT Operations
|
||||
</h1>
|
||||
</div>
|
||||
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
|
||||
<Link
|
||||
to="/"
|
||||
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
to="/transactions"
|
||||
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
>
|
||||
Transactions
|
||||
</Link>
|
||||
<Link
|
||||
to="/treasury"
|
||||
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
>
|
||||
Treasury
|
||||
</Link>
|
||||
<Link
|
||||
to="/reports"
|
||||
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
>
|
||||
Reports
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<ToastProvider>
|
||||
<BrowserRouter>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Navigation />
|
||||
|
||||
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<Routes>
|
||||
<Route path="/" element={<DashboardPage />} />
|
||||
<Route path="/transactions" element={<TransactionsPage />} />
|
||||
<Route path="/treasury" element={<TreasuryPage />} />
|
||||
<Route path="/reports" element={<ReportsPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<Routes>
|
||||
<Route path="/" element={<DashboardPage />} />
|
||||
<Route path="/transactions" element={<TransactionsPage />} />
|
||||
<Route path="/treasury" element={<TreasuryPage />} />
|
||||
<Route path="/reports" element={<ReportsPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</ToastProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
50
apps/web/src/components/Toast.tsx
Normal file
50
apps/web/src/components/Toast.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { FiCheckCircle, FiXCircle, FiAlertCircle, FiInfo } from 'react-icons/fi';
|
||||
|
||||
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
interface ToastProps {
|
||||
type: ToastType;
|
||||
message: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function Toast({ type, message, onClose }: ToastProps) {
|
||||
const icons = {
|
||||
success: <FiCheckCircle className="w-5 h-5" />,
|
||||
error: <FiXCircle className="w-5 h-5" />,
|
||||
warning: <FiAlertCircle className="w-5 h-5" />,
|
||||
info: <FiInfo className="w-5 h-5" />,
|
||||
};
|
||||
|
||||
const colors = {
|
||||
success: 'bg-green-50 border-green-200 text-green-800',
|
||||
error: 'bg-red-50 border-red-200 text-red-800',
|
||||
warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
|
||||
info: 'bg-blue-50 border-blue-200 text-blue-800',
|
||||
};
|
||||
|
||||
const iconColors = {
|
||||
success: 'text-green-600',
|
||||
error: 'text-red-600',
|
||||
warning: 'text-yellow-600',
|
||||
info: 'text-blue-600',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center space-x-3 px-4 py-3 rounded-md border shadow-lg min-w-[300px] max-w-md animate-slide-in ${colors[type]}`}
|
||||
role="alert"
|
||||
>
|
||||
<div className={iconColors[type]}>{icons[type]}</div>
|
||||
<p className="flex-1 text-sm font-medium">{message}</p>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 transition"
|
||||
aria-label="Close"
|
||||
>
|
||||
<FiXCircle className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTransactionStore } from '../stores/transactionStore';
|
||||
import { useToast } from '../components/ToastProvider';
|
||||
import { generateBCBReport, exportBCBReportToCSV } from '@brazil-swift-ops/audit';
|
||||
import type { BCBReport } from '@brazil-swift-ops/types';
|
||||
|
||||
export default function ReportsPage() {
|
||||
const { transactions, results } = useTransactionStore();
|
||||
const { showToast } = useToast();
|
||||
const [report, setReport] = useState<BCBReport | null>(null);
|
||||
const [dateRange, setDateRange] = useState({
|
||||
start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
||||
@@ -33,34 +35,52 @@ export default function ReportsPage() {
|
||||
);
|
||||
|
||||
setReport(generatedReport);
|
||||
showToast('success', `Report generated successfully with ${generatedReport.summary.totalTransactions} transactions`);
|
||||
} catch (error) {
|
||||
console.error('Error generating report:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to generate report';
|
||||
showToast('error', errorMessage);
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportJSON = () => {
|
||||
if (!report) return;
|
||||
const blob = new Blob([report.data], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `BCB_Report_${report.reportId}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
if (!report) {
|
||||
showToast('warning', 'Please generate a report first');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const blob = new Blob([report.data], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `BCB_Report_${report.reportId}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
showToast('success', 'Report exported as JSON');
|
||||
} catch (error) {
|
||||
showToast('error', 'Failed to export report');
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportCSV = () => {
|
||||
if (!report) return;
|
||||
const csv = exportBCBReportToCSV(report);
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `BCB_Report_${report.reportId}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
if (!report) {
|
||||
showToast('warning', 'Please generate a report first');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const csv = exportBCBReportToCSV(report);
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `BCB_Report_${report.reportId}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
showToast('success', 'Report exported as CSV');
|
||||
} catch (error) {
|
||||
showToast('error', 'Failed to export report');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { useTransactionStore } from '../stores/transactionStore';
|
||||
import { useToast } from '../components/ToastProvider';
|
||||
import type { Transaction } from '@brazil-swift-ops/types';
|
||||
import { calculateTransactionEOUplift, getDefaultConverter, getAllBanks, getBankBySwiftCode } from '@brazil-swift-ops/utils';
|
||||
import {
|
||||
@@ -16,6 +17,7 @@ import LoadingSpinner from '../components/LoadingSpinner';
|
||||
|
||||
export default function TransactionsPage() {
|
||||
const { transactions, results, addTransaction, evaluateTransaction } = useTransactionStore();
|
||||
const { showToast } = useToast();
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [formData, setFormData] = useState<Partial<Transaction>>({
|
||||
direction: 'outbound',
|
||||
@@ -170,13 +172,15 @@ export default function TransactionsPage() {
|
||||
country: 'BR',
|
||||
},
|
||||
});
|
||||
setErrors({});
|
||||
} catch (error) {
|
||||
console.error('Error processing transaction:', error);
|
||||
setErrors({ submit: 'Failed to process transaction. Please try again.' });
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to process transaction. Please try again.';
|
||||
showToast('error', errorMessage);
|
||||
setErrors({ submit: errorMessage });
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}, [formData, addTransaction, converter]);
|
||||
}, [formData, addTransaction, converter, showToast]);
|
||||
|
||||
return (
|
||||
<div className="px-4 py-6 sm:px-0">
|
||||
|
||||
11
pnpm-lock.yaml
generated
11
pnpm-lock.yaml
generated
@@ -81,6 +81,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1(react@18.3.1)
|
||||
react-icons:
|
||||
specifier: ^5.5.0
|
||||
version: 5.5.0(react@18.3.1)
|
||||
react-router-dom:
|
||||
specifier: ^6.20.0
|
||||
version: 6.30.3(react-dom@18.3.1)(react@18.3.1)
|
||||
@@ -2453,6 +2456,14 @@ packages:
|
||||
scheduler: 0.23.2
|
||||
dev: false
|
||||
|
||||
/react-icons@5.5.0(react@18.3.1):
|
||||
resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/react-is@18.3.1:
|
||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||
dev: true
|
||||
|
||||
Reference in New Issue
Block a user