333 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
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/my_confirmation_dialog.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
class NotesView extends StatefulWidget {
const NotesView({super.key});
@override
State<NotesView> createState() => _NotesViewState();
}
class _NotesViewState extends State<NotesView> with UIMixin {
final NotesController controller = Get.find();
final TextEditingController searchController = TextEditingController();
Future<void> _refreshNotes() async {
try {
await controller.fetchNotes();
} catch (e, st) {
debugPrint('Error refreshing notes: $e');
debugPrintStack(stackTrace: st);
}
}
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,
),
],
),
);
}
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
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) =>
Obx(() => _buildNoteItem(notes[index])),
),
);
}),
),
],
);
}
}