250 lines
9.9 KiB
Dart
250 lines
9.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../../transactions/providers/providers.dart';
|
|
import '../../transactions/data/models.dart';
|
|
import '../../../core/theme/nature_colors.dart';
|
|
|
|
class WalletLedgerScreen extends ConsumerStatefulWidget {
|
|
final Wallet wallet;
|
|
|
|
const WalletLedgerScreen({super.key, required this.wallet});
|
|
|
|
@override
|
|
ConsumerState<WalletLedgerScreen> createState() => _WalletLedgerScreenState();
|
|
}
|
|
|
|
class _WalletLedgerScreenState extends ConsumerState<WalletLedgerScreen> {
|
|
String _selectedFilter = 'All Time';
|
|
DateTimeRange? _customDateRange;
|
|
|
|
void _showFilterSheet() {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
|
|
builder: (ctx) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text('Filter Ledger', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 16),
|
|
ListTile(
|
|
title: const Text('Today'),
|
|
onTap: () {
|
|
setState(() => _selectedFilter = 'Today');
|
|
Navigator.pop(ctx);
|
|
},
|
|
),
|
|
ListTile(
|
|
title: const Text('Current Month'),
|
|
onTap: () {
|
|
setState(() => _selectedFilter = 'Current Month');
|
|
Navigator.pop(ctx);
|
|
},
|
|
),
|
|
ListTile(
|
|
title: const Text('All Time'),
|
|
onTap: () {
|
|
setState(() => _selectedFilter = 'All Time');
|
|
Navigator.pop(ctx);
|
|
},
|
|
),
|
|
ListTile(
|
|
title: const Text('Custom Range'),
|
|
onTap: () async {
|
|
Navigator.pop(ctx);
|
|
final range = await showDateRangePicker(
|
|
context: context,
|
|
firstDate: DateTime(2000),
|
|
lastDate: DateTime(2100),
|
|
);
|
|
if (range != null) {
|
|
setState(() {
|
|
_selectedFilter = 'Custom Range';
|
|
_customDateRange = range;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final transactionsState = ref.watch(transactionProvider);
|
|
final walletsState = ref.watch(walletProvider);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text('${widget.wallet.name} Ledger'),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.filter_list),
|
|
onPressed: _showFilterSheet,
|
|
),
|
|
],
|
|
),
|
|
body: transactionsState.when(
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (err, stack) => Center(child: Text('Error: $err')),
|
|
data: (transactions) {
|
|
// 1. Filter all transactions related to this wallet
|
|
final walletTxs = transactions.where((t) => t.fromWalletId == widget.wallet.id || t.toWalletId == widget.wallet.id).toList();
|
|
|
|
// Sort chronologically (oldest first) to compute running balance
|
|
walletTxs.sort((a, b) => a.date.compareTo(b.date));
|
|
|
|
// 2. Determine date range
|
|
DateTime? startDate;
|
|
DateTime? endDate;
|
|
final now = DateTime.now();
|
|
if (_selectedFilter == 'Today') {
|
|
startDate = DateTime(now.year, now.month, now.day);
|
|
endDate = DateTime(now.year, now.month, now.day, 23, 59, 59);
|
|
} else if (_selectedFilter == 'Current Month') {
|
|
startDate = DateTime(now.year, now.month, 1);
|
|
endDate = DateTime(now.year, now.month + 1, 0, 23, 59, 59);
|
|
} else if (_selectedFilter == 'Custom Range' && _customDateRange != null) {
|
|
startDate = _customDateRange!.start;
|
|
endDate = DateTime(_customDateRange!.end.year, _customDateRange!.end.month, _customDateRange!.end.day, 23, 59, 59);
|
|
}
|
|
|
|
// 3. Calculate opening balance (sum of all transactions BEFORE start date)
|
|
double openingBalance = 0.0;
|
|
List<Transaction> visibleTxs = [];
|
|
|
|
for (var t in walletTxs) {
|
|
double impact = 0;
|
|
if (t.toWalletId == widget.wallet.id) {
|
|
impact = t.amount;
|
|
} else if (t.fromWalletId == widget.wallet.id) {
|
|
impact = -t.amount;
|
|
}
|
|
|
|
if (startDate != null && t.date.isBefore(startDate)) {
|
|
openingBalance += impact;
|
|
} else if (endDate != null && t.date.isAfter(endDate)) {
|
|
// skip
|
|
} else {
|
|
visibleTxs.add(t);
|
|
}
|
|
}
|
|
|
|
// 4. Generate Ledger Rows
|
|
double runningBalance = openingBalance;
|
|
List<DataRow> rows = [];
|
|
|
|
// Add Opening Balance Row if we have a date filter
|
|
if (startDate != null) {
|
|
rows.add(DataRow(
|
|
cells: [
|
|
const DataCell(Text('-')),
|
|
DataCell(Text(DateFormat('dd MMM yyyy').format(startDate))),
|
|
DataCell(Text('Rs. ${openingBalance.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold))),
|
|
const DataCell(Text('-')),
|
|
const DataCell(Text('-')),
|
|
DataCell(Text('Rs. ${runningBalance.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold))),
|
|
],
|
|
));
|
|
}
|
|
|
|
for (int i = 0; i < visibleTxs.length; i++) {
|
|
final t = visibleTxs[i];
|
|
|
|
bool isIncome = t.toWalletId == widget.wallet.id && t.fromWalletId == null;
|
|
bool isExpense = t.fromWalletId == widget.wallet.id && t.toWalletId == null;
|
|
bool isTransferIn = t.toWalletId == widget.wallet.id && t.fromWalletId != null;
|
|
bool isTransferOut = t.fromWalletId == widget.wallet.id && t.toWalletId != null;
|
|
|
|
double amount = t.amount;
|
|
if (isExpense || isTransferOut) {
|
|
runningBalance -= amount;
|
|
} else {
|
|
runningBalance += amount;
|
|
}
|
|
|
|
String fromName = '-';
|
|
String toName = '-';
|
|
|
|
if (isIncome || isTransferIn) {
|
|
if (t.fromWalletId != null && walletsState.hasValue) {
|
|
fromName = walletsState.value!.where((w) => w.id == t.fromWalletId).firstOrNull?.name ?? 'Unknown';
|
|
} else {
|
|
fromName = 'External';
|
|
}
|
|
toName = widget.wallet.name;
|
|
} else if (isExpense || isTransferOut) {
|
|
fromName = widget.wallet.name;
|
|
if (t.toWalletId != null && walletsState.hasValue) {
|
|
toName = walletsState.value!.where((w) => w.id == t.toWalletId).firstOrNull?.name ?? 'Unknown';
|
|
} else {
|
|
toName = 'External';
|
|
}
|
|
}
|
|
|
|
Color amountColor = (isExpense || isTransferOut) ? Colors.red : Colors.green.shade700;
|
|
String amountPrefix = (isExpense || isTransferOut) ? '-' : '+';
|
|
|
|
rows.add(DataRow(
|
|
cells: [
|
|
DataCell(Text('${startDate != null ? i + 1 : i + 1}')),
|
|
DataCell(Text(DateFormat('dd MMM yyyy').format(t.date))),
|
|
DataCell(Text('$amountPrefix Rs. ${amount.toStringAsFixed(2)}', style: TextStyle(color: amountColor, fontWeight: FontWeight.w600))),
|
|
DataCell(Text(fromName)),
|
|
DataCell(Text(toName)),
|
|
DataCell(Text('Rs. ${runningBalance.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold))),
|
|
],
|
|
));
|
|
}
|
|
|
|
if (rows.isEmpty) {
|
|
return const Center(child: Text('No transactions found for this period.'));
|
|
}
|
|
|
|
return Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text('Filter: $_selectedFilter', style: TextStyle(color: Colors.grey.shade600)),
|
|
Text('Closing Balance: Rs. ${runningBalance.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.vertical,
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: DataTable(
|
|
headingRowColor: WidgetStateProperty.resolveWith((states) => Colors.grey.shade100),
|
|
columnSpacing: 24,
|
|
columns: const [
|
|
DataColumn(label: Text('S.No', style: TextStyle(fontWeight: FontWeight.bold))),
|
|
DataColumn(label: Text('Date', style: TextStyle(fontWeight: FontWeight.bold))),
|
|
DataColumn(label: Text('Amount', style: TextStyle(fontWeight: FontWeight.bold))),
|
|
DataColumn(label: Text('From', style: TextStyle(fontWeight: FontWeight.bold))),
|
|
DataColumn(label: Text('To', style: TextStyle(fontWeight: FontWeight.bold))),
|
|
DataColumn(label: Text('Balance', style: TextStyle(fontWeight: FontWeight.bold))),
|
|
],
|
|
rows: rows,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|