From 5ac977e90322570f0be524baa6ed58bf1aa17293 Mon Sep 17 00:00:00 2001 From: Nilanjan Chakrabarti Date: Tue, 4 Nov 2025 14:53:14 +0530 Subject: [PATCH] Self-Transfer #1 --- .../dashboard/screens/dashboard_screen.dart | 30 ++- .../screens/fund_transfer_screen.dart | 204 +++++++++------ .../fund_transfer_self_accounts_screen.dart | 94 +++++++ .../fund_transfer_self_amount_screen.dart | 245 ++++++++++++++++++ 4 files changed, 476 insertions(+), 97 deletions(-) create mode 100644 lib/features/fund_transfer/screens/fund_transfer_self_accounts_screen.dart create mode 100644 lib/features/fund_transfer/screens/fund_transfer_self_amount_screen.dart diff --git a/lib/features/dashboard/screens/dashboard_screen.dart b/lib/features/dashboard/screens/dashboard_screen.dart index 957b846..93c5874 100644 --- a/lib/features/dashboard/screens/dashboard_screen.dart +++ b/lib/features/dashboard/screens/dashboard_screen.dart @@ -138,7 +138,7 @@ class _DashboardScreenState extends State // Convert to title case switch (accountType.toLowerCase()) { case 'sa': - return AppLocalizations.of(context).savingsAccount; + return AppLocalizations.of(context).savingsAccount; case 'sb': return AppLocalizations.of(context).savingsAccount; case 'ln': @@ -147,6 +147,8 @@ class _DashboardScreenState extends State return AppLocalizations.of(context).termDeposit; case 'rd': return AppLocalizations.of(context).recurringDeposit; + case 'ca': + return "Current Account"; default: return AppLocalizations.of(context).unknownAccount; } @@ -495,18 +497,20 @@ class _DashboardScreenState extends State }, ), _buildQuickLink(Symbols.send_money, - AppLocalizations.of(context).fundTransfer, () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => FundTransferScreen( - creditAccountNo: - users[selectedAccountIndex] - .accountNo!, - remitterName: - users[selectedAccountIndex] - .name!))); - }, disable: false), + AppLocalizations.of(context).fundTransfer, () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FundTransferScreen( + creditAccountNo: + users[selectedAccountIndex] + .accountNo!, + remitterName: + users[selectedAccountIndex] + .name!, + // Pass the full list of accounts + accounts: users))); +}, disable: false), _buildQuickLink( Symbols.server_person, AppLocalizations.of(context).accountInfo, diff --git a/lib/features/fund_transfer/screens/fund_transfer_screen.dart b/lib/features/fund_transfer/screens/fund_transfer_screen.dart index cfb66d0..a8640d1 100644 --- a/lib/features/fund_transfer/screens/fund_transfer_screen.dart +++ b/lib/features/fund_transfer/screens/fund_transfer_screen.dart @@ -1,90 +1,126 @@ -import 'package:flutter/material.dart'; -import 'package:kmobile/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart'; -import 'package:material_symbols_icons/symbols.dart'; -import '../../../l10n/app_localizations.dart'; + import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; + import 'package:kmobile/data/models/user.dart'; + import 'package:kmobile/features/auth/controllers/auth_cubit.dart'; + import 'package:kmobile/features/auth/controllers/auth_state.dart'; + import 'package:kmobile/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart'; + import 'package:kmobile/features/fund_transfer/screens/fund_transfer_self_accounts_screen.dart'; + import 'package:material_symbols_icons/symbols.dart'; + import '../../../l10n/app_localizations.dart'; // Keep localizations -class FundTransferScreen extends StatelessWidget { - final String creditAccountNo; - final String remitterName; + class FundTransferScreen extends StatelessWidget { + final String creditAccountNo; + final String remitterName; + final List accounts; // Continue to accept the list of accounts - const FundTransferScreen({ - super.key, - required this.creditAccountNo, - required this.remitterName, - }); + const FundTransferScreen({ + super.key, + required this.creditAccountNo, + required this.remitterName, + required this.accounts, // It is passed from the dashboard + }); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context) - .fundTransfer - .replaceFirst(RegExp('\n'), '')), - ), - body: ListView( - children: [ - FundTransferManagementTile( - icon: Symbols.input_circle, - label: AppLocalizations.of(context).ownBank, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => FundTransferBeneficiaryScreen( - creditAccountNo: creditAccountNo, - remitterName: remitterName, - isOwnBank: true, - ), - ), - ); - }, - ), - const Divider(height: 1), - FundTransferManagementTile( - icon: Symbols.output_circle, - label: AppLocalizations.of(context).outsideBank, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => FundTransferBeneficiaryScreen( - creditAccountNo: creditAccountNo, - remitterName: remitterName, - isOwnBank: false, - ), - ), - ); - }, - ), - const Divider(height: 1), - ], - ), - ); - } -} + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + // Restore localization for the title + title: Text(AppLocalizations.of(context) + .fundTransfer + .replaceFirst(RegExp('\n'), '')), + ), + // Wrap with BlocBuilder to check the authentication state + body: BlocBuilder( + builder: (context, state) { + return ListView( + children: [ + FundTransferManagementTile( + icon: Symbols.person, + // Restore localization for the label + label: "Self Pay", + onTap: () { + // The accounts list is passed directly from the constructor + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FundTransferSelfAccountsScreen( + debitAccountNo: creditAccountNo, + remitterName: remitterName, + accounts: accounts, + ), + ), + ); + }, + // Disable the tile if the state is not Authenticated + disable: state is! Authenticated, + ), + const Divider(height: 1), + FundTransferManagementTile( + icon: Symbols.input_circle, + // Restore localization for the label + label: AppLocalizations.of(context).ownBank, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FundTransferBeneficiaryScreen( + creditAccountNo: creditAccountNo, + remitterName: remitterName, + isOwnBank: true, + ), + ), + ); + }, + ), + const Divider(height: 1), + FundTransferManagementTile( + icon: Symbols.output_circle, + // Restore localization for the label + label: AppLocalizations.of(context).outsideBank, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FundTransferBeneficiaryScreen( + creditAccountNo: creditAccountNo, + remitterName: remitterName, + isOwnBank: false, + ), + ), + ); + }, + ), + const Divider(height: 1), + ], + ); + }, + ), + ); + } + } -class FundTransferManagementTile extends StatelessWidget { - final IconData icon; - final String label; - final VoidCallback onTap; - final bool disable; + class FundTransferManagementTile extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onTap; + final bool disable; - const FundTransferManagementTile({ - super.key, - required this.icon, - required this.label, - required this.onTap, - this.disable = false, - }); + const FundTransferManagementTile({ + super.key, + required this.icon, + required this.label, + required this.onTap, + this.disable = false, + }); - @override - Widget build(BuildContext context) { - return ListTile( - leading: Icon(icon), - title: Text(label), - trailing: const Icon(Symbols.arrow_right, size: 20), - onTap: onTap, - enabled: !disable, - ); - } -} + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(icon), + title: Text(label), + trailing: const Icon(Symbols.arrow_right, size: 20), + onTap: onTap, + enabled: !disable, + ); + } + } \ No newline at end of file diff --git a/lib/features/fund_transfer/screens/fund_transfer_self_accounts_screen.dart b/lib/features/fund_transfer/screens/fund_transfer_self_accounts_screen.dart new file mode 100644 index 0000000..583fc89 --- /dev/null +++ b/lib/features/fund_transfer/screens/fund_transfer_self_accounts_screen.dart @@ -0,0 +1,94 @@ + import 'package:flutter/material.dart'; + import 'package:kmobile/data/models/user.dart'; + import 'package:kmobile/features/fund_transfer/screens/fund_transfer_self_amount_screen.dart'; + import 'package:kmobile/widgets/bank_logos.dart'; + + class FundTransferSelfAccountsScreen extends StatelessWidget { + final String debitAccountNo; + final String remitterName; + final List accounts; + + const FundTransferSelfAccountsScreen({ + super.key, + required this.debitAccountNo, + required this.remitterName, + required this.accounts, + }); + + // Helper function to get the full account type name from the short code + String _getFullAccountType(String? accountType) { + if (accountType == null || accountType.isEmpty) return 'N/A'; + switch (accountType.toLowerCase()) { + case 'sa': + case 'sb': + return "Savings Account"; + case 'ln': + return "Loan Account"; + case 'td': + return "Term Deposit"; + case 'rd': + return "Recurring Deposit"; + case 'ca': + return "Current Account"; + default: + return "Unknown Account"; + } + } + + @override + Widget build(BuildContext context) { + // Filter out the account from which the transfer is being made + final filteredAccounts = + accounts.where((acc) => acc.accountNo != debitAccountNo).toList(); + + return Scaffold( + appBar: AppBar( + title: const Text("Select Account"), + ), + body: filteredAccounts.isEmpty + ? const Center( + child: Text("No other accounts found"), + ) + : ListView.builder( + itemCount: filteredAccounts.length, + itemBuilder: (context, index) { + final account = filteredAccounts[index]; + return ListTile( + leading: CircleAvatar( + radius: 24, + backgroundColor: Colors.transparent, + child: + getBankLogo('Kangra Central Co-operative Bank', context), + ), + title: Text(account.name ?? 'N/A'), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(account.accountNo ?? 'N/A'), + Text( + _getFullAccountType(account.accountType), + style: + TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + ], + ), + onTap: () { + // Navigate to the amount screen, passing the selected User object directly. + // No Beneficiary object is created. + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FundTransferSelfAmountScreen( + debitAccountNo: debitAccountNo, + creditAccount: account, // Pass the User object + remitterName: remitterName, + ), + ), + ); + }, + ); + }, + ), + ); + } + } \ No newline at end of file diff --git a/lib/features/fund_transfer/screens/fund_transfer_self_amount_screen.dart b/lib/features/fund_transfer/screens/fund_transfer_self_amount_screen.dart new file mode 100644 index 0000000..6b4f1f1 --- /dev/null +++ b/lib/features/fund_transfer/screens/fund_transfer_self_amount_screen.dart @@ -0,0 +1,245 @@ + import 'package:flutter/material.dart'; + import 'package:intl/intl.dart'; + import 'package:kmobile/api/services/limit_service.dart'; + import 'package:kmobile/api/services/payment_service.dart'; + import 'package:kmobile/data/models/transfer.dart'; + import 'package:kmobile/data/models/user.dart'; + import 'package:kmobile/di/injection.dart'; + import 'package:kmobile/features/fund_transfer/screens/payment_animation.dart'; + import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart'; +import 'package:kmobile/widgets/bank_logos.dart'; + + class FundTransferSelfAmountScreen extends StatefulWidget { + final String debitAccountNo; + final User creditAccount; + final String remitterName; + + const FundTransferSelfAmountScreen({ + super.key, + required this.debitAccountNo, + required this.creditAccount, + required this.remitterName, + }); + + @override + State createState() => + _FundTransferSelfAmountScreenState(); + } + + class _FundTransferSelfAmountScreenState + extends State { + final _formKey = GlobalKey(); + final _amountController = TextEditingController(); + final _remarksController = TextEditingController(); + + // --- Limit Checking Variables --- + final _limitService = getIt(); + Limit? _limit; + bool _isLoadingLimit = true; + bool _isAmountOverLimit = false; + final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '₹'); + + + @override + void initState() { + super.initState(); + _loadLimit(); // Fetch the daily limit + _amountController.addListener(_checkAmountLimit); // Listen for amount changes + } + + @override + void dispose() { + _amountController.removeListener(_checkAmountLimit); + _amountController.dispose(); + _remarksController.dispose(); + super.dispose(); + } + + Future _loadLimit() async { + setState(() { + _isLoadingLimit = true; + }); + try { + final limitData = await _limitService.getLimit(); + setState(() { + _limit = limitData; + _isLoadingLimit = false; + }); + } catch (e) { + setState(() { + _isLoadingLimit = false; + }); + } + } + + void _checkAmountLimit() { + if (_limit == null) return; + + final amount = double.tryParse(_amountController.text) ?? 0; + final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit; + final bool isOverLimit = amount > remainingLimit; + + if (isOverLimit) { + WidgetsBinding.instance.addPostFrameCallback((_) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'), + backgroundColor: Colors.red, + ), + ); + }); + } + + if (_isAmountOverLimit != isOverLimit) { + setState(() { + _isAmountOverLimit = isOverLimit; + }); + } + } + + void _onProceed() { + if (_formKey.currentState!.validate()) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TransactionPinScreen( + onPinCompleted: (pinScreenContext, tpin) async { + final transfer = Transfer( + fromAccount: widget.debitAccountNo, + toAccount: widget.creditAccount.accountNo!, + toAccountType: 'Savings', // Assuming 'SB' for savings + amount: _amountController.text, + tpin: tpin, + ); + + final paymentService = getIt(); + final paymentResponseFuture = + paymentService.processQuickPayWithinBank(transfer); + + Navigator.of(pinScreenContext).pushReplacement( + MaterialPageRoute( + builder: (_) => + PaymentAnimationScreen(paymentResponse: paymentResponseFuture), + ), + ); + }, + ), + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Fund Transfer"), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Debit Account (User) + Text( + "Debit From", + style: Theme.of(context).textTheme.titleSmall, + ), + Card( + elevation: 0, + margin: const EdgeInsets.symmetric(vertical: 8.0), + child: ListTile( + leading: Image.asset( + 'assets/images/logo.png', + width: 40, + height: 40, + ), + title: Text(widget.remitterName), + subtitle: Text(widget.debitAccountNo), + ), + ), + const SizedBox(height: 24), + + // Credit Account (Self) + Text( + "Credited To", + style: Theme.of(context).textTheme.titleSmall, + ), + Card( + elevation: 0, + margin: const EdgeInsets.symmetric(vertical: 8.0), + child: ListTile( + leading: + getBankLogo('Kangra Central Co-operative Bank', context), + title: Text(widget.creditAccount.name ?? 'N/A'), + subtitle: Text(widget.creditAccount.accountNo ?? 'N/A'), + ), + ), + const SizedBox(height: 24), + + // Remarks + TextFormField( + controller: _remarksController, + decoration: const InputDecoration( + labelText: "Remarks (Optional)", + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 24), + + // Amount + TextFormField( + controller: _amountController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: "Amount", + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.currency_rupee), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return "Amount is required"; + } + if (double.tryParse(value) == null || + double.parse(value) <= 0) { + return "Please enter a valid amount"; + } + return null; + }, + ), + const SizedBox(height: 8), + + // Daily Limit Display + if (_isLoadingLimit) + const Text('Fetching daily limit...'), + if (!_isLoadingLimit && _limit != null) + Text( + 'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}', + style: Theme.of(context).textTheme.bodySmall, + ), + const Spacer(), + + // Proceed Button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isAmountOverLimit ? null : _onProceed, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: const Text("Proceed"), + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ), + ); + } + }