diff --git a/lib/model/directory/add_comment_bottom_sheet.dart b/lib/model/directory/add_comment_bottom_sheet.dart index bcae2a9..25b5540 100644 --- a/lib/model/directory/add_comment_bottom_sheet.dart +++ b/lib/model/directory/add_comment_bottom_sheet.dart @@ -1,10 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:flutter_quill/flutter_quill.dart' as quill; import 'package:marco/controller/directory/add_comment_controller.dart'; -import 'package:marco/helpers/widgets/my_spacing.dart'; -import 'package:marco/helpers/widgets/my_text.dart'; -import 'package:marco/helpers/widgets/Directory/comment_editor_card.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; class AddCommentBottomSheet extends StatefulWidget { final String contactId; @@ -17,120 +14,59 @@ class AddCommentBottomSheet extends StatefulWidget { class _AddCommentBottomSheetState extends State { late final AddCommentController controller; - late final quill.QuillController quillController; + final TextEditingController textController = TextEditingController(); + bool isSubmitting = false; @override void initState() { super.initState(); controller = Get.put(AddCommentController(contactId: widget.contactId)); - quillController = quill.QuillController.basic(); } @override void dispose() { - quillController.dispose(); - Get.delete(); + textController.dispose(); super.dispose(); } + Future handleSubmit() async { + final noteText = textController.text.trim(); + if (noteText.isEmpty) return; + + setState(() { + isSubmitting = true; + }); + + controller.updateNote(noteText); + await controller.submitComment(); + + if (mounted) { + setState(() { + isSubmitting = false; + }); + Get.back(result: true); + } + } + @override Widget build(BuildContext context) { - return SingleChildScrollView( - padding: MediaQuery.of(context).viewInsets, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).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: Column( - mainAxisSize: MainAxisSize.min, - 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("Add Note", fontWeight: 700)), - MySpacing.height(24), - CommentEditorCard( - controller: quillController, - onCancel: () => Get.back(), - onSave: (editorController) async { - final delta = editorController.document.toDelta(); - final htmlOutput = _convertDeltaToHtml(delta); - controller.updateNote(htmlOutput); - await controller.submitComment(); - }, - ), - ], + return BaseBottomSheet( + title: "Add Note", + onCancel: () => Get.back(), + onSubmit: handleSubmit, + isSubmitting: isSubmitting, + child: TextField( + controller: textController, + maxLines: null, + minLines: 5, + decoration: InputDecoration( + hintText: "Enter your note here...", + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), ), + contentPadding: const EdgeInsets.all(12), ), ), ); } - - 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'); - final trimmedData = data.trim(); - - if (isListItem && !inList) { - buffer.write('
    '); - inList = true; - } - - if (!isListItem && inList) { - buffer.write('
'); - inList = false; - } - - if (isListItem && trimmedData.isEmpty) continue; - - if (isListItem) buffer.write('
  • '); - - 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(trimmedData.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(); - } } diff --git a/lib/view/directory/contact_detail_screen.dart b/lib/view/directory/contact_detail_screen.dart index 28eaddd..badb0bf 100644 --- a/lib/view/directory/contact_detail_screen.dart +++ b/lib/view/directory/contact_detail_screen.dart @@ -1,7 +1,5 @@ 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'; @@ -9,58 +7,12 @@ 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'; +import 'package:marco/helpers/utils/date_time_utils.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; -import 'package:marco/model/directory/directory_comment_model.dart'; - -// HELPER: Delta to HTML conversion -String _convertDeltaToHtml(dynamic delta) { - final buffer = StringBuffer(); - bool inList = false; - - for (var op in delta.toList()) { - final String data = op.data?.toString() ?? ''; - final attr = op.attributes ?? {}; - final bool isListItem = attr.containsKey('list'); - - if (isListItem && !inList) { - buffer.write('
      '); - inList = true; - } - if (!isListItem && inList) { - buffer.write('
    '); - inList = false; - } - - if (isListItem) buffer.write('
  • '); - - 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(); -} +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; class ContactDetailScreen extends StatefulWidget { final ContactModel contact; @@ -70,10 +22,10 @@ class ContactDetailScreen extends StatefulWidget { State createState() => _ContactDetailScreenState(); } -class _ContactDetailScreenState extends State { +class _ContactDetailScreenState extends State + with UIMixin { late final DirectoryController directoryController; late final ProjectController projectController; - late Rx contactRx; @override @@ -90,7 +42,6 @@ class _ContactDetailScreenState extends State { active: false); }); - // Listen to controller's allContacts and update contact if changed ever(directoryController.allContacts, (_) { final updated = directoryController.allContacts .firstWhereOrNull((c) => c.id == contactRx.value.id); @@ -173,11 +124,7 @@ class _ContactDetailScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ - Avatar( - firstName: firstName, - lastName: lastName, - size: 35, - ), + Avatar(firstName: firstName, lastName: lastName, size: 35), MySpacing.width(12), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -191,16 +138,9 @@ class _ContactDetailScreenState extends State { ), ]), TabBar( - labelColor: Colors.red, - unselectedLabelColor: Colors.black, - indicator: MaterialIndicator( - color: Colors.red, - height: 4, - topLeftRadius: 8, - topRightRadius: 8, - bottomLeftRadius: 8, - bottomRightRadius: 8, - ), + labelColor: Colors.black, + unselectedLabelColor: Colors.grey, + indicatorColor: contentTheme.primary, tabs: const [ Tab(text: "Details"), Tab(text: "Notes"), @@ -288,8 +228,7 @@ class _ContactDetailScreenState extends State { _iconInfoRow(Icons.location_on, "Address", contact.address), ]), _infoCard("Organization", [ - _iconInfoRow( - Icons.business, "Organization", contact.organization), + _iconInfoRow(Icons.business, "Organization", contact.organization), _iconInfoRow(Icons.category, "Category", category), ]), _infoCard("Meta Info", [ @@ -317,7 +256,7 @@ class _ContactDetailScreenState extends State { bottom: 20, right: 20, child: FloatingActionButton.extended( - backgroundColor: Colors.red, + backgroundColor: contentTheme.primary, onPressed: () async { final result = await Get.bottomSheet( AddContactBottomSheet(existingContact: contact), @@ -345,25 +284,37 @@ class _ContactDetailScreenState extends State { Widget _buildCommentsTab() { return Obx(() { final contactId = contactRx.value.id; - final comments = directoryController.combinedComments(contactId); + + final activeComments = directoryController + .getCommentsForContact(contactId) + .where((c) => c.isActive) + .toList(); + final inactiveComments = directoryController + .getCommentsForContact(contactId) + .where((c) => !c.isActive) + .toList(); + + final comments = + [...activeComments, ...inactiveComments].reversed.toList(); final editingId = directoryController.editingCommentId.value; return Stack( children: [ - comments.isEmpty - ? Center( - child: MyText.bodyLarge("No notes yet.", color: Colors.grey), - ) - : MyRefreshIndicator( - onRefresh: () async { - await directoryController.fetchCommentsForContact(contactId, - active: true); - await directoryController.fetchCommentsForContact(contactId, - active: false); - }, - child: Padding( - padding: MySpacing.xy(12, 12), - child: ListView.separated( + MyRefreshIndicator( + onRefresh: () async { + await directoryController.fetchCommentsForContact(contactId, + active: true); + await directoryController.fetchCommentsForContact(contactId, + active: false); + }, + child: Padding( + padding: MySpacing.xy(12, 12), + child: comments.isEmpty + ? Center( + child: + MyText.bodyLarge("No notes yet.", color: Colors.grey), + ) + : ListView.separated( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.only(bottom: 100), itemCount: comments.length, @@ -371,19 +322,21 @@ class _ContactDetailScreenState extends State { itemBuilder: (_, index) => _buildCommentItem( comments[index], editingId, contactId), ), - ), - ), + ), + ), if (editingId == null) Positioned( bottom: 20, right: 20, child: FloatingActionButton.extended( - backgroundColor: Colors.red, + backgroundColor: contentTheme.primary, onPressed: () async { final result = await Get.bottomSheet( AddCommentBottomSheet(contactId: contactId), isScrollControlled: true, + enableDrag: true, ); + if (result == true) { await directoryController.fetchCommentsForContact(contactId, active: true); @@ -401,205 +354,214 @@ class _ContactDetailScreenState extends State { }); } - Widget _buildCommentItem( - DirectoryComment comment, String? editingId, String contactId) { + Widget _buildCommentItem(comment, editingId, contactId) { 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; - - final isInactive = !comment.isActive; + final textController = TextEditingController(text: comment.note); return Container( - margin: const EdgeInsets.symmetric(vertical: 6), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: Colors.grey.shade200), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.03), - blurRadius: 6, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 🧑 Header - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Avatar( - firstName: initials, - lastName: '', - size: 40, - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + margin: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header: Avatar + Name + Role + Timestamp + Actions + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Avatar(firstName: initials, lastName: '', size: 40), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${comment.createdBy.firstName} ${comment.createdBy.lastName}", + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 15, + color: Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + if (comment.createdBy.jobRoleName?.isNotEmpty ?? false) Text( - "${comment.createdBy.firstName} ${comment.createdBy.lastName}", + comment.createdBy.jobRoleName, style: TextStyle( - fontWeight: FontWeight.w700, - fontSize: 15, - color: isInactive ? Colors.grey : Colors.black87, - fontStyle: - isInactive ? FontStyle.italic : FontStyle.normal, - ), - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 2), - if (comment.createdBy.jobRoleName.isNotEmpty) - Text( - comment.createdBy.jobRoleName, - style: TextStyle( - fontSize: 13, - color: - isInactive ? Colors.grey : Colors.indigo[600], - fontWeight: FontWeight.w500, - fontStyle: isInactive - ? FontStyle.italic - : FontStyle.normal, - ), - ), - const SizedBox(height: 2), - Text( - DateTimeUtils.convertUtcToLocal( - comment.createdAt.toString(), - format: 'dd MMM yyyy, hh:mm a', - ), - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - fontStyle: - isInactive ? FontStyle.italic : FontStyle.normal, + fontSize: 13, + color: Colors.indigo[600], + fontWeight: FontWeight.w500, ), ), - ], - ), + const SizedBox(height: 2), + Text( + DateTimeUtils.convertUtcToLocal( + comment.createdAt.toString(), + format: 'dd MMM yyyy, hh:mm a', + ), + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], ), - - // ⚡ Action buttons - if (!isInactive) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.edit_outlined, - size: 18, color: Colors.indigo), - tooltip: "Edit", - splashRadius: 18, - onPressed: () { - directoryController.editingCommentId.value = - isEditing ? null : comment.id; - }, - ), - IconButton( - icon: const Icon(Icons.delete_outline, - size: 18, color: Colors.red), - tooltip: "Delete", - splashRadius: 18, - onPressed: () async { - await Get.dialog( - ConfirmDialog( - title: "Delete Note", - message: - "Are you sure you want to delete this note?", - confirmText: "Delete", - confirmColor: Colors.red, - icon: Icons.delete_forever, - onConfirm: () async { - await directoryController.deleteComment( - comment.id, contactId); - }, - ), - ); - }, - ), - ], - ) - else - IconButton( - icon: const Icon(Icons.restore, - size: 18, color: Colors.green), - tooltip: "Restore", - splashRadius: 18, - onPressed: () async { - await Get.dialog( - ConfirmDialog( - title: "Restore Note", - message: - "Are you sure you want to restore this note?", - confirmText: "Restore", - confirmColor: Colors.green, - icon: Icons.restore, - onConfirm: () async { - await directoryController.restoreComment( - comment.id, contactId); - }, - ), - ); - }, - ), - ], - ), - - const SizedBox(height: 8), - - // 📝 Comment Content - if (isEditing && quillController != null) - CommentEditorCard( - controller: quillController, - onCancel: () => - directoryController.editingCommentId.value = null, - onSave: (ctrl) async { - final delta = ctrl.document.toDelta(); - final htmlOutput = _convertDeltaToHtml(delta); - final updated = comment.copyWith(note: htmlOutput); - await directoryController.updateComment(updated); - await directoryController.fetchCommentsForContact(contactId, - active: true); - await directoryController.fetchCommentsForContact(contactId, - active: false); - 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(14), - color: isInactive ? Colors.grey : Colors.black87, - fontStyle: isInactive ? FontStyle.italic : FontStyle.normal, - ), - "p": html.Style( - margin: html.Margins.only(bottom: 6), - lineHeight: const html.LineHeight(1.4), - ), - "strong": html.Style( - fontWeight: FontWeight.w700, - color: isInactive ? Colors.grey : Colors.black87, - ), - }, ), - ], - )); + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (!comment.isActive) + IconButton( + icon: const Icon(Icons.restore, + size: 18, color: Colors.green), + tooltip: "Restore", + splashRadius: 18, + onPressed: () async { + await Get.dialog( + ConfirmDialog( + title: "Restore Note", + message: + "Are you sure you want to restore this note?", + confirmText: "Restore", + confirmColor: Colors.green, + icon: Icons.restore, + onConfirm: () async { + await directoryController.restoreComment( + comment.id, contactId); + }, + ), + ); + }, + ), + if (comment.isActive) ...[ + IconButton( + icon: const Icon(Icons.edit_outlined, + size: 18, color: Colors.indigo), + tooltip: "Edit", + splashRadius: 18, + onPressed: () { + directoryController.editingCommentId.value = + isEditing ? null : comment.id; + }, + ), + IconButton( + icon: const Icon(Icons.delete_outline, + size: 18, color: Colors.red), + tooltip: "Delete", + splashRadius: 18, + onPressed: () async { + await Get.dialog( + ConfirmDialog( + title: "Delete Note", + message: + "Are you sure you want to delete this note?", + confirmText: "Delete", + confirmColor: Colors.red, + icon: Icons.delete_forever, + onConfirm: () async { + await directoryController.deleteComment( + comment.id, contactId); + }, + ), + ); + }, + ), + ], + ], + ), + ], + ), + const SizedBox(height: 8), + if (isEditing) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + controller: textController, + maxLines: null, + minLines: 5, + decoration: InputDecoration( + hintText: "Edit note...", + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12)), + contentPadding: const EdgeInsets.all(12), + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => + directoryController.editingCommentId.value = null, + icon: const Icon(Icons.close, color: Colors.white), + label: const Text( + "Cancel", + style: TextStyle(color: Colors.white), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () async { + final updated = + comment.copyWith(note: textController.text); + await directoryController.updateComment(updated); + await directoryController + .fetchCommentsForContact(contactId); + directoryController.editingCommentId.value = null; + }, + icon: const Icon(Icons.check_circle_outline, + color: Colors.white), + label: const Text( + "Save", + style: TextStyle(color: Colors.white), + ), + style: ElevatedButton.styleFrom( + backgroundColor: contentTheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + ], + ), + ], + ) + else + Text( + comment.note, + style: TextStyle(color: Colors.grey[800], fontSize: 14), + ), + ], + ), + ); } Widget _iconInfoRow( @@ -661,7 +623,6 @@ class _ContactDetailScreenState extends State { } } -// Helper widget for Project label in AppBar class ProjectLabel extends StatelessWidget { final String? projectName; const ProjectLabel(this.projectName, {super.key}); diff --git a/lib/view/directory/notes_view.dart b/lib/view/directory/notes_view.dart index fa65804..00f579e 100644 --- a/lib/view/directory/notes_view.dart +++ b/lib/view/directory/notes_view.dart @@ -1,24 +1,25 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:flutter_quill/flutter_quill.dart' as quill; -import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart'; -import 'package:flutter_html/flutter_html.dart' as html; - import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/controller/directory/notes_controller.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/utils/date_time_utils.dart'; -import 'package:marco/helpers/widgets/Directory/comment_editor_card.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; -class NotesView extends StatelessWidget { +class NotesView extends StatefulWidget { + const NotesView({super.key}); + + @override + State createState() => _NotesViewState(); +} + +class _NotesViewState extends State with UIMixin { final NotesController controller = Get.find(); final TextEditingController searchController = TextEditingController(); - NotesView({super.key}); - Future _refreshNotes() async { try { await controller.fetchNotes(); @@ -28,49 +29,6 @@ class NotesView extends StatelessWidget { } } - 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'); - - if (isListItem && !inList) { - buffer.write('
      '); - inList = true; - } - if (!isListItem && inList) { - buffer.write('
    '); - inList = false; - } - - if (isListItem) buffer.write('
  • '); - - 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(); - } - Widget _buildEmptyState() { return Center( child: Column( @@ -93,11 +51,206 @@ class NotesView extends StatelessWidget { ); } + Widget _buildNoteItem(note) { + final isEditing = controller.editingNoteId.value == note.id; + final textController = TextEditingController(text: note.note); + final initials = note.contactName.trim().isNotEmpty + ? note.contactName + .trim() + .split(' ') + .map((e) => e[0]) + .take(2) + .join() + .toUpperCase() + : "NA"; + + return Container( + margin: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header: Avatar + Name + Timestamp + Actions + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Avatar(firstName: initials, lastName: '', size: 40), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleSmall( + "${note.contactName} (${note.organizationName})", + fontWeight: 600, + color: Colors.black87, + ), + const SizedBox(height: 2), + MyText.bodySmall( + "by ${note.createdBy.firstName} ${note.createdBy.lastName} • " + "${DateTimeUtils.convertUtcToLocal(note.createdAt.toString(), format: 'dd MMM yyyy, hh:mm a')}", + color: Colors.grey[600], + ), + ], + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (!note.isActive) + IconButton( + icon: const Icon(Icons.restore, + size: 18, color: Colors.green), + tooltip: "Restore", + splashRadius: 18, + onPressed: () async { + await Get.dialog( + ConfirmDialog( + title: "Restore Note", + message: + "Are you sure you want to restore this note?", + confirmText: "Restore", + confirmColor: Colors.green, + icon: Icons.restore, + onConfirm: () async { + await controller.restoreOrDeleteNote(note, + restore: true); + }, + ), + ); + }, + ), + if (note.isActive) ...[ + IconButton( + icon: Icon(isEditing ? Icons.close : Icons.edit_outlined, + color: Colors.indigo, size: 18), + splashRadius: 18, + onPressed: () { + controller.editingNoteId.value = + isEditing ? null : note.id; + }, + ), + IconButton( + icon: const Icon(Icons.delete_outline, + size: 18, color: Colors.red), + splashRadius: 18, + onPressed: () async { + await Get.dialog( + ConfirmDialog( + title: "Delete Note", + message: + "Are you sure you want to delete this note?", + confirmText: "Delete", + confirmColor: Colors.red, + icon: Icons.delete_forever, + onConfirm: () async { + await controller.restoreOrDeleteNote(note, + restore: false); + }, + ), + ); + }, + ), + ], + ], + ), + ], + ), + const SizedBox(height: 8), + + // Content: TextField when editing or plain text + if (isEditing) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + controller: textController, + maxLines: null, + minLines: 5, + decoration: InputDecoration( + hintText: "Edit note...", + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5)), + contentPadding: const EdgeInsets.all(12), + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => controller.editingNoteId.value = null, + icon: const Icon(Icons.close, color: Colors.white), + label: MyText.bodyMedium( + "Cancel", + color: Colors.white, + fontWeight: 600, + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 8), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () async { + final updated = + note.copyWith(note: textController.text); + await controller.updateNote(updated); + controller.editingNoteId.value = null; + }, + icon: const Icon(Icons.check_circle_outline, + color: Colors.white), + label: MyText.bodyMedium( + "Save", + color: Colors.white, + fontWeight: 600, + ), + style: ElevatedButton.styleFrom( + backgroundColor: contentTheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 8), + ), + ), + ), + ], + ), + ], + ) + else + Text( + note.note, + style: TextStyle(color: Colors.grey[800], fontSize: 14), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { return Column( children: [ - /// 🔍 Search Field + // Search Padding( padding: MySpacing.xy(8, 8), child: Row( @@ -131,7 +284,7 @@ class NotesView extends StatelessWidget { ), ), - /// 📄 Notes List + // Notes List Expanded( child: Obx(() { if (controller.isLoading.value) { @@ -150,7 +303,9 @@ class NotesView extends StatelessWidget { child: ConstrainedBox( constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: Center(child: _buildEmptyState()), + child: Center( + child: _buildEmptyState(), + ), ), ); }, @@ -165,228 +320,8 @@ class NotesView extends StatelessWidget { padding: MySpacing.only(left: 8, right: 8, top: 4, bottom: 80), itemCount: notes.length, separatorBuilder: (_, __) => MySpacing.height(12), - itemBuilder: (_, index) { - final note = notes[index]; - - return Obx(() { - final isEditing = controller.editingNoteId.value == note.id; - - final initials = note.contactName.trim().isNotEmpty - ? note.contactName - .trim() - .split(' ') - .map((e) => e[0]) - .take(2) - .join() - .toUpperCase() - : "NA"; - - final createdDate = DateTimeUtils.convertUtcToLocal( - note.createdAt.toString(), - format: 'dd MMM yyyy'); - final createdTime = DateTimeUtils.convertUtcToLocal( - note.createdAt.toString(), - format: 'hh:mm a'); - - final decodedDelta = HtmlToDelta().convert(note.note); - final quillController = isEditing - ? quill.QuillController( - document: quill.Document.fromDelta(decodedDelta), - selection: TextSelection.collapsed( - offset: decodedDelta.length), - ) - : null; - - return AnimatedContainer( - duration: const Duration(milliseconds: 250), - padding: MySpacing.xy(12, 12), - decoration: BoxDecoration( - color: isEditing - ? Colors.indigo[50] - : note.isActive - ? Colors.white - : Colors.grey.shade100, - border: Border.all( - color: note.isActive - ? (isEditing - ? Colors.indigo - : Colors.grey.shade300) - : Colors.grey.shade400, - width: 1.1, - ), - borderRadius: BorderRadius.circular(5), - boxShadow: const [ - BoxShadow( - color: Colors.black12, - blurRadius: 4, - offset: Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header & Note content (fade them if inactive) - AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: note.isActive ? 1.0 : 0.6, - child: IgnorePointer( - ignoring: !note.isActive, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Avatar( - firstName: initials, - lastName: '', - size: 40, - backgroundColor: note.isActive - ? null - : Colors.grey.shade400, - ), - MySpacing.width(12), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - MyText.titleSmall( - "${note.contactName} (${note.organizationName})", - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: note.isActive - ? Colors.indigo[800] - : Colors.grey, - ), - MyText.bodySmall( - "by ${note.createdBy.firstName} • $createdDate, $createdTime", - color: note.isActive - ? Colors.grey[600] - : Colors.grey, - ), - ], - ), - ), - ], - ), - MySpacing.height(12), - if (isEditing && quillController != null) - CommentEditorCard( - controller: quillController, - onCancel: () => - controller.editingNoteId.value = null, - onSave: (quillCtrl) async { - final delta = - quillCtrl.document.toDelta(); - final htmlOutput = - _convertDeltaToHtml(delta); - final updated = - note.copyWith(note: htmlOutput); - await controller.updateNote(updated); - controller.editingNoteId.value = null; - }, - ) - else - html.Html( - data: note.note, - style: { - "body": html.Style( - margin: html.Margins.zero, - padding: html.HtmlPaddings.zero, - fontSize: html.FontSize.medium, - color: note.isActive - ? Colors.black87 - : Colors.grey, - ), - }, - ), - ], - ), - ), - ), - - // Action buttons (always fully visible) - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (note.isActive) ...[ - IconButton( - icon: Icon( - isEditing ? Icons.close : Icons.edit, - color: Colors.indigo, - size: 20, - ), - padding: EdgeInsets.all(2), - constraints: const BoxConstraints(), - onPressed: () { - controller.editingNoteId.value = - isEditing ? null : note.id; - }, - ), - IconButton( - icon: const Icon( - Icons.delete_outline, - color: Colors.redAccent, - size: 20, - ), - constraints: const BoxConstraints(), - onPressed: () async { - await Get.dialog( - ConfirmDialog( - title: "Delete Note", - message: - "Are you sure you want to delete this note?", - confirmText: "Delete", - confirmColor: Colors.redAccent, - icon: Icons.delete_forever, - onConfirm: () async { - await controller.restoreOrDeleteNote( - note, - restore: false); - }, - ), - barrierDismissible: false, - ); - }, - ), - ], - if (!note.isActive) - IconButton( - icon: const Icon( - Icons.restore, - color: Colors.green, - size: 22, - ), - tooltip: "Restore", - onPressed: () async { - await Get.dialog( - ConfirmDialog( - title: "Restore Note", - message: - "Are you sure you want to restore this note?", - confirmText: "Restore", - confirmColor: Colors.green, - icon: Icons.restore, - onConfirm: () async { - await controller.restoreOrDeleteNote( - note, - restore: true); - }, - ), - barrierDismissible: false, - ); - }, - ), - ], - ), - ], - ), - ); - }); - }, + itemBuilder: (_, index) => + Obx(() => _buildNoteItem(notes[index])), ), ); }),