Refactor theme editor widget and dashboard screen layout; enhance user document screen with improved search, filter, and document management features
This commit is contained in:
parent
3c89b4ddbb
commit
d15d9f22df
@ -5,54 +5,63 @@ import 'package:marco/model/document/document_filter_model.dart';
|
|||||||
import 'package:marco/model/document/documents_list_model.dart';
|
import 'package:marco/model/document/documents_list_model.dart';
|
||||||
|
|
||||||
class DocumentController extends GetxController {
|
class DocumentController extends GetxController {
|
||||||
// ------------------ Observables ---------------------
|
// ==================== Observables ====================
|
||||||
var isLoading = false.obs;
|
final isLoading = false.obs;
|
||||||
var documents = <DocumentItem>[].obs;
|
final documents = <DocumentItem>[].obs;
|
||||||
var filters = Rxn<DocumentFiltersData>();
|
final filters = Rxn<DocumentFiltersData>();
|
||||||
|
|
||||||
// ✅ Selected filters (multi-select support)
|
// Selected filters (multi-select)
|
||||||
var selectedUploadedBy = <String>[].obs;
|
final selectedUploadedBy = <String>[].obs;
|
||||||
var selectedCategory = <String>[].obs;
|
final selectedCategory = <String>[].obs;
|
||||||
var selectedType = <String>[].obs;
|
final selectedType = <String>[].obs;
|
||||||
var selectedTag = <String>[].obs;
|
final selectedTag = <String>[].obs;
|
||||||
|
|
||||||
// Pagination state
|
// Pagination
|
||||||
var pageNumber = 1.obs;
|
final pageNumber = 1.obs;
|
||||||
final int pageSize = 20;
|
final pageSize = 20;
|
||||||
var hasMore = true.obs;
|
final hasMore = true.obs;
|
||||||
|
|
||||||
// Error message
|
// Error handling
|
||||||
var errorMessage = "".obs;
|
final errorMessage = ''.obs;
|
||||||
|
|
||||||
// NEW: show inactive toggle
|
// Preferences
|
||||||
var showInactive = false.obs;
|
final showInactive = false.obs;
|
||||||
|
|
||||||
// NEW: search
|
// Search
|
||||||
var searchQuery = ''.obs;
|
final searchQuery = ''.obs;
|
||||||
var searchController = TextEditingController();
|
final searchController = TextEditingController();
|
||||||
// New filter fields
|
|
||||||
var isUploadedAt = true.obs;
|
|
||||||
var isVerified = RxnBool();
|
|
||||||
var startDate = Rxn<String>();
|
|
||||||
var endDate = Rxn<String>();
|
|
||||||
|
|
||||||
// ------------------ API Calls -----------------------
|
// Additional filters
|
||||||
|
final isUploadedAt = true.obs;
|
||||||
|
final isVerified = RxnBool();
|
||||||
|
final startDate = Rxn<String>();
|
||||||
|
final endDate = Rxn<String>();
|
||||||
|
|
||||||
/// Fetch Document Filters for an Entity
|
// ==================== Lifecycle ====================
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
// Don't dispose searchController here - it's managed by the page
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== API Methods ====================
|
||||||
|
|
||||||
|
/// Fetch document filters for entity
|
||||||
Future<void> fetchFilters(String entityTypeId) async {
|
Future<void> fetchFilters(String entityTypeId) async {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
|
||||||
final response = await ApiService.getDocumentFilters(entityTypeId);
|
final response = await ApiService.getDocumentFilters(entityTypeId);
|
||||||
|
|
||||||
if (response != null && response.success) {
|
if (response != null && response.success) {
|
||||||
filters.value = response.data;
|
filters.value = response.data;
|
||||||
} else {
|
} else {
|
||||||
errorMessage.value = response?.message ?? "Failed to fetch filters";
|
errorMessage.value = response?.message ?? 'Failed to fetch filters';
|
||||||
|
_showError('Failed to load filters');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMessage.value = "Error fetching filters: $e";
|
errorMessage.value = 'Error fetching filters: $e';
|
||||||
} finally {
|
_showError('Error loading filters');
|
||||||
isLoading.value = false;
|
debugPrint('❌ Error fetching filters: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,11 +74,14 @@ class DocumentController extends GetxController {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
final success =
|
|
||||||
await ApiService.deleteDocumentApi(id: id, isActive: isActive);
|
final success = await ApiService.deleteDocumentApi(
|
||||||
|
id: id,
|
||||||
|
isActive: isActive,
|
||||||
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
// 🔥 Always fetch fresh list after toggle
|
// Refresh list after state change
|
||||||
await fetchDocuments(
|
await fetchDocuments(
|
||||||
entityTypeId: entityTypeId,
|
entityTypeId: entityTypeId,
|
||||||
entityId: entityId,
|
entityId: entityId,
|
||||||
@ -77,41 +89,19 @@ class DocumentController extends GetxController {
|
|||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
errorMessage.value = "Failed to update document state";
|
errorMessage.value = 'Failed to update document state';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMessage.value = "Error updating document: $e";
|
errorMessage.value = 'Error updating document: $e';
|
||||||
|
debugPrint('❌ Error toggling document state: $e');
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Permanently delete a document (or deactivate depending on API)
|
/// Fetch documents for entity with pagination
|
||||||
Future<bool> deleteDocument(String id, {bool isActive = false}) async {
|
|
||||||
try {
|
|
||||||
isLoading.value = true;
|
|
||||||
final success =
|
|
||||||
await ApiService.deleteDocumentApi(id: id, isActive: isActive);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
// remove from local list immediately for better UX
|
|
||||||
documents.removeWhere((doc) => doc.id == id);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
errorMessage.value = "Failed to delete document";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
errorMessage.value = "Error deleting document: $e";
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch Documents for an entity
|
|
||||||
Future<void> fetchDocuments({
|
Future<void> fetchDocuments({
|
||||||
required String entityTypeId,
|
required String entityTypeId,
|
||||||
required String entityId,
|
required String entityId,
|
||||||
@ -120,20 +110,25 @@ class DocumentController extends GetxController {
|
|||||||
bool reset = false,
|
bool reset = false,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
|
// Reset pagination if needed
|
||||||
if (reset) {
|
if (reset) {
|
||||||
pageNumber.value = 1;
|
pageNumber.value = 1;
|
||||||
documents.clear();
|
documents.clear();
|
||||||
hasMore.value = true;
|
hasMore.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasMore.value) return;
|
// Don't fetch if no more data
|
||||||
|
if (!hasMore.value && !reset) return;
|
||||||
|
|
||||||
|
// Prevent duplicate requests
|
||||||
|
if (isLoading.value) return;
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getDocumentListApi(
|
final response = await ApiService.getDocumentListApi(
|
||||||
entityTypeId: entityTypeId,
|
entityTypeId: entityTypeId,
|
||||||
entityId: entityId,
|
entityId: entityId,
|
||||||
filter: filter ?? "",
|
filter: filter ?? '',
|
||||||
searchString: searchString ?? searchQuery.value,
|
searchString: searchString ?? searchQuery.value,
|
||||||
pageNumber: pageNumber.value,
|
pageNumber: pageNumber.value,
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
@ -147,19 +142,27 @@ class DocumentController extends GetxController {
|
|||||||
} else {
|
} else {
|
||||||
hasMore.value = false;
|
hasMore.value = false;
|
||||||
}
|
}
|
||||||
|
errorMessage.value = '';
|
||||||
} else {
|
} else {
|
||||||
errorMessage.value = response?.message ?? "Failed to fetch documents";
|
errorMessage.value = response?.message ?? 'Failed to fetch documents';
|
||||||
|
if (documents.isEmpty) {
|
||||||
|
_showError('Failed to load documents');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMessage.value = "Error fetching documents: $e";
|
errorMessage.value = 'Error fetching documents: $e';
|
||||||
|
if (documents.isEmpty) {
|
||||||
|
_showError('Error loading documents');
|
||||||
|
}
|
||||||
|
debugPrint('❌ Error fetching documents: $e');
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Helpers -----------------------
|
// ==================== Helper Methods ====================
|
||||||
|
|
||||||
/// Clear selected filters
|
/// Clear all selected filters
|
||||||
void clearFilters() {
|
void clearFilters() {
|
||||||
selectedUploadedBy.clear();
|
selectedUploadedBy.clear();
|
||||||
selectedCategory.clear();
|
selectedCategory.clear();
|
||||||
@ -171,11 +174,40 @@ class DocumentController extends GetxController {
|
|||||||
endDate.value = null;
|
endDate.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if any filters are active (for red dot indicator)
|
/// Check if any filters are active
|
||||||
bool hasActiveFilters() {
|
bool hasActiveFilters() {
|
||||||
return selectedUploadedBy.isNotEmpty ||
|
return selectedUploadedBy.isNotEmpty ||
|
||||||
selectedCategory.isNotEmpty ||
|
selectedCategory.isNotEmpty ||
|
||||||
selectedType.isNotEmpty ||
|
selectedType.isNotEmpty ||
|
||||||
selectedTag.isNotEmpty;
|
selectedTag.isNotEmpty ||
|
||||||
|
startDate.value != null ||
|
||||||
|
endDate.value != null ||
|
||||||
|
isVerified.value != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show error message
|
||||||
|
void _showError(String message) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
message,
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red.shade100,
|
||||||
|
colorText: Colors.red.shade900,
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
borderRadius: 8,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset controller state
|
||||||
|
void reset() {
|
||||||
|
documents.clear();
|
||||||
|
clearFilters();
|
||||||
|
searchController.clear();
|
||||||
|
searchQuery.value = '';
|
||||||
|
pageNumber.value = 1;
|
||||||
|
hasMore.value = true;
|
||||||
|
showInactive.value = false;
|
||||||
|
errorMessage.value = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import 'package:marco/helpers/widgets/my_text.dart';
|
|||||||
import 'package:marco/helpers/widgets/wave_background.dart';
|
import 'package:marco/helpers/widgets/wave_background.dart';
|
||||||
import 'package:marco/helpers/theme/admin_theme.dart';
|
import 'package:marco/helpers/theme/admin_theme.dart';
|
||||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
|
||||||
|
|
||||||
class ThemeOption {
|
class ThemeOption {
|
||||||
final String label;
|
final String label;
|
||||||
@ -106,20 +105,6 @@ class _ThemeEditorWidgetState extends State<ThemeEditorWidget> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
InkWell(
|
|
||||||
onTap: () {
|
|
||||||
ThemeCustomizer.setTheme(
|
|
||||||
ThemeCustomizer.instance.theme == ThemeMode.dark
|
|
||||||
? ThemeMode.light
|
|
||||||
: ThemeMode.dark);
|
|
||||||
},
|
|
||||||
child: Icon(
|
|
||||||
ThemeCustomizer.instance.theme == ThemeMode.dark
|
|
||||||
? LucideIcons.sun
|
|
||||||
: LucideIcons.moon,
|
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Theme cards wrapped in reactive Obx widget
|
// Theme cards wrapped in reactive Obx widget
|
||||||
Center(
|
Center(
|
||||||
child: Obx(
|
child: Obx(
|
||||||
|
|||||||
@ -62,6 +62,10 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
children: [
|
children: [
|
||||||
_buildDashboardStats(context),
|
_buildDashboardStats(context),
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
|
_buildAttendanceChartSection(),
|
||||||
|
MySpacing.height(24),
|
||||||
|
_buildProjectProgressChartSection(),
|
||||||
|
MySpacing.height(24),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: DashboardOverviewWidgets.teamsOverview(),
|
child: DashboardOverviewWidgets.teamsOverview(),
|
||||||
@ -71,10 +75,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: DashboardOverviewWidgets.tasksOverview(),
|
child: DashboardOverviewWidgets.tasksOverview(),
|
||||||
),
|
),
|
||||||
MySpacing.height(24),
|
|
||||||
_buildAttendanceChartSection(),
|
|
||||||
MySpacing.height(24),
|
|
||||||
_buildProjectProgressChartSection(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user