first commit
This commit is contained in:
350
lib/Screens/cash and bank/cansh in hand/adjust_cash_screen.dart
Normal file
350
lib/Screens/cash and bank/cansh in hand/adjust_cash_screen.dart
Normal file
@@ -0,0 +1,350 @@
|
||||
// File: adjust_cash_balance_screen.dart
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/cansh%20in%20hand/repo/cash_in_hand_repo.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
// --- Local Imports ---
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
import '../bank%20account/model/bank_transfer_history_model.dart'; // TransactionData Model
|
||||
import '../widgets/image_picker_widget.dart';
|
||||
import 'model/cash_transaction_list_model.dart';
|
||||
|
||||
// Adjustment Type Model (Add/Reduce Cash)
|
||||
class CashAdjustmentType {
|
||||
final String displayName;
|
||||
final String apiValue; // 'credit' for Add, 'debit' for Reduce
|
||||
const CashAdjustmentType(this.displayName, this.apiValue);
|
||||
}
|
||||
|
||||
List<CashAdjustmentType> adjustmentTypes = [
|
||||
CashAdjustmentType(lang.S.current.addCash, 'credit'),
|
||||
CashAdjustmentType(lang.S.current.reduceCash, 'debit'),
|
||||
];
|
||||
|
||||
class AdjustCashBalanceScreen extends ConsumerStatefulWidget {
|
||||
// Optional transaction parameter for editing (TransactionData is used here)
|
||||
final CashTransactionData? transaction;
|
||||
|
||||
const AdjustCashBalanceScreen({super.key, this.transaction});
|
||||
|
||||
@override
|
||||
ConsumerState<AdjustCashBalanceScreen> createState() => _AdjustCashBalanceScreenState();
|
||||
}
|
||||
|
||||
class _AdjustCashBalanceScreenState extends ConsumerState<AdjustCashBalanceScreen> {
|
||||
final GlobalKey<FormState> _key = GlobalKey();
|
||||
|
||||
final amountController = TextEditingController();
|
||||
final dateController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
|
||||
// State
|
||||
CashAdjustmentType? _selectedType;
|
||||
DateTime? _selectedDate;
|
||||
File? _pickedImage;
|
||||
String? _existingImageUrl; // For editing: stores existing image URL
|
||||
|
||||
// API Constants (Based on your POST fields)
|
||||
final num _cashIdentifier = 0; // 'from' field will be 0/Cash
|
||||
final String _transactionType = 'adjust_cash';
|
||||
|
||||
final DateFormat _displayFormat = DateFormat('dd/MM/yyyy');
|
||||
final DateFormat _apiFormat = DateFormat('yyyy-MM-dd', 'en_US');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final transaction = widget.transaction;
|
||||
|
||||
if (transaction != null) {
|
||||
// Pre-fill data for editing
|
||||
amountController.text = transaction.amount?.toString() ?? '';
|
||||
descriptionController.text = transaction.note ?? '';
|
||||
_existingImageUrl = transaction.image;
|
||||
|
||||
// Determine adjustment type based on transaction.type ('credit' or 'debit')
|
||||
_selectedType = adjustmentTypes.firstWhere(
|
||||
(type) => type.apiValue == transaction.type,
|
||||
orElse: () => adjustmentTypes.first,
|
||||
);
|
||||
|
||||
try {
|
||||
if (transaction.date != null) {
|
||||
_selectedDate = _apiFormat.parse(transaction.date!);
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
} catch (e) {
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
} else {
|
||||
// For a new transaction: Default to Add Cash (Credit)
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
_selectedType = adjustmentTypes.first;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
amountController.dispose();
|
||||
dateController.dispose();
|
||||
descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _selectDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: _selectedDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_selectedDate = picked;
|
||||
dateController.text = _displayFormat.format(picked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- Submission Logic (Handles both Create and Update) ---
|
||||
void _submit() async {
|
||||
if (!_key.currentState!.validate()) return;
|
||||
if (_selectedType == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please select an adjustment type.')));
|
||||
return;
|
||||
}
|
||||
|
||||
final repo = CashTransactionRepo();
|
||||
final isEditing = widget.transaction != null;
|
||||
final transactionId = widget.transaction?.id;
|
||||
|
||||
// Common parameters
|
||||
final num cashId = _cashIdentifier;
|
||||
final num amount = num.tryParse(amountController.text) ?? 0;
|
||||
final String date = _apiFormat.format(_selectedDate!);
|
||||
final String note = descriptionController.text.trim();
|
||||
final String type = _selectedType!.apiValue;
|
||||
|
||||
if (isEditing && transactionId != null) {
|
||||
// Call UPDATE function
|
||||
await repo.updateCashTransfer(
|
||||
transactionId: transactionId,
|
||||
existingImageUrl: _existingImageUrl,
|
||||
ref: ref,
|
||||
context: context,
|
||||
fromBankId: cashId, // Cash identifier
|
||||
toBankId: cashId, // Cash identifier (as per your API structure for adjustment)
|
||||
amount: amount,
|
||||
date: date,
|
||||
note: note,
|
||||
image: _pickedImage,
|
||||
transactionType: _transactionType, // 'adjust_cash'
|
||||
type: type, // 'credit' or 'debit'
|
||||
);
|
||||
} else {
|
||||
// Call CREATE function
|
||||
await repo.createCashTransfer(
|
||||
ref: ref,
|
||||
context: context,
|
||||
fromBankId: cashId,
|
||||
toBankId: cashId, // Cash identifier
|
||||
amount: amount,
|
||||
date: date,
|
||||
note: note,
|
||||
image: _pickedImage,
|
||||
transactionType: _transactionType,
|
||||
type: type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reset/Cancel Logic ---
|
||||
void _resetForm() {
|
||||
setState(() {
|
||||
_selectedType = adjustmentTypes.first;
|
||||
_pickedImage = null;
|
||||
_existingImageUrl = null;
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
amountController.clear();
|
||||
descriptionController.clear();
|
||||
});
|
||||
_key.currentState?.reset();
|
||||
}
|
||||
|
||||
void _resetOrCancel() {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = l.S.of(context);
|
||||
final isEditing = widget.transaction != null;
|
||||
final appBarTitle = isEditing ? _lang.editCashAdjustment : _lang.adjustCashBalance;
|
||||
final saveButtonText = isEditing ? _lang.update : _lang.save;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(appBarTitle),
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(onPressed: _resetOrCancel, icon: const Icon(Icons.close)),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Form(
|
||||
key: _key,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. Adjustment Type (Radio Buttons)
|
||||
_buildAdjustmentTypeSelector(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 2. Amount
|
||||
_buildAmountInput(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 3. Adjustment Date
|
||||
_buildDateInput(context),
|
||||
const SizedBox(height: 20),
|
||||
// 5. Description
|
||||
_buildDescriptionInput(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 4. Image Picker
|
||||
ReusableImagePicker(
|
||||
initialImage: _pickedImage,
|
||||
existingImageUrl: _existingImageUrl,
|
||||
onImagePicked: (file) {
|
||||
setState(() {
|
||||
_pickedImage = file;
|
||||
if (file != null) _existingImageUrl = null;
|
||||
});
|
||||
},
|
||||
onImageRemoved: () {
|
||||
setState(() {
|
||||
_pickedImage = null;
|
||||
_existingImageUrl = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: _resetForm, // Reset the form fields
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
side: const BorderSide(color: Colors.red),
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
child: Text(_lang.resets),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
backgroundColor: const Color(0xFFB71C1C),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: Text(saveButtonText),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Widget Builders ---
|
||||
|
||||
Widget _buildAdjustmentTypeSelector() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: adjustmentTypes.map((type) {
|
||||
return RadioListTile<CashAdjustmentType>(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
title: Text(
|
||||
type.displayName,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
value: type,
|
||||
groupValue: _selectedType,
|
||||
onChanged: (CashAdjustmentType? newValue) {
|
||||
setState(() {
|
||||
_selectedType = newValue;
|
||||
});
|
||||
},
|
||||
dense: false,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAmountInput() {
|
||||
return TextFormField(
|
||||
controller: amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).amount,
|
||||
hintText: 'Ex: 500',
|
||||
prefixText: currency,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) return l.S.of(context).amountsIsRequired;
|
||||
if (num.tryParse(value) == null || num.parse(value) <= 0) return l.S.of(context).invalidAmount;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateInput(BuildContext context) {
|
||||
return TextFormField(
|
||||
readOnly: true,
|
||||
controller: dateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).adjustmentDate,
|
||||
hintText: 'DD/MM/YYYY',
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
onPressed: () => _selectDate(context),
|
||||
),
|
||||
),
|
||||
validator: (value) => value!.isEmpty ? l.S.of(context).dateIsRequired : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescriptionInput() {
|
||||
return TextFormField(
|
||||
controller: descriptionController,
|
||||
maxLines: 4,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).description,
|
||||
hintText: l.S.of(context).description,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
436
lib/Screens/cash and bank/cansh in hand/cash_in_hand_screen.dart
Normal file
436
lib/Screens/cash and bank/cansh in hand/cash_in_hand_screen.dart
Normal file
@@ -0,0 +1,436 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/cansh%20in%20hand/provider/cash_in_hand_provider.dart';
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/cansh%20in%20hand/repo/cash_in_hand_repo.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/widgets/model_bottom_sheet.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/widgets/deleteing_alart_dialog.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
import '../widgets/cheques_filter_search.dart';
|
||||
import 'adjust_cash_screen.dart';
|
||||
import 'cash_to_bank_transfer_screen.dart';
|
||||
import 'model/cash_transaction_list_model.dart';
|
||||
|
||||
class CashInHandScreen extends ConsumerStatefulWidget {
|
||||
const CashInHandScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<CashInHandScreen> createState() => _CashInHandScreenState();
|
||||
}
|
||||
|
||||
class _CashInHandScreenState extends ConsumerState<CashInHandScreen> {
|
||||
String _currentSearchQuery = '';
|
||||
DateTime? _currentFromDate;
|
||||
DateTime? _currentToDate;
|
||||
|
||||
final DateFormat _displayFormat = DateFormat('dd/MM/yyyy');
|
||||
final DateFormat _apiFormat = DateFormat('yyyy-MM-dd');
|
||||
|
||||
// List of filter options needed for the reusable widget
|
||||
final List<String> _timeFilterOptions = [
|
||||
'Today',
|
||||
'Yesterday',
|
||||
'Last 7 Days',
|
||||
'Last 30 Days',
|
||||
'Current Month',
|
||||
'Last Month',
|
||||
'Current Year',
|
||||
'Custom Date'
|
||||
];
|
||||
|
||||
final Map<String, String> timeFilterBn = {
|
||||
'Today': l.S.current.today,
|
||||
'Yesterday': l.S.current.yesterday,
|
||||
'Last 7 Days': l.S.current.last7Days,
|
||||
'Last 30 Days': l.S.current.last30Days,
|
||||
'Current Month': l.S.current.currentMonth,
|
||||
'Last Month': l.S.current.lastMonth,
|
||||
'Current Year': l.S.current.currentYear,
|
||||
'Custom Date': l.S.current.customDate,
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Initialize default date range for local filtering (e.g., Current Year)
|
||||
final now = DateTime.now();
|
||||
_currentFromDate = DateTime(now.year, 1, 1);
|
||||
_currentToDate = DateTime(now.year, now.month, now.day, 23, 59, 59);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _formatDate(String? date) {
|
||||
if (date == null) return 'N/A';
|
||||
try {
|
||||
return DateFormat('dd MMM, yyyy').format(DateTime.parse(date));
|
||||
} catch (_) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
// --- DELETE Logic (Unchanged) ---
|
||||
Future<void> _confirmAndDeleteTransaction(CashTransactionData transaction) async {
|
||||
final transactionId = transaction.id;
|
||||
if (transactionId == null) return;
|
||||
|
||||
final confirmed = await showDeleteConfirmationDialog(
|
||||
context: context,
|
||||
itemName: 'cash transaction',
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
final repo = CashTransactionRepo();
|
||||
await repo.deleteCashTransaction(
|
||||
ref: ref,
|
||||
context: context,
|
||||
transactionId: transactionId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Logic Helpers (Unchanged) ---
|
||||
|
||||
String _getListName(CashTransactionData t) {
|
||||
final nameFromUser = t.user?.name ?? 'System';
|
||||
|
||||
if (t.transactionType == 'cash_to_bank') {
|
||||
return 'To: Bank (ID: ${t.toBank})';
|
||||
} else if (t.transactionType == 'bank_to_cash') {
|
||||
return 'From: Bank (ID: ${t.fromBank})';
|
||||
} else if (t.transactionType == 'adjust_cash') {
|
||||
return t.type == 'credit' ? 'Adjustment (Credit)' : 'Adjustment (Debit)';
|
||||
}
|
||||
return nameFromUser;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getAmountDetails(CashTransactionData t) {
|
||||
bool isOutgoing = false;
|
||||
|
||||
if (t.transactionType == 'adjust_cash') {
|
||||
isOutgoing = t.type == 'debit';
|
||||
} else if (t.transactionType == 'cash_to_bank') {
|
||||
isOutgoing = true;
|
||||
} else if (t.transactionType == 'bank_to_cash') {
|
||||
isOutgoing = false;
|
||||
}
|
||||
|
||||
final color = isOutgoing ? Colors.red.shade700 : Colors.green.shade700;
|
||||
final sign = isOutgoing ? '-' : '+';
|
||||
|
||||
return {'sign': sign, 'color': color};
|
||||
}
|
||||
|
||||
// --- Filter Callback Handler ---
|
||||
// 🔔 FIX: Callback now uses the defined CashFilterState type
|
||||
void _handleFilterChange(CashFilterState filterState) {
|
||||
setState(() {
|
||||
_currentSearchQuery = filterState.searchQuery;
|
||||
_currentFromDate = filterState.fromDate;
|
||||
_currentToDate = filterState.toDate;
|
||||
});
|
||||
}
|
||||
|
||||
// --- LOCAL FILTERING FUNCTION (Unchanged) ---
|
||||
List<CashTransactionData> _filterTransactionsLocally(List<CashTransactionData> transactions) {
|
||||
// 1. Filter by Date Range
|
||||
Iterable<CashTransactionData> dateFiltered = transactions.where((t) {
|
||||
if (_currentFromDate == null && _currentToDate == null) return true;
|
||||
if (t.date == null) return false;
|
||||
|
||||
try {
|
||||
final transactionDate = DateTime.parse(t.date!);
|
||||
|
||||
final start = _currentFromDate;
|
||||
final end = _currentToDate;
|
||||
|
||||
bool afterStart = start == null || transactionDate.isAfter(start) || transactionDate.isAtSameMomentAs(start);
|
||||
bool beforeEnd = end == null || transactionDate.isBefore(end) || transactionDate.isAtSameMomentAs(end);
|
||||
|
||||
return afterStart && beforeEnd;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Filter by Search Query
|
||||
final query = _currentSearchQuery.toLowerCase();
|
||||
if (query.isEmpty) {
|
||||
return dateFiltered.toList();
|
||||
}
|
||||
|
||||
return dateFiltered.where((c) {
|
||||
return (c.transactionType ?? '').toLowerCase().contains(query) ||
|
||||
(c.user?.name ?? '').toLowerCase().contains(query) ||
|
||||
(c.amount?.toString() ?? '').contains(query) ||
|
||||
(c.invoiceNo ?? '').toLowerCase().contains(query);
|
||||
}).toList();
|
||||
}
|
||||
// --- END LOCAL FILTERING FUNCTION ---
|
||||
|
||||
// --- Navigation Helpers for App Bar Menu (Unchanged) ---
|
||||
void _navigateToTransfer() {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const CashToBankTransferScreen()));
|
||||
}
|
||||
|
||||
void _navigateToAdjust() {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const AdjustCashBalanceScreen()));
|
||||
}
|
||||
|
||||
// --- UI Builders (Only _buildBalanceCard and _buildActionMenu shown for brevity) ---
|
||||
|
||||
Widget _buildBalanceCard(ThemeData theme, num balance) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
color: kMainColor.withOpacity(0.9),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l.S.of(context).cashInHand,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold, color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// Balance Info
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'$currency${balance.toStringAsFixed(2)}',
|
||||
style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold, color: Colors.white),
|
||||
),
|
||||
Text(l.S.of(context).currentCashBalance,
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: Colors.white70)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionMenu(CashTransactionData transaction, BuildContext context) {
|
||||
final _lang = l.S.of(context);
|
||||
// ... (Implementation unchanged) ...
|
||||
Map<String, String> _compileDetails() {
|
||||
final details = <String, String>{};
|
||||
details[_lang.transactionType] = (transaction.transactionType ?? 'N/A').replaceAll('_', ' ').toUpperCase();
|
||||
details[_lang.date] = _formatDate(transaction.date);
|
||||
details[_lang.amount] = '$currency${transaction.amount?.toStringAsFixed(2) ?? '0.00'}';
|
||||
details[_lang.user] = transaction.user?.name ?? 'System';
|
||||
details[_lang.invoiceNumber] = transaction.invoiceNo ?? 'N/A';
|
||||
details[_lang.note] = transaction.note ?? 'No Note';
|
||||
|
||||
if (transaction.transactionType == 'cash_to_bank') {
|
||||
details[_lang.toAccount] = 'Bank ID ${transaction.toBank}';
|
||||
} else if (transaction.transactionType == 'bank_to_cash') {
|
||||
details[_lang.fromAccount] = 'Bank ID ${transaction.fromBank}';
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: 30,
|
||||
child: PopupMenuButton<String>(
|
||||
onSelected: (value) {
|
||||
if (transaction.id == null) return;
|
||||
|
||||
if (value == 'view') {
|
||||
viewModalSheet(
|
||||
context: context,
|
||||
item: _compileDetails(),
|
||||
descriptionTitle: '${_lang.description}:',
|
||||
description: transaction.note ?? 'N/A',
|
||||
);
|
||||
} else if (value == 'edit') {
|
||||
if (transaction.transactionType != 'adjust_cash') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CashToBankTransferScreen(
|
||||
transaction: transaction,
|
||||
),
|
||||
));
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AdjustCashBalanceScreen(
|
||||
transaction: transaction,
|
||||
),
|
||||
));
|
||||
}
|
||||
} else if (value == 'delete') {
|
||||
_confirmAndDeleteTransaction(transaction);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(value: 'view', child: Text(l.S.of(context).view)),
|
||||
if ((transaction.transactionType == 'cash_to_bank') || (transaction.transactionType == 'adjust_cash'))
|
||||
PopupMenuItem(value: 'edit', child: Text(l.S.of(context).edit)),
|
||||
PopupMenuItem(value: 'delete', child: Text(l.S.of(context).delete, style: TextStyle(color: Colors.red))),
|
||||
],
|
||||
icon: const Icon(Icons.more_vert, color: kNeutral800),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = l.S.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final historyAsync = ref.watch(cashTransactionHistoryProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(_lang.cashInHand),
|
||||
centerTitle: true,
|
||||
toolbarHeight: 80,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(50),
|
||||
child: Column(
|
||||
children: [
|
||||
ChequesFilterSearch(
|
||||
displayFormat: _displayFormat,
|
||||
timeOptions: _timeFilterOptions,
|
||||
onFilterChanged: (filterState) {
|
||||
// Cast the dynamic output to the expected CashFilterState
|
||||
_handleFilterChange(filterState as CashFilterState);
|
||||
},
|
||||
),
|
||||
Divider(thickness: 1, color: kLineColor),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
body: historyAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error: ${err.toString()}')),
|
||||
data: (model) {
|
||||
final allTransactions = model.data ?? [];
|
||||
|
||||
// Apply local date and search filtering
|
||||
final filteredTransactions = _filterTransactionsLocally(allTransactions);
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.refresh(cashTransactionHistoryProvider.future),
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 1. Balance and Account Info Card
|
||||
// _buildBalanceCard(theme, model.totalBalance ?? 0),
|
||||
|
||||
// 2. Filters and Search (Using Reusable Widget)
|
||||
// 🔔 FIX: Using ChequesFilterSearch from external file
|
||||
|
||||
// 3. Transaction List
|
||||
if (filteredTransactions.isEmpty)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: Text(
|
||||
_lang.noTransactionFoundForThisFilter,
|
||||
textAlign: TextAlign.center,
|
||||
)))
|
||||
else
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: filteredTransactions.length,
|
||||
separatorBuilder: (_, __) => const Divider(color: kBackgroundColor),
|
||||
itemBuilder: (_, index) {
|
||||
final transaction = filteredTransactions[index];
|
||||
final amountDetails = _getAmountDetails(transaction);
|
||||
return ListTile(
|
||||
visualDensity: VisualDensity(vertical: -4, horizontal: -4),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
(transaction.transactionType ?? 'N/A').replaceAll('_', ' ').toUpperCase(),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$currency${transaction.amount?.toStringAsFixed(2) ?? '0.00'}',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: amountDetails['color'],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_formatDate(transaction.date),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kGrey6,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
transaction.platform.toString(),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kGrey6,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: _buildActionMenu(transaction, context),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _navigateToTransfer(),
|
||||
child: Text(_lang.transfer),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: kMainColor,
|
||||
),
|
||||
onPressed: () => _navigateToAdjust(),
|
||||
child: Text(_lang.adjustCash, style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
// File: cash_to_bank_transfer_screen.dart
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/cash%20and%20bank/cansh%20in%20hand/repo/cash_in_hand_repo.dart';
|
||||
|
||||
// --- Local Imports ---
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
|
||||
import '../bank%20account/model/bank_transfer_history_model.dart';
|
||||
import '../bank%20account/model/bank_account_list_model.dart';
|
||||
import '../bank%20account/provider/bank_account_provider.dart';
|
||||
import '../widgets/image_picker_widget.dart';
|
||||
import 'model/cash_transaction_list_model.dart';
|
||||
|
||||
class CashToBankTransferScreen extends ConsumerStatefulWidget {
|
||||
// Optional transaction parameter for editing
|
||||
final CashTransactionData? transaction;
|
||||
|
||||
const CashToBankTransferScreen({super.key, this.transaction});
|
||||
|
||||
@override
|
||||
ConsumerState<CashToBankTransferScreen> createState() => _CashToBankTransferScreenState();
|
||||
}
|
||||
|
||||
class _CashToBankTransferScreenState extends ConsumerState<CashToBankTransferScreen> {
|
||||
final GlobalKey<FormState> _key = GlobalKey();
|
||||
|
||||
// Controllers
|
||||
final amountController = TextEditingController();
|
||||
final dateController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
|
||||
// State
|
||||
BankData? _toBank; // Now we select the destination Bank
|
||||
DateTime? _selectedDate;
|
||||
File? _pickedImage;
|
||||
String? _existingImageUrl; // For editing: stores existing image URL
|
||||
|
||||
// Placeholder for Cash ID/Platform identifier (API must accept this)
|
||||
final num _cashPlatformId = 0;
|
||||
final String _transactionType = 'cash_to_bank';
|
||||
|
||||
final DateFormat _displayFormat = DateFormat('dd/MM/yyyy');
|
||||
final DateFormat _apiFormat = DateFormat('yyyy-MM-dd');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final transaction = widget.transaction;
|
||||
|
||||
if (transaction != null) {
|
||||
// Pre-fill data for editing
|
||||
amountController.text = transaction.amount?.toString() ?? '';
|
||||
descriptionController.text = transaction.note ?? '';
|
||||
_existingImageUrl = transaction.image;
|
||||
|
||||
try {
|
||||
if (transaction.date != null) {
|
||||
_selectedDate = _apiFormat.parse(transaction.date!);
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
} catch (e) {
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
|
||||
// Note: _toBank selection will be handled in _setInitialBank
|
||||
} else {
|
||||
// For a new transaction
|
||||
_selectedDate = DateTime.now();
|
||||
dateController.text = _displayFormat.format(_selectedDate!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
amountController.dispose();
|
||||
dateController.dispose();
|
||||
descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _selectDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: _selectedDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_selectedDate = picked;
|
||||
dateController.text = _displayFormat.format(picked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to pre-select the 'To' bank on data load
|
||||
void _setInitialBank(List<BankData> banks) {
|
||||
if (widget.transaction != null && _toBank == null) {
|
||||
// For Cash to Bank, the destination bank is 'toBankId'
|
||||
_toBank = banks.firstWhere(
|
||||
(bank) => bank.id == widget.transaction!.toBank,
|
||||
orElse: () => _toBank!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Submission Logic ---
|
||||
void _submit() async {
|
||||
if (!_key.currentState!.validate()) return;
|
||||
if (_toBank == null) {
|
||||
// Check if destination bank is selected
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l.S.of(context).pleaseSelectADestinationBankAccounts,
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final repo = CashTransactionRepo(); // Using Cash Repo
|
||||
final isEditing = widget.transaction != null;
|
||||
final transactionId = widget.transaction?.id;
|
||||
|
||||
// Common parameters
|
||||
final num fromBankId = _cashPlatformId; // Cash is always FROM
|
||||
final num toBankId = _toBank!.id!; // Destination bank is TO
|
||||
final num amount = num.tryParse(amountController.text) ?? 0;
|
||||
final String date = _apiFormat.format(_selectedDate!);
|
||||
final String note = descriptionController.text.trim();
|
||||
const String type = ''; // Assuming type is blank for transfers
|
||||
|
||||
if (isEditing && transactionId != null) {
|
||||
// Call UPDATE function (Assuming CashTransactionRepo has a similar update structure)
|
||||
await repo.updateCashTransfer(
|
||||
// *** NOTE: Must match the function signature added to CashTransactionRepo below ***
|
||||
transactionId: transactionId,
|
||||
existingImageUrl: _existingImageUrl,
|
||||
ref: ref,
|
||||
context: context,
|
||||
fromBankId: fromBankId,
|
||||
toBankId: toBankId,
|
||||
amount: amount,
|
||||
date: date,
|
||||
note: note,
|
||||
image: _pickedImage,
|
||||
transactionType: _transactionType,
|
||||
type: type,
|
||||
);
|
||||
} else {
|
||||
// Call CREATE function
|
||||
await repo.createCashTransfer(
|
||||
// *** NOTE: Must match the function signature added to CashTransactionRepo below ***
|
||||
ref: ref,
|
||||
context: context,
|
||||
fromBankId: fromBankId,
|
||||
toBankId: toBankId,
|
||||
amount: amount,
|
||||
date: date,
|
||||
note: note,
|
||||
image: _pickedImage,
|
||||
transactionType: _transactionType,
|
||||
type: type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reset/Cancel Logic ---
|
||||
void _resetOrCancel() {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = l.S.of(context);
|
||||
final banksAsync = ref.watch(bankListProvider); // To get destination banks
|
||||
final isEditing = widget.transaction != null;
|
||||
final appBarTitle = isEditing ? _lang.editCashToBank : _lang.bankToCashTransfer;
|
||||
final saveButtonText = isEditing ? _lang.update : _lang.save;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(appBarTitle),
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(onPressed: _resetOrCancel, icon: const Icon(Icons.close)),
|
||||
],
|
||||
),
|
||||
body: banksAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(child: Text('Error loading bank accounts: $err')),
|
||||
data: (bankModel) {
|
||||
final banks = bankModel.data ?? [];
|
||||
|
||||
_setInitialBank(banks); // Set initial bank selection for editing
|
||||
|
||||
if (banks.isEmpty) {
|
||||
return Center(child: Text(_lang.noDestinationBankAccountFond, textAlign: TextAlign.center));
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Form(
|
||||
key: _key,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Row 1: From (Static Cash)
|
||||
_buildStaticCashField(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 2: To Bank (Dropdown)
|
||||
_buildToBankDropdown(banks),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 3: Amount
|
||||
_buildAmountInput(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 4: Date
|
||||
_buildDateInput(context),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 5: Description
|
||||
_buildDescriptionInput(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Row 6: Image Picker
|
||||
ReusableImagePicker(
|
||||
initialImage: _pickedImage,
|
||||
existingImageUrl: _existingImageUrl,
|
||||
onImagePicked: (file) {
|
||||
setState(() {
|
||||
_pickedImage = file;
|
||||
if (file != null) _existingImageUrl = null;
|
||||
});
|
||||
},
|
||||
onImageRemoved: () {
|
||||
setState(() {
|
||||
_pickedImage = null;
|
||||
_existingImageUrl = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: _resetOrCancel,
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
side: const BorderSide(color: Colors.red),
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
child: Text(_lang.cancel),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
backgroundColor: const Color(0xFFB71C1C),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: Text(saveButtonText),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Widget Builders ---
|
||||
|
||||
Widget _buildStaticCashField() {
|
||||
return TextFormField(
|
||||
initialValue: 'Cash',
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).from, // Changed label to 'From'
|
||||
hintText: l.S.of(context).cash,
|
||||
filled: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToBankDropdown(List<BankData> banks) {
|
||||
return DropdownButtonFormField<BankData>(
|
||||
value: _toBank,
|
||||
icon: Icon(Icons.keyboard_arrow_down),
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).to,
|
||||
hintText: l.S.of(context).selectOneAccount,
|
||||
),
|
||||
validator: (value) => value == null ? l.S.of(context).selectAccount : null,
|
||||
items: banks.map((bank) {
|
||||
return DropdownMenuItem<BankData>(
|
||||
value: bank,
|
||||
child: Text(bank.name ?? 'Unknown'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (BankData? newValue) {
|
||||
setState(() {
|
||||
_toBank = newValue;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAmountInput() {
|
||||
return TextFormField(
|
||||
controller: amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).amount,
|
||||
hintText: 'Ex: 500',
|
||||
prefixText: currency,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) return l.S.of(context).amountsIsRequired;
|
||||
if (num.tryParse(value) == null || num.parse(value) <= 0) return l.S.of(context).invalidAmount;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateInput(BuildContext context) {
|
||||
return TextFormField(
|
||||
readOnly: true,
|
||||
controller: dateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).adjustmentDate,
|
||||
hintText: 'DD/MM/YYYY',
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
onPressed: () => _selectDate(context),
|
||||
),
|
||||
),
|
||||
validator: (value) => value!.isEmpty ? l.S.of(context).dateIsRequired : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescriptionInput() {
|
||||
return TextFormField(
|
||||
controller: descriptionController,
|
||||
maxLines: 4,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).description,
|
||||
hintText: l.S.of(context).enterDescription,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// File: cash_transaction_model.dart
|
||||
|
||||
class CashTransactionModel {
|
||||
final String? message;
|
||||
final List<CashTransactionData>? data;
|
||||
final num? totalBalance;
|
||||
|
||||
CashTransactionModel({this.message, this.data, this.totalBalance});
|
||||
|
||||
factory CashTransactionModel.fromJson(Map<String, dynamic> json) {
|
||||
return CashTransactionModel(
|
||||
message: json['message'],
|
||||
data: (json['data'] as List<dynamic>?)?.map((e) => CashTransactionData.fromJson(e as Map<String, dynamic>)).toList(),
|
||||
totalBalance: json['total_balance'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CashTransactionData {
|
||||
final int? id;
|
||||
final String? platform;
|
||||
final String? transactionType;
|
||||
final String? type;
|
||||
final num? amount;
|
||||
final String? date;
|
||||
final num? fromBank;
|
||||
final num? toBank;
|
||||
final String? invoiceNo;
|
||||
final String? image;
|
||||
final String? note;
|
||||
final User? user;
|
||||
|
||||
CashTransactionData({
|
||||
this.id,
|
||||
this.platform,
|
||||
this.transactionType,
|
||||
this.type,
|
||||
this.amount,
|
||||
this.date,
|
||||
this.fromBank,
|
||||
this.toBank,
|
||||
this.invoiceNo,
|
||||
this.image,
|
||||
this.note,
|
||||
this.user,
|
||||
});
|
||||
|
||||
factory CashTransactionData.fromJson(Map<String, dynamic> json) {
|
||||
return CashTransactionData(
|
||||
id: json['id'],
|
||||
platform: json['platform'],
|
||||
transactionType: json['transaction_type'],
|
||||
type: json['type'],
|
||||
amount: json['amount'],
|
||||
date: json['date'],
|
||||
fromBank: json['from_bank'],
|
||||
toBank: json['to_bank'],
|
||||
invoiceNo: json['invoice_no'],
|
||||
image: json['image'],
|
||||
note: json['note'],
|
||||
user: json['user'] != null ? User.fromJson(json['user']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class User {
|
||||
final int? id;
|
||||
final String? name;
|
||||
|
||||
User({this.id, this.name});
|
||||
|
||||
factory User.fromJson(Map<String, dynamic> json) {
|
||||
return User(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// File: cash_transaction_provider.dart
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../model/cash_transaction_list_model.dart';
|
||||
import '../repo/cash_in_hand_repo.dart';
|
||||
|
||||
// Simple AutoDisposeProvider for the cash transaction history list
|
||||
// Note: You can optionally make this a FamilyProvider if filtering is complex.
|
||||
final cashTransactionHistoryProvider = FutureProvider.autoDispose<CashTransactionModel>((ref) async {
|
||||
final repo = CashTransactionRepo();
|
||||
|
||||
return repo.fetchCashTransactions(filter: null);
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
// File: cash_transaction_repo.dart
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/http_client/customer_http_client_get.dart';
|
||||
|
||||
// --- Local Imports ---
|
||||
import '../../../../Const/api_config.dart';
|
||||
import '../../../../http_client/custome_http_client.dart';
|
||||
import '../../bank account/provider/bank_account_provider.dart';
|
||||
import '../model/cash_transaction_list_model.dart';
|
||||
import '../provider/cash_in_hand_provider.dart';
|
||||
|
||||
class CashTransactionRepo {
|
||||
static const String _endpoint = '/cashes';
|
||||
|
||||
// --- FETCH Cash Transactions ---
|
||||
Future<CashTransactionModel> fetchCashTransactions({
|
||||
required String? filter,
|
||||
}) async {
|
||||
// NOTE: Filter logic (date range) would be implemented here in the real code
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint');
|
||||
|
||||
try {
|
||||
CustomHttpClientGet customHttpClient = CustomHttpClientGet(client: http.Client());
|
||||
final response = await customHttpClient.get(
|
||||
url: uri,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return CashTransactionModel.fromJson(jsonDecode(response.body));
|
||||
} else {
|
||||
throw Exception('Failed to load cash data: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Network Error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
///---------------- CREATE CASH TRANSFER (e.g., Cash to Bank) (POST - FORM-DATA) ----------------///
|
||||
Future<void> createCashTransfer({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num fromBankId, // Should be 0 for Cash
|
||||
required num toBankId, // Destination Bank ID
|
||||
required num amount,
|
||||
required String date, // YYYY-MM-DD
|
||||
required String transactionType, // Should be 'cash_to_bank'
|
||||
required String type,
|
||||
String? note,
|
||||
File? image, // Optional image file
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint');
|
||||
|
||||
final Map<String, String> fields = {
|
||||
// NOTE: API expects 'from' to be the bank ID or cash identifier, 'to' is the destination
|
||||
'transaction_type': transactionType,
|
||||
'amount': amount.toString(),
|
||||
'date': date,
|
||||
'from': 'Cash',
|
||||
if (transactionType != 'adjust_cash') 'to': toBankId.toString(),
|
||||
'note': note ?? '',
|
||||
'type': type,
|
||||
// 'platform': 'cash', // Platform should be 'cash' for this endpoint
|
||||
};
|
||||
print('POSTING DATA: $fields');
|
||||
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Transferring...');
|
||||
|
||||
var streamedResponse = await customHttpClient.uploadFile(
|
||||
url: uri,
|
||||
file: image,
|
||||
fileFieldName: 'image',
|
||||
fields: fields,
|
||||
);
|
||||
|
||||
var response = await http.Response.fromStream(streamedResponse);
|
||||
final parsedData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
ref.invalidate(cashTransactionHistoryProvider); // Refresh Cash History
|
||||
ref.invalidate(bankListProvider); // Refresh Bank List (since a bank balance changed)
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Transfer successful')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Transfer failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///---------------- UPDATE CASH TRANSFER (e.g., Cash to Bank) (POST/PUT/PATCH - FORM-DATA) ----------------///
|
||||
Future<void> updateCashTransfer({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num transactionId,
|
||||
required num fromBankId, // Should be 0 for Cash
|
||||
required num toBankId, // Destination Bank ID
|
||||
required num amount,
|
||||
required String date, // YYYY-MM-DD
|
||||
required String transactionType, // Should be 'cash_to_bank'
|
||||
required String type,
|
||||
String? note,
|
||||
File? image, // Optional: New image file to upload
|
||||
String? existingImageUrl, // Optional: Used to determine if image was removed
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint/$transactionId');
|
||||
|
||||
final Map<String, String> fields = {
|
||||
'transaction_type': transactionType,
|
||||
'amount': amount.toString(),
|
||||
'date': date,
|
||||
'from': fromBankId.toString(),
|
||||
if (transactionType != 'adjust_cash')'to': toBankId.toString(),
|
||||
'note': note ?? '',
|
||||
'type': type,
|
||||
'platform': 'cash',
|
||||
'_method': 'PUT',
|
||||
'image_removed': (image == null && existingImageUrl == null) ? '1' : '0',
|
||||
};
|
||||
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Updating...');
|
||||
|
||||
var streamedResponse = await customHttpClient.uploadFile(
|
||||
url: uri,
|
||||
file: image,
|
||||
fileFieldName: 'image',
|
||||
fields: fields,
|
||||
permission: 'cash_transaction_edit_permit',
|
||||
);
|
||||
|
||||
var response = await http.Response.fromStream(streamedResponse);
|
||||
final parsedData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
ref.invalidate(cashTransactionHistoryProvider); // Refresh Cash History
|
||||
ref.invalidate(bankListProvider); // Refresh Bank List
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Update successful')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Update failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- DELETE Cash Transaction ---
|
||||
Future<bool> deleteCashTransaction({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num transactionId,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}$_endpoint/$transactionId');
|
||||
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Deleting...');
|
||||
|
||||
var response = await customHttpClient.delete(
|
||||
url: uri,
|
||||
permission: 'cash_transaction_delete_permit',
|
||||
);
|
||||
|
||||
final parsedData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 204) {
|
||||
ref.invalidate(cashTransactionHistoryProvider); // Refresh the cash history list
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(parsedData['message'] ?? 'Transaction deleted successfully!')),
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Deletion failed: ${parsedData['message'] ?? 'Unknown error'}')),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred: $error')),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user