355 lines
12 KiB
Dart
355 lines
12 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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|