feat(contact): implement contact editing functionality and update API integration
This commit is contained in:
parent
5fb18a13d2
commit
445cd75e03
@ -82,6 +82,7 @@ class AddContactController extends GetxController {
|
||||
}
|
||||
|
||||
Future<void> submitContact({
|
||||
String? id,
|
||||
required String name,
|
||||
required String organization,
|
||||
required List<Map<String, String>> emails,
|
||||
@ -96,10 +97,13 @@ class AddContactController extends GetxController {
|
||||
|
||||
final tagObjects = enteredTags.map((tagName) {
|
||||
final tagId = tagsMap[tagName];
|
||||
return tagId != null ? {"id": tagId, "name": tagName} : {"name": tagName};
|
||||
return tagId != null
|
||||
? {"id": tagId, "name": tagName}
|
||||
: {"name": tagName};
|
||||
}).toList();
|
||||
|
||||
final body = {
|
||||
if (id != null) "id": id,
|
||||
"name": name,
|
||||
"organization": organization,
|
||||
"contactCategoryId": categoryId,
|
||||
@ -112,27 +116,30 @@ class AddContactController extends GetxController {
|
||||
"description": description,
|
||||
};
|
||||
|
||||
logSafe("Submitting contact", sensitive: true);
|
||||
logSafe("${id != null ? 'Updating' : 'Creating'} contact");
|
||||
|
||||
final response = id != null
|
||||
? await ApiService.updateContact(id, body)
|
||||
: await ApiService.createContact(body);
|
||||
|
||||
final response = await ApiService.createContact(body);
|
||||
if (response == true) {
|
||||
logSafe("Contact creation succeeded");
|
||||
Get.back(result: true);
|
||||
showAppSnackbar(
|
||||
title: "Success",
|
||||
message: "Contact created successfully",
|
||||
message: id != null
|
||||
? "Contact updated successfully"
|
||||
: "Contact created successfully",
|
||||
type: SnackbarType.success,
|
||||
);
|
||||
} else {
|
||||
logSafe("Contact creation failed", level: LogLevel.error);
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Failed to create contact",
|
||||
message: "Failed to ${id != null ? 'update' : 'create'} contact",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logSafe("Contact creation error: \$e", level: LogLevel.error);
|
||||
logSafe("Submit contact error: $e", level: LogLevel.error);
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Something went wrong",
|
||||
@ -149,9 +156,12 @@ class AddContactController extends GetxController {
|
||||
|
||||
final lower = query.toLowerCase();
|
||||
filteredOrgSuggestions.assignAll(
|
||||
organizationNames.where((name) => name.toLowerCase().contains(lower)).toList(),
|
||||
organizationNames
|
||||
.where((name) => name.toLowerCase().contains(lower))
|
||||
.toList(),
|
||||
);
|
||||
logSafe("Filtered organization suggestions for: \$query", level: LogLevel.debug);
|
||||
logSafe("Filtered organization suggestions for: \$query",
|
||||
level: LogLevel.debug);
|
||||
}
|
||||
|
||||
Future<void> fetchGlobalProjects() async {
|
||||
@ -197,7 +207,10 @@ class AddContactController extends GetxController {
|
||||
|
||||
final lower = query.toLowerCase();
|
||||
filteredSuggestions.assignAll(
|
||||
tags.where((tag) => tag.toLowerCase().contains(lower) && !enteredTags.contains(tag)).toList(),
|
||||
tags
|
||||
.where((tag) =>
|
||||
tag.toLowerCase().contains(lower) && !enteredTags.contains(tag))
|
||||
.toList(),
|
||||
);
|
||||
logSafe("Filtered tag suggestions for: \$query", level: LogLevel.debug);
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ class ApiEndpoints {
|
||||
static const String getDirectoryContactTags = "/master/contact-tags";
|
||||
static const String getDirectoryOrganization = "/directory/organization";
|
||||
static const String createContact = "/directory";
|
||||
static const String updateContact = "/directory";
|
||||
static const String getDirectoryNotes = "/directory/notes";
|
||||
static const String updateDirectoryNotes = "/directory/note";
|
||||
}
|
||||
|
@ -365,6 +365,30 @@ class ApiService {
|
||||
return data is List ? data : null;
|
||||
}
|
||||
|
||||
static Future<bool> updateContact(
|
||||
String contactId, Map<String, dynamic> payload) async {
|
||||
try {
|
||||
final endpoint = "${ApiEndpoints.updateContact}/$contactId";
|
||||
|
||||
logSafe("Updating contact [$contactId] with payload: $payload");
|
||||
|
||||
final response = await _putRequest(endpoint, payload);
|
||||
if (response != null) {
|
||||
final json = jsonDecode(response.body);
|
||||
if (json['success'] == true) {
|
||||
logSafe("Contact updated successfully.");
|
||||
return true;
|
||||
} else {
|
||||
logSafe("Update contact failed: ${json['message']}",
|
||||
level: LogLevel.warning);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logSafe("Error updating contact: $e", level: LogLevel.error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static Future<bool> createContact(Map<String, dynamic> payload) async {
|
||||
try {
|
||||
logSafe("Submitting contact payload: $payload");
|
||||
|
@ -1,26 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:marco/controller/directory/add_contact_controller.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:marco/model/directory/contact_model.dart';
|
||||
|
||||
class AddContactBottomSheet extends StatelessWidget {
|
||||
AddContactBottomSheet({super.key}) {
|
||||
final ContactModel? existingContact;
|
||||
|
||||
AddContactBottomSheet({super.key, this.existingContact}) {
|
||||
controller.resetForm();
|
||||
|
||||
nameController.clear();
|
||||
orgController.clear();
|
||||
nameController.text = existingContact?.name ?? '';
|
||||
orgController.text = existingContact?.organization ?? '';
|
||||
tagTextController.clear();
|
||||
addressController.clear();
|
||||
descriptionController.clear();
|
||||
addressController.text = existingContact?.address ?? '';
|
||||
descriptionController.text = existingContact?.description ?? '';
|
||||
|
||||
emailControllers.add(TextEditingController());
|
||||
emailLabels.add('Office'.obs);
|
||||
if (existingContact != null) {
|
||||
emailControllers.clear();
|
||||
emailLabels.clear();
|
||||
for (var email in existingContact!.contactEmails) {
|
||||
emailControllers.add(TextEditingController(text: email.emailAddress));
|
||||
emailLabels.add((email.label ?? 'Office').obs);
|
||||
}
|
||||
|
||||
phoneControllers.add(TextEditingController());
|
||||
phoneLabels.add('Work'.obs);
|
||||
if (emailControllers.isEmpty) {
|
||||
emailControllers.add(TextEditingController());
|
||||
emailLabels.add('Office'.obs);
|
||||
}
|
||||
|
||||
phoneControllers.clear();
|
||||
phoneLabels.clear();
|
||||
for (var phone in existingContact!.contactPhones) {
|
||||
phoneControllers.add(TextEditingController(text: phone.phoneNumber));
|
||||
phoneLabels.add((phone.label ?? 'Work').obs);
|
||||
}
|
||||
|
||||
if (phoneControllers.isEmpty) {
|
||||
phoneControllers.add(TextEditingController());
|
||||
phoneLabels.add('Work'.obs);
|
||||
}
|
||||
|
||||
controller.selectedCategory.value =
|
||||
existingContact!.contactCategory?.name ?? '';
|
||||
|
||||
if (existingContact!.projectIds?.isNotEmpty == true) {
|
||||
controller.selectedProject.value = controller.globalProjects
|
||||
.firstWhereOrNull(
|
||||
(e) => e == existingContact!.projectIds!.first,
|
||||
)
|
||||
?.toString() ??
|
||||
'';
|
||||
}
|
||||
|
||||
if (existingContact!.bucketIds.isNotEmpty) {
|
||||
controller.selectedBucket.value = controller.buckets
|
||||
.firstWhereOrNull(
|
||||
(b) => b == existingContact!.bucketIds.first,
|
||||
)
|
||||
?.toString() ??
|
||||
'';
|
||||
}
|
||||
|
||||
controller.enteredTags.assignAll(
|
||||
existingContact!.tags.map((tag) => tag.name).toList(),
|
||||
);
|
||||
} else {
|
||||
emailControllers.add(TextEditingController());
|
||||
emailLabels.add('Office'.obs);
|
||||
|
||||
phoneControllers.add(TextEditingController());
|
||||
phoneLabels.add('Work'.obs);
|
||||
}
|
||||
}
|
||||
|
||||
final controller = Get.put(AddContactController());
|
||||
@ -113,10 +167,9 @@ class AddContactBottomSheet extends StatelessWidget {
|
||||
MyText.labelMedium(label),
|
||||
MySpacing.height(8),
|
||||
_popupSelector(
|
||||
hint: "Label",
|
||||
selectedValue: selectedLabel,
|
||||
options: options,
|
||||
),
|
||||
hint: "Label",
|
||||
selectedValue: selectedLabel,
|
||||
options: options),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -128,40 +181,35 @@ class AddContactBottomSheet extends StatelessWidget {
|
||||
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: "", // hides length indicator
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return "$inputLabel is required";
|
||||
}
|
||||
|
||||
final trimmed = value.trim();
|
||||
|
||||
if (inputType == TextInputType.phone) {
|
||||
if (!RegExp(r'^\d{10}$').hasMatch(trimmed)) {
|
||||
return "Enter a valid 10-digit phone number";
|
||||
}
|
||||
if (RegExp(r'^0+$').hasMatch(trimmed)) {
|
||||
return "Phone number cannot be all zeroes";
|
||||
}
|
||||
}
|
||||
|
||||
if (inputType == TextInputType.emailAddress &&
|
||||
!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(trimmed)) {
|
||||
return "Enter a valid email address";
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
),
|
||||
|
||||
controller: controller,
|
||||
keyboardType: inputType,
|
||||
maxLength: inputType == TextInputType.phone ? 10 : null,
|
||||
inputFormatters: inputType == TextInputType.phone
|
||||
? [FilteringTextInputFormatter.digitsOnly]
|
||||
: [],
|
||||
decoration: _inputDecoration("Enter $inputLabel")
|
||||
.copyWith(counterText: ""),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return "$inputLabel is required";
|
||||
}
|
||||
final trimmed = value.trim();
|
||||
if (inputType == TextInputType.phone) {
|
||||
if (!RegExp(r'^\d{10}$').hasMatch(trimmed)) {
|
||||
return "Enter a valid 10-digit phone number";
|
||||
}
|
||||
if (RegExp(r'^0+$').hasMatch(trimmed)) {
|
||||
return "Phone number cannot be all zeroes";
|
||||
}
|
||||
}
|
||||
if (inputType == TextInputType.emailAddress &&
|
||||
!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
|
||||
.hasMatch(trimmed)) {
|
||||
return "Enter a valid email address";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -177,53 +225,49 @@ class AddContactBottomSheet extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmailList() {
|
||||
return Column(
|
||||
children: List.generate(emailControllers.length, (index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: _buildLabeledRow(
|
||||
"Email Label",
|
||||
emailLabels[index],
|
||||
["Office", "Personal", "Other"],
|
||||
"Email",
|
||||
emailControllers[index],
|
||||
TextInputType.emailAddress,
|
||||
onRemove: emailControllers.length > 1
|
||||
? () {
|
||||
emailControllers.removeAt(index);
|
||||
emailLabels.removeAt(index);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
Widget _buildEmailList() => Column(
|
||||
children: List.generate(emailControllers.length, (index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: _buildLabeledRow(
|
||||
"Email Label",
|
||||
emailLabels[index],
|
||||
["Office", "Personal", "Other"],
|
||||
"Email",
|
||||
emailControllers[index],
|
||||
TextInputType.emailAddress,
|
||||
onRemove: emailControllers.length > 1
|
||||
? () {
|
||||
emailControllers.removeAt(index);
|
||||
emailLabels.removeAt(index);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
Widget _buildPhoneList() {
|
||||
return Column(
|
||||
children: List.generate(phoneControllers.length, (index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: _buildLabeledRow(
|
||||
"Phone Label",
|
||||
phoneLabels[index],
|
||||
["Work", "Mobile", "Other"],
|
||||
"Phone",
|
||||
phoneControllers[index],
|
||||
TextInputType.phone,
|
||||
onRemove: phoneControllers.length > 1
|
||||
? () {
|
||||
phoneControllers.removeAt(index);
|
||||
phoneLabels.removeAt(index);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
Widget _buildPhoneList() => Column(
|
||||
children: List.generate(phoneControllers.length, (index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: _buildLabeledRow(
|
||||
"Phone Label",
|
||||
phoneLabels[index],
|
||||
["Work", "Mobile", "Other"],
|
||||
"Phone",
|
||||
phoneControllers[index],
|
||||
TextInputType.phone,
|
||||
onRemove: phoneControllers.length > 1
|
||||
? () {
|
||||
phoneControllers.removeAt(index);
|
||||
phoneLabels.removeAt(index);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
Widget _dropdownField({
|
||||
required String label,
|
||||
@ -350,8 +394,13 @@ class AddContactBottomSheet extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: MyText.titleMedium("Create New Contact",
|
||||
fontWeight: 700)),
|
||||
child: MyText.titleMedium(
|
||||
existingContact != null
|
||||
? "Edit Contact"
|
||||
: "Create New Contact",
|
||||
fontWeight: 700,
|
||||
),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
_sectionLabel("Basic Info"),
|
||||
MySpacing.height(16),
|
||||
@ -519,8 +568,9 @@ class AddContactBottomSheet extends StatelessWidget {
|
||||
"phoneNumber": entry.value.text.trim(),
|
||||
})
|
||||
.toList();
|
||||
|
||||
print("Submitting contact payload , id: ${existingContact?.id}");
|
||||
controller.submitContact(
|
||||
id: existingContact?.id,
|
||||
name: nameController.text.trim(),
|
||||
organization: orgController.text.trim(),
|
||||
emails: emails,
|
||||
|
@ -14,6 +14,7 @@ import 'package:marco/helpers/widgets/Directory/comment_editor_card.dart';
|
||||
import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart';
|
||||
import 'package:marco/model/directory/add_comment_bottom_sheet.dart';
|
||||
import 'package:marco/helpers/utils/date_time_utils.dart';
|
||||
import 'package:marco/model/directory/add_contact_bottom_sheet.dart';
|
||||
|
||||
class ContactDetailScreen extends StatefulWidget {
|
||||
final ContactModel contact;
|
||||
@ -252,47 +253,72 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
|
||||
final category = widget.contact.contactCategory?.name ?? "-";
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: MySpacing.xy(8, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_infoCard("Basic Info", [
|
||||
_iconInfoRow(Icons.email, "Email", email,
|
||||
onTap: () => LauncherUtils.launchEmail(email),
|
||||
onLongPress: () =>
|
||||
LauncherUtils.copyToClipboard(email, typeLabel: "Email")),
|
||||
_iconInfoRow(Icons.phone, "Phone", phone,
|
||||
onTap: () => LauncherUtils.launchPhone(phone),
|
||||
onLongPress: () =>
|
||||
LauncherUtils.copyToClipboard(phone, typeLabel: "Phone")),
|
||||
_iconInfoRow(Icons.location_on, "Address", widget.contact.address),
|
||||
]),
|
||||
_infoCard("Organization", [
|
||||
_iconInfoRow(
|
||||
Icons.business, "Organization", widget.contact.organization),
|
||||
_iconInfoRow(Icons.category, "Category", category),
|
||||
]),
|
||||
_infoCard("Meta Info", [
|
||||
_iconInfoRow(Icons.label, "Tags", tags.isNotEmpty ? tags : "-"),
|
||||
_iconInfoRow(Icons.folder_shared, "Contact Buckets",
|
||||
bucketNames.isNotEmpty ? bucketNames : "-"),
|
||||
_iconInfoRow(Icons.work_outline, "Projects", projectNames),
|
||||
]),
|
||||
_infoCard("Description", [
|
||||
MySpacing.height(6),
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: MyText.bodyMedium(
|
||||
widget.contact.description,
|
||||
color: Colors.grey[800],
|
||||
maxLines: 10,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
return Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding: MySpacing.fromLTRB(8, 8, 8, 80),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(12),
|
||||
_infoCard("Basic Info", [
|
||||
_iconInfoRow(Icons.email, "Email", email,
|
||||
onTap: () => LauncherUtils.launchEmail(email),
|
||||
onLongPress: () => LauncherUtils.copyToClipboard(email,
|
||||
typeLabel: "Email")),
|
||||
_iconInfoRow(Icons.phone, "Phone", phone,
|
||||
onTap: () => LauncherUtils.launchPhone(phone),
|
||||
onLongPress: () => LauncherUtils.copyToClipboard(phone,
|
||||
typeLabel: "Phone")),
|
||||
_iconInfoRow(
|
||||
Icons.location_on, "Address", widget.contact.address),
|
||||
]),
|
||||
_infoCard("Organization", [
|
||||
_iconInfoRow(Icons.business, "Organization",
|
||||
widget.contact.organization),
|
||||
_iconInfoRow(Icons.category, "Category", category),
|
||||
]),
|
||||
_infoCard("Meta Info", [
|
||||
_iconInfoRow(Icons.label, "Tags", tags.isNotEmpty ? tags : "-"),
|
||||
_iconInfoRow(Icons.folder_shared, "Contact Buckets",
|
||||
bucketNames.isNotEmpty ? bucketNames : "-"),
|
||||
_iconInfoRow(Icons.work_outline, "Projects", projectNames),
|
||||
]),
|
||||
_infoCard("Description", [
|
||||
MySpacing.height(6),
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: MyText.bodyMedium(
|
||||
widget.contact.description,
|
||||
color: Colors.grey[800],
|
||||
maxLines: 10,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
])
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
child: FloatingActionButton.extended(
|
||||
backgroundColor: Colors.red,
|
||||
onPressed: () {
|
||||
Get.bottomSheet(
|
||||
AddContactBottomSheet(existingContact: widget.contact),
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.edit, color: Colors.white),
|
||||
label: const Text(
|
||||
"Edit Contact",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
])
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -408,7 +434,6 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(12),
|
||||
// Comment Content
|
||||
if (isEditing && quillController != null)
|
||||
CommentEditorCard(
|
||||
@ -453,7 +478,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
child: FloatingActionButton.extended(
|
||||
backgroundColor: Colors.indigo,
|
||||
backgroundColor: Colors.red,
|
||||
onPressed: () {
|
||||
Get.bottomSheet(
|
||||
AddCommentBottomSheet(contactId: contactId),
|
||||
|
Loading…
x
Reference in New Issue
Block a user