This commit is contained in:
lokeshramchand-ctrl 2025-06-19 06:26:39 +05:30
parent 6b0162bb12
commit ab85157934
10 changed files with 2074 additions and 534 deletions

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://192.168.29.129:5000/api/predict', {
const predictRes = await axios.post('http://192.168.1.9:5000/api/predict', {
description,
});

View file

@ -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<AddExpenseScreen> createState() => _AddExpenseScreenState();
}
class _AddExpenseScreenState extends State<AddExpenseScreen>
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<Offset> _slideAnimation;
late Animation<double> _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<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(
CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic),
);
_fadeAnimation = Tween<double>(
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<String> 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<void> 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<Color>(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<String> 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,
),
),
),
),
),
],
),
),
),
),
],
),
),
),
),
);
}
}

View file

@ -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<FinTrackHomePage> createState() => _FinTrackHomePageState();
}
class _FinTrackHomePageState extends State<FinTrackHomePage>
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<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
// Sample data for dashboard
final double totalBalance = 5847.32;
final double monthlyIncome = 8500.00;
final double monthlyExpenses = 2652.68;
final List<Map<String, dynamic>> 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<Map<String, dynamic>> 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<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(parent: _fadeController, curve: Curves.easeOut));
_slideAnimation = Tween<Offset>(
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<Color>(
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,
);
}
}

View file

@ -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<AddTransactionPage> {
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<AddTransactionPage> {
);
}
} 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<AddTransactionPage> {
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const TransactionsPage(),
),
MaterialPageRoute(builder: (context) => const Statistics()),
);
},
child: const Text('NextPage'),

View file

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

View file

@ -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<Statistics> createState() => StatisticsState();
}
class StatisticsState extends State<Statistics> {
class StatisticsState extends State<Statistics> with TickerProviderStateMixin {
List<Transaction> transactions = [];
bool isLoading = false;
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
final List<String> categories = [
'All',
@ -56,14 +62,39 @@ class StatisticsState extends State<Statistics> {
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
// 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<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
);
_slideAnimation = Tween<Offset>(
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<Statistics> {
});
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<Statistics> {
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<Statistics> {
}
}
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<double>(
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<Color>(
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<double>(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<Statistics> {
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<Statistics> {
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<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()),
_buildHeader(),
_buildTotalSpentCard(),
_buildToggleButtons(),
buildCategoryChips(),
const SizedBox(height: 10),
Expanded(
child:
isLoading
? const Center(child: CircularProgressIndicator())
? Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(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,
);
}
}

View file

@ -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<String, dynamic> 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<TransactionsPage> {
List<Transaction> transactions = [];
bool isLoading = false;
final List<String> categories = [
'All',
'Food',
'Shopping',
'Bills',
'Travel',
'Entertainment',
'Other',
];
String selectedCategory = 'All';
@override
void initState() {
super.initState();
fetchTransactions();
}
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'];
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]),
),
),
],
),
);
}
}

View file

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

View file

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

View file

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