import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/directory/add_contact_controller.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:flutter/services.dart'; class AddContactBottomSheet extends StatelessWidget { AddContactBottomSheet({super.key}) { controller.resetForm(); nameController.clear(); orgController.clear(); tagTextController.clear(); addressController.clear(); descriptionController.clear(); emailControllers.add(TextEditingController()); emailLabels.add('Office'.obs); phoneControllers.add(TextEditingController()); phoneLabels.add('Work'.obs); } final controller = Get.put(AddContactController()); final formKey = GlobalKey(); final nameController = TextEditingController(); final orgController = TextEditingController(); final tagTextController = TextEditingController(); final addressController = TextEditingController(); final descriptionController = TextEditingController(); final RxList emailControllers = [].obs; final RxList emailLabels = [].obs; final RxList phoneControllers = [].obs; final RxList phoneLabels = [].obs; InputDecoration _inputDecoration(String hint) => InputDecoration( hintText: hint, hintStyle: MyTextStyle.bodySmall(xMuted: true), filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5), ), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), isDense: true, ); Widget _popupSelector({ required String hint, required RxString selectedValue, required List options, }) { return Obx(() => GestureDetector( onTap: () async { final selected = await showMenu( context: Navigator.of(Get.context!).overlay!.context, position: const RelativeRect.fromLTRB(100, 300, 100, 0), items: options .map((e) => PopupMenuItem(value: e, child: Text(e))) .toList(), ); if (selected != null) selectedValue.value = selected; }, child: AbsorbPointer( child: SizedBox( height: 48, child: TextFormField( readOnly: true, initialValue: selectedValue.value, style: const TextStyle(fontSize: 14), decoration: _inputDecoration(hint).copyWith( suffixIcon: const Icon(Icons.expand_more), ), ), ), ), )); } Widget _buildLabeledRow( String label, RxString selectedLabel, List options, String inputLabel, TextEditingController controller, TextInputType inputType, { VoidCallback? onRemove, }) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.labelMedium(label), MySpacing.height(8), _popupSelector( hint: "Label", selectedValue: selectedLabel, options: options, ), ], ), ), MySpacing.width(12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.labelMedium(inputLabel), MySpacing.height(8), TextFormField( controller: controller, keyboardType: inputType, maxLength: inputType == TextInputType.phone ? 10 : null, inputFormatters: inputType == TextInputType.phone ? [FilteringTextInputFormatter.digitsOnly] : [], decoration: _inputDecoration("Enter $inputLabel").copyWith( counterText: "", // hides length indicator ), validator: (value) { if (value == null || value.trim().isEmpty) { return "$inputLabel is required"; } final trimmed = value.trim(); if (inputType == TextInputType.phone) { if (!RegExp(r'^\d{10}$').hasMatch(trimmed)) { return "Enter a valid 10-digit phone number"; } if (RegExp(r'^0+$').hasMatch(trimmed)) { return "Phone number cannot be all zeroes"; } } if (inputType == TextInputType.emailAddress && !RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(trimmed)) { return "Enter a valid email address"; } return null; }, ), ], ), ), if (onRemove != null) Padding( padding: const EdgeInsets.only(top: 24), child: IconButton( icon: const Icon(Icons.remove_circle_outline, color: Colors.red), onPressed: onRemove, ), ), ], ); } Widget _buildEmailList() { return Column( children: List.generate(emailControllers.length, (index) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: _buildLabeledRow( "Email Label", emailLabels[index], ["Office", "Personal", "Other"], "Email", emailControllers[index], TextInputType.emailAddress, onRemove: emailControllers.length > 1 ? () { emailControllers.removeAt(index); emailLabels.removeAt(index); } : null, ), ); }), ); } Widget _buildPhoneList() { return Column( children: List.generate(phoneControllers.length, (index) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: _buildLabeledRow( "Phone Label", phoneLabels[index], ["Work", "Mobile", "Other"], "Phone", phoneControllers[index], TextInputType.phone, onRemove: phoneControllers.length > 1 ? () { phoneControllers.removeAt(index); phoneLabels.removeAt(index); } : null, ), ); }), ); } Widget _dropdownField({ required String label, required RxString selectedValue, required RxList options, }) { return Obx(() => SizedBox( height: 48, child: PopupMenuButton( onSelected: (value) => selectedValue.value = value, itemBuilder: (_) => options .map((item) => PopupMenuItem(value: item, child: Text(item))) .toList(), padding: EdgeInsets.zero, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), color: Colors.grey.shade100, ), alignment: Alignment.centerLeft, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( selectedValue.value.isEmpty ? label : selectedValue.value, style: const TextStyle(fontSize: 14), overflow: TextOverflow.ellipsis, ), ), const Icon(Icons.arrow_drop_down), ], ), ), ), )); } Widget _tagInputSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 48, child: TextField( controller: tagTextController, onChanged: controller.filterSuggestions, onSubmitted: (value) { controller.addEnteredTag(value); tagTextController.clear(); controller.clearSuggestions(); }, decoration: _inputDecoration("Start typing to add tags"), ), ), Obx(() => controller.filteredSuggestions.isEmpty ? const SizedBox() : _buildSuggestionsList()), MySpacing.height(8), Obx(() => Wrap( spacing: 8, children: controller.enteredTags .map((tag) => Chip( label: Text(tag), onDeleted: () => controller.removeEnteredTag(tag), )) .toList(), )), ], ); } Widget _buildSuggestionsList() => Container( margin: const EdgeInsets.only(top: 4), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4)], ), child: ListView.builder( shrinkWrap: true, itemCount: controller.filteredSuggestions.length, itemBuilder: (context, index) { final suggestion = controller.filteredSuggestions[index]; return ListTile( dense: true, title: Text(suggestion), onTap: () { controller.addEnteredTag(suggestion); tagTextController.clear(); controller.clearSuggestions(); }, ); }, ), ); Widget _sectionLabel(String title) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.labelLarge(title, fontWeight: 600), MySpacing.height(4), Divider(thickness: 1, color: Colors.grey.shade200), ], ); @override Widget build(BuildContext context) { return SingleChildScrollView( padding: MediaQuery.of(context).viewInsets, child: Container( decoration: BoxDecoration( color: Theme.of(context).cardColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), child: Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), child: Form( key: formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: MyText.titleMedium("Create New Contact", fontWeight: 700)), MySpacing.height(24), _sectionLabel("Basic Info"), MySpacing.height(16), _buildTextField("Name", nameController), MySpacing.height(16), _buildOrganizationField(), MySpacing.height(24), _sectionLabel("Contact Info"), MySpacing.height(16), Obx(() => _buildEmailList()), TextButton.icon( onPressed: () { emailControllers.add(TextEditingController()); emailLabels.add('Office'.obs); }, icon: const Icon(Icons.add), label: const Text("Add Email"), ), Obx(() => _buildPhoneList()), TextButton.icon( onPressed: () { phoneControllers.add(TextEditingController()); phoneLabels.add('Work'.obs); }, icon: const Icon(Icons.add), label: const Text("Add Phone"), ), MySpacing.height(24), _sectionLabel("Other Details"), MySpacing.height(16), MyText.labelMedium("Category"), MySpacing.height(8), _dropdownField( label: "Select Category", selectedValue: controller.selectedCategory, options: controller.categories, ), MySpacing.height(16), MyText.labelMedium("Select Projects"), MySpacing.height(8), _dropdownField( label: "Select Project", selectedValue: controller.selectedProject, options: controller.globalProjects, ), MySpacing.height(16), MyText.labelMedium("Select Bucket"), MySpacing.height(8), _dropdownField( label: "Select Bucket", selectedValue: controller.selectedBucket, options: controller.buckets, ), MySpacing.height(16), MyText.labelMedium("Tags"), MySpacing.height(8), _tagInputSection(), MySpacing.height(16), _buildTextField("Address", addressController, maxLines: 2), MySpacing.height(16), _buildTextField("Description", descriptionController, maxLines: 2), MySpacing.height(24), _buildActionButtons(), ], ), ), ), ), ); } Widget _buildTextField(String label, TextEditingController controller, {int maxLines = 1}) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.labelMedium(label), MySpacing.height(8), TextFormField( controller: controller, maxLines: maxLines, decoration: _inputDecoration("Enter $label"), validator: (value) => value == null || value.trim().isEmpty ? "$label is required" : null, ), ], ); } Widget _buildOrganizationField() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.labelMedium("Organization"), MySpacing.height(8), TextField( controller: orgController, onChanged: controller.filterOrganizationSuggestions, decoration: _inputDecoration("Enter organization"), ), Obx(() => controller.filteredOrgSuggestions.isEmpty ? const SizedBox() : ListView.builder( shrinkWrap: true, itemCount: controller.filteredOrgSuggestions.length, itemBuilder: (context, index) { final suggestion = controller.filteredOrgSuggestions[index]; return ListTile( dense: true, title: Text(suggestion), onTap: () { orgController.text = suggestion; controller.filteredOrgSuggestions.clear(); }, ); }, )), ], ); } Widget _buildActionButtons() { return Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: () { Get.back(); Get.delete(); }, icon: const Icon(Icons.close, color: Colors.red), label: MyText.bodyMedium("Cancel", color: Colors.red, fontWeight: 600), style: OutlinedButton.styleFrom( side: const BorderSide(color: Colors.red), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), ), ), ), MySpacing.width(12), Expanded( child: ElevatedButton.icon( onPressed: () { if (formKey.currentState!.validate()) { final emails = emailControllers .asMap() .entries .where((entry) => entry.value.text.trim().isNotEmpty) .map((entry) => { "label": emailLabels[entry.key].value, "emailAddress": entry.value.text.trim(), }) .toList(); final phones = phoneControllers .asMap() .entries .where((entry) => entry.value.text.trim().isNotEmpty) .map((entry) => { "label": phoneLabels[entry.key].value, "phoneNumber": entry.value.text.trim(), }) .toList(); controller.submitContact( name: nameController.text.trim(), organization: orgController.text.trim(), emails: emails, phones: phones, address: addressController.text.trim(), description: descriptionController.text.trim(), ); } }, icon: const Icon(Icons.check_circle_outline, color: Colors.white), label: MyText.bodyMedium("Save", color: Colors.white, fontWeight: 600), style: ElevatedButton.styleFrom( backgroundColor: Colors.indigo, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), ), ), ), ], ); } }