marco.pms.mobileapp/lib/model/directory/add_contact_bottom_sheet.dart
Vaibhav Surve a0f1602f4e feat(directory): add contact profile and directory management features
- Implemented ContactProfileResponse and related models for handling contact details.
- Created ContactTagResponse and ContactTag models for managing contact tags.
- Added DirectoryCommentResponse and DirectoryComment models for comment management.
- Developed DirectoryFilterBottomSheet for filtering contacts.
- Introduced OrganizationListModel for organization data handling.
- Updated routes to include DirectoryMainScreen.
- Enhanced DashboardScreen to navigate to the new directory page.
- Created ContactDetailScreen for displaying detailed contact information.
- Developed DirectoryMainScreen for managing and displaying contacts.
- Added dependencies for font_awesome_flutter and flutter_html in pubspec.yaml.
2025-07-02 15:57:39 +05:30

445 lines
15 KiB
Dart

// unchanged imports
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/directory/add_contact_controller.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart';
class AddContactBottomSheet extends StatelessWidget {
AddContactBottomSheet({super.key});
final controller = Get.put(AddContactController());
final formKey = GlobalKey<FormState>();
final emailLabel = 'Office'.obs;
final phoneLabel = 'Work'.obs;
final nameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final orgController = TextEditingController();
final tagTextController = TextEditingController();
final addressController = TextEditingController();
final descriptionController = TextEditingController();
InputDecoration _inputDecoration(String hint) => InputDecoration(
hintText: hint,
hintStyle: MyTextStyle.bodySmall(xMuted: true),
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
isDense: true,
);
Widget _popupSelector({
required String hint,
required RxString selectedValue,
required List<String> options,
}) {
return Obx(() => GestureDetector(
onTap: () async {
final selected = await showMenu<String>(
context: Navigator.of(Get.context!).overlay!.context,
position: const RelativeRect.fromLTRB(100, 300, 100, 0),
items: options
.map((e) => PopupMenuItem<String>(value: e, child: Text(e)))
.toList(),
);
if (selected != null) selectedValue.value = selected;
},
child: AbsorbPointer(
child: SizedBox(
height: 48,
child: TextFormField(
readOnly: true,
initialValue: selectedValue.value,
style: const TextStyle(fontSize: 14),
decoration: _inputDecoration(hint)
.copyWith(suffixIcon: const Icon(Icons.expand_more)),
),
),
),
));
}
Widget _dropdownField({
required String label,
required RxString selectedValue,
required RxList<String> options,
}) {
return Obx(() => SizedBox(
height: 48,
child: PopupMenuButton<String>(
onSelected: (value) => selectedValue.value = value,
itemBuilder: (_) => options
.map((item) => PopupMenuItem(value: item, child: Text(item)))
.toList(),
padding: EdgeInsets.zero,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
color: Colors.grey.shade100,
),
alignment: Alignment.centerLeft,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
selectedValue.value.isEmpty ? label : selectedValue.value,
style: const TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.arrow_drop_down),
],
),
),
),
));
}
Widget _tagInputSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 48,
child: TextField(
controller: tagTextController,
onChanged: controller.filterSuggestions,
onSubmitted: (value) {
controller.addEnteredTag(value);
tagTextController.clear();
controller.clearSuggestions();
},
decoration: _inputDecoration("Start typing to add tags"),
),
),
Obx(() => controller.filteredSuggestions.isEmpty
? const SizedBox()
: _buildSuggestionsList()),
MySpacing.height(8),
Obx(() => Wrap(
spacing: 8,
children: controller.enteredTags
.map((tag) => Chip(
label: Text(tag),
onDeleted: () => controller.removeEnteredTag(tag),
))
.toList(),
)),
],
);
}
Widget _buildSuggestionsList() => Container(
margin: const EdgeInsets.only(top: 4),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
boxShadow: const [
BoxShadow(
color: Colors.black12, blurRadius: 4, offset: Offset(0, 2)),
],
),
child: ListView.builder(
shrinkWrap: true,
itemCount: controller.filteredSuggestions.length,
itemBuilder: (context, index) {
final suggestion = controller.filteredSuggestions[index];
return ListTile(
dense: true,
title: Text(suggestion),
onTap: () {
controller.addEnteredTag(suggestion);
tagTextController.clear();
controller.clearSuggestions();
},
);
},
),
);
Widget _sectionLabel(String title) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelLarge(title, fontWeight: 600),
MySpacing.height(4),
Divider(thickness: 1, color: Colors.grey.shade200),
],
);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SingleChildScrollView(
padding: MediaQuery.of(context).viewInsets,
child: Container(
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: const [
BoxShadow(
color: Colors.black12, blurRadius: 12, offset: Offset(0, -2))
],
),
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
child: Form(
key: formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 5,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(10),
),
),
),
MySpacing.height(12),
Center(
child:
MyText.titleMedium("Create New Contact", fontWeight: 700),
),
MySpacing.height(24),
_sectionLabel("Basic Info"),
MySpacing.height(16),
_buildTextField("Name", nameController),
MySpacing.height(16),
_buildOrganizationField(),
MySpacing.height(24),
_sectionLabel("Contact Info"),
MySpacing.height(16),
_buildLabeledRow(
"Email Label",
emailLabel,
["Office", "Personal", "Other"],
"Email",
emailController,
TextInputType.emailAddress),
MySpacing.height(16),
_buildLabeledRow(
"Phone Label",
phoneLabel,
["Work", "Mobile", "Other"],
"Phone",
phoneController,
TextInputType.phone),
MySpacing.height(24),
_sectionLabel("Other Details"),
MySpacing.height(16),
MyText.labelMedium("Category"),
MySpacing.height(8),
_dropdownField(
label: "Select Category",
selectedValue: controller.selectedCategory,
options: controller.categories,
),
MySpacing.height(16),
MyText.labelMedium("Select Projects"),
MySpacing.height(8),
_dropdownField(
label: "Select Project",
selectedValue: controller.selectedProject,
options: controller.globalProjects,
),
MySpacing.height(16),
MyText.labelMedium("Tags"),
MySpacing.height(8),
_tagInputSection(),
MySpacing.height(16),
MyText.labelMedium("Select Bucket"),
MySpacing.height(8),
_dropdownField(
label: "Select Bucket",
selectedValue: controller.selectedBucket,
options: controller.buckets,
),
MySpacing.height(16),
_buildTextField("Address", addressController, maxLines: 2),
MySpacing.height(16),
_buildTextField("Description", descriptionController,
maxLines: 2),
MySpacing.height(24),
_buildActionButtons(),
],
),
),
),
),
);
}
Widget _buildTextField(String label, TextEditingController controller,
{int maxLines = 1}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelMedium(label),
MySpacing.height(8),
TextFormField(
controller: controller,
maxLines: maxLines,
decoration: _inputDecoration("Enter $label"),
validator: (value) => (value == null || value.trim().isEmpty)
? "$label is required"
: null,
),
],
);
}
Widget _buildOrganizationField() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelMedium("Organization"),
MySpacing.height(8),
TextField(
controller: orgController,
onChanged: controller.filterOrganizationSuggestions,
decoration: _inputDecoration("Enter organization"),
),
Obx(() => controller.filteredOrgSuggestions.isEmpty
? const SizedBox()
: ListView.builder(
shrinkWrap: true,
itemCount: controller.filteredOrgSuggestions.length,
itemBuilder: (context, index) {
final suggestion = controller.filteredOrgSuggestions[index];
return ListTile(
dense: true,
title: Text(suggestion),
onTap: () {
orgController.text = suggestion;
controller.filteredOrgSuggestions.clear();
},
);
},
))
],
);
}
Widget _buildLabeledRow(
String label,
RxString selectedLabel,
List<String> options,
String inputLabel,
TextEditingController controller,
TextInputType inputType,
) {
return Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelMedium(label),
MySpacing.height(8),
_popupSelector(
hint: "Label",
selectedValue: selectedLabel,
options: options,
),
],
),
),
MySpacing.width(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelMedium(inputLabel),
MySpacing.height(8),
SizedBox(
height: 48,
child: TextFormField(
controller: controller,
keyboardType: inputType,
decoration: _inputDecoration("Enter $inputLabel"),
validator: (value) =>
(value == null || value.trim().isEmpty)
? "$inputLabel is required"
: null,
),
),
],
),
),
],
);
}
Widget _buildActionButtons() {
return Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => Get.back(),
icon: const Icon(Icons.close, color: Colors.red),
label:
MyText.bodyMedium("Cancel", color: Colors.red, fontWeight: 600),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.red),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
),
),
),
MySpacing.width(12),
Expanded(
child: ElevatedButton.icon(
onPressed: () {
if (formKey.currentState!.validate()) {
controller.submitContact(
name: nameController.text.trim(),
organization: orgController.text.trim(),
email: emailController.text.trim(),
emailLabel: emailLabel.value,
phone: phoneController.text.trim(),
phoneLabel: phoneLabel.value,
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),
),
),
),
],
);
}
}