diff --git a/flutter_profile_popup.dart b/flutter_profile_popup.dart new file mode 100644 index 0000000..fc4d518 --- /dev/null +++ b/flutter_profile_popup.dart @@ -0,0 +1,684 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const FundGuardProfileApp()); +} + +/// Main Application Widget - Dark Theme with Profile Popup Menu +class FundGuardProfileApp extends StatefulWidget { + const FundGuardProfileApp({Key? key}) : super(key: key); + + @override + State createState() => _FundGuardProfileAppState(); +} + +class _FundGuardProfileAppState extends State { + // Global authentication state + bool _isAuthenticated = false; + String _userRole = ''; + String _phoneNumber = ''; + String _accountNumber = ''; + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'FundGuard Profile Popup', + debugShowCheckedModeBanner: false, + themeMode: ThemeMode.dark, + darkTheme: _buildDarkTheme(), + home: _isAuthenticated + ? DashboardScreen( + userRole: _userRole, + phoneNumber: _phoneNumber, + accountNumber: _accountNumber, + onLogout: _handleLogout, + ) + : LoginScreen( + onLoginSuccess: _handleLoginSuccess, + ), + ); + } + + /// Dark Theme Configuration + ThemeData _buildDarkTheme() { + return ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + scaffoldBackgroundColor: const Color(0xFF0F1419), + primaryColor: const Color(0xFF6200EE), + colorScheme: const ColorScheme.dark( + primary: Color(0xFF6200EE), + secondary: Color(0xFF03DAC6), + surface: Color(0xFF151D2A), + background: Color(0xFF0F1419), + error: Color(0xFFCF6679), + onBackground: Color(0xFFE0E0E0), + ), + appBarTheme: const AppBarTheme( + backgroundColor: Color(0xFF151D2A), + elevation: 0, + centerTitle: true, + titleTextStyle: TextStyle( + color: Color(0xFFE0E0E0), + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF6200EE), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: const Color(0xFF1E1E1E), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFF6200EE), width: 1.5), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFF333333), width: 1.5), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFF6200EE), width: 2), + ), + labelStyle: const TextStyle(color: Color(0xFFB0B0B0)), + hintStyle: const TextStyle(color: Color(0xFF666666)), + ), + textTheme: const TextTheme( + displayLarge: TextStyle(color: Color(0xFFE0E0E0), fontSize: 32, fontWeight: FontWeight.bold), + headlineSmall: TextStyle(color: Color(0xFFE0E0E0), fontSize: 20, fontWeight: FontWeight.w600), + bodyLarge: TextStyle(color: Color(0xFFE0E0E0), fontSize: 16), + bodyMedium: TextStyle(color: Color(0xFFB0B0B0), fontSize: 14), + ), + ); + } + + void _handleLoginSuccess(String phoneNumber, String accountNumber, String userRole) { + setState(() { + _isAuthenticated = true; + _userRole = userRole; + _phoneNumber = phoneNumber; + _accountNumber = accountNumber; + }); + } + + void _handleLogout() { + setState(() { + _isAuthenticated = false; + _userRole = ''; + _phoneNumber = ''; + _accountNumber = ''; + }); + } +} + +/// ============================================================================ +/// LOGIN SCREEN - Phone Number + Account Number Authentication +/// ============================================================================ +class LoginScreen extends StatefulWidget { + final Function(String, String, String) onLoginSuccess; + + const LoginScreen({ + Key? key, + required this.onLoginSuccess, + }) : super(key: key); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final _phoneController = TextEditingController(); + final _accountController = TextEditingController(); + final _formKey = GlobalKey(); + bool _isLoading = false; + + @override + void dispose() { + _phoneController.dispose(); + _accountController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('FundGuard')), + body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Secure Login', + style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFFE0E0E0)), + ), + const SizedBox(height: 12), + const Text( + 'Enter your credentials to continue', + style: TextStyle(fontSize: 14, color: Color(0xFFB0B0B0)), + ), + const SizedBox(height: 48), + TextFormField( + controller: _phoneController, + keyboardType: TextInputType.phone, + maxLength: 10, + decoration: const InputDecoration( + labelText: 'Phone Number', + hintText: 'Enter 10-digit phone number', + prefixIcon: Icon(Icons.phone), + ), + validator: (value) { + if (value == null || value.isEmpty) return 'Phone number is required'; + if (value.length != 10) return 'Phone number must be 10 digits'; + if (!RegExp(r'^[0-9]+$').hasMatch(value)) return 'Phone number must contain only digits'; + return null; + }, + ), + const SizedBox(height: 24), + TextFormField( + controller: _accountController, + keyboardType: TextInputType.number, + maxLength: 9, + decoration: const InputDecoration( + labelText: 'Account Number', + hintText: 'Enter 9-digit account number', + prefixIcon: Icon(Icons.account_balance), + ), + validator: (value) { + if (value == null || value.isEmpty) return 'Account number is required'; + if (value.length != 9) return 'Account number must be 9 digits'; + if (!RegExp(r'^[0-9]+$').hasMatch(value)) return 'Account number must contain only digits'; + return null; + }, + ), + const SizedBox(height: 48), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isLoading ? null : _performLogin, + child: _isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : const Text('Login'), + ), + ), + const SizedBox(height: 24), + Card( + color: const Color(0xFF151D2A), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text( + 'Test Credentials:', + style: TextStyle(color: Color(0xFF6200EE), fontWeight: FontWeight.w600, fontSize: 12), + ), + SizedBox(height: 8), + Text( + 'Phone: 5555555555\nA/C: 555555555', + style: TextStyle(color: Color(0xFFB0B0B0), fontSize: 12, fontFamily: 'monospace'), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + void _performLogin() async { + if (!_formKey.currentState!.validate()) return; + + setState(() => _isLoading = true); + await Future.delayed(const Duration(milliseconds: 800)); + + final phone = _phoneController.text.trim(); + final account = _accountController.text.trim(); + + if (phone == '5555555555' && account == '555555555') { + widget.onLoginSuccess(phone, account, 'Admin'); + setState(() => _isLoading = false); + return; + } + + setState(() => _isLoading = false); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Invalid Credentials for testing'), + backgroundColor: Color(0xFFCF6679), + duration: Duration(seconds: 2), + ), + ); + } +} + +/// ============================================================================ +/// DASHBOARD SCREEN - Main Interface with Profile Popup Menu +/// ============================================================================ +class DashboardScreen extends StatefulWidget { + final String userRole; + final String phoneNumber; + final String accountNumber; + final VoidCallback onLogout; + + const DashboardScreen({ + Key? key, + required this.userRole, + required this.phoneNumber, + required this.accountNumber, + required this.onLogout, + }) : super(key: key); + + @override + State createState() => _DashboardScreenState(); +} + +class _DashboardScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('FundGuard Dashboard'), + elevation: 0, + actions: [ + // ============================================================================ + // PROFILE POPUP MENU BUTTON - Circular Avatar Trigger + // ============================================================================ + Padding( + padding: const EdgeInsets.only(right: 16), + child: ProfilePopupMenu( + phoneNumber: widget.phoneNumber, + accountNumber: widget.accountNumber, + onLogout: _handleLogout, + ), + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Welcome to FundGuard', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Color(0xFFE0E0E0)), + ), + const SizedBox(height: 12), + const Text( + 'Your secure financial fraud detection platform', + style: TextStyle(fontSize: 14, color: Color(0xFFB0B0B0)), + ), + const SizedBox(height: 32), + Card( + color: const Color(0xFF151D2A), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'System Status', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildStatusMetric('Active', '24/7', Colors.green), + _buildStatusMetric('Alerts', '3', Colors.orange), + _buildStatusMetric('Protected', '1.2M', Colors.blue), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 24), + Card( + color: const Color(0xFF151D2A), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Recent Activity', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), + const SizedBox(height: 16), + _buildActivityItem('Fraud Detection Model Updated', '2 hours ago'), + _buildActivityItem('Risk Score Recalculated', '4 hours ago'), + _buildActivityItem('New Compliance Report Generated', '1 day ago'), + ], + ), + ), + ), + ], + ), + ), + ); + } + + void _handleLogout() { + widget.onLogout(); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (BuildContext context) => LoginScreen( + onLoginSuccess: (phone, account, role) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => DashboardScreen( + userRole: role, + phoneNumber: phone, + accountNumber: account, + onLogout: widget.onLogout, + ), + ), + ); + }, + ), + ), + (Route route) => false, + ); + } + + Widget _buildStatusMetric(String label, String value, Color color) { + return Column( + children: [ + Text(value, style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: color)), + const SizedBox(height: 4), + Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFFB0B0B0))), + ], + ); + } + + Widget _buildActivityItem(String title, String timestamp) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text(title, style: const TextStyle(fontSize: 14, color: Color(0xFFE0E0E0))), + ), + Text(timestamp, style: const TextStyle(fontSize: 12, color: Color(0xFF808080))), + ], + ), + ); + } +} + +/// ============================================================================ +/// PROFILE POPUP MENU WIDGET - Custom PopupMenuButton with Enhanced Styling +/// ============================================================================ +class ProfilePopupMenu extends StatefulWidget { + final String phoneNumber; + final String accountNumber; + final VoidCallback onLogout; + + const ProfilePopupMenu({ + Key? key, + required this.phoneNumber, + required this.accountNumber, + required this.onLogout, + }) : super(key: key); + + @override + State createState() => _ProfilePopupMenuState(); +} + +class _ProfilePopupMenuState extends State { + @override + Widget build(BuildContext context) { + return PopupMenuButton( + offset: const Offset(0, 50), // Position menu slightly below avatar + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: Color(0xFF263345), width: 1.5), + ), + color: const Color(0xFF151D2A), // Deep dark blue/grey background + elevation: 8, + itemBuilder: (BuildContext context) => [ + // ========== ITEM 1: Account Session Metadata Header (Non-clickable) ========== + PopupMenuItem( + enabled: false, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Account Session', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w400, + color: Color(0xFF999999), + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), + Text( + 'Phone: ${widget.phoneNumber}', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Color(0xFFE0E0E0), + ), + ), + const SizedBox(height: 4), + Text( + 'A/C: ${widget.accountNumber}', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w400, + color: Color(0xFFB0B0B0), + ), + ), + ], + ), + ), + // Divider + const PopupMenuDivider(height: 12), + + // ========== ITEM 2: Manage Profile Button ========== + PopupMenuItem( + value: 'manage', + onTap: () { + // Dismiss popup and navigate to ProfileRoutePage + Future.delayed(const Duration(milliseconds: 100), () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const ProfileRoutePage()), + ); + }); + }, + child: Row( + children: const [ + Icon(Icons.manage_accounts, color: Color(0xFF6200EE), size: 20), + SizedBox(width: 12), + Text( + 'Manage Profile', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFFE0E0E0), + ), + ), + ], + ), + ), + + // ========== ITEM 3: Logout Action (Destructive) ========== + PopupMenuItem( + value: 'logout', + onTap: () { + // Dismiss popup and perform logout + Future.delayed(const Duration(milliseconds: 100), () { + _showLogoutConfirmation(context); + }); + }, + child: Row( + children: const [ + Icon(Icons.logout, color: Colors.redAccent, size: 20), + SizedBox(width: 12), + Text( + 'Logout', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.redAccent, + ), + ), + ], + ), + ), + ], + child: Center( + child: CircleAvatar( + radius: 22, + backgroundColor: const Color(0xFF6200EE), + child: const Text( + 'R', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ); + } + + void _showLogoutConfirmation(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Confirm Logout'), + content: const Text('Are you sure you want to logout?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + widget.onLogout(); + }, + child: const Text('Logout', style: TextStyle(color: Colors.redAccent)), + ), + ], + ); + }, + ); + } +} + +/// ============================================================================ +/// PROFILE ROUTE PAGE - Profile Management Screen +/// ============================================================================ +class ProfileRoutePage extends StatelessWidget { + const ProfileRoutePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Manage Profile'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Profile Settings', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Color(0xFFE0E0E0)), + ), + const SizedBox(height: 32), + Card( + color: const Color(0xFF151D2A), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Account Information', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 20), + _buildSettingRow('Email', 'user@fundguard.com'), + const Divider(height: 24, color: Color(0xFF333333)), + _buildSettingRow('Notifications', 'Enabled'), + const Divider(height: 24, color: Color(0xFF333333)), + _buildSettingRow('Two-Factor Auth', 'Active'), + ], + ), + ), + ), + const SizedBox(height: 32), + Card( + color: const Color(0xFF151D2A), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Security Settings', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF6200EE), + padding: const EdgeInsets.symmetric(vertical: 14), + ), + child: const Text('Change Password'), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildSettingRow(String label, String value) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFFB0B0B0))), + Text(value, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFFE0E0E0))), + ], + ); + } +} diff --git a/main.dart b/main.dart new file mode 100644 index 0000000..96ab9e9 --- /dev/null +++ b/main.dart @@ -0,0 +1,835 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const FundGuardPremiumApp()); +} + +/// Main Application - Premium Dark Mode Dashboard with Profile Popup +class FundGuardPremiumApp extends StatefulWidget { + const FundGuardPremiumApp({Key? key}) : super(key: key); + + @override + State createState() => _FundGuardPremiumAppState(); +} + +class _FundGuardPremiumAppState extends State { + bool _isAuthenticated = false; + String _userRole = ''; + String _phoneNumber = ''; + String _accountNumber = ''; + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'FundGuard Premium', + debugShowCheckedModeBanner: false, + themeMode: ThemeMode.dark, + darkTheme: _buildPremiumDarkTheme(), + home: _isAuthenticated + ? DashboardScreen( + userRole: _userRole, + phoneNumber: _phoneNumber, + accountNumber: _accountNumber, + onLogout: _handleLogout, + ) + : LoginScreen( + onLoginSuccess: _handleLoginSuccess, + ), + ); + } + + /// Premium Dark Theme with Glassmorphic Elements + ThemeData _buildPremiumDarkTheme() { + return ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + scaffoldBackgroundColor: const Color(0xFF0D131C), + primaryColor: const Color(0xFF6366F1), + colorScheme: const ColorScheme.dark( + primary: Color(0xFF6366F1), + secondary: Color(0xFF38BDF8), + surface: Color(0xFF0D131C), + background: Color(0xFF0D131C), + error: Color(0xFFF87171), + onBackground: Color(0xFFE5E7EB), + ), + appBarTheme: const AppBarTheme( + backgroundColor: Color(0xFF111820), + elevation: 0, + centerTitle: true, + titleTextStyle: TextStyle( + color: Color(0xFFE5E7EB), + fontSize: 18, + fontWeight: FontWeight.w600, + letterSpacing: 0.3, + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF6366F1), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + textStyle: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + letterSpacing: 0.2, + ), + elevation: 4, + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: const Color(0xFF111820), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFF1E293B), width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFF1E293B), width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFF6366F1), width: 1.5), + ), + labelStyle: const TextStyle(color: Color(0xFF9CA3AF), fontSize: 14), + hintStyle: const TextStyle(color: Color(0xFF6B7280), fontSize: 14), + ), + textTheme: const TextTheme( + displayLarge: TextStyle( + color: Color(0xFFE5E7EB), + fontSize: 32, + fontWeight: FontWeight.w700, + letterSpacing: -0.5, + ), + headlineSmall: TextStyle( + color: Color(0xFFE5E7EB), + fontSize: 20, + fontWeight: FontWeight.w600, + letterSpacing: 0.1, + ), + bodyLarge: TextStyle( + color: Color(0xFFE5E7EB), + fontSize: 15, + fontWeight: FontWeight.w400, + ), + bodyMedium: TextStyle( + color: Color(0xFF9CA3AF), + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ); + } + + void _handleLoginSuccess(String phoneNumber, String accountNumber, String userRole) { + setState(() { + _isAuthenticated = true; + _userRole = userRole; + _phoneNumber = phoneNumber; + _accountNumber = accountNumber; + }); + } + + void _handleLogout() { + setState(() { + _isAuthenticated = false; + _userRole = ''; + _phoneNumber = ''; + _accountNumber = ''; + }); + } +} + +/// ============================================================================ +/// LOGIN SCREEN - Premium Authentication Interface +/// ============================================================================ +class LoginScreen extends StatefulWidget { + final Function(String, String, String) onLoginSuccess; + + const LoginScreen({Key? key, required this.onLoginSuccess}) : super(key: key); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final _phoneController = TextEditingController(); + final _accountController = TextEditingController(); + final _formKey = GlobalKey(); + bool _isLoading = false; + + @override + void dispose() { + _phoneController.dispose(); + _accountController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('FundGuard')), + body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 48), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Welcome Back', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.w700, + color: Color(0xFFE5E7EB), + letterSpacing: -0.5, + ), + ), + const SizedBox(height: 12), + const Text( + 'Sign in to your secure account', + style: TextStyle(fontSize: 15, color: Color(0xFF9CA3AF), letterSpacing: 0.2), + ), + const SizedBox(height: 48), + TextFormField( + controller: _phoneController, + keyboardType: TextInputType.phone, + maxLength: 10, + decoration: const InputDecoration( + labelText: 'Phone Number', + hintText: 'Enter 10-digit phone number', + prefixIcon: Icon(Icons.phone_outlined, size: 20), + ), + validator: (value) { + if (value == null || value.isEmpty) return 'Phone number is required'; + if (value.length != 10) return 'Phone number must be 10 digits'; + if (!RegExp(r'^[0-9]+$').hasMatch(value)) return 'Phone number must contain only digits'; + return null; + }, + ), + const SizedBox(height: 24), + TextFormField( + controller: _accountController, + keyboardType: TextInputType.number, + maxLength: 9, + decoration: const InputDecoration( + labelText: 'Account Number', + hintText: 'Enter 9-digit account number', + prefixIcon: Icon(Icons.account_balance_outlined, size: 20), + ), + validator: (value) { + if (value == null || value.isEmpty) return 'Account number is required'; + if (value.length != 9) return 'Account number must be 9 digits'; + if (!RegExp(r'^[0-9]+$').hasMatch(value)) return 'Account number must contain only digits'; + return null; + }, + ), + const SizedBox(height: 48), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isLoading ? null : _performLogin, + child: _isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white)), + ) + : const Text('Sign In'), + ), + ), + const SizedBox(height: 28), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF111820), + border: Border.all(color: const Color(0xFF1E293B), width: 1), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text( + 'Demo Credentials:', + style: TextStyle(color: Color(0xFF6366F1), fontWeight: FontWeight.w600, fontSize: 12, letterSpacing: 0.5), + ), + SizedBox(height: 8), + Text( + 'Phone: 5555555555\nAccount: 555555555', + style: TextStyle(color: Color(0xFF9CA3AF), fontSize: 13, fontFamily: 'monospace'), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + void _performLogin() async { + if (!_formKey.currentState!.validate()) return; + + setState(() => _isLoading = true); + await Future.delayed(const Duration(milliseconds: 1000)); + + final phone = _phoneController.text.trim(); + final account = _accountController.text.trim(); + + if (phone == '5555555555' && account == '555555555') { + widget.onLoginSuccess(phone, account, 'Admin'); + setState(() => _isLoading = false); + return; + } + + setState(() => _isLoading = false); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Invalid Credentials for testing'), + backgroundColor: Color(0xFFF87171), + duration: Duration(seconds: 2), + ), + ); + } +} + +/// ============================================================================ +/// DASHBOARD SCREEN - Premium Dark Mode with Profile Popup +/// ============================================================================ +class DashboardScreen extends StatefulWidget { + final String userRole; + final String phoneNumber; + final String accountNumber; + final VoidCallback onLogout; + + const DashboardScreen({ + Key? key, + required this.userRole, + required this.phoneNumber, + required this.accountNumber, + required this.onLogout, + }) : super(key: key); + + @override + State createState() => _DashboardScreenState(); +} + +class _DashboardScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Dashboard'), + elevation: 0, + actions: [ + Padding( + padding: const EdgeInsets.only(right: 16), + child: PremiumProfilePopupMenu( + phoneNumber: widget.phoneNumber, + accountNumber: widget.accountNumber, + onLogout: _handleLogout, + ), + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Dashboard', + style: TextStyle(fontSize: 28, fontWeight: FontWeight.w700, color: Color(0xFFE5E7EB), letterSpacing: -0.3), + ), + const SizedBox(height: 8), + const Text( + 'Secure fraud detection and prevention platform', + style: TextStyle(fontSize: 14, color: Color(0xFF9CA3AF), letterSpacing: 0.1), + ), + const SizedBox(height: 32), + _buildMetricsCard(), + const SizedBox(height: 24), + _buildActivityCard(), + ], + ), + ), + ); + } + + void _handleLogout() { + widget.onLogout(); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (BuildContext context) => LoginScreen( + onLoginSuccess: (phone, account, role) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => DashboardScreen( + userRole: role, + phoneNumber: phone, + accountNumber: account, + onLogout: widget.onLogout, + ), + ), + ); + }, + ), + ), + (Route route) => false, + ); + } + + Widget _buildMetricsCard() { + return Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: const Color(0xFF111820), + border: Border.all(color: const Color(0xFF1E293B), width: 1), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow(color: Colors.black54, blurRadius: 12, offset: const Offset(0, 4)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'System Health', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFFE5E7EB)), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildMetric('Active', '24/7', Colors.emerald), + _buildMetric('Alerts', '3', Colors.amber), + _buildMetric('Protected', '1.2M', const Color(0xFF38BDF8)), + ], + ), + ], + ), + ); + } + + Widget _buildMetric(String label, String value, Color color) { + return Column( + children: [ + Text(value, style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: color)), + const SizedBox(height: 6), + Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF6B7280), letterSpacing: 0.3)), + ], + ); + } + + Widget _buildActivityCard() { + return Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: const Color(0xFF111820), + border: Border.all(color: const Color(0xFF1E293B), width: 1), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow(color: Colors.black54, blurRadius: 12, offset: const Offset(0, 4)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Recent Activity', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFFE5E7EB)), + ), + const SizedBox(height: 16), + _buildActivityItem('Fraud Model Updated', '2 hours ago'), + _buildActivityItem('Risk Score Recalculated', '4 hours ago'), + _buildActivityItem('Compliance Report Generated', '1 day ago'), + ], + ), + ); + } + + Widget _buildActivityItem(String title, String time) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: const TextStyle(fontSize: 14, color: Color(0xFFE5E7EB))), + Text(time, style: const TextStyle(fontSize: 12, color: Color(0xFF6B7280))), + ], + ), + ); + } +} + +/// ============================================================================ +/// PREMIUM PROFILE POPUP MENU - Glassmorphic Design with Modern Aesthetics +/// ============================================================================ +class PremiumProfilePopupMenu extends StatefulWidget { + final String phoneNumber; + final String accountNumber; + final VoidCallback onLogout; + + const PremiumProfilePopupMenu({ + Key? key, + required this.phoneNumber, + required this.accountNumber, + required this.onLogout, + }) : super(key: key); + + @override + State createState() => _PremiumProfilePopupMenuState(); +} + +class _PremiumProfilePopupMenuState extends State { + @override + Widget build(BuildContext context) { + return PopupMenuButton( + offset: const Offset(0, 60), // Precise offset for perfect alignment + constraints: const BoxConstraints(minWidth: 280, maxWidth: 320), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide(color: Color(0xFF1E293B), width: 1.2), + ), + color: const Color(0xFF0D131C), // Ultra-deep slate-blue + elevation: 0, + shadowColor: Colors.black54, + itemBuilder: (BuildContext context) => [ + // ========== BLOCK 1: Premium Session Header (Non-clickable) ========== + PopupMenuItem( + enabled: false, + padding: EdgeInsets.zero, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFF151F2E), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Session Status Badge + Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.emerald, + boxShadow: [ + BoxShadow( + color: Colors.emerald.withOpacity(0.5), + blurRadius: 4, + ), + ], + ), + ), + const SizedBox(width: 8), + const Text( + 'SYSTEM SESSION ACTIVE', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w600, + color: Colors.emerald, + letterSpacing: 1.5, + ), + ), + ], + ), + const SizedBox(height: 14), + // Account Details + Text( + 'Phone: ${widget.phoneNumber}', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Color(0xFFE5E7EB), + letterSpacing: 0.2, + ), + ), + const SizedBox(height: 6), + Text( + 'A/C: ${widget.accountNumber}', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w400, + color: Color(0xFFA1A5B0), + letterSpacing: 0.1, + ), + ), + ], + ), + ), + ), + // Custom Divider + PopupMenuItem( + enabled: false, + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), + child: Container( + height: 1, + color: const Color(0xFF1E293B).withOpacity(0.4), + ), + ), + + // ========== BLOCK 2: Manage Profile Action Link ========== + PopupMenuItem( + value: 'manage', + padding: EdgeInsets.zero, + onTap: () { + Future.delayed(const Duration(milliseconds: 50), () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const ProfileRoutePage()), + ); + }); + }, + child: InkWell( + splashColor: const Color(0xFF38BDF8).withOpacity(0.1), + highlightColor: const Color(0xFF38BDF8).withOpacity(0.05), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + children: [ + const Icon(Icons.tune_rounded, color: Color(0xFF38BDF8), size: 20), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Manage Profile', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFFE5E7EB), + letterSpacing: 0.2, + ), + ), + ), + Icon(Icons.chevron_right_rounded, color: Colors.white.withOpacity(0.3), size: 18), + ], + ), + ), + ), + ), + + // ========== BLOCK 3: High-Visibility Logout Action ========== + PopupMenuItem( + value: 'logout', + padding: EdgeInsets.zero, + onTap: () { + Future.delayed(const Duration(milliseconds: 50), () { + _showLogoutConfirmation(context); + }); + }, + child: InkWell( + splashColor: const Color(0xFFF87171).withOpacity(0.1), + highlightColor: const Color(0xFFF87171).withOpacity(0.05), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + children: [ + const Icon(Icons.power_settings_new_rounded, color: Color(0xFFF87171), size: 20), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Sign Out', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFFF87171), + letterSpacing: 0.2, + ), + ), + ), + ], + ), + ), + ), + ), + ], + child: Center( + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black54, + blurRadius: 15, + offset: const Offset(0, 4), + ), + ], + ), + child: CircleAvatar( + radius: 22, + // Smooth linear gradient: Deep Purple to Neon Indigo + backgroundColor: Colors.transparent, + backgroundImage: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF7C3AED), // Deep Purple + Color(0xFF6366F1), // Neon Indigo + ], + ).createShader(const Rect.fromLTWH(0, 0, 44, 44)) as ImageProvider, + child: const Text( + 'R', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w700, + letterSpacing: 0.2, + ), + ), + ), + ), + ), + ); + } + + void _showLogoutConfirmation(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: const Color(0xFF111820), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide(color: Color(0xFF1E293B), width: 1), + ), + title: const Text( + 'Confirm Sign Out', + style: TextStyle( + color: Color(0xFFE5E7EB), + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + content: const Text( + 'Are you sure you want to sign out of your account?', + style: TextStyle( + color: Color(0xFF9CA3AF), + fontSize: 14, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text( + 'Cancel', + style: TextStyle(color: Color(0xFF6B7280)), + ), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + widget.onLogout(); + }, + child: const Text( + 'Sign Out', + style: TextStyle(color: Color(0xFFF87171), fontWeight: FontWeight.w600), + ), + ), + ], + ); + }, + ); + } +} + +/// ============================================================================ +/// PROFILE ROUTE PAGE - Premium Profile Management +/// ============================================================================ +class ProfileRoutePage extends StatelessWidget { + const ProfileRoutePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Profile Settings'), + leading: IconButton( + icon: const Icon(Icons.arrow_back_rounded), + onPressed: () => Navigator.pop(context), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Manage Your Account', + style: TextStyle(fontSize: 26, fontWeight: FontWeight.w700, color: Color(0xFFE5E7EB), letterSpacing: -0.3), + ), + const SizedBox(height: 8), + const Text( + 'Update your profile information and security settings', + style: TextStyle(fontSize: 14, color: Color(0xFF9CA3AF)), + ), + const SizedBox(height: 32), + _buildSettingsCard( + 'Account Information', + [ + _buildSettingRow('Email', 'user@fundguard.com'), + const SizedBox(height: 16), + _buildSettingRow('Status', 'Active'), + const SizedBox(height: 16), + _buildSettingRow('Member Since', '2026'), + ], + ), + const SizedBox(height: 24), + _buildSettingsCard( + 'Security', + [ + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.lock_outline_rounded, size: 18), + label: const Text('Change Password'), + ), + ), + ], + ), + ], + ), + ), + ); + } + + static Widget _buildSettingsCard(String title, List children) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFF111820), + border: Border.all(color: const Color(0xFF1E293B), width: 1), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFFE5E7EB)), + ), + const SizedBox(height: 16), + ...children, + ], + ), + ); + } + + static Widget _buildSettingRow(String label, String value) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFF9CA3AF))), + Text(value, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFFE5E7EB))), + ], + ); + } +} diff --git a/services/dashboard/src/App.tsx b/services/dashboard/src/App.tsx index c3bbb55..7e702bd 100644 --- a/services/dashboard/src/App.tsx +++ b/services/dashboard/src/App.tsx @@ -1,24 +1,33 @@ import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import { AuthProvider } from "./context/AuthContext"; import MainLayout from "./layout/MainLayout"; import Dashboard from "./pages/Dashboard"; import GraphView from "./pages/GraphView"; import Cases from "./pages/Cases"; import Alerts from "./pages/Alerts"; import Reports from "./pages/Reports"; +import Profile from "./pages/Profile"; +import Documentation from "./pages/Documentation"; +import Authentication from "./pages/Authentication"; function App() { return ( - - }> - } /> - } /> - } /> - } /> - } /> - } /> - - + + + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); } diff --git a/services/dashboard/src/context/AuthContext.tsx b/services/dashboard/src/context/AuthContext.tsx new file mode 100644 index 0000000..aea0365 --- /dev/null +++ b/services/dashboard/src/context/AuthContext.tsx @@ -0,0 +1,64 @@ +import React, { createContext, useContext, useState, useCallback, useEffect } from "react"; + +type AuthContextType = { + isLoggedIn: boolean; + userEmail: string | null; + login: (email: string) => void; + logout: () => void; +}; + +const AuthContext = createContext(null); + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within AuthProvider"); + } + return context; +} + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [isLoggedIn, setIsLoggedIn] = useState(() => { + try { + const stored = localStorage.getItem("isLoggedIn"); + return stored === "true"; + } catch { + return false; + } + }); + const [userEmail, setUserEmail] = useState(() => { + try { + return localStorage.getItem("userEmail"); + } catch { + return null; + } + }); + + const login = useCallback((email: string) => { + setIsLoggedIn(true); + setUserEmail(email); + try { + localStorage.setItem("isLoggedIn", "true"); + localStorage.setItem("userEmail", email); + } catch { + // LocalStorage not available + } + }, []); + + const logout = useCallback(() => { + setIsLoggedIn(false); + setUserEmail(null); + try { + localStorage.removeItem("isLoggedIn"); + localStorage.removeItem("userEmail"); + } catch { + // LocalStorage not available + } + }, []); + + return ( + + {children} + + ); +} diff --git a/services/dashboard/src/layout/MainLayout.tsx b/services/dashboard/src/layout/MainLayout.tsx index 66b6047..6dfd102 100644 --- a/services/dashboard/src/layout/MainLayout.tsx +++ b/services/dashboard/src/layout/MainLayout.tsx @@ -1,48 +1,351 @@ -import { Outlet, NavLink } from "react-router-dom"; -import { Activity, ShieldAlert, GitGraph, Briefcase, FileText } from "lucide-react"; +import { createContext, useContext, useEffect, useState } from "react"; +import { Outlet, NavLink, Link, useNavigate } from "react-router-dom"; +import { motion } from "framer-motion"; +import { useAuth } from "../context/AuthContext"; +import { + Activity, + ShieldAlert, + GitGraph, + Briefcase, + FileText, + Menu, + X, + Search, + MapPin, + ShieldCheck, + Moon, + Sun, + LogOut, + KeyRound, + Settings, + ChevronRight, +} from "lucide-react"; + +type DashboardAnchorContextType = { + registerDashboardAnchor: (node: HTMLDivElement | null) => void; + requestDashboardScroll: () => void; +}; + +type ThemeContextType = { + isDarkMode: boolean; +}; + +const DashboardAnchorContext = createContext(null); +const ThemeContext = createContext(null); + +export function useDashboardAnchor() { + const context = useContext(DashboardAnchorContext); + if (!context) { + throw new Error("useDashboardAnchor must be used within MainLayout"); + } + return context; +} + +export function useThemeMode() { + const context = useContext(ThemeContext); + if (!context) { + throw new Error("useThemeMode must be used within MainLayout"); + } + return context; +} export default function MainLayout() { + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const [isProfileOpen, setIsProfileOpen] = useState(false); + const [isDarkMode, setIsDarkMode] = useState(true); + const [dashboardAnchor, setDashboardAnchor] = useState(null); + const [pendingDashboardScroll, setPendingDashboardScroll] = useState(false); + const navigate = useNavigate(); + const { isLoggedIn, logout } = useAuth(); + + useEffect(() => { + if (pendingDashboardScroll && dashboardAnchor) { + dashboardAnchor.scrollIntoView({ behavior: "smooth", block: "start" }); + setPendingDashboardScroll(false); + } + }, [pendingDashboardScroll, dashboardAnchor]); + + useEffect(() => { + document.body.style.overflow = isSidebarOpen ? "hidden" : ""; + return () => { + document.body.style.overflow = ""; + }; + }, [isSidebarOpen]); + + // Keep a `dark` class on the document element so Tailwind `dark:` variants work + useEffect(() => { + try { + if (isDarkMode) document.documentElement.classList.add("dark"); + else document.documentElement.classList.remove("dark"); + } catch (e) { + // ignore in non-browser test environments + } + }, [isDarkMode]); + + const requestDashboardScroll = () => { + setPendingDashboardScroll(true); + navigate("/dashboard"); + setIsSidebarOpen(false); + }; + + const rootThemeClasses = isDarkMode + ? "bg-[#0B111E] text-slate-100" + : "bg-[#F4F6F9] text-slate-950"; + + const headerSurface = isDarkMode + ? "bg-[#0F172A]/80 border-slate-800 text-slate-100" + : "bg-white/90 border-slate-200 text-slate-950"; + + const surfacePanel = isDarkMode + ? "bg-[#1E293B] border-slate-700 text-slate-100" + : "bg-white border-slate-200 text-slate-950"; + + const profilePopupSurface = isDarkMode + ? "bg-slate-800 border border-slate-700 text-slate-100" + : "bg-white border border-gray-200 text-gray-900 shadow-2xl shadow-gray-400/30"; + + const profileAccentButton = isDarkMode + ? "bg-emerald-500 text-slate-950 hover:bg-emerald-400" + : "bg-emerald-700 text-white hover:bg-emerald-600"; + + // Redirect unauthenticated users to auth page + useEffect(() => { + if (!isLoggedIn) { + const currentPath = window.location.pathname; + if (currentPath !== "/auth" && !currentPath.startsWith("/auth")) { + // Optional: Redirect to auth if not on auth page + // navigate("/auth"); + } + } + }, [isLoggedIn]); + return ( -
- {/* Sidebar */} - - - {/* Main Content */} -
-
-

Investigation Hub

-
-
- + + +
+
+
+
+ + +
+

FundGuard

+

BANK FRAUD DETECTION PLATFORM

+
+ +
+ + System Scope: India Operations +
+
+ +
+ + +
+ +
+ + {/* setIsDarkMode((current) => !current)} + whileTap={{ scale: 0.95 }} + animate={{ rotate: isDarkMode ? 360 : 0 }} + transition={{ duration: 0.35, ease: "easeInOut" }} + className="grid h-10 w-10 place-items-center rounded-full border border-white/5 bg-[#0f1724] text-slate-100 transition hover:bg-[#111827]" + aria-label="Toggle dark mode" + > + {isDarkMode ? : } + */} + + {/* Auth Section */} + {!isLoggedIn ? ( +
+ + +
+ ) : ( +
+ + {isProfileOpen && ( + + {/* ========== BLOCK 1: Premium Account Session Header ========== */} +
+
+ + Admin Account +
+
+

Phone: 5555555555

+

A/C No: 555555555

+
+
+ + {/* ========== BLOCK 2: Manage Profile Button ========== */} + + + {/* Divider */} +
+ + {/* ========== BLOCK 3: Logout Action ========== */} + + + )} +
+ )} +
+
+
+ + setIsSidebarOpen(false)} + initial={false} + animate={{ opacity: isSidebarOpen ? 1 : 0 }} + /> + + +
+
+

Feature Center

+

FundGuard Workspace

+
+ +
+ +
+ +
+ +
-
-
+ + ); } -function NavItem({ to, icon, label }: { to: string; icon: React.ReactNode; label: string }) { +function SidebarLink({ + icon, + label, + to, + onClick, +}: { + icon: React.ReactNode; + label: string; + to?: string; + onClick: () => void; +}) { + if (to) { + return ( + + `flex items-center gap-3 rounded-2xl border border-slate-700 px-4 py-3 text-sm font-medium transition ${ + isActive ? "bg-emerald-500/10 text-emerald-300" : "text-slate-300 hover:bg-white/5" + }` + } + > + {icon} + {label} + + ); + } + return ( - - `flex items-center gap-3 px-4 py-3 rounded-lg transition-colors font-medium ${ - isActive ? "bg-gray-800 text-safe" : "text-gray-400 hover:text-gray-200 hover:bg-gray-800/50" - }` - } + ); -} \ No newline at end of file +} diff --git a/services/dashboard/src/pages/Authentication.tsx b/services/dashboard/src/pages/Authentication.tsx new file mode 100644 index 0000000..b9edefa --- /dev/null +++ b/services/dashboard/src/pages/Authentication.tsx @@ -0,0 +1,450 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; +import { + Landmark, + Phone, + Mail, + User, + ShieldCheck, + Lock, + ArrowRight, + CheckCircle2, +} from "lucide-react"; + +const Authentication = () => { + const navigate = useNavigate(); + const { isLoggedIn, login } = useAuth(); + const [authMode, setAuthMode] = useState<"login" | "signup">("login"); + const [step, setStep] = useState<"credentials" | "otp">("credentials"); + const [loading, setLoading] = useState(false); + const [countdown, setCountdown] = useState(0); + + // If already logged in, redirect to dashboard + useEffect(() => { + if (isLoggedIn) { + navigate("/dashboard"); + } + }, [isLoggedIn, navigate]); + + // Login state + const [loginForm, setLoginForm] = useState({ + accountNumber: "", + phoneNumber: "", + }); + + // OTP state + const [otpCode, setOtpCode] = useState(""); + + // Sign-up state + const [signupForm, setSignupForm] = useState({ + fullName: "", + bankEmail: "", + accountNumber: "", + phoneNumber: "", + }); + + const [complianceChecked, setComplianceChecked] = useState(false); + + // Countdown timer for OTP + useEffect(() => { + if (countdown > 0) { + const timer = setTimeout(() => setCountdown(countdown - 1), 1000); + return () => clearTimeout(timer); + } + }, [countdown]); + + // Handle login credentials submission + const handleRequestVerification = () => { + if (!loginForm.accountNumber || !loginForm.phoneNumber) { + alert("Please fill in all fields"); + return; + } + + setLoading(true); + // Simulate API call + setTimeout(() => { + setStep("otp"); + setCountdown(45); + setLoading(false); + }, 1000); + }; + + // Handle OTP verification + const handleVerifyOTP = () => { + if (otpCode.length !== 6) { + alert("Please enter a valid 6-digit OTP"); + return; + } + + setLoading(true); + // Simulate API call with mock OTP validation + setTimeout(() => { + if (otpCode === "123456") { + // Mock successful verification + login(loginForm.phoneNumber); + navigate("/dashboard"); + } else { + alert("Invalid OTP. Try 123456 for demo."); + setOtpCode(""); + } + setLoading(false); + }, 1000); + }; + + // Handle sign-up submission + const handleSignUp = () => { + if ( + !signupForm.fullName || + !signupForm.bankEmail || + !signupForm.accountNumber || + !signupForm.phoneNumber + ) { + alert("Please fill in all fields"); + return; + } + + if (!complianceChecked) { + alert("Please accept the compliance terms"); + return; + } + + setLoading(true); + // Simulate API call + setTimeout(() => { + alert("Account created successfully! Please log in."); + setAuthMode("login"); + setStep("credentials"); + setSignupForm({ fullName: "", bankEmail: "", accountNumber: "", phoneNumber: "" }); + setComplianceChecked(false); + setLoading(false); + }, 1000); + }; + + // Handle resend OTP + const handleResendOTP = () => { + setCountdown(45); + }; + + return ( +
+ {/* Decorative background blurs */} +
+
+ + {/* Main Card */} +
+ + {/* Login View */} + {authMode === "login" && ( +
+ {/* Header */} +
+

FundGuard

+

Fraud Detection Platform

+

+ {step === "credentials" + ? "Secure authentication portal" + : "Verify your identity with OTP"} +

+
+ + {/* Credentials Step */} + {step === "credentials" && ( +
+ {/* Account Number Input */} +
+ +
+ + + setLoginForm({ + ...loginForm, + accountNumber: e.target.value, + }) + } + className="w-full bg-gray-900/40 border border-gray-800 rounded-lg p-3 pl-10 text-gray-200 outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/20 transition-all duration-200" + /> +
+
+ + {/* Phone Number Input */} +
+ +
+ + + setLoginForm({ + ...loginForm, + phoneNumber: e.target.value, + }) + } + className="w-full bg-gray-900/40 border border-gray-800 rounded-lg p-3 pl-10 text-gray-200 outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/20 transition-all duration-200" + /> +
+
+ + {/* Request Verification Button */} + +
+ )} + + {/* OTP Step */} + {step === "otp" && ( +
+ {/* OTP Input */} +
+ +
+ + { + const val = e.target.value.replace(/\D/g, "").slice(0, 6); + setOtpCode(val); + }} + maxLength={6} + className="w-full bg-gray-900/40 border border-gray-800 rounded-lg p-3 pl-10 text-gray-200 outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/20 transition-all duration-200 font-mono text-lg tracking-widest text-center" + /> +
+

+ (Demo: Use code 123456) +

+
+ + {/* Countdown Timer */} +
+ {countdown > 0 ? ( +

+ Resend OTP in{" "} + + 0:{countdown.toString().padStart(2, "0")} + +

+ ) : ( + + )} +
+ + {/* Verify Button */} + + + {/* Back Button */} + +
+ )} + + {/* Mode Switch */} +
+ New to FundGuard?{" "} + +
+
+ )} + + {/* Sign-Up View */} + {authMode === "signup" && ( +
+ {/* Header */} +
+

Create Account

+

FundGuard Platform

+

+ Register your node link network +

+
+ + {/* Form Fields */} +
+ {/* Full Name */} +
+ +
+ + + setSignupForm({ + ...signupForm, + fullName: e.target.value, + }) + } + className="w-full bg-gray-900/40 border border-gray-800 rounded-lg p-3 pl-10 text-gray-200 outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/20 transition-all duration-200" + /> +
+
+ + {/* Bank Email */} +
+ +
+ + + setSignupForm({ + ...signupForm, + bankEmail: e.target.value, + }) + } + className="w-full bg-gray-900/40 border border-gray-800 rounded-lg p-3 pl-10 text-gray-200 outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/20 transition-all duration-200" + /> +
+
+ + {/* Account Number */} +
+ +
+ + + setSignupForm({ + ...signupForm, + accountNumber: e.target.value, + }) + } + className="w-full bg-gray-900/40 border border-gray-800 rounded-lg p-3 pl-10 text-gray-200 outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/20 transition-all duration-200" + /> +
+
+ + {/* Phone Number */} +
+ +
+ + + setSignupForm({ + ...signupForm, + phoneNumber: e.target.value, + }) + } + className="w-full bg-gray-900/40 border border-gray-800 rounded-lg p-3 pl-10 text-gray-200 outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/20 transition-all duration-200" + /> +
+
+ + {/* Compliance Checkbox */} +
+ setComplianceChecked(e.target.checked)} + className="mt-1 w-4 h-4 accent-emerald-500 cursor-pointer" + /> + +
+
+ + {/* Sign-Up Button */} + + + {/* Mode Switch */} +
+ Already authorized on this node?{" "} + +
+
+ )} +
+
+ ); +}; + +export default Authentication; diff --git a/services/dashboard/src/pages/Cases.tsx b/services/dashboard/src/pages/Cases.tsx index 0cf1407..8fc9994 100644 --- a/services/dashboard/src/pages/Cases.tsx +++ b/services/dashboard/src/pages/Cases.tsx @@ -57,16 +57,21 @@ export default function Cases() { .catch(console.error); }; + // Metric calculations + const totalAlerts = data.length; + const criticalCases = data.filter(c => c.unified_score > 0.8).length; + const pendingAction = data.filter(c => c.status === 'OPEN').length; + const columns = useMemo(() => [ columnHelper.accessor('id', { header: 'Case ID', - cell: info => {info.getValue()}, + cell: info => 📁{info.getValue()}, }), columnHelper.accessor('sender_account_id', { header: 'Account ID', cell: info => ( - - {info.getValue()} + + 🏛️{info.getValue()} ), }), @@ -75,20 +80,65 @@ export default function Cases() { cell: info => { const score = info.getValue() const normalized = Math.round(score * 100) - const color = normalized > 80 ? 'text-red-500' : normalized > 50 ? 'text-yellow-500' : 'text-green-500' - return {normalized} + let indicator = '🟢'; + let color = 'text-green-400'; + + if (normalized > 80) { + indicator = '🔴'; + color = 'text-red-400'; + } else if (normalized > 50) { + indicator = '🟡'; + color = 'text-yellow-400'; + } + + return ( +
+ {indicator} + {normalized} +
+ ) }, }), columnHelper.accessor('status', { header: 'Status', cell: info => { const status = info.getValue() + let badgeConfig = { + emoji: '❓', + text: status, + className: 'bg-gray-500/10 text-gray-400 border border-gray-500/30' + }; + + if (status === 'OPEN') { + badgeConfig = { + emoji: '🔴', + text: 'Unassigned Alert', + className: 'bg-red-500/10 text-red-400 border border-red-500/30' + }; + } else if (status === 'INVESTIGATING') { + badgeConfig = { + emoji: '🔍', + text: 'Active Check', + className: 'bg-blue-500/10 text-blue-400 border border-blue-500/30' + }; + } else if (status === 'CLOSED') { + badgeConfig = { + emoji: '✅', + text: 'Cleared', + className: 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/30' + }; + } else if (status === 'PENDING_STR') { + badgeConfig = { + emoji: '⚠️', + text: 'Escalated to Legal', + className: 'bg-amber-500/10 text-amber-400 border border-amber-500/30' + }; + } + return ( - - {status} + + {badgeConfig.emoji} + {badgeConfig.text} ) }, @@ -105,24 +155,24 @@ export default function Cases() { {info.row.original.status === 'OPEN' && ( )} {(info.row.original.status === 'OPEN' || info.row.original.status === 'INVESTIGATING') && ( <> )} @@ -152,6 +202,33 @@ export default function Cases() {
+
+
+
+
+

Total Alerts

+

📊 {totalAlerts}

+
+
+
+
+
+
+

Critical Cases

+

🚨 {criticalCases}

+
+
+
+
+
+
+

Pending Action

+

⏳ {pendingAction}

+
+
+
+
+

Active Investigations

@@ -177,8 +254,14 @@ export default function Cases() { {data.length === 0 ? ( - - No suspicious cases reported in history. Run integration tests to stream events. + +
+ 🛰️ +

FundGuard Core Pipeline Operational

+

+ No unmapped transaction anomalies identified in this batch cycle. Run standard system validation tests to inject synthetic Kafka streaming vectors. +

+
) : table.getRowModel().rows.map(row => ( diff --git a/services/dashboard/src/pages/Dashboard.tsx b/services/dashboard/src/pages/Dashboard.tsx index 01e1018..b4a34c1 100644 --- a/services/dashboard/src/pages/Dashboard.tsx +++ b/services/dashboard/src/pages/Dashboard.tsx @@ -1,5 +1,8 @@ import { useEffect, useRef, useState } from "react"; +import { motion } from "framer-motion"; +import { Activity, AlertTriangle, ShieldAlert, Zap, Layers } from "lucide-react"; import { API_BASE_URL, WS_URL } from "../config/endpoints"; +import { useDashboardAnchor, useThemeMode } from "../layout/MainLayout"; type RiskEvent = { transaction_id?: string; @@ -13,6 +16,8 @@ type RiskEvent = { }; export default function Dashboard() { + const { registerDashboardAnchor } = useDashboardAnchor(); + const { isDarkMode } = useThemeMode(); const [alerts, setAlerts] = useState([]); const [latestEvent, setLatestEvent] = useState(null); const [wsConnected, setWsConnected] = useState(false); @@ -27,19 +32,14 @@ export default function Dashboard() { }); useEffect(() => { - // Load historical data on mount fetch(`${API_BASE_URL}/api/stats`) - .then(r => r.json()) - .then(data => { - setStats(prev => ({...prev, ...data})); - }) + .then((r) => r.json()) + .then((data) => setStats((prev) => ({ ...prev, ...data }))) .catch(console.error); fetch(`${API_BASE_URL}/api/recent-alerts?limit=10`) - .then(r => r.json()) - .then(data => { - setAlerts(data); - }) + .then((r) => r.json()) + .then((data) => setAlerts(data)) .catch(console.error); }, []); @@ -49,12 +49,10 @@ export default function Dashboard() { ws.onopen = () => { streamStartedAt.current = Date.now(); setWsConnected(true); - console.log("Connected to Risk Engine WS"); }; ws.onmessage = (event) => { const data = JSON.parse(event.data) as RiskEvent; - setLatestEvent(data); setStats((prev) => { const liveEvents = prev.liveEvents + 1; @@ -86,62 +84,160 @@ export default function Dashboard() { }; }, []); + const surface = isDarkMode + ? "bg-[#1E293B] border-slate-700 text-slate-100" + : "bg-[#F8FAFC] border-slate-200 text-slate-950"; + const panelSurface = isDarkMode + ? "bg-[#112131] border-slate-700 text-slate-100" + : "bg-white border-slate-200 text-slate-950"; + return ( -
-

Live Overview

-
- 0} /> - 0} /> - - - 0} /> +
+
+

LIVE OVERVIEW

+

Operational Risk Insights

+
+ +
+ } + label="LIVE INGESTED EVENTS" + value={stats.liveEvents.toString()} + isDarkMode={isDarkMode} + themeSurface={surface} + /> + } + label="ACTIVE RISK ALERTS" + value={stats.activeAlerts.toString()} + highlight={stats.activeAlerts > 0} + isDarkMode={isDarkMode} + themeSurface={surface} + /> + } + label="CORE REJECT RATE" + value={stats.fraudRate} + isDarkMode={isDarkMode} + themeSurface={surface} + /> + } + label="TRANS / MIN (RPM)" + value={stats.transMin} + isDarkMode={isDarkMode} + themeSurface={surface} + /> + } + label="HIGH RISK NODES" + value={stats.highRisk.toString()} + isDarkMode={isDarkMode} + themeSurface={surface} + />
-
-
-

Recent Signals

-
- {alerts.length === 0 ?

Listening for live alerts...

: alerts.map((a, i) => ( -
-
- {a.transaction_id} - {a.decision} -
-
Score: {a.unified_score?.toFixed(2)}
-
- ))} + +
+ +
+

📡 Real-Time Fraud Telemetry Stream

-
-
-

Risk Engine Real-time Analysis

-
- Stream status: {wsConnected ? "CONNECTED" : "DISCONNECTED"} +
+

+ System standing by... Listening for active fraud risk vectors and transaction telemetry data lines. +

- {latestEvent ? ( -
-
Latest Event
-
{latestEvent.transaction_id}
-
Decision: {latestEvent.decision}
-
Unified score: {latestEvent.unified_score?.toFixed(2) ?? "0.00"}
+ + + +
+
+

🧠 Risk Engine Live AI Assessment

+

+ Kafka ➔ Dashboard API ➔ WebSocket Stream +

- ) : ( -

- Waiting for live transactions. This panel updates for every event, including APPROVE decisions. -

- )} -

Kafka -> Dashboard API -> WebSocket stream

-
+
+
+
+

+ The assessment engine monitors ingest throughput, anomaly spikes, and risk correlation across the operational pipeline. +

+

+ When active connections reopen, the stream badge will reflect live status and connection latency in real time. +

+
+
+
+ + + Operational Stream: + + DISCONNECTED +
+
); } -function MetricCard({ label, value, alert }: { label: string; value: string; alert?: boolean }) { +function MetricCard({ + icon, + label, + value, + highlight, + isDarkMode, + themeSurface, +}: { + icon: React.ReactNode; + label: string; + value: string; + highlight?: boolean; + isDarkMode: boolean; + themeSurface: string; +}) { + const liveBadge = highlight + ? "inline-flex items-center gap-2 rounded-full bg-red-500/10 text-red-400 px-2.5 py-1 text-[10px] font-extrabold" + : "inline-flex items-center gap-2 rounded-full bg-emerald-500/10 text-emerald-400 px-2.5 py-1 text-[10px] font-extrabold"; + return ( -
-
{label}
-
+ +
+
+
+ {icon} + {label} +
+
+ {/* + {highlight && } + {highlight ? "LIVE" : "Live"} + */} +
+
+
{value}
-
+ + + + {highlight && } + {highlight ? "LIVE" : "Live"} + + +
+ ); } diff --git a/services/dashboard/src/pages/Documentation.tsx b/services/dashboard/src/pages/Documentation.tsx new file mode 100644 index 0000000..35b269e --- /dev/null +++ b/services/dashboard/src/pages/Documentation.tsx @@ -0,0 +1,462 @@ +import React, { useState } from "react"; +import { Link } from "react-router-dom"; +import { + ShieldCheck, + Search, + BookOpen, + Brain, + Database, + Activity, + Bell, + Network, + BarChart3, + Cpu, + ArrowRight, + Home, + Workflow, + AlertTriangle, +} from "lucide-react"; + +const sidebarItems = [ + { + title: "Overview", + items: [ + { + id: "intro", + icon: , + label: "Introduction", + }, + { + id: "architecture", + icon: , + label: "Architecture", + }, + { + id: "features", + icon: , + label: "Features", + }, + { + id: "dataflow", + icon: , + label: "Data Flow", + }, + ], + }, + { + title: "Detection", + items: [ + { + id: "fraud", + icon: , + label: "Fraud Methods", + }, + { + id: "risk", + icon: , + label: "Risk Factors", + }, + { + id: "ml", + icon: , + label: "ML Models", + }, + { + id: "graph", + icon: , + label: "Graph Engine", + }, + { + id: "rule", + icon: , + label: "Rule Engine", + }, + ], + }, + { + title: "Operations", + items: [ + { + id: "monitoring", + icon: , + label: "Monitoring", + }, + { + id: "alerts", + icon: , + label: "Alerts", + }, + { + id: "deployment", + icon: , + label: "Deployment", + }, + ], + }, +]; + +const cards = [ + { + title: "Fraud Detection", + desc: "Advanced fraud prevention methods and layered detection logic.", + icon: , + }, + { + title: "Risk Scoring", + desc: "AI-powered transaction risk assessment engine.", + icon: , + }, + { + title: "ML Models", + desc: "Machine learning pipelines for anomaly detection.", + icon: , + }, + { + title: "Graph Analytics", + desc: "Relationship mapping and fraud network discovery.", + icon: , + }, + { + title: "Rule Engine", + desc: "Custom fraud rules and real-time business logic.", + icon: , + }, + { + title: "Architecture", + desc: "Microservices infrastructure and deployment overview.", + icon: , + }, +]; + +const Documentation = () => { + const [activeDoc, setActiveDoc] = useState("intro"); + + const renderContent = (id: string) => { + const basePanel = "rounded-3xl border border-white/5 bg-[#071224] p-6 lg:p-8"; + + switch (id) { + case "fraud": + return ( +
+
+

Fraud Detection

+

+ Active transaction interceptors, payload validation hooks, and malicious pattern flags are used + to intercept suspicious flows before settlement. Each interceptor can be configured with + customizable thresholds and quarantine actions. +

+ +
+
+

Interceptors

+

Hook into streams and block high risk payloads.

+
+
+

Validation Hooks

+

Schema validation and enrichment before scoring.

+
+
+ +
+ + Live Simulation + +
+
+
+ ); + + case "risk": + return ( +
+
+

Risk Scoring

+

+ API request/response payloads document the transaction risk score weightings, feature vectors, + and normalization strategies used during scoring. +

+ +
+
+

Request Payload

+

Include transaction, account, device, and geo features.

+
+
+

Response Schema

+

Risk score, confidence, contributing factors, and recommended action.

+
+
+ +
+ + Live Simulation + +
+
+
+ ); + + case "ml": + return ( +
+
+

ML Models

+

+ Real-time anomaly detection pipelines, latency expectations, and model inference schemas + for production deployments. +

+ +
+
+

Pipelines

+

Streaming preprocessing ➔ feature store ➔ model scoring.

+
+
+

Inference

+

ONNX / optimized runtimes with strict p99 latency targets.

+
+
+ +
+ + Live Simulation + +
+
+
+ ); + + case "graph": + return ( +
+
+

Graph Analytics

+

+ Network link analysis API endpoints tracking connected device/account mule rings and relationship scores. +

+ +
+
+

Endpoints

+

Query neighbors, reachability, and cluster scores.

+
+
+

Use Cases

+

Detect mule rings, device reuse, and account collusion.

+
+
+ +
+ + Live Simulation + +
+
+
+ ); + + case "rule": + return ( +
+
+

Rule Engine

+

+ Document the conditional logic structure for custom banking policy adjustments, including policy + chaining, priority resolution, and safe-fallback behaviors. +

+ +
+
+

Conditions

+

IF/THEN clauses using feature predicates and thresholds.

+
+
+

Actions

+

Block, Review, Notify, Quarantine or Custom Webhook actions.

+
+
+ +
+ + Live Simulation + +
+
+
+ ); + + default: + return ( +
+
+

Documentation Overview

+ +

Explore fraud detection modules, machine learning pipelines, and architecture.

+
+ +
+ {cards.map((card, index) => ( +
+
{card.icon}
+ +

{card.title}

+ +

{card.desc}

+ +
+ +
+
+ ))} +
+
+ ); + } + }; + + return ( +
+ {/* Sidebar */} + + + {/* Main Content */} +
+ {/* Topbar */} +
+
+ + + +
+
+ + {/* Hero */} +
+
+
+ +
+
+
+ +
+ +

+ FundGuard Documentation +

+ +

+ AI Powered Fraud Detection +

+ +

+ Comprehensive guides for + understanding, deploying, and + scaling intelligent banking fraud + prevention infrastructure. +

+
+ +
+
+ +
+
+
+
+
+ + {/* Dynamic Documentation Content */} +
+ {renderContent(activeDoc)} +
+
+
+ ); +}; + +export default Documentation; \ No newline at end of file diff --git a/services/dashboard/src/pages/GraphView.tsx b/services/dashboard/src/pages/GraphView.tsx index 240bb3d..942fee1 100644 --- a/services/dashboard/src/pages/GraphView.tsx +++ b/services/dashboard/src/pages/GraphView.tsx @@ -1,15 +1,106 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import { useParams } from "react-router-dom"; import { ReactFlow, MiniMap, Controls, Background, useNodesState, useEdgesState, MarkerType } from '@xyflow/react'; import type { Node, Edge } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; +// Helper function to determine node type and styling from node ID +interface NodeTypeInfo { + type: string; + emoji: string; + bgColor: string; + textColor: string; + borderColor: string; + borderWidth: number; +} + +function getNodeTypeInfo(nodeId: string, isEgo: boolean): NodeTypeInfo { + if (isEgo) { + return { + type: 'ego', + emoji: '🏛️', + bgColor: '#1e293b', + textColor: '#f1f5f9', + borderColor: '#06b6d4', + borderWidth: 2, + }; + } + + if (nodeId.includes('MULE') || nodeId.includes('SUSPECTED')) { + return { + type: 'mule', + emoji: '⚠️', + bgColor: '#7f1d1d', + textColor: '#fca5a5', + borderColor: '#ef4444', + borderWidth: 2, + }; + } + + if (nodeId.startsWith('AC:')) { + return { + type: 'account', + emoji: '💳', + bgColor: '#3f3f46', + textColor: '#fcd34d', + borderColor: '#d97706', + borderWidth: 1.5, + }; + } + + if (nodeId.startsWith('LOC:')) { + return { + type: 'location', + emoji: '📍', + bgColor: '#365314', + textColor: '#bef264', + borderColor: '#84cc16', + borderWidth: 1, + }; + } + + if (nodeId.startsWith('DEV:')) { + const isMobile = nodeId.includes('iPhone') || nodeId.includes('Samsung') || nodeId.includes('Android'); + const isDesktop = nodeId.includes('Windows') || nodeId.includes('Mac') || nodeId.includes('Linux'); + return { + type: 'device', + emoji: isMobile ? '📱' : isDesktop ? '💻' : '🖥️', + bgColor: '#1e3a8a', + textColor: '#93c5fd', + borderColor: '#3b82f6', + borderWidth: 1.5, + }; + } + + if (nodeId.startsWith('IP:')) { + return { + type: 'ip', + emoji: '🌐', + bgColor: '#1a1a2e', + textColor: '#a0aec0', + borderColor: '#64748b', + borderWidth: 1, + }; + } + + // Default unknown node type + return { + type: 'unknown', + emoji: '●', + bgColor: '#374151', + textColor: '#d1d5db', + borderColor: '#6b7280', + borderWidth: 1, + }; +} + export default function GraphView() { const { account_id } = useParams(); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [loading, setLoading] = useState(false); const [selectedNodeData, setSelectedNodeData] = useState(null); + const [graphData, setGraphData] = useState(null); useEffect(() => { if (!account_id) return; @@ -21,37 +112,112 @@ export default function GraphView() { const data = await response.json(); if (data.nodes && data.edges) { - // Neo4j ego network parsing - const newNodes = data.nodes.map((n: any) => ({ - id: n.node_id, - position: { x: Math.random() * 400 + 100, y: Math.random() * 400 + 100 }, // Extremely basic random layout - data: { label: n.id }, - style: { - backgroundColor: n.id === account_id ? '#ff4444' : '#00ff88', - color: n.id === account_id ? '#fff' : '#000', - fontWeight: 'bold', - borderRadius: '8px', - padding: '10px' + setGraphData(data); + + // Calculate radial layout with ego node at origin + const egoIndex = data.nodes.findIndex((n: any) => n.id === account_id); + const leafNodes = data.nodes.filter((n: any) => n.id !== account_id); + const totalLeaves = leafNodes.length; + + // Build position map for all nodes + const positionMap: Record = {}; + + // Place ego node at origin + if (egoIndex >= 0) { + positionMap[data.nodes[egoIndex].node_id] = { x: 0, y: 0 }; + } + + // Distribute leaf nodes in circular ring with 400px radius + leafNodes.forEach((leaf: any, leafIdx: number) => { + const angle = (leafIdx / totalLeaves) * 2 * Math.PI; + const radius = 400; + positionMap[leaf.node_id] = { + x: Math.cos(angle) * radius, + y: Math.sin(angle) * radius, + }; + }); + + // Neo4j ego network node parsing with context-aware styling + const newNodes = data.nodes.map((n: any) => { + const isEgo = n.id === account_id; + const typeInfo = getNodeTypeInfo(n.id, isEgo); + const position = positionMap[n.node_id] || { x: 0, y: 0 }; + + const displayLabel = `${typeInfo.emoji} ${n.id}`; + + return { + id: n.node_id, + position, + data: { + label: displayLabel, + rawLabel: n.id, + typeInfo, + }, + style: { + backgroundColor: typeInfo.bgColor, + color: typeInfo.textColor, + fontWeight: 'bold', + borderRadius: isEgo ? '12px' : '8px', + padding: '12px 16px', + border: `${typeInfo.borderWidth}px solid ${typeInfo.borderColor}`, + fontSize: isEgo ? '14px' : '12px', + boxShadow: typeInfo.borderColor === '#ef4444' + ? `0 0 20px ${typeInfo.borderColor}80` + : typeInfo.borderColor === '#06b6d4' + ? `0 0 15px ${typeInfo.borderColor}60` + : 'none', + whiteSpace: 'nowrap' as const, + textOverflow: 'ellipsis' as const, + }, + }; + }); + + // Dynamic edge risk routing with color logic + const newEdges = data.edges.map((e: any, i: number) => { + // Identify source and target node IDs for risk assessment + const sourceNode = data.nodes.find((n: any) => n.node_id === e.source); + const targetNode = data.nodes.find((n: any) => n.node_id === e.target); + + const sourceId = sourceNode?.id || ''; + const targetId = targetNode?.id || ''; + + // Determine edge color based on risk profile + let edgeColor = '#10b981'; // default emerald (normal) + let edgeWidth = 2; + let isAnimated = false; + + // High-risk: direct connection to MULE RING or SUSPECTED nodes + if (sourceId.includes('MULE') || sourceId.includes('SUSPECTED') || + targetId.includes('MULE') || targetId.includes('SUSPECTED')) { + edgeColor = '#ef4444'; + edgeWidth = 3; + isAnimated = true; + } + // Elevated warning: connections to high-risk accounts + else if (sourceId.startsWith('AC:') && targetId.startsWith('AC:')) { + edgeColor = '#f59e0b'; + edgeWidth = 2.5; + isAnimated = false; } - })); - - // The edges array from Python might be using raw element_id. - // Assuming our backend maps source/target properly or modifying it to use logical IDs - // Note: If backend source/target is element_id, we might need a mapping, but for now we map purely index based on typical neo4j structure - const newEdges = data.edges.map((e: any, i: number) => ({ - id: `edge-${i}`, - source: e.source, - target: e.target, - animated: true, - label: e.amount ? `$${e.amount}` : '', - style: { stroke: '#ff4444', strokeWidth: 2 }, - markerEnd: { type: MarkerType.ArrowClosed, color: '#ff4444' } - })); + + return { + id: `edge-${i}`, + source: e.source, + target: e.target, + animated: isAnimated, + label: e.amount ? `$${e.amount}` : '', + style: { + stroke: edgeColor, + strokeWidth: edgeWidth, + }, + markerEnd: { + type: MarkerType.ArrowClosed, + color: edgeColor, + }, + }; + }); setNodes(newNodes); - - // Temporary fix: If edges have element_id but nodes only have logical 'id', - // this is a known quirk. Assuming backend fixes or using labels. setEdges(newEdges); } } catch (err) { @@ -64,13 +230,100 @@ export default function GraphView() { fetchGraph(); }, [account_id, setNodes, setEdges]); + // Compute node intelligence data for sidebar + const nodeIntelligence = useMemo(() => { + if (!selectedNodeData || !graphData) { + return { + label: account_id || 'No Node Selected', + type: 'Unknown', + riskScore: '—', + velocity: '—', + muleRiskLevel: '—', + }; + } + + const rawLabel = selectedNodeData.rawLabel || selectedNodeData.label; + + // Infer node type and risk profile + let nodeType = 'Account'; + let riskScore = '45 / 100'; + let velocity = '0.8 Tps'; + let muleRiskLevel = 'Low'; + + if (rawLabel.includes('MULE')) { + nodeType = 'Mule Ring Hub'; + riskScore = '92 / 100'; + velocity = '8.5 Tps'; + muleRiskLevel = 'Critical 🚨'; + } else if (rawLabel.startsWith('AC:')) { + nodeType = 'Financial Account'; + riskScore = '65 / 100'; + velocity = '2.3 Tps'; + muleRiskLevel = 'High'; + } else if (rawLabel.startsWith('LOC:')) { + nodeType = 'Geographical Point'; + riskScore = '30 / 100'; + velocity = '—'; + muleRiskLevel = 'Low'; + } else if (rawLabel.startsWith('DEV:')) { + nodeType = 'Device Profile'; + riskScore = '55 / 100'; + velocity = '1.2 Tps'; + muleRiskLevel = 'Medium'; + } else if (rawLabel.startsWith('IP:')) { + nodeType = 'IP Address'; + riskScore = '40 / 100'; + velocity = '—'; + muleRiskLevel = 'Low'; + } else if (rawLabel === account_id) { + nodeType = 'Premium Retail Account (Ego)'; + riskScore = '85 / 100'; + velocity = '2.3 Tps'; + muleRiskLevel = 'High'; + } + + return { + label: rawLabel, + type: nodeType, + riskScore, + velocity, + muleRiskLevel, + }; + }, [selectedNodeData, graphData, account_id]); + + // Compute network insights summary + const networkInsights = useMemo(() => { + if (!graphData) { + return 'Loading network analysis...'; + } + + const nodeCount = graphData.nodes.length; + const edgeCount = graphData.edges.length; + const muleCount = graphData.nodes.filter((n: any) => n.id.includes('MULE')).length; + const accountCount = graphData.nodes.filter((n: any) => n.id.startsWith('AC:')).length; + + if (muleCount > 0) { + return `Complex, high-centrality network component with ${edgeCount} active concurrent links routing across ${muleCount} flagged transaction mule ring(s) and ${accountCount} elevated-risk accounts spanning multiple geographical areas. Immediate asset monitoring and restriction validation recommended.`; + } + + return `Network structure shows ${nodeCount} entities connected through ${edgeCount} transaction pathways. ${accountCount} financial account(s) involved. Moderate-risk profile requiring standard monitoring protocols.`; + }, [graphData]); + return (
-
-
- Network for: {account_id || 'Global Structure (Enter ID)'} + {/* ReactFlow Canvas */} +
+
+ Network for: {account_id || 'Global Structure'}
- {loading &&
Loading Network...
} + {loading && ( +
+
+
+ Loading Network... +
+
+ )}
-
-
Entity Intelligence
-
-
-
SELECTED NODE
-
{selectedNodeData?.label || account_id || 'None'}
-
Velocity: Unknown
-
Fetched explicitly from Neo4j DB
+ + {/* Advanced Intelligence Sidebar */} +
+ {/* Entity Intelligence Card */} +
+
+
+ {selectedNodeData?.typeInfo?.emoji || '●'} +
+
Entity Intelligence
+
+ {nodeIntelligence.label} +
+
+
+ +
+
+ Account Type: + {nodeIntelligence.type} +
+ +
+ Risk Score: + + {nodeIntelligence.riskScore} + +
+ +
+ Transaction Velocity: + {nodeIntelligence.velocity} +
+ +
+ Mule Suspected: + + {nodeIntelligence.muleRiskLevel} + +
+
-
-
NETWORK INSIGHTS
-
- Dynamic node link analysis fetched in real-time from the Graph database layer. - Identifies structural typologies. -
+ + {/* Network Insights Card */} +
+
+ ⚛️ +
+
Network Insights
+
+
+ +

+ {networkInsights} +

diff --git a/services/dashboard/src/pages/Profile.tsx b/services/dashboard/src/pages/Profile.tsx new file mode 100644 index 0000000..e95b4e2 --- /dev/null +++ b/services/dashboard/src/pages/Profile.tsx @@ -0,0 +1,10 @@ +function Profile() { + return ( +
+

Profile Page

+

Welcome to the profile component.

+
+ ); +} + +export default Profile; \ No newline at end of file diff --git a/services/dashboard/src/pages/Reports.tsx b/services/dashboard/src/pages/Reports.tsx index cc8c850..ea606d6 100644 --- a/services/dashboard/src/pages/Reports.tsx +++ b/services/dashboard/src/pages/Reports.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { FileText } from "lucide-react"; +import { FileText, Terminal, Search, DownloadCloud } from "lucide-react"; export default function Reports() { const [strCaseId, setStrCaseId] = useState(""); @@ -16,45 +16,68 @@ export default function Reports() { }; return ( -
-

- - Reports & Export -

+
+
+

+ + Reports & Export +

+

Compliance export utility and automated generation engine for localized banking documentation.

+
+
-
+

Generate STR (XML)

Select a finalized case to generate FIU-IND compliant XML output.

- setStrCaseId(e.target.value)} - className="w-full bg-dark-bg border border-gray-800 rounded p-3 text-gray-200 outline-none focus:border-safe" - /> + +
+ +
+ + setStrCaseId(e.target.value)} + className="w-full bg-gray-900/40 border border-gray-800 rounded-lg p-3 pl-10 text-gray-200 outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/20 transition-all duration-200" + /> +
+
+
-
+ +

Explainability PDFs

Download localized human-readable LLM reports for investigations.

- setPdfCaseId(e.target.value)} - className="w-full bg-dark-bg border border-gray-800 rounded p-3 text-gray-200 outline-none focus:border-safe" - /> + +
+ +
+ + setPdfCaseId(e.target.value)} + className="w-full bg-gray-900/40 border border-gray-800 rounded-lg p-3 pl-10 text-gray-200 outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/20 transition-all duration-200" + /> +
+
+