feat(contact): enhance AddContact functionality with validation and initialization state
This commit is contained in:
parent
445cd75e03
commit
77e27ff98e
@ -24,6 +24,7 @@ class AddContactController extends GetxController {
|
|||||||
final RxMap<String, String> bucketsMap = <String, String>{}.obs;
|
final RxMap<String, String> bucketsMap = <String, String>{}.obs;
|
||||||
final RxMap<String, String> projectsMap = <String, String>{}.obs;
|
final RxMap<String, String> projectsMap = <String, String>{}.obs;
|
||||||
final RxMap<String, String> tagsMap = <String, String>{}.obs;
|
final RxMap<String, String> tagsMap = <String, String>{}.obs;
|
||||||
|
final RxBool isInitialized = false.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -41,6 +42,9 @@ class AddContactController extends GetxController {
|
|||||||
fetchCategories(),
|
fetchCategories(),
|
||||||
fetchOrganizationNames(),
|
fetchOrganizationNames(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// ✅ Mark initialization as done
|
||||||
|
isInitialized.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetForm() {
|
void resetForm() {
|
||||||
@ -90,11 +94,103 @@ class AddContactController extends GetxController {
|
|||||||
required String address,
|
required String address,
|
||||||
required String description,
|
required String description,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
final categoryId = categoriesMap[selectedCategory.value];
|
||||||
final categoryId = categoriesMap[selectedCategory.value];
|
final bucketId = bucketsMap[selectedBucket.value];
|
||||||
final bucketId = bucketsMap[selectedBucket.value];
|
final projectId = projectsMap[selectedProject.value];
|
||||||
final projectId = projectsMap[selectedProject.value];
|
|
||||||
|
|
||||||
|
// === Per-field Validation with Specific Messages ===
|
||||||
|
if (name.trim().isEmpty) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Missing Name",
|
||||||
|
message: "Please enter the contact name.",
|
||||||
|
type: SnackbarType.warning,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization.trim().isEmpty) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Missing Organization",
|
||||||
|
message: "Please enter the organization name.",
|
||||||
|
type: SnackbarType.warning,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emails.isEmpty) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Missing Email",
|
||||||
|
message: "Please add at least one email.",
|
||||||
|
type: SnackbarType.warning,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phones.isEmpty) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Missing Phone Number",
|
||||||
|
message: "Please add at least one phone number.",
|
||||||
|
type: SnackbarType.warning,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address.trim().isEmpty) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Missing Address",
|
||||||
|
message: "Please enter the address.",
|
||||||
|
type: SnackbarType.warning,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description.trim().isEmpty) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Missing Description",
|
||||||
|
message: "Please enter a description.",
|
||||||
|
type: SnackbarType.warning,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedCategory.value.trim().isEmpty || categoryId == null) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Missing Category",
|
||||||
|
message: "Please select a contact category.",
|
||||||
|
type: SnackbarType.warning,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedBucket.value.trim().isEmpty || bucketId == null) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Missing Bucket",
|
||||||
|
message: "Please select a bucket.",
|
||||||
|
type: SnackbarType.warning,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedProject.value.trim().isEmpty || projectId == null) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Missing Project",
|
||||||
|
message: "Please select a project.",
|
||||||
|
type: SnackbarType.warning,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enteredTags.isEmpty) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Missing Tags",
|
||||||
|
message: "Please enter at least one tag.",
|
||||||
|
type: SnackbarType.warning,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Submit if all validations passed ===
|
||||||
|
try {
|
||||||
final tagObjects = enteredTags.map((tagName) {
|
final tagObjects = enteredTags.map((tagName) {
|
||||||
final tagId = tagsMap[tagName];
|
final tagId = tagsMap[tagName];
|
||||||
return tagId != null
|
return tagId != null
|
||||||
@ -104,16 +200,16 @@ class AddContactController extends GetxController {
|
|||||||
|
|
||||||
final body = {
|
final body = {
|
||||||
if (id != null) "id": id,
|
if (id != null) "id": id,
|
||||||
"name": name,
|
"name": name.trim(),
|
||||||
"organization": organization,
|
"organization": organization.trim(),
|
||||||
"contactCategoryId": categoryId,
|
"contactCategoryId": categoryId,
|
||||||
"projectIds": projectId != null ? [projectId] : [],
|
"projectIds": [projectId],
|
||||||
"bucketIds": bucketId != null ? [bucketId] : [],
|
"bucketIds": [bucketId],
|
||||||
"tags": tagObjects,
|
"tags": tagObjects,
|
||||||
"contactEmails": emails,
|
"contactEmails": emails,
|
||||||
"contactPhones": phones,
|
"contactPhones": phones,
|
||||||
"address": address,
|
"address": address.trim(),
|
||||||
"description": description,
|
"description": description.trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
logSafe("${id != null ? 'Updating' : 'Creating'} contact");
|
logSafe("${id != null ? 'Updating' : 'Creating'} contact");
|
||||||
|
@ -1,90 +1,30 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:marco/controller/directory/add_contact_controller.dart';
|
import 'package:marco/controller/directory/add_contact_controller.dart';
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||||
import 'package:marco/model/directory/contact_model.dart';
|
import 'package:marco/model/directory/contact_model.dart';
|
||||||
|
|
||||||
class AddContactBottomSheet extends StatelessWidget {
|
class AddContactBottomSheet extends StatefulWidget {
|
||||||
final ContactModel? existingContact;
|
final ContactModel? existingContact;
|
||||||
|
const AddContactBottomSheet({super.key, this.existingContact});
|
||||||
|
|
||||||
AddContactBottomSheet({super.key, this.existingContact}) {
|
@override
|
||||||
controller.resetForm();
|
State<AddContactBottomSheet> createState() => _AddContactBottomSheetState();
|
||||||
|
}
|
||||||
nameController.text = existingContact?.name ?? '';
|
|
||||||
orgController.text = existingContact?.organization ?? '';
|
|
||||||
tagTextController.clear();
|
|
||||||
addressController.text = existingContact?.address ?? '';
|
|
||||||
descriptionController.text = existingContact?.description ?? '';
|
|
||||||
|
|
||||||
if (existingContact != null) {
|
|
||||||
emailControllers.clear();
|
|
||||||
emailLabels.clear();
|
|
||||||
for (var email in existingContact!.contactEmails) {
|
|
||||||
emailControllers.add(TextEditingController(text: email.emailAddress));
|
|
||||||
emailLabels.add((email.label ?? 'Office').obs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emailControllers.isEmpty) {
|
|
||||||
emailControllers.add(TextEditingController());
|
|
||||||
emailLabels.add('Office'.obs);
|
|
||||||
}
|
|
||||||
|
|
||||||
phoneControllers.clear();
|
|
||||||
phoneLabels.clear();
|
|
||||||
for (var phone in existingContact!.contactPhones) {
|
|
||||||
phoneControllers.add(TextEditingController(text: phone.phoneNumber));
|
|
||||||
phoneLabels.add((phone.label ?? 'Work').obs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (phoneControllers.isEmpty) {
|
|
||||||
phoneControllers.add(TextEditingController());
|
|
||||||
phoneLabels.add('Work'.obs);
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.selectedCategory.value =
|
|
||||||
existingContact!.contactCategory?.name ?? '';
|
|
||||||
|
|
||||||
if (existingContact!.projectIds?.isNotEmpty == true) {
|
|
||||||
controller.selectedProject.value = controller.globalProjects
|
|
||||||
.firstWhereOrNull(
|
|
||||||
(e) => e == existingContact!.projectIds!.first,
|
|
||||||
)
|
|
||||||
?.toString() ??
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingContact!.bucketIds.isNotEmpty) {
|
|
||||||
controller.selectedBucket.value = controller.buckets
|
|
||||||
.firstWhereOrNull(
|
|
||||||
(b) => b == existingContact!.bucketIds.first,
|
|
||||||
)
|
|
||||||
?.toString() ??
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.enteredTags.assignAll(
|
|
||||||
existingContact!.tags.map((tag) => tag.name).toList(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
emailControllers.add(TextEditingController());
|
|
||||||
emailLabels.add('Office'.obs);
|
|
||||||
|
|
||||||
phoneControllers.add(TextEditingController());
|
|
||||||
phoneLabels.add('Work'.obs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||||
final controller = Get.put(AddContactController());
|
final controller = Get.put(AddContactController());
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
final nameController = TextEditingController();
|
final nameController = TextEditingController();
|
||||||
final orgController = TextEditingController();
|
final orgController = TextEditingController();
|
||||||
final tagTextController = TextEditingController();
|
|
||||||
final addressController = TextEditingController();
|
final addressController = TextEditingController();
|
||||||
final descriptionController = TextEditingController();
|
final descriptionController = TextEditingController();
|
||||||
|
final tagTextController = TextEditingController();
|
||||||
|
|
||||||
final RxList<TextEditingController> emailControllers =
|
final RxList<TextEditingController> emailControllers =
|
||||||
<TextEditingController>[].obs;
|
<TextEditingController>[].obs;
|
||||||
@ -94,6 +34,94 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
<TextEditingController>[].obs;
|
<TextEditingController>[].obs;
|
||||||
final RxList<RxString> phoneLabels = <RxString>[].obs;
|
final RxList<RxString> phoneLabels = <RxString>[].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 projectId = widget.existingContact!.projectIds?.firstOrNull;
|
||||||
|
final bucketId = widget.existingContact!.bucketIds.firstOrNull;
|
||||||
|
final categoryName = widget.existingContact!.contactCategory?.name;
|
||||||
|
|
||||||
|
if (categoryName != null) {
|
||||||
|
controller.selectedCategory.value = categoryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectId != null) {
|
||||||
|
final name = controller.projectsMap.entries
|
||||||
|
.firstWhereOrNull((e) => e.value == projectId)
|
||||||
|
?.key;
|
||||||
|
if (name != null) {
|
||||||
|
controller.selectedProject.value = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<AddContactController>();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
InputDecoration _inputDecoration(String hint) => InputDecoration(
|
InputDecoration _inputDecoration(String hint) => InputDecoration(
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||||
@ -116,47 +144,14 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
isDense: true,
|
isDense: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _popupSelector({
|
|
||||||
required String hint,
|
|
||||||
required RxString selectedValue,
|
|
||||||
required List<String> options,
|
|
||||||
}) {
|
|
||||||
return Obx(() => GestureDetector(
|
|
||||||
onTap: () async {
|
|
||||||
final selected = await showMenu<String>(
|
|
||||||
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(
|
Widget _buildLabeledRow(
|
||||||
String label,
|
String label,
|
||||||
RxString selectedLabel,
|
RxString selectedLabel,
|
||||||
List<String> options,
|
List<String> options,
|
||||||
String inputLabel,
|
String inputLabel,
|
||||||
TextEditingController controller,
|
TextEditingController controller,
|
||||||
TextInputType inputType, {
|
TextInputType inputType,
|
||||||
VoidCallback? onRemove,
|
{VoidCallback? onRemove}) {
|
||||||
}) {
|
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -190,22 +185,19 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
decoration: _inputDecoration("Enter $inputLabel")
|
decoration: _inputDecoration("Enter $inputLabel")
|
||||||
.copyWith(counterText: ""),
|
.copyWith(counterText: ""),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.trim().isEmpty) {
|
if (value == null || value.trim().isEmpty)
|
||||||
return "$inputLabel is required";
|
return "$inputLabel is required";
|
||||||
}
|
|
||||||
final trimmed = value.trim();
|
final trimmed = value.trim();
|
||||||
if (inputType == TextInputType.phone) {
|
if (inputType == TextInputType.phone) {
|
||||||
if (!RegExp(r'^\d{10}$').hasMatch(trimmed)) {
|
if (!RegExp(r'^[1-9][0-9]{9}$').hasMatch(trimmed)) {
|
||||||
return "Enter a valid 10-digit phone number";
|
return "Enter valid phone number";
|
||||||
}
|
|
||||||
if (RegExp(r'^0+$').hasMatch(trimmed)) {
|
|
||||||
return "Phone number cannot be all zeroes";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputType == TextInputType.emailAddress &&
|
if (inputType == TextInputType.emailAddress &&
|
||||||
!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
|
!RegExp(r'^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$')
|
||||||
.hasMatch(trimmed)) {
|
.hasMatch(trimmed)) {
|
||||||
return "Enter a valid email address";
|
return "Enter valid email";
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@ -269,45 +261,54 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _dropdownField({
|
Widget _popupSelector({
|
||||||
required String label,
|
required String hint,
|
||||||
required RxString selectedValue,
|
required RxString selectedValue,
|
||||||
required RxList<String> options,
|
required List<String> options,
|
||||||
}) {
|
}) {
|
||||||
return Obx(() => SizedBox(
|
return Obx(() => GestureDetector(
|
||||||
height: 48,
|
onTap: () async {
|
||||||
child: PopupMenuButton<String>(
|
final selected = await showMenu<String>(
|
||||||
onSelected: (value) => selectedValue.value = value,
|
context: context,
|
||||||
itemBuilder: (_) => options
|
position: const RelativeRect.fromLTRB(100, 300, 100, 0),
|
||||||
.map((item) => PopupMenuItem(value: item, child: Text(item)))
|
items: options
|
||||||
.toList(),
|
.map((e) => PopupMenuItem(value: e, child: Text(e)))
|
||||||
padding: EdgeInsets.zero,
|
.toList(),
|
||||||
child: Container(
|
);
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
if (selected != null) selectedValue.value = selected;
|
||||||
decoration: BoxDecoration(
|
},
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
child: Container(
|
||||||
borderRadius: BorderRadius.circular(8),
|
height: 48,
|
||||||
color: Colors.grey.shade100,
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
alignment: Alignment.centerLeft,
|
color: Colors.grey.shade100,
|
||||||
child: Row(
|
borderRadius: BorderRadius.circular(12),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
children: [
|
),
|
||||||
Expanded(
|
alignment: Alignment.centerLeft,
|
||||||
child: Text(
|
child: Row(
|
||||||
selectedValue.value.isEmpty ? label : selectedValue.value,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
style: const TextStyle(fontSize: 14),
|
children: [
|
||||||
overflow: TextOverflow.ellipsis,
|
Text(
|
||||||
),
|
selectedValue.value.isNotEmpty ? selectedValue.value : hint,
|
||||||
),
|
style: const TextStyle(fontSize: 14),
|
||||||
const Icon(Icons.arrow_drop_down),
|
),
|
||||||
],
|
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() {
|
Widget _tagInputSection() {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -327,7 +328,33 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Obx(() => controller.filteredSuggestions.isEmpty
|
Obx(() => controller.filteredSuggestions.isEmpty
|
||||||
? const SizedBox()
|
? const SizedBox()
|
||||||
: _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();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
Obx(() => Wrap(
|
Obx(() => Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
@ -342,137 +369,6 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
|
||||||
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),
|
|
||||||
_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,
|
Widget _buildTextField(String label, TextEditingController controller,
|
||||||
{int maxLines = 1}) {
|
{int maxLines = 1}) {
|
||||||
return Column(
|
return Column(
|
||||||
@ -568,9 +464,9 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
"phoneNumber": entry.value.text.trim(),
|
"phoneNumber": entry.value.text.trim(),
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
print("Submitting contact payload , id: ${existingContact?.id}");
|
|
||||||
controller.submitContact(
|
controller.submitContact(
|
||||||
id: existingContact?.id,
|
id: widget.existingContact?.id,
|
||||||
name: nameController.text.trim(),
|
name: nameController.text.trim(),
|
||||||
organization: orgController.text.trim(),
|
organization: orgController.text.trim(),
|
||||||
emails: emails,
|
emails: emails,
|
||||||
@ -594,4 +490,106 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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),
|
||||||
|
_popupSelector(
|
||||||
|
hint: "Select Project",
|
||||||
|
selectedValue: controller.selectedProject,
|
||||||
|
options: controller.globalProjects,
|
||||||
|
),
|
||||||
|
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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user