marco.pms.mobileapp/lib/model/finance/make_expense_bottom_sheet.dart
Vaibhav Surve 5c53a3f4be Refactor project structure and rename from 'marco' to 'on field work'
- Updated import paths across multiple files to reflect the new package name.
- Changed application name and identifiers in CMakeLists.txt, Runner.rc, and other configuration files.
- Modified web index.html and manifest.json to update the app title and name.
- Adjusted macOS and Windows project settings to align with the new application name.
- Ensured consistency in naming across all relevant files and directories.
2025-11-22 14:20:37 +05:30

241 lines
7.7 KiB
Dart

// create_expense_bottom_sheet.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/helpers/widgets/expense/expense_form_widgets.dart';
import 'package:on_field_work/helpers/utils/validators.dart';
import 'package:on_field_work/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;
}
}