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 + Refresh (Top Row) 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 View 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] : Colors.white, border: Border.all( color: isEditing ? Colors.indigo : Colors.grey.shade300, 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 Row Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Avatar( firstName: initials, lastName: '', size: 40), MySpacing.width(12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.titleSmall( "${note.contactName} (${note.organizationName})", fontWeight: 600, overflow: TextOverflow.ellipsis, color: Colors.indigo[800], ), MyText.bodySmall( "by ${note.createdBy.firstName} • $createdDate, $createdTime", color: Colors.grey[600], ), ], ), ), /// Edit / Delete / Restore Icons if (!note.isActive) IconButton( icon: const Icon(Icons.restore, color: Colors.green, size: 20), tooltip: "Restore", padding: EdgeInsets .zero, 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, ); }, ) else Row( mainAxisSize: MainAxisSize.min, children: [ /// Edit Icon IconButton( icon: Icon( isEditing ? Icons.close : Icons.edit, color: Colors.indigo, size: 20, ), padding: EdgeInsets .zero, constraints: const BoxConstraints(), onPressed: () { controller.editingNoteId.value = isEditing ? null : note.id; }, ), const SizedBox( width: 6), /// Delete Icon IconButton( icon: const Icon(Icons.delete_outline, color: Colors.redAccent, size: 20), padding: EdgeInsets.zero, 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, ); }, ), ], ), ], ), MySpacing.height(12), /// Content 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: Colors.black87, ), }, ), ], ), ); }); }, ), ); }), ), ], ); } }