refactor: remove unused comment editor and related dependencies, update version

This commit is contained in:
Vaibhav Surve 2025-10-27 14:47:56 +05:30
parent d26e7e3774
commit cd21a3ac38
16 changed files with 325 additions and 600 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 409 KiB

View File

@ -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<void> Function(quill.QuillController controller) onSave;
const CommentEditorCard({
super.key,
required this.controller,
required this.onCancel,
required this.onSave,
});
@override
State<CommentEditorCard> createState() => _CommentEditorCardState();
}
class _CommentEditorCardState extends State<CommentEditorCard> {
bool _isSubmitting = false;
Future<void> _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),
),
),
),
],
),
],
);
}
}

View File

@ -274,7 +274,7 @@ class _AttendanceChart extends StatelessWidget {
return Container( return Container(
height: 600, height: 600,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blueGrey.shade50, color: Colors.transparent,
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
), ),
child: const Center( child: const Center(
@ -302,7 +302,7 @@ class _AttendanceChart extends StatelessWidget {
height: 600, height: 600,
padding: const EdgeInsets.all(6), padding: const EdgeInsets.all(6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blueGrey.shade50, color: Colors.transparent,
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
), ),
child: SfCartesianChart( child: SfCartesianChart(
@ -378,7 +378,7 @@ class _AttendanceTable extends StatelessWidget {
return Container( return Container(
height: 300, height: 300,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade50, color: Colors.transparent,
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
), ),
child: const Center( child: const Center(

View File

@ -1,10 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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/controller/directory/add_comment_controller.dart';
import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/Directory/comment_editor_card.dart';
class AddCommentBottomSheet extends StatefulWidget { class AddCommentBottomSheet extends StatefulWidget {
final String contactId; final String contactId;
@ -17,120 +14,59 @@ class AddCommentBottomSheet extends StatefulWidget {
class _AddCommentBottomSheetState extends State<AddCommentBottomSheet> { class _AddCommentBottomSheetState extends State<AddCommentBottomSheet> {
late final AddCommentController controller; late final AddCommentController controller;
late final quill.QuillController quillController; final TextEditingController textController = TextEditingController();
bool isSubmitting = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
controller = Get.put(AddCommentController(contactId: widget.contactId)); controller = Get.put(AddCommentController(contactId: widget.contactId));
quillController = quill.QuillController.basic();
} }
@override @override
void dispose() { void dispose() {
quillController.dispose(); textController.dispose();
Get.delete<AddCommentController>();
super.dispose(); super.dispose();
} }
Future<void> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView( return BaseBottomSheet(
padding: MediaQuery.of(context).viewInsets, title: "Add Note",
child: Container( onCancel: () => Get.back(),
decoration: BoxDecoration( onSubmit: handleSubmit,
color: Theme.of(context).cardColor, isSubmitting: isSubmitting,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), child: TextField(
boxShadow: const [ controller: textController,
BoxShadow( maxLines: null,
color: Colors.black12, minLines: 5,
blurRadius: 12, decoration: InputDecoration(
offset: Offset(0, -2), hintText: "Enter your note here...",
), border: OutlineInputBorder(
], borderRadius: BorderRadius.circular(12),
),
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();
},
),
],
), ),
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('<ul>');
inList = true;
}
if (!isListItem && inList) {
buffer.write('</ul>');
inList = false;
}
if (isListItem && trimmedData.isEmpty) continue;
if (isListItem) buffer.write('<li>');
if (attr.containsKey('bold')) buffer.write('<strong>');
if (attr.containsKey('italic')) buffer.write('<em>');
if (attr.containsKey('underline')) buffer.write('<u>');
if (attr.containsKey('strike')) buffer.write('<s>');
if (attr.containsKey('link')) buffer.write('<a href="${attr['link']}">');
buffer.write(trimmedData.replaceAll('\n', ''));
if (attr.containsKey('link')) buffer.write('</a>');
if (attr.containsKey('strike')) buffer.write('</s>');
if (attr.containsKey('underline')) buffer.write('</u>');
if (attr.containsKey('italic')) buffer.write('</em>');
if (attr.containsKey('bold')) buffer.write('</strong>');
if (isListItem) {
buffer.write('</li>');
} else if (data.contains('\n')) {
buffer.write('<br>');
}
}
if (inList) buffer.write('</ul>');
return buffer.toString();
}
} }

View File

@ -1,7 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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/project_controller.dart';
import 'package:marco/controller/directory/directory_controller.dart'; import 'package:marco/controller/directory/directory_controller.dart';
import 'package:marco/helpers/widgets/my_spacing.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/model/directory/contact_model.dart';
import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/helpers/utils/launcher_utils.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/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/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_refresh_indicator.dart';
import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.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('<ul>');
inList = true;
}
if (!isListItem && inList) {
buffer.write('</ul>');
inList = false;
}
if (isListItem) buffer.write('<li>');
if (attr.containsKey('bold')) buffer.write('<strong>');
if (attr.containsKey('italic')) buffer.write('<em>');
if (attr.containsKey('underline')) buffer.write('<u>');
if (attr.containsKey('strike')) buffer.write('<s>');
if (attr.containsKey('link')) buffer.write('<a href="${attr['link']}">');
buffer.write(data.replaceAll('\n', ''));
if (attr.containsKey('link')) buffer.write('</a>');
if (attr.containsKey('strike')) buffer.write('</s>');
if (attr.containsKey('underline')) buffer.write('</u>');
if (attr.containsKey('italic')) buffer.write('</em>');
if (attr.containsKey('bold')) buffer.write('</strong>');
if (isListItem)
buffer.write('</li>');
else if (data.contains('\n')) {
buffer.write('<br>');
}
}
if (inList) buffer.write('</ul>');
return buffer.toString();
}
class ContactDetailScreen extends StatefulWidget { class ContactDetailScreen extends StatefulWidget {
final ContactModel contact; final ContactModel contact;
const ContactDetailScreen({super.key, required this.contact}); const ContactDetailScreen({super.key, required this.contact});
@ -69,10 +22,10 @@ class ContactDetailScreen extends StatefulWidget {
State<ContactDetailScreen> createState() => _ContactDetailScreenState(); State<ContactDetailScreen> createState() => _ContactDetailScreenState();
} }
class _ContactDetailScreenState extends State<ContactDetailScreen> with UIMixin{ class _ContactDetailScreenState extends State<ContactDetailScreen>
with UIMixin {
late final DirectoryController directoryController; late final DirectoryController directoryController;
late final ProjectController projectController; late final ProjectController projectController;
late Rx<ContactModel> contactRx; late Rx<ContactModel> contactRx;
@override @override
@ -89,7 +42,6 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> with UIMixin{
active: false); active: false);
}); });
// Listen to controller's allContacts and update contact if changed
ever(directoryController.allContacts, (_) { ever(directoryController.allContacts, (_) {
final updated = directoryController.allContacts final updated = directoryController.allContacts
.firstWhereOrNull((c) => c.id == contactRx.value.id); .firstWhereOrNull((c) => c.id == contactRx.value.id);
@ -172,11 +124,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> with UIMixin{
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row(children: [ Row(children: [
Avatar( Avatar(firstName: firstName, lastName: lastName, size: 35),
firstName: firstName,
lastName: lastName,
size: 35,
),
MySpacing.width(12), MySpacing.width(12),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -280,8 +228,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> with UIMixin{
_iconInfoRow(Icons.location_on, "Address", contact.address), _iconInfoRow(Icons.location_on, "Address", contact.address),
]), ]),
_infoCard("Organization", [ _infoCard("Organization", [
_iconInfoRow( _iconInfoRow(Icons.business, "Organization", contact.organization),
Icons.business, "Organization", contact.organization),
_iconInfoRow(Icons.category, "Category", category), _iconInfoRow(Icons.category, "Category", category),
]), ]),
_infoCard("Meta Info", [ _infoCard("Meta Info", [
@ -338,7 +285,6 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> with UIMixin{
return Obx(() { return Obx(() {
final contactId = contactRx.value.id; final contactId = contactRx.value.id;
// Get active and inactive comments
final activeComments = directoryController final activeComments = directoryController
.getCommentsForContact(contactId) .getCommentsForContact(contactId)
.where((c) => c.isActive) .where((c) => c.isActive)
@ -348,7 +294,6 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> with UIMixin{
.where((c) => !c.isActive) .where((c) => !c.isActive)
.toList(); .toList();
// Combine both and keep the same sorting (recent first)
final comments = final comments =
[...activeComments, ...inactiveComments].reversed.toList(); [...activeComments, ...inactiveComments].reversed.toList();
final editingId = directoryController.editingCommentId.value; final editingId = directoryController.editingCommentId.value;
@ -389,7 +334,9 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> with UIMixin{
final result = await Get.bottomSheet( final result = await Get.bottomSheet(
AddCommentBottomSheet(contactId: contactId), AddCommentBottomSheet(contactId: contactId),
isScrollControlled: true, isScrollControlled: true,
enableDrag: true,
); );
if (result == true) { if (result == true) {
await directoryController.fetchCommentsForContact(contactId, await directoryController.fetchCommentsForContact(contactId,
active: true); active: true);
@ -413,13 +360,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> with UIMixin{
? comment.createdBy.firstName[0].toUpperCase() ? comment.createdBy.firstName[0].toUpperCase()
: "?"; : "?";
final decodedDelta = HtmlToDelta().convert(comment.note); final textController = TextEditingController(text: comment.note);
final quillController = isEditing
? quill.QuillController(
document: quill.Document.fromDelta(decodedDelta),
selection: TextSelection.collapsed(offset: decodedDelta.length),
)
: null;
return Container( return Container(
margin: const EdgeInsets.symmetric(vertical: 6), margin: const EdgeInsets.symmetric(vertical: 6),
@ -439,21 +380,16 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> with UIMixin{
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 🧑 Header // Header: Avatar + Name + Role + Timestamp + Actions
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Avatar( Avatar(firstName: initials, lastName: '', size: 40),
firstName: initials,
lastName: '',
size: 40,
),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Full name on top
Text( Text(
"${comment.createdBy.firstName} ${comment.createdBy.lastName}", "${comment.createdBy.firstName} ${comment.createdBy.lastName}",
style: const TextStyle( style: const TextStyle(
@ -464,7 +400,6 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> with UIMixin{
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
// Job Role
if (comment.createdBy.jobRoleName?.isNotEmpty ?? false) if (comment.createdBy.jobRoleName?.isNotEmpty ?? false)
Text( Text(
comment.createdBy.jobRoleName, comment.createdBy.jobRoleName,
@ -475,7 +410,6 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> with UIMixin{
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
// Timestamp
Text( Text(
DateTimeUtils.convertUtcToLocal( DateTimeUtils.convertUtcToLocal(
comment.createdAt.toString(), comment.createdAt.toString(),
@ -489,8 +423,6 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> with UIMixin{
], ],
), ),
), ),
// Action buttons
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -555,42 +487,77 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> with UIMixin{
), ),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
if (isEditing)
// 📝 Comment Content Column(
if (isEditing && quillController != null) crossAxisAlignment: CrossAxisAlignment.stretch,
CommentEditorCard( children: [
controller: quillController, TextField(
onCancel: () => directoryController.editingCommentId.value = null, controller: textController,
onSave: (ctrl) async { maxLines: null,
final delta = ctrl.document.toDelta(); minLines: 5,
final htmlOutput = _convertDeltaToHtml(delta); decoration: InputDecoration(
final updated = comment.copyWith(note: htmlOutput); hintText: "Edit note...",
await directoryController.updateComment(updated); border: OutlineInputBorder(
await directoryController.fetchCommentsForContact(contactId); borderRadius: BorderRadius.circular(12)),
directoryController.editingCommentId.value = null; 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 else
html.Html( Text(
data: comment.note, comment.note,
style: { style: TextStyle(color: Colors.grey[800], fontSize: 14),
"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,
),
},
), ),
], ],
), ),
@ -656,7 +623,6 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> with UIMixin{
} }
} }
// Helper widget for Project label in AppBar
class ProjectLabel extends StatelessWidget { class ProjectLabel extends StatelessWidget {
final String? projectName; final String? projectName;
const ProjectLabel(this.projectName, {super.key}); const ProjectLabel(this.projectName, {super.key});

View File

@ -1,26 +1,25 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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/helpers/widgets/my_refresh_indicator.dart';
import 'package:marco/controller/directory/notes_controller.dart'; import 'package:marco/controller/directory/notes_controller.dart';
import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/helpers/utils/date_time_utils.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/widgets/my_confirmation_dialog.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
class NotesView extends StatefulWidget {
const NotesView({super.key});
class NotesView extends StatelessWidget { @override
State<NotesView> createState() => _NotesViewState();
}
class _NotesViewState extends State<NotesView> with UIMixin {
final NotesController controller = Get.find(); final NotesController controller = Get.find();
final TextEditingController searchController = TextEditingController(); final TextEditingController searchController = TextEditingController();
NotesView({super.key});
Future<void> _refreshNotes() async { Future<void> _refreshNotes() async {
try { try {
await controller.fetchNotes(); 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('<ul>');
inList = true;
}
if (!isListItem && inList) {
buffer.write('</ul>');
inList = false;
}
if (isListItem) buffer.write('<li>');
if (attr.containsKey('bold')) buffer.write('<strong>');
if (attr.containsKey('italic')) buffer.write('<em>');
if (attr.containsKey('underline')) buffer.write('<u>');
if (attr.containsKey('strike')) buffer.write('<s>');
if (attr.containsKey('link')) buffer.write('<a href="${attr['link']}">');
buffer.write(data.replaceAll('\n', ''));
if (attr.containsKey('link')) buffer.write('</a>');
if (attr.containsKey('strike')) buffer.write('</s>');
if (attr.containsKey('underline')) buffer.write('</u>');
if (attr.containsKey('italic')) buffer.write('</em>');
if (attr.containsKey('bold')) buffer.write('</strong>');
if (isListItem)
buffer.write('</li>');
else if (data.contains('\n')) buffer.write('<br>');
}
if (inList) buffer.write('</ul>');
return buffer.toString();
}
Widget _buildEmptyState() { Widget _buildEmptyState() {
return Center( return Center(
child: Column( 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
/// 🔍 Search + Refresh (Top Row) // Search
Padding( Padding(
padding: MySpacing.xy(8, 8), padding: MySpacing.xy(8, 8),
child: Row( child: Row(
@ -134,7 +284,7 @@ class NotesView extends StatelessWidget {
), ),
), ),
/// 📄 Notes List View // Notes List
Expanded( Expanded(
child: Obx(() { child: Obx(() {
if (controller.isLoading.value) { if (controller.isLoading.value) {
@ -170,196 +320,8 @@ class NotesView extends StatelessWidget {
padding: MySpacing.only(left: 8, right: 8, top: 4, bottom: 80), padding: MySpacing.only(left: 8, right: 8, top: 4, bottom: 80),
itemCount: notes.length, itemCount: notes.length,
separatorBuilder: (_, __) => MySpacing.height(12), separatorBuilder: (_, __) => MySpacing.height(12),
itemBuilder: (_, index) { itemBuilder: (_, index) =>
final note = notes[index]; Obx(() => _buildNoteItem(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,
),
},
),
],
),
);
});
},
), ),
); );
}), }),

View File

@ -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 # 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 # 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. # 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: environment:
sdk: ^3.5.3 sdk: ^3.5.3
@ -48,7 +48,6 @@ dependencies:
carousel_slider: ^5.0.0 carousel_slider: ^5.0.0
reorderable_grid: ^1.0.10 reorderable_grid: ^1.0.10
loading_animation_widget: ^1.3.0 loading_animation_widget: ^1.3.0
flutter_quill: ^10.8.5
intl: ^0.19.0 intl: ^0.19.0
syncfusion_flutter_core: ^29.1.40 syncfusion_flutter_core: ^29.1.40
syncfusion_flutter_sliders: ^29.1.40 syncfusion_flutter_sliders: ^29.1.40
@ -74,9 +73,6 @@ dependencies:
font_awesome_flutter: ^10.8.0 font_awesome_flutter: ^10.8.0
flutter_html: ^3.0.0 flutter_html: ^3.0.0
tab_indicator_styler: ^2.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 connectivity_plus: ^6.1.4
geocoding: ^4.0.0 geocoding: ^4.0.0
firebase_core: ^4.0.0 firebase_core: ^4.0.0