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()); final RxBool isProjectExpanded = false.obs; void _showEmployeeList(BuildContext context) { final employees = controller.allEmployees; showModalBottomSheet( context: context, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (BuildContext context) { return Obx(() { return SizedBox( height: 300, child: ListView.builder( itemCount: employees.length, itemBuilder: (context, 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); }, ); }, ), ); }); }, ); } @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: Stack( children: [ Obx(() { return SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildDragHandle(), Center( child: MyText.titleLarge( "Add Expense", fontWeight: 700, ), ), const SizedBox(height: 20), // Project Dropdown const _SectionTitle( icon: Icons.work_outline, title: "Project", requiredField: true, ), const SizedBox(height: 6), Obx(() { return _DropdownTile( title: controller.selectedProject.value.isEmpty ? "Select Project" : controller.selectedProject.value, onTap: () => _showOptionList( context, controller.globalProjects.toList(), (p) => p, (val) => controller.selectedProject.value = val, ), ); }), const SizedBox(height: 16), // Expense Type & GST const _SectionTitle( icon: Icons.category_outlined, title: "Expense Type & GST No.", requiredField: true, ), const SizedBox(height: 6), Obx(() { return _DropdownTile( title: controller.selectedExpenseType.value?.name ?? "Select Expense Type", onTap: () => _showOptionList( context, controller.expenseTypes.toList(), (e) => e.name, (val) => controller.selectedExpenseType.value = val, ), ); }), const SizedBox(height: 8), _CustomTextField( controller: controller.gstController, hint: "Enter GST No.", ), const SizedBox(height: 16), // Payment Mode const _SectionTitle( icon: Icons.payment, title: "Payment Mode", requiredField: true, ), const SizedBox(height: 6), Obx(() { return _DropdownTile( title: controller.selectedPaymentMode.value?.name ?? "Select Payment Mode", onTap: () => _showOptionList( context, controller.paymentModes.toList(), (m) => m.name, (val) => controller.selectedPaymentMode.value = val, ), ); }), const SizedBox(height: 16), // Paid By const _SectionTitle( icon: Icons.person_outline, title: "Paid By", requiredField: true, ), const SizedBox(height: 6), Obx(() { final selected = controller.selectedPaidBy.value; return GestureDetector( onTap: () => _showEmployeeList(context), 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.shade400), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( selected == null ? "Select Paid By" : '${selected.firstName} ${selected.lastName}', style: const TextStyle(fontSize: 14), ), const Icon(Icons.arrow_drop_down, size: 22), ], ), ), ); }), const SizedBox(height: 16), // Amount const _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), // Supplier Name const _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), // Transaction ID const _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), // Location const _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), // Attachments Section const _SectionTitle( icon: Icons.attach_file, title: "Attachments", requiredField: true, ), const SizedBox(height: 6), Obx(() { return Wrap( spacing: 8, runSpacing: 8, children: [ ...controller.attachments.map((file) { final fileName = file.path.split('/').last; final extension = fileName.split('.').last.toLowerCase(); final isImage = ['jpg', 'jpeg', 'png'].contains(extension); IconData fileIcon; Color iconColor = Colors.blueAccent; 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; default: fileIcon = Icons.insert_drive_file; iconColor = Colors.blueGrey; } return Stack( 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: () => controller.removeAttachment(file), ), ), ], ); }).toList(), GestureDetector( onTap: controller.pickAttachments, 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), ), ), ], ); }), const SizedBox(height: 16), // Description const _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), // Action Buttons 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), ), ); }), ), ], ) ], ), ); }), ], ), ), ), ); } Future _showOptionList( BuildContext context, List options, String Function(T) getLabel, ValueChanged onSelected, ) async { final RenderBox button = context.findRenderObject() as RenderBox; final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; final Offset 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) { return PopupMenuItem( value: option, child: Text(getLabel(option)), ); }).toList(), ); if (selected != null) onSelected(selected); } Widget _buildDragHandle() => 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), ], ), ), ); } }