import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_html/flutter_html.dart' as html; import 'package:flutter_quill/flutter_quill.dart' as quill; import 'package:marco/controller/project_controller.dart'; import 'package:marco/controller/directory/directory_controller.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/model/directory/contact_model.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/utils/launcher_utils.dart'; import 'package:tab_indicator_styler/tab_indicator_styler.dart'; 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; const ContactDetailScreen({super.key, required this.contact}); @override State createState() => _ContactDetailScreenState(); } String _convertDeltaToHtml(dynamic delta) { final buffer = StringBuffer(); bool inList = false; for (var op in delta.toList()) { final data = op.data?.toString() ?? ''; final attr = op.attributes ?? {}; final isListItem = attr.containsKey('list'); // Start list if (isListItem && !inList) { buffer.write(''); inList = false; } if (isListItem) buffer.write('
  • '); // Apply inline styles if (attr.containsKey('bold')) buffer.write(''); if (attr.containsKey('italic')) buffer.write(''); if (attr.containsKey('underline')) buffer.write(''); if (attr.containsKey('strike')) buffer.write(''); if (attr.containsKey('link')) buffer.write(''); buffer.write(data.replaceAll('\n', '')); if (attr.containsKey('link')) buffer.write(''); if (attr.containsKey('strike')) buffer.write(''); if (attr.containsKey('underline')) buffer.write(''); if (attr.containsKey('italic')) buffer.write(''); if (attr.containsKey('bold')) buffer.write(''); if (isListItem) buffer.write('
  • '); else if (data.contains('\n')) buffer.write('
    '); } if (inList) buffer.write(''); return buffer.toString(); } class _ContactDetailScreenState extends State { late final DirectoryController directoryController; late final ProjectController projectController; late ContactModel contact; @override void initState() { super.initState(); directoryController = Get.find(); projectController = Get.find(); contact = widget.contact; WidgetsBinding.instance.addPostFrameCallback((_) { directoryController.fetchCommentsForContact(contact.id); }); } @override Widget build(BuildContext context) { return DefaultTabController( length: 2, child: Scaffold( backgroundColor: const Color(0xFFF5F5F5), appBar: _buildMainAppBar(), body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSubHeader(), Expanded( child: TabBarView( children: [ _buildDetailsTab(), _buildCommentsTab(context), ], ), ), ], ), ), ), ); } PreferredSizeWidget _buildMainAppBar() { return AppBar( backgroundColor: const Color(0xFFF5F5F5), elevation: 0.2, automaticallyImplyLeading: false, titleSpacing: 0, title: Padding( padding: MySpacing.xy(16, 0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20), onPressed: () => Get.offAllNamed('/dashboard/directory-main-page'), ), MySpacing.width(8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ MyText.titleLarge('Contact Profile', fontWeight: 700, color: Colors.black), MySpacing.height(2), GetBuilder( builder: (projectController) { final projectName = projectController.selectedProject?.name ?? 'Select Project'; return Row( children: [ const Icon(Icons.work_outline, size: 14, color: Colors.grey), MySpacing.width(4), Expanded( child: MyText.bodySmall( projectName, fontWeight: 600, overflow: TextOverflow.ellipsis, color: Colors.grey[700], ), ), ], ); }, ), ], ), ), ], ), ), ); } Widget _buildSubHeader() { return Padding( padding: MySpacing.xy(16, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Avatar( firstName: contact.name.split(" ").first, lastName: contact.name.split(" ").length > 1 ? contact.name.split(" ").last : "", size: 35, backgroundColor: Colors.indigo, ), MySpacing.width(12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.titleSmall(contact.name, fontWeight: 600, color: Colors.black), MySpacing.height(2), MyText.bodySmall(contact.organization, fontWeight: 500, color: Colors.grey[700]), ], ), ], ), TabBar( labelColor: Colors.red, unselectedLabelColor: Colors.black, indicator: MaterialIndicator( color: Colors.red, height: 4, topLeftRadius: 8, topRightRadius: 8, bottomLeftRadius: 8, bottomRightRadius: 8, ), tabs: const [ Tab(text: "Details"), Tab(text: "Comments"), ], ), ], ), ); } Widget _buildDetailsTab() { final email = contact.contactEmails.isNotEmpty ? contact.contactEmails.first.emailAddress : "-"; final phone = contact.contactPhones.isNotEmpty ? contact.contactPhones.first.phoneNumber : "-"; final tags = contact.tags.map((e) => e.name).join(", "); final bucketNames = contact.bucketIds .map((id) => directoryController.contactBuckets .firstWhereOrNull((b) => b.id == id) ?.name) .whereType() .join(", "); final projectNames = contact.projectIds ?.map((id) => projectController.projects .firstWhereOrNull((p) => p.id == id) ?.name) .whereType() .join(", ") ?? "-"; final category = contact.contactCategory?.name ?? "-"; 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", contact.address), ]), _infoCard("Organization", [ _iconInfoRow( Icons.business, "Organization", 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( contact.description, color: Colors.grey[800], maxLines: 10, textAlign: TextAlign.left, ), ), ]) ], ), ), Positioned( bottom: 20, right: 20, child: FloatingActionButton.extended( backgroundColor: Colors.red, onPressed: () async { final result = await Get.bottomSheet( AddContactBottomSheet(existingContact: contact), isScrollControlled: true, backgroundColor: Colors.transparent, ); if (result == true) { await directoryController.fetchContacts(); final updated = directoryController.allContacts.firstWhereOrNull( (c) => c.id == contact.id, ); if (updated != null) { setState(() { contact = updated; }); } } }, icon: const Icon(Icons.edit, color: Colors.white), label: const Text( "Edit Contact", style: TextStyle(color: Colors.white), ), ), ), ], ); } Widget _buildCommentsTab(BuildContext context) { return Obx(() { final contactId = contact.id; if (!directoryController.contactCommentsMap.containsKey(contactId)) { return const Center(child: CircularProgressIndicator()); } final comments = directoryController .getCommentsForContact(contactId) .reversed .toList(); final editingId = directoryController.editingCommentId.value; return Stack( children: [ comments.isEmpty ? Center( child: MyText.bodyLarge("No comments yet.", color: Colors.grey), ) : Padding( padding: MySpacing.xy(12, 12), child: ListView.separated( padding: const EdgeInsets.only(bottom: 100), itemCount: comments.length, separatorBuilder: (_, __) => MySpacing.height(14), itemBuilder: (_, index) { final comment = comments[index]; final isEditing = editingId == comment.id; final initials = comment.createdBy.firstName.isNotEmpty ? comment.createdBy.firstName[0].toUpperCase() : "?"; final decodedDelta = HtmlToDelta().convert(comment.note); final quillController = isEditing ? quill.QuillController( document: quill.Document.fromDelta(decodedDelta), selection: TextSelection.collapsed( offset: decodedDelta.length), ) : null; return AnimatedContainer( duration: const Duration(milliseconds: 300), padding: MySpacing.xy(8, 7), decoration: BoxDecoration( color: isEditing ? Colors.indigo[50] : Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all( color: isEditing ? Colors.indigo : Colors.grey.shade300, width: 1.2, ), boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 4, offset: Offset(0, 2), ) ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Row Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Avatar( firstName: initials, lastName: '', size: 36), MySpacing.width(12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodyMedium( "By: ${comment.createdBy.firstName}", fontWeight: 600, color: Colors.indigo[800], ), MySpacing.height(4), MyText.bodySmall( DateTimeUtils.convertUtcToLocal( comment.createdAt.toString(), format: 'dd MMM yyyy, hh:mm a', ), color: Colors.grey[600], ), ], ), ), IconButton( icon: Icon( isEditing ? Icons.close : Icons.edit, size: 20, color: Colors.indigo, ), onPressed: () { directoryController.editingCommentId.value = isEditing ? null : comment.id; }, ), ], ), // Comment Content if (isEditing && quillController != null) CommentEditorCard( controller: quillController, onCancel: () { directoryController.editingCommentId.value = null; }, onSave: (controller) async { final delta = controller.document.toDelta(); final htmlOutput = _convertDeltaToHtml(delta); final updated = comment.copyWith(note: htmlOutput); await directoryController .updateComment(updated); // ✅ Re-fetch comments to get updated list await directoryController .fetchCommentsForContact(contactId); // ✅ Exit editing mode directoryController.editingCommentId.value = null; }, ) else html.Html( data: comment.note, style: { "body": html.Style( margin: html.Margins.zero, padding: html.HtmlPaddings.zero, fontSize: html.FontSize.medium, color: Colors.black87, ), }, ), ], ), ); }, ), ), // Floating Action Button if (directoryController.editingCommentId.value == null) Positioned( bottom: 20, right: 20, child: FloatingActionButton.extended( backgroundColor: Colors.red, onPressed: () async { final result = await Get.bottomSheet( AddCommentBottomSheet(contactId: contactId), isScrollControlled: true, ); if (result == true) { await directoryController .fetchCommentsForContact(contactId); } }, icon: const Icon(Icons.add_comment, color: Colors.white), label: const Text( "Add Comment", style: TextStyle(color: Colors.white), ), ), ), ], ); }); } Widget _iconInfoRow(IconData icon, String label, String value, {VoidCallback? onTap, VoidCallback? onLongPress}) { return Padding( padding: MySpacing.y(8), child: GestureDetector( onTap: onTap, onLongPress: onLongPress, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, size: 22, color: Colors.indigo), MySpacing.width(12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodySmall(label, fontWeight: 600, color: Colors.black87), MySpacing.height(2), MyText.bodyMedium(value, color: Colors.grey[800]), ], ), ), ], ), ), ); } Widget _infoCard(String title, List children) { return Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), elevation: 2, margin: MySpacing.bottom(12), child: Padding( padding: MySpacing.xy(16, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.titleSmall(title, fontWeight: 700, color: Colors.indigo[700]), MySpacing.height(8), ...children, ], ), ), ); } }