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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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());

View File

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

View File

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

View File

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

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

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

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