import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/expense/add_expense_controller.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/model/expense/payment_types_model.dart'; import 'package:marco/model/expense/expense_type_model.dart'; void showAddExpenseBottomSheet() { Get.bottomSheet(const _AddExpenseBottomSheet(), isScrollControlled: true); } class _AddExpenseBottomSheet extends StatefulWidget { const _AddExpenseBottomSheet(); @override State<_AddExpenseBottomSheet> createState() => _AddExpenseBottomSheetState(); } class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> { final AddExpenseController controller = Get.put(AddExpenseController()); void _showEmployeeList() { showModalBottomSheet( context: context, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16))), builder: (_) => Obx(() { final employees = controller.allEmployees; return SizedBox( height: 300, child: ListView.builder( itemCount: employees.length, itemBuilder: (_, index) { final emp = employees[index]; final fullName = '${emp.firstName} ${emp.lastName}'.trim(); return ListTile( title: Text(fullName.isNotEmpty ? fullName : "Unnamed"), onTap: () { controller.selectedPaidBy.value = emp; Navigator.pop(context); }, ); }, ), ); }), ); } Future _showOptionList( List options, String Function(T) getLabel, ValueChanged onSelected, ) async { final button = context.findRenderObject() as RenderBox; final overlay = Overlay.of(context).context.findRenderObject() as RenderBox; final position = button.localToGlobal(Offset.zero, ancestor: overlay); final selected = await showMenu( context: context, position: RelativeRect.fromLTRB( position.dx, position.dy + button.size.height, position.dx + button.size.width, 0, ), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), items: options .map( (option) => PopupMenuItem( value: option, child: Text(getLabel(option)), ), ) .toList(), ); if (selected != null) onSelected(selected); } @override Widget build(BuildContext context) { return SafeArea( child: Padding( padding: const EdgeInsets.only(top: 60), child: Material( color: Colors.white, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), child: Obx(() { return SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const _DragHandle(), Center( child: MyText.titleLarge("Add Expense", fontWeight: 700), ), const SizedBox(height: 20), _buildSectionWithDropdown( icon: Icons.work_outline, title: "Project", requiredField: true, currentValue: controller.selectedProject.value.isEmpty ? "Select Project" : controller.selectedProject.value, onTap: () => _showOptionList( controller.globalProjects.toList(), (p) => p, (val) => controller.selectedProject.value = val), ), const SizedBox(height: 16), _buildSectionWithDropdown( icon: Icons.category_outlined, title: "Expense Type", requiredField: true, currentValue: controller.selectedExpenseType.value?.name ?? "Select Expense Type", onTap: () => _showOptionList( controller.expenseTypes.toList(), (e) => e.name, (val) => controller.selectedExpenseType.value = val, ), ), if (controller .selectedExpenseType.value?.noOfPersonsRequired == true) Padding( padding: const EdgeInsets.only(top: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _SectionTitle( icon: Icons.people_outline, title: "No. of Persons", requiredField: true, ), const SizedBox(height: 6), _CustomTextField( controller: controller.noOfPersonsController, hint: "Enter No. of Persons", keyboardType: TextInputType.number, ), ], ), ), const SizedBox(height: 16), _SectionTitle( icon: Icons.confirmation_number_outlined, title: "GST No.", ), const SizedBox(height: 6), _CustomTextField( controller: controller.gstController, hint: "Enter GST No.", ), const SizedBox(height: 16), _buildSectionWithDropdown( icon: Icons.payment, title: "Payment Mode", requiredField: true, currentValue: controller.selectedPaymentMode.value?.name ?? "Select Payment Mode", onTap: () => _showOptionList( controller.paymentModes.toList(), (m) => m.name, (val) => controller.selectedPaymentMode.value = val, ), ), const SizedBox(height: 16), _SectionTitle( icon: Icons.person_outline, title: "Paid By", requiredField: true), const SizedBox(height: 6), GestureDetector( onTap: _showEmployeeList, child: _TileContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( controller.selectedPaidBy.value == null ? "Select Paid By" : '${controller.selectedPaidBy.value?.firstName ?? ''} ${controller.selectedPaidBy.value?.lastName ?? ''}', style: const TextStyle(fontSize: 14), ), const Icon(Icons.arrow_drop_down, size: 22), ], ), ), ), const SizedBox(height: 16), _SectionTitle( icon: Icons.currency_rupee, title: "Amount", requiredField: true), const SizedBox(height: 6), _CustomTextField( controller: controller.amountController, hint: "Enter Amount", keyboardType: TextInputType.number, ), const SizedBox(height: 16), _SectionTitle( icon: Icons.store_mall_directory_outlined, title: "Supplier Name", requiredField: true), const SizedBox(height: 6), _CustomTextField( controller: controller.supplierController, hint: "Enter Supplier Name", ), const SizedBox(height: 16), _SectionTitle( icon: Icons.confirmation_number_outlined, title: "Transaction ID"), const SizedBox(height: 6), _CustomTextField( controller: controller.transactionIdController, hint: "Enter Transaction ID", ), const SizedBox(height: 16), _SectionTitle( icon: Icons.calendar_today, title: "Transaction Date", requiredField: true), const SizedBox(height: 6), GestureDetector( onTap: () => controller.pickTransactionDate(context), child: AbsorbPointer( child: _CustomTextField( controller: controller.transactionDateController, hint: "Select Transaction Date", ), ), ), const SizedBox(height: 16), _SectionTitle( icon: Icons.location_on_outlined, title: "Location"), const SizedBox(height: 6), TextField( controller: controller.locationController, decoration: InputDecoration( hintText: "Enter Location", filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8)), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10), suffixIcon: controller.isFetchingLocation.value ? const Padding( padding: EdgeInsets.all(12), child: SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ), ) : IconButton( icon: const Icon(Icons.my_location), tooltip: "Use Current Location", onPressed: controller.fetchCurrentLocation, ), ), ), const SizedBox(height: 16), _SectionTitle( icon: Icons.attach_file, title: "Attachments", requiredField: true), const SizedBox(height: 6), _AttachmentsSection( attachments: controller.attachments, onRemove: controller.removeAttachment, onAdd: controller.pickAttachments, ), const SizedBox(height: 16), _SectionTitle( icon: Icons.description_outlined, title: "Description", requiredField: true), const SizedBox(height: 6), _CustomTextField( controller: controller.descriptionController, hint: "Enter Description", maxLines: 3, ), const SizedBox(height: 24), Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: Get.back, icon: const Icon(Icons.close, size: 18), label: MyText.bodyMedium("Cancel", fontWeight: 600), style: OutlinedButton.styleFrom( minimumSize: const Size.fromHeight(48)), ), ), const SizedBox(width: 12), Expanded( child: Obx( () { final isLoading = controller.isSubmitting.value; return ElevatedButton.icon( onPressed: isLoading ? null : controller.submitExpense, icon: isLoading ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white), ) : const Icon(Icons.check, size: 18), label: MyText.bodyMedium( isLoading ? "Submitting..." : "Submit", color: Colors.white, fontWeight: 600, ), style: ElevatedButton.styleFrom( backgroundColor: Colors.indigo, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(vertical: 14), minimumSize: const Size.fromHeight(48), ), ); }, ), ), ], ), ], ), ); }), ), ), ); } Widget _buildSectionWithDropdown({ required IconData icon, required String title, required bool requiredField, required String currentValue, required VoidCallback onTap, Widget? extraWidget, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _SectionTitle(icon: icon, title: title, requiredField: requiredField), const SizedBox(height: 6), _DropdownTile(title: currentValue, onTap: onTap), if (extraWidget != null) extraWidget, ], ); } } class _DragHandle extends StatelessWidget { const _DragHandle(); @override Widget build(BuildContext context) { return Center( child: Container( width: 40, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: Colors.grey.shade400, borderRadius: BorderRadius.circular(2), ), ), ); } } class _SectionTitle extends StatelessWidget { final IconData icon; final String title; final bool requiredField; const _SectionTitle({ required this.icon, required this.title, this.requiredField = false, }); @override Widget build(BuildContext context) { final color = Colors.grey[700]; return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(icon, color: color, size: 18), const SizedBox(width: 8), RichText( text: TextSpan( style: DefaultTextStyle.of(context).style.copyWith( fontWeight: FontWeight.w600, color: Colors.black87, ), children: [ TextSpan(text: title), if (requiredField) const TextSpan( text: ' *', style: TextStyle(color: Colors.red), ), ], ), ), ], ); } } class _CustomTextField extends StatelessWidget { final TextEditingController controller; final String hint; final int maxLines; final TextInputType keyboardType; const _CustomTextField({ required this.controller, required this.hint, this.maxLines = 1, this.keyboardType = TextInputType.text, }); @override Widget build(BuildContext context) { return TextField( controller: controller, maxLines: maxLines, keyboardType: keyboardType, decoration: InputDecoration( hintText: hint, filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), ), ); } } class _DropdownTile extends StatelessWidget { final String title; final VoidCallback onTap; const _DropdownTile({ required this.title, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( title, style: const TextStyle(fontSize: 14, color: Colors.black87), overflow: TextOverflow.ellipsis, ), ), const Icon(Icons.arrow_drop_down), ], ), ), ); } } class _TileContainer extends StatelessWidget { final Widget child; const _TileContainer({required this.child}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade400), ), child: child, ); } } class _AttachmentsSection extends StatelessWidget { final List attachments; final ValueChanged onRemove; final VoidCallback onAdd; const _AttachmentsSection({ required this.attachments, required this.onRemove, required this.onAdd, }); @override Widget build(BuildContext context) { return Wrap( spacing: 8, runSpacing: 8, children: [ ...attachments.map((file) => _AttachmentTile(file: file, onRemove: () => onRemove(file))), GestureDetector( onTap: onAdd, child: Container( width: 80, height: 80, decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade400), borderRadius: BorderRadius.circular(8), color: Colors.grey.shade100, ), child: const Icon(Icons.add, size: 30, color: Colors.grey), ), ), ], ); } } class _AttachmentTile extends StatelessWidget { final File file; final VoidCallback onRemove; const _AttachmentTile({required this.file, required this.onRemove}); @override Widget build(BuildContext context) { final fileName = file.path.split('/').last; final extension = fileName.split('.').last.toLowerCase(); final isImage = ['jpg', 'jpeg', 'png'].contains(extension); IconData fileIcon = Icons.insert_drive_file; Color iconColor = Colors.blueGrey; switch (extension) { case 'pdf': fileIcon = Icons.picture_as_pdf; iconColor = Colors.redAccent; break; case 'doc': case 'docx': fileIcon = Icons.description; iconColor = Colors.blueAccent; break; case 'xls': case 'xlsx': fileIcon = Icons.table_chart; iconColor = Colors.green; break; case 'txt': fileIcon = Icons.article; iconColor = Colors.grey; break; } return Stack( clipBehavior: Clip.none, children: [ Container( width: 80, height: 80, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), color: Colors.grey.shade100, ), child: isImage ? ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.file(file, fit: BoxFit.cover), ) : Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(fileIcon, color: iconColor, size: 30), const SizedBox(height: 4), Text( extension.toUpperCase(), style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: iconColor), overflow: TextOverflow.ellipsis, ), ], ), ), Positioned( top: -6, right: -6, child: IconButton( icon: const Icon(Icons.close, color: Colors.red, size: 18), onPressed: onRemove, ), ), ], ); } }