Vaibhav_Feature-#768 #59
@ -24,6 +24,7 @@ class AddContactController extends GetxController {
|
||||
final RxMap<String, String> tagsMap = <String, String>{}.obs;
|
||||
final RxBool isInitialized = false.obs;
|
||||
final RxList<String> selectedProjects = <String>[].obs;
|
||||
final RxBool isSubmitting = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -94,6 +95,9 @@ class AddContactController extends GetxController {
|
||||
required String address,
|
||||
required String description,
|
||||
}) async {
|
||||
if (isSubmitting.value) return;
|
||||
isSubmitting.value = true;
|
||||
|
||||
final categoryId = categoriesMap[selectedCategory.value];
|
||||
final bucketId = bucketsMap[selectedBucket.value];
|
||||
final projectIds = selectedProjects
|
||||
@ -101,13 +105,13 @@ class AddContactController extends GetxController {
|
||||
.whereType<String>()
|
||||
.toList();
|
||||
|
||||
// === Required validations only for name, organization, and bucket ===
|
||||
if (name.trim().isEmpty) {
|
||||
showAppSnackbar(
|
||||
title: "Missing Name",
|
||||
message: "Please enter the contact name.",
|
||||
type: SnackbarType.warning,
|
||||
);
|
||||
isSubmitting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -117,6 +121,7 @@ class AddContactController extends GetxController {
|
||||
message: "Please enter the organization name.",
|
||||
type: SnackbarType.warning,
|
||||
);
|
||||
isSubmitting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -126,10 +131,10 @@ class AddContactController extends GetxController {
|
||||
message: "Please select a bucket.",
|
||||
type: SnackbarType.warning,
|
||||
);
|
||||
isSubmitting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// === Build body (include optional fields if available) ===
|
||||
try {
|
||||
final tagObjects = enteredTags.map((tagName) {
|
||||
final tagId = tagsMap[tagName];
|
||||
@ -182,6 +187,8 @@ class AddContactController extends GetxController {
|
||||
message: "Something went wrong",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,7 +137,6 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
||||
onCancel: () => Navigator.of(context).pop(),
|
||||
onSubmit: _submitComment,
|
||||
isSubmitting: controller.isLoading.value,
|
||||
submitText: 'Comment',
|
||||
bottomContent: _buildCommentsSection(),
|
||||
child: Form(
|
||||
// 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_snackbar.dart';
|
||||
|
||||
|
||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||
|
||||
void showCreateTaskBottomSheet({
|
||||
required String workArea,
|
||||
@ -27,147 +26,15 @@ void showCreateTaskBottomSheet({
|
||||
Get.bottomSheet(
|
||||
StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isLarge = constraints.maxWidth > 600;
|
||||
final horizontalPadding =
|
||||
isLarge ? constraints.maxWidth * 0.2 : 16.0;
|
||||
return BaseBottomSheet(
|
||||
title: "Create Task",
|
||||
onCancel: () => Get.back(),
|
||||
onSubmit: () async {
|
||||
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) {
|
||||
showAppSnackbar(
|
||||
title: "error",
|
||||
@ -188,8 +55,7 @@ void showCreateTaskBottomSheet({
|
||||
|
||||
if (success) {
|
||||
Get.back();
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 300), () {
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
onSubmit();
|
||||
showAppSnackbar(
|
||||
title: "Success",
|
||||
@ -199,25 +65,81 @@ void showCreateTaskBottomSheet({
|
||||
});
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.check, size: 18),
|
||||
label: MyText.bodyMedium("Submit",
|
||||
color: Colors.white, fontWeight: 600),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blueAccent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
submitText: "Submit",
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:marco/controller/directory/add_contact_controller.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
@ -8,6 +8,7 @@ import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||
import 'package:marco/model/directory/contact_model.dart';
|
||||
import 'package:marco/helpers/utils/contact_picker_helper.dart';
|
||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||
|
||||
class AddContactBottomSheet extends StatefulWidget {
|
||||
final ContactModel? existingContact;
|
||||
@ -18,25 +19,24 @@ class AddContactBottomSheet extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
// Controllers and state
|
||||
final AddContactController controller = Get.put(AddContactController());
|
||||
final controller = Get.put(AddContactController());
|
||||
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 emailControllers = <TextEditingController>[].obs;
|
||||
final emailLabels = <RxString>[].obs;
|
||||
final phoneControllers = <TextEditingController>[].obs;
|
||||
final phoneLabels = <RxString>[].obs;
|
||||
|
||||
// For required bucket validation (new)
|
||||
final bucketError = ''.obs;
|
||||
|
||||
final emailCtrls = <TextEditingController>[].obs;
|
||||
final emailLabels = <RxString>[].obs;
|
||||
|
||||
final phoneCtrls = <TextEditingController>[].obs;
|
||||
final phoneLabels = <RxString>[].obs;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -47,34 +47,40 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
void _initFields() {
|
||||
final c = widget.existingContact;
|
||||
if (c != null) {
|
||||
nameController.text = c.name;
|
||||
orgController.text = c.organization;
|
||||
addressController.text = c.address;
|
||||
descriptionController.text = c.description ;
|
||||
}
|
||||
if (c != null) {
|
||||
emailControllers.assignAll(c.contactEmails.isEmpty
|
||||
nameCtrl.text = c.name;
|
||||
orgCtrl.text = c.organization;
|
||||
addrCtrl.text = c.address;
|
||||
descCtrl.text = c.description;
|
||||
|
||||
emailCtrls.assignAll(c.contactEmails.isEmpty
|
||||
? [TextEditingController()]
|
||||
: c.contactEmails.map((e) => TextEditingController(text: e.emailAddress)));
|
||||
: c.contactEmails
|
||||
.map((e) => TextEditingController(text: e.emailAddress)));
|
||||
emailLabels.assignAll(c.contactEmails.isEmpty
|
||||
? ['Office'.obs]
|
||||
: c.contactEmails.map((e) => e.label.obs));
|
||||
phoneControllers.assignAll(c.contactPhones.isEmpty
|
||||
|
||||
phoneCtrls.assignAll(c.contactPhones.isEmpty
|
||||
? [TextEditingController()]
|
||||
: c.contactPhones.map((p) => TextEditingController(text: p.phoneNumber)));
|
||||
: c.contactPhones
|
||||
.map((p) => TextEditingController(text: p.phoneNumber)));
|
||||
phoneLabels.assignAll(c.contactPhones.isEmpty
|
||||
? ['Work'.obs]
|
||||
: c.contactPhones.map((p) => p.label.obs));
|
||||
controller.enteredTags.assignAll(c.tags.map((tag) => tag.name));
|
||||
|
||||
controller.enteredTags.assignAll(c.tags.map((e) => e.name));
|
||||
|
||||
ever(controller.isInitialized, (bool ready) {
|
||||
if (ready) {
|
||||
final projectIds = c.projectIds;
|
||||
final bucketId = c.bucketIds.firstOrNull;
|
||||
final categoryName = c.contactCategory?.name;
|
||||
if (categoryName != null) controller.selectedCategory.value = categoryName;
|
||||
final category = c.contactCategory?.name;
|
||||
|
||||
if (category != null) controller.selectedCategory.value = category;
|
||||
|
||||
if (projectIds != null) {
|
||||
controller.selectedProjects.assignAll(
|
||||
projectIds //
|
||||
projectIds
|
||||
.map((id) => controller.projectsMap.entries
|
||||
.firstWhereOrNull((e) => e.value == id)
|
||||
?.key)
|
||||
@ -82,6 +88,7 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
if (bucketId != null) {
|
||||
final name = controller.bucketsMap.entries
|
||||
.firstWhereOrNull((e) => e.value == bucketId)
|
||||
@ -91,32 +98,26 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
emailControllers.add(TextEditingController());
|
||||
emailCtrls.add(TextEditingController());
|
||||
emailLabels.add('Office'.obs);
|
||||
phoneControllers.add(TextEditingController());
|
||||
phoneCtrls.add(TextEditingController());
|
||||
phoneLabels.add('Work'.obs);
|
||||
}
|
||||
tagTextController.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
orgController.dispose();
|
||||
tagTextController.dispose();
|
||||
addressController.dispose();
|
||||
descriptionController.dispose();
|
||||
for (final c in emailControllers) {
|
||||
c.dispose();
|
||||
}
|
||||
for (final c in phoneControllers) {
|
||||
c.dispose();
|
||||
}
|
||||
nameCtrl.dispose();
|
||||
orgCtrl.dispose();
|
||||
addrCtrl.dispose();
|
||||
descCtrl.dispose();
|
||||
tagCtrl.dispose();
|
||||
emailCtrls.forEach((c) => c.dispose());
|
||||
phoneCtrls.forEach((c) => c.dispose());
|
||||
Get.delete<AddContactController>();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- COMMON WIDGETS ---
|
||||
InputDecoration _inputDecoration(String hint) => InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||
@ -134,167 +135,43 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
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,
|
||||
);
|
||||
|
||||
// DRY'd: LABELED FIELD ROW (used for phone/email)
|
||||
Widget _buildLabeledRow({
|
||||
required String label,
|
||||
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(
|
||||
Widget _textField(String label, TextEditingController ctrl,
|
||||
{bool required = false, int maxLines = 1}) {
|
||||
return 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: "",
|
||||
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,
|
||||
),
|
||||
controller: ctrl,
|
||||
maxLines: maxLines,
|
||||
decoration: _inputDecoration("Enter $label"),
|
||||
validator: required
|
||||
? (v) =>
|
||||
(v == null || v.trim().isEmpty) ? "$label is required" : null
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// DRY: List builder for email/phone fields
|
||||
Widget _buildDynamicList({
|
||||
required RxList<TextEditingController> ctrls,
|
||||
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(
|
||||
Widget _popupSelector(RxString selected, List<String> options, String hint) =>
|
||||
Obx(() {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final selected = await showMenu<String>(
|
||||
final selectedItem = await showMenu<String>(
|
||||
context: context,
|
||||
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(
|
||||
height: 48,
|
||||
@ -308,38 +185,119 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
selectedValue.value.isNotEmpty ? selectedValue.value : hint,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
Text(selected.value.isNotEmpty ? selected.value : hint,
|
||||
style: const TextStyle(fontSize: 14)),
|
||||
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,
|
||||
children: [
|
||||
MyText.labelLarge(title, fontWeight: 600),
|
||||
MySpacing.height(4),
|
||||
Divider(thickness: 1, color: Colors.grey.shade200),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelMedium("$labelType Label"),
|
||||
MySpacing.height(8),
|
||||
_popupSelector(labels[i], labelOptions, "Label"),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelMedium(labelType),
|
||||
MySpacing.height(8),
|
||||
TextFormField(
|
||||
controller: ctrls[i],
|
||||
keyboardType: type,
|
||||
maxLength: type == TextInputType.phone ? 10 : null,
|
||||
inputFormatters: type == TextInputType.phone
|
||||
? [FilteringTextInputFormatter.digitsOnly]
|
||||
: [],
|
||||
decoration:
|
||||
_inputDecoration("Enter $labelType").copyWith(
|
||||
counterText: "",
|
||||
suffixIcon: type == TextInputType.phone
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.contact_phone,
|
||||
color: Colors.blue),
|
||||
onPressed: () async {
|
||||
final phone = await ContactPickerHelper
|
||||
.pickIndianPhoneNumber(context);
|
||||
if (phone != null) ctrls[i].text = phone;
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty)
|
||||
return null;
|
||||
final trimmed = value.trim();
|
||||
if (type == TextInputType.phone &&
|
||||
!RegExp(r'^[1-9][0-9]{9}$').hasMatch(trimmed)) {
|
||||
return "Enter valid phone number";
|
||||
}
|
||||
if (type == TextInputType.emailAddress &&
|
||||
!RegExp(r'^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$')
|
||||
.hasMatch(trimmed)) {
|
||||
return "Enter valid email";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (ctrls.length > 1)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.remove_circle_outline,
|
||||
color: Colors.red),
|
||||
onPressed: () {
|
||||
ctrls.removeAt(i);
|
||||
labels.removeAt(i);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// CHIP list for tags
|
||||
Widget _tagInputSection() {
|
||||
Widget _tagInput() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: TextField(
|
||||
controller: tagTextController,
|
||||
controller: tagCtrl,
|
||||
onChanged: controller.filterSuggestions,
|
||||
onSubmitted: (value) {
|
||||
controller.addEnteredTag(value);
|
||||
tagTextController.clear();
|
||||
onSubmitted: (v) {
|
||||
controller.addEnteredTag(v);
|
||||
tagCtrl.clear();
|
||||
controller.clearSuggestions();
|
||||
},
|
||||
decoration: _inputDecoration("Start typing to add tags"),
|
||||
@ -353,19 +311,21 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4)],
|
||||
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];
|
||||
itemBuilder: (_, i) {
|
||||
final suggestion = controller.filteredSuggestions[i];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
title: Text(suggestion),
|
||||
onTap: () {
|
||||
controller.addEnteredTag(suggestion);
|
||||
tagTextController.clear();
|
||||
tagCtrl.clear();
|
||||
controller.clearSuggestions();
|
||||
},
|
||||
);
|
||||
@ -386,279 +346,91 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
);
|
||||
}
|
||||
|
||||
// ---- REQUIRED FIELD (reusable)
|
||||
Widget _buildTextField(
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
void _handleSubmit() {
|
||||
bool valid = formKey.currentState?.validate() ?? false;
|
||||
|
||||
// -- 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) {
|
||||
bucketError.value = "Bucket is required";
|
||||
valid = false;
|
||||
} else {
|
||||
bucketError.value = "";
|
||||
}
|
||||
if (valid) {
|
||||
final emails = emailControllers
|
||||
|
||||
if (!valid) return;
|
||||
|
||||
final emails = emailCtrls
|
||||
.asMap()
|
||||
.entries
|
||||
.where((entry) => entry.value.text.trim().isNotEmpty)
|
||||
.map((entry) => {
|
||||
"label": emailLabels[entry.key].value,
|
||||
"emailAddress": entry.value.text.trim(),
|
||||
.where((e) => e.value.text.trim().isNotEmpty)
|
||||
.map((e) => {
|
||||
"label": emailLabels[e.key].value,
|
||||
"emailAddress": e.value.text.trim()
|
||||
})
|
||||
.toList();
|
||||
final phones = phoneControllers
|
||||
|
||||
final phones = phoneCtrls
|
||||
.asMap()
|
||||
.entries
|
||||
.where((entry) => entry.value.text.trim().isNotEmpty)
|
||||
.map((entry) => {
|
||||
"label": phoneLabels[entry.key].value,
|
||||
"phoneNumber": entry.value.text.trim(),
|
||||
.where((e) => e.value.text.trim().isNotEmpty)
|
||||
.map((e) => {
|
||||
"label": phoneLabels[e.key].value,
|
||||
"phoneNumber": e.value.text.trim()
|
||||
})
|
||||
.toList();
|
||||
|
||||
controller.submitContact(
|
||||
id: widget.existingContact?.id,
|
||||
name: nameController.text.trim(),
|
||||
organization: orgController.text.trim(),
|
||||
name: nameCtrl.text.trim(),
|
||||
organization: orgCtrl.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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// 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),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
address: addrCtrl.text.trim(),
|
||||
description: descCtrl.text.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
// --- MAIN BUILD ---
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
if (!controller.isInitialized.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(top: 32).add(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),
|
||||
|
||||
return BaseBottomSheet(
|
||||
title: widget.existingContact != null
|
||||
? "Edit Contact"
|
||||
: "Create New Contact",
|
||||
onCancel: () => Get.back(),
|
||||
onSubmit: _handleSubmit,
|
||||
isSubmitting: controller.isSubmitting.value,
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
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),
|
||||
_textField("Name", nameCtrl, required: true),
|
||||
MySpacing.height(16),
|
||||
_buildOrganizationField(),
|
||||
_textField("Organization", orgCtrl, required: true),
|
||||
MySpacing.height(16),
|
||||
MyText.labelMedium("Select Bucket"),
|
||||
MySpacing.height(8),
|
||||
Stack(
|
||||
children: [
|
||||
_popupSelector(
|
||||
hint: "Select Bucket",
|
||||
selectedValue: controller.selectedBucket,
|
||||
options: controller.buckets,
|
||||
),
|
||||
// Validation message for bucket
|
||||
_popupSelector(controller.selectedBucket, controller.buckets,
|
||||
"Select Bucket"),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 56,
|
||||
child: Obx(
|
||||
() => bucketError.value.isEmpty
|
||||
child: Obx(() => bucketError.value.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
||||
child: Text(
|
||||
bucketError.value,
|
||||
style: const TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text(bucketError.value,
|
||||
style: const TextStyle(
|
||||
color: Colors.red, fontSize: 12)),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -668,8 +440,11 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.labelLarge("Advanced Details (Optional)", fontWeight: 600),
|
||||
Icon(showAdvanced.value ? Icons.expand_less : Icons.expand_more),
|
||||
MyText.labelLarge("Advanced Details (Optional)",
|
||||
fontWeight: 600),
|
||||
Icon(showAdvanced.value
|
||||
? Icons.expand_less
|
||||
: Icons.expand_more),
|
||||
],
|
||||
),
|
||||
)),
|
||||
@ -678,59 +453,49 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(24),
|
||||
_sectionLabel("Contact Info"),
|
||||
MySpacing.height(16),
|
||||
_buildEmailList(),
|
||||
_dynamicList(
|
||||
emailCtrls,
|
||||
emailLabels,
|
||||
"Email",
|
||||
["Office", "Personal", "Other"],
|
||||
TextInputType.emailAddress),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
emailControllers.add(TextEditingController());
|
||||
emailLabels.add('Office'.obs);
|
||||
emailCtrls.add(TextEditingController());
|
||||
emailLabels.add("Office".obs);
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text("Add Email"),
|
||||
),
|
||||
_buildPhoneList(),
|
||||
_dynamicList(phoneCtrls, phoneLabels, "Phone",
|
||||
["Work", "Mobile", "Other"], TextInputType.phone),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
phoneControllers.add(TextEditingController());
|
||||
phoneLabels.add('Work'.obs);
|
||||
phoneCtrls.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),
|
||||
_projectSelectorUI(),
|
||||
_popupSelector(controller.selectedCategory,
|
||||
controller.categories, "Select Category"),
|
||||
MySpacing.height(16),
|
||||
MyText.labelMedium("Tags"),
|
||||
MySpacing.height(8),
|
||||
_tagInputSection(),
|
||||
_tagInput(),
|
||||
MySpacing.height(16),
|
||||
_buildTextField("Address", addressController, maxLines: 2, required: false),
|
||||
_textField("Address", addrCtrl),
|
||||
MySpacing.height(16),
|
||||
_buildTextField("Description", descriptionController, maxLines: 2, required: false),
|
||||
_textField("Description", descCtrl),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink()),
|
||||
MySpacing.height(24),
|
||||
_buildActionButtons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user