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'; import 'package:marco/model/expense/expense_status_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"), 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.", ), 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"), 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), 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), // Expense Status const _SectionTitle( icon: Icons.flag_outlined, title: "Status"), const SizedBox(height: 6), Obx(() { return _DropdownTile( title: controller.selectedExpenseStatus.value?.name ?? "Select Status", onTap: () => _showOptionList( context, controller.expenseStatuses.toList(), (s) => s.name, (val) => controller.selectedExpenseStatus.value = val, ), ); }), const SizedBox(height: 16), // Amount const _SectionTitle( icon: Icons.currency_rupee, title: "Amount"), 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", ), 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"), 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", ), 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: ElevatedButton.icon( onPressed: controller.submitExpense, icon: const Icon(Icons.check, size: 18), label: MyText.bodyMedium( "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), ), ), ), ], ) ], ), ); }), // Project Selection List Obx(() { if (!isProjectExpanded.value) return const SizedBox.shrink(); return Positioned( top: 110, left: 16, right: 16, child: Material( elevation: 4, borderRadius: BorderRadius.circular(12), child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.all(10), child: _buildProjectSelectionList(), ), ), ); }), ], ), ), ), ); } Widget _buildProjectSelectionList() { return ConstrainedBox( constraints: const BoxConstraints(maxHeight: 300), child: ListView.builder( shrinkWrap: true, itemCount: controller.globalProjects.length, itemBuilder: (context, index) { final project = controller.globalProjects[index]; final isSelected = project == controller.selectedProject.value; return RadioListTile( value: project, groupValue: controller.selectedProject.value, onChanged: (val) { controller.selectedProject.value = val!; isProjectExpanded.value = false; }, title: Text( project, style: TextStyle( fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: isSelected ? Colors.blueAccent : Colors.black87, ), ), activeColor: Colors.blueAccent, tileColor: isSelected ? Colors.blueAccent.withOpacity(0.1) : Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), visualDensity: const VisualDensity(vertical: -4), ); }, ), ); } 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; const _SectionTitle({required this.icon, required this.title}); @override Widget build(BuildContext context) { final color = Colors.grey[700]; return Row( children: [ Icon(icon, color: color, size: 18), const SizedBox(width: 8), MyText.bodyMedium(title, fontWeight: 600), ], ); } } 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), ], ), ), ); } }