diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index ca14f30..cceebae 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index ca14f30..cceebae 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index ca14f30..cceebae 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index ca14f30..cceebae 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index ca14f30..cceebae 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/logo/loading_logo.png b/assets/logo/loading_logo.png index eaece50..5e680ac 100644 Binary files a/assets/logo/loading_logo.png and b/assets/logo/loading_logo.png differ diff --git a/assets/logo/logo_dark.png b/assets/logo/logo_dark.png index ca14f30..9249715 100644 Binary files a/assets/logo/logo_dark.png and b/assets/logo/logo_dark.png differ diff --git a/assets/logo/logo_dark_small.png b/assets/logo/logo_dark_small.png index ca14f30..9249715 100644 Binary files a/assets/logo/logo_dark_small.png and b/assets/logo/logo_dark_small.png differ diff --git a/assets/logo/logo_light.png b/assets/logo/logo_light.png index ca14f30..5e680ac 100644 Binary files a/assets/logo/logo_light.png and b/assets/logo/logo_light.png differ diff --git a/assets/logo/logo_light_small.png b/assets/logo/logo_light_small.png index ca14f30..5e680ac 100644 Binary files a/assets/logo/logo_light_small.png and b/assets/logo/logo_light_small.png differ diff --git a/lib/helpers/widgets/Directory/comment_editor_card.dart b/lib/helpers/widgets/Directory/comment_editor_card.dart index c6125af..e69de29 100644 --- a/lib/helpers/widgets/Directory/comment_editor_card.dart +++ b/lib/helpers/widgets/Directory/comment_editor_card.dart @@ -1,135 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart' as quill; -import 'package:marco/helpers/widgets/my_text.dart'; - -class CommentEditorCard extends StatefulWidget { - final quill.QuillController controller; - final VoidCallback onCancel; - final Future Function(quill.QuillController controller) onSave; - - const CommentEditorCard({ - super.key, - required this.controller, - required this.onCancel, - required this.onSave, - }); - - @override - State createState() => _CommentEditorCardState(); -} - -class _CommentEditorCardState extends State { - bool _isSubmitting = false; - - Future _handleSave() async { - if (_isSubmitting) return; - setState(() => _isSubmitting = true); - - try { - await widget.onSave(widget.controller); - } finally { - if (mounted) setState(() => _isSubmitting = false); - } - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - quill.QuillSimpleToolbar( - controller: widget.controller, - configurations: const quill.QuillSimpleToolbarConfigurations( - showBoldButton: true, - showItalicButton: true, - showUnderLineButton: true, - showListBullets: false, - showListNumbers: false, - showAlignmentButtons: true, - showLink: true, - showFontSize: false, - showFontFamily: false, - showColorButton: false, - showBackgroundColorButton: false, - showUndo: false, - showRedo: false, - showCodeBlock: false, - showQuote: false, - showSuperscript: false, - showSubscript: false, - showInlineCode: false, - showDirection: false, - showListCheck: false, - showStrikeThrough: false, - showClearFormat: false, - showDividers: false, - showHeaderStyle: false, - multiRowsDisplay: false, - ), - ), - const SizedBox(height: 24), - Container( - height: 140, - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(8), - color: const Color(0xFFFDFDFD), - ), - child: quill.QuillEditor.basic( - controller: widget.controller, - configurations: const quill.QuillEditorConfigurations( - autoFocus: true, - expands: false, - scrollable: true, - ), - ), - ), - const SizedBox(height: 16), - - // 👇 Buttons same as BaseBottomSheet - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: _isSubmitting ? null : widget.onCancel, - 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: _isSubmitting ? null : _handleSave, - icon: const Icon(Icons.check_circle_outline, color: Colors.white), - label: MyText.bodyMedium( - _isSubmitting ? "Submitting..." : "Submit", - color: Colors.white, - fontWeight: 600, - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.indigo, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.symmetric(vertical: 8), - ), - ), - ), - ], - ), - ], - ); - } -} diff --git a/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart b/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart index 4ec74b0..3de75cd 100644 --- a/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart +++ b/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart @@ -274,7 +274,7 @@ class _AttendanceChart extends StatelessWidget { return Container( height: 600, decoration: BoxDecoration( - color: Colors.blueGrey.shade50, + color: Colors.transparent, borderRadius: BorderRadius.circular(5), ), child: const Center( @@ -302,7 +302,7 @@ class _AttendanceChart extends StatelessWidget { height: 600, padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: Colors.blueGrey.shade50, + color: Colors.transparent, borderRadius: BorderRadius.circular(5), ), child: SfCartesianChart( @@ -378,7 +378,7 @@ class _AttendanceTable extends StatelessWidget { return Container( height: 300, decoration: BoxDecoration( - color: Colors.grey.shade50, + color: Colors.transparent, borderRadius: BorderRadius.circular(5), ), child: const Center( 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 71361af..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,13 @@ 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: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/helpers/utils/mixins/ui_mixin.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(); -} - class ContactDetailScreen extends StatefulWidget { final ContactModel contact; const ContactDetailScreen({super.key, required this.contact}); @@ -69,10 +22,10 @@ class ContactDetailScreen extends StatefulWidget { State createState() => _ContactDetailScreenState(); } -class _ContactDetailScreenState extends State with UIMixin{ +class _ContactDetailScreenState extends State + with UIMixin { late final DirectoryController directoryController; late final ProjectController projectController; - late Rx contactRx; @override @@ -89,7 +42,6 @@ class _ContactDetailScreenState extends State with UIMixin{ 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); @@ -172,11 +124,7 @@ class _ContactDetailScreenState extends State with UIMixin{ 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, @@ -280,8 +228,7 @@ class _ContactDetailScreenState extends State with UIMixin{ _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", [ @@ -338,7 +285,6 @@ class _ContactDetailScreenState extends State with UIMixin{ return Obx(() { final contactId = contactRx.value.id; - // Get active and inactive comments final activeComments = directoryController .getCommentsForContact(contactId) .where((c) => c.isActive) @@ -348,7 +294,6 @@ class _ContactDetailScreenState extends State with UIMixin{ .where((c) => !c.isActive) .toList(); - // Combine both and keep the same sorting (recent first) final comments = [...activeComments, ...inactiveComments].reversed.toList(); final editingId = directoryController.editingCommentId.value; @@ -389,7 +334,9 @@ class _ContactDetailScreenState extends State with UIMixin{ final result = await Get.bottomSheet( AddCommentBottomSheet(contactId: contactId), isScrollControlled: true, + enableDrag: true, ); + if (result == true) { await directoryController.fetchCommentsForContact(contactId, active: true); @@ -413,13 +360,7 @@ class _ContactDetailScreenState extends State with UIMixin{ ? 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 textController = TextEditingController(text: comment.note); return Container( margin: const EdgeInsets.symmetric(vertical: 6), @@ -439,21 +380,16 @@ class _ContactDetailScreenState extends State with UIMixin{ child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 🧑 Header + // Header: Avatar + Name + Role + Timestamp + Actions Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Avatar( - firstName: initials, - lastName: '', - size: 40, - ), + Avatar(firstName: initials, lastName: '', size: 40), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Full name on top Text( "${comment.createdBy.firstName} ${comment.createdBy.lastName}", style: const TextStyle( @@ -464,7 +400,6 @@ class _ContactDetailScreenState extends State with UIMixin{ overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), - // Job Role if (comment.createdBy.jobRoleName?.isNotEmpty ?? false) Text( comment.createdBy.jobRoleName, @@ -475,7 +410,6 @@ class _ContactDetailScreenState extends State with UIMixin{ ), ), const SizedBox(height: 2), - // Timestamp Text( DateTimeUtils.convertUtcToLocal( comment.createdAt.toString(), @@ -489,8 +423,6 @@ class _ContactDetailScreenState extends State with UIMixin{ ], ), ), - - // ⚡ Action buttons Row( mainAxisSize: MainAxisSize.min, children: [ @@ -555,42 +487,77 @@ class _ContactDetailScreenState extends State with UIMixin{ ), ], ), - 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); - directoryController.editingCommentId.value = null; - }, + 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 - html.Html( - data: comment.note, - style: { - "body": html.Style( - margin: html.Margins.zero, - padding: html.HtmlPaddings.zero, - fontSize: html.FontSize(14), - color: Colors.black87, - ), - "p": html.Style( - margin: html.Margins.only(bottom: 6), - lineHeight: const html.LineHeight(1.4), - ), - "strong": html.Style( - fontWeight: FontWeight.w700, - color: Colors.black87, - ), - }, + Text( + comment.note, + style: TextStyle(color: Colors.grey[800], fontSize: 14), ), ], ), @@ -656,7 +623,6 @@ class _ContactDetailScreenState extends State with UIMixin{ } } -// 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 20c44b6..00f579e 100644 --- a/lib/view/directory/notes_view.dart +++ b/lib/view/directory/notes_view.dart @@ -1,26 +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 StatefulWidget { + const NotesView({super.key}); -class NotesView extends StatelessWidget { + @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(); @@ -30,50 +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( @@ -96,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 + Refresh (Top Row) + // Search Padding( padding: MySpacing.xy(8, 8), child: Row( @@ -134,7 +284,7 @@ class NotesView extends StatelessWidget { ), ), - /// 📄 Notes List View + // Notes List Expanded( child: Obx(() { if (controller.isLoading.value) { @@ -170,196 +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] : 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, - ), - }, - ), - ], - ), - ); - }); - }, + itemBuilder: (_, index) => + Obx(() => _buildNoteItem(notes[index])), ), ); }), diff --git a/pubspec.yaml b/pubspec.yaml index 31e2374..f7e01f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+7 +version: 1.0.0+11 environment: sdk: ^3.5.3 @@ -48,7 +48,6 @@ dependencies: carousel_slider: ^5.0.0 reorderable_grid: ^1.0.10 loading_animation_widget: ^1.3.0 - flutter_quill: ^10.8.5 intl: ^0.19.0 syncfusion_flutter_core: ^29.1.40 syncfusion_flutter_sliders: ^29.1.40 @@ -74,9 +73,6 @@ dependencies: font_awesome_flutter: ^10.8.0 flutter_html: ^3.0.0 tab_indicator_styler: ^2.0.0 - html_editor_enhanced: ^2.7.0 - flutter_quill_delta_from_html: ^1.5.2 - quill_delta: ^3.0.0-nullsafety.2 connectivity_plus: ^6.1.4 geocoding: ^4.0.0 firebase_core: ^4.0.0