marco.pms.mobileapp/lib/model/finance/add_payment_request_bottom_sheet.dart

397 lines
14 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/finance/add_payment_request_controller.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
import 'package:marco/helpers/utils/validators.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/widgets/expense/expense_form_widgets.dart';
import 'package:marco/helpers/widgets/my_confirmation_dialog.dart';
/// Show Payment Request Bottom Sheet
Future<T?> showPaymentRequestBottomSheet<T>({bool isEdit = false}) {
return Get.bottomSheet<T>(
_PaymentRequestBottomSheet(isEdit: isEdit),
isScrollControlled: true,
);
}
class _PaymentRequestBottomSheet extends StatefulWidget {
final bool isEdit;
const _PaymentRequestBottomSheet({this.isEdit = false});
@override
State<_PaymentRequestBottomSheet> createState() =>
_PaymentRequestBottomSheetState();
}
class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet>
with UIMixin {
final PaymentRequestController controller =
Get.put(PaymentRequestController());
final _formKey = GlobalKey<FormState>();
final GlobalKey _projectDropdownKey = GlobalKey();
final GlobalKey _categoryDropdownKey = GlobalKey();
final GlobalKey _payeeDropdownKey = GlobalKey();
final GlobalKey _currencyDropdownKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Obx(
() => Form(
key: _formKey,
child: BaseBottomSheet(
title:
widget.isEdit ? "Edit Payment Request" : "Create Payment Request",
isSubmitting: false,
onCancel: Get.back,
onSubmit: () {
if (_formKey.currentState!.validate() && _validateSelections()) {
// Call your submit API here
showAppSnackbar(
title: "Success",
message: "Payment request submitted!",
type: SnackbarType.success,
);
Get.back();
}
},
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDropdownField<String>(
icon: Icons.work_outline,
title: " Select Project",
requiredField: true,
value: controller.selectedProject.value.isEmpty
? "Select Project"
: controller.selectedProject.value,
onTap: () => _showOptionList<String>(
controller.globalProjects.toList(),
(p) => p,
controller.selectProject,
_projectDropdownKey,
),
dropdownKey: _projectDropdownKey,
),
_gap(),
_buildDropdownField(
icon: Icons.category_outlined,
title: "Expense Category",
requiredField: true,
value: controller.selectedCategory.value?.name ??
"Select Category",
onTap: () => _showOptionList(
controller.categories.toList(),
(c) => c.name,
controller.selectCategory,
_categoryDropdownKey),
dropdownKey: _categoryDropdownKey,
),
_gap(),
_buildTextField(
icon: Icons.title_outlined,
title: "Title",
controller: TextEditingController(),
hint: "Enter title",
validator: Validators.requiredField,
),
_gap(),
// Is Advance Payment Radio Buttons with Icon and Primary Color
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.attach_money_outlined, size: 20),
SizedBox(width: 6),
Text(
"Is Advance Payment",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
MySpacing.height(6),
Obx(() => Row(
children: [
Expanded(
child: RadioListTile<bool>(
contentPadding: EdgeInsets.zero,
title: Text("Yes"),
value: true,
groupValue: controller.isAdvancePayment.value,
activeColor: contentTheme.primary,
onChanged: (val) {
if (val != null)
controller.isAdvancePayment.value = val;
},
),
),
Expanded(
child: RadioListTile<bool>(
contentPadding: EdgeInsets.zero,
title: Text("No"),
value: false,
groupValue: controller.isAdvancePayment.value,
activeColor: contentTheme.primary,
onChanged: (val) {
if (val != null)
controller.isAdvancePayment.value = val;
},
),
),
],
)),
_gap(),
],
),
_buildTextField(
icon: Icons.calendar_today,
title: "Due To Date",
controller: TextEditingController(),
hint: "DD-MM-YYYY",
validator: Validators.requiredField,
),
_gap(),
_buildTextField(
icon: Icons.currency_rupee,
title: "Amount",
controller: TextEditingController(),
hint: "Enter Amount",
keyboardType: TextInputType.number,
validator: (v) =>
(v != null && v.isNotEmpty && double.tryParse(v) != null)
? null
: "Enter valid amount",
),
_gap(),
_buildDropdownField<String>(
icon: Icons.person_outline,
title: "Payee",
requiredField: true,
value: controller.selectedPayee.value.isEmpty
? "Select Payee"
: controller.selectedPayee.value,
onTap: () => _showOptionList(controller.payees.toList(),
(p) => p, controller.selectPayee, _payeeDropdownKey),
dropdownKey: _payeeDropdownKey,
),
_gap(),
_buildDropdownField(
icon: Icons.monetization_on_outlined,
title: "Currency",
requiredField: true,
value: controller.selectedCurrency.value?.currencyName ??
"Select Currency",
onTap: () => _showOptionList(
controller.currencies.toList(),
(c) => c.currencyName, // <-- changed here
controller.selectCurrency,
_currencyDropdownKey),
dropdownKey: _currencyDropdownKey,
),
_gap(),
_buildTextField(
icon: Icons.description_outlined,
title: "Description",
controller: TextEditingController(),
hint: "Enter description",
maxLines: 3,
validator: Validators.requiredField,
),
_gap(),
_buildAttachmentsSection(),
],
),
),
),
),
);
}
Widget _gap([double h = 16]) => MySpacing.height(h);
Widget _buildDropdownField<T>({
required IconData icon,
required String title,
required bool requiredField,
required String value,
required VoidCallback onTap,
required GlobalKey dropdownKey,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionTitle(icon: icon, title: title, requiredField: requiredField),
MySpacing.height(6),
DropdownTile(key: dropdownKey, title: value, onTap: onTap),
],
);
}
Widget _buildTextField({
required IconData icon,
required String title,
required TextEditingController controller,
String? hint,
TextInputType? keyboardType,
FormFieldValidator<String>? validator,
int maxLines = 1,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionTitle(
icon: icon, title: title, requiredField: validator != null),
MySpacing.height(6),
CustomTextField(
controller: controller,
hint: hint ?? "",
keyboardType: keyboardType ?? TextInputType.text,
validator: validator,
maxLines: maxLines,
),
],
);
}
Widget _buildAttachmentsSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SectionTitle(
icon: Icons.attach_file,
title: "Attachments",
requiredField: true,
),
MySpacing.height(10),
Obx(() {
if (controller.isProcessingAttachment.value) {
return Center(
child: Column(
children: [
CircularProgressIndicator(
color: contentTheme.primary,
),
const SizedBox(height: 8),
Text(
"Processing image, please wait...",
style: TextStyle(
fontSize: 14,
color: contentTheme.primary,
),
),
],
),
);
}
return AttachmentsSection(
attachments: controller.attachments,
existingAttachments: controller.existingAttachments,
onRemoveNew: controller.removeAttachment,
controller: controller,
onRemoveExisting: (item) async {
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => ConfirmDialog(
title: "Remove Attachment",
message: "Are you sure you want to remove this attachment?",
confirmText: "Remove",
icon: Icons.delete,
confirmColor: Colors.redAccent,
onConfirm: () async {
final index = controller.existingAttachments.indexOf(item);
if (index != -1) {
controller.existingAttachments[index]['isActive'] = false;
controller.existingAttachments.refresh();
}
showAppSnackbar(
title: 'Removed',
message: 'Attachment has been removed.',
type: SnackbarType.success,
);
Navigator.pop(context);
},
),
);
},
onAdd: controller.pickAttachments,
);
}),
],
);
}
/// Generic option list for dropdowns
Future<void> _showOptionList<T>(List<T> options, String Function(T) getLabel,
ValueChanged<T> onSelected, GlobalKey key) async {
if (options.isEmpty) {
_showError("No options available");
return;
}
final RenderBox button =
key.currentContext!.findRenderObject() as RenderBox;
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
final position = button.localToGlobal(Offset.zero, ancestor: overlay);
final selected = await showMenu<T>(
context: context,
position: RelativeRect.fromLTRB(
position.dx,
position.dy + button.size.height,
overlay.size.width - position.dx - button.size.width,
0,
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
items: options
.map((opt) => PopupMenuItem<T>(
value: opt,
child: Text(getLabel(opt)),
))
.toList(),
);
if (selected != null) onSelected(selected);
}
bool _validateSelections() {
if (controller.selectedProject.value.isEmpty) {
_showError("Please select a project");
return false;
}
if (controller.selectedCategory.value == null) {
_showError("Please select a category");
return false;
}
if (controller.selectedPayee.value.isEmpty) {
_showError("Please select a payee");
return false;
}
if (controller.selectedCurrency.value == null) {
_showError("Please select currency");
return false;
}
return true;
}
void _showError(String msg) {
showAppSnackbar(
title: "Error",
message: msg,
type: SnackbarType.error,
);
}
}