diff --git a/mobile/lib/screens/calendar_screen.dart b/mobile/lib/screens/calendar_screen.dart new file mode 100644 index 000000000..26754e08f --- /dev/null +++ b/mobile/lib/screens/calendar_screen.dart @@ -0,0 +1,498 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:intl/intl.dart'; +import '../models/account.dart'; +import '../models/transaction.dart'; +import '../providers/accounts_provider.dart'; +import '../providers/transactions_provider.dart'; +import '../providers/auth_provider.dart'; +import '../services/log_service.dart'; + +class CalendarScreen extends StatefulWidget { + const CalendarScreen({super.key}); + + @override + State createState() => _CalendarScreenState(); +} + +class _CalendarScreenState extends State { + final LogService _log = LogService.instance; + Account? _selectedAccount; + DateTime _currentMonth = DateTime.now(); + Map _dailyChanges = {}; + bool _isLoading = false; + String _accountType = 'asset'; // 'asset' or 'liability' + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadInitialData(); + }); + } + + Future _loadInitialData() async { + final accountsProvider = context.read(); + final authProvider = context.read(); + + final accessToken = await authProvider.getValidAccessToken(); + + if (accountsProvider.accounts.isEmpty && accessToken != null) { + await accountsProvider.fetchAccounts( + accessToken: accessToken, + forceSync: false, + ); + } + + if (accountsProvider.accounts.isNotEmpty) { + // Select first account of the selected type + final filteredAccounts = _getFilteredAccounts(accountsProvider.accounts); + setState(() { + _selectedAccount = filteredAccounts.isNotEmpty ? filteredAccounts.first : null; + }); + if (_selectedAccount != null) { + await _loadTransactionsForAccount(); + } + } + } + + List _getFilteredAccounts(List accounts) { + if (_accountType == 'asset') { + return accounts.where((a) => a.isAsset).toList(); + } else { + return accounts.where((a) => a.isLiability).toList(); + } + } + + Future _loadTransactionsForAccount() async { + if (_selectedAccount == null) return; + + setState(() { + _isLoading = true; + }); + + final authProvider = context.read(); + final transactionsProvider = context.read(); + + final accessToken = await authProvider.getValidAccessToken(); + + if (accessToken != null) { + await transactionsProvider.fetchTransactions( + accessToken: accessToken, + accountId: _selectedAccount!.id, + forceSync: false, + ); + + final transactions = transactionsProvider.transactions; + _log.info('CalendarScreen', 'Loaded ${transactions.length} transactions for account ${_selectedAccount!.name}'); + + if (transactions.isNotEmpty) { + _log.debug('CalendarScreen', 'Sample transaction - name: ${transactions.first.name}, amount: ${transactions.first.amount}, nature: ${transactions.first.nature}'); + } + + _calculateDailyChanges(transactions); + _log.info('CalendarScreen', 'Calculated ${_dailyChanges.length} days with changes'); + } + + setState(() { + _isLoading = false; + }); + } + + void _calculateDailyChanges(List transactions) { + final changes = {}; + + _log.debug('CalendarScreen', 'Starting to calculate daily changes for ${transactions.length} transactions'); + + for (var transaction in transactions) { + try { + final date = DateTime.parse(transaction.date); + final dateKey = DateFormat('yyyy-MM-dd').format(date); + + // Parse amount with proper sign handling + String trimmedAmount = transaction.amount.trim(); + trimmedAmount = trimmedAmount.replaceAll('\u2212', '-'); // Normalize minus sign + + // Detect if the amount has a negative sign + bool hasNegativeSign = trimmedAmount.startsWith('-') || trimmedAmount.endsWith('-'); + + // Remove all non-numeric characters except decimal point and minus sign + String numericString = trimmedAmount.replaceAll(RegExp(r'[^\d.\-]'), ''); + + // Parse the numeric value + double amount = double.tryParse(numericString.replaceAll('-', '')) ?? 0.0; + + // Apply the sign from the string + if (hasNegativeSign) { + amount = -amount; + } + + // For asset accounts, flip the sign to match accounting conventions + // For liability accounts, also flip the sign + if (_selectedAccount?.isAsset == true || _selectedAccount?.isLiability == true) { + amount = -amount; + } + + _log.debug('CalendarScreen', 'Processing transaction ${transaction.name} - date: $dateKey, raw amount: ${transaction.amount}, parsed: $amount, isAsset: ${_selectedAccount?.isAsset}, isLiability: ${_selectedAccount?.isLiability}'); + + changes[dateKey] = (changes[dateKey] ?? 0.0) + amount; + _log.debug('CalendarScreen', 'Date $dateKey now has total: ${changes[dateKey]}'); + } catch (e) { + _log.error('CalendarScreen', 'Failed to parse transaction date: ${transaction.date}, error: $e'); + } + } + + _log.info('CalendarScreen', 'Final changes map has ${changes.length} entries'); + changes.forEach((date, amount) { + _log.debug('CalendarScreen', '$date -> $amount'); + }); + + setState(() { + _dailyChanges = changes; + }); + } + + void _previousMonth() { + setState(() { + _currentMonth = DateTime(_currentMonth.year, _currentMonth.month - 1); + }); + } + + void _nextMonth() { + setState(() { + _currentMonth = DateTime(_currentMonth.year, _currentMonth.month + 1); + }); + } + + double _getTotalForMonth() { + double total = 0.0; + final yearMonth = DateFormat('yyyy-MM').format(_currentMonth); + + _dailyChanges.forEach((date, change) { + if (date.startsWith(yearMonth)) { + total += change; + } + }); + + return total; + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final accountsProvider = context.watch(); + + return Scaffold( + appBar: AppBar( + title: const Text('Account Calendar'), + ), + body: Column( + children: [ + // Account type selector + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colorScheme.surface, + border: Border( + bottom: BorderSide( + color: colorScheme.outlineVariant, + width: 1, + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Account Type', + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + SegmentedButton( + segments: const [ + ButtonSegment( + value: 'asset', + label: Text('Assets'), + icon: Icon(Icons.account_balance_wallet), + ), + ButtonSegment( + value: 'liability', + label: Text('Liabilities'), + icon: Icon(Icons.credit_card), + ), + ], + selected: {_accountType}, + onSelectionChanged: (Set newSelection) { + setState(() { + _accountType = newSelection.first; + // Switch to first account of new type + final filteredAccounts = _getFilteredAccounts(accountsProvider.accounts); + _selectedAccount = filteredAccounts.isNotEmpty ? filteredAccounts.first : null; + _dailyChanges = {}; + }); + if (_selectedAccount != null) { + _loadTransactionsForAccount(); + } + }, + ), + ], + ), + ), + + // Account selector + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colorScheme.surface, + border: Border( + bottom: BorderSide( + color: colorScheme.outlineVariant, + width: 1, + ), + ), + ), + child: DropdownButtonFormField( + value: _selectedAccount, + decoration: InputDecoration( + labelText: 'Select Account', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + items: _getFilteredAccounts(accountsProvider.accounts).map((account) { + return DropdownMenuItem( + value: account, + child: Text('${account.name} (${account.currency})'), + ); + }).toList(), + onChanged: (Account? newAccount) { + setState(() { + _selectedAccount = newAccount; + _dailyChanges = {}; + }); + _loadTransactionsForAccount(); + }, + ), + ), + + // Month selector + Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + border: Border( + bottom: BorderSide( + color: colorScheme.outlineVariant, + width: 1, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon(Icons.chevron_left), + onPressed: _previousMonth, + ), + Text( + DateFormat('yyyy-MM').format(_currentMonth), + style: Theme.of(context).textTheme.titleLarge, + ), + IconButton( + icon: const Icon(Icons.chevron_right), + onPressed: _nextMonth, + ), + ], + ), + ), + + // Monthly total + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + border: Border( + bottom: BorderSide( + color: colorScheme.outlineVariant, + width: 1, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Monthly Change', + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + _formatCurrency(_getTotalForMonth()), + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: _getTotalForMonth() >= 0 + ? Colors.green + : Colors.red, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + + // Calendar + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _buildCalendar(colorScheme), + ), + ], + ), + ); + } + + Widget _buildCalendar(ColorScheme colorScheme) { + final firstDayOfMonth = DateTime(_currentMonth.year, _currentMonth.month, 1); + final lastDayOfMonth = DateTime(_currentMonth.year, _currentMonth.month + 1, 0); + final daysInMonth = lastDayOfMonth.day; + final startWeekday = firstDayOfMonth.weekday % 7; // 0 = Sunday + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + // Weekday headers + SizedBox( + height: 40, + child: Row( + children: ['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day) { + return Expanded( + child: Center( + child: Text( + day, + style: TextStyle( + fontWeight: FontWeight.bold, + color: colorScheme.onSurfaceVariant, + ), + ), + ), + ); + }).toList(), + ), + ), + + // Calendar grid + ...List.generate((daysInMonth + startWeekday + 6) ~/ 7, (weekIndex) { + return SizedBox( + height: 70, + child: Row( + children: List.generate(7, (dayIndex) { + final dayNumber = weekIndex * 7 + dayIndex - startWeekday + 1; + + if (dayNumber < 1 || dayNumber > daysInMonth) { + return const Expanded(child: SizedBox.shrink()); + } + + final date = DateTime(_currentMonth.year, _currentMonth.month, dayNumber); + final dateKey = DateFormat('yyyy-MM-dd').format(date); + final change = _dailyChanges[dateKey] ?? 0.0; + final hasChange = _dailyChanges.containsKey(dateKey); + + return Expanded( + child: _buildDayCell( + dayNumber, + change, + hasChange, + colorScheme, + ), + ); + }).toList(), + ), + ); + }), + ], + ), + ), + ); + } + + Widget _buildDayCell(int day, double change, bool hasChange, ColorScheme colorScheme) { + Color? backgroundColor; + Color? textColor; + + if (hasChange) { + if (change > 0) { + backgroundColor = Colors.green.withValues(alpha: 0.2); + textColor = Colors.green.shade700; + } else if (change < 0) { + backgroundColor = Colors.red.withValues(alpha: 0.2); + textColor = Colors.red.shade700; + } + } + + return Container( + margin: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: backgroundColor ?? colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: colorScheme.outlineVariant, + width: 1, + ), + ), + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + day.toString(), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: colorScheme.onSurface, + ), + ), + if (hasChange) ...[ + const SizedBox(height: 2), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + _formatAmount(change), + style: TextStyle( + fontSize: 10, + color: textColor, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ], + ), + ), + ); + } + + String _formatAmount(double amount) { + // Support up to 8 decimal places, but omit unnecessary trailing zeros + final formatter = NumberFormat('#,##0.########'); + final sign = amount >= 0 ? '+' : ''; + return '$sign${formatter.format(amount)}'; + } + + String _formatCurrency(double amount) { + final currencySymbol = _selectedAccount?.currency ?? ''; + // Support up to 8 decimal places for monthly total + final formatter = NumberFormat('#,##0.########'); + final sign = amount >= 0 ? '+' : ''; + return '$sign$currencySymbol${formatter.format(amount.abs())}'; + } +} diff --git a/mobile/lib/screens/main_navigation_screen.dart b/mobile/lib/screens/main_navigation_screen.dart index 074caee24..a253bf614 100644 --- a/mobile/lib/screens/main_navigation_screen.dart +++ b/mobile/lib/screens/main_navigation_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'dashboard_screen.dart'; import 'chat_list_screen.dart'; +import 'more_screen.dart'; import 'settings_screen.dart'; class MainNavigationScreen extends StatefulWidget { @@ -16,7 +17,7 @@ class _MainNavigationScreenState extends State { final List _screens = [ const DashboardScreen(), const ChatListScreen(), - const PlaceholderScreen(), + const MoreScreen(), const SettingsScreen(), ]; @@ -60,44 +61,3 @@ class _MainNavigationScreenState extends State { ); } } - -class PlaceholderScreen extends StatelessWidget { - const PlaceholderScreen({super.key}); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return Scaffold( - appBar: AppBar( - title: const Text('More'), - ), - body: Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.construction, - size: 64, - color: colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 16), - Text( - 'Coming Soon', - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 8), - Text( - 'This section is under development.', - style: TextStyle(color: colorScheme.onSurfaceVariant), - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/lib/screens/more_screen.dart b/mobile/lib/screens/more_screen.dart new file mode 100644 index 000000000..80cf4ba4a --- /dev/null +++ b/mobile/lib/screens/more_screen.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'calendar_screen.dart'; +import 'recent_transactions_screen.dart'; + +class MoreScreen extends StatelessWidget { + const MoreScreen({super.key}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Scaffold( + appBar: AppBar( + title: const Text('More'), + ), + body: ListView( + children: [ + _buildMenuItem( + context: context, + icon: Icons.calendar_month, + title: 'Account Calendar', + subtitle: 'View monthly balance changes by account', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CalendarScreen(), + ), + ); + }, + ), + Divider(height: 1, color: colorScheme.outlineVariant), + _buildMenuItem( + context: context, + icon: Icons.receipt_long, + title: 'Recent Transactions', + subtitle: 'View recent transactions across all accounts', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const RecentTransactionsScreen(), + ), + ); + }, + ), + ], + ), + ); + } + + Widget _buildMenuItem({ + required BuildContext context, + required IconData icon, + required String title, + required String subtitle, + required VoidCallback onTap, + }) { + final colorScheme = Theme.of(context).colorScheme; + + return ListTile( + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: colorScheme.onPrimaryContainer, + ), + ), + title: Text( + title, + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Text( + subtitle, + style: TextStyle(color: colorScheme.onSurfaceVariant), + ), + trailing: Icon( + Icons.chevron_right, + color: colorScheme.onSurfaceVariant, + ), + onTap: onTap, + ); + } +} diff --git a/mobile/lib/screens/recent_transactions_screen.dart b/mobile/lib/screens/recent_transactions_screen.dart new file mode 100644 index 000000000..8decc3071 --- /dev/null +++ b/mobile/lib/screens/recent_transactions_screen.dart @@ -0,0 +1,292 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:intl/intl.dart'; +import '../models/transaction.dart'; +import '../models/account.dart'; +import '../providers/transactions_provider.dart'; +import '../providers/accounts_provider.dart'; +import '../providers/auth_provider.dart'; + +class RecentTransactionsScreen extends StatefulWidget { + const RecentTransactionsScreen({super.key}); + + @override + State createState() => _RecentTransactionsScreenState(); +} + +class _RecentTransactionsScreenState extends State { + int _transactionLimit = 20; + final List _limitOptions = [10, 20, 50, 100]; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadAllTransactions(); + }); + } + + Future _loadAllTransactions() async { + final authProvider = context.read(); + final transactionsProvider = context.read(); + + final accessToken = await authProvider.getValidAccessToken(); + + if (accessToken != null) { + // Load transactions for all accounts + await transactionsProvider.fetchTransactions( + accessToken: accessToken, + forceSync: false, + ); + } + } + + Future _refreshTransactions() async { + final authProvider = context.read(); + final transactionsProvider = context.read(); + + final accessToken = await authProvider.getValidAccessToken(); + + if (accessToken != null) { + await transactionsProvider.fetchTransactions( + accessToken: accessToken, + forceSync: true, + ); + } + } + + Account? _getAccount(String accountId) { + final accountsProvider = context.read(); + try { + return accountsProvider.accounts.firstWhere( + (a) => a.id == accountId, + ); + } catch (e) { + return null; + } + } + + List _getSortedTransactions(List transactions) { + final sorted = List.from(transactions); + sorted.sort((a, b) { + try { + final dateA = DateTime.parse(a.date); + final dateB = DateTime.parse(b.date); + return dateB.compareTo(dateA); // Most recent first + } catch (e) { + return 0; + } + }); + return sorted.take(_transactionLimit).toList(); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final transactionsProvider = context.watch(); + + final recentTransactions = _getSortedTransactions( + transactionsProvider.transactions, + ); + + return Scaffold( + appBar: AppBar( + title: const Text('Recent Transactions'), + actions: [ + PopupMenuButton( + initialValue: _transactionLimit, + icon: const Icon(Icons.filter_list), + tooltip: 'Display Limit', + onSelected: (int value) { + setState(() { + _transactionLimit = value; + }); + }, + itemBuilder: (context) => _limitOptions.map((limit) { + return PopupMenuItem( + value: limit, + child: Row( + children: [ + if (limit == _transactionLimit) + Icon(Icons.check, color: colorScheme.primary, size: 20) + else + const SizedBox(width: 20), + const SizedBox(width: 8), + Text('Show $limit'), + ], + ), + ); + }).toList(), + ), + ], + ), + body: RefreshIndicator( + onRefresh: _refreshTransactions, + child: transactionsProvider.isLoading + ? const Center(child: CircularProgressIndicator()) + : recentTransactions.isEmpty + ? _buildEmptyState(colorScheme) + : ListView.separated( + itemCount: recentTransactions.length, + separatorBuilder: (context, index) => Divider( + height: 1, + color: colorScheme.outlineVariant, + ), + itemBuilder: (context, index) { + final transaction = recentTransactions[index]; + return _buildTransactionItem( + transaction, + colorScheme, + ); + }, + ), + ), + ); + } + + Widget _buildEmptyState(ColorScheme colorScheme) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.receipt_long_outlined, + size: 64, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 16), + Text( + 'No Transactions', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + Text( + 'Pull to refresh', + style: TextStyle(color: colorScheme.onSurfaceVariant), + ), + ], + ), + ), + ); + } + + Widget _buildTransactionItem(Transaction transaction, ColorScheme colorScheme) { + final account = _getAccount(transaction.accountId); + final accountName = account?.name ?? 'Unknown Account'; + + // Parse amount with proper sign handling (same logic as transactions_list_screen.dart) + String trimmedAmount = transaction.amount.trim(); + trimmedAmount = trimmedAmount.replaceAll('\u2212', '-'); // Normalize minus sign + + // Detect if the amount has a negative sign + bool hasNegativeSign = trimmedAmount.startsWith('-') || trimmedAmount.endsWith('-'); + + // Remove all non-numeric characters except decimal point and minus sign + String numericString = trimmedAmount.replaceAll(RegExp(r'[^\d.\-]'), ''); + + // Parse the numeric value + double amount = double.tryParse(numericString.replaceAll('-', '')) ?? 0.0; + + // Apply the sign from the string + if (hasNegativeSign) { + amount = -amount; + } + + // For asset accounts and liability accounts, flip the sign to match accounting conventions + if (account?.isAsset == true || account?.isLiability == true) { + amount = -amount; + } + + // Determine display properties based on final amount + final isPositive = amount >= 0; + Color amountColor; + String sign; + + if (isPositive) { + amountColor = Colors.green.shade700; + sign = '+'; + } else { + amountColor = Colors.red.shade700; + sign = '-'; + } + + String formattedDate; + try { + final date = DateTime.parse(transaction.date); + formattedDate = DateFormat('yyyy-MM-dd HH:mm').format(date); + } catch (e) { + formattedDate = transaction.date; + } + + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isPositive + ? Colors.green.withValues(alpha: 0.1) + : Colors.red.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + isPositive ? Icons.arrow_upward : Icons.arrow_downward, + color: amountColor, + ), + ), + title: Text( + transaction.name, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text( + accountName, + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 2), + Text( + formattedDate, + style: TextStyle( + fontSize: 11, + color: colorScheme.onSurfaceVariant, + ), + ), + if (transaction.notes != null && transaction.notes!.isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + transaction.notes!, + style: TextStyle( + fontSize: 11, + color: colorScheme.onSurfaceVariant, + fontStyle: FontStyle.italic, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + trailing: Text( + '$sign${transaction.currency} ${_formatAmount(amount.abs())}', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: amountColor, + ), + ), + ); + } + + String _formatAmount(double amount) { + // Support up to 8 decimal places, but omit unnecessary trailing zeros + final formatter = NumberFormat('#,##0.########'); + return formatter.format(amount); + } +}