Revised
This commit is contained in:
parent
6b0162bb12
commit
ab85157934
10 changed files with 2074 additions and 534 deletions
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
608
Frontend/monarch/lib/add.dart
Normal file
608
Frontend/monarch/lib/add.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
696
Frontend/monarch/lib/homepage.dart
Normal file
696
Frontend/monarch/lib/homepage.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
|
|
|
|||
149
Frontend/monarch/lib/navbar.dart
Normal file
149
Frontend/monarch/lib/navbar.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue