first commit
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
// File: add_edit_new_bank.dart
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/bank%20account/repo/bank_account_repo.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
// --- Local Imports ---
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
|
||||
import 'model/bank_account_list_model.dart';
|
||||
|
||||
// Accept optional BankData for editing
|
||||
class AddEditNewBank extends ConsumerStatefulWidget {
|
||||
final BankData? bankData;
|
||||
const AddEditNewBank({super.key, this.bankData});
|
||||
|
||||
@override
|
||||
ConsumerState<AddEditNewBank> createState() => _AddEditNewBankState();
|
||||
}
|
||||
|
||||
class _AddEditNewBankState extends ConsumerState<AddEditNewBank> {
|
||||
final _key = GlobalKey<FormState>();
|
||||
|
||||
// Core fields
|
||||
final nameController = TextEditingController(); // Account Display Name
|
||||
final openingBalanceController = TextEditingController();
|
||||
final asOfDateController = TextEditingController();
|
||||
|
||||
// Meta fields
|
||||
final accNumberController = TextEditingController();
|
||||
final ifscController = TextEditingController();
|
||||
final upiController = TextEditingController();
|
||||
final bankNameController = TextEditingController();
|
||||
final accHolderController = TextEditingController();
|
||||
|
||||
// State
|
||||
bool _showMoreFields = false;
|
||||
bool _showInInvoice = false;
|
||||
DateTime? _selectedDate;
|
||||
|
||||
// Date formats
|
||||
final DateFormat _displayFormat = DateFormat('dd/MM/yyyy');
|
||||
final DateFormat _apiFormat = DateFormat('yyyy-MM-dd', 'en_US');
|
||||
|
||||
bool get isEditing => widget.bankData != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!isEditing) {
|
||||
_selectedDate = DateTime.now();
|
||||
asOfDateController.text = _displayFormat.format(_selectedDate!);
|
||||
} else {
|
||||
_loadInitialData();
|
||||
}
|
||||
}
|
||||
|
||||
void _loadInitialData() {
|
||||
final data = widget.bankData!;
|
||||
nameController.text = data.name ?? '';
|
||||
openingBalanceController.text = data.openingBalance?.toString() ?? '';
|
||||
_showInInvoice = data.showInInvoice == 1;
|
||||
|
||||
if (data.openingDate != null) {
|
||||
try {
|
||||
_selectedDate = DateTime.parse(data.openingDate!);
|
||||
asOfDateController.text = _displayFormat.format(_selectedDate!);
|
||||
} catch (_) {
|
||||
asOfDateController.text = data.openingDate!;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.meta != null) {
|
||||
_showMoreFields = true;
|
||||
accNumberController.text = data.meta!.accountNumber ?? '';
|
||||
ifscController.text = data.meta!.ifscCode ?? '';
|
||||
upiController.text = data.meta!.upiId ?? '';
|
||||
bankNameController.text = data.meta!.bankName ?? '';
|
||||
accHolderController.text = data.meta!.accountHolder ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
openingBalanceController.dispose();
|
||||
asOfDateController.dispose();
|
||||
accNumberController.dispose();
|
||||
ifscController.dispose();
|
||||
upiController.dispose();
|
||||
bankNameController.dispose();
|
||||
accHolderController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _selectDate(BuildContext context) async {
|
||||
DateTime initialDate = _selectedDate ?? DateTime.now();
|
||||
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: initialDate,
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_selectedDate = picked;
|
||||
asOfDateController.text = _displayFormat.format(picked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- Submission Logic ---
|
||||
void _submit() async {
|
||||
if (!_key.currentState!.validate() || _selectedDate == null) {
|
||||
if (_selectedDate == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('As of Date is required.')));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final repo = BankRepo();
|
||||
final apiOpeningDate = _apiFormat.format(_selectedDate!);
|
||||
final apiShowInInvoice = _showInInvoice ? 1 : 0;
|
||||
|
||||
final meta = BankMeta(
|
||||
accountNumber: accNumberController.text.trim(),
|
||||
ifscCode: ifscController.text.trim(),
|
||||
upiId: upiController.text.trim(),
|
||||
bankName: bankNameController.text.trim(),
|
||||
accountHolder: accHolderController.text.trim(),
|
||||
);
|
||||
|
||||
if (isEditing) {
|
||||
await repo.updateBank(
|
||||
ref: ref,
|
||||
context: context,
|
||||
id: widget.bankData!.id!,
|
||||
name: nameController.text,
|
||||
openingBalance: num.tryParse(openingBalanceController.text) ?? 0,
|
||||
openingDate: apiOpeningDate,
|
||||
showInInvoice: apiShowInInvoice,
|
||||
meta: meta,
|
||||
);
|
||||
} else {
|
||||
await repo.createBank(
|
||||
ref: ref,
|
||||
context: context,
|
||||
name: nameController.text,
|
||||
openingBalance: num.tryParse(openingBalanceController.text) ?? 0,
|
||||
openingDate: apiOpeningDate,
|
||||
showInInvoice: apiShowInInvoice,
|
||||
meta: meta,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reset/Cancel Logic ---
|
||||
void _resetOrCancel() {
|
||||
if (isEditing) {
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
setState(() {
|
||||
_key.currentState?.reset();
|
||||
nameController.clear();
|
||||
openingBalanceController.clear();
|
||||
accNumberController.clear();
|
||||
ifscController.clear();
|
||||
upiController.clear();
|
||||
bankNameController.clear();
|
||||
accHolderController.clear();
|
||||
_showInInvoice = false;
|
||||
_showMoreFields = false;
|
||||
_selectedDate = DateTime.now();
|
||||
asOfDateController.text = _displayFormat.format(_selectedDate!);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
final _lang = l.S.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
isEditing ? _lang.editBankAccounts : _lang.addNewBankAccounts,
|
||||
),
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close)),
|
||||
],
|
||||
bottom: const PreferredSize(
|
||||
preferredSize: Size.fromHeight(1),
|
||||
child: Divider(height: 2, color: kBackgroundColor),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _key,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// --- Row 1: Account Display Name ---
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.accountDisplayName,
|
||||
hintText: _lang.enterAccountDisplayName,
|
||||
),
|
||||
validator: (value) => value!.isEmpty ? _lang.displayNameIsRequired : null,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// --- Row 2: Balance, Date (Max 2 fields) ---
|
||||
TextFormField(
|
||||
controller: openingBalanceController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.openingBalance,
|
||||
hintText: 'Ex: 500',
|
||||
prefixText: currency,
|
||||
),
|
||||
validator: (value) => value!.isEmpty ? _lang.openingBalanceIsRequired : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
readOnly: true,
|
||||
controller: asOfDateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.asOfDate,
|
||||
hintText: 'DD/MM/YYYY',
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
onPressed: () => _selectDate(context),
|
||||
),
|
||||
),
|
||||
validator: (value) => value!.isEmpty ? _lang.dateIsRequired : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// --- Toggle More Fields Button ---
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_showMoreFields = !_showMoreFields;
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text(
|
||||
_showMoreFields ? '- ${_lang.hideFiled}' : '+ ${_lang.addMoreFiled}',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kSuccessColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// --- Extra Fields (Meta Data) ---
|
||||
if (_showMoreFields)
|
||||
Column(
|
||||
children: [
|
||||
// Row 3: Account Number, IFSC
|
||||
TextFormField(
|
||||
controller: accNumberController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.accountNumber,
|
||||
hintText: _lang.enterAccountName,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
controller: ifscController,
|
||||
decoration: InputDecoration(labelText: _lang.ifscCode, hintText: 'Ex: DBBL0001234'),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 4: UPI, Bank Name
|
||||
TextFormField(
|
||||
controller: upiController,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.upiIdForQrCode,
|
||||
hintText: 'yourname@upi',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
controller: bankNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.bankName,
|
||||
hintText: _lang.enterBankName,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 5: Account Holder (Single field)
|
||||
TextFormField(
|
||||
controller: accHolderController,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.accountHolderName,
|
||||
hintText: _lang.enterAccountHolderName,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
|
||||
// --- Show in Invoice Checkbox ---
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
checkboxTheme: CheckboxThemeData(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
side: const BorderSide(color: Colors.grey, width: 1),
|
||||
),
|
||||
),
|
||||
child: Checkbox(
|
||||
value: _showInInvoice,
|
||||
onChanged: (value) {
|
||||
setState(() => _showInInvoice = value ?? false);
|
||||
},
|
||||
activeColor: Colors.blue, // your kMainColor
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: _lang.printBankDetailsAndInvoice,
|
||||
style: const TextStyle(color: Colors.black),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
setState(() => _showInInvoice = !_showInInvoice);
|
||||
},
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 18,
|
||||
color: Colors.grey, // your kGreyTextColor
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: _resetOrCancel,
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
side: const BorderSide(color: Colors.red),
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
child: Text(isEditing ? _lang.cancel : _lang.resets),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
backgroundColor: const Color(0xFFB71C1C),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: Text(_lang.save),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,550 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:hugeicons/hugeicons.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/bank%20account/provider/bank_account_provider.dart';
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/bank%20account/repo/bank_account_repo.dart';
|
||||
|
||||
// --- Local Imports ---
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/widgets/model_bottom_sheet.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/widgets/global_search_appbar.dart';
|
||||
import '../../../service/check_user_role_permission_provider.dart';
|
||||
import '../../../widgets/empty_widget/_empty_widget.dart';
|
||||
import '../../hrm/widgets/deleteing_alart_dialog.dart';
|
||||
import '../adjust bank balance/adjust_bank_balance_screen.dart';
|
||||
import '../bank to bank transfer/bank_to_bank_transfer_screen.dart';
|
||||
import '../bank to cash transfer/bank_to_cash_transfer.dart';
|
||||
import 'add_edit_new_bank_account_screen.dart';
|
||||
import 'bank_transfer_history_screen.dart';
|
||||
import 'model/bank_account_list_model.dart';
|
||||
|
||||
class BankAccountListScreen extends ConsumerStatefulWidget {
|
||||
const BankAccountListScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<BankAccountListScreen> createState() => _BankAccountListScreenState();
|
||||
}
|
||||
|
||||
class _BankAccountListScreenState extends ConsumerState<BankAccountListScreen> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
String _searchQuery = '';
|
||||
bool _isSearch = false;
|
||||
|
||||
List<BankData> _filteredList = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.addListener(_applyFilters);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- Date Formatting Utility ---
|
||||
String _formatDateForDisplay(String? date) {
|
||||
if (date == null || date.isEmpty) return 'N/A';
|
||||
try {
|
||||
final dateTime = DateFormat('yyyy-MM-dd').parse(date);
|
||||
return DateFormat('dd MMM, yyyy').format(dateTime);
|
||||
} catch (_) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
// --- END Date Formatting Utility ---
|
||||
|
||||
void _applyFilters() {
|
||||
setState(() {
|
||||
_searchQuery = _searchController.text;
|
||||
});
|
||||
}
|
||||
|
||||
void _filterBanks(List<BankData> allBanks) {
|
||||
final query = _searchQuery.toLowerCase().trim();
|
||||
if (query.isEmpty) {
|
||||
_filteredList = allBanks;
|
||||
} else {
|
||||
_filteredList = allBanks.where((bank) {
|
||||
final name = (bank.name ?? '').toLowerCase();
|
||||
final bankName = (bank.meta?.bankName ?? '').toLowerCase();
|
||||
final accNumber = (bank.meta?.accountNumber ?? '').toLowerCase();
|
||||
final holderName = (bank.meta?.accountHolder ?? '').toLowerCase();
|
||||
|
||||
return name.contains(query) ||
|
||||
bankName.contains(query) ||
|
||||
accNumber.contains(query) ||
|
||||
holderName.contains(query);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// --- CRITICAL FIX 1: NAVIGATION AND ACTION METHODS ---
|
||||
void _navigateToEdit(BankData bank) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => AddEditNewBank(bankData: bank)),
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToTransactions(BankData bank) {
|
||||
// Placeholder for navigating to transactions screen
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('${l.S.of(context).viewingTransactionFor} ${bank.name}')),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteConfirmationDialog(num id, String name) async {
|
||||
bool result = await showDeleteConfirmationDialog(context: context, itemName: name);
|
||||
if (result) {
|
||||
final repo = BankRepo();
|
||||
await repo.deleteBank(id: id, context: context, ref: ref);
|
||||
// Repo handles invalidate(bankListProvider)
|
||||
}
|
||||
}
|
||||
// --- END FIX 1 ---
|
||||
|
||||
// --- Pull to Refresh ---
|
||||
Future<void> _refreshData() async {
|
||||
ref.invalidate(bankListProvider);
|
||||
return ref.watch(bankListProvider.future);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = l.S.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final bankListAsync = ref.watch(bankListProvider);
|
||||
final permissionService = PermissionService(ref); // Assuming this is defined
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: GlobalSearchAppBar(
|
||||
isSearch: _isSearch,
|
||||
onSearchToggle: () {
|
||||
setState(() {
|
||||
_isSearch = !_isSearch;
|
||||
if (!_isSearch) {
|
||||
_searchController.clear();
|
||||
}
|
||||
});
|
||||
},
|
||||
title: _lang.bankAccounts,
|
||||
controller: _searchController,
|
||||
onChanged: (query) {
|
||||
// Handled by _searchController.addListener
|
||||
},
|
||||
),
|
||||
body: bankListAsync.when(
|
||||
data: (model) {
|
||||
// Check read permission (Assuming 'bank_read_permit' exists)
|
||||
if (!permissionService.hasPermission('bank_read_permit')) {
|
||||
return const Center(child: PermitDenyWidget()); // Assuming PermitDenyWidget exists
|
||||
}
|
||||
final allBanks = model.data ?? [];
|
||||
|
||||
_filterBanks(allBanks);
|
||||
|
||||
if (_filteredList.isEmpty) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Text(
|
||||
_searchController.text.isEmpty
|
||||
? _lang.noBankAccountFound
|
||||
: '${_lang.noAccountsFoundMissing} "${_searchController.text}".',
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: _filteredList.length,
|
||||
itemBuilder: (_, index) => _buildBankItem(
|
||||
context: context,
|
||||
ref: ref,
|
||||
bank: _filteredList[index],
|
||||
),
|
||||
separatorBuilder: (_, __) => const Divider(
|
||||
color: kLineColor,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (err, stack) => Center(child: Text('Failed to load bank accounts: $err')),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
bottomNavigationBar: permissionService.hasPermission('bank_create_permit')
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
spacing: 16,
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _depositPopUp(context),
|
||||
child: Text(_lang.deposit),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AddEditNewBank(),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.add, color: Colors.white),
|
||||
label: Text(_lang.addBank),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
//------Deposit/Withdraw Popup-------------------
|
||||
void _depositPopUp(BuildContext context) {
|
||||
final _lang = l.S.of(context);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: InkWell(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(vertical: -2, horizontal: -2),
|
||||
leading: SvgPicture.asset(
|
||||
'assets/bank.svg',
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
title: Text(_lang.bankToBankTransfer),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => BankToBankTransferScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(vertical: -2, horizontal: -2),
|
||||
leading: SvgPicture.asset(
|
||||
'assets/bank_cash.svg',
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
title: Text(_lang.bankToCashTransfer),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => BankToCashTransferScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(vertical: -2, horizontal: -2),
|
||||
leading: SvgPicture.asset(
|
||||
'assets/bank_adjust.svg',
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
title: Text(_lang.adjustBankBalance),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => AdjustBankBalanceScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// --- List Item Builder ---
|
||||
|
||||
Widget _buildBankItem({
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
required BankData bank,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
final _lang = l.S.of(context);
|
||||
final bankMeta = bank.meta;
|
||||
final balanceDisplay = '$currency${bank.balance?.toStringAsFixed(2) ?? '0.00'}';
|
||||
final accountName = bank.name ?? 'N/A';
|
||||
final bankName = bankMeta?.bankName ?? 'N/A Bank';
|
||||
|
||||
return InkWell(
|
||||
onTap: () => viewModalSheet(
|
||||
context: context,
|
||||
item: {
|
||||
_lang.accountName: accountName,
|
||||
_lang.accountNumber: bankMeta?.accountNumber ?? 'N/A',
|
||||
_lang.bankName: bankName,
|
||||
_lang.holderName: bankMeta?.accountHolder ?? 'N/A',
|
||||
_lang.openingDate: _formatDateForDisplay(bank.openingDate),
|
||||
},
|
||||
descriptionTitle: '${_lang.currentBalance}:',
|
||||
description: balanceDisplay,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(horizontal: -4),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
accountName,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
balanceDisplay,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: kSuccessColor,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
bankName,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kGrey6,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
bankMeta?.accountNumber ?? 'N/A',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kGrey6,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: _buildActionButtons(context, ref, bank),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeColumn({
|
||||
required String time,
|
||||
required String label,
|
||||
required ThemeData theme,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
time,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kNeutral800,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(BuildContext context, WidgetRef ref, BankData bank) {
|
||||
final _theme = Theme.of(context);
|
||||
final _lang = l.S.of(context);
|
||||
final bankMeta = bank.meta;
|
||||
final balanceDisplay = '$currency${bank.balance?.toStringAsFixed(2) ?? '0.00'}';
|
||||
final accountName = bank.name ?? 'N/A';
|
||||
final bankName = bankMeta?.bankName ?? 'N/A Bank';
|
||||
final permissionService = PermissionService(ref);
|
||||
return SizedBox(
|
||||
width: 20,
|
||||
child: PopupMenuButton<String>(
|
||||
padding: EdgeInsets.zero,
|
||||
onSelected: (value) {
|
||||
if (bank.id == null) return;
|
||||
if (value == 'view') {
|
||||
if (!permissionService.hasPermission('bank_view_permit')) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(_lang.permissionDeniedToViewBank)));
|
||||
return;
|
||||
}
|
||||
viewModalSheet(
|
||||
context: context,
|
||||
item: {
|
||||
_lang.accountName: accountName,
|
||||
_lang.accountNumber: bankMeta?.accountNumber ?? 'N/A',
|
||||
_lang.bankName: bankName,
|
||||
_lang.holderName: bankMeta?.accountHolder ?? 'N/A',
|
||||
_lang.openingDate: _formatDateForDisplay(bank.openingDate),
|
||||
},
|
||||
descriptionTitle: '${_lang.currentBalance}:',
|
||||
description: balanceDisplay,
|
||||
);
|
||||
} else if (value == 'edit') {
|
||||
if (!permissionService.hasPermission('bank_update_permit')) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(_lang.permissionDeniedToUpdateBank)));
|
||||
return;
|
||||
}
|
||||
_navigateToEdit(bank);
|
||||
} else if (value == 'transactions') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BankTransactionHistoryScreen(
|
||||
accountName: bank.name ?? '',
|
||||
accountNumber: '',
|
||||
bankId: bank.id ?? 0,
|
||||
currentBalance: bank.balance ?? 0,
|
||||
bank: bank,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (value == 'delete') {
|
||||
if (!permissionService.hasPermission('bank_delete_permit')) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(_lang.permissionDeniedToDeleteBank)));
|
||||
return;
|
||||
}
|
||||
_showDeleteConfirmationDialog(bank.id!, bank.name ?? _lang.bankAccounts);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'view',
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedView,
|
||||
color: kPeraColor,
|
||||
size: 20,
|
||||
),
|
||||
Text(
|
||||
_lang.view,
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
PopupMenuItem(
|
||||
value: 'transactions',
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedMoneyExchange02,
|
||||
color: kPeraColor,
|
||||
size: 20,
|
||||
),
|
||||
Text(
|
||||
_lang.transactions,
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedPencilEdit02,
|
||||
color: kPeraColor,
|
||||
size: 20,
|
||||
),
|
||||
Text(
|
||||
_lang.edit,
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedDelete03,
|
||||
color: kPeraColor,
|
||||
size: 20,
|
||||
),
|
||||
Text(
|
||||
_lang.delete,
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
icon: const Icon(
|
||||
Icons.more_vert,
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
// File: bank_transaction_history_screen.dart (Final Fixed Code)
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
// --- Local Imports ---
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/widgets/model_bottom_sheet.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
// --- Data Layer Imports ---
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/bank%20account/provider/bank_account_provider.dart';
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/bank%20account/provider/bank_transfers_history_provider.dart';
|
||||
import '../../hrm/widgets/deleteing_alart_dialog.dart';
|
||||
import '../adjust%20bank%20balance/adjust_bank_balance_screen.dart';
|
||||
import '../bank%20to%20cash%20transfer/bank_to_cash_transfer.dart';
|
||||
import '../bank%20to%20bank%20transfer/bank_to_bank_transfer_screen.dart';
|
||||
import '../bank%20to%20bank%20transfer/repo/bank_to_bank_transfar_repo.dart';
|
||||
import '../widgets/cheques_filter_search.dart'; // Reusable Filter Widget
|
||||
import 'model/bank_account_list_model.dart';
|
||||
import 'model/bank_transfer_history_model.dart';
|
||||
|
||||
// 🔔 Filter State Model (Must match the data returned by ChequesFilterSearch)
|
||||
class BankFilterState {
|
||||
final String searchQuery;
|
||||
final DateTime? fromDate;
|
||||
final DateTime? toDate;
|
||||
|
||||
BankFilterState({
|
||||
required this.searchQuery,
|
||||
this.fromDate,
|
||||
this.toDate,
|
||||
});
|
||||
}
|
||||
|
||||
class BankTransactionHistoryScreen extends ConsumerStatefulWidget {
|
||||
final num bankId;
|
||||
final String accountName;
|
||||
final String accountNumber;
|
||||
final num currentBalance;
|
||||
final BankData bank;
|
||||
|
||||
const BankTransactionHistoryScreen({
|
||||
super.key,
|
||||
required this.bankId,
|
||||
required this.accountName,
|
||||
required this.accountNumber,
|
||||
required this.currentBalance,
|
||||
required this.bank,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<BankTransactionHistoryScreen> createState() => _BankTransactionHistoryScreenState();
|
||||
}
|
||||
|
||||
class _BankTransactionHistoryScreenState extends ConsumerState<BankTransactionHistoryScreen> {
|
||||
// Local states to hold filter values from the child widget
|
||||
String _currentSearchQuery = '';
|
||||
DateTime? _currentFromDate;
|
||||
DateTime? _currentToDate;
|
||||
|
||||
final DateFormat _displayFormat = DateFormat('dd/MM/yyyy');
|
||||
final DateFormat _apiFormat = DateFormat('yyyy-MM-dd');
|
||||
|
||||
final List<String> _timeFilterOptions = [
|
||||
'Today',
|
||||
'Yesterday',
|
||||
'Last 7 Days',
|
||||
'Last 30 Days',
|
||||
'Current Month',
|
||||
'Last Month',
|
||||
'Current Year',
|
||||
'Custom Date'
|
||||
];
|
||||
|
||||
// FIX: Helper to initialize filter dates
|
||||
void _updateInitialDateRange() {
|
||||
final now = DateTime.now();
|
||||
// Default to Current Year
|
||||
_currentFromDate = DateTime(now.year, 1, 1);
|
||||
// End of today for inclusive filtering
|
||||
_currentToDate = DateTime(now.year, now.month, now.day, 23, 59, 59);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 🔔 FIX: Initialize filter date range right away
|
||||
_updateInitialDateRange();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _formatDate(String? date) {
|
||||
if (date == null) return 'N/A';
|
||||
try {
|
||||
return DateFormat('dd MMM, yyyy').format(DateTime.parse(date));
|
||||
} catch (_) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
// --- DELETE Logic ---
|
||||
|
||||
Future<void> _confirmAndDeleteTransaction(TransactionData transaction) async {
|
||||
final transactionId = transaction.id;
|
||||
if (transactionId == null) return;
|
||||
|
||||
final confirmed = await showDeleteConfirmationDialog(
|
||||
context: context,
|
||||
itemName: 'transaction',
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
final repo = BankTransactionRepo();
|
||||
await repo.deleteBankTransaction(
|
||||
ref: ref,
|
||||
context: context,
|
||||
transactionId: transactionId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Filter Callback Handler ---
|
||||
void _handleFilterChange(BankFilterState filterState) {
|
||||
setState(() {
|
||||
_currentSearchQuery = filterState.searchQuery;
|
||||
_currentFromDate = filterState.fromDate;
|
||||
_currentToDate = filterState.toDate;
|
||||
});
|
||||
}
|
||||
|
||||
// --- LOCAL FILTERING FUNCTION (with robust date checks) ---
|
||||
List<TransactionData> _filterTransactionsLocally(List<TransactionData> transactions) {
|
||||
// 1. Filter by Date Range
|
||||
Iterable<TransactionData> dateFiltered = transactions.where((t) {
|
||||
if (_currentFromDate == null && _currentToDate == null) return true;
|
||||
if (t.date == null) return false;
|
||||
|
||||
try {
|
||||
final transactionDate = DateTime.parse(t.date!);
|
||||
|
||||
final start = _currentFromDate;
|
||||
final end = _currentToDate;
|
||||
|
||||
bool afterStart = start == null || transactionDate.isAfter(start) || transactionDate.isAtSameMomentAs(start);
|
||||
bool beforeEnd = end == null || transactionDate.isBefore(end) || transactionDate.isAtSameMomentAs(end);
|
||||
|
||||
return afterStart && beforeEnd;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Filter by Search Query
|
||||
final query = _currentSearchQuery.toLowerCase();
|
||||
if (query.isEmpty) {
|
||||
return dateFiltered.toList();
|
||||
}
|
||||
|
||||
return dateFiltered.where((t) {
|
||||
return (t.transactionType ?? '').toLowerCase().contains(query) ||
|
||||
(t.user?.name ?? '').toLowerCase().contains(query) ||
|
||||
(t.amount?.toString() ?? '').contains(query) ||
|
||||
(t.invoiceNo ?? '').toLowerCase().contains(query);
|
||||
}).toList();
|
||||
}
|
||||
// --- END LOCAL FILTERING FUNCTION ---
|
||||
|
||||
// --- Core Logic Helpers (Unchanged) ---
|
||||
|
||||
String _getBankNameById(num? id, List<BankData> banks) {
|
||||
if (id == null) return 'Cash/System';
|
||||
final bank = banks.firstWhere((b) => b.id == id, orElse: () => BankData(name: 'Bank ID $id', id: id));
|
||||
return bank.name ?? 'Bank ID $id';
|
||||
}
|
||||
|
||||
String _getListName(TransactionData t, List<BankData> banks) {
|
||||
final nameFromUser = t.user?.name ?? 'System';
|
||||
|
||||
if (t.transactionType == 'bank_to_bank') {
|
||||
if (t.fromBankId != widget.bankId) {
|
||||
return 'From: ${_getBankNameById(t.fromBankId, banks)}';
|
||||
} else if (t.toBankId != widget.bankId) {
|
||||
return 'To: ${_getBankNameById(t.toBankId, banks)}';
|
||||
}
|
||||
return 'Internal Transfer';
|
||||
} else if (t.transactionType == 'bank_to_cash') {
|
||||
return 'To: Cash';
|
||||
} else if (t.transactionType == 'adjust_bank') {
|
||||
return t.type == 'credit' ? 'Adjustment (Credit)' : 'Adjustment (Debit)';
|
||||
}
|
||||
return nameFromUser;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getAmountDetails(TransactionData t) {
|
||||
bool isOutgoing = false;
|
||||
|
||||
if (t.transactionType == 'adjust_bank') {
|
||||
isOutgoing = t.type == 'debit';
|
||||
} else if (t.transactionType == 'bank_to_bank' || t.transactionType == 'bank_to_cash') {
|
||||
isOutgoing = t.fromBankId == widget.bankId;
|
||||
}
|
||||
|
||||
final color = isOutgoing ? Colors.red.shade700 : Colors.green.shade700;
|
||||
final sign = isOutgoing ? '-' : '+';
|
||||
|
||||
return {'sign': sign, 'color': color};
|
||||
}
|
||||
|
||||
// --- UI Builders ---
|
||||
|
||||
Widget _buildBalanceCard(ThemeData theme) {
|
||||
final _lang = l.S.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Container(
|
||||
height: 77,
|
||||
width: double.infinity,
|
||||
color: kSuccessColor.withValues(alpha: 0.1),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'$currency${widget.currentBalance.toStringAsFixed(2)}',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_lang.balance,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: kSubPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionMenu(TransactionData transaction, List<BankData> allBanks) {
|
||||
// Helper to compile details for the view modal
|
||||
Map<String, String> _compileDetails() {
|
||||
final details = <String, String>{};
|
||||
details['Transaction Type'] = (transaction.transactionType ?? 'N/A').replaceAll('_', ' ').toUpperCase();
|
||||
details['Date'] = _formatDate(transaction.date);
|
||||
details['Amount'] = '$currency${transaction.amount?.toStringAsFixed(2) ?? '0.00'}';
|
||||
details['User'] = transaction.user?.name ?? 'System';
|
||||
details['Invoice No'] = transaction.invoiceNo ?? 'N/A';
|
||||
details['Note'] = transaction.note ?? 'No Note';
|
||||
|
||||
if (transaction.transactionType == 'bank_to_bank') {
|
||||
details['From Account'] = _getBankNameById(transaction.fromBankId, allBanks);
|
||||
details['To Account'] = _getBankNameById(transaction.toBankId, allBanks);
|
||||
} else if (transaction.transactionType == 'bank_to_cash') {
|
||||
details['From Account'] = _getBankNameById(transaction.fromBankId, allBanks);
|
||||
details['To'] = 'Cash';
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
return PopupMenuButton<String>(
|
||||
onSelected: (value) {
|
||||
if (transaction.id == null) return;
|
||||
|
||||
if (value == 'view') {
|
||||
// *** VIEW IMPLEMENTATION ***
|
||||
viewModalSheet(
|
||||
context: context,
|
||||
item: _compileDetails(),
|
||||
descriptionTitle: '${l.S.of(context).description}:',
|
||||
description: transaction.note ?? 'N/A',
|
||||
);
|
||||
} else if (value == 'edit') {
|
||||
// --- Determine the Destination Screen based on transaction_type ---
|
||||
Widget destinationScreen;
|
||||
|
||||
switch (transaction.transactionType) {
|
||||
case 'bank_to_bank':
|
||||
destinationScreen = BankToBankTransferScreen(transaction: transaction);
|
||||
break;
|
||||
case 'bank_to_cash':
|
||||
destinationScreen = BankToCashTransferScreen(transaction: transaction);
|
||||
break;
|
||||
case 'adjust_bank':
|
||||
destinationScreen = AdjustBankBalanceScreen(
|
||||
transaction: transaction,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(l.S.of(context).canNotEditThisTransactionType)));
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => destinationScreen));
|
||||
} else if (value == 'delete') {
|
||||
// *** DELETE IMPLEMENTATION - Call the confirmation dialog ***
|
||||
_confirmAndDeleteTransaction(transaction);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(value: 'view', child: Text(l.S.of(context).view)),
|
||||
PopupMenuItem(value: 'edit', child: Text(l.S.of(context).edit)),
|
||||
PopupMenuItem(value: 'delete', child: Text(l.S.of(context).delete, style: TextStyle(color: Colors.red))),
|
||||
],
|
||||
icon: const Icon(Icons.more_vert, color: kNeutral800),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = l.S.of(context);
|
||||
final bankMeta = widget.bank.meta;
|
||||
final bankName = bankMeta?.bankName ?? 'N/A ${_lang.bank}';
|
||||
final theme = Theme.of(context);
|
||||
final historyAsync = ref.watch(bankTransactionHistoryProvider(widget.bankId));
|
||||
final banksListAsync = ref.watch(bankListProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
title: Text(
|
||||
widget.accountName,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
bankName,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kGrey6,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () => ref.refresh(bankTransactionHistoryProvider(widget.bankId).future),
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 1. Balance and Account Info Card
|
||||
_buildBalanceCard(theme),
|
||||
// // 2. Filters and Search (Using Reusable Widget)
|
||||
// ChequesFilterSearch(
|
||||
// displayFormat: _displayFormat, // Use local display format
|
||||
// timeOptions: _timeFilterOptions,
|
||||
// onFilterChanged: (filterState) {
|
||||
// // Cast the dynamic output to the expected BankFilterState
|
||||
// _handleFilterChange(filterState as BankFilterState);
|
||||
// },
|
||||
// ),
|
||||
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
height: 42,
|
||||
width: double.infinity,
|
||||
color: kBackgroundColor,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_lang.transactions,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_lang.amount,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 3. Transaction List
|
||||
banksListAsync.when(
|
||||
loading: () =>
|
||||
const Center(child: Padding(padding: EdgeInsets.all(20), child: CircularProgressIndicator())),
|
||||
error: (e, s) => Center(child: Text('Error loading bank data: ${e.toString()}')),
|
||||
data: (bankModel) {
|
||||
final allBanks = bankModel.data ?? []; // List for lookup
|
||||
|
||||
return historyAsync.when(
|
||||
loading: () =>
|
||||
const Center(child: Padding(padding: EdgeInsets.all(20), child: CircularProgressIndicator())),
|
||||
error: (err, stack) => Center(child: Text('Error: ${err.toString()}')),
|
||||
data: (model) {
|
||||
final allTransactions = model.data ?? [];
|
||||
|
||||
// Apply local date and search filtering
|
||||
final filteredTransactions = _filterTransactionsLocally(allTransactions);
|
||||
|
||||
if (filteredTransactions.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: Text(
|
||||
_lang.noTransactionFoundForThisFilter,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: filteredTransactions.length,
|
||||
separatorBuilder: (_, __) => const Divider(color: kLineColor, height: 1),
|
||||
itemBuilder: (_, index) {
|
||||
final transaction = filteredTransactions[index];
|
||||
final amountDetails = _getAmountDetails(transaction);
|
||||
|
||||
return ListTile(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
(transaction.transactionType ?? 'N/A').replaceAll('_', ' ').toUpperCase(),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$currency${transaction.amount?.toStringAsFixed(2) ?? '0.00'}',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: amountDetails['color'],
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_formatDate(transaction.date),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kGrey6,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
transaction.platform.toString(),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kGrey6,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// File: bank_account_model.dart
|
||||
|
||||
class BankListModel {
|
||||
BankListModel({this.message, this.data});
|
||||
|
||||
BankListModel.fromJson(dynamic json) {
|
||||
message = json['message'];
|
||||
if (json['data'] != null) {
|
||||
data = [];
|
||||
json['data'].forEach((v) {
|
||||
data?.add(BankData.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
String? message;
|
||||
List<BankData>? data;
|
||||
}
|
||||
|
||||
class BankData {
|
||||
BankData({
|
||||
this.id,
|
||||
this.name, // Account Display Name
|
||||
this.meta,
|
||||
this.showInInvoice,
|
||||
this.openingDate,
|
||||
this.openingBalance,
|
||||
this.balance,
|
||||
this.status,
|
||||
});
|
||||
|
||||
BankData.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
meta = json['meta'] != null ? BankMeta.fromJson(json['meta']) : null;
|
||||
showInInvoice = json['show_in_invoice'];
|
||||
openingDate = json['opening_date'];
|
||||
openingBalance = json['opening_balance'];
|
||||
balance = json['balance'];
|
||||
status = json['status'];
|
||||
}
|
||||
num? id;
|
||||
String? name;
|
||||
BankMeta? meta;
|
||||
num? showInInvoice;
|
||||
String? openingDate;
|
||||
num? openingBalance;
|
||||
num? balance;
|
||||
num? status;
|
||||
}
|
||||
|
||||
class BankMeta {
|
||||
BankMeta({
|
||||
this.accountNumber,
|
||||
this.ifscCode,
|
||||
this.upiId,
|
||||
this.bankName,
|
||||
this.accountHolder,
|
||||
});
|
||||
|
||||
BankMeta.fromJson(dynamic json) {
|
||||
accountNumber = json['account_number'];
|
||||
ifscCode = json['ifsc_code'];
|
||||
upiId = json['upi_id'];
|
||||
bankName = json['bank_name'];
|
||||
accountHolder = json['account_holder'];
|
||||
}
|
||||
String? accountNumber;
|
||||
String? ifscCode;
|
||||
String? upiId;
|
||||
String? bankName;
|
||||
String? accountHolder;
|
||||
|
||||
// Helper method to convert back to API format (meta fields are sent as separate inputs)
|
||||
Map<String, dynamic> toApiMetaJson() {
|
||||
return {
|
||||
'account_number': accountNumber,
|
||||
'ifsc_code': ifscCode,
|
||||
'upi_id': upiId,
|
||||
'bank_name': bankName,
|
||||
'account_holder': accountHolder,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// File: bank_transaction_history_model.dart (Updated to full structure)
|
||||
|
||||
class TransactionHistoryListModel {
|
||||
TransactionHistoryListModel({this.message, this.data});
|
||||
|
||||
TransactionHistoryListModel.fromJson(dynamic json) {
|
||||
message = json['message'];
|
||||
if (json['data'] != null) {
|
||||
data = [];
|
||||
json['data'].forEach((v) {
|
||||
data?.add(TransactionData.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
String? message;
|
||||
List<TransactionData>? data;
|
||||
}
|
||||
|
||||
class TransactionData {
|
||||
TransactionData({
|
||||
this.id,
|
||||
this.platform,
|
||||
this.transactionType,
|
||||
this.type, // credit / debit / transfer
|
||||
this.amount,
|
||||
this.date,
|
||||
this.fromBankId,
|
||||
this.toBankId,
|
||||
this.invoiceNo,
|
||||
this.image,
|
||||
this.note,
|
||||
this.user,
|
||||
// Add nested bank models if API provides bank objects, otherwise we only use IDs
|
||||
});
|
||||
|
||||
TransactionData.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
platform = json['platform'];
|
||||
transactionType = json['transaction_type'];
|
||||
type = json['type'];
|
||||
amount = json['amount'];
|
||||
date = json['date'];
|
||||
fromBankId = json['from_bank'];
|
||||
toBankId = json['to_bank'];
|
||||
invoiceNo = json['invoice_no'];
|
||||
image = json['image'];
|
||||
note = json['note'];
|
||||
user = json['user'] != null ? TransactionUser.fromJson(json['user']) : null;
|
||||
}
|
||||
num? id;
|
||||
String? platform;
|
||||
String? transactionType;
|
||||
String? type;
|
||||
num? amount;
|
||||
String? date;
|
||||
num? fromBankId;
|
||||
num? toBankId;
|
||||
String? invoiceNo;
|
||||
String? image;
|
||||
String? note;
|
||||
TransactionUser? user;
|
||||
}
|
||||
|
||||
class TransactionUser {
|
||||
TransactionUser({this.id, this.name});
|
||||
TransactionUser.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
}
|
||||
num? id;
|
||||
String? name;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../model/bank_account_list_model.dart';
|
||||
import '../repo/bank_account_repo.dart';
|
||||
|
||||
final repo = BankRepo();
|
||||
final bankListProvider = FutureProvider.autoDispose<BankListModel>((ref) => repo.fetchAllBanks());
|
||||
@@ -0,0 +1,14 @@
|
||||
// File: bank_transaction_history_provider.dart
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../model/bank_transfer_history_model.dart';
|
||||
import '../repo/bank_transfer_history_repo.dart';
|
||||
|
||||
final repo = BankTransactionHistoryRepo();
|
||||
|
||||
// Provider that takes bankId as a parameter (Family Provider)
|
||||
final bankTransactionHistoryProvider = FutureProvider.autoDispose.family<TransactionHistoryListModel, num>((ref, bankId) {
|
||||
// Pass the bankId to the repository
|
||||
return repo.fetchHistory(bankId: bankId);
|
||||
});
|
||||
@@ -0,0 +1,206 @@
|
||||
// File: bank_repo.dart
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../Const/api_config.dart';
|
||||
import '../../../../http_client/custome_http_client.dart';
|
||||
import '../../../../http_client/customer_http_client_get.dart';
|
||||
import '../model/bank_account_list_model.dart';
|
||||
import '../provider/bank_account_provider.dart';
|
||||
|
||||
class BankRepo {
|
||||
static const String _endpoint = '/banks';
|
||||
|
||||
///---------------- FETCH ALL BANKS (GET) ----------------///
|
||||
Future<BankListModel> fetchAllBanks() async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint');
|
||||
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body);
|
||||
return BankListModel.fromJson(parsedData);
|
||||
} else {
|
||||
throw Exception('Failed to fetch bank list. Status: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to construct API body from core data and meta data
|
||||
Map<String, dynamic> _buildBody({
|
||||
required String name,
|
||||
required num openingBalance,
|
||||
required String openingDate,
|
||||
required num showInInvoice,
|
||||
required BankMeta meta,
|
||||
num? branchId,
|
||||
}) {
|
||||
// NOTE: API requires meta fields to be nested meta[key] in form-data.
|
||||
// When sending JSON, we flatten the meta data and prefix it.
|
||||
|
||||
// Convert meta to flat fields with 'meta[key]' prefix
|
||||
final metaFields = meta.toApiMetaJson().map((key, value) => MapEntry(key, value));
|
||||
|
||||
return {
|
||||
'name': name,
|
||||
'branch_id': branchId, // Assuming branchId is managed separately or is nullable
|
||||
'opening_balance': openingBalance,
|
||||
'opening_date': openingDate, // YYYY-MM-DD format
|
||||
'show_in_invoice': showInInvoice,
|
||||
...metaFields // Flattened meta fields
|
||||
};
|
||||
}
|
||||
|
||||
///---------------- CREATE BANK (POST) ----------------///
|
||||
Future<void> createBank({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required String name,
|
||||
required num openingBalance,
|
||||
required String openingDate,
|
||||
required num showInInvoice,
|
||||
required BankMeta meta,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint');
|
||||
|
||||
final requestBody = jsonEncode(_buildBody(
|
||||
name: name,
|
||||
openingBalance: openingBalance,
|
||||
openingDate: openingDate,
|
||||
showInInvoice: showInInvoice,
|
||||
meta: meta,
|
||||
));
|
||||
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Creating Bank...');
|
||||
var responseData = await customHttpClient.post(
|
||||
url: uri,
|
||||
addContentTypeInHeader: true,
|
||||
body: requestBody,
|
||||
permission: 'bank_create_permit', // Assuming permit exists
|
||||
);
|
||||
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
EasyLoading.dismiss();
|
||||
print('Add Bank Response: $parsedData');
|
||||
|
||||
if (responseData.statusCode == 200 || responseData.statusCode == 201) {
|
||||
ref.invalidate(bankListProvider);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Bank Account created successfully')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Creation failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///---------------- UPDATE BANK (PUT) ----------------///
|
||||
Future<void> updateBank({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num id,
|
||||
required String name,
|
||||
required num openingBalance,
|
||||
required String openingDate,
|
||||
required num showInInvoice,
|
||||
required BankMeta meta,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint/$id');
|
||||
|
||||
final baseBody = _buildBody(
|
||||
name: name,
|
||||
openingBalance: openingBalance,
|
||||
openingDate: openingDate,
|
||||
showInInvoice: showInInvoice,
|
||||
meta: meta,
|
||||
);
|
||||
// Add PUT method override
|
||||
baseBody['_method'] = 'put';
|
||||
|
||||
final requestBody = jsonEncode(baseBody);
|
||||
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Updating Bank...');
|
||||
var responseData = await customHttpClient.post(
|
||||
url: uri,
|
||||
addContentTypeInHeader: true,
|
||||
body: requestBody,
|
||||
permission: 'bank_update_permit', // Assuming permit exists
|
||||
);
|
||||
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (responseData.statusCode == 200) {
|
||||
ref.invalidate(bankListProvider);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Bank Account updated successfully')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Update failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///---------------- DELETE BANK ----------------///
|
||||
Future<bool> deleteBank({
|
||||
required num id,
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
}) async {
|
||||
try {
|
||||
EasyLoading.show(status: 'Deleting...');
|
||||
final url = Uri.parse('${APIConfig.url}$_endpoint/$id');
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(ref: ref, context: context, client: http.Client());
|
||||
final response = await customHttpClient.delete(
|
||||
url: url,
|
||||
permission: 'bank_delete_permit', // Assuming permit exists
|
||||
);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
if (response.statusCode == 200) {
|
||||
ref.invalidate(bankListProvider);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Bank Account deleted successfully')),
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
final parsedData = jsonDecode(response.body);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Deletion failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred during deletion: $error')),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// File: bank_transaction_history_repo.dart
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
// --- Local Imports ---
|
||||
import '../../../../Const/api_config.dart';
|
||||
import '../../../../http_client/customer_http_client_get.dart';
|
||||
import '../model/bank_transfer_history_model.dart';
|
||||
|
||||
class BankTransactionHistoryRepo {
|
||||
static const String _endpoint = '/bank-transactions';
|
||||
|
||||
// NOTE: This API must accept bankId and optional filters (like time range)
|
||||
Future<TransactionHistoryListModel> fetchHistory({
|
||||
required num bankId,
|
||||
String? timeFilter, // e.g., 'Today', 'Current Year'
|
||||
String? transactionTypeFilter,
|
||||
}) async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
|
||||
// Construct query parameters
|
||||
final Map<String, dynamic> queryParams = {
|
||||
'bank_id': bankId.toString(),
|
||||
// Add other filters as API requires (e.g., 'filter_time': timeFilter)
|
||||
};
|
||||
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint').replace(queryParameters: queryParams);
|
||||
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body);
|
||||
return TransactionHistoryListModel.fromJson(parsedData);
|
||||
} else {
|
||||
throw Exception('Failed to fetch transaction history. Status: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: You would add methods here for deleting and updating individual transactions
|
||||
// if required by the action menu.
|
||||
|
||||
// --- Deletion Placeholder ---
|
||||
Future<void> deleteTransaction(num transactionId, BuildContext context, WidgetRef ref) async {
|
||||
// ... Implementation using CustomHttpClient().delete() ...
|
||||
// ref.invalidate(bankTransactionHistoryProvider(bankId));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user