333 lines
12 KiB
Dart
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])),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|