Vaibhav_Feature-#768 #59
@ -24,6 +24,7 @@ class AddContactController extends GetxController {
|
|||||||
final RxMap<String, String> tagsMap = <String, String>{}.obs;
|
final RxMap<String, String> tagsMap = <String, String>{}.obs;
|
||||||
final RxBool isInitialized = false.obs;
|
final RxBool isInitialized = false.obs;
|
||||||
final RxList<String> selectedProjects = <String>[].obs;
|
final RxList<String> selectedProjects = <String>[].obs;
|
||||||
|
final RxBool isSubmitting = false.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -94,6 +95,9 @@ class AddContactController extends GetxController {
|
|||||||
required String address,
|
required String address,
|
||||||
required String description,
|
required String description,
|
||||||
}) async {
|
}) async {
|
||||||
|
if (isSubmitting.value) return;
|
||||||
|
isSubmitting.value = true;
|
||||||
|
|
||||||
final categoryId = categoriesMap[selectedCategory.value];
|
final categoryId = categoriesMap[selectedCategory.value];
|
||||||
final bucketId = bucketsMap[selectedBucket.value];
|
final bucketId = bucketsMap[selectedBucket.value];
|
||||||
final projectIds = selectedProjects
|
final projectIds = selectedProjects
|
||||||
@ -101,13 +105,13 @@ class AddContactController extends GetxController {
|
|||||||
.whereType<String>()
|
.whereType<String>()
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// === Required validations only for name, organization, and bucket ===
|
|
||||||
if (name.trim().isEmpty) {
|
if (name.trim().isEmpty) {
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Missing Name",
|
title: "Missing Name",
|
||||||
message: "Please enter the contact name.",
|
message: "Please enter the contact name.",
|
||||||
type: SnackbarType.warning,
|
type: SnackbarType.warning,
|
||||||
);
|
);
|
||||||
|
isSubmitting.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,6 +121,7 @@ class AddContactController extends GetxController {
|
|||||||
message: "Please enter the organization name.",
|
message: "Please enter the organization name.",
|
||||||
type: SnackbarType.warning,
|
type: SnackbarType.warning,
|
||||||
);
|
);
|
||||||
|
isSubmitting.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,10 +131,10 @@ class AddContactController extends GetxController {
|
|||||||
message: "Please select a bucket.",
|
message: "Please select a bucket.",
|
||||||
type: SnackbarType.warning,
|
type: SnackbarType.warning,
|
||||||
);
|
);
|
||||||
|
isSubmitting.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Build body (include optional fields if available) ===
|
|
||||||
try {
|
try {
|
||||||
final tagObjects = enteredTags.map((tagName) {
|
final tagObjects = enteredTags.map((tagName) {
|
||||||
final tagId = tagsMap[tagName];
|
final tagId = tagsMap[tagName];
|
||||||
@ -182,6 +187,8 @@ class AddContactController extends GetxController {
|
|||||||
message: "Something went wrong",
|
message: "Something went wrong",
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +137,6 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
|||||||
onCancel: () => Navigator.of(context).pop(),
|
onCancel: () => Navigator.of(context).pop(),
|
||||||
onSubmit: _submitComment,
|
onSubmit: _submitComment,
|
||||||
isSubmitting: controller.isLoading.value,
|
isSubmitting: controller.isLoading.value,
|
||||||
submitText: 'Comment',
|
|
||||||
bottomContent: _buildCommentsSection(),
|
bottomContent: _buildCommentsSection(),
|
||||||
child: Form(
|
child: Form(
|
||||||
// moved to last
|
// moved to last
|
||||||
|
@ -4,8 +4,7 @@ import 'package:marco/controller/task_planing/add_task_controller.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
|
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
|
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||||
|
|
||||||
|
|
||||||
void showCreateTaskBottomSheet({
|
void showCreateTaskBottomSheet({
|
||||||
required String workArea,
|
required String workArea,
|
||||||
@ -27,147 +26,15 @@ void showCreateTaskBottomSheet({
|
|||||||
Get.bottomSheet(
|
Get.bottomSheet(
|
||||||
StatefulBuilder(
|
StatefulBuilder(
|
||||||
builder: (context, setState) {
|
builder: (context, setState) {
|
||||||
return LayoutBuilder(
|
return BaseBottomSheet(
|
||||||
builder: (context, constraints) {
|
title: "Create Task",
|
||||||
final isLarge = constraints.maxWidth > 600;
|
onCancel: () => Get.back(),
|
||||||
final horizontalPadding =
|
onSubmit: () async {
|
||||||
isLarge ? constraints.maxWidth * 0.2 : 16.0;
|
final plannedValue =
|
||||||
|
int.tryParse(plannedTaskController.text.trim()) ?? 0;
|
||||||
|
final comment = descriptionController.text.trim();
|
||||||
|
final selectedCategoryId = controller.selectedCategoryId.value;
|
||||||
|
|
||||||
return // Inside showManageTaskBottomSheet...
|
|
||||||
|
|
||||||
SafeArea(
|
|
||||||
child: Material(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius:
|
|
||||||
const BorderRadius.vertical(top: Radius.circular(20)),
|
|
||||||
child: Container(
|
|
||||||
constraints: const BoxConstraints(maxHeight: 760),
|
|
||||||
padding: EdgeInsets.fromLTRB(
|
|
||||||
horizontalPadding, 12, horizontalPadding, 24),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Center(
|
|
||||||
child: Container(
|
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade400,
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: MyText.titleLarge(
|
|
||||||
"Create Task",
|
|
||||||
fontWeight: 700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
_infoCardSection([
|
|
||||||
_infoRowWithIcon(
|
|
||||||
Icons.workspaces, "Selected Work Area", workArea),
|
|
||||||
_infoRowWithIcon(
|
|
||||||
Icons.list_alt, "Selected Activity", activity),
|
|
||||||
_infoRowWithIcon(Icons.check_circle_outline,
|
|
||||||
"Completed Work", completedWork),
|
|
||||||
]),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_sectionTitle(Icons.edit_calendar, "Planned Work"),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
_customTextField(
|
|
||||||
controller: plannedTaskController,
|
|
||||||
hint: "Enter planned work",
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_sectionTitle(Icons.description_outlined, "Comment"),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
_customTextField(
|
|
||||||
controller: descriptionController,
|
|
||||||
hint: "Enter task description",
|
|
||||||
maxLines: 3,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_sectionTitle(
|
|
||||||
Icons.category_outlined, "Selected Work Category"),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Obx(() {
|
|
||||||
final categoryMap = controller.categoryIdNameMap;
|
|
||||||
final String selectedName =
|
|
||||||
controller.selectedCategoryId.value != null
|
|
||||||
? (categoryMap[controller
|
|
||||||
.selectedCategoryId.value!] ??
|
|
||||||
'Select Category')
|
|
||||||
: 'Select Category';
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12, vertical: 14),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade100,
|
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: PopupMenuButton<String>(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
onSelected: (val) {
|
|
||||||
controller.selectCategory(val);
|
|
||||||
onCategoryChanged(val);
|
|
||||||
},
|
|
||||||
itemBuilder: (context) => categoryMap.entries
|
|
||||||
.map(
|
|
||||||
(entry) => PopupMenuItem<String>(
|
|
||||||
value: entry.key,
|
|
||||||
child: Text(entry.value),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
selectedName,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14, color: Colors.black87),
|
|
||||||
),
|
|
||||||
const Icon(Icons.arrow_drop_down),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
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(
|
|
||||||
side: const BorderSide(color: Colors.grey),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () async {
|
|
||||||
final plannedValue = int.tryParse(
|
|
||||||
plannedTaskController.text.trim()) ??
|
|
||||||
0;
|
|
||||||
final comment =
|
|
||||||
descriptionController.text.trim();
|
|
||||||
final selectedCategoryId =
|
|
||||||
controller.selectedCategoryId.value;
|
|
||||||
if (selectedCategoryId == null) {
|
if (selectedCategoryId == null) {
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "error",
|
title: "error",
|
||||||
@ -188,8 +55,7 @@ void showCreateTaskBottomSheet({
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
Get.back();
|
Get.back();
|
||||||
Future.delayed(
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
const Duration(milliseconds: 300), () {
|
|
||||||
onSubmit();
|
onSubmit();
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@ -199,25 +65,81 @@ void showCreateTaskBottomSheet({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.check, size: 18),
|
submitText: "Submit",
|
||||||
label: MyText.bodyMedium("Submit",
|
child: Column(
|
||||||
color: Colors.white, fontWeight: 600),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: ElevatedButton.styleFrom(
|
children: [
|
||||||
backgroundColor: Colors.blueAccent,
|
_infoCardSection([
|
||||||
shape: RoundedRectangleBorder(
|
_infoRowWithIcon(
|
||||||
borderRadius: BorderRadius.circular(10)),
|
Icons.workspaces, "Selected Work Area", workArea),
|
||||||
|
_infoRowWithIcon(Icons.list_alt, "Selected Activity", activity),
|
||||||
|
_infoRowWithIcon(Icons.check_circle_outline, "Completed Work",
|
||||||
|
completedWork),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_sectionTitle(Icons.edit_calendar, "Planned Work"),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_customTextField(
|
||||||
|
controller: plannedTaskController,
|
||||||
|
hint: "Enter planned work",
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_sectionTitle(Icons.description_outlined, "Comment"),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_customTextField(
|
||||||
|
controller: descriptionController,
|
||||||
|
hint: "Enter task description",
|
||||||
|
maxLines: 3,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_sectionTitle(Icons.category_outlined, "Selected Work Category"),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Obx(() {
|
||||||
|
final categoryMap = controller.categoryIdNameMap;
|
||||||
|
final String selectedName =
|
||||||
|
controller.selectedCategoryId.value != null
|
||||||
|
? (categoryMap[controller.selectedCategoryId.value!] ??
|
||||||
|
'Select Category')
|
||||||
|
: 'Select Category';
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
|
child: PopupMenuButton<String>(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onSelected: (val) {
|
||||||
|
controller.selectCategory(val);
|
||||||
|
onCategoryChanged(val);
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => categoryMap.entries
|
||||||
|
.map((entry) => PopupMenuItem<String>(
|
||||||
|
value: entry.key,
|
||||||
|
child: Text(entry.value),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
selectedName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14, color: Colors.black87),
|
||||||
|
),
|
||||||
|
const Icon(Icons.arrow_drop_down),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:collection/collection.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';
|
||||||
@ -8,6 +8,7 @@ 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';
|
||||||
import 'package:marco/helpers/utils/contact_picker_helper.dart';
|
import 'package:marco/helpers/utils/contact_picker_helper.dart';
|
||||||
|
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||||
|
|
||||||
class AddContactBottomSheet extends StatefulWidget {
|
class AddContactBottomSheet extends StatefulWidget {
|
||||||
final ContactModel? existingContact;
|
final ContactModel? existingContact;
|
||||||
@ -18,25 +19,24 @@ class AddContactBottomSheet extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||||
// Controllers and state
|
final controller = Get.put(AddContactController());
|
||||||
final AddContactController controller = Get.put(AddContactController());
|
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
final nameController = TextEditingController();
|
|
||||||
final orgController = TextEditingController();
|
|
||||||
final addressController = TextEditingController();
|
|
||||||
final descriptionController = TextEditingController();
|
|
||||||
final tagTextController = TextEditingController();
|
|
||||||
|
|
||||||
// Use Rx for advanced toggle and dynamic fields
|
final nameCtrl = TextEditingController();
|
||||||
|
final orgCtrl = TextEditingController();
|
||||||
|
final addrCtrl = TextEditingController();
|
||||||
|
final descCtrl = TextEditingController();
|
||||||
|
final tagCtrl = TextEditingController();
|
||||||
|
|
||||||
final showAdvanced = false.obs;
|
final showAdvanced = false.obs;
|
||||||
final emailControllers = <TextEditingController>[].obs;
|
|
||||||
final emailLabels = <RxString>[].obs;
|
|
||||||
final phoneControllers = <TextEditingController>[].obs;
|
|
||||||
final phoneLabels = <RxString>[].obs;
|
|
||||||
|
|
||||||
// For required bucket validation (new)
|
|
||||||
final bucketError = ''.obs;
|
final bucketError = ''.obs;
|
||||||
|
|
||||||
|
final emailCtrls = <TextEditingController>[].obs;
|
||||||
|
final emailLabels = <RxString>[].obs;
|
||||||
|
|
||||||
|
final phoneCtrls = <TextEditingController>[].obs;
|
||||||
|
final phoneLabels = <RxString>[].obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -47,34 +47,40 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
void _initFields() {
|
void _initFields() {
|
||||||
final c = widget.existingContact;
|
final c = widget.existingContact;
|
||||||
if (c != null) {
|
if (c != null) {
|
||||||
nameController.text = c.name;
|
nameCtrl.text = c.name;
|
||||||
orgController.text = c.organization;
|
orgCtrl.text = c.organization;
|
||||||
addressController.text = c.address;
|
addrCtrl.text = c.address;
|
||||||
descriptionController.text = c.description ;
|
descCtrl.text = c.description;
|
||||||
}
|
|
||||||
if (c != null) {
|
emailCtrls.assignAll(c.contactEmails.isEmpty
|
||||||
emailControllers.assignAll(c.contactEmails.isEmpty
|
|
||||||
? [TextEditingController()]
|
? [TextEditingController()]
|
||||||
: c.contactEmails.map((e) => TextEditingController(text: e.emailAddress)));
|
: c.contactEmails
|
||||||
|
.map((e) => TextEditingController(text: e.emailAddress)));
|
||||||
emailLabels.assignAll(c.contactEmails.isEmpty
|
emailLabels.assignAll(c.contactEmails.isEmpty
|
||||||
? ['Office'.obs]
|
? ['Office'.obs]
|
||||||
: c.contactEmails.map((e) => e.label.obs));
|
: c.contactEmails.map((e) => e.label.obs));
|
||||||
phoneControllers.assignAll(c.contactPhones.isEmpty
|
|
||||||
|
phoneCtrls.assignAll(c.contactPhones.isEmpty
|
||||||
? [TextEditingController()]
|
? [TextEditingController()]
|
||||||
: c.contactPhones.map((p) => TextEditingController(text: p.phoneNumber)));
|
: c.contactPhones
|
||||||
|
.map((p) => TextEditingController(text: p.phoneNumber)));
|
||||||
phoneLabels.assignAll(c.contactPhones.isEmpty
|
phoneLabels.assignAll(c.contactPhones.isEmpty
|
||||||
? ['Work'.obs]
|
? ['Work'.obs]
|
||||||
: c.contactPhones.map((p) => p.label.obs));
|
: c.contactPhones.map((p) => p.label.obs));
|
||||||
controller.enteredTags.assignAll(c.tags.map((tag) => tag.name));
|
|
||||||
|
controller.enteredTags.assignAll(c.tags.map((e) => e.name));
|
||||||
|
|
||||||
ever(controller.isInitialized, (bool ready) {
|
ever(controller.isInitialized, (bool ready) {
|
||||||
if (ready) {
|
if (ready) {
|
||||||
final projectIds = c.projectIds;
|
final projectIds = c.projectIds;
|
||||||
final bucketId = c.bucketIds.firstOrNull;
|
final bucketId = c.bucketIds.firstOrNull;
|
||||||
final categoryName = c.contactCategory?.name;
|
final category = c.contactCategory?.name;
|
||||||
if (categoryName != null) controller.selectedCategory.value = categoryName;
|
|
||||||
|
if (category != null) controller.selectedCategory.value = category;
|
||||||
|
|
||||||
if (projectIds != null) {
|
if (projectIds != null) {
|
||||||
controller.selectedProjects.assignAll(
|
controller.selectedProjects.assignAll(
|
||||||
projectIds //
|
projectIds
|
||||||
.map((id) => controller.projectsMap.entries
|
.map((id) => controller.projectsMap.entries
|
||||||
.firstWhereOrNull((e) => e.value == id)
|
.firstWhereOrNull((e) => e.value == id)
|
||||||
?.key)
|
?.key)
|
||||||
@ -82,6 +88,7 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bucketId != null) {
|
if (bucketId != null) {
|
||||||
final name = controller.bucketsMap.entries
|
final name = controller.bucketsMap.entries
|
||||||
.firstWhereOrNull((e) => e.value == bucketId)
|
.firstWhereOrNull((e) => e.value == bucketId)
|
||||||
@ -91,32 +98,26 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
emailControllers.add(TextEditingController());
|
emailCtrls.add(TextEditingController());
|
||||||
emailLabels.add('Office'.obs);
|
emailLabels.add('Office'.obs);
|
||||||
phoneControllers.add(TextEditingController());
|
phoneCtrls.add(TextEditingController());
|
||||||
phoneLabels.add('Work'.obs);
|
phoneLabels.add('Work'.obs);
|
||||||
}
|
}
|
||||||
tagTextController.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
nameController.dispose();
|
nameCtrl.dispose();
|
||||||
orgController.dispose();
|
orgCtrl.dispose();
|
||||||
tagTextController.dispose();
|
addrCtrl.dispose();
|
||||||
addressController.dispose();
|
descCtrl.dispose();
|
||||||
descriptionController.dispose();
|
tagCtrl.dispose();
|
||||||
for (final c in emailControllers) {
|
emailCtrls.forEach((c) => c.dispose());
|
||||||
c.dispose();
|
phoneCtrls.forEach((c) => c.dispose());
|
||||||
}
|
|
||||||
for (final c in phoneControllers) {
|
|
||||||
c.dispose();
|
|
||||||
}
|
|
||||||
Get.delete<AddContactController>();
|
Get.delete<AddContactController>();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- COMMON WIDGETS ---
|
|
||||||
InputDecoration _inputDecoration(String hint) => InputDecoration(
|
InputDecoration _inputDecoration(String hint) => InputDecoration(
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||||
@ -134,167 +135,43 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5),
|
borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5),
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// DRY'd: LABELED FIELD ROW (used for phone/email)
|
Widget _textField(String label, TextEditingController ctrl,
|
||||||
Widget _buildLabeledRow({
|
{bool required = false, int maxLines = 1}) {
|
||||||
required String label,
|
return Column(
|
||||||
required RxString selectedLabel,
|
|
||||||
required List<String> options,
|
|
||||||
required String inputLabel,
|
|
||||||
required TextEditingController controller,
|
|
||||||
required TextInputType inputType,
|
|
||||||
VoidCallback? onRemove,
|
|
||||||
Widget? suffixIcon,
|
|
||||||
}) {
|
|
||||||
return Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MyText.labelMedium(label),
|
MyText.labelMedium(label),
|
||||||
MySpacing.height(8),
|
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(
|
TextFormField(
|
||||||
controller: controller,
|
controller: ctrl,
|
||||||
keyboardType: inputType,
|
maxLines: maxLines,
|
||||||
maxLength: inputType == TextInputType.phone ? 10 : null,
|
decoration: _inputDecoration("Enter $label"),
|
||||||
inputFormatters: inputType == TextInputType.phone
|
validator: required
|
||||||
? [FilteringTextInputFormatter.digitsOnly]
|
? (v) =>
|
||||||
: [],
|
(v == null || v.trim().isEmpty) ? "$label is required" : null
|
||||||
decoration: _inputDecoration("Enter $inputLabel").copyWith(
|
: null,
|
||||||
counterText: "",
|
|
||||||
suffixIcon: suffixIcon,
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.trim().isEmpty) return null;
|
|
||||||
final trimmed = value.trim();
|
|
||||||
if (inputType == TextInputType.phone &&
|
|
||||||
!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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// DRY: List builder for email/phone fields
|
Widget _popupSelector(RxString selected, List<String> options, String hint) =>
|
||||||
Widget _buildDynamicList({
|
Obx(() {
|
||||||
required RxList<TextEditingController> ctrls,
|
return GestureDetector(
|
||||||
required RxList<RxString> labels,
|
|
||||||
required List<String> labelOptions,
|
|
||||||
required String label,
|
|
||||||
required String inputLabel,
|
|
||||||
required TextInputType inputType,
|
|
||||||
required RxList listToRemoveFrom,
|
|
||||||
Widget? phoneSuffixIcon,
|
|
||||||
}) {
|
|
||||||
return Obx(() {
|
|
||||||
return Column(
|
|
||||||
children: List.generate(ctrls.length, (index) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
|
||||||
child: _buildLabeledRow(
|
|
||||||
label: label,
|
|
||||||
selectedLabel: labels[index],
|
|
||||||
options: labelOptions,
|
|
||||||
inputLabel: inputLabel,
|
|
||||||
controller: ctrls[index],
|
|
||||||
inputType: inputType,
|
|
||||||
onRemove: ctrls.length > 1
|
|
||||||
? () {
|
|
||||||
ctrls.removeAt(index);
|
|
||||||
labels.removeAt(index);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
suffixIcon: phoneSuffixIcon != null && inputType == TextInputType.phone
|
|
||||||
? IconButton(
|
|
||||||
icon: const Icon(Icons.contact_phone, color: Colors.blue),
|
|
||||||
onPressed: () async {
|
|
||||||
final selectedPhone =
|
|
||||||
await ContactPickerHelper.pickIndianPhoneNumber(context);
|
|
||||||
if (selectedPhone != null) {
|
|
||||||
ctrls[index].text = selectedPhone;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEmailList() => _buildDynamicList(
|
|
||||||
ctrls: emailControllers,
|
|
||||||
labels: emailLabels,
|
|
||||||
labelOptions: ["Office", "Personal", "Other"],
|
|
||||||
label: "Email Label",
|
|
||||||
inputLabel: "Email",
|
|
||||||
inputType: TextInputType.emailAddress,
|
|
||||||
listToRemoveFrom: emailControllers,
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildPhoneList() => _buildDynamicList(
|
|
||||||
ctrls: phoneControllers,
|
|
||||||
labels: phoneLabels,
|
|
||||||
labelOptions: ["Work", "Mobile", "Other"],
|
|
||||||
label: "Phone Label",
|
|
||||||
inputLabel: "Phone",
|
|
||||||
inputType: TextInputType.phone,
|
|
||||||
listToRemoveFrom: phoneControllers,
|
|
||||||
phoneSuffixIcon: const Icon(Icons.contact_phone, color: Colors.blue),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _popupSelector({
|
|
||||||
required String hint,
|
|
||||||
required RxString selectedValue,
|
|
||||||
required List<String> options,
|
|
||||||
}) =>
|
|
||||||
Obx(() => GestureDetector(
|
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final selected = await showMenu<String>(
|
final selectedItem = await showMenu<String>(
|
||||||
context: context,
|
context: context,
|
||||||
position: RelativeRect.fromLTRB(100, 300, 100, 0),
|
position: RelativeRect.fromLTRB(100, 300, 100, 0),
|
||||||
items: options.map((option) => PopupMenuItem<String>(value: option, child: Text(option))).toList(),
|
items: options
|
||||||
|
.map((e) => PopupMenuItem<String>(value: e, child: Text(e)))
|
||||||
|
.toList(),
|
||||||
);
|
);
|
||||||
if (selected != null) selectedValue.value = selected;
|
if (selectedItem != null) selected.value = selectedItem;
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 48,
|
height: 48,
|
||||||
@ -308,38 +185,119 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(selected.value.isNotEmpty ? selected.value : hint,
|
||||||
selectedValue.value.isNotEmpty ? selectedValue.value : hint,
|
style: const TextStyle(fontSize: 14)),
|
||||||
style: const TextStyle(fontSize: 14),
|
|
||||||
),
|
|
||||||
const Icon(Icons.expand_more, size: 20),
|
const Icon(Icons.expand_more, size: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
);
|
||||||
|
});
|
||||||
|
|
||||||
Widget _sectionLabel(String title) => Column(
|
Widget _dynamicList(
|
||||||
|
RxList<TextEditingController> ctrls,
|
||||||
|
RxList<RxString> labels,
|
||||||
|
String labelType,
|
||||||
|
List<String> 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,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MyText.labelLarge(title, fontWeight: 600),
|
Expanded(
|
||||||
MySpacing.height(4),
|
child: Column(
|
||||||
Divider(thickness: 1, color: Colors.grey.shade200),
|
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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// CHIP list for tags
|
Widget _tagInput() {
|
||||||
Widget _tagInputSection() {
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 48,
|
height: 48,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: tagTextController,
|
controller: tagCtrl,
|
||||||
onChanged: controller.filterSuggestions,
|
onChanged: controller.filterSuggestions,
|
||||||
onSubmitted: (value) {
|
onSubmitted: (v) {
|
||||||
controller.addEnteredTag(value);
|
controller.addEnteredTag(v);
|
||||||
tagTextController.clear();
|
tagCtrl.clear();
|
||||||
controller.clearSuggestions();
|
controller.clearSuggestions();
|
||||||
},
|
},
|
||||||
decoration: _inputDecoration("Start typing to add tags"),
|
decoration: _inputDecoration("Start typing to add tags"),
|
||||||
@ -353,19 +311,21 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4)],
|
boxShadow: const [
|
||||||
|
BoxShadow(color: Colors.black12, blurRadius: 4)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: controller.filteredSuggestions.length,
|
itemCount: controller.filteredSuggestions.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (_, i) {
|
||||||
final suggestion = controller.filteredSuggestions[index];
|
final suggestion = controller.filteredSuggestions[i];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Text(suggestion),
|
title: Text(suggestion),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
controller.addEnteredTag(suggestion);
|
controller.addEnteredTag(suggestion);
|
||||||
tagTextController.clear();
|
tagCtrl.clear();
|
||||||
controller.clearSuggestions();
|
controller.clearSuggestions();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -386,279 +346,91 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- REQUIRED FIELD (reusable)
|
void _handleSubmit() {
|
||||||
Widget _buildTextField(
|
bool valid = formKey.currentState?.validate() ?? false;
|
||||||
String label,
|
|
||||||
TextEditingController controller, {
|
|
||||||
int maxLines = 1,
|
|
||||||
bool required = false,
|
|
||||||
}) =>
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MyText.labelMedium(label),
|
|
||||||
MySpacing.height(8),
|
|
||||||
TextFormField(
|
|
||||||
controller: controller,
|
|
||||||
maxLines: maxLines,
|
|
||||||
decoration: _inputDecoration("Enter $label"),
|
|
||||||
validator: required
|
|
||||||
? (value) =>
|
|
||||||
value == null || value.trim().isEmpty ? "$label is required" : null
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// -- Organization as required TextFormField
|
|
||||||
Widget _buildOrganizationField() => Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MyText.labelMedium("Organization"),
|
|
||||||
MySpacing.height(8),
|
|
||||||
TextFormField(
|
|
||||||
controller: orgController,
|
|
||||||
onChanged: controller.filterOrganizationSuggestions,
|
|
||||||
decoration: _inputDecoration("Enter organization"),
|
|
||||||
validator: (value) =>
|
|
||||||
value == null || value.trim().isEmpty ? "Organization is required" : null,
|
|
||||||
),
|
|
||||||
Obx(() => controller.filteredOrgSuggestions.isEmpty
|
|
||||||
? const SizedBox.shrink()
|
|
||||||
: 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();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Action button row
|
|
||||||
Widget _buildActionButtons() => Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
Get.back();
|
|
||||||
Get.delete<AddContactController>();
|
|
||||||
},
|
|
||||||
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: () {
|
|
||||||
// Validate bucket first in UI and show error under dropdown if empty
|
|
||||||
bool valid = formKey.currentState!.validate();
|
|
||||||
if (controller.selectedBucket.value.isEmpty) {
|
if (controller.selectedBucket.value.isEmpty) {
|
||||||
bucketError.value = "Bucket is required";
|
bucketError.value = "Bucket is required";
|
||||||
valid = false;
|
valid = false;
|
||||||
} else {
|
} else {
|
||||||
bucketError.value = "";
|
bucketError.value = "";
|
||||||
}
|
}
|
||||||
if (valid) {
|
|
||||||
final emails = emailControllers
|
if (!valid) return;
|
||||||
|
|
||||||
|
final emails = emailCtrls
|
||||||
.asMap()
|
.asMap()
|
||||||
.entries
|
.entries
|
||||||
.where((entry) => entry.value.text.trim().isNotEmpty)
|
.where((e) => e.value.text.trim().isNotEmpty)
|
||||||
.map((entry) => {
|
.map((e) => {
|
||||||
"label": emailLabels[entry.key].value,
|
"label": emailLabels[e.key].value,
|
||||||
"emailAddress": entry.value.text.trim(),
|
"emailAddress": e.value.text.trim()
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
final phones = phoneControllers
|
|
||||||
|
final phones = phoneCtrls
|
||||||
.asMap()
|
.asMap()
|
||||||
.entries
|
.entries
|
||||||
.where((entry) => entry.value.text.trim().isNotEmpty)
|
.where((e) => e.value.text.trim().isNotEmpty)
|
||||||
.map((entry) => {
|
.map((e) => {
|
||||||
"label": phoneLabels[entry.key].value,
|
"label": phoneLabels[e.key].value,
|
||||||
"phoneNumber": entry.value.text.trim(),
|
"phoneNumber": e.value.text.trim()
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
controller.submitContact(
|
controller.submitContact(
|
||||||
id: widget.existingContact?.id,
|
id: widget.existingContact?.id,
|
||||||
name: nameController.text.trim(),
|
name: nameCtrl.text.trim(),
|
||||||
organization: orgController.text.trim(),
|
organization: orgCtrl.text.trim(),
|
||||||
emails: emails,
|
emails: emails,
|
||||||
phones: phones,
|
phones: phones,
|
||||||
address: addressController.text.trim(),
|
address: addrCtrl.text.trim(),
|
||||||
description: descriptionController.text.trim(),
|
description: descCtrl.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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Projects multi-select section
|
|
||||||
Widget _projectSelectorUI() {
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () async {
|
|
||||||
await showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (_) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('Select Projects'),
|
|
||||||
content: Obx(() => 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(
|
|
||||||
checkboxTheme: CheckboxThemeData(
|
|
||||||
fillColor: MaterialStateProperty.resolveWith<Color>(
|
|
||||||
(states) => states.contains(MaterialState.selected)
|
|
||||||
? Colors.white
|
|
||||||
: Colors.transparent),
|
|
||||||
checkColor: MaterialStateProperty.all(Colors.black),
|
|
||||||
side: const BorderSide(color: Colors.black, width: 2),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: CheckboxListTile(
|
|
||||||
dense: true,
|
|
||||||
title: Text(project),
|
|
||||||
value: isSelected,
|
|
||||||
onChanged: (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),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MAIN BUILD ---
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
if (!controller.isInitialized.value) {
|
if (!controller.isInitialized.value) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
return SafeArea(
|
|
||||||
child: SingleChildScrollView(
|
return BaseBottomSheet(
|
||||||
padding: EdgeInsets.only(top: 32).add(MediaQuery.of(context).viewInsets),
|
title: widget.existingContact != null
|
||||||
child: Container(
|
? "Edit Contact"
|
||||||
decoration: BoxDecoration(
|
: "Create New Contact",
|
||||||
color: Theme.of(context).cardColor,
|
onCancel: () => Get.back(),
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
onSubmit: _handleSubmit,
|
||||||
),
|
isSubmitting: controller.isSubmitting.value,
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
|
|
||||||
child: Form(
|
child: Form(
|
||||||
key: formKey,
|
key: formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Center(
|
_textField("Name", nameCtrl, required: true),
|
||||||
child: MyText.titleMedium(
|
|
||||||
widget.existingContact != null ? "Edit Contact" : "Create New Contact",
|
|
||||||
fontWeight: 700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(24),
|
|
||||||
_sectionLabel("Required Fields"),
|
|
||||||
MySpacing.height(12),
|
|
||||||
_buildTextField("Name", nameController, required: true),
|
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
_buildOrganizationField(),
|
_textField("Organization", orgCtrl, required: true),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
MyText.labelMedium("Select Bucket"),
|
MyText.labelMedium("Select Bucket"),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
Stack(
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
_popupSelector(
|
_popupSelector(controller.selectedBucket, controller.buckets,
|
||||||
hint: "Select Bucket",
|
"Select Bucket"),
|
||||||
selectedValue: controller.selectedBucket,
|
|
||||||
options: controller.buckets,
|
|
||||||
),
|
|
||||||
// Validation message for bucket
|
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 56,
|
top: 56,
|
||||||
child: Obx(
|
child: Obx(() => bucketError.value.isEmpty
|
||||||
() => bucketError.value.isEmpty
|
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
padding:
|
||||||
child: Text(
|
const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
bucketError.value,
|
child: Text(bucketError.value,
|
||||||
style: const TextStyle(color: Colors.red, fontSize: 12),
|
style: const TextStyle(
|
||||||
),
|
color: Colors.red, fontSize: 12)),
|
||||||
),
|
)),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -668,8 +440,11 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
MyText.labelLarge("Advanced Details (Optional)", fontWeight: 600),
|
MyText.labelLarge("Advanced Details (Optional)",
|
||||||
Icon(showAdvanced.value ? Icons.expand_less : Icons.expand_more),
|
fontWeight: 600),
|
||||||
|
Icon(showAdvanced.value
|
||||||
|
? Icons.expand_less
|
||||||
|
: Icons.expand_more),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
@ -678,59 +453,49 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
_sectionLabel("Contact Info"),
|
_dynamicList(
|
||||||
MySpacing.height(16),
|
emailCtrls,
|
||||||
_buildEmailList(),
|
emailLabels,
|
||||||
|
"Email",
|
||||||
|
["Office", "Personal", "Other"],
|
||||||
|
TextInputType.emailAddress),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
emailControllers.add(TextEditingController());
|
emailCtrls.add(TextEditingController());
|
||||||
emailLabels.add('Office'.obs);
|
emailLabels.add("Office".obs);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: const Text("Add Email"),
|
label: const Text("Add Email"),
|
||||||
),
|
),
|
||||||
_buildPhoneList(),
|
_dynamicList(phoneCtrls, phoneLabels, "Phone",
|
||||||
|
["Work", "Mobile", "Other"], TextInputType.phone),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
phoneControllers.add(TextEditingController());
|
phoneCtrls.add(TextEditingController());
|
||||||
phoneLabels.add('Work'.obs);
|
phoneLabels.add("Work".obs);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: const Text("Add Phone"),
|
label: const Text("Add Phone"),
|
||||||
),
|
),
|
||||||
MySpacing.height(24),
|
|
||||||
_sectionLabel("Other Details"),
|
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
MyText.labelMedium("Category"),
|
MyText.labelMedium("Category"),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
_popupSelector(
|
_popupSelector(controller.selectedCategory,
|
||||||
hint: "Select Category",
|
controller.categories, "Select Category"),
|
||||||
selectedValue: controller.selectedCategory,
|
|
||||||
options: controller.categories,
|
|
||||||
),
|
|
||||||
MySpacing.height(16),
|
|
||||||
MyText.labelMedium("Select Projects"),
|
|
||||||
MySpacing.height(8),
|
|
||||||
_projectSelectorUI(),
|
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
MyText.labelMedium("Tags"),
|
MyText.labelMedium("Tags"),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
_tagInputSection(),
|
_tagInput(),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
_buildTextField("Address", addressController, maxLines: 2, required: false),
|
_textField("Address", addrCtrl),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
_buildTextField("Description", descriptionController, maxLines: 2, required: false),
|
_textField("Description", descCtrl),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink()),
|
: const SizedBox.shrink()),
|
||||||
MySpacing.height(24),
|
|
||||||
_buildActionButtons(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user