import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:collection/collection.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:marco/model/directory/contact_model.dart'; import 'package:marco/helpers/utils/contact_picker_helper.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; class AddContactBottomSheet extends StatefulWidget { final ContactModel? existingContact; const AddContactBottomSheet({super.key, this.existingContact}); @override State createState() => _AddContactBottomSheetState(); } class _AddContactBottomSheetState extends State { final controller = Get.put(AddContactController()); final formKey = GlobalKey(); final nameCtrl = TextEditingController(); final orgCtrl = TextEditingController(); final designationCtrl = TextEditingController(); final addrCtrl = TextEditingController(); final descCtrl = TextEditingController(); final tagCtrl = TextEditingController(); final showAdvanced = false.obs; final bucketError = ''.obs; final emailCtrls = [].obs; final emailLabels = [].obs; final phoneCtrls = [].obs; final phoneLabels = [].obs; @override void initState() { super.initState(); controller.resetForm(); _initFields(); } void _initFields() { final c = widget.existingContact; if (c != null) { nameCtrl.text = c.name; orgCtrl.text = c.organization; designationCtrl.text = c.designation ?? ''; addrCtrl.text = c.address; descCtrl.text = c.description; emailCtrls.assignAll(c.contactEmails.isEmpty ? [TextEditingController()] : c.contactEmails .map((e) => TextEditingController(text: e.emailAddress))); emailLabels.assignAll(c.contactEmails.isEmpty ? ['Office'.obs] : c.contactEmails.map((e) => e.label.obs)); phoneCtrls.assignAll(c.contactPhones.isEmpty ? [TextEditingController()] : c.contactPhones .map((p) => TextEditingController(text: p.phoneNumber))); phoneLabels.assignAll(c.contactPhones.isEmpty ? ['Work'.obs] : c.contactPhones.map((p) => p.label.obs)); controller.enteredTags.assignAll(c.tags.map((e) => e.name)); ever(controller.isInitialized, (bool ready) { if (ready) { // Buckets - map all if (c.bucketIds.isNotEmpty) { final names = c.bucketIds.map((id) { return controller.bucketsMap.entries .firstWhereOrNull((e) => e.value == id) ?.key; }).whereType().toList(); controller.selectedBuckets.assignAll(names); } // Projects and Category mapping - as before final projectIds = c.projectIds; if (projectIds != null) { controller.selectedProjects.assignAll( projectIds .map((id) => controller.projectsMap.entries .firstWhereOrNull((e) => e.value == id) ?.key) .whereType() .toList(), ); } final category = c.contactCategory?.name; if (category != null) controller.selectedCategory.value = category; } }); } else { showAdvanced.value = false; // Optional emailCtrls.add(TextEditingController()); emailLabels.add('Office'.obs); phoneCtrls.add(TextEditingController()); phoneLabels.add('Work'.obs); } } @override void dispose() { nameCtrl.dispose(); orgCtrl.dispose(); designationCtrl.dispose(); addrCtrl.dispose(); descCtrl.dispose(); tagCtrl.dispose(); emailCtrls.forEach((c) => c.dispose()); phoneCtrls.forEach((c) => c.dispose()); Get.delete(); super.dispose(); } Widget _labelWithStar(String label, {bool required = false}) { return Row( mainAxisSize: MainAxisSize.min, children: [ MyText.labelMedium(label), if (required) const Text( " *", style: TextStyle(color: Colors.red, fontSize: 14), ), ], ); } 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 _textField(String label, TextEditingController ctrl, {bool required = false, int maxLines = 1}) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _labelWithStar(label, required: required), MySpacing.height(8), TextFormField( controller: ctrl, maxLines: maxLines, decoration: _inputDecoration("Enter $label"), validator: required ? (v) => (v == null || v.trim().isEmpty) ? "$label is required" : null : null, ), ], ); } Widget _popupSelector(RxString selected, List options, String hint) => Obx(() { return GestureDetector( onTap: () async { final selectedItem = await showMenu( context: context, position: RelativeRect.fromLTRB(100, 300, 100, 0), items: options .map((e) => PopupMenuItem(value: e, child: Text(e))) .toList(), ); if (selectedItem != null) selected.value = selectedItem; }, child: Container( height: 48, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade300), ), alignment: Alignment.centerLeft, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(selected.value.isNotEmpty ? selected.value : hint, style: const TextStyle(fontSize: 14)), const Icon(Icons.expand_more, size: 20), ], ), ), ); }); Widget _dynamicList( RxList ctrls, RxList labels, String labelType, List labelOptions, TextInputType type) { return Obx(() { return Column( children: List.generate(ctrls.length, (i) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.labelMedium("$labelType Label"), MySpacing.height(8), _popupSelector(labels[i], labelOptions, "Label"), ], ), ), MySpacing.width(12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.labelMedium(labelType), MySpacing.height(8), TextFormField( controller: ctrls[i], keyboardType: type, maxLength: type == TextInputType.phone ? 10 : null, inputFormatters: type == TextInputType.phone ? [FilteringTextInputFormatter.digitsOnly] : [], decoration: _inputDecoration("Enter $labelType").copyWith( counterText: "", suffixIcon: type == TextInputType.phone ? IconButton( icon: const Icon(Icons.contact_phone, color: Colors.blue), onPressed: () async { final phone = await ContactPickerHelper .pickIndianPhoneNumber(context); if (phone != null) ctrls[i].text = phone; }, ) : null, ), validator: (value) { if (value == null || value.trim().isEmpty) return null; final trimmed = value.trim(); if (type == TextInputType.phone && !RegExp(r'^[1-9][0-9]{9}$').hasMatch(trimmed)) { return "Enter valid phone number"; } if (type == TextInputType.emailAddress && !RegExp(r'^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$') .hasMatch(trimmed)) { return "Enter valid email"; } return null; }, ), ], ), ), if (ctrls.length > 1) Padding( padding: const EdgeInsets.only(top: 24), child: IconButton( icon: const Icon(Icons.remove_circle_outline, color: Colors.red), onPressed: () { ctrls.removeAt(i); labels.removeAt(i); }, ), ), ], ), ); }), ); }); } Widget _tagInput() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 48, child: TextField( controller: tagCtrl, onChanged: controller.filterSuggestions, onSubmitted: (v) { controller.addEnteredTag(v); tagCtrl.clear(); controller.clearSuggestions(); }, decoration: _inputDecoration("Start typing to add tags"), ), ), Obx(() => controller.filteredSuggestions.isEmpty ? const SizedBox.shrink() : 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: (_, i) { final suggestion = controller.filteredSuggestions[i]; return ListTile( dense: true, title: Text(suggestion), onTap: () { controller.addEnteredTag(suggestion); tagCtrl.clear(); controller.clearSuggestions(); }, ); }, ), )), MySpacing.height(8), Obx(() => Wrap( spacing: 8, children: controller.enteredTags .map((tag) => Chip( label: Text(tag), onDeleted: () => controller.removeEnteredTag(tag), )) .toList(), )), ], ); } Widget _bucketMultiSelectField() { return _multiSelectField( items: controller.buckets .map((name) => FilterItem(id: name, name: name)) .toList(), fallback: "Choose Buckets", selectedValues: controller.selectedBuckets, ); } Widget _multiSelectField({ required List items, required String fallback, required RxList selectedValues, }) { if (items.isEmpty) return const SizedBox.shrink(); return Obx(() { final selectedNames = items .where((f) => selectedValues.contains(f.id)) .map((f) => f.name) .join(", "); final displayText = selectedNames.isNotEmpty ? selectedNames : fallback; return Builder( builder: (context) { return GestureDetector( onTap: () async { final RenderBox button = context.findRenderObject() as RenderBox; final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; final position = button.localToGlobal(Offset.zero); await showMenu( context: context, position: RelativeRect.fromLTRB( position.dx, position.dy + button.size.height, overlay.size.width - position.dx - button.size.width, 0, ), items: [ PopupMenuItem( enabled: false, child: StatefulBuilder( builder: (context, setState) { return SizedBox( width: 250, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: items.map((f) { final isChecked = selectedValues.contains(f.id); return CheckboxListTile( dense: true, title: Text(f.name), value: isChecked, contentPadding: EdgeInsets.zero, controlAffinity: ListTileControlAffinity.leading, side: const BorderSide(color: Colors.black, width: 1.5), fillColor: MaterialStateProperty.resolveWith((states) { if (states.contains(MaterialState.selected)) { return Colors.indigo; // selected color } return Colors.white; // unselected background }), checkColor: Colors.white, // tick color onChanged: (val) { if (val == true) { selectedValues.add(f.id); } else { selectedValues.remove(f.id); } setState(() {}); }, ); }).toList(), ), ), ); }, ), ), ], ); }, child: Container( padding: MySpacing.all(12), decoration: BoxDecoration( color: Colors.grey.shade100, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: MyText( displayText, style: const TextStyle(color: Colors.black87), overflow: TextOverflow.ellipsis, ), ), const Icon(Icons.arrow_drop_down, color: Colors.grey), ], ), ), ); }, ); }); } void _handleSubmit() { bool valid = formKey.currentState?.validate() ?? false; if (controller.selectedBuckets.isEmpty) { bucketError.value = "Bucket is required"; valid = false; } else { bucketError.value = ""; } if (!valid) return; final emails = emailCtrls .asMap() .entries .where((e) => e.value.text.trim().isNotEmpty) .map((e) => { "label": emailLabels[e.key].value, "emailAddress": e.value.text.trim() }) .toList(); final phones = phoneCtrls .asMap() .entries .where((e) => e.value.text.trim().isNotEmpty) .map((e) => { "label": phoneLabels[e.key].value, "phoneNumber": e.value.text.trim() }) .toList(); controller.submitContact( id: widget.existingContact?.id, name: nameCtrl.text.trim(), organization: orgCtrl.text.trim(), emails: emails, phones: phones, address: addrCtrl.text.trim(), description: descCtrl.text.trim(), designation: designationCtrl.text.trim(), ); } @override Widget build(BuildContext context) { return Obx(() { if (!controller.isInitialized.value) { return const Center(child: CircularProgressIndicator()); } return BaseBottomSheet( title: widget.existingContact != null ? "Edit Contact" : "Create New Contact", onCancel: () => Get.back(), onSubmit: _handleSubmit, isSubmitting: controller.isSubmitting.value, child: Form( key: formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _textField("Name", nameCtrl, required: true), MySpacing.height(16), _textField("Organization", orgCtrl, required: true), MySpacing.height(16), _labelWithStar("Buckets", required: true), MySpacing.height(8), Stack( children: [ _bucketMultiSelectField(), ], ), MySpacing.height(12), Obx(() => GestureDetector( onTap: () => showAdvanced.toggle(), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ MyText.labelLarge("Advanced Details (Optional)", fontWeight: 600), Icon(showAdvanced.value ? Icons.expand_less : Icons.expand_more), ], ), )), Obx(() => showAdvanced.value ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MySpacing.height(24), _dynamicList( emailCtrls, emailLabels, "Email", ["Office", "Personal", "Other"], TextInputType.emailAddress), TextButton.icon( onPressed: () { emailCtrls.add(TextEditingController()); emailLabels.add("Office".obs); }, icon: const Icon(Icons.add), label: const Text("Add Email"), ), _dynamicList(phoneCtrls, phoneLabels, "Phone", ["Work", "Mobile", "Other"], TextInputType.phone), TextButton.icon( onPressed: () { phoneCtrls.add(TextEditingController()); phoneLabels.add("Work".obs); }, icon: const Icon(Icons.add), label: const Text("Add Phone"), ), Obx(() => showAdvanced.value ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ✅ Move Designation field here _textField("Designation", designationCtrl), MySpacing.height(16), _dynamicList( emailCtrls, emailLabels, "Email", ["Office", "Personal", "Other"], TextInputType.emailAddress, ), TextButton.icon( onPressed: () { emailCtrls.add(TextEditingController()); emailLabels.add("Office".obs); }, icon: const Icon(Icons.add), label: const Text("Add Email"), ), _dynamicList( phoneCtrls, phoneLabels, "Phone", ["Work", "Mobile", "Other"], TextInputType.phone, ), TextButton.icon( onPressed: () { phoneCtrls.add(TextEditingController()); phoneLabels.add("Work".obs); }, icon: const Icon(Icons.add), label: const Text("Add Phone"), ), MyText.labelMedium("Category"), MySpacing.height(8), _popupSelector( controller.selectedCategory, controller.categories, "Choose Category", ), MySpacing.height(16), MyText.labelMedium("Tags"), MySpacing.height(8), _tagInput(), MySpacing.height(16), _textField("Address", addrCtrl), MySpacing.height(16), _textField("Description", descCtrl, maxLines: 3), ], ) : const SizedBox.shrink()), ], ) : const SizedBox.shrink()), ], ), ), ); }); } } class FilterItem { final String id; final String name; FilterItem({required this.id, required this.name}); }