diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..89cd618 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "monarch", + "cwd": "Frontend\\monarch", + "request": "launch", + "type": "dart" + }, + { + "name": "monarch (profile mode)", + "cwd": "Frontend\\monarch", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "monarch (release mode)", + "cwd": "Frontend\\monarch", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} \ No newline at end of file diff --git a/Backend/index.js b/Backend/index.js index d7fdc71..11ba7d9 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://10.117.157.140:5000/api/predict', { + const predictRes = await axios.post('http://192.168.29.129:5000/api/predict', { description, }); diff --git a/Frontend/monarch/android/app/build.gradle.kts b/Frontend/monarch/android/app/build.gradle.kts index c86b4f0..0ec856c 100644 --- a/Frontend/monarch/android/app/build.gradle.kts +++ b/Frontend/monarch/android/app/build.gradle.kts @@ -8,7 +8,7 @@ plugins { android { namespace = "com.example.monarch" compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + ndkVersion = "27.0.12077973" compileOptions { sourceCompatibility = JavaVersion.VERSION_11 diff --git a/Frontend/monarch/lib/main.dart b/Frontend/monarch/lib/main.dart index 5472948..45abfbc 100644 --- a/Frontend/monarch/lib/main.dart +++ b/Frontend/monarch/lib/main.dart @@ -4,9 +4,11 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import 'package:monarch/static.dart'; import 'package:monarch/transaction.dart'; void main() { + WidgetsFlutterBinding.ensureInitialized(); // This fixes the issue runApp(const MyApp()); } @@ -18,7 +20,7 @@ class MyApp extends StatelessWidget { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData(primarySwatch: Colors.blue), - home: const AddTransactionPage(), + home: const Statistics(), ); } } @@ -43,7 +45,7 @@ class _AddTransactionPageState extends State { try { final response = await http.post( - Uri.parse('http://192.168.172.140:3000/api/transaction/add'), + Uri.parse('http://192.168.1.5:3000/api/transaction/add'), headers: {'Content-Type': 'application/json'}, body: json.encode({ 'description': descriptionController.text, diff --git a/Frontend/monarch/lib/static.dart b/Frontend/monarch/lib/static.dart new file mode 100644 index 0000000..a6f1765 --- /dev/null +++ b/Frontend/monarch/lib/static.dart @@ -0,0 +1,501 @@ +// ignore_for_file: sort_child_properties_last, use_build_context_synchronously, deprecated_member_use + +import 'dart:convert'; +import 'dart:ui'; +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; + +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 Statistics extends StatefulWidget { + const Statistics({super.key}); + + @override + State createState() => StatisticsState(); +} + +class StatisticsState extends State { + List transactions = []; + bool isLoading = false; + + final List categories = [ + 'All', + 'Food', + 'Shopping', + 'Bills', + 'Travel', + 'Entertainment', + 'Other', + ]; + + String selectedCategory = 'All'; + String _selectedPlan = 'Monthly'; + + Map totalAmountPerCategory = {}; + + final Color backgroundColor = Color(0xFF121212); // Deep black + final Color accentColor = Color(0xFF00FF88); // Modern green + final Color cardColor = Color(0xFF1F1F1F); // Darker gray + + @override + void initState() { + super.initState(); + fetchTransactions(); + } + + //API Call for Fetch + 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']; + + final fetched = list.map((json) => Transaction.fromJson(json)).toList(); + + final totals = {}; + for (var tx in fetched) { + totals[tx.category] = (totals[tx.category] ?? 0) + tx.amount; + } + + setState(() { + transactions = fetched; + totalAmountPerCategory = totals; + }); + } 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: backgroundColor, + backgroundColor: accentColor, + labelStyle: GoogleFonts.poppins( + color: isSelected ? accentColor : cardColor, + ), + 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: cardColor.withOpacity(0.8), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.4), + blurRadius: 10, + offset: Offset(0, 6), + ), + ], + ), + child: ListTile( + title: Text( + tx.description, + style: GoogleFonts.poppins( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + 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 _buildToggleButton(String label) { + final bool isSelected = _selectedPlan == label; + + return GestureDetector( + onTap: () => setState(() => _selectedPlan = label), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + color: isSelected ? accentColor : cardColor, + borderRadius: BorderRadius.circular(18), + boxShadow: + isSelected + ? [ + BoxShadow( + color: accentColor.withOpacity(0.4), + blurRadius: 12, + spreadRadius: 1, + ), + ] + : [], + ), + alignment: Alignment.center, + child: Text( + label, + style: GoogleFonts.poppins( + color: isSelected ? backgroundColor : Colors.white70, + fontWeight: FontWeight.w600, + fontSize: 16, + ), + ), + ), + ); + } + + 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()), + buildCategoryChips(), + const SizedBox(height: 10), + Expanded( + child: + isLoading + ? const Center(child: CircularProgressIndicator()) + : transactions.isEmpty + ? Center( + child: Text( + 'No transactions found.', + style: GoogleFonts.poppins(color: Colors.white70), + ), + ) + : ListView.builder( + 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, + ), + ), + ), + ); + } +} diff --git a/Frontend/monarch/lib/transaction.dart b/Frontend/monarch/lib/transaction.dart index 645d7c8..e96a53d 100644 --- a/Frontend/monarch/lib/transaction.dart +++ b/Frontend/monarch/lib/transaction.dart @@ -64,7 +64,7 @@ class _TransactionsPageState extends State { try { final uri = Uri.parse( - 'http://192.168.172.140:3000/api/transactions', + 'http://192.168.1.5:3000/api/transactions', ).replace( queryParameters: category == 'All' ? null : {'category': category}, ); diff --git a/Frontend/monarch/macos/Flutter/GeneratedPluginRegistrant.swift b/Frontend/monarch/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..e777c67 100644 --- a/Frontend/monarch/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/Frontend/monarch/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/Frontend/monarch/pubspec.lock b/Frontend/monarch/pubspec.lock index 670b8db..f40dcfb 100644 --- a/Frontend/monarch/pubspec.lock +++ b/Frontend/monarch/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -49,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -57,6 +73,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "577aeac8ca414c25333334d7c4bb246775234c0e44b38b10a82b559dd4d764e7" + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter: dependency: "direct main" description: flutter @@ -75,6 +107,14 @@ packages: description: flutter source: sdk version: "0.0.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" http: dependency: "direct main" description: @@ -155,6 +195,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" sky_engine: dependency: transitive description: flutter @@ -240,6 +344,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: dart: ">=3.7.2 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.27.4" diff --git a/Frontend/monarch/pubspec.yaml b/Frontend/monarch/pubspec.yaml index 570f892..b670fff 100644 --- a/Frontend/monarch/pubspec.yaml +++ b/Frontend/monarch/pubspec.yaml @@ -35,6 +35,8 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 http: ^1.4.0 + fl_chart: ^1.0.0 + google_fonts: ^6.2.1 dev_dependencies: flutter_test: