369 lines
15 KiB
Dart

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<void> _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('<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() {
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,
),
},
),
],
),
);
});
},
),
);
}),
),
],
);
}
}