first commit
This commit is contained in:
@@ -0,0 +1,415 @@
|
||||
// File: adjust_bank_balance_screen.dart
|
||||
|
||||
import 'dart:io';
|
||||
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/constant.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
|
||||
// --- Local Imports ---
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
|
||||
// Data Source Imports
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/bank%20to%20bank%20transfer/repo/bank_to_bank_transfar_repo.dart';
|
||||
import '../bank account/model/bank_transfer_history_model.dart';
|
||||
import '../bank%20account/model/bank_account_list_model.dart';
|
||||
import '../bank%20account/provider/bank_account_provider.dart';
|
||||
import '../widgets/image_picker_widget.dart';
|
||||
|
||||
// Adjustment Type Model (Reused)
|
||||
class AdjustmentType {
|
||||
final String displayName;
|
||||
final String apiValue;
|
||||
const AdjustmentType(this.displayName, this.apiValue);
|
||||
}
|
||||
|
||||
const List<AdjustmentType> adjustmentTypes = [
|
||||
AdjustmentType('Increase balance', 'credit'),
|
||||
AdjustmentType('Decrease balance', 'debit'),
|
||||
];
|
||||
|
||||
class AdjustBankBalanceScreen extends ConsumerStatefulWidget {
|
||||
// Added optional transaction parameter for editing
|
||||
final TransactionData? transaction;
|
||||
|
||||
const AdjustBankBalanceScreen({super.key, this.transaction});
|
||||
|
||||
@override
|
||||
ConsumerState<AdjustBankBalanceScreen> createState() => _AdjustBankBalanceScreenState();
|
||||
}
|
||||
|
||||
class _AdjustBankBalanceScreenState extends ConsumerState<AdjustBankBalanceScreen> {
|
||||
final GlobalKey<FormState> _key = GlobalKey();
|
||||
|
||||
final amountController = TextEditingController();
|
||||
final dateController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
|
||||
BankData? _selectedBank;
|
||||
AdjustmentType? _selectedType;
|
||||
DateTime? _selectedDate;
|
||||
File? _pickedImage;
|
||||
String? _existingImageUrl; // State for image already on the server
|
||||
|
||||
final DateFormat _displayFormat = DateFormat('dd/MM/yyyy');
|
||||
final DateFormat _apiFormat = DateFormat('yyyy-MM-dd');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final transaction = widget.transaction;
|
||||
|
||||
if (transaction != null) {
|
||||
// Pre-fill data for editing
|
||||
amountController.text = transaction.amount?.toString() ?? '';
|
||||
descriptionController.text = transaction.note ?? '';
|
||||
_existingImageUrl = transaction.image;
|
||||
_selectedType = adjustmentTypes.firstWhere(
|
||||
(type) => type.apiValue == transaction.type,
|
||||
orElse: () => adjustmentTypes.first,
|
||||
);
|
||||
|
||||
try {
|
||||
if (transaction.date != null) {
|
||||
_selectedDate = _apiFormat.parse(transaction.date!);
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
} catch (e) {
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
} else {
|
||||
// For a new transaction
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
_selectedType = adjustmentTypes.first;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
amountController.dispose();
|
||||
dateController.dispose();
|
||||
descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _selectDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: _selectedDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_selectedDate = picked;
|
||||
dateController.text = _displayFormat.format(picked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- Submission Logic (Handles both Create and Update) ---
|
||||
// --- Submission Logic (Handles both Create and Update) ---
|
||||
void _submit() async {
|
||||
if (!_key.currentState!.validate()) return;
|
||||
if (_selectedBank == null || _selectedType == null) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text('Please select an account and adjustment type.')));
|
||||
return;
|
||||
}
|
||||
|
||||
final repo = BankTransactionRepo();
|
||||
final isEditing = widget.transaction != null;
|
||||
final transactionId = widget.transaction?.id;
|
||||
|
||||
// Base parameters are collected from the state
|
||||
final num amount = num.tryParse(amountController.text) ?? 0;
|
||||
final String date = _apiFormat.format(_selectedDate!);
|
||||
final String note = descriptionController.text.trim();
|
||||
|
||||
if (isEditing && transactionId != null) {
|
||||
// Call UPDATE function by passing EACH parameter explicitly
|
||||
await repo.updateBankTransfer(
|
||||
ref: ref,
|
||||
context: context,
|
||||
transactionId: transactionId, // Specific to UPDATE
|
||||
existingImageUrl: _existingImageUrl, // Specific to UPDATE
|
||||
// Common parameters passed explicitly
|
||||
fromBankId: _selectedBank!.id!,
|
||||
toBankId: _selectedBank!.id!, // Same bank for adjustment
|
||||
amount: amount,
|
||||
date: date,
|
||||
note: note,
|
||||
image: _pickedImage,
|
||||
transactionType: 'adjust_bank',
|
||||
type: _selectedType!.apiValue,
|
||||
);
|
||||
} else {
|
||||
// Call CREATE function by passing EACH parameter explicitly
|
||||
await repo.createBankTransfer(
|
||||
ref: ref,
|
||||
context: context,
|
||||
|
||||
// Common parameters passed explicitly
|
||||
fromBankId: _selectedBank!.id!,
|
||||
toBankId: _selectedBank!.id!, // Same bank for adjustment
|
||||
amount: amount,
|
||||
date: date,
|
||||
note: note,
|
||||
image: _pickedImage,
|
||||
transactionType: 'adjust_bank',
|
||||
type: _selectedType!.apiValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Form Reset Logic ---
|
||||
void _clearForm() {
|
||||
setState(() {
|
||||
_selectedBank = null;
|
||||
_selectedType = adjustmentTypes.first;
|
||||
_pickedImage = null;
|
||||
_existingImageUrl = null;
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
amountController.clear();
|
||||
descriptionController.clear();
|
||||
});
|
||||
_key.currentState?.reset();
|
||||
}
|
||||
|
||||
void _resetOrCancel(bool isResetButton) {
|
||||
if (isResetButton) {
|
||||
_clearForm();
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helper to pre-select bank on data load ---
|
||||
void _setInitialBank(List<BankData> banks) {
|
||||
if (widget.transaction != null && _selectedBank == null) {
|
||||
_selectedBank = banks.firstWhere(
|
||||
(bank) => bank.id == widget.transaction!.fromBankId,
|
||||
orElse: () => _selectedBank!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = l.S.of(context);
|
||||
final banksAsync = ref.watch(bankListProvider);
|
||||
final isEditing = widget.transaction != null;
|
||||
final appBarTitle = isEditing ? _lang.editBankAdjustment : _lang.adjustBankBalance;
|
||||
final saveButtonText = isEditing ? _lang.update : _lang.save;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(appBarTitle),
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
),
|
||||
body: banksAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error loading bank accounts: $err')),
|
||||
data: (bankModel) {
|
||||
final banks = bankModel.data ?? [];
|
||||
// Set initial bank state once the data is loaded
|
||||
_setInitialBank(banks);
|
||||
|
||||
if (banks.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: Text(_lang.pleaseAddAtLeastOneBank, textAlign: TextAlign.center),
|
||||
));
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Form(
|
||||
key: _key,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. Account Name
|
||||
_buildAccountDropdown(banks),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 2. Type (Increase/Decrease)
|
||||
_buildAdjustmentTypeDropdown(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 3. Amount
|
||||
_buildAmountInput(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 4. Adjustment Date
|
||||
_buildDateInput(context),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 5. Description
|
||||
_buildDescriptionInput(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 6. Image Picker (Updated for Edit)
|
||||
ReusableImagePicker(
|
||||
initialImage: _pickedImage,
|
||||
existingImageUrl: _existingImageUrl,
|
||||
onImagePicked: (file) {
|
||||
setState(() {
|
||||
_pickedImage = file;
|
||||
if (file != null) _existingImageUrl = null;
|
||||
});
|
||||
},
|
||||
onImageRemoved: () {
|
||||
setState(() {
|
||||
_pickedImage = null;
|
||||
_existingImageUrl = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _resetOrCancel(true),
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
side: const BorderSide(color: Colors.red),
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
child: Text(_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(saveButtonText), // Dynamically shows Save/Update
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Widget Builders ---
|
||||
|
||||
Widget _buildAccountDropdown(List<BankData> banks) {
|
||||
return DropdownButtonFormField<BankData>(
|
||||
icon: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: kPeraColor,
|
||||
),
|
||||
value: _selectedBank,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).accountNumber,
|
||||
hintText: l.S.of(context).selectOne,
|
||||
),
|
||||
validator: (value) => value == null ? l.S.of(context).selectAccount : null,
|
||||
items: banks.map((bank) {
|
||||
return DropdownMenuItem<BankData>(
|
||||
value: bank,
|
||||
child: Text(bank.name ?? 'Unknown'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (BankData? newValue) {
|
||||
setState(() {
|
||||
_selectedBank = newValue;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAdjustmentTypeDropdown() {
|
||||
return DropdownButtonFormField<AdjustmentType>(
|
||||
icon: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: kPeraColor,
|
||||
),
|
||||
value: _selectedType,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).type,
|
||||
hintText: l.S.of(context).selectType,
|
||||
),
|
||||
validator: (value) => value == null ? l.S.of(context).selectType : null,
|
||||
items: adjustmentTypes.map((type) {
|
||||
return DropdownMenuItem<AdjustmentType>(
|
||||
value: type,
|
||||
child: Text(type.displayName),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (AdjustmentType? newValue) {
|
||||
setState(() {
|
||||
_selectedType = newValue;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAmountInput() {
|
||||
return TextFormField(
|
||||
controller: amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).amount,
|
||||
hintText: 'Ex: 500',
|
||||
prefixText: currency,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) return l.S.of(context).amountsIsRequired;
|
||||
if (num.tryParse(value) == null || num.parse(value) <= 0) return l.S.of(context).invalidAmount;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateInput(BuildContext context) {
|
||||
return TextFormField(
|
||||
readOnly: true,
|
||||
controller: dateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).adjustmentDate,
|
||||
hintText: 'DD/MM/YYYY',
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
onPressed: () => _selectDate(context),
|
||||
),
|
||||
),
|
||||
validator: (value) => value!.isEmpty ? l.S.of(context).dateIsRequired : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescriptionInput() {
|
||||
return TextFormField(
|
||||
controller: descriptionController,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).description,
|
||||
hintText: l.S.of(context).enterDescription,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
// File: bank_to_bank_transfer_screen.dart
|
||||
|
||||
import 'dart:io';
|
||||
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/constant.dart';
|
||||
|
||||
// --- Local Imports ---
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
// Data Source Imports
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/bank%20to%20bank%20transfer/repo/bank_to_bank_transfar_repo.dart';
|
||||
import '../bank account/model/bank_transfer_history_model.dart';
|
||||
import '../bank%20account/model/bank_account_list_model.dart';
|
||||
import '../bank%20account/provider/bank_account_provider.dart';
|
||||
import '../widgets/image_picker_widget.dart';
|
||||
|
||||
class BankToBankTransferScreen extends ConsumerStatefulWidget {
|
||||
// 1. Add optional transaction parameter for editing
|
||||
final TransactionData? transaction;
|
||||
|
||||
const BankToBankTransferScreen({super.key, this.transaction});
|
||||
|
||||
@override
|
||||
ConsumerState<BankToBankTransferScreen> createState() => _BankToBankTransferScreenState();
|
||||
}
|
||||
|
||||
class _BankToBankTransferScreenState extends ConsumerState<BankToBankTransferScreen> {
|
||||
final GlobalKey<FormState> _key = GlobalKey();
|
||||
|
||||
// Controllers
|
||||
final amountController = TextEditingController();
|
||||
final dateController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
|
||||
// State
|
||||
BankData? _fromBank;
|
||||
BankData? _toBank;
|
||||
DateTime? _selectedDate;
|
||||
File? _pickedImage; // Image file state (for new upload/replace)
|
||||
String? _existingImageUrl; // State for image already on the server
|
||||
|
||||
final DateFormat _displayFormat = DateFormat('dd/MM/yyyy');
|
||||
final DateFormat _apiFormat = DateFormat('yyyy-MM-dd');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final transaction = widget.transaction;
|
||||
|
||||
if (transaction != null) {
|
||||
// 2. Pre-fill data for editing
|
||||
amountController.text = transaction.amount?.toString() ?? '';
|
||||
descriptionController.text = transaction.note ?? '';
|
||||
_existingImageUrl = transaction.image; // Set existing image URL
|
||||
|
||||
// Parse and set the date
|
||||
try {
|
||||
if (transaction.date != null) {
|
||||
_selectedDate = _apiFormat.parse(transaction.date!);
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to current date if parsing fails
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
|
||||
// The actual bank selection (fromBankId and toBankId) will be handled
|
||||
// when the bank list loads (in the 'data' block of banksAsync.when).
|
||||
} else {
|
||||
// For a new transaction
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
amountController.dispose();
|
||||
dateController.dispose();
|
||||
descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _selectDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: _selectedDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_selectedDate = picked;
|
||||
dateController.text = _displayFormat.format(picked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- Submission Logic ---
|
||||
void _submit() async {
|
||||
if (!_key.currentState!.validate()) return;
|
||||
if (_fromBank == null || _toBank == null) {
|
||||
// Show an error if banks haven't been selected/pre-filled
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(l.S.of(context).pleaseSelectBothAccounts)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_fromBank!.id == _toBank!.id) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l.S.of(context).cannotTransferToSameAccounts,
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final repo = BankTransactionRepo();
|
||||
|
||||
final isEditing = widget.transaction != null;
|
||||
final transactionId = widget.transaction?.id;
|
||||
|
||||
if (isEditing && transactionId != null) {
|
||||
// 3. Call UPDATE function
|
||||
await repo.updateBankTransfer(
|
||||
// **You need to implement this in your repo**
|
||||
ref: ref,
|
||||
context: context,
|
||||
transactionId: transactionId, // Pass the ID for update
|
||||
fromBankId: _fromBank!.id!,
|
||||
toBankId: _toBank!.id!,
|
||||
amount: num.tryParse(amountController.text) ?? 0,
|
||||
date: _apiFormat.format(_selectedDate!),
|
||||
note: descriptionController.text.trim(),
|
||||
image: _pickedImage, // New image to upload (or null)
|
||||
existingImageUrl: _existingImageUrl, // Existing image URL if needed by the API
|
||||
transactionType: "bank_to_bank",
|
||||
type: '',
|
||||
);
|
||||
} else {
|
||||
// 3. Call CREATE function
|
||||
await repo.createBankTransfer(
|
||||
ref: ref,
|
||||
context: context,
|
||||
fromBankId: _fromBank!.id!,
|
||||
toBankId: _toBank!.id!,
|
||||
amount: num.tryParse(amountController.text) ?? 0,
|
||||
date: _apiFormat.format(_selectedDate!),
|
||||
note: descriptionController.text.trim(),
|
||||
image: _pickedImage,
|
||||
transactionType: "bank_to_bank",
|
||||
type: '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reset/Cancel Logic ---
|
||||
void _resetOrCancel() {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
// --- Helper to pre-select banks on data load ---
|
||||
void _setInitialBanks(List<BankData> banks) {
|
||||
if (widget.transaction != null && _fromBank == null && _toBank == null) {
|
||||
_fromBank = banks.firstWhere(
|
||||
(bank) => bank.id == widget.transaction!.fromBankId,
|
||||
orElse: () => _fromBank!, // Fallback (shouldn't happen if IDs are correct)
|
||||
);
|
||||
_toBank = banks.firstWhere(
|
||||
(bank) => bank.id == widget.transaction!.toBankId,
|
||||
orElse: () => _toBank!, // Fallback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = l.S.of(context);
|
||||
final banksAsync = ref.watch(bankListProvider);
|
||||
final isEditing = widget.transaction != null;
|
||||
final appBarTitle = isEditing ? _lang.editBankTransfer : _lang.bankToBankTransfer;
|
||||
final saveButtonText = isEditing ? _lang.update : _lang.save;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(appBarTitle),
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
),
|
||||
body: banksAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error loading bank accounts: $err')),
|
||||
data: (bankModel) {
|
||||
final banks = bankModel.data ?? [];
|
||||
|
||||
// Important: Set initial bank state once the data is loaded
|
||||
_setInitialBanks(banks);
|
||||
|
||||
if (banks.length < 2) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: Text(_lang.needAtLeastTwoBankAccount, textAlign: TextAlign.center),
|
||||
));
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Form(
|
||||
key: _key,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Row 1: From Bank (Full Width)
|
||||
_buildBankDropdown(banks, isFrom: true),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 2: To Bank (Full Width)
|
||||
_buildBankDropdown(banks, isFrom: false),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 3: Amount (Full Width)
|
||||
_buildAmountInput(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 4: Date (Full Width)
|
||||
_buildDateInput(context),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 5: Description (Full Width)
|
||||
_buildDescriptionInput(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 6: Image Picker (Full Width, using reusable widget)
|
||||
ReusableImagePicker(
|
||||
initialImage: _pickedImage,
|
||||
// Pass existing image URL for display when editing
|
||||
existingImageUrl: _existingImageUrl,
|
||||
onImagePicked: (file) {
|
||||
// Update the local state variable when image is picked/removed
|
||||
setState(() {
|
||||
_pickedImage = file;
|
||||
// If a new image is picked, clear the existing URL
|
||||
if (file != null) _existingImageUrl = null;
|
||||
});
|
||||
},
|
||||
onImageRemoved: () {
|
||||
setState(() {
|
||||
_pickedImage = null;
|
||||
_existingImageUrl = null; // Clear both file and URL
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
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(_lang.cancel),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
// 4. Update button text
|
||||
child: Text(saveButtonText),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBankDropdown(List<BankData> banks, {required bool isFrom}) {
|
||||
return DropdownButtonFormField<BankData>(
|
||||
value: isFrom ? _fromBank : _toBank,
|
||||
icon: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: kPeraColor,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: isFrom ? l.S.of(context).from : l.S.of(context).to,
|
||||
hintText: l.S.of(context).selectOne,
|
||||
),
|
||||
validator: (value) => value == null ? l.S.of(context).selectAccount : null,
|
||||
items: banks.map((bank) {
|
||||
return DropdownMenuItem<BankData>(
|
||||
value: bank,
|
||||
enabled: isFrom ? (bank.id != _toBank?.id) : (bank.id != _fromBank?.id),
|
||||
child: Text(bank.name ?? 'Unknown'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (BankData? newValue) {
|
||||
setState(() {
|
||||
if (isFrom) {
|
||||
_fromBank = newValue;
|
||||
} else {
|
||||
_toBank = newValue;
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAmountInput() {
|
||||
return TextFormField(
|
||||
controller: amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).amount,
|
||||
hintText: 'Ex: 500',
|
||||
prefixText: currency,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) return l.S.of(context).amountsIsRequired;
|
||||
if (num.tryParse(value) == null || num.parse(value) <= 0) return l.S.of(context).invalidAmount;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateInput(BuildContext context) {
|
||||
return TextFormField(
|
||||
readOnly: true,
|
||||
controller: dateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).adjustmentDate,
|
||||
hintText: 'DD/MM/YYYY',
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
onPressed: () => _selectDate(context),
|
||||
),
|
||||
),
|
||||
validator: (value) => value!.isEmpty ? l.S.of(context).dateIsRequired : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescriptionInput() {
|
||||
return TextFormField(
|
||||
controller: descriptionController,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).description,
|
||||
hintText: l.S.of(context).enterDescription,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// File: bank_transaction_model.dart (Simplified for create operation)
|
||||
|
||||
class BankTransactionData {
|
||||
// Only defining fields needed for creation/submission reference
|
||||
final String transactionType;
|
||||
final num amount;
|
||||
final String date;
|
||||
final num fromBankId;
|
||||
final num toBankId;
|
||||
final String? note;
|
||||
|
||||
BankTransactionData({
|
||||
required this.transactionType,
|
||||
required this.amount,
|
||||
required this.date,
|
||||
required this.fromBankId,
|
||||
required this.toBankId,
|
||||
this.note,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// File: bank_transaction_repo.dart
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
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';
|
||||
|
||||
// --- Local Imports ---
|
||||
import '../../../../Const/api_config.dart';
|
||||
import '../../../../http_client/custome_http_client.dart';
|
||||
import '../../bank account/provider/bank_account_provider.dart';
|
||||
import '../../bank account/provider/bank_transfers_history_provider.dart';
|
||||
// Note: We don't need a specific provider for transactions list update right now.
|
||||
|
||||
class BankTransactionRepo {
|
||||
static const String _endpoint = '/bank-transactions';
|
||||
|
||||
///---------------- CREATE BANK TO BANK TRANSFER (POST - FORM-DATA) ----------------///
|
||||
Future<void> createBankTransfer({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num fromBankId,
|
||||
num? toBankId,
|
||||
required num amount,
|
||||
required String date, // YYYY-MM-DD
|
||||
required String transactionType,
|
||||
required String type,
|
||||
String? note,
|
||||
File? image, // Optional image file
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint');
|
||||
|
||||
// Prepare fields for MultipartRequest
|
||||
final Map<String, String> fields = {
|
||||
'transaction_type': transactionType,
|
||||
'amount': amount.toString(),
|
||||
'date': date,
|
||||
'from': fromBankId.toString(),
|
||||
'to': toBankId.toString(),
|
||||
'note': note ?? '',
|
||||
'type': type,
|
||||
};
|
||||
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
try {
|
||||
print(fields);
|
||||
EasyLoading.show(status: 'Transferring...');
|
||||
|
||||
var streamedResponse = await customHttpClient.uploadFile(
|
||||
url: uri,
|
||||
file: image,
|
||||
fileFieldName: 'image',
|
||||
fields: fields,
|
||||
permission: 'bank_transaction_create_permit',
|
||||
);
|
||||
|
||||
var response = await http.Response.fromStream(streamedResponse);
|
||||
final parsedData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
ref.invalidate(bankListProvider); // Invalidate bank list to update balances
|
||||
ref.invalidate(bankTransactionHistoryProvider); // Invalidate history
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Transfer successful')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Transfer failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///---------------- UPDATE BANK TO BANK TRANSFER/ADJUSTMENT (PUT/PATCH - FORM-DATA) ----------------///
|
||||
Future<void> updateBankTransfer({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num transactionId, // New: ID of the transaction being updated
|
||||
required num fromBankId,
|
||||
num? toBankId,
|
||||
required num amount,
|
||||
required String date, // YYYY-MM-DD
|
||||
required String transactionType,
|
||||
required String type,
|
||||
String? note,
|
||||
File? image, // Optional: New image file to upload
|
||||
String? existingImageUrl, // Optional: Used to determine if image was removed
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint/$transactionId');
|
||||
|
||||
// Prepare fields for MultipartRequest
|
||||
final Map<String, String> fields = {
|
||||
'transaction_type': transactionType,
|
||||
'amount': amount.toString(),
|
||||
'date': date,
|
||||
'from': fromBankId.toString(),
|
||||
'to': toBankId.toString(),
|
||||
'note': note ?? '',
|
||||
'type': type,
|
||||
'_method': 'PUT', // Important: Tells backend this is a PUT/PATCH request
|
||||
|
||||
'image_removed': (image == null && existingImageUrl == null) ? '1' : '0',
|
||||
};
|
||||
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
try {
|
||||
print(fields);
|
||||
EasyLoading.show(status: 'Updating...');
|
||||
|
||||
var streamedResponse = await customHttpClient.uploadFile(
|
||||
url: uri,
|
||||
file: image, // Will upload new image if present
|
||||
fileFieldName: 'image',
|
||||
fields: fields,
|
||||
permission: 'bank_transaction_edit_permit', // Assuming a different permission for editing
|
||||
);
|
||||
|
||||
var response = await http.Response.fromStream(streamedResponse);
|
||||
final parsedData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
ref.invalidate(bankListProvider); // Invalidate bank list to update balances
|
||||
ref.invalidate(bankTransactionHistoryProvider); // Invalidate history
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Update successful')),
|
||||
);
|
||||
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 TRANSACTION (DELETE) ----------------///
|
||||
Future<bool> deleteBankTransaction({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num transactionId,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint/$transactionId');
|
||||
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Deleting...');
|
||||
|
||||
// Assuming your CustomHttpClient has a standard method for DELETE requests
|
||||
// If not, you'll need to use http.delete(uri, headers: customHttpClient.headers) directly.
|
||||
var response = await customHttpClient.delete(
|
||||
url: uri,
|
||||
permission: 'bank_transaction_delete_permit', // Assuming required permission
|
||||
);
|
||||
|
||||
final parsedData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 204) {
|
||||
ref.invalidate(bankListProvider); // Refresh bank balances
|
||||
ref.invalidate(bankTransactionHistoryProvider); // Refresh history list
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Transaction deleted successfully!')),
|
||||
);
|
||||
// Do NOT pop here; let the calling widget handle navigation (e.g., pop from the list view)
|
||||
return true;
|
||||
} else {
|
||||
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: $error')),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
// File: bank_to_cash_transfer_screen.dart
|
||||
|
||||
import 'dart:io';
|
||||
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/constant.dart';
|
||||
|
||||
// --- Local Imports ---
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
// Data Source Imports
|
||||
// Assuming BankTransactionRepo is the class name in the imported file
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/bank%20to%20bank%20transfer/repo/bank_to_bank_transfar_repo.dart';
|
||||
import '../bank account/model/bank_transfer_history_model.dart';
|
||||
import '../bank%20account/model/bank_account_list_model.dart';
|
||||
import '../bank%20account/provider/bank_account_provider.dart';
|
||||
import '../widgets/image_picker_widget.dart';
|
||||
|
||||
class BankToCashTransferScreen extends ConsumerStatefulWidget {
|
||||
// Optional transaction parameter for editing
|
||||
final TransactionData? transaction;
|
||||
|
||||
const BankToCashTransferScreen({super.key, this.transaction});
|
||||
|
||||
@override
|
||||
ConsumerState<BankToCashTransferScreen> createState() => _BankToCashTransferScreenState();
|
||||
}
|
||||
|
||||
class _BankToCashTransferScreenState extends ConsumerState<BankToCashTransferScreen> {
|
||||
final GlobalKey<FormState> _key = GlobalKey();
|
||||
|
||||
// Controllers
|
||||
final amountController = TextEditingController();
|
||||
final dateController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
|
||||
// State
|
||||
BankData? _fromBank;
|
||||
DateTime? _selectedDate;
|
||||
File? _pickedImage;
|
||||
String? _existingImageUrl; // For editing: stores existing image URL
|
||||
|
||||
final DateFormat _displayFormat = DateFormat('dd/MM/yyyy');
|
||||
final DateFormat _apiFormat = DateFormat('yyyy-MM-dd');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final transaction = widget.transaction;
|
||||
|
||||
if (transaction != null) {
|
||||
// Pre-fill data for editing
|
||||
amountController.text = transaction.amount?.toString() ?? '';
|
||||
descriptionController.text = transaction.note ?? '';
|
||||
_existingImageUrl = transaction.image;
|
||||
|
||||
try {
|
||||
if (transaction.date != null) {
|
||||
_selectedDate = _apiFormat.parse(transaction.date!);
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
} catch (e) {
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
} else {
|
||||
// For a new transaction
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
amountController.dispose();
|
||||
dateController.dispose();
|
||||
descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _selectDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: _selectedDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_selectedDate = picked;
|
||||
dateController.text = _displayFormat.format(picked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to pre-select the 'From' bank on data load
|
||||
void _setInitialBank(List<BankData> banks) {
|
||||
if (widget.transaction != null && _fromBank == null) {
|
||||
_fromBank = banks.firstWhere(
|
||||
(bank) => bank.id == widget.transaction!.fromBankId,
|
||||
orElse: () => _fromBank!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Submission Logic ---
|
||||
void _submit() async {
|
||||
if (!_key.currentState!.validate()) return;
|
||||
if (_fromBank == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please select an account.')));
|
||||
return;
|
||||
}
|
||||
|
||||
final repo = BankTransactionRepo();
|
||||
final isEditing = widget.transaction != null;
|
||||
final transactionId = widget.transaction?.id;
|
||||
|
||||
// Common parameters
|
||||
final num fromBankId = _fromBank!.id!;
|
||||
// Using 0 as a placeholder for CASH destination (check API docs)
|
||||
const num toBankId = 0;
|
||||
final num amount = num.tryParse(amountController.text) ?? 0;
|
||||
final String date = _apiFormat.format(_selectedDate!);
|
||||
final String note = descriptionController.text.trim();
|
||||
const String transactionType = 'bank_to_cash';
|
||||
const String type = '';
|
||||
|
||||
if (isEditing && transactionId != null) {
|
||||
// Call UPDATE function
|
||||
await repo.updateBankTransfer(
|
||||
transactionId: transactionId,
|
||||
existingImageUrl: _existingImageUrl,
|
||||
ref: ref,
|
||||
context: context,
|
||||
fromBankId: fromBankId,
|
||||
toBankId: toBankId,
|
||||
amount: amount,
|
||||
date: date,
|
||||
note: note,
|
||||
image: _pickedImage,
|
||||
transactionType: transactionType,
|
||||
type: type,
|
||||
);
|
||||
} else {
|
||||
// Call CREATE function
|
||||
await repo.createBankTransfer(
|
||||
ref: ref,
|
||||
context: context,
|
||||
fromBankId: fromBankId,
|
||||
toBankId: toBankId,
|
||||
amount: amount,
|
||||
date: date,
|
||||
note: note,
|
||||
image: _pickedImage,
|
||||
transactionType: transactionType,
|
||||
type: type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reset/Cancel Logic ---
|
||||
void _resetOrCancel() {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = l.S.of(context);
|
||||
final banksAsync = ref.watch(bankListProvider);
|
||||
final isEditing = widget.transaction != null;
|
||||
final appBarTitle = isEditing ? _lang.editBankToCash : _lang.bankToCashTransfer;
|
||||
final saveButtonText = isEditing ? _lang.update : _lang.save;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(appBarTitle),
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
),
|
||||
body: banksAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error loading bank accounts: $err')),
|
||||
data: (bankModel) {
|
||||
final banks = bankModel.data ?? [];
|
||||
|
||||
_setInitialBank(banks); // Set initial bank selection for editing
|
||||
|
||||
if (banks.isEmpty) {
|
||||
return Center(child: Text(_lang.noBankAccountsFoundToTransferFrom, textAlign: TextAlign.center));
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Form(
|
||||
key: _key,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Row 1: From Bank
|
||||
_buildFromBankDropdown(banks),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 2: To (Static Cash)
|
||||
_buildStaticCashField(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 3: Amount
|
||||
_buildAmountInput(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 4: Date
|
||||
_buildDateInput(context),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 5: Description
|
||||
_buildDescriptionInput(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 6: Image Picker
|
||||
ReusableImagePicker(
|
||||
initialImage: _pickedImage,
|
||||
existingImageUrl: _existingImageUrl,
|
||||
onImagePicked: (file) {
|
||||
setState(() {
|
||||
_pickedImage = file;
|
||||
if (file != null) _existingImageUrl = null;
|
||||
});
|
||||
},
|
||||
onImageRemoved: () {
|
||||
setState(() {
|
||||
_pickedImage = null;
|
||||
_existingImageUrl = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 40)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
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(_lang.cancel),
|
||||
),
|
||||
),
|
||||
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(saveButtonText),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Widget Builders ---
|
||||
|
||||
Widget _buildFromBankDropdown(List<BankData> banks) {
|
||||
return DropdownButtonFormField<BankData>(
|
||||
value: _fromBank,
|
||||
icon: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: kPeraColor,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).from,
|
||||
hintText: l.S.of(context).selectOneAccount,
|
||||
),
|
||||
validator: (value) => value == null ? l.S.of(context).selectOneAccount : null,
|
||||
items: banks.map((bank) {
|
||||
return DropdownMenuItem<BankData>(
|
||||
value: bank,
|
||||
child: Text(bank.name ?? 'Unknown'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (BankData? newValue) {
|
||||
setState(() {
|
||||
_fromBank = newValue;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStaticCashField() {
|
||||
return TextFormField(
|
||||
initialValue: 'Cash',
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).to,
|
||||
hintText: l.S.of(context).cash,
|
||||
filled: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAmountInput() {
|
||||
return TextFormField(
|
||||
controller: amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).amount,
|
||||
hintText: 'Ex: 500',
|
||||
prefixText: currency,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) return l.S.of(context).amountsIsRequired;
|
||||
if (num.tryParse(value) == null || num.parse(value) <= 0) return l.S.of(context).invalidAmount;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateInput(BuildContext context) {
|
||||
return TextFormField(
|
||||
readOnly: true,
|
||||
controller: dateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).date,
|
||||
hintText: 'DD/MM/YYYY',
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
onPressed: () => _selectDate(context),
|
||||
),
|
||||
),
|
||||
validator: (value) => value!.isEmpty ? l.S.of(context).dateIsRequired : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescriptionInput() {
|
||||
return TextFormField(
|
||||
controller: descriptionController,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).description,
|
||||
hintText: l.S.of(context).enterDescription,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
350
lib/Screens/cash and bank/cansh in hand/adjust_cash_screen.dart
Normal file
350
lib/Screens/cash and bank/cansh in hand/adjust_cash_screen.dart
Normal file
@@ -0,0 +1,350 @@
|
||||
// File: adjust_cash_balance_screen.dart
|
||||
|
||||
import 'dart:io';
|
||||
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/cansh%20in%20hand/repo/cash_in_hand_repo.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
// --- Local Imports ---
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
import '../bank%20account/model/bank_transfer_history_model.dart'; // TransactionData Model
|
||||
import '../widgets/image_picker_widget.dart';
|
||||
import 'model/cash_transaction_list_model.dart';
|
||||
|
||||
// Adjustment Type Model (Add/Reduce Cash)
|
||||
class CashAdjustmentType {
|
||||
final String displayName;
|
||||
final String apiValue; // 'credit' for Add, 'debit' for Reduce
|
||||
const CashAdjustmentType(this.displayName, this.apiValue);
|
||||
}
|
||||
|
||||
List<CashAdjustmentType> adjustmentTypes = [
|
||||
CashAdjustmentType(lang.S.current.addCash, 'credit'),
|
||||
CashAdjustmentType(lang.S.current.reduceCash, 'debit'),
|
||||
];
|
||||
|
||||
class AdjustCashBalanceScreen extends ConsumerStatefulWidget {
|
||||
// Optional transaction parameter for editing (TransactionData is used here)
|
||||
final CashTransactionData? transaction;
|
||||
|
||||
const AdjustCashBalanceScreen({super.key, this.transaction});
|
||||
|
||||
@override
|
||||
ConsumerState<AdjustCashBalanceScreen> createState() => _AdjustCashBalanceScreenState();
|
||||
}
|
||||
|
||||
class _AdjustCashBalanceScreenState extends ConsumerState<AdjustCashBalanceScreen> {
|
||||
final GlobalKey<FormState> _key = GlobalKey();
|
||||
|
||||
final amountController = TextEditingController();
|
||||
final dateController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
|
||||
// State
|
||||
CashAdjustmentType? _selectedType;
|
||||
DateTime? _selectedDate;
|
||||
File? _pickedImage;
|
||||
String? _existingImageUrl; // For editing: stores existing image URL
|
||||
|
||||
// API Constants (Based on your POST fields)
|
||||
final num _cashIdentifier = 0; // 'from' field will be 0/Cash
|
||||
final String _transactionType = 'adjust_cash';
|
||||
|
||||
final DateFormat _displayFormat = DateFormat('dd/MM/yyyy');
|
||||
final DateFormat _apiFormat = DateFormat('yyyy-MM-dd', 'en_US');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final transaction = widget.transaction;
|
||||
|
||||
if (transaction != null) {
|
||||
// Pre-fill data for editing
|
||||
amountController.text = transaction.amount?.toString() ?? '';
|
||||
descriptionController.text = transaction.note ?? '';
|
||||
_existingImageUrl = transaction.image;
|
||||
|
||||
// Determine adjustment type based on transaction.type ('credit' or 'debit')
|
||||
_selectedType = adjustmentTypes.firstWhere(
|
||||
(type) => type.apiValue == transaction.type,
|
||||
orElse: () => adjustmentTypes.first,
|
||||
);
|
||||
|
||||
try {
|
||||
if (transaction.date != null) {
|
||||
_selectedDate = _apiFormat.parse(transaction.date!);
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
} catch (e) {
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
} else {
|
||||
// For a new transaction: Default to Add Cash (Credit)
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
_selectedType = adjustmentTypes.first;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
amountController.dispose();
|
||||
dateController.dispose();
|
||||
descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _selectDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: _selectedDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_selectedDate = picked;
|
||||
dateController.text = _displayFormat.format(picked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- Submission Logic (Handles both Create and Update) ---
|
||||
void _submit() async {
|
||||
if (!_key.currentState!.validate()) return;
|
||||
if (_selectedType == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please select an adjustment type.')));
|
||||
return;
|
||||
}
|
||||
|
||||
final repo = CashTransactionRepo();
|
||||
final isEditing = widget.transaction != null;
|
||||
final transactionId = widget.transaction?.id;
|
||||
|
||||
// Common parameters
|
||||
final num cashId = _cashIdentifier;
|
||||
final num amount = num.tryParse(amountController.text) ?? 0;
|
||||
final String date = _apiFormat.format(_selectedDate!);
|
||||
final String note = descriptionController.text.trim();
|
||||
final String type = _selectedType!.apiValue;
|
||||
|
||||
if (isEditing && transactionId != null) {
|
||||
// Call UPDATE function
|
||||
await repo.updateCashTransfer(
|
||||
transactionId: transactionId,
|
||||
existingImageUrl: _existingImageUrl,
|
||||
ref: ref,
|
||||
context: context,
|
||||
fromBankId: cashId, // Cash identifier
|
||||
toBankId: cashId, // Cash identifier (as per your API structure for adjustment)
|
||||
amount: amount,
|
||||
date: date,
|
||||
note: note,
|
||||
image: _pickedImage,
|
||||
transactionType: _transactionType, // 'adjust_cash'
|
||||
type: type, // 'credit' or 'debit'
|
||||
);
|
||||
} else {
|
||||
// Call CREATE function
|
||||
await repo.createCashTransfer(
|
||||
ref: ref,
|
||||
context: context,
|
||||
fromBankId: cashId,
|
||||
toBankId: cashId, // Cash identifier
|
||||
amount: amount,
|
||||
date: date,
|
||||
note: note,
|
||||
image: _pickedImage,
|
||||
transactionType: _transactionType,
|
||||
type: type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reset/Cancel Logic ---
|
||||
void _resetForm() {
|
||||
setState(() {
|
||||
_selectedType = adjustmentTypes.first;
|
||||
_pickedImage = null;
|
||||
_existingImageUrl = null;
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
amountController.clear();
|
||||
descriptionController.clear();
|
||||
});
|
||||
_key.currentState?.reset();
|
||||
}
|
||||
|
||||
void _resetOrCancel() {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = l.S.of(context);
|
||||
final isEditing = widget.transaction != null;
|
||||
final appBarTitle = isEditing ? _lang.editCashAdjustment : _lang.adjustCashBalance;
|
||||
final saveButtonText = isEditing ? _lang.update : _lang.save;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(appBarTitle),
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(onPressed: _resetOrCancel, icon: const Icon(Icons.close)),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Form(
|
||||
key: _key,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. Adjustment Type (Radio Buttons)
|
||||
_buildAdjustmentTypeSelector(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 2. Amount
|
||||
_buildAmountInput(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 3. Adjustment Date
|
||||
_buildDateInput(context),
|
||||
const SizedBox(height: 20),
|
||||
// 5. Description
|
||||
_buildDescriptionInput(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 4. Image Picker
|
||||
ReusableImagePicker(
|
||||
initialImage: _pickedImage,
|
||||
existingImageUrl: _existingImageUrl,
|
||||
onImagePicked: (file) {
|
||||
setState(() {
|
||||
_pickedImage = file;
|
||||
if (file != null) _existingImageUrl = null;
|
||||
});
|
||||
},
|
||||
onImageRemoved: () {
|
||||
setState(() {
|
||||
_pickedImage = null;
|
||||
_existingImageUrl = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: _resetForm, // Reset the form fields
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
side: const BorderSide(color: Colors.red),
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
child: Text(_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(saveButtonText),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Widget Builders ---
|
||||
|
||||
Widget _buildAdjustmentTypeSelector() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: adjustmentTypes.map((type) {
|
||||
return RadioListTile<CashAdjustmentType>(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
title: Text(
|
||||
type.displayName,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
value: type,
|
||||
groupValue: _selectedType,
|
||||
onChanged: (CashAdjustmentType? newValue) {
|
||||
setState(() {
|
||||
_selectedType = newValue;
|
||||
});
|
||||
},
|
||||
dense: false,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAmountInput() {
|
||||
return TextFormField(
|
||||
controller: amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).amount,
|
||||
hintText: 'Ex: 500',
|
||||
prefixText: currency,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) return l.S.of(context).amountsIsRequired;
|
||||
if (num.tryParse(value) == null || num.parse(value) <= 0) return l.S.of(context).invalidAmount;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateInput(BuildContext context) {
|
||||
return TextFormField(
|
||||
readOnly: true,
|
||||
controller: dateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).adjustmentDate,
|
||||
hintText: 'DD/MM/YYYY',
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
onPressed: () => _selectDate(context),
|
||||
),
|
||||
),
|
||||
validator: (value) => value!.isEmpty ? l.S.of(context).dateIsRequired : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescriptionInput() {
|
||||
return TextFormField(
|
||||
controller: descriptionController,
|
||||
maxLines: 4,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).description,
|
||||
hintText: l.S.of(context).description,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
436
lib/Screens/cash and bank/cansh in hand/cash_in_hand_screen.dart
Normal file
436
lib/Screens/cash and bank/cansh in hand/cash_in_hand_screen.dart
Normal file
@@ -0,0 +1,436 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/cansh%20in%20hand/provider/cash_in_hand_provider.dart';
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/cansh%20in%20hand/repo/cash_in_hand_repo.dart';
|
||||
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/Screens/hrm/widgets/deleteing_alart_dialog.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
import '../widgets/cheques_filter_search.dart';
|
||||
import 'adjust_cash_screen.dart';
|
||||
import 'cash_to_bank_transfer_screen.dart';
|
||||
import 'model/cash_transaction_list_model.dart';
|
||||
|
||||
class CashInHandScreen extends ConsumerStatefulWidget {
|
||||
const CashInHandScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<CashInHandScreen> createState() => _CashInHandScreenState();
|
||||
}
|
||||
|
||||
class _CashInHandScreenState extends ConsumerState<CashInHandScreen> {
|
||||
String _currentSearchQuery = '';
|
||||
DateTime? _currentFromDate;
|
||||
DateTime? _currentToDate;
|
||||
|
||||
final DateFormat _displayFormat = DateFormat('dd/MM/yyyy');
|
||||
final DateFormat _apiFormat = DateFormat('yyyy-MM-dd');
|
||||
|
||||
// List of filter options needed for the reusable widget
|
||||
final List<String> _timeFilterOptions = [
|
||||
'Today',
|
||||
'Yesterday',
|
||||
'Last 7 Days',
|
||||
'Last 30 Days',
|
||||
'Current Month',
|
||||
'Last Month',
|
||||
'Current Year',
|
||||
'Custom Date'
|
||||
];
|
||||
|
||||
final Map<String, String> timeFilterBn = {
|
||||
'Today': l.S.current.today,
|
||||
'Yesterday': l.S.current.yesterday,
|
||||
'Last 7 Days': l.S.current.last7Days,
|
||||
'Last 30 Days': l.S.current.last30Days,
|
||||
'Current Month': l.S.current.currentMonth,
|
||||
'Last Month': l.S.current.lastMonth,
|
||||
'Current Year': l.S.current.currentYear,
|
||||
'Custom Date': l.S.current.customDate,
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Initialize default date range for local filtering (e.g., Current Year)
|
||||
final now = DateTime.now();
|
||||
_currentFromDate = DateTime(now.year, 1, 1);
|
||||
_currentToDate = DateTime(now.year, now.month, now.day, 23, 59, 59);
|
||||
}
|
||||
|
||||
@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 (Unchanged) ---
|
||||
Future<void> _confirmAndDeleteTransaction(CashTransactionData transaction) async {
|
||||
final transactionId = transaction.id;
|
||||
if (transactionId == null) return;
|
||||
|
||||
final confirmed = await showDeleteConfirmationDialog(
|
||||
context: context,
|
||||
itemName: 'cash transaction',
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
final repo = CashTransactionRepo();
|
||||
await repo.deleteCashTransaction(
|
||||
ref: ref,
|
||||
context: context,
|
||||
transactionId: transactionId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Logic Helpers (Unchanged) ---
|
||||
|
||||
String _getListName(CashTransactionData t) {
|
||||
final nameFromUser = t.user?.name ?? 'System';
|
||||
|
||||
if (t.transactionType == 'cash_to_bank') {
|
||||
return 'To: Bank (ID: ${t.toBank})';
|
||||
} else if (t.transactionType == 'bank_to_cash') {
|
||||
return 'From: Bank (ID: ${t.fromBank})';
|
||||
} else if (t.transactionType == 'adjust_cash') {
|
||||
return t.type == 'credit' ? 'Adjustment (Credit)' : 'Adjustment (Debit)';
|
||||
}
|
||||
return nameFromUser;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getAmountDetails(CashTransactionData t) {
|
||||
bool isOutgoing = false;
|
||||
|
||||
if (t.transactionType == 'adjust_cash') {
|
||||
isOutgoing = t.type == 'debit';
|
||||
} else if (t.transactionType == 'cash_to_bank') {
|
||||
isOutgoing = true;
|
||||
} else if (t.transactionType == 'bank_to_cash') {
|
||||
isOutgoing = false;
|
||||
}
|
||||
|
||||
final color = isOutgoing ? Colors.red.shade700 : Colors.green.shade700;
|
||||
final sign = isOutgoing ? '-' : '+';
|
||||
|
||||
return {'sign': sign, 'color': color};
|
||||
}
|
||||
|
||||
// --- Filter Callback Handler ---
|
||||
// 🔔 FIX: Callback now uses the defined CashFilterState type
|
||||
void _handleFilterChange(CashFilterState filterState) {
|
||||
setState(() {
|
||||
_currentSearchQuery = filterState.searchQuery;
|
||||
_currentFromDate = filterState.fromDate;
|
||||
_currentToDate = filterState.toDate;
|
||||
});
|
||||
}
|
||||
|
||||
// --- LOCAL FILTERING FUNCTION (Unchanged) ---
|
||||
List<CashTransactionData> _filterTransactionsLocally(List<CashTransactionData> transactions) {
|
||||
// 1. Filter by Date Range
|
||||
Iterable<CashTransactionData> 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((c) {
|
||||
return (c.transactionType ?? '').toLowerCase().contains(query) ||
|
||||
(c.user?.name ?? '').toLowerCase().contains(query) ||
|
||||
(c.amount?.toString() ?? '').contains(query) ||
|
||||
(c.invoiceNo ?? '').toLowerCase().contains(query);
|
||||
}).toList();
|
||||
}
|
||||
// --- END LOCAL FILTERING FUNCTION ---
|
||||
|
||||
// --- Navigation Helpers for App Bar Menu (Unchanged) ---
|
||||
void _navigateToTransfer() {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const CashToBankTransferScreen()));
|
||||
}
|
||||
|
||||
void _navigateToAdjust() {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const AdjustCashBalanceScreen()));
|
||||
}
|
||||
|
||||
// --- UI Builders (Only _buildBalanceCard and _buildActionMenu shown for brevity) ---
|
||||
|
||||
Widget _buildBalanceCard(ThemeData theme, num balance) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
color: kMainColor.withOpacity(0.9),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l.S.of(context).cashInHand,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold, color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// Balance Info
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'$currency${balance.toStringAsFixed(2)}',
|
||||
style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold, color: Colors.white),
|
||||
),
|
||||
Text(l.S.of(context).currentCashBalance,
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: Colors.white70)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionMenu(CashTransactionData transaction, BuildContext context) {
|
||||
final _lang = l.S.of(context);
|
||||
// ... (Implementation unchanged) ...
|
||||
Map<String, String> _compileDetails() {
|
||||
final details = <String, String>{};
|
||||
details[_lang.transactionType] = (transaction.transactionType ?? 'N/A').replaceAll('_', ' ').toUpperCase();
|
||||
details[_lang.date] = _formatDate(transaction.date);
|
||||
details[_lang.amount] = '$currency${transaction.amount?.toStringAsFixed(2) ?? '0.00'}';
|
||||
details[_lang.user] = transaction.user?.name ?? 'System';
|
||||
details[_lang.invoiceNumber] = transaction.invoiceNo ?? 'N/A';
|
||||
details[_lang.note] = transaction.note ?? 'No Note';
|
||||
|
||||
if (transaction.transactionType == 'cash_to_bank') {
|
||||
details[_lang.toAccount] = 'Bank ID ${transaction.toBank}';
|
||||
} else if (transaction.transactionType == 'bank_to_cash') {
|
||||
details[_lang.fromAccount] = 'Bank ID ${transaction.fromBank}';
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: 30,
|
||||
child: PopupMenuButton<String>(
|
||||
onSelected: (value) {
|
||||
if (transaction.id == null) return;
|
||||
|
||||
if (value == 'view') {
|
||||
viewModalSheet(
|
||||
context: context,
|
||||
item: _compileDetails(),
|
||||
descriptionTitle: '${_lang.description}:',
|
||||
description: transaction.note ?? 'N/A',
|
||||
);
|
||||
} else if (value == 'edit') {
|
||||
if (transaction.transactionType != 'adjust_cash') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CashToBankTransferScreen(
|
||||
transaction: transaction,
|
||||
),
|
||||
));
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AdjustCashBalanceScreen(
|
||||
transaction: transaction,
|
||||
),
|
||||
));
|
||||
}
|
||||
} else if (value == 'delete') {
|
||||
_confirmAndDeleteTransaction(transaction);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(value: 'view', child: Text(l.S.of(context).view)),
|
||||
if ((transaction.transactionType == 'cash_to_bank') || (transaction.transactionType == 'adjust_cash'))
|
||||
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 theme = Theme.of(context);
|
||||
final historyAsync = ref.watch(cashTransactionHistoryProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(_lang.cashInHand),
|
||||
centerTitle: true,
|
||||
toolbarHeight: 80,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(50),
|
||||
child: Column(
|
||||
children: [
|
||||
ChequesFilterSearch(
|
||||
displayFormat: _displayFormat,
|
||||
timeOptions: _timeFilterOptions,
|
||||
onFilterChanged: (filterState) {
|
||||
// Cast the dynamic output to the expected CashFilterState
|
||||
_handleFilterChange(filterState as CashFilterState);
|
||||
},
|
||||
),
|
||||
Divider(thickness: 1, color: kLineColor),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
body: historyAsync.when(
|
||||
loading: () => const Center(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);
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.refresh(cashTransactionHistoryProvider.future),
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 1. Balance and Account Info Card
|
||||
// _buildBalanceCard(theme, model.totalBalance ?? 0),
|
||||
|
||||
// 2. Filters and Search (Using Reusable Widget)
|
||||
// 🔔 FIX: Using ChequesFilterSearch from external file
|
||||
|
||||
// 3. Transaction List
|
||||
if (filteredTransactions.isEmpty)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: Text(
|
||||
_lang.noTransactionFoundForThisFilter,
|
||||
textAlign: TextAlign.center,
|
||||
)))
|
||||
else
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: filteredTransactions.length,
|
||||
separatorBuilder: (_, __) => const Divider(color: kBackgroundColor),
|
||||
itemBuilder: (_, index) {
|
||||
final transaction = filteredTransactions[index];
|
||||
final amountDetails = _getAmountDetails(transaction);
|
||||
return ListTile(
|
||||
visualDensity: VisualDensity(vertical: -4, horizontal: -4),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16),
|
||||
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'],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: _buildActionMenu(transaction, context),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _navigateToTransfer(),
|
||||
child: Text(_lang.transfer),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: kMainColor,
|
||||
),
|
||||
onPressed: () => _navigateToAdjust(),
|
||||
child: Text(_lang.adjustCash, style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
// File: cash_to_bank_transfer_screen.dart
|
||||
|
||||
import 'dart:io';
|
||||
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/cansh%20in%20hand/repo/cash_in_hand_repo.dart';
|
||||
|
||||
// --- Local Imports ---
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
|
||||
import '../bank%20account/model/bank_transfer_history_model.dart';
|
||||
import '../bank%20account/model/bank_account_list_model.dart';
|
||||
import '../bank%20account/provider/bank_account_provider.dart';
|
||||
import '../widgets/image_picker_widget.dart';
|
||||
import 'model/cash_transaction_list_model.dart';
|
||||
|
||||
class CashToBankTransferScreen extends ConsumerStatefulWidget {
|
||||
// Optional transaction parameter for editing
|
||||
final CashTransactionData? transaction;
|
||||
|
||||
const CashToBankTransferScreen({super.key, this.transaction});
|
||||
|
||||
@override
|
||||
ConsumerState<CashToBankTransferScreen> createState() => _CashToBankTransferScreenState();
|
||||
}
|
||||
|
||||
class _CashToBankTransferScreenState extends ConsumerState<CashToBankTransferScreen> {
|
||||
final GlobalKey<FormState> _key = GlobalKey();
|
||||
|
||||
// Controllers
|
||||
final amountController = TextEditingController();
|
||||
final dateController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
|
||||
// State
|
||||
BankData? _toBank; // Now we select the destination Bank
|
||||
DateTime? _selectedDate;
|
||||
File? _pickedImage;
|
||||
String? _existingImageUrl; // For editing: stores existing image URL
|
||||
|
||||
// Placeholder for Cash ID/Platform identifier (API must accept this)
|
||||
final num _cashPlatformId = 0;
|
||||
final String _transactionType = 'cash_to_bank';
|
||||
|
||||
final DateFormat _displayFormat = DateFormat('dd/MM/yyyy');
|
||||
final DateFormat _apiFormat = DateFormat('yyyy-MM-dd');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final transaction = widget.transaction;
|
||||
|
||||
if (transaction != null) {
|
||||
// Pre-fill data for editing
|
||||
amountController.text = transaction.amount?.toString() ?? '';
|
||||
descriptionController.text = transaction.note ?? '';
|
||||
_existingImageUrl = transaction.image;
|
||||
|
||||
try {
|
||||
if (transaction.date != null) {
|
||||
_selectedDate = _apiFormat.parse(transaction.date!);
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
} catch (e) {
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
|
||||
// Note: _toBank selection will be handled in _setInitialBank
|
||||
} else {
|
||||
// For a new transaction
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
amountController.dispose();
|
||||
dateController.dispose();
|
||||
descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _selectDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: _selectedDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_selectedDate = picked;
|
||||
dateController.text = _displayFormat.format(picked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to pre-select the 'To' bank on data load
|
||||
void _setInitialBank(List<BankData> banks) {
|
||||
if (widget.transaction != null && _toBank == null) {
|
||||
// For Cash to Bank, the destination bank is 'toBankId'
|
||||
_toBank = banks.firstWhere(
|
||||
(bank) => bank.id == widget.transaction!.toBank,
|
||||
orElse: () => _toBank!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Submission Logic ---
|
||||
void _submit() async {
|
||||
if (!_key.currentState!.validate()) return;
|
||||
if (_toBank == null) {
|
||||
// Check if destination bank is selected
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l.S.of(context).pleaseSelectADestinationBankAccounts,
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final repo = CashTransactionRepo(); // Using Cash Repo
|
||||
final isEditing = widget.transaction != null;
|
||||
final transactionId = widget.transaction?.id;
|
||||
|
||||
// Common parameters
|
||||
final num fromBankId = _cashPlatformId; // Cash is always FROM
|
||||
final num toBankId = _toBank!.id!; // Destination bank is TO
|
||||
final num amount = num.tryParse(amountController.text) ?? 0;
|
||||
final String date = _apiFormat.format(_selectedDate!);
|
||||
final String note = descriptionController.text.trim();
|
||||
const String type = ''; // Assuming type is blank for transfers
|
||||
|
||||
if (isEditing && transactionId != null) {
|
||||
// Call UPDATE function (Assuming CashTransactionRepo has a similar update structure)
|
||||
await repo.updateCashTransfer(
|
||||
// *** NOTE: Must match the function signature added to CashTransactionRepo below ***
|
||||
transactionId: transactionId,
|
||||
existingImageUrl: _existingImageUrl,
|
||||
ref: ref,
|
||||
context: context,
|
||||
fromBankId: fromBankId,
|
||||
toBankId: toBankId,
|
||||
amount: amount,
|
||||
date: date,
|
||||
note: note,
|
||||
image: _pickedImage,
|
||||
transactionType: _transactionType,
|
||||
type: type,
|
||||
);
|
||||
} else {
|
||||
// Call CREATE function
|
||||
await repo.createCashTransfer(
|
||||
// *** NOTE: Must match the function signature added to CashTransactionRepo below ***
|
||||
ref: ref,
|
||||
context: context,
|
||||
fromBankId: fromBankId,
|
||||
toBankId: toBankId,
|
||||
amount: amount,
|
||||
date: date,
|
||||
note: note,
|
||||
image: _pickedImage,
|
||||
transactionType: _transactionType,
|
||||
type: type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reset/Cancel Logic ---
|
||||
void _resetOrCancel() {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = l.S.of(context);
|
||||
final banksAsync = ref.watch(bankListProvider); // To get destination banks
|
||||
final isEditing = widget.transaction != null;
|
||||
final appBarTitle = isEditing ? _lang.editCashToBank : _lang.bankToCashTransfer;
|
||||
final saveButtonText = isEditing ? _lang.update : _lang.save;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(appBarTitle),
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(onPressed: _resetOrCancel, icon: const Icon(Icons.close)),
|
||||
],
|
||||
),
|
||||
body: banksAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error loading bank accounts: $err')),
|
||||
data: (bankModel) {
|
||||
final banks = bankModel.data ?? [];
|
||||
|
||||
_setInitialBank(banks); // Set initial bank selection for editing
|
||||
|
||||
if (banks.isEmpty) {
|
||||
return Center(child: Text(_lang.noDestinationBankAccountFond, textAlign: TextAlign.center));
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Form(
|
||||
key: _key,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Row 1: From (Static Cash)
|
||||
_buildStaticCashField(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 2: To Bank (Dropdown)
|
||||
_buildToBankDropdown(banks),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 3: Amount
|
||||
_buildAmountInput(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 4: Date
|
||||
_buildDateInput(context),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 5: Description
|
||||
_buildDescriptionInput(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 6: Image Picker
|
||||
ReusableImagePicker(
|
||||
initialImage: _pickedImage,
|
||||
existingImageUrl: _existingImageUrl,
|
||||
onImagePicked: (file) {
|
||||
setState(() {
|
||||
_pickedImage = file;
|
||||
if (file != null) _existingImageUrl = null;
|
||||
});
|
||||
},
|
||||
onImageRemoved: () {
|
||||
setState(() {
|
||||
_pickedImage = null;
|
||||
_existingImageUrl = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
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(_lang.cancel),
|
||||
),
|
||||
),
|
||||
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(saveButtonText),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Widget Builders ---
|
||||
|
||||
Widget _buildStaticCashField() {
|
||||
return TextFormField(
|
||||
initialValue: 'Cash',
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).from, // Changed label to 'From'
|
||||
hintText: l.S.of(context).cash,
|
||||
filled: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToBankDropdown(List<BankData> banks) {
|
||||
return DropdownButtonFormField<BankData>(
|
||||
value: _toBank,
|
||||
icon: Icon(Icons.keyboard_arrow_down),
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).to,
|
||||
hintText: l.S.of(context).selectOneAccount,
|
||||
),
|
||||
validator: (value) => value == null ? l.S.of(context).selectAccount : null,
|
||||
items: banks.map((bank) {
|
||||
return DropdownMenuItem<BankData>(
|
||||
value: bank,
|
||||
child: Text(bank.name ?? 'Unknown'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (BankData? newValue) {
|
||||
setState(() {
|
||||
_toBank = newValue;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAmountInput() {
|
||||
return TextFormField(
|
||||
controller: amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).amount,
|
||||
hintText: 'Ex: 500',
|
||||
prefixText: currency,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) return l.S.of(context).amountsIsRequired;
|
||||
if (num.tryParse(value) == null || num.parse(value) <= 0) return l.S.of(context).invalidAmount;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateInput(BuildContext context) {
|
||||
return TextFormField(
|
||||
readOnly: true,
|
||||
controller: dateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).adjustmentDate,
|
||||
hintText: 'DD/MM/YYYY',
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
onPressed: () => _selectDate(context),
|
||||
),
|
||||
),
|
||||
validator: (value) => value!.isEmpty ? l.S.of(context).dateIsRequired : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescriptionInput() {
|
||||
return TextFormField(
|
||||
controller: descriptionController,
|
||||
maxLines: 4,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).description,
|
||||
hintText: l.S.of(context).enterDescription,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// File: cash_transaction_model.dart
|
||||
|
||||
class CashTransactionModel {
|
||||
final String? message;
|
||||
final List<CashTransactionData>? data;
|
||||
final num? totalBalance;
|
||||
|
||||
CashTransactionModel({this.message, this.data, this.totalBalance});
|
||||
|
||||
factory CashTransactionModel.fromJson(Map<String, dynamic> json) {
|
||||
return CashTransactionModel(
|
||||
message: json['message'],
|
||||
data: (json['data'] as List<dynamic>?)?.map((e) => CashTransactionData.fromJson(e as Map<String, dynamic>)).toList(),
|
||||
totalBalance: json['total_balance'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CashTransactionData {
|
||||
final int? id;
|
||||
final String? platform;
|
||||
final String? transactionType;
|
||||
final String? type;
|
||||
final num? amount;
|
||||
final String? date;
|
||||
final num? fromBank;
|
||||
final num? toBank;
|
||||
final String? invoiceNo;
|
||||
final String? image;
|
||||
final String? note;
|
||||
final User? user;
|
||||
|
||||
CashTransactionData({
|
||||
this.id,
|
||||
this.platform,
|
||||
this.transactionType,
|
||||
this.type,
|
||||
this.amount,
|
||||
this.date,
|
||||
this.fromBank,
|
||||
this.toBank,
|
||||
this.invoiceNo,
|
||||
this.image,
|
||||
this.note,
|
||||
this.user,
|
||||
});
|
||||
|
||||
factory CashTransactionData.fromJson(Map<String, dynamic> json) {
|
||||
return CashTransactionData(
|
||||
id: json['id'],
|
||||
platform: json['platform'],
|
||||
transactionType: json['transaction_type'],
|
||||
type: json['type'],
|
||||
amount: json['amount'],
|
||||
date: json['date'],
|
||||
fromBank: json['from_bank'],
|
||||
toBank: json['to_bank'],
|
||||
invoiceNo: json['invoice_no'],
|
||||
image: json['image'],
|
||||
note: json['note'],
|
||||
user: json['user'] != null ? User.fromJson(json['user']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class User {
|
||||
final int? id;
|
||||
final String? name;
|
||||
|
||||
User({this.id, this.name});
|
||||
|
||||
factory User.fromJson(Map<String, dynamic> json) {
|
||||
return User(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// File: cash_transaction_provider.dart
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../model/cash_transaction_list_model.dart';
|
||||
import '../repo/cash_in_hand_repo.dart';
|
||||
|
||||
// Simple AutoDisposeProvider for the cash transaction history list
|
||||
// Note: You can optionally make this a FamilyProvider if filtering is complex.
|
||||
final cashTransactionHistoryProvider = FutureProvider.autoDispose<CashTransactionModel>((ref) async {
|
||||
final repo = CashTransactionRepo();
|
||||
|
||||
return repo.fetchCashTransactions(filter: null);
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
// File: cash_transaction_repo.dart
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
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 'package:mobile_pos/http_client/customer_http_client_get.dart';
|
||||
|
||||
// --- Local Imports ---
|
||||
import '../../../../Const/api_config.dart';
|
||||
import '../../../../http_client/custome_http_client.dart';
|
||||
import '../../bank account/provider/bank_account_provider.dart';
|
||||
import '../model/cash_transaction_list_model.dart';
|
||||
import '../provider/cash_in_hand_provider.dart';
|
||||
|
||||
class CashTransactionRepo {
|
||||
static const String _endpoint = '/cashes';
|
||||
|
||||
// --- FETCH Cash Transactions ---
|
||||
Future<CashTransactionModel> fetchCashTransactions({
|
||||
required String? filter,
|
||||
}) async {
|
||||
// NOTE: Filter logic (date range) would be implemented here in the real code
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint');
|
||||
|
||||
try {
|
||||
CustomHttpClientGet customHttpClient = CustomHttpClientGet(client: http.Client());
|
||||
final response = await customHttpClient.get(
|
||||
url: uri,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return CashTransactionModel.fromJson(jsonDecode(response.body));
|
||||
} else {
|
||||
throw Exception('Failed to load cash data: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Network Error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
///---------------- CREATE CASH TRANSFER (e.g., Cash to Bank) (POST - FORM-DATA) ----------------///
|
||||
Future<void> createCashTransfer({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num fromBankId, // Should be 0 for Cash
|
||||
required num toBankId, // Destination Bank ID
|
||||
required num amount,
|
||||
required String date, // YYYY-MM-DD
|
||||
required String transactionType, // Should be 'cash_to_bank'
|
||||
required String type,
|
||||
String? note,
|
||||
File? image, // Optional image file
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint');
|
||||
|
||||
final Map<String, String> fields = {
|
||||
// NOTE: API expects 'from' to be the bank ID or cash identifier, 'to' is the destination
|
||||
'transaction_type': transactionType,
|
||||
'amount': amount.toString(),
|
||||
'date': date,
|
||||
'from': 'Cash',
|
||||
if (transactionType != 'adjust_cash') 'to': toBankId.toString(),
|
||||
'note': note ?? '',
|
||||
'type': type,
|
||||
// 'platform': 'cash', // Platform should be 'cash' for this endpoint
|
||||
};
|
||||
print('POSTING DATA: $fields');
|
||||
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Transferring...');
|
||||
|
||||
var streamedResponse = await customHttpClient.uploadFile(
|
||||
url: uri,
|
||||
file: image,
|
||||
fileFieldName: 'image',
|
||||
fields: fields,
|
||||
);
|
||||
|
||||
var response = await http.Response.fromStream(streamedResponse);
|
||||
final parsedData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
ref.invalidate(cashTransactionHistoryProvider); // Refresh Cash History
|
||||
ref.invalidate(bankListProvider); // Refresh Bank List (since a bank balance changed)
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Transfer successful')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Transfer failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///---------------- UPDATE CASH TRANSFER (e.g., Cash to Bank) (POST/PUT/PATCH - FORM-DATA) ----------------///
|
||||
Future<void> updateCashTransfer({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num transactionId,
|
||||
required num fromBankId, // Should be 0 for Cash
|
||||
required num toBankId, // Destination Bank ID
|
||||
required num amount,
|
||||
required String date, // YYYY-MM-DD
|
||||
required String transactionType, // Should be 'cash_to_bank'
|
||||
required String type,
|
||||
String? note,
|
||||
File? image, // Optional: New image file to upload
|
||||
String? existingImageUrl, // Optional: Used to determine if image was removed
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint/$transactionId');
|
||||
|
||||
final Map<String, String> fields = {
|
||||
'transaction_type': transactionType,
|
||||
'amount': amount.toString(),
|
||||
'date': date,
|
||||
'from': fromBankId.toString(),
|
||||
if (transactionType != 'adjust_cash')'to': toBankId.toString(),
|
||||
'note': note ?? '',
|
||||
'type': type,
|
||||
'platform': 'cash',
|
||||
'_method': 'PUT',
|
||||
'image_removed': (image == null && existingImageUrl == null) ? '1' : '0',
|
||||
};
|
||||
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Updating...');
|
||||
|
||||
var streamedResponse = await customHttpClient.uploadFile(
|
||||
url: uri,
|
||||
file: image,
|
||||
fileFieldName: 'image',
|
||||
fields: fields,
|
||||
permission: 'cash_transaction_edit_permit',
|
||||
);
|
||||
|
||||
var response = await http.Response.fromStream(streamedResponse);
|
||||
final parsedData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
ref.invalidate(cashTransactionHistoryProvider); // Refresh Cash History
|
||||
ref.invalidate(bankListProvider); // Refresh Bank List
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Update successful')),
|
||||
);
|
||||
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 Cash Transaction ---
|
||||
Future<bool> deleteCashTransaction({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num transactionId,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint/$transactionId');
|
||||
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Deleting...');
|
||||
|
||||
var response = await customHttpClient.delete(
|
||||
url: uri,
|
||||
permission: 'cash_transaction_delete_permit',
|
||||
);
|
||||
|
||||
final parsedData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 204) {
|
||||
ref.invalidate(cashTransactionHistoryProvider); // Refresh the cash history list
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Transaction deleted successfully!')),
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
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: $error')),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
92
lib/Screens/cash and bank/cash_and_bank_manu_screen.dart
Normal file
92
lib/Screens/cash and bank/cash_and_bank_manu_screen.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
// --- Local Imports ---
|
||||
import '../../constant.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
import 'bank account/bank_account_list_screen.dart';
|
||||
import 'cansh in hand/cash_in_hand_screen.dart';
|
||||
import 'cheques/cheques_list_screen.dart';
|
||||
|
||||
class CashAndBankScreen extends ConsumerStatefulWidget {
|
||||
const CashAndBankScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<CashAndBankScreen> createState() => _CashAndBankScreenState();
|
||||
}
|
||||
|
||||
class _CashAndBankScreenState extends ConsumerState<CashAndBankScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = l.S.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: kBackgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text(_lang.cashAndBankManagement), // Updated title
|
||||
centerTitle: true,
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
spacing: 10,
|
||||
children: [
|
||||
_buildListItem(
|
||||
context,
|
||||
icon: 'assets/hrm/depertment.svg',
|
||||
title: _lang.bankAccounts,
|
||||
destination: BankAccountListScreen(),
|
||||
),
|
||||
_buildListItem(
|
||||
context,
|
||||
icon: 'assets/hrm/depertment.svg',
|
||||
title: _lang.cashInHand,
|
||||
destination: CashInHandScreen(),
|
||||
),
|
||||
_buildListItem(
|
||||
context,
|
||||
icon: 'assets/hrm/depertment.svg',
|
||||
title: _lang.cheque,
|
||||
destination: ChequesListScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///-------------build menu item------------------------------
|
||||
Widget _buildListItem(
|
||||
BuildContext context, {
|
||||
required String icon,
|
||||
required String title,
|
||||
required Widget destination,
|
||||
}) {
|
||||
final _theme = Theme.of(context);
|
||||
return ListTile(
|
||||
tileColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadiusGeometry.circular(6)),
|
||||
horizontalTitleGap: 15,
|
||||
contentPadding: EdgeInsetsDirectional.symmetric(horizontal: 8),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => destination));
|
||||
},
|
||||
leading: SvgPicture.asset(
|
||||
icon,
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: _theme.textTheme.bodyLarge,
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 18,
|
||||
color: kNeutral800,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
271
lib/Screens/cash and bank/cheques/cheques_deposit_screen.dart
Normal file
271
lib/Screens/cash and bank/cheques/cheques_deposit_screen.dart
Normal file
@@ -0,0 +1,271 @@
|
||||
// File: transfer_cheque_deposit_screen.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/cheques/repo/cheque_repository.dart';
|
||||
|
||||
// --- Local Imports ---
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
// Data Layer Imports
|
||||
import '../bank%20account/model/bank_account_list_model.dart';
|
||||
import '../bank%20account/provider/bank_account_provider.dart';
|
||||
import 'model/cheques_list_model.dart';
|
||||
|
||||
// NOTE: Add a static Cash option to the dropdown list
|
||||
final BankData _cashOption = BankData(name: 'Cash', id: 0);
|
||||
|
||||
class TransferChequeDepositScreen extends ConsumerStatefulWidget {
|
||||
final ChequeTransactionData cheque;
|
||||
|
||||
const TransferChequeDepositScreen({super.key, required this.cheque});
|
||||
|
||||
@override
|
||||
ConsumerState<TransferChequeDepositScreen> createState() => _TransferChequeDepositScreenState();
|
||||
}
|
||||
|
||||
class _TransferChequeDepositScreenState extends ConsumerState<TransferChequeDepositScreen> {
|
||||
final GlobalKey<FormState> _key = GlobalKey();
|
||||
final descriptionController = TextEditingController();
|
||||
final dateController = TextEditingController();
|
||||
|
||||
// Changed to dynamic to hold either BankData or _cashOption
|
||||
BankData? _depositDestination;
|
||||
DateTime? _selectedDate;
|
||||
|
||||
final DateFormat _displayFormat = DateFormat('dd/MM/yyyy');
|
||||
final DateFormat _apiFormat = DateFormat('yyyy-MM-dd');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
descriptionController.dispose();
|
||||
dateController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _selectDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: _selectedDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_selectedDate = picked;
|
||||
dateController.text = _displayFormat.format(picked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _submit() async {
|
||||
if (!_key.currentState!.validate()) return;
|
||||
if (_depositDestination == null) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text('Please select a deposit destination (Bank or Cash).')));
|
||||
return;
|
||||
}
|
||||
|
||||
final repo = ChequeRepository();
|
||||
|
||||
// Determine the value to send to the repository: Bank ID or 'cash' string
|
||||
dynamic paymentDestination;
|
||||
if (_depositDestination!.id == 0) {
|
||||
// Using 0 for Cash option
|
||||
paymentDestination = 'cash';
|
||||
} else {
|
||||
paymentDestination = _depositDestination!.id; // Bank ID
|
||||
}
|
||||
|
||||
await repo.depositCheque(
|
||||
ref: ref,
|
||||
context: context,
|
||||
chequeTransactionId: widget.cheque.id!,
|
||||
paymentDestination: paymentDestination,
|
||||
transferDate: _apiFormat.format(_selectedDate!),
|
||||
description: descriptionController.text.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
void _resetForm() {
|
||||
setState(() {
|
||||
_depositDestination = null;
|
||||
descriptionController.clear();
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
});
|
||||
// Reset form validation & states
|
||||
_key.currentState?.reset();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final _lang = l.S.of(context);
|
||||
final cheque = widget.cheque;
|
||||
final banksAsync = ref.watch(bankListProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(_lang.transferCheque),
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
IconButton(onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close)),
|
||||
],
|
||||
),
|
||||
body: banksAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error loading banks: $err')),
|
||||
data: (bankModel) {
|
||||
// Combine Bank List with the static Cash option
|
||||
final banks = [
|
||||
_cashOption, // Cash option first
|
||||
...(bankModel.data ?? []),
|
||||
];
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Form(
|
||||
key: _key,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// --- Cheque Details ---
|
||||
_buildDetailRow(theme, _lang.receivedFrom, cheque.user?.name ?? 'N/A'),
|
||||
_buildDetailRow(theme, _lang.chequeAmount, '$currency${cheque.amount?.toStringAsFixed(2) ?? '0.00'}'),
|
||||
_buildDetailRow(theme, _lang.chequeNumber, cheque.meta?.chequeNumber ?? 'N/A'),
|
||||
_buildDetailRow(theme, _lang.chequeDate, _formatDate(cheque.date)),
|
||||
_buildDetailRow(theme, _lang.referenceNo, cheque.invoiceNo ?? 'N/A'),
|
||||
const Divider(height: 30),
|
||||
|
||||
DropdownButtonFormField<BankData>(
|
||||
value: _depositDestination, // use value instead of initialValue
|
||||
decoration: InputDecoration(
|
||||
hintText: _lang.selectBankToCash,
|
||||
labelText: _lang.depositTo,
|
||||
),
|
||||
validator: (value) => value == null ? _lang.selectDepositDestination : null,
|
||||
items: banks.map((destination) {
|
||||
return DropdownMenuItem<BankData>(
|
||||
value: destination,
|
||||
child: Text(destination.name ?? 'Unknown'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (BankData? newValue) {
|
||||
setState(() {
|
||||
_depositDestination = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// --- Transfer Date Input ---
|
||||
_buildDateInput(context),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// --- Description Input ---
|
||||
_buildDescriptionInput(),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: _resetForm,
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
side: const BorderSide(color: Colors.red),
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
child: Text(_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.send),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(ThemeData theme, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(label, style: theme.textTheme.bodyMedium),
|
||||
),
|
||||
Text(': ', style: theme.textTheme.bodyMedium),
|
||||
Expanded(
|
||||
child: Text(value, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateInput(BuildContext context) {
|
||||
return TextFormField(
|
||||
readOnly: true,
|
||||
controller: dateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).transferDate,
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
onPressed: () => _selectDate(context),
|
||||
),
|
||||
),
|
||||
validator: (value) => value!.isEmpty ? l.S.of(context).dateIsRequired : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescriptionInput() {
|
||||
return TextFormField(
|
||||
controller: descriptionController,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).description,
|
||||
hintText: l.S.of(context).enterDescription,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDate(String? date) {
|
||||
if (date == null) return 'N/A';
|
||||
try {
|
||||
return DateFormat('dd MMM, yyyy').format(DateTime.parse(date));
|
||||
} catch (_) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
}
|
||||
401
lib/Screens/cash and bank/cheques/cheques_list_screen.dart
Normal file
401
lib/Screens/cash and bank/cheques/cheques_list_screen.dart
Normal file
@@ -0,0 +1,401 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/cheques/repo/cheque_repository.dart';
|
||||
|
||||
// --- Local Imports ---
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/core/theme/_app_colors.dart';
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
|
||||
import '../widgets/cheques_filter_search.dart';
|
||||
import 'cheques_deposit_screen.dart';
|
||||
import 'model/cheques_list_model.dart';
|
||||
|
||||
class ChequesListScreen extends ConsumerStatefulWidget {
|
||||
const ChequesListScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ChequesListScreen> createState() => _ChequesListScreenState();
|
||||
}
|
||||
|
||||
class _ChequesListScreenState extends ConsumerState<ChequesListScreen> {
|
||||
String _currentSearchQuery = '';
|
||||
DateTime? _currentFromDate;
|
||||
DateTime? _currentToDate;
|
||||
|
||||
final DateFormat _apiFormat = DateFormat('yyyy-MM-dd');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final now = DateTime.now();
|
||||
_currentFromDate = DateTime(now.year, 1, 1);
|
||||
_currentToDate = DateTime(now.year, now.month, now.day, 23, 59, 59);
|
||||
}
|
||||
|
||||
String _formatDate(String? date) {
|
||||
if (date == null) return 'N/A';
|
||||
try {
|
||||
return DateFormat('dd MMM, yyyy').format(DateTime.parse(date));
|
||||
} catch (_) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateToDepositScreen(ChequeTransactionData cheque) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TransferChequeDepositScreen(cheque: cheque),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//------------ Re Open dialog -----------------------------------
|
||||
void _showOpenDialog(ChequeTransactionData cheque) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
return Dialog(
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadiusGeometry.circular(8),
|
||||
),
|
||||
child: 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),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
l.S.of(context).doYouWantToRellyReOpenThisCheque,
|
||||
textAlign: TextAlign.center,
|
||||
style: _theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 40),
|
||||
side: const BorderSide(color: Colors.red),
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
child: Text(l.S.of(context).cancel),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
// --- IMPLEMENTATION HERE ---
|
||||
if (cheque.id != null) {
|
||||
final repo = ChequeRepository();
|
||||
await repo.reOpenCheque(
|
||||
ref: ref,
|
||||
context: context,
|
||||
chequeTransactionId: cheque.id!,
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 40),
|
||||
backgroundColor: const Color(0xFFB71C1C),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: Text(l.S.of(context).okay),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// --- LOCAL FILTERING FUNCTION ---
|
||||
List<ChequeTransactionData> _filterTransactionsLocally(List<ChequeTransactionData> transactions) {
|
||||
Iterable<ChequeTransactionData> 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;
|
||||
}
|
||||
});
|
||||
|
||||
final query = _currentSearchQuery.toLowerCase();
|
||||
if (query.isEmpty) {
|
||||
return dateFiltered.toList();
|
||||
}
|
||||
|
||||
return dateFiltered.where((c) {
|
||||
return (c.user?.name ?? '').toLowerCase().contains(query) ||
|
||||
(c.meta?.chequeNumber ?? '').contains(query) ||
|
||||
(c.amount?.toString() ?? '').contains(query) ||
|
||||
(c.invoiceNo ?? '').toLowerCase().contains(query);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// --- Filter Callback Handler ---
|
||||
void _handleFilterChange(CashFilterState filterState) {
|
||||
setState(() {
|
||||
_currentSearchQuery = filterState.searchQuery;
|
||||
_currentFromDate = filterState.fromDate;
|
||||
_currentToDate = filterState.toDate;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildChequeListTile(ThemeData theme, ChequeTransactionData cheque) {
|
||||
final status = cheque.type ?? 'N/A';
|
||||
final isDepositable = status == 'pending';
|
||||
return ListTile(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
cheque.user?.name ?? 'n/a',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
isDepositable ? _navigateToDepositScreen(cheque) : _showOpenDialog(cheque);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
visualDensity: VisualDensity(vertical: -4),
|
||||
padding: WidgetStatePropertyAll(
|
||||
EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
),
|
||||
),
|
||||
shape: WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadiusGeometry.circular(4),
|
||||
),
|
||||
),
|
||||
foregroundColor: WidgetStatePropertyAll(
|
||||
isDepositable
|
||||
? DAppColors.kWarning.withValues(
|
||||
alpha: 0.5,
|
||||
)
|
||||
: kSuccessColor.withValues(
|
||||
alpha: 0.5,
|
||||
),
|
||||
),
|
||||
backgroundColor: WidgetStatePropertyAll(isDepositable
|
||||
? kSuccessColor.withValues(
|
||||
alpha: 0.1,
|
||||
)
|
||||
: DAppColors.kWarning.withValues(
|
||||
alpha: 0.1,
|
||||
)),
|
||||
),
|
||||
child: Text(
|
||||
isDepositable ? l.S.of(context).deposit : l.S.of(context).reOpen,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: isDepositable ? kSuccessColor : DAppColors.kWarning,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('dd MMM, yyyy').format(
|
||||
DateTime.parse(cheque.date ?? 'n/a'),
|
||||
),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kGrey6,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$currency${cheque.amount?.toStringAsFixed(2) ?? '0.00'}',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: '${l.S.of(context).type}: ',
|
||||
style: TextStyle(color: kGreyTextColor),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: cheque.platform.capitalizeFirstLetter(),
|
||||
style: TextStyle(
|
||||
color: kTitleColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
isDepositable
|
||||
? l.S.of(context).open
|
||||
: 'Deposit to ${cheque.paymentType == null ? l.S.of(context).cash : cheque.paymentType?.name ?? 'n/a'}',
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = l.S.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final chequesAsync = ref.watch(chequeListProvider);
|
||||
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(_lang.chequeList),
|
||||
centerTitle: true,
|
||||
toolbarHeight: 100,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(10),
|
||||
child: Column(
|
||||
children: [
|
||||
Divider(
|
||||
color: kLineColor,
|
||||
height: 1,
|
||||
),
|
||||
TabBar(
|
||||
dividerColor: kLineColor,
|
||||
dividerHeight: 0.1,
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
labelStyle: theme.textTheme.titleMedium?.copyWith(
|
||||
color: kMainColor,
|
||||
),
|
||||
unselectedLabelStyle: theme.textTheme.titleMedium?.copyWith(
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
tabs: [
|
||||
Tab(
|
||||
text: _lang.all,
|
||||
),
|
||||
Tab(
|
||||
text: _lang.open,
|
||||
),
|
||||
Tab(
|
||||
text: _lang.closed,
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(
|
||||
color: kLineColor,
|
||||
height: 1,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => ref.refresh(chequeListProvider.future),
|
||||
child: chequesAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Text(
|
||||
'Error loading cheques: ${err.toString()}',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
data: (model) {
|
||||
final allCheques = model.data ?? [];
|
||||
final filteredCheques = _filterTransactionsLocally(allCheques);
|
||||
|
||||
return TabBarView(
|
||||
children: [
|
||||
_buildChequeList(theme, filteredCheques),
|
||||
_buildChequeList(
|
||||
theme,
|
||||
filteredCheques.where((c) => (c.type ?? '').toLowerCase() == 'pending').toList(),
|
||||
),
|
||||
_buildChequeList(
|
||||
theme,
|
||||
filteredCheques.where((c) => (c.type ?? '').toLowerCase() == 'deposit').toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChequeList(ThemeData theme, List<ChequeTransactionData> cheques) {
|
||||
if (cheques.isEmpty) {
|
||||
return Center(child: Text(l.S.of(context).noChequeFound));
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
itemCount: cheques.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1, color: kBackgroundColor),
|
||||
itemBuilder: (_, index) {
|
||||
final cheque = cheques[index];
|
||||
return _buildChequeListTile(theme, cheque);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/bank%20account/model/bank_account_list_model.dart';
|
||||
|
||||
import '../../../../model/business_info_model.dart';
|
||||
|
||||
class ChequeTransactionModel {
|
||||
final String? message;
|
||||
final List<ChequeTransactionData>? data;
|
||||
|
||||
ChequeTransactionModel({this.message, this.data});
|
||||
|
||||
factory ChequeTransactionModel.fromJson(Map<String, dynamic> json) {
|
||||
return ChequeTransactionModel(
|
||||
message: json['message'],
|
||||
data: (json['data'] as List<dynamic>?)?.map((e) => ChequeTransactionData.fromJson(e as Map<String, dynamic>)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChequeTransactionData {
|
||||
final int? id;
|
||||
final String? platform;
|
||||
final String? transactionType;
|
||||
final String? type; // 'credit'
|
||||
final num? amount;
|
||||
final String? date;
|
||||
final num? referenceId;
|
||||
final String? invoiceNo;
|
||||
final String? image;
|
||||
final String? note;
|
||||
final ChequeMeta? meta;
|
||||
final User? user; // Received From
|
||||
BankData? paymentType;
|
||||
|
||||
ChequeTransactionData({
|
||||
this.id,
|
||||
this.platform,
|
||||
this.transactionType,
|
||||
this.type,
|
||||
this.amount,
|
||||
this.date,
|
||||
this.referenceId,
|
||||
this.invoiceNo,
|
||||
this.image,
|
||||
this.note,
|
||||
this.meta,
|
||||
this.user,
|
||||
this.paymentType,
|
||||
});
|
||||
|
||||
factory ChequeTransactionData.fromJson(Map<String, dynamic> json) {
|
||||
return ChequeTransactionData(
|
||||
id: json['id'],
|
||||
platform: json['platform'],
|
||||
transactionType: json['transaction_type'],
|
||||
type: json['type'],
|
||||
amount: json['amount'],
|
||||
date: json['date'],
|
||||
referenceId: json['reference_id'],
|
||||
invoiceNo: json['invoice_no'],
|
||||
image: json['image'],
|
||||
note: json['note'],
|
||||
meta: json['meta'] != null ? ChequeMeta.fromJson(json['meta']) : null,
|
||||
user: json['user'] != null ? User.fromJson(json['user']) : null,
|
||||
paymentType: json['payment_type'] != null ? BankData.fromJson(json['payment_type']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChequeMeta {
|
||||
final String? chequeNumber;
|
||||
final String? status; // 'open'
|
||||
|
||||
ChequeMeta({this.chequeNumber, this.status});
|
||||
|
||||
factory ChequeMeta.fromJson(Map<String, dynamic> json) {
|
||||
return ChequeMeta(
|
||||
chequeNumber: json['cheque_number'],
|
||||
status: json['status'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// User model is assumed to be shared from bank_transfer_history_model.dart
|
||||
141
lib/Screens/cash and bank/cheques/repo/cheque_repository.dart
Normal file
141
lib/Screens/cash and bank/cheques/repo/cheque_repository.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
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 '../../bank account/provider/bank_account_provider.dart';
|
||||
import '../model/cheques_list_model.dart';
|
||||
|
||||
final chequeListProvider = FutureProvider.autoDispose<ChequeTransactionModel>((ref) async {
|
||||
final repo = ChequeRepository();
|
||||
return repo.fetchChequeList(filter: 'Current Year');
|
||||
});
|
||||
|
||||
class ChequeRepository {
|
||||
static const String _endpoint = '/cheques';
|
||||
|
||||
// --- 1. FETCH LIST ---
|
||||
Future<ChequeTransactionModel> fetchChequeList({
|
||||
required String? filter,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint');
|
||||
|
||||
try {
|
||||
CustomHttpClientGet customHttpClientGet = CustomHttpClientGet(client: http.Client());
|
||||
final response = await customHttpClientGet.get(
|
||||
url: uri,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return ChequeTransactionModel.fromJson(jsonDecode(response.body));
|
||||
} else {
|
||||
throw Exception('Failed to load cheques: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Network Error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2. DEPOSIT Cheque (POST /api/v1/cheques) ---
|
||||
Future<void> depositCheque({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num chequeTransactionId,
|
||||
required dynamic paymentDestination,
|
||||
required String transferDate,
|
||||
required String description,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint');
|
||||
|
||||
final Map<String, dynamic> fields = {
|
||||
'transaction_id': chequeTransactionId.toString(),
|
||||
'payment_type': paymentDestination.toString(),
|
||||
'date': transferDate,
|
||||
'note': description,
|
||||
};
|
||||
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Depositing Cheque...');
|
||||
|
||||
var response = await customHttpClient.post(
|
||||
url: uri,
|
||||
body: fields,
|
||||
permission: 'cheque_deposit_permit',
|
||||
);
|
||||
|
||||
final parsedData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
ref.invalidate(chequeListProvider);
|
||||
ref.invalidate(bankListProvider);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Cheque Deposited Successfully!')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Deposit Failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. RE-OPEN Cheque (POST /api/v1/cheque-reopen/{transaction_id}) ---
|
||||
Future<void> reOpenCheque({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num chequeTransactionId,
|
||||
}) async {
|
||||
// API Call: POST /cheque-reopen/{id}
|
||||
final uri = Uri.parse('${APIConfig.url}/cheque-reopen/$chequeTransactionId');
|
||||
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Re-opening Cheque...');
|
||||
|
||||
// Sending Empty body as the ID is in the URL
|
||||
var response = await customHttpClient.post(
|
||||
url: uri,
|
||||
body: {},
|
||||
);
|
||||
|
||||
final parsedData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
// Success: Refresh Lists and Close Dialog
|
||||
ref.invalidate(chequeListProvider);
|
||||
ref.invalidate(bankListProvider);
|
||||
|
||||
Navigator.pop(context); // Close the confirmation dialog
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Cheque Re-opened Successfully!')),
|
||||
);
|
||||
} else {
|
||||
// API Error
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
407
lib/Screens/cash and bank/widgets/cheques_filter_search.dart
Normal file
407
lib/Screens/cash and bank/widgets/cheques_filter_search.dart
Normal file
@@ -0,0 +1,407 @@
|
||||
// File: widgets/cheques_filter_search.dart (Update this file content)
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
class CashFilterState {
|
||||
final String searchQuery;
|
||||
final DateTime? fromDate;
|
||||
final DateTime? toDate;
|
||||
|
||||
CashFilterState({
|
||||
required this.searchQuery,
|
||||
this.fromDate,
|
||||
this.toDate,
|
||||
});
|
||||
}
|
||||
|
||||
class ChequesFilterSearch extends ConsumerStatefulWidget {
|
||||
final Function(dynamic) onFilterChanged; // Use dynamic if model name differs
|
||||
final DateFormat displayFormat;
|
||||
final List<String> timeOptions;
|
||||
|
||||
const ChequesFilterSearch({
|
||||
super.key,
|
||||
required this.onFilterChanged,
|
||||
required this.displayFormat,
|
||||
required this.timeOptions,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ChequesFilterSearch> createState() => _ChequesFilterSearchState();
|
||||
}
|
||||
|
||||
class _ChequesFilterSearchState extends ConsumerState<ChequesFilterSearch> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
String _searchQuery = '';
|
||||
String? _selectedTimeFilter;
|
||||
DateTime? _fromDate;
|
||||
DateTime? _toDate;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.addListener(_onSearchChanged);
|
||||
|
||||
// default filter
|
||||
_selectedTimeFilter = widget.timeOptions.contains('Today') ? 'Today' : widget.timeOptions.first;
|
||||
_updateDateRange(_selectedTimeFilter!, notify: false);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.removeListener(_onSearchChanged);
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _notifyParent() {
|
||||
widget.onFilterChanged(
|
||||
CashFilterState(
|
||||
searchQuery: _searchQuery,
|
||||
fromDate: _fromDate,
|
||||
toDate: _toDate,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onSearchChanged() {
|
||||
setState(() {
|
||||
_searchQuery = _searchController.text;
|
||||
});
|
||||
_notifyParent();
|
||||
}
|
||||
|
||||
void _updateDateRange(String range, {bool notify = true}) {
|
||||
final now = DateTime.now();
|
||||
DateTime newFromDate;
|
||||
|
||||
setState(() {
|
||||
_selectedTimeFilter = range;
|
||||
|
||||
if (range == 'Custom Date') {
|
||||
_fromDate = null;
|
||||
_toDate = null;
|
||||
if (notify) _notifyParent();
|
||||
return;
|
||||
}
|
||||
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
_toDate = DateTime(now.year, now.month, now.day, 23, 59, 59);
|
||||
|
||||
switch (range) {
|
||||
case 'Today':
|
||||
newFromDate = today;
|
||||
break;
|
||||
case 'Yesterday':
|
||||
newFromDate = today.subtract(const Duration(days: 1));
|
||||
_toDate = DateTime(now.year, now.month, now.day - 1, 23, 59, 59);
|
||||
break;
|
||||
case 'Last 7 Days':
|
||||
newFromDate = today.subtract(const Duration(days: 6));
|
||||
break;
|
||||
case 'Last 30 Days':
|
||||
newFromDate = today.subtract(const Duration(days: 29));
|
||||
break;
|
||||
case 'Current Month':
|
||||
newFromDate = DateTime(now.year, now.month, 1);
|
||||
break;
|
||||
case 'Last Month':
|
||||
newFromDate = DateTime(now.year, now.month - 1, 1);
|
||||
_toDate = DateTime(now.year, now.month, 0, 23, 59, 59);
|
||||
break;
|
||||
case 'Current Year':
|
||||
default:
|
||||
newFromDate = DateTime(now.year, 1, 1);
|
||||
break;
|
||||
}
|
||||
_fromDate = newFromDate;
|
||||
});
|
||||
if (notify) _notifyParent();
|
||||
}
|
||||
|
||||
String? _tempSelectedFilter; // used when opening sheet
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: TextFormField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: l.S.of(context).searchTransaction,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.all(1),
|
||||
child: Container(
|
||||
height: 44,
|
||||
width: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffFEF0F1),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(6),
|
||||
bottomRight: Radius.circular(6),
|
||||
)),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
IconlyLight.filter,
|
||||
color: kMainColor,
|
||||
),
|
||||
onPressed: () => _openTimeFilterSheet(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openTimeFilterSheet(BuildContext context) {
|
||||
// initialize temp values from current parent state
|
||||
_tempSelectedFilter = _selectedTimeFilter;
|
||||
DateTime? tempFrom = _fromDate;
|
||||
DateTime? tempTo = _toDate;
|
||||
final _theme = Theme.of(context);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
// use StatefulBuilder so we can update sheet-local state
|
||||
return StatefulBuilder(builder: (context, setModalState) {
|
||||
final showCustomDates = _tempSelectedFilter == 'Custom Date';
|
||||
|
||||
Future<void> pickDateLocal(bool isFrom) async {
|
||||
final initial = isFrom ? (tempFrom ?? DateTime.now()) : (tempTo ?? tempFrom ?? DateTime.now());
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: initial,
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2101),
|
||||
);
|
||||
if (picked != null) {
|
||||
setModalState(() {
|
||||
if (isFrom) {
|
||||
tempFrom = DateTime(picked.year, picked.month, picked.day);
|
||||
// ensure tempTo >= tempFrom
|
||||
if (tempTo != null && tempTo!.isBefore(tempFrom!)) {
|
||||
tempTo = DateTime(picked.year, picked.month, picked.day, 23, 59, 59);
|
||||
}
|
||||
} else {
|
||||
tempTo = DateTime(picked.year, picked.month, picked.day, 23, 59, 59);
|
||||
if (tempFrom != null && tempFrom!.isAfter(tempTo!)) {
|
||||
tempFrom = DateTime(picked.year, picked.month, picked.day);
|
||||
}
|
||||
}
|
||||
// if user picked any date, ensure filter is Custom Date
|
||||
_tempSelectedFilter = 'Custom Date';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String formatSafe(DateTime? d) => d == null ? '' : widget.displayFormat.format(d);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Text(
|
||||
l.S.of(context).filterByDate,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Icon(Icons.close),
|
||||
),
|
||||
]),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
DropdownButtonFormField<String>(
|
||||
value: _tempSelectedFilter,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).filterByDate,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
),
|
||||
),
|
||||
// items: widget.timeOptions.map((item) {
|
||||
// return DropdownMenuItem(
|
||||
// value: item,
|
||||
// child: Text(
|
||||
// item,
|
||||
// style: _theme.textTheme.bodyLarge,
|
||||
// ),
|
||||
// );
|
||||
// }).toList(),
|
||||
// List of filter options needed for the reusable widget
|
||||
// final List<String> _timeFilterOptions = [
|
||||
// 'Today',
|
||||
// 'Yesterday',
|
||||
// 'Last 7 Days',
|
||||
// 'Last 30 Days',
|
||||
// 'Current Month',
|
||||
// 'Last Month',
|
||||
// 'Current Year',
|
||||
// 'Custom Date'
|
||||
// ];
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'Today',
|
||||
child: Text(l.S.of(context).today),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'Yesterday',
|
||||
child: Text(l.S.of(context).yesterday),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'Last 7 Days',
|
||||
child: Text(l.S.of(context).last7Days),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'Last 30 Days',
|
||||
child: Text(l.S.of(context).last30Days),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'Current Month',
|
||||
child: Text(l.S.of(context).currentMonth),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'Last Month',
|
||||
child: Text(l.S.of(context).lastMonth),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'Current Year',
|
||||
child: Text(l.S.of(context).currentYear),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'Custom Date',
|
||||
child: Text(l.S.of(context).customDate),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
setModalState(() {
|
||||
_tempSelectedFilter = value;
|
||||
// if selecting a pre-defined range, clear temp custom dates
|
||||
if (_tempSelectedFilter != 'Custom Date') {
|
||||
tempFrom = null;
|
||||
tempTo = null;
|
||||
} else {
|
||||
// keep current parent's dates as starting point if available
|
||||
tempFrom ??= _fromDate;
|
||||
tempTo ??= _toDate;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Custom Date Fields
|
||||
if (showCustomDates)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () => pickDateLocal(true),
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).fromDate,
|
||||
suffixIcon: Icon(IconlyLight.calendar),
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||
),
|
||||
child: Text(
|
||||
formatSafe(tempFrom),
|
||||
style: _theme.textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () => pickDateLocal(false),
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).toDate,
|
||||
suffixIcon: Icon(IconlyLight.calendar),
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||
),
|
||||
child: Text(
|
||||
formatSafe(tempTo),
|
||||
style: _theme.textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(l.S.of(context).cancel),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: kMainColor),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
setState(() {
|
||||
if (_tempSelectedFilter == 'Custom Date') {
|
||||
// commit custom dates (if any)
|
||||
_selectedTimeFilter = 'Custom Date';
|
||||
_fromDate = tempFrom;
|
||||
_toDate = tempTo;
|
||||
// ensure to normalize times if needed
|
||||
if (_fromDate != null && _toDate == null) {
|
||||
_toDate = DateTime(_fromDate!.year, _fromDate!.month, _fromDate!.day, 23, 59, 59);
|
||||
}
|
||||
} else if (_tempSelectedFilter != null) {
|
||||
_updateDateRange(_tempSelectedFilter!);
|
||||
}
|
||||
});
|
||||
|
||||
_notifyParent();
|
||||
},
|
||||
child: Text(l.S.of(context).apply, style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
192
lib/Screens/cash and bank/widgets/image_picker_widget.dart
Normal file
192
lib/Screens/cash and bank/widgets/image_picker_widget.dart
Normal file
@@ -0,0 +1,192 @@
|
||||
// File: shared_widgets/reusable_image_picker.dart
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:mobile_pos/Const/api_config.dart';
|
||||
|
||||
// Assuming you have a l10n package for lang.S.of(context)
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:mobile_pos/constant.dart'; // kMainColor, kNeutral800 etc.
|
||||
|
||||
class ReusableImagePicker extends StatefulWidget {
|
||||
final File? initialImage;
|
||||
final String? existingImageUrl; // NEW: Image URL for editing
|
||||
final Function(File?) onImagePicked;
|
||||
final Function()? onImageRemoved; // NEW: Callback for explicit removal
|
||||
|
||||
const ReusableImagePicker({
|
||||
super.key,
|
||||
this.initialImage,
|
||||
this.existingImageUrl, // Added to constructor
|
||||
required this.onImagePicked,
|
||||
this.onImageRemoved, // Added to constructor
|
||||
});
|
||||
|
||||
@override
|
||||
State<ReusableImagePicker> createState() => _ReusableImagePickerState();
|
||||
}
|
||||
|
||||
class _ReusableImagePickerState extends State<ReusableImagePicker> {
|
||||
File? _pickedImage;
|
||||
String? _existingImageUrl; // State for the image URL
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Prioritize new file if passed, otherwise use existing URL
|
||||
_pickedImage = widget.initialImage;
|
||||
_existingImageUrl = widget.existingImageUrl;
|
||||
}
|
||||
|
||||
// Update state if parent widget sends new values (e.g., when switching between edit screens)
|
||||
@override
|
||||
void didUpdateWidget(covariant ReusableImagePicker oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.initialImage != oldWidget.initialImage || widget.existingImageUrl != oldWidget.existingImageUrl) {
|
||||
// Keep the new image if present, otherwise load the URL
|
||||
_pickedImage = widget.initialImage;
|
||||
_existingImageUrl = widget.existingImageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickImage(ImageSource source, BuildContext dialogContext) async {
|
||||
final XFile? xFile = await _picker.pickImage(source: source);
|
||||
|
||||
// Close the dialog after selection attempt
|
||||
Navigator.of(dialogContext).pop();
|
||||
|
||||
if (xFile != null) {
|
||||
final newFile = File(xFile.path);
|
||||
setState(() {
|
||||
_pickedImage = newFile;
|
||||
_existingImageUrl = null; // A new file means we discard the existing URL
|
||||
});
|
||||
widget.onImagePicked(newFile); // Notify parent screen
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Cupertino Dialog for image source selection (unchanged)
|
||||
void _showImageSourceDialog() {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (BuildContext contexts) => BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
||||
child: CupertinoAlertDialog(
|
||||
insetAnimationCurve: Curves.bounceInOut,
|
||||
title: Text(
|
||||
lang.S.of(context).uploadImage, // Assuming this string exists
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
actions: <Widget>[
|
||||
CupertinoDialogAction(
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(IconlyLight.image, size: 30.0),
|
||||
Text(
|
||||
lang.S.of(context).useGallery, // Assuming this string exists
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.bodySmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
)
|
||||
],
|
||||
),
|
||||
onPressed: () => _pickImage(ImageSource.gallery, contexts),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(IconlyLight.camera, size: 30.0),
|
||||
Text(
|
||||
lang.S.of(context).openCamera, // Assuming this string exists
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.bodySmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
)
|
||||
],
|
||||
),
|
||||
onPressed: () => _pickImage(ImageSource.camera, contexts),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Determine the source to display
|
||||
final bool hasImage = _pickedImage != null || (_existingImageUrl?.isNotEmpty ?? false);
|
||||
|
||||
return Container(
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: _showImageSourceDialog, // Always allow tapping to change/add image
|
||||
child: !hasImage
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(IconlyLight.image, size: 30),
|
||||
const SizedBox(height: 5),
|
||||
Text(lang.S.of(context).addImage, style: Theme.of(context).textTheme.bodyMedium),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
// Conditional Image Widget
|
||||
child: _pickedImage != null
|
||||
? Image.file(_pickedImage!, fit: BoxFit.cover) // Display new file
|
||||
: Image.network(
|
||||
// Display existing image from URL
|
||||
'${APIConfig.domain}${_existingImageUrl!}',
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => const Center(
|
||||
child: Icon(Icons.error_outline, color: Colors.red)), // Show error icon on failed load
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_pickedImage = null;
|
||||
_existingImageUrl = null; // Crucial: clear URL as well
|
||||
});
|
||||
// Notify parent that the image (file or url) is removed
|
||||
widget.onImagePicked(null);
|
||||
if (widget.onImageRemoved != null) {
|
||||
widget.onImageRemoved!();
|
||||
}
|
||||
},
|
||||
child: const CircleAvatar(
|
||||
radius: 12,
|
||||
backgroundColor: Colors.black54,
|
||||
child: Icon(Icons.close, size: 16, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user