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