first commit

This commit is contained in:
2026-02-07 15:57:09 +07:00
commit 157096f164
1153 changed files with 415766 additions and 0 deletions

View File

@@ -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),
),
);
}
}

View File

@@ -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,
});
}

View File

@@ -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;
}
}
}