241 lines
7.6 KiB
Dart
241 lines
7.6 KiB
Dart
// create_expense_bottom_sheet.dart
|
|
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:marco/helpers/utils/base_bottom_sheet.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/utils/validators.dart';
|
|
import 'package:marco/controller/finance/payment_request_detail_controller.dart';
|
|
|
|
Future<T?> showCreateExpenseBottomSheet<T>({required String statusId}) {
|
|
return Get.bottomSheet<T>(
|
|
_CreateExpenseBottomSheet(statusId: statusId),
|
|
isScrollControlled: true,
|
|
);
|
|
}
|
|
|
|
class _CreateExpenseBottomSheet extends StatefulWidget {
|
|
final String statusId;
|
|
|
|
const _CreateExpenseBottomSheet({required this.statusId, Key? key})
|
|
: super(key: key);
|
|
@override
|
|
State<_CreateExpenseBottomSheet> createState() =>
|
|
_CreateExpenseBottomSheetState();
|
|
}
|
|
|
|
class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> {
|
|
final controller = Get.put(PaymentRequestDetailController());
|
|
final _formKey = GlobalKey<FormState>();
|
|
final TextEditingController commentController = TextEditingController();
|
|
|
|
final _paymentModeDropdownKey = GlobalKey();
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Obx(
|
|
() => Form(
|
|
key: _formKey,
|
|
child: BaseBottomSheet(
|
|
title: "Create New Expense",
|
|
isSubmitting: controller.isSubmitting.value,
|
|
onCancel: Get.back,
|
|
onSubmit: () async {
|
|
if (_formKey.currentState!.validate() && _validateSelections()) {
|
|
final success = await controller.submitExpense(
|
|
statusId: widget.statusId,
|
|
comment: commentController.text.trim(),
|
|
);
|
|
if (success) {
|
|
Get.back();
|
|
showAppSnackbar(
|
|
title: "Success",
|
|
message: "Expense created successfully!",
|
|
type: SnackbarType.success,
|
|
);
|
|
}
|
|
}
|
|
;
|
|
},
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildDropdown(
|
|
"Payment Mode*",
|
|
Icons.payment_outlined,
|
|
controller.selectedPaymentMode.value?.name ?? "Select Mode",
|
|
controller.paymentModes,
|
|
(p) => p.name,
|
|
controller.selectPaymentMode,
|
|
key: _paymentModeDropdownKey,
|
|
),
|
|
_gap(),
|
|
_buildTextField(
|
|
"GST Number",
|
|
Icons.receipt_outlined,
|
|
controller.gstNumberController,
|
|
hint: "Enter GST Number",
|
|
validator: null,
|
|
),
|
|
_gap(),
|
|
_buildTextField(
|
|
"Location*",
|
|
Icons.location_on_outlined,
|
|
controller.locationController,
|
|
hint: "Enter location",
|
|
validator: Validators.requiredField,
|
|
keyboardType: TextInputType.text,
|
|
suffixIcon: IconButton(
|
|
icon: const Icon(Icons.my_location_outlined),
|
|
onPressed: () async {
|
|
await controller.fetchCurrentLocation();
|
|
},
|
|
),
|
|
),
|
|
_gap(),
|
|
_buildAttachmentField(),
|
|
_gap(),
|
|
_buildTextField(
|
|
"Comment",
|
|
Icons.comment_outlined,
|
|
commentController,
|
|
hint: "Enter a comment (optional)",
|
|
validator: null,
|
|
),
|
|
_gap(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDropdown<T>(String title, IconData icon, String value,
|
|
List<T> options, String Function(T) getLabel, ValueChanged<T> onSelected,
|
|
{required GlobalKey key}) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SectionTitle(icon: icon, title: title, requiredField: true),
|
|
MySpacing.height(6),
|
|
DropdownTile(
|
|
key: key,
|
|
title: value,
|
|
onTap: () => _showOptionList(options, getLabel, onSelected, key),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildTextField(
|
|
String title,
|
|
IconData icon,
|
|
TextEditingController controller, {
|
|
String? hint,
|
|
FormFieldValidator<String>? validator,
|
|
TextInputType? keyboardType,
|
|
Widget? suffixIcon, // add this
|
|
}) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SectionTitle(
|
|
icon: icon, title: title, requiredField: validator != null),
|
|
MySpacing.height(6),
|
|
CustomTextField(
|
|
controller: controller,
|
|
hint: hint ?? "",
|
|
validator: validator,
|
|
keyboardType: keyboardType ?? TextInputType.text,
|
|
suffixIcon: suffixIcon,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildAttachmentField() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SectionTitle(
|
|
icon: Icons.attach_file,
|
|
title: "Upload Bill*",
|
|
requiredField: true),
|
|
MySpacing.height(6),
|
|
Obx(() {
|
|
if (controller.isProcessingAttachment.value) {
|
|
return Center(
|
|
child: Column(
|
|
children: const [
|
|
CircularProgressIndicator(),
|
|
SizedBox(height: 8),
|
|
Text("Processing file, please wait..."),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
return AttachmentsSection(
|
|
attachments: controller.attachments,
|
|
existingAttachments: controller.existingAttachments,
|
|
onRemoveNew: controller.removeAttachment,
|
|
controller: controller,
|
|
onAdd: controller.pickAttachments,
|
|
);
|
|
}),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _gap([double h = 16]) => MySpacing.height(h);
|
|
|
|
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.selectedPaymentMode.value == null) {
|
|
return _showError("Please select a payment mode");
|
|
}
|
|
if (controller.locationController.text.trim().isEmpty) {
|
|
return _showError("Please enter location");
|
|
}
|
|
if (controller.attachments.isEmpty) {
|
|
return _showError("Please upload bill");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool _showError(String msg) {
|
|
showAppSnackbar(title: "Error", message: msg, type: SnackbarType.error);
|
|
return false;
|
|
}
|
|
}
|