import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.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'; 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; @override void initState() { super.initState(); directoryController = Get.find(); projectController = Get.find(); WidgetsBinding.instance.addPostFrameCallback((_) { if (!directoryController.contactCommentsMap .containsKey(widget.contact.id)) { directoryController.fetchCommentsForContact(widget.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.back(), ), 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: widget.contact.name.split(" ").first, lastName: widget.contact.name.split(" ").length > 1 ? widget.contact.name.split(" ").last : "", size: 35, backgroundColor: Colors.indigo, ), MySpacing.width(12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.titleSmall(widget.contact.name, fontWeight: 600, color: Colors.black), MySpacing.height(2), MyText.bodySmall(widget.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 = widget.contact.contactEmails.isNotEmpty ? widget.contact.contactEmails.first.emailAddress : "-"; final phone = widget.contact.contactPhones.isNotEmpty ? widget.contact.contactPhones.first.phoneNumber : "-"; final createdDate = DateTime.now(); final formattedDate = DateFormat('MMMM dd, yyyy').format(createdDate); final tags = widget.contact.tags.map((e) => e.name).join(", "); final bucketNames = widget.contact.bucketIds .map((id) => directoryController.contactBuckets .firstWhereOrNull((b) => b.id == id) ?.name) .whereType() .join(", "); final projectNames = widget.contact.projectIds ?.map((id) => projectController.projects .firstWhereOrNull((p) => p.id == id) ?.name) .whereType() .join(", ") ?? "-"; 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.calendar_today, "Created", formattedDate), _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, ), ), ]) ], ), ); } Widget _buildCommentsTab(BuildContext context) { return Obx(() { final contactId = widget.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(8, 8), // Same padding as Details tab child: ListView.separated( padding: EdgeInsets.only( bottom: 80, // Extra bottom padding to avoid FAB overlap ), itemCount: comments.length, separatorBuilder: (_, __) => MySpacing.height(12), 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 Container( padding: MySpacing.xy(14, 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 4, offset: Offset(0, 2), ) ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Avatar( firstName: initials, lastName: '', size: 31), MySpacing.width(8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodySmall( "By: ${comment.createdBy.firstName}", fontWeight: 600, color: Colors.indigo[700]), MySpacing.height(2), MyText.bodySmall( DateFormat('dd MMM yyyy, hh:mm a') .format(comment.createdAt), fontWeight: 500, color: Colors.grey[600], ), ], ), ), IconButton( icon: Icon( isEditing ? Icons.close : Icons.edit, size: 20, color: Colors.grey[700]), onPressed: () { directoryController.editingCommentId.value = isEditing ? null : comment.id; }, ), ], ), MySpacing.height(10), 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); directoryController.editingCommentId.value = null; }) else html.Html( data: comment.note, style: { "body": html.Style( margin: html.Margins.all(0), padding: html.HtmlPaddings.all(0), fontSize: html.FontSize.medium, color: Colors.black87, ), }, ), ], ), ); }, ), ), // Floating Action Button to Add Comment if (directoryController.editingCommentId.value == null) Positioned( bottom: 16, right: 16, child: FloatingActionButton.extended( backgroundColor: Colors.red, onPressed: () { Get.bottomSheet( AddCommentBottomSheet(contactId: contactId), isScrollControlled: true, ); }, 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, ], ), ), ); } }