476 lines
22 KiB
Dart
476 lines
22 KiB
Dart
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||
|
|
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
|
import 'package:mobile_pos/Screens/Due%20Calculation/Model/due_collection_model.dart';
|
||
|
|
import 'package:mobile_pos/Screens/Due%20Calculation/Repo/due_repo.dart';
|
||
|
|
import 'package:mobile_pos/Screens/invoice_details/due_invoice_details.dart';
|
||
|
|
import 'package:mobile_pos/core/theme/_app_colors.dart';
|
||
|
|
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||
|
|
import 'package:nb_utils/nb_utils.dart';
|
||
|
|
|
||
|
|
import '../../GlobalComponents/glonal_popup.dart';
|
||
|
|
import '../../Provider/profile_provider.dart';
|
||
|
|
import '../../constant.dart';
|
||
|
|
import '../../currency.dart';
|
||
|
|
import '../../widgets/multipal payment mathods/multi_payment_widget.dart';
|
||
|
|
import '../Customers/Model/parties_model.dart';
|
||
|
|
import 'Model/due_collection_invoice_model.dart';
|
||
|
|
import 'Providers/due_provider.dart';
|
||
|
|
|
||
|
|
class DueCollectionScreen extends StatefulWidget {
|
||
|
|
const DueCollectionScreen({super.key, required this.customerModel});
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<DueCollectionScreen> createState() => _DueCollectionScreenState();
|
||
|
|
final Party customerModel;
|
||
|
|
}
|
||
|
|
|
||
|
|
class _DueCollectionScreenState extends State<DueCollectionScreen> {
|
||
|
|
// Key for MultiPaymentWidget
|
||
|
|
final GlobalKey<MultiPaymentWidgetState> paymentWidgetKey = GlobalKey();
|
||
|
|
|
||
|
|
num paidAmount = 0;
|
||
|
|
num remainDueAmount = 0;
|
||
|
|
num dueAmount = 0;
|
||
|
|
|
||
|
|
num calculateDueAmount({required num total}) {
|
||
|
|
if (total < 0) {
|
||
|
|
remainDueAmount = 0;
|
||
|
|
} else {
|
||
|
|
remainDueAmount = dueAmount - total;
|
||
|
|
}
|
||
|
|
return dueAmount - total;
|
||
|
|
}
|
||
|
|
|
||
|
|
TextEditingController paidText = TextEditingController();
|
||
|
|
TextEditingController dateController = TextEditingController(text: DateTime.now().toString().substring(0, 10));
|
||
|
|
DateTime selectedDate = DateTime.now();
|
||
|
|
|
||
|
|
SalesDuesInvoice? selectedInvoice;
|
||
|
|
// int? paymentType; // Removed old single payment type
|
||
|
|
|
||
|
|
// List of items in our dropdown menu
|
||
|
|
int count = 0;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
// Listener to update state when paidText changes (either manually or via MultiPaymentWidget)
|
||
|
|
paidText.addListener(() {
|
||
|
|
if (paidText.text.isEmpty) {
|
||
|
|
if (mounted) {
|
||
|
|
setState(() {
|
||
|
|
paidAmount = 0;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
final val = double.tryParse(paidText.text) ?? 0;
|
||
|
|
// Validation: Cannot pay more than due
|
||
|
|
if (val <= dueAmount) {
|
||
|
|
if (mounted) {
|
||
|
|
setState(() {
|
||
|
|
paidAmount = val;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// If widget pushes value > due, or user types > due
|
||
|
|
// You might want to handle this gracefully.
|
||
|
|
// For now, keeping your old logic:
|
||
|
|
paidText.clear();
|
||
|
|
if (mounted) {
|
||
|
|
setState(() {
|
||
|
|
paidAmount = 0;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
EasyLoading.showError(lang.S.of(context).youCanNotPayMoreThenDue);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
count++;
|
||
|
|
return Consumer(builder: (context, consumerRef, __) {
|
||
|
|
final personalData = consumerRef.watch(businessInfoProvider);
|
||
|
|
final dueInvoiceData = consumerRef.watch(dueInvoiceListProvider(widget.customerModel.id?.round() ?? 0));
|
||
|
|
final _theme = Theme.of(context);
|
||
|
|
|
||
|
|
return personalData.when(data: (data) {
|
||
|
|
List<SalesDuesInvoice> items = [];
|
||
|
|
num openingDueAmount = 0;
|
||
|
|
|
||
|
|
return GlobalPopup(
|
||
|
|
child: Scaffold(
|
||
|
|
backgroundColor: kWhite,
|
||
|
|
appBar: AppBar(
|
||
|
|
backgroundColor: Colors.white,
|
||
|
|
title: Text(
|
||
|
|
lang.S.of(context).collectDue,
|
||
|
|
),
|
||
|
|
centerTitle: true,
|
||
|
|
iconTheme: const IconThemeData(color: Colors.black),
|
||
|
|
elevation: 0.0,
|
||
|
|
),
|
||
|
|
body: SingleChildScrollView(
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Padding(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16),
|
||
|
|
child: Column(
|
||
|
|
children: [
|
||
|
|
Row(
|
||
|
|
children: [
|
||
|
|
dueInvoiceData.when(data: (data) {
|
||
|
|
num totalDueInInvoice = 0;
|
||
|
|
if (data.salesDues?.isNotEmpty ?? false) {
|
||
|
|
for (var element in data.salesDues!) {
|
||
|
|
totalDueInInvoice += element.dueAmount ?? 0;
|
||
|
|
items.add(element);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
openingDueAmount = (data.due ?? 0) - totalDueInInvoice;
|
||
|
|
if (selectedInvoice == null) {
|
||
|
|
dueAmount = openingDueAmount;
|
||
|
|
}
|
||
|
|
|
||
|
|
return Expanded(
|
||
|
|
child: DropdownButtonFormField<SalesDuesInvoice>(
|
||
|
|
isExpanded: true,
|
||
|
|
value: selectedInvoice,
|
||
|
|
hint: Text(
|
||
|
|
lang.S.of(context).selectAInvoice,
|
||
|
|
),
|
||
|
|
icon: selectedInvoice != null
|
||
|
|
? GestureDetector(
|
||
|
|
onTap: () {
|
||
|
|
setState(() {
|
||
|
|
selectedInvoice = null;
|
||
|
|
// Reset payment widget when invoice is cleared
|
||
|
|
// paymentWidgetKey.currentState?.clear();
|
||
|
|
});
|
||
|
|
},
|
||
|
|
child: const Icon(
|
||
|
|
Icons.close,
|
||
|
|
color: Colors.red,
|
||
|
|
size: 16,
|
||
|
|
),
|
||
|
|
)
|
||
|
|
: const Icon(Icons.keyboard_arrow_down, color: kGreyTextColor),
|
||
|
|
items: items.map((SalesDuesInvoice invoice) {
|
||
|
|
return DropdownMenuItem(
|
||
|
|
value: invoice,
|
||
|
|
child: Text(
|
||
|
|
invoice.invoiceNumber.toString(),
|
||
|
|
style: _theme.textTheme.bodyMedium,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}).toList(),
|
||
|
|
onChanged: (newValue) {
|
||
|
|
setState(() {
|
||
|
|
dueAmount = newValue?.dueAmount ?? 0;
|
||
|
|
paidAmount = 0;
|
||
|
|
paidText.clear();
|
||
|
|
selectedInvoice = newValue;
|
||
|
|
// Reset payment widget when invoice changes
|
||
|
|
// paymentWidgetKey.currentState?.clear();
|
||
|
|
});
|
||
|
|
},
|
||
|
|
decoration: const InputDecoration(),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}, error: (e, stack) {
|
||
|
|
return Text(e.toString());
|
||
|
|
}, loading: () {
|
||
|
|
return const Center(child: CircularProgressIndicator());
|
||
|
|
}),
|
||
|
|
const SizedBox(width: 14),
|
||
|
|
Expanded(
|
||
|
|
child: TextFormField(
|
||
|
|
keyboardType: TextInputType.name,
|
||
|
|
readOnly: true,
|
||
|
|
controller: dateController,
|
||
|
|
decoration: InputDecoration(
|
||
|
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||
|
|
labelText: lang.S.of(context).date,
|
||
|
|
border: const OutlineInputBorder(),
|
||
|
|
suffixIcon: IconButton(
|
||
|
|
onPressed: () async {
|
||
|
|
final DateTime? picked = await showDatePicker(
|
||
|
|
initialDate: DateTime.now(),
|
||
|
|
firstDate: DateTime(2015, 8),
|
||
|
|
lastDate: DateTime(2101),
|
||
|
|
context: context,
|
||
|
|
);
|
||
|
|
if (picked != null) {
|
||
|
|
setState(() {
|
||
|
|
selectedDate = selectedDate.copyWith(
|
||
|
|
year: picked.year,
|
||
|
|
month: picked.month,
|
||
|
|
day: picked.day,
|
||
|
|
);
|
||
|
|
dateController.text = picked.toString().substring(0, 10);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
},
|
||
|
|
icon: const Icon(FeatherIcons.calendar),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
const SizedBox(height: 20),
|
||
|
|
Row(
|
||
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
||
|
|
children: [
|
||
|
|
RichText(
|
||
|
|
text: TextSpan(
|
||
|
|
text: "${lang.S.of(context).totalDueAmount}: ",
|
||
|
|
style: _theme.textTheme.bodyMedium?.copyWith(
|
||
|
|
fontSize: 14,
|
||
|
|
color: DAppColors.kSecondary,
|
||
|
|
),
|
||
|
|
children: [
|
||
|
|
TextSpan(
|
||
|
|
text: widget.customerModel.due == null
|
||
|
|
? '$currency${0}'
|
||
|
|
: '$currency${widget.customerModel.due!}',
|
||
|
|
style: const TextStyle(color: Color(0xFFFF8C34)),
|
||
|
|
),
|
||
|
|
]),
|
||
|
|
)
|
||
|
|
],
|
||
|
|
),
|
||
|
|
const SizedBox(height: 10),
|
||
|
|
TextFormField(
|
||
|
|
keyboardType: TextInputType.name,
|
||
|
|
readOnly: true,
|
||
|
|
initialValue: widget.customerModel.name,
|
||
|
|
decoration: InputDecoration(
|
||
|
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||
|
|
labelText: lang.S.of(context).customerName,
|
||
|
|
border: const OutlineInputBorder(),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 24),
|
||
|
|
|
||
|
|
///_____Total______________________________
|
||
|
|
Container(
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
borderRadius: const BorderRadius.all(
|
||
|
|
Radius.circular(5),
|
||
|
|
),
|
||
|
|
color: _theme.colorScheme.primaryContainer,
|
||
|
|
boxShadow: [
|
||
|
|
BoxShadow(
|
||
|
|
color: const Color(0xff000000).withValues(alpha: 0.08),
|
||
|
|
spreadRadius: 0,
|
||
|
|
offset: const Offset(0, 4),
|
||
|
|
blurRadius: 24,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
child: Column(
|
||
|
|
children: [
|
||
|
|
Container(
|
||
|
|
padding: const EdgeInsets.all(10),
|
||
|
|
decoration: const BoxDecoration(
|
||
|
|
color: Color(0xffFEF0F1),
|
||
|
|
borderRadius: BorderRadius.only(
|
||
|
|
topRight: Radius.circular(5),
|
||
|
|
topLeft: Radius.circular(5),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
child: Row(
|
||
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
lang.S.of(context).totalAmount,
|
||
|
|
style: const TextStyle(fontSize: 16),
|
||
|
|
),
|
||
|
|
Text(
|
||
|
|
dueAmount.toStringAsFixed(2),
|
||
|
|
style: const TextStyle(fontSize: 16),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
Padding(
|
||
|
|
padding: const EdgeInsets.all(10.0),
|
||
|
|
child: Row(
|
||
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
lang.S.of(context).paidAmount,
|
||
|
|
style: const TextStyle(fontSize: 16),
|
||
|
|
),
|
||
|
|
SizedBox(
|
||
|
|
width: context.width() / 4,
|
||
|
|
height: 30,
|
||
|
|
child: TextFormField(
|
||
|
|
controller: paidText,
|
||
|
|
// Make ReadOnly if multiple payments are selected to avoid conflict
|
||
|
|
readOnly: (paymentWidgetKey.currentState?.getPaymentEntries().length ?? 1) > 1,
|
||
|
|
textAlign: TextAlign.right,
|
||
|
|
decoration: const InputDecoration(
|
||
|
|
hintText: '0',
|
||
|
|
hintStyle: TextStyle(color: kNeutralColor),
|
||
|
|
border: UnderlineInputBorder(borderSide: BorderSide(color: kBorder)),
|
||
|
|
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: kBorder)),
|
||
|
|
focusedBorder: UnderlineInputBorder(),
|
||
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 0, vertical: 8),
|
||
|
|
),
|
||
|
|
keyboardType: TextInputType.number,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
Padding(
|
||
|
|
padding: const EdgeInsets.all(10.0),
|
||
|
|
child: Row(
|
||
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
lang.S.of(context).dueAmount,
|
||
|
|
style: const TextStyle(fontSize: 16),
|
||
|
|
),
|
||
|
|
Text(
|
||
|
|
calculateDueAmount(total: paidAmount).toStringAsFixed(2),
|
||
|
|
style: const TextStyle(fontSize: 16),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 10),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
///__________Payment_Type_Widget_______________________________________
|
||
|
|
Padding(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||
|
|
child: Column(
|
||
|
|
children: [
|
||
|
|
const Divider(height: 20),
|
||
|
|
MultiPaymentWidget(
|
||
|
|
key: paymentWidgetKey,
|
||
|
|
showWalletOption: true, // Configure as needed
|
||
|
|
showChequeOption: (widget.customerModel.type != 'Supplier'), // Configure as needed
|
||
|
|
totalAmountController: paidText,
|
||
|
|
onPaymentListChanged: () {},
|
||
|
|
),
|
||
|
|
const Divider(height: 20),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
bottomNavigationBar: Padding(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16),
|
||
|
|
child: Row(
|
||
|
|
children: [
|
||
|
|
Expanded(
|
||
|
|
child: OutlinedButton(
|
||
|
|
style: OutlinedButton.styleFrom(
|
||
|
|
maximumSize: const Size(double.infinity, 48),
|
||
|
|
minimumSize: const Size(double.infinity, 48),
|
||
|
|
disabledBackgroundColor: _theme.colorScheme.primary.withValues(alpha: 0.15),
|
||
|
|
),
|
||
|
|
onPressed: () async {
|
||
|
|
Navigator.pop(context);
|
||
|
|
},
|
||
|
|
child: Text(
|
||
|
|
lang.S.of(context).cancel,
|
||
|
|
maxLines: 1,
|
||
|
|
overflow: TextOverflow.ellipsis,
|
||
|
|
style: _theme.textTheme.bodyMedium?.copyWith(
|
||
|
|
color: _theme.colorScheme.primary,
|
||
|
|
fontWeight: FontWeight.w600,
|
||
|
|
fontSize: 16,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 20),
|
||
|
|
Expanded(
|
||
|
|
child: ElevatedButton(
|
||
|
|
style: OutlinedButton.styleFrom(
|
||
|
|
maximumSize: const Size(double.infinity, 48),
|
||
|
|
minimumSize: const Size(double.infinity, 48),
|
||
|
|
disabledBackgroundColor: _theme.colorScheme.primary.withValues(alpha: 0.15),
|
||
|
|
),
|
||
|
|
onPressed: () async {
|
||
|
|
if (paidAmount > 0 && dueAmount > 0) {
|
||
|
|
// Get payments from widget
|
||
|
|
List<PaymentEntry> payments = paymentWidgetKey.currentState?.getPaymentEntries() ?? [];
|
||
|
|
|
||
|
|
if (payments.isEmpty) {
|
||
|
|
EasyLoading.showError(lang.S.of(context).noDueSelected); // Or "Please select payment"
|
||
|
|
} else {
|
||
|
|
EasyLoading.show();
|
||
|
|
|
||
|
|
// Serialize Payment List
|
||
|
|
List<Map<String, dynamic>> paymentData = payments.map((e) => e.toJson()).toList();
|
||
|
|
|
||
|
|
DueRepo repo = DueRepo();
|
||
|
|
DueCollection? dueData;
|
||
|
|
dueData = await repo.dueCollect(
|
||
|
|
ref: consumerRef,
|
||
|
|
context: context,
|
||
|
|
partyId: widget.customerModel.id ?? 0,
|
||
|
|
invoiceNumber: selectedInvoice?.invoiceNumber,
|
||
|
|
paymentDate: selectedDate.toIso8601String(),
|
||
|
|
payments: paymentData,
|
||
|
|
payDueAmount: paidAmount,
|
||
|
|
);
|
||
|
|
|
||
|
|
if (dueData != null) {
|
||
|
|
DueInvoiceDetails(
|
||
|
|
dueCollection: dueData,
|
||
|
|
personalInformationModel: data,
|
||
|
|
isFromDue: true,
|
||
|
|
).launch(context);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
EasyLoading.showError(
|
||
|
|
lang.S.of(context).noDueSelected,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
child: Text(
|
||
|
|
lang.S.of(context).save,
|
||
|
|
maxLines: 1,
|
||
|
|
overflow: TextOverflow.ellipsis,
|
||
|
|
style: _theme.textTheme.bodyMedium?.copyWith(
|
||
|
|
color: _theme.colorScheme.primaryContainer,
|
||
|
|
fontWeight: FontWeight.w600,
|
||
|
|
fontSize: 16,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}, error: (e, stack) {
|
||
|
|
return Center(
|
||
|
|
child: Text(e.toString()),
|
||
|
|
);
|
||
|
|
}, loading: () {
|
||
|
|
return const Center(child: CircularProgressIndicator());
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|