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'; class NotesView extends StatelessWidget { final NotesController controller = Get.find(); final TextEditingController searchController = TextEditingController(); NotesView({super.key}); Future _refreshNotes() async { try { await controller.fetchNotes(); } catch (e, st) { debugPrint('Error refreshing notes: $e'); debugPrintStack(stackTrace: st); } } 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 = 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( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.perm_contact_cal, size: 60, color: Colors.grey), MySpacing.height(18), MyText.titleMedium( 'No matching notes found.', fontWeight: 600, color: Colors.grey, ), MySpacing.height(10), MyText.bodySmall( 'Try adjusting your filters or refresh to reload.', color: Colors.grey, ), ], ), ); } @override Widget build(BuildContext context) { return Column( children: [ /// 🔍 Search Field Padding( padding: MySpacing.xy(8, 8), child: Row( children: [ Expanded( child: SizedBox( height: 35, child: TextField( controller: searchController, onChanged: (value) => controller.searchQuery.value = value, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 6), prefixIcon: const Icon(Icons.search, size: 20, color: Colors.grey), hintText: 'Search notes...', filled: true, fillColor: Colors.white, border: OutlineInputBorder( borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300), ), ), ), ), ), ], ), ), /// 📄 Notes List Expanded( child: Obx(() { if (controller.isLoading.value) { return const Center(child: CircularProgressIndicator()); } final notes = controller.filteredNotesList; if (notes.isEmpty) { return MyRefreshIndicator( onRefresh: _refreshNotes, child: LayoutBuilder( builder: (context, constraints) { return SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: ConstrainedBox( constraints: BoxConstraints(minHeight: constraints.maxHeight), child: Center(child: _buildEmptyState()), ), ); }, ), ); } return MyRefreshIndicator( onRefresh: _refreshNotes, child: ListView.separated( physics: const AlwaysScrollableScrollPhysics(), 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, ); }, ), ], ), ], ), ); }); }, ), ); }), ), ], ); } }