import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter/services.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'; 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 nameController = TextEditingController(); final orgController = TextEditingController(); final addressController = TextEditingController(); final descriptionController = TextEditingController(); final tagTextController = TextEditingController(); final RxList emailControllers = [].obs; final RxList emailLabels = [].obs; final RxList phoneControllers = [].obs; final RxList phoneLabels = [].obs; @override void initState() { super.initState(); controller.resetForm(); nameController.text = widget.existingContact?.name ?? ''; orgController.text = widget.existingContact?.organization ?? ''; addressController.text = widget.existingContact?.address ?? ''; descriptionController.text = widget.existingContact?.description ?? ''; tagTextController.clear(); if (widget.existingContact != null) { emailControllers.clear(); emailLabels.clear(); for (var email in widget.existingContact!.contactEmails) { emailControllers.add(TextEditingController(text: email.emailAddress)); emailLabels.add((email.label).obs); } if (emailControllers.isEmpty) { emailControllers.add(TextEditingController()); emailLabels.add('Office'.obs); } phoneControllers.clear(); phoneLabels.clear(); for (var phone in widget.existingContact!.contactPhones) { phoneControllers.add(TextEditingController(text: phone.phoneNumber)); phoneLabels.add((phone.label).obs); } if (phoneControllers.isEmpty) { phoneControllers.add(TextEditingController()); phoneLabels.add('Work'.obs); } controller.enteredTags.assignAll( widget.existingContact!.tags.map((tag) => tag.name).toList(), ); ever(controller.isInitialized, (bool ready) { if (ready) { final projectIds = widget.existingContact!.projectIds; final bucketId = widget.existingContact!.bucketIds.firstOrNull; final categoryName = widget.existingContact!.contactCategory?.name; if (categoryName != null) { controller.selectedCategory.value = categoryName; } if (projectIds != null) { final names = projectIds .map((id) { return controller.projectsMap.entries .firstWhereOrNull((e) => e.value == id) ?.key; }) .whereType() .toList(); controller.selectedProjects.assignAll(names); } if (bucketId != null) { final name = controller.bucketsMap.entries .firstWhereOrNull((e) => e.value == bucketId) ?.key; if (name != null) { controller.selectedBucket.value = name; } } } }); } else { emailControllers.add(TextEditingController()); emailLabels.add('Office'.obs); phoneControllers.add(TextEditingController()); phoneLabels.add('Work'.obs); } } @override void dispose() { nameController.dispose(); orgController.dispose(); tagTextController.dispose(); addressController.dispose(); descriptionController.dispose(); emailControllers.forEach((e) => e.dispose()); phoneControllers.forEach((p) => p.dispose()); Get.delete(); super.dispose(); } 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 _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: ""), validator: (value) { if (value == null || value.trim().isEmpty) return "$inputLabel is required"; final trimmed = value.trim(); if (inputType == TextInputType.phone) { if (!RegExp(r'^[1-9][0-9]{9}$').hasMatch(trimmed)) { return "Enter valid phone number"; } } if (inputType == TextInputType.emailAddress && !RegExp(r'^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$') .hasMatch(trimmed)) { return "Enter valid email"; } 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() => 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() => 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 _popupSelector({ required String hint, required RxString selectedValue, required List options, }) { return Obx(() => GestureDetector( onTap: () async { final selected = await showMenu( context: context, position: RelativeRect.fromLTRB(100, 300, 100, 0), items: options.map((option) { return PopupMenuItem( value: option, child: Text(option), ); }).toList(), ); if (selected != null) { selectedValue.value = selected; } }, 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( selectedValue.value.isNotEmpty ? selectedValue.value : hint, style: const TextStyle(fontSize: 14), ), const Icon(Icons.expand_more, size: 20), ], ), ), )); } Widget _sectionLabel(String title) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.labelLarge(title, fontWeight: 600), MySpacing.height(4), Divider(thickness: 1, color: Colors.grey.shade200), ], ); 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() : 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(); }, ); }, ), )), MySpacing.height(8), Obx(() => Wrap( spacing: 8, children: controller.enteredTags .map((tag) => Chip( label: Text(tag), onDeleted: () => controller.removeEnteredTag(tag), )) .toList(), )), ], ); } 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( id: widget.existingContact?.id, 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), ), ), ), ], ); } @override Widget build(BuildContext context) { return Obx(() { if (!controller.isInitialized.value) { return const Center(child: CircularProgressIndicator()); } 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( widget.existingContact != null ? "Edit Contact" : "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), _popupSelector( hint: "Select Category", selectedValue: controller.selectedCategory, options: controller.categories, ), MySpacing.height(16), MyText.labelMedium("Select Projects"), MySpacing.height(8), GestureDetector( onTap: () async { await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('Select Projects'), content: Obx(() { return SizedBox( width: double.maxFinite, child: ListView( shrinkWrap: true, children: controller.globalProjects.map((project) { final isSelected = controller .selectedProjects .contains(project); return Theme( data: Theme.of(context).copyWith( unselectedWidgetColor: Colors .black, // checkbox border when not selected checkboxTheme: CheckboxThemeData( fillColor: MaterialStateProperty .resolveWith((states) { if (states.contains( MaterialState.selected)) { return Colors .white; // fill when selected } return Colors.transparent; }), checkColor: MaterialStateProperty.all( Colors.black), // check mark color side: const BorderSide( color: Colors.black, width: 2), // border color shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(4), ), ), ), child: CheckboxListTile( dense: true, title: Text(project), value: isSelected, onChanged: (bool? selected) { if (selected == true) { controller.selectedProjects .add(project); } else { controller.selectedProjects .remove(project); } }, ), ); }).toList(), ), ); }), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Done'), ), ], ); }, ); }, 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: Obx(() { final selected = controller.selectedProjects; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( selected.isEmpty ? "Select Projects" : selected.join(', '), overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 14), ), ), const Icon(Icons.expand_more, size: 20), ], ); }), ), ), MySpacing.height(16), MyText.labelMedium("Select Bucket"), MySpacing.height(8), _popupSelector( hint: "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(), ], ), ), ), ), ); }); } }