diff --git a/lib/main.dart b/lib/main.dart index 9e851f4..76a2a64 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,8 @@ import 'package:flutter_workshop_25/screens/home_screen.dart'; import 'package:flutter_workshop_25/services/hive_service.dart'; import 'package:flutter_workshop_25/theme.dart'; import 'package:hive_flutter/hive_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_workshop_25/providers/expense_provider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -20,11 +22,14 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Expense Tracker', - debugShowCheckedModeBanner: false, - theme: appTheme, - home: const HomeScreen(), + return ChangeNotifierProvider( + create: (_) => ExpenseProvider(), + child: MaterialApp( + title: 'Expense Tracker', + debugShowCheckedModeBanner: false, + theme: appTheme, + home: const HomeScreen(), + ), ); } } diff --git a/lib/providers/expense_provider.dart b/lib/providers/expense_provider.dart new file mode 100644 index 0000000..e64e62c --- /dev/null +++ b/lib/providers/expense_provider.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_workshop_25/models/expense.dart'; +import 'package:flutter_workshop_25/services/hive_service.dart'; +import 'package:intl/intl.dart'; + +class ExpenseProvider extends ChangeNotifier { + List _expenses = []; + + ExpenseProvider() { + _loadExpenses(); + } + + List get expenses => _expenses; + + void _loadExpenses() { + _expenses = HiveService.getAllExpenses(); + notifyListeners(); + } + + void addExpense(Expense expense) { + HiveService.addExpense(expense); + _loadExpenses(); + } + + double get totalMonthlyExpense { + final now = DateTime.now(); + return _expenses + .where((e) => e.date.month == now.month && e.date.year == now.year) + .fold(0.0, (sum, e) => sum + e.amount); + } + + double get weeklyAverage { + final now = DateTime.now(); + final sevenDaysAgo = now.subtract(const Duration(days: 7)); + final recentExpenses = _expenses.where((e) => e.date.isAfter(sevenDaysAgo)).toList(); + if (recentExpenses.isEmpty) return 0.0; + final total = recentExpenses.fold(0.0, (sum, e) => sum + e.amount); + return total / 7; + } + + double get highestSpending { + final now = DateTime.now(); + final sevenDaysAgo = now.subtract(const Duration(days: 7)); + final recentExpenses = _expenses.where((e) => e.date.isAfter(sevenDaysAgo)).toList(); + if (recentExpenses.isEmpty) return 0.0; + return recentExpenses.map((e) => e.amount).reduce((a, b) => a > b ? a : b); + } + + Map getWeeklyChartData() { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final Map result = {}; + for (int i = 0; i < 7; i++) { + final date = today.subtract(Duration(days: i)); + result[DateFormat('EEE').format(date)] = 0.0; + } + final sevenDaysAgo = today.subtract(const Duration(days: 6)); + for (var expense in _expenses) { + final expenseDate = DateTime(expense.date.year, expense.date.month, expense.date.day); + if (!expenseDate.isBefore(sevenDaysAgo)) { + final dayKey = DateFormat('EEE').format(expenseDate); + if (result.containsKey(dayKey)) { + result[dayKey] = (result[dayKey] ?? 0) + expense.amount; + } + } + } + final reversedKeys = result.keys.toList().reversed; + final finalResult = {for (var k in reversedKeys) k: result[k]!}; + return finalResult; + } +} diff --git a/lib/screens/add_expense_screen.dart b/lib/screens/add_expense_screen.dart index 0ffbdeb..440a08d 100644 --- a/lib/screens/add_expense_screen.dart +++ b/lib/screens/add_expense_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_workshop_25/models/expense.dart'; -import 'package:flutter_workshop_25/services/hive_service.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_workshop_25/providers/expense_provider.dart'; class AddExpenseScreen extends StatefulWidget { const AddExpenseScreen({super.key}); @@ -23,11 +24,12 @@ class _AddExpenseScreenState extends State { } Future _pickDate(BuildContext context) async { + final DateTime now = DateTime.now(); final DateTime? picked = await showDatePicker( context: context, - initialDate: _selectedDate ?? DateTime.now(), + initialDate: _selectedDate ?? now, firstDate: DateTime(2000), - lastDate: DateTime(2101), + lastDate: DateTime(now.year, now.month, now.day), ); if (picked != null && picked != _selectedDate) { setState(() { @@ -53,7 +55,7 @@ class _AddExpenseScreenState extends State { date: _selectedDate!, ); - HiveService.addExpense(newExpense); + Provider.of(context, listen: false).addExpense(newExpense); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Expense added successfully!')), @@ -108,12 +110,24 @@ class _AddExpenseScreenState extends State { decoration: InputDecoration( labelText: "Title", hintText: "Enter expense title", + prefixIcon: const Icon(Icons.title, color: Colors.teal), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: colorScheme.primary, width: 2), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: colorScheme.outline, width: 1.5), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: colorScheme.primary, width: 2), ), filled: true, fillColor: colorScheme.surfaceContainerHighest, + contentPadding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16), ), + style: textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500), validator: (value) { if (value == null || value.trim().isEmpty) { return "Please enter a title"; @@ -129,13 +143,24 @@ class _AddExpenseScreenState extends State { decoration: InputDecoration( labelText: "Amount", hintText: "Enter amount spent", - prefixText: "₹ ", + prefixIcon: const Icon(Icons.currency_rupee, color: Colors.teal), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: colorScheme.primary, width: 2), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: colorScheme.outline, width: 1.5), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: colorScheme.primary, width: 2), ), filled: true, fillColor: colorScheme.surfaceContainerHighest, + contentPadding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16), ), + style: textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500), validator: (value) { if (value == null || value.trim().isEmpty) { return "Please enter an amount"; @@ -153,6 +178,7 @@ class _AddExpenseScreenState extends State { child: InputDecorator( decoration: InputDecoration( labelText: "Date", + prefixIcon: const Icon(Icons.calendar_today_rounded, color: Colors.teal), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), @@ -172,9 +198,9 @@ class _AddExpenseScreenState extends State { : colorScheme.onSurface, ), ), - Icon( - Icons.calendar_today_rounded, - color: colorScheme.primary, + const Icon( + Icons.arrow_drop_down, + color: Colors.teal, ), ], ), diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 0e216c8..064f2e7 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1,12 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:flutter_workshop_25/models/expense.dart'; import 'package:flutter_workshop_25/screens/add_expense_screen.dart'; import 'package:flutter_workshop_25/screens/expense_history_screen.dart'; -import 'package:flutter_workshop_25/services/hive_service.dart'; import 'package:flutter_workshop_25/theme.dart'; -import 'package:flutter_workshop_25/utils/expense_data_helper.dart'; import 'package:flutter_workshop_25/widgets/spending_graph.dart'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:flutter_workshop_25/providers/expense_provider.dart'; +import 'package:provider/provider.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -18,13 +16,12 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State { @override Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: HiveService.getExpenseBox().listenable(), - builder: (context, Box box, _) { - final totalMonthlyExpense = ExpenseDataHelper.getTotalExpense(); - final weeklyAverage = ExpenseDataHelper.getWeeklyAverage(); - final highestSpending = ExpenseDataHelper.getHighestSpending(); - final weeklyData = ExpenseDataHelper.getWeeklyChartData(); + return Consumer( + builder: (context, expenseProvider, _) { + final totalMonthlyExpense = expenseProvider.totalMonthlyExpense; + final weeklyAverage = expenseProvider.weeklyAverage; + final highestSpending = expenseProvider.highestSpending; + final weeklyData = expenseProvider.getWeeklyChartData(); return Scaffold( appBar: AppBar( @@ -108,7 +105,7 @@ class _HomeScreenState extends State { child: _buildStatCard( context, icon: Icons.calendar_view_week, - title: "Weekly Avg", + title: "Week Avg", value: "₹${weeklyAverage.toStringAsFixed(0)}", ), ), diff --git a/lib/utils/expense_data_helper.dart b/lib/utils/expense_data_helper.dart index 625ccba..33c98af 100644 --- a/lib/utils/expense_data_helper.dart +++ b/lib/utils/expense_data_helper.dart @@ -91,4 +91,28 @@ class ExpenseDataHelper { return recentExpenses.map((e) => e.amount).reduce((a, b) => a > b ? a : b); } + + static Map getExpensesForDateRange(DateTime start, DateTime end) { + final expenses = getAllExpenses(); + final Map grouped = {}; + for (var expense in expenses) { + final expenseDate = DateTime(expense.date.year, expense.date.month, expense.date.day); + if (!expenseDate.isBefore(start) && !expenseDate.isAfter(end)) { + final dayKey = DateFormat('EEE').format(expenseDate); + grouped[dayKey] = (grouped[dayKey] ?? 0) + expense.amount; + } + } + for (int i = 0; i <= end.difference(start).inDays; i++) { + final date = start.add(Duration(days: i)); + final dayKey = DateFormat('EEE').format(date); + grouped.putIfAbsent(dayKey, () => 0.0); + } + final ordered = {}; + for (int i = 0; i <= end.difference(start).inDays; i++) { + final date = start.add(Duration(days: i)); + final dayKey = DateFormat('EEE').format(date); + ordered[dayKey] = grouped[dayKey]!; + } + return ordered; + } } diff --git a/lib/widgets/spending_graph.dart b/lib/widgets/spending_graph.dart index 21bc3d0..ea96db5 100644 --- a/lib/widgets/spending_graph.dart +++ b/lib/widgets/spending_graph.dart @@ -24,8 +24,17 @@ class SpendingGraph extends StatelessWidget { borderData: FlBorderData(show: true), gridData: const FlGridData(show: false), titlesData: FlTitlesData( - leftTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: true, reservedSize: 40), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 48, + getTitlesWidget: (value, meta) { + return Text( + value.toInt() == 0 ? '0' : value >= 1000 ? '${(value ~/ 1000)}K' : value.toInt().toString(), + style: const TextStyle(fontSize: 14), + ); + }, + ), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), diff --git a/pubspec.lock b/pubspec.lock index c9657fe..0d61bef 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -14,6 +14,14 @@ packages: description: dart source: sdk version: "0.3.3" + all: + dependency: "direct main" + description: + name: all + sha256: "6a8d1b379a7a683fe73bf8dc9add8f491661a653907e39f9b92afab030e42bd4" + url: "https://pub.dev" + source: hosted + version: "0.0.3" analyzer: dependency: transitive description: @@ -248,6 +256,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -264,6 +277,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + go_router: + dependency: transitive + description: + name: go_router + sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + url: "https://pub.dev" + source: hosted + version: "14.8.1" google_fonts: dependency: "direct main" description: @@ -440,6 +461,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -528,6 +557,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.2" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" pub_semver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8d62d39..32d4b40 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,8 @@ dependencies: hive: ^2.2.3 hive_flutter: ^1.1.0 path_provider: ^2.1.5 + all: ^0.0.3 + provider: ^6.1.5+1 dev_dependencies: flutter_test: