This commit is contained in:
lokeshramchand-ctrl 2025-06-17 14:47:57 +05:30
parent 9ba173f00e
commit 6b0162bb12
9 changed files with 653 additions and 6 deletions

28
.vscode/launch.json vendored Normal file
View file

@ -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"
}
]
}

View file

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

View file

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

View file

@ -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<AddTransactionPage> {
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,

View file

@ -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<String, dynamic> 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<Statistics> createState() => StatisticsState();
}
class StatisticsState extends State<Statistics> {
List<Transaction> transactions = [];
bool isLoading = false;
final List<String> categories = [
'All',
'Food',
'Shopping',
'Bills',
'Travel',
'Entertainment',
'Other',
];
String selectedCategory = 'All';
String _selectedPlan = 'Monthly';
Map<String, double> 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<void> 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<dynamic> list = data['data'];
final fetched = list.map((json) => Transaction.fromJson(json)).toList();
final totals = <String, double>{};
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<double>(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,
),
),
),
);
}
}

View file

@ -64,7 +64,7 @@ class _TransactionsPageState extends State<TransactionsPage> {
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},
);

View file

@ -5,6 +5,8 @@
import FlutterMacOS
import Foundation
import path_provider_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
}

View file

@ -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"

View file

@ -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: