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