diff --git a/Backend/index.js b/Backend/index.js index 11ba7d9..a255b68 100644 --- a/Backend/index.js +++ b/Backend/index.js @@ -91,7 +91,7 @@ app.post('/api/transaction/add', async (req, res) => { const { description, amount } = req.body; // Call your AI prediction endpoint - const predictRes = await axios.post('http://192.168.29.129:5000/api/predict', { + const predictRes = await axios.post('http://192.168.1.9:5000/api/predict', { description, }); diff --git a/Frontend/monarch/lib/add.dart b/Frontend/monarch/lib/add.dart new file mode 100644 index 0000000..b44088a --- /dev/null +++ b/Frontend/monarch/lib/add.dart @@ -0,0 +1,608 @@ +// ignore_for_file: use_build_context_synchronously, deprecated_member_use + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:http/http.dart' as http; +import 'package:monarch/static.dart'; + +class AddExpenseScreen extends StatefulWidget { + const AddExpenseScreen({super.key}); + + @override + State createState() => _AddExpenseScreenState(); +} + +class _AddExpenseScreenState extends State + with TickerProviderStateMixin { + // Modern color palette inspired by the image + final Color backgroundColor = const Color(0xFFF8F9FA); // Light cream/white + final Color primaryColor = const Color(0xFF2D3436); // Deep charcoal + final Color accentColor = const Color(0xFF00B894); // Emerald green + final Color cardColor = const Color(0xFFFFFFFF); // Pure white + final Color textSecondary = const Color(0xFF636E72); // Muted gray + final Color surfaceColor = const Color(0xFFF1F2F6); // Light surface + + final TextEditingController descriptionController = TextEditingController(); + final TextEditingController amountController = TextEditingController(); + final FocusNode _nameFocus = FocusNode(); + final FocusNode _amountFocus = FocusNode(); + + late AnimationController _slideController; + late AnimationController _fadeController; + late Animation _slideAnimation; + late Animation _fadeAnimation; + + bool _isNameFocused = false; + String _displayAmount = ''; + + @override + void initState() { + super.initState(); + + _slideController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + + _fadeController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + _slideAnimation = Tween( + begin: const Offset(0, 1), + end: Offset.zero, + ).animate( + CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic), + ); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation(parent: _fadeController, curve: Curves.easeOut)); + + _nameFocus.addListener(() { + setState(() { + _isNameFocused = _nameFocus.hasFocus; + }); + }); + + _amountFocus.addListener(() { + setState(() {}); + }); + + // Start animations + _slideController.forward(); + _fadeController.forward(); + } + + @override + void dispose() { + _slideController.dispose(); + _fadeController.dispose(); + descriptionController.dispose(); + amountController.dispose(); + _nameFocus.dispose(); + _amountFocus.dispose(); + super.dispose(); + } + + void _onAmountChanged(String value) { + if (value.isEmpty) { + setState(() { + _displayAmount = ''; + }); + return; + } + + // Remove any non-digit characters except decimal point + String cleanValue = value.replaceAll(RegExp(r'[^\d.]'), ''); + + // Ensure only one decimal point + List parts = cleanValue.split('.'); + if (parts.length > 2) { + cleanValue = '${parts[0]}.${parts.sublist(1).join('')}'; + } + + if (cleanValue.isNotEmpty) { + double? amount = double.tryParse(cleanValue); + if (amount != null) { + setState(() { + _displayAmount = '\$${amount.toStringAsFixed(2)}'; + }); + } + } + } + + Widget _buildKeypadButton( + String text, { + bool isSpecial = false, + double buttonHeight = 60, + }) { + return GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + if (text == '⌫') { + if (amountController.text.isNotEmpty) { + amountController.text = amountController.text.substring( + 0, + amountController.text.length - 1, + ); + _onAmountChanged(amountController.text); + } + } else if (text == '.') { + if (!amountController.text.contains('.')) { + amountController.text += text; + _onAmountChanged(amountController.text); + } + } else { + amountController.text += text; + _onAmountChanged(amountController.text); + } + }, + child: Container( + height: buttonHeight, + decoration: BoxDecoration( + color: isSpecial ? surfaceColor : cardColor, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSpecial ? textSecondary.withOpacity(0.2) : surfaceColor, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.08), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Center( + child: Text( + text, + style: GoogleFonts.inter( + fontSize: 24, + fontWeight: FontWeight.w600, + color: isSpecial ? textSecondary : primaryColor, + ), + ), + ), + ), + ); + } + + Future addTransaction() async { + // Validation first + if (descriptionController.text.trim().isEmpty || + amountController.text.trim().isEmpty) { + _showCustomSnackBar( + message: 'Please fill in both name and amount', + icon: Icons.warning_rounded, + backgroundColor: const Color(0xFFFF6B6B), + iconColor: Colors.white, + ); + return; + } + + // Show beautiful loading indicator + showDialog( + context: context, + barrierDismissible: false, + builder: + (_) => Center( + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(accentColor), + strokeWidth: 3, + ), + const SizedBox(height: 16), + Text( + 'Processing transaction...', + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w500, + color: primaryColor, + ), + ), + ], + ), + ), + ), + ); + + try { + final response = await http.post( + Uri.parse('http://192.168.1.9:3000/api/transaction/add'), + headers: {'Content-Type': 'application/json'}, + body: json.encode({ + 'description': descriptionController.text, + 'amount': double.tryParse(amountController.text), + }), + ); + + Navigator.of(context).pop(); // Close loading dialog + + if (response.statusCode == 200) { + final responseData = json.decode(response.body); + final predictedCategory = responseData['data']['category']; + + // Clear the input fields + descriptionController.clear(); + amountController.clear(); + setState(() { + _displayAmount = ''; + }); + + // Show success message + _showCustomSnackBar( + message: + 'Transaction added successfully!\nCategorized as: $predictedCategory', + icon: Icons.check_circle_rounded, + backgroundColor: accentColor, + iconColor: Colors.white, + duration: const Duration(seconds: 2), + ); + + // Navigate back to statistics page after a short delay + Future.delayed(const Duration(milliseconds: 1500), () { + if (mounted) { + Navigator.of(context).pop(); // Go back to previous screen + } + }); + } else { + _showCustomSnackBar( + message: 'Failed to add transaction\nPlease try again', + icon: Icons.error_rounded, + backgroundColor: const Color(0xFFFF6B6B), + iconColor: Colors.white, + ); + } + } catch (e) { + Navigator.of(context).pop(); // Close loading dialog + _showCustomSnackBar( + message: 'Network error occurred\nPlease check your connection', + icon: Icons.wifi_off_rounded, + backgroundColor: const Color(0xFFFF9F43), + iconColor: Colors.white, + ); + } + } + + void _showCustomSnackBar({ + required String message, + required IconData icon, + required Color backgroundColor, + required Color iconColor, + Duration duration = const Duration(seconds: 3), + }) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Container( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: iconColor, size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + message, + style: GoogleFonts.inter( + fontWeight: FontWeight.w500, + color: Colors.white, + fontSize: 14, + height: 1.4, + ), + ), + ), + ], + ), + ), + backgroundColor: backgroundColor, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + margin: const EdgeInsets.all(16), + duration: duration, + elevation: 8, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: primaryColor, + body: SafeArea( + child: SlideTransition( + position: _slideAnimation, + child: FadeTransition( + opacity: _fadeAnimation, + child: Column( + children: [ + // Header + Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + GestureDetector( + onTap: + () => Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => Statistics(), + ), + ), + child: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.8), + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: Colors.white.withOpacity(0.1), + width: 1, + ), + ), + child: const Icon( + Icons.arrow_back_ios_new, + color: Colors.white, + size: 20, + ), + ), + ), + const SizedBox(width: 16), + Text( + 'Add Expense', + style: GoogleFonts.inter( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + + // Main Content Card + Expanded( + child: Container( + margin: const EdgeInsets.only(top: 8), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Drag indicator + Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: textSecondary.withOpacity(0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + const SizedBox(height: 32), + + // Expense Name Section + Text( + 'Expense Name', + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w600, + color: primaryColor, + ), + ), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + color: _isNameFocused ? cardColor : surfaceColor, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: + _isNameFocused + ? accentColor + : Colors.transparent, + width: 2, + ), + boxShadow: + _isNameFocused + ? [ + BoxShadow( + color: accentColor.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 4), + ), + ] + : [ + BoxShadow( + color: primaryColor.withOpacity(0.06), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: TextField( + controller: descriptionController, + focusNode: _nameFocus, + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w500, + color: primaryColor, + ), + decoration: InputDecoration( + hintText: 'Enter expense name', + hintStyle: GoogleFonts.inter( + color: textSecondary, + fontWeight: FontWeight.w400, + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.all(20), + ), + ), + ), + + const SizedBox(height: 40), + + // Amount Display + Center( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 16, + ), + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.08), + blurRadius: 16, + offset: const Offset(0, 4), + ), + ], + ), + child: Text( + _displayAmount.isEmpty + ? '\$0.00' + : _displayAmount, + style: GoogleFonts.inter( + fontSize: 36, + fontWeight: FontWeight.w700, + color: primaryColor, + ), + ), + ), + ), + + const SizedBox(height: 20), + + // Custom Keypad + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + double availableHeight = constraints.maxHeight; + double buttonHeight = + (availableHeight - 32) / + 4; // 4 rows with spacing + buttonHeight = buttonHeight.clamp( + 50.0, + 70.0, + ); // Min 50, Max 70 + + return GridView.builder( + physics: const NeverScrollableScrollPhysics(), + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: + constraints.maxWidth / + (buttonHeight * 3 + 32), + crossAxisSpacing: 10, + mainAxisSpacing: 10, + ), + itemCount: 12, + itemBuilder: (context, index) { + List keys = [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '.', + '0', + '⌫', + ]; + return _buildKeypadButton( + keys[index], + isSpecial: + keys[index] == '.' || + keys[index] == '⌫', + buttonHeight: buttonHeight, + ); + }, + ); + }, + ), + ), + + const SizedBox(height: 24), + + // Add Expense Button + GestureDetector( + onTap: addTransaction, + child: Container( + width: double.infinity, + height: 60, + decoration: BoxDecoration( + color: accentColor, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: accentColor.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Center( + child: Text( + 'Add Expense', + style: GoogleFonts.inter( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/Frontend/monarch/lib/homepage.dart b/Frontend/monarch/lib/homepage.dart new file mode 100644 index 0000000..ca7617f --- /dev/null +++ b/Frontend/monarch/lib/homepage.dart @@ -0,0 +1,696 @@ +// ignore_for_file: deprecated_member_use, unused_local_variable, use_super_parameters + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:monarch/navbar.dart'; +//import 'package:monarch/static.dart'; + +class FinTrackHomePage extends StatefulWidget { + const FinTrackHomePage({Key? key}) : super(key: key); + + @override + State createState() => _FinTrackHomePageState(); +} + +class _FinTrackHomePageState extends State + with TickerProviderStateMixin { + // Modern color palette + final Color backgroundColor = const Color(0xFFF8F9FA); // Light cream/white + final Color primaryColor = const Color(0xFF2D3436); // Deep charcoal + final Color accentColor = const Color(0xFF00B894); // Emerald green + final Color cardColor = const Color(0xFFFFFFFF); // Pure white + final Color textSecondary = const Color(0xFF636E72); // Muted gray + final Color surfaceColor = const Color(0xFFF1F2F6); // Light surface + + late AnimationController _fadeController; + late AnimationController _slideController; + late Animation _fadeAnimation; + late Animation _slideAnimation; + + // Sample data for dashboard + final double totalBalance = 5847.32; + final double monthlyIncome = 8500.00; + final double monthlyExpenses = 2652.68; + final List> recentTransactions = [ + { + 'title': 'Grocery Shopping', + 'category': 'Food & Dining', + 'amount': -128.50, + 'date': 'Today', + 'icon': Icons.shopping_cart, + 'color': Colors.orange, + }, + { + 'title': 'Salary Credit', + 'category': 'Income', + 'amount': 8500.00, + 'date': 'Yesterday', + 'icon': Icons.account_balance_wallet, + 'color': Colors.green, + }, + { + 'title': 'Netflix Subscription', + 'category': 'Entertainment', + 'amount': -15.99, + 'date': '2 days ago', + 'icon': Icons.play_circle, + 'color': Colors.red, + }, + { + 'title': 'Coffee Shop', + 'category': 'Food & Dining', + 'amount': -4.75, + 'date': '3 days ago', + 'icon': Icons.local_cafe, + 'color': Colors.brown, + }, + ]; + + final List> categories = [ + { + 'name': 'Food & Dining', + 'amount': 450.32, + 'percentage': 28, + 'color': Colors.orange, + }, + { + 'name': 'Transportation', + 'amount': 320.15, + 'percentage': 20, + 'color': Colors.blue, + }, + { + 'name': 'Entertainment', + 'amount': 180.50, + 'percentage': 11, + 'color': Colors.purple, + }, + { + 'name': 'Shopping', + 'amount': 275.80, + 'percentage': 17, + 'color': Colors.pink, + }, + ]; + + @override + void initState() { + super.initState(); + + _fadeController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + ); + + _slideController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation(parent: _fadeController, curve: Curves.easeOut)); + + _slideAnimation = Tween( + begin: const Offset(0, 0.3), + end: Offset.zero, + ).animate( + CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic), + ); + + // Start animations + _fadeController.forward(); + _slideController.forward(); + } + + @override + void dispose() { + _fadeController.dispose(); + _slideController.dispose(); + super.dispose(); + } + + Widget _buildBalanceCard() { + return Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [accentColor, accentColor.withOpacity(0.8)], + ), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: accentColor.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Total Balance', + style: GoogleFonts.inter( + color: Colors.white.withOpacity(0.9), + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + Icon( + Icons.visibility_outlined, + color: Colors.white.withOpacity(0.9), + size: 20, + ), + ], + ), + const SizedBox(height: 8), + Text( + '\$${totalBalance.toStringAsFixed(2)}', + style: GoogleFonts.inter( + color: Colors.white, + fontSize: 36, + fontWeight: FontWeight.w700, + letterSpacing: -1, + ), + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Income', + style: GoogleFonts.inter( + color: Colors.white.withOpacity(0.8), + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 4), + Text( + '\$${monthlyIncome.toStringAsFixed(0)}', + style: GoogleFonts.inter( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + Container( + width: 1, + height: 40, + color: Colors.white.withOpacity(0.3), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Expenses', + style: GoogleFonts.inter( + color: Colors.white.withOpacity(0.8), + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 4), + Text( + '\$${monthlyExpenses.toStringAsFixed(0)}', + style: GoogleFonts.inter( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildQuickActions() { + return Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + // Navigate to Add Expense + }, + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.08), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: accentColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(Icons.add, color: accentColor, size: 24), + ), + const SizedBox(height: 12), + Text( + 'Add Expense', + style: GoogleFonts.inter( + color: primaryColor, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + // Navigate to Statistics + }, + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.08), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.bar_chart, + color: Colors.blue, + size: 24, + ), + ), + const SizedBox(height: 12), + Text( + 'Statistics', + style: GoogleFonts.inter( + color: primaryColor, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + ], + ); + } + + Widget _buildRecentTransactions() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.08), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Recent Transactions', + style: GoogleFonts.inter( + color: primaryColor, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + Text( + 'View All', + style: GoogleFonts.inter( + color: accentColor, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 16), + ...recentTransactions.map( + (transaction) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: transaction['color'].withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + transaction['icon'], + color: transaction['color'], + size: 20, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + transaction['title'], + style: GoogleFonts.inter( + color: primaryColor, + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + '${transaction['category']} • ${transaction['date']}', + style: GoogleFonts.inter( + color: textSecondary, + fontSize: 13, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + Text( + '${transaction['amount'] > 0 ? '+' : ''}\$${transaction['amount'].abs().toStringAsFixed(2)}', + style: GoogleFonts.inter( + color: + transaction['amount'] > 0 ? accentColor : Colors.red, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildSpendingCategories() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.08), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Spending by Category', + style: GoogleFonts.inter( + color: primaryColor, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 20), + ...categories.map( + (category) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: category['color'], + borderRadius: BorderRadius.circular(6), + ), + ), + const SizedBox(width: 12), + Text( + category['name'], + style: GoogleFonts.inter( + color: primaryColor, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + Text( + '\$${category['amount'].toStringAsFixed(2)}', + style: GoogleFonts.inter( + color: primaryColor, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 8), + LinearProgressIndicator( + value: category['percentage'] / 100, + backgroundColor: surfaceColor, + valueColor: AlwaysStoppedAnimation( + category['color'], + ), + borderRadius: BorderRadius.circular(4), + ), + ], + ), + ), + ), + ], + ), + ); + } + + int _selectedIndex = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: backgroundColor, + body: SafeArea( + child: FadeTransition( + opacity: _fadeAnimation, + child: SlideTransition( + position: _slideAnimation, + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Good Morning! 👋', + style: GoogleFonts.inter( + color: textSecondary, + fontSize: 16, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 4), + Text( + 'Alex Johnson', + style: GoogleFonts.inter( + color: primaryColor, + fontSize: 24, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: accentColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + ), + child: Icon( + Icons.notifications_outlined, + color: accentColor, + size: 24, + ), + ), + ], + ), + + const SizedBox(height: 32), + + // Balance Card + _buildBalanceCard(), + + const SizedBox(height: 24), + + // Quick Actions + _buildQuickActions(), + + const SizedBox(height: 32), + + // AI Insight Card + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.purple.withOpacity(0.1), + Colors.blue.withOpacity(0.1), + ], + ), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.purple.withOpacity(0.2), + width: 1, + ), + ), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: Colors.purple.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.psychology, + color: Colors.purple, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Smart Insight', + style: GoogleFonts.inter( + color: primaryColor, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + 'Your food expenses increased by 15% this month. Consider meal planning to save more!', + style: GoogleFonts.inter( + color: textSecondary, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 32), + + // Recent Transactions + _buildRecentTransactions(), + + const SizedBox(height: 24), + + // Spending Categories + _buildSpendingCategories(), + + const SizedBox(height: 20), + ], + ), + ), + ), + ), + ), + + floatingActionButton: CustomNavBar( + currentIndex: _selectedIndex, + onTap: (index) { + setState(() => _selectedIndex = index); + }, + backgroundColor: backgroundColor, + accentColor: accentColor, + primaryColor: primaryColor, + cardColor: cardColor, + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + ); + } +} diff --git a/Frontend/monarch/lib/main.dart b/Frontend/monarch/lib/main.dart index 45abfbc..cae22ee 100644 --- a/Frontend/monarch/lib/main.dart +++ b/Frontend/monarch/lib/main.dart @@ -4,8 +4,9 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import 'package:monarch/homepage.dart'; +//import 'package:monarch/homepage.dart'; import 'package:monarch/static.dart'; -import 'package:monarch/transaction.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); // This fixes the issue @@ -20,7 +21,7 @@ class MyApp extends StatelessWidget { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData(primarySwatch: Colors.blue), - home: const Statistics(), + home: const FinTrackHomePage(), ); } } @@ -45,7 +46,7 @@ class _AddTransactionPageState extends State { try { final response = await http.post( - Uri.parse('http://192.168.1.5:3000/api/transaction/add'), + Uri.parse('http://192.168.1.9:3000/api/transaction/add'), headers: {'Content-Type': 'application/json'}, body: json.encode({ 'description': descriptionController.text, @@ -78,7 +79,10 @@ class _AddTransactionPageState extends State { ); } } catch (e) { - Navigator.of(context).pop(); // Close loading dialog + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const Statistics()), + ); ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('⚠️ Error: $e'))); @@ -108,9 +112,7 @@ class _AddTransactionPageState extends State { onPressed: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => const TransactionsPage(), - ), + MaterialPageRoute(builder: (context) => const Statistics()), ); }, child: const Text('NextPage'), diff --git a/Frontend/monarch/lib/navbar.dart b/Frontend/monarch/lib/navbar.dart new file mode 100644 index 0000000..3adf1a6 --- /dev/null +++ b/Frontend/monarch/lib/navbar.dart @@ -0,0 +1,149 @@ +// ignore_for_file: deprecated_member_use + +import 'package:flutter/material.dart'; +import 'package:monarch/add.dart'; +import 'package:monarch/homepage.dart'; +import 'package:monarch/static.dart'; + +class CustomNavBar extends StatelessWidget { + final int currentIndex; + final void Function(int) onTap; + final Color backgroundColor; + final Color accentColor; + final Color primaryColor; + final Color cardColor; + + const CustomNavBar({ + super.key, + required this.currentIndex, + required this.onTap, + required this.backgroundColor, + required this.accentColor, + required this.primaryColor, + required this.cardColor, + }); + + // Define navigation items configuration + final List _navItems = const [ + NavItem( + icon: Icons.home_rounded, + index: 0, + page: FinTrackHomePage(), + ), + NavItem( + icon: Icons.bar_chart_rounded, + index: 1, + page: Statistics(), + ), + NavItem( + icon: Icons.settings_rounded, + index: 2, + page: AddExpenseScreen(), + ), + ]; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + height: 72, + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(36), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.1), + blurRadius: 30, + offset: const Offset(0, 8), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ..._navItems.map((item) => _buildNavItem(context, item)), + _buildAddButton(context), + ], + ), + ); + } + + Widget _buildNavItem(BuildContext context, NavItem item) { + final isActive = currentIndex == item.index; + + return Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(20), + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () { + onTap(item.index); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => item.page), + ); + }, + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: isActive ? primaryColor.withOpacity(0.1) : Colors.transparent, + borderRadius: BorderRadius.circular(20), + ), + child: Icon( + item.icon, + color: isActive ? primaryColor : primaryColor.withOpacity(0.4), + size: 24, + ), + ), + ), + ); + } + + Widget _buildAddButton(BuildContext context) { + return Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: accentColor, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: accentColor.withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(20), + child: InkWell( + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const AddExpenseScreen()), + ), + borderRadius: BorderRadius.circular(20), + child: const Icon( + Icons.add_rounded, + color: Colors.white, + size: 24, + ), + ), + ), + ); + } +} + +// Navigation item model +class NavItem { + final IconData icon; + final int index; + final Widget page; + + const NavItem({ + required this.icon, + required this.index, + required this.page, + }); +} \ No newline at end of file diff --git a/Frontend/monarch/lib/static.dart b/Frontend/monarch/lib/static.dart index a6f1765..a4a985a 100644 --- a/Frontend/monarch/lib/static.dart +++ b/Frontend/monarch/lib/static.dart @@ -1,4 +1,4 @@ -// ignore_for_file: sort_child_properties_last, use_build_context_synchronously, deprecated_member_use +// ignore_for_file: sort_child_properties_last, use_build_context_synchronously, deprecated_member_use, unnecessary_import, unused_local_variable, unused_import, sized_box_for_whitespace import 'dart:convert'; import 'dart:ui'; @@ -6,6 +6,9 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:http/http.dart' as http; +import 'package:monarch/add.dart'; +import 'package:monarch/homepage.dart'; +import 'package:monarch/navbar.dart'; class Transaction { final String description; @@ -37,9 +40,12 @@ class Statistics extends StatefulWidget { State createState() => StatisticsState(); } -class StatisticsState extends State { +class StatisticsState extends State with TickerProviderStateMixin { List transactions = []; bool isLoading = false; + late AnimationController _animationController; + late Animation _fadeAnimation; + late Animation _slideAnimation; final List categories = [ 'All', @@ -56,14 +62,39 @@ class StatisticsState extends State { Map totalAmountPerCategory = {}; - final Color backgroundColor = Color(0xFF121212); // Deep black - final Color accentColor = Color(0xFF00FF88); // Modern green - final Color cardColor = Color(0xFF1F1F1F); // Darker gray + // Modern color palette inspired by the image + final Color backgroundColor = Color(0xFFF8F9FA); // Light cream/white + final Color primaryColor = Color(0xFF2D3436); // Deep charcoal + final Color accentColor = Color(0xFF00B894); // Emerald green + final Color cardColor = Color(0xFFFFFFFF); // Pure white + final Color textSecondary = Color(0xFF636E72); // Muted gray + final Color surfaceColor = Color(0xFFF1F2F6); // Light surface @override void initState() { super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + ); + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeOut), + ); + _slideAnimation = Tween( + begin: const Offset(0, 0.3), + end: Offset.zero, + ).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeOut), + ); + fetchTransactions(); + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); } //API Call for Fetch @@ -73,7 +104,7 @@ class StatisticsState extends State { }); try { - final uri = Uri.parse('http://192.168.1.5:3000/api/transactions').replace( + final uri = Uri.parse('http://192.168.1.9:3000/api/transactions').replace( queryParameters: category == 'All' ? null : {'category': category}, ); @@ -98,9 +129,13 @@ class StatisticsState extends State { throw Exception('Failed to load transactions'); } } catch (e) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error loading transactions: $e'))); + _showCustomSnackBar( + message: 'Error fetching transactions', + icon: Icons.data_exploration_rounded, + backgroundColor: const Color(0xFFFF9F43), + iconColor: Colors.white, + duration: const Duration(seconds: 4), + ); } finally { setState(() { isLoading = false; @@ -108,73 +143,320 @@ class StatisticsState extends State { } } - Widget buildCategoryChips() { - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: Row( - children: - categories.map((cat) { - final bool isSelected = cat == selectedCategory; - return Padding( - padding: const EdgeInsets.only(right: 8), - child: ChoiceChip( - label: Text(cat), - selected: isSelected, - selectedColor: backgroundColor, - backgroundColor: accentColor, - labelStyle: GoogleFonts.poppins( - color: isSelected ? accentColor : cardColor, - ), - onSelected: (selected) { - if (selected) { - setState(() { - selectedCategory = cat; - }); - fetchTransactions(category: cat); - } - }, + void _showCustomSnackBar({ + required String message, + required IconData icon, + required Color backgroundColor, + required Color iconColor, + Duration duration = const Duration(seconds: 3), + }) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Container( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), ), - ); - }).toList(), + child: Icon(icon, color: iconColor, size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + message, + style: GoogleFonts.inter( + fontWeight: FontWeight.w500, + color: Colors.white, + fontSize: 14, + height: 1.4, + ), + ), + ), + ], + ), + ), + backgroundColor: backgroundColor, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + margin: const EdgeInsets.all(16), + duration: duration, + elevation: 8, ), ); } - Widget buildTransactionCard(Transaction tx) { + Widget _buildHeader() { return Container( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: cardColor.withOpacity(0.8), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.4), - blurRadius: 10, - offset: Offset(0, 6), + padding: const EdgeInsets.fromLTRB(24, 60, 24, 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(22), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.08), + blurRadius: 20, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(22), + child: InkWell( + borderRadius: BorderRadius.circular(22), + onTap: () => Navigator.pop(context), + child: Icon( + Icons.arrow_back_ios_rounded, + color: primaryColor, + size: 20, + ), + ), + ), + ), + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(22), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.08), + blurRadius: 20, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(22), + child: InkWell( + borderRadius: BorderRadius.circular(22), + onTap: () {}, + child: Icon( + Icons.more_horiz_rounded, + color: primaryColor, + size: 24, + ), + ), + ), ), ], ), - child: ListTile( - title: Text( - tx.description, - style: GoogleFonts.poppins( - color: Colors.white, - fontWeight: FontWeight.bold, + ); + } + + Widget _buildTotalSpentCard() { + final totalSpent = transactions.fold( + 0, + (sum, tx) => sum + tx.amount, + ); + final budget = 10000.0; // You can make this dynamic + final progress = totalSpent / budget; + + return SlideTransition( + position: _slideAnimation, + child: FadeTransition( + opacity: _fadeAnimation, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 24), + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(28), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.06), + blurRadius: 30, + offset: const Offset(0, 8), + spreadRadius: 0, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'TOTAL SPENT', + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w500, + color: textSecondary, + letterSpacing: 1.2, + ), + ), + const SizedBox(height: 12), + Text( + '₹${totalSpent.toStringAsFixed(1)}', + style: GoogleFonts.inter( + fontSize: 42, + fontWeight: FontWeight.w700, + color: primaryColor, + height: 1.1, + ), + ), + const SizedBox(height: 32), + // Modern Circular Progress Ring + Center( + child: Container( + width: 160, + height: 160, + child: Stack( + alignment: Alignment.center, + children: [ + // Background circle + Container( + width: 160, + height: 160, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: surfaceColor, width: 8), + ), + ), + // Progress circle + SizedBox( + width: 160, + height: 160, + child: CircularProgressIndicator( + value: progress > 1 ? 1 : progress, + strokeWidth: 8, + backgroundColor: Colors.transparent, + valueColor: AlwaysStoppedAnimation( + progress > 0.8 + ? Color(0xFFE74C3C) + : progress > 0.6 + ? Color(0xFFF39C12) + : accentColor, + ), + strokeCap: StrokeCap.round, + ), + ), + // Center content + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${(progress * 100).toInt()}%', + style: GoogleFonts.inter( + fontSize: 24, + fontWeight: FontWeight.w700, + color: primaryColor, + ), + ), + const SizedBox(height: 4), + Text( + 'of ₹${budget.toInt()}', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w500, + color: textSecondary, + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 24), + // Modern category breakdown + if (totalAmountPerCategory.isNotEmpty) _buildCategoryBreakdown(), + ], ), ), - subtitle: Text( - '${tx.category} • ${tx.date.toLocal().toString().split(' ')[0]}', - style: GoogleFonts.poppins(color: Colors.white60, fontSize: 12), - ), - trailing: Text( - '₹${tx.amount.toStringAsFixed(2)}', - style: GoogleFonts.poppins( - color: accentColor, - fontWeight: FontWeight.bold, - fontSize: 16, + ), + ); + } + + Widget _buildCategoryBreakdown() { + final sortedCategories = + totalAmountPerCategory.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + final topCategories = sortedCategories.take(3).toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'TOP CATEGORIES', + style: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w600, + color: textSecondary, + letterSpacing: 1, ), ), + const SizedBox(height: 16), + ...topCategories.map( + (entry) => _buildCategoryRow(entry.key, entry.value), + ), + ], + ); + } + + Widget _buildCategoryRow(String category, double amount) { + final total = transactions.fold(0, (sum, tx) => sum + tx.amount); + final percentage = total > 0 ? (amount / total) : 0.0; + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: _getCategoryColor(category), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + category, + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w500, + color: primaryColor, + ), + ), + ), + Text( + '${(percentage * 100).toInt()}%', + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + color: textSecondary, + ), + ), + ], + ), + ); + } + + Widget _buildToggleButtons() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 24), + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: surfaceColor, + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + Expanded(child: _buildToggleButton('Monthly')), + Expanded(child: _buildToggleButton('Yearly')), + ], ), ); } @@ -186,17 +468,19 @@ class StatisticsState extends State { onTap: () => setState(() => _selectedPlan = label), child: AnimatedContainer( duration: const Duration(milliseconds: 300), - padding: const EdgeInsets.symmetric(vertical: 14), + curve: Curves.easeInOut, + padding: const EdgeInsets.symmetric(vertical: 16), decoration: BoxDecoration( - color: isSelected ? accentColor : cardColor, - borderRadius: BorderRadius.circular(18), + color: isSelected ? cardColor : Colors.transparent, + borderRadius: BorderRadius.circular(12), boxShadow: isSelected ? [ BoxShadow( - color: accentColor.withOpacity(0.4), - blurRadius: 12, - spreadRadius: 1, + color: primaryColor.withOpacity(0.08), + blurRadius: 10, + spreadRadius: 0, + offset: const Offset(0, 2), ), ] : [], @@ -204,298 +488,225 @@ class StatisticsState extends State { alignment: Alignment.center, child: Text( label, - style: GoogleFonts.poppins( - color: isSelected ? backgroundColor : Colors.white70, + style: GoogleFonts.inter( + color: isSelected ? primaryColor : textSecondary, + fontWeight: FontWeight.w600, + fontSize: 15, + ), + ), + ), + ); + } + + Widget buildCategoryChips() { + return Container( + height: 50, + margin: const EdgeInsets.only(bottom: 16), + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 20), + itemCount: categories.length, + itemBuilder: (context, index) { + final cat = categories[index]; + final bool isSelected = cat == selectedCategory; + + return Container( + margin: const EdgeInsets.only(right: 12), + child: FilterChip( + label: Text(cat), + selected: isSelected, + onSelected: (selected) { + if (selected) { + setState(() { + selectedCategory = cat; + }); + fetchTransactions(category: cat); + } + }, + backgroundColor: surfaceColor, + selectedColor: primaryColor, + checkmarkColor: cardColor, + labelStyle: GoogleFonts.inter( + color: isSelected ? cardColor : textSecondary, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + side: BorderSide.none, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + ); + }, + ), + ); + } + + Widget buildTransactionCard(Transaction tx) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 6), + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.04), + blurRadius: 20, + offset: const Offset(0, 4), + ), + ], + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + leading: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: _getCategoryColor(tx.category), + borderRadius: BorderRadius.circular(14), + ), + child: Icon( + _getCategoryIcon(tx.category), + color: cardColor, + size: 24, + ), + ), + title: Text( + tx.description, + style: GoogleFonts.inter( + color: primaryColor, fontWeight: FontWeight.w600, fontSize: 16, ), ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + '${tx.category.toUpperCase()} • ${_formatTime(tx.date)}', + style: GoogleFonts.inter( + color: textSecondary, + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ), + trailing: Text( + '₹${tx.amount.toStringAsFixed(1)}', + style: GoogleFonts.inter( + color: primaryColor, + fontWeight: FontWeight.w700, + fontSize: 17, + ), + ), ), ); } + Color _getCategoryColor(String category) { + final colors = [ + Color(0xFF6C5CE7), + Color(0xFFE17055), + Color(0xFF00B894), + Color(0xFF0984E3), + Color(0xFFE84393), + Color(0xFFFD79A8), + Color(0xFF6C5CE7), + ]; + return colors[categories.indexOf(category) % colors.length]; + } + + IconData _getCategoryIcon(String category) { + switch (category) { + case 'Food': + return Icons.restaurant_rounded; + case 'Shopping': + return Icons.shopping_bag_rounded; + case 'Bills': + return Icons.receipt_rounded; + case 'Travel': + return Icons.flight_rounded; + case 'Entertainment': + return Icons.movie_rounded; + default: + return Icons.category_rounded; + } + } + + String _formatTime(DateTime date) { + final now = DateTime.now(); + final difference = now.difference(date); + + if (difference.inDays == 0) { + return '${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; + } else { + return '${date.day.toString().padLeft(2, '0')}-${date.month.toString().padLeft(2, '0')}'; + } + } + int _selectedIndex = 0; - Widget buildBarChart() { - final barData = totalAmountPerCategory.entries.toList(); - - if (barData.isEmpty) { - return Center( - child: Text( - 'No data to display', - style: GoogleFonts.poppins(color: Colors.white70), - ), - ); - } - - double maxY = barData.map((e) => e.value).reduce((a, b) => a > b ? a : b); - - return AspectRatio( - aspectRatio: 1.7, - child: BarChart( - BarChartData( - backgroundColor: backgroundColor, - alignment: BarChartAlignment.spaceAround, - maxY: maxY + 20, - borderData: FlBorderData(show: false), - titlesData: FlTitlesData( - leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), - - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - getTitlesWidget: (value, _) { - if (value.toInt() < barData.length) { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - barData[value.toInt()].key, - style: GoogleFonts.poppins( - color: Colors.white, - fontSize: 12, - ), - ), - ); - } else { - return const SizedBox.shrink(); - } - }, - reservedSize: 30, - ), - ), - rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), - topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), - ), - barGroups: List.generate(barData.length, (i) { - return BarChartGroupData( - x: i, - barRods: [ - BarChartRodData( - toY: barData[i].value, - gradient: LinearGradient( - colors: [accentColor, accentColor.withOpacity(0.6)], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - ), - width: 20, - borderRadius: BorderRadius.circular(30), - ), - ], - ); - }), - ), - ), - ); - } - @override Widget build(BuildContext context) { return Scaffold( backgroundColor: backgroundColor, - appBar: AppBar( - backgroundColor: backgroundColor, - elevation: 0, - title: Text( - 'Statistics', - style: GoogleFonts.poppins(color: Colors.white), - ), - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: () => Navigator.pop(context), - ), - actions: [ - IconButton( - icon: const Icon(Icons.more_vert, color: Colors.white), - onPressed: () {}, - ), - ], - ), body: Column( children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - Expanded(child: _buildToggleButton('Monthly')), - const SizedBox(width: 10), - Expanded(child: _buildToggleButton('Yearly')), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), - child: Text( - '₹ ${transactions.fold(0, (sum, tx) => sum + tx.amount).toStringAsFixed(2)}', - style: GoogleFonts.poppins( - fontSize: 32, - fontWeight: FontWeight.bold, - color: accentColor, - ), - ), - ), - Padding(padding: const EdgeInsets.all(16.0), child: buildBarChart()), + _buildHeader(), + _buildTotalSpentCard(), + _buildToggleButtons(), buildCategoryChips(), - const SizedBox(height: 10), Expanded( child: isLoading - ? const Center(child: CircularProgressIndicator()) + ? Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(primaryColor), + strokeWidth: 2, + ), + ) : transactions.isEmpty ? Center( - child: Text( - 'No transactions found.', - style: GoogleFonts.poppins(color: Colors.white70), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.analytics_outlined, + size: 64, + color: textSecondary.withOpacity(0.5), + ), + const SizedBox(height: 16), + Text( + 'No transactions found', + style: GoogleFonts.inter( + color: textSecondary, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], ), ) : ListView.builder( + padding: const EdgeInsets.only(top: 8, bottom: 120), itemCount: transactions.length, itemBuilder: (context, index) => buildTransactionCard(transactions[index]), ), ), - - CustomNavBar( - currentIndex: _selectedIndex, - onTap: (index) { - setState(() => _selectedIndex = index); - }, - backgroundColor: backgroundColor.withOpacity(0.0), - accentColor: accentColor, - ), ], ), - ); - } -} - -class CustomNavBar extends StatelessWidget { - final int currentIndex; - final void Function(int) onTap; - final Color backgroundColor; - final Color accentColor; - - const CustomNavBar({ - super.key, - required this.currentIndex, - required this.onTap, - required this.backgroundColor, - required this.accentColor, - }); - - @override - Widget build(BuildContext context) { - final screenWidth = MediaQuery.of(context).size.width; - - return Align( - alignment: Alignment.bottomLeft, - child: SizedBox( - width: screenWidth, - height: 100, // enough space, but no visible background - child: Stack( - clipBehavior: Clip.none, - children: [ - // Translucent nav bar - Positioned( - left: 20, - bottom: 20, - child: Container( - width: screenWidth * 0.5, - height: 70, - decoration: BoxDecoration( - color: backgroundColor.withOpacity(0.2), - borderRadius: BorderRadius.circular(40), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 20, - spreadRadius: 2, - offset: const Offset(0, 10), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(40), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 12.0, sigmaY: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildNavItem(Icons.home, 0), - _buildNavItem(Icons.pie_chart, 1), - _buildNavItem(Icons.settings, 2), - ], - ), - ), - ), - ), - ), - - // Floating Add Button - Positioned( - right: 20, - bottom: 12, - child: Material( - color: Colors.transparent, - shape: const CircleBorder(), - child: InkWell( - onTap: () => onTap(4), - customBorder: const CircleBorder(), - splashColor: Colors.white.withOpacity(0.2), - child: Container( - width: 58, - height: 58, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: accentColor, - boxShadow: [ - BoxShadow( - color: accentColor.withOpacity(0.6), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ], - ), - child: const Icon(Icons.add, color: Colors.black, size: 30), - ), - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildNavItem(IconData icon, int index) { - final isActive = currentIndex == index; - - return Material( - color: Colors.transparent, - shape: const CircleBorder(), - child: InkWell( - onTap: () => onTap(index), - customBorder: const CircleBorder(), - splashColor: accentColor.withOpacity(0.2), - child: AnimatedContainer( - duration: const Duration(milliseconds: 250), - width: isActive ? 56 : 50, - height: isActive ? 56 : 50, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: isActive ? accentColor.withOpacity(0.1) : Colors.transparent, - border: - isActive - ? Border.all(color: accentColor.withOpacity(0.5), width: 2) - : null, - ), - child: Icon( - icon, - color: isActive ? accentColor : Colors.white70, - size: 24, - ), - ), - ), + floatingActionButton: CustomNavBar( + currentIndex: _selectedIndex, + onTap: (index) { + setState(() => _selectedIndex = index); + }, + backgroundColor: backgroundColor, + accentColor: accentColor, + primaryColor: primaryColor, + cardColor: cardColor, + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, ); } } diff --git a/Frontend/monarch/lib/transaction.dart b/Frontend/monarch/lib/transaction.dart deleted file mode 100644 index e96a53d..0000000 --- a/Frontend/monarch/lib/transaction.dart +++ /dev/null @@ -1,198 +0,0 @@ -// ignore_for_file: library_private_types_in_public_api, use_build_context_synchronously - -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; - -class Transaction { - final String description; - final double amount; - final String category; - final DateTime date; - - Transaction({ - required this.description, - required this.amount, - required this.category, - required this.date, - }); - - factory Transaction.fromJson(Map json) { - return Transaction( - description: json['description'], - amount: (json['amount'] as num).toDouble(), - category: json['category'], - date: DateTime.parse(json['date']), - ); - } -} - -class TransactionsPage extends StatefulWidget { - const TransactionsPage({super.key}); - - @override - _TransactionsPageState createState() => _TransactionsPageState(); -} - -class _TransactionsPageState extends State { - List transactions = []; - bool isLoading = false; - - final List categories = [ - 'All', - 'Food', - 'Shopping', - 'Bills', - 'Travel', - 'Entertainment', - 'Other', - ]; - - String selectedCategory = 'All'; - - @override - void initState() { - super.initState(); - fetchTransactions(); - } - - Future fetchTransactions({String category = 'All'}) async { - setState(() { - isLoading = true; - }); - - try { - final uri = Uri.parse( - 'http://192.168.1.5:3000/api/transactions', - ).replace( - queryParameters: category == 'All' ? null : {'category': category}, - ); - - final response = await http.get(uri); - - if (response.statusCode == 200) { - final data = json.decode(response.body); - final List list = data['data']; - - setState(() { - transactions = - list.map((json) => Transaction.fromJson(json)).toList(); - }); - } else { - throw Exception('Failed to load transactions'); - } - } catch (e) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error loading transactions: $e'))); - } finally { - setState(() { - isLoading = false; - }); - } - } - - Widget buildCategoryChips() { - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: Row( - children: - categories.map((cat) { - final bool isSelected = cat == selectedCategory; - return Padding( - padding: const EdgeInsets.only(right: 8), - child: ChoiceChip( - label: Text(cat), - selected: isSelected, - selectedColor: const Color(0xFF00221F), - backgroundColor: Colors.grey[200], - labelStyle: TextStyle( - color: isSelected ? Colors.white : Colors.black, - ), - onSelected: (selected) { - if (selected) { - setState(() { - selectedCategory = cat; - }); - fetchTransactions(category: cat); - } - }, - ), - ); - }).toList(), - ), - ); - } - - Widget buildTransactionCard(Transaction tx) { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: const Color(0xFF00221F), - borderRadius: BorderRadius.circular(16), - boxShadow: const [ - BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 4)), - ], - ), - child: ListTile( - title: Text( - tx.description, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - subtitle: Text( - '${tx.category} • ${tx.date.toLocal().toString().split(' ')[0]}', - style: const TextStyle(color: Colors.white70), - ), - trailing: Text( - '₹${tx.amount.toStringAsFixed(2)}', - style: const TextStyle( - color: Color(0xFFFABF02), - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFF001815), - appBar: AppBar( - backgroundColor: const Color(0xFF00221F), - title: const Text('Transactions'), - centerTitle: true, - elevation: 0, - ), - body: Column( - children: [ - buildCategoryChips(), - Expanded( - child: - isLoading - ? const Center(child: CircularProgressIndicator()) - : transactions.isEmpty - ? const Center( - child: Text( - 'No transactions found.', - style: TextStyle(color: Colors.white70), - ), - ) - : ListView.builder( - itemCount: transactions.length, - itemBuilder: - (context, index) => - buildTransactionCard(transactions[index]), - ), - ), - ], - ), - ); - } -} diff --git a/Frontend/monarch/macos/Flutter/GeneratedPluginRegistrant.swift b/Frontend/monarch/macos/Flutter/GeneratedPluginRegistrant.swift index e777c67..b8e2b22 100644 --- a/Frontend/monarch/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/Frontend/monarch/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,9 @@ import FlutterMacOS import Foundation import path_provider_foundation +import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/Frontend/monarch/pubspec.lock b/Frontend/monarch/pubspec.lock index f40dcfb..dbd585b 100644 --- a/Frontend/monarch/pubspec.lock +++ b/Frontend/monarch/pubspec.lock @@ -81,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" fl_chart: dependency: "direct main" description: @@ -107,6 +115,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" google_fonts: dependency: "direct main" description: @@ -259,6 +272,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter diff --git a/Frontend/monarch/pubspec.yaml b/Frontend/monarch/pubspec.yaml index b670fff..07a28fd 100644 --- a/Frontend/monarch/pubspec.yaml +++ b/Frontend/monarch/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: http: ^1.4.0 fl_chart: ^1.0.0 google_fonts: ^6.2.1 + shared_preferences: ^2.5.3 dev_dependencies: flutter_test: