feat: Enhance document management with delete/activate functionality, search, and inactive toggle
This commit is contained in:
parent
2133dedfae
commit
e12e5ab13b
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/document/document_filter_model.dart';
|
import 'package:marco/model/document/document_filter_model.dart';
|
||||||
@ -8,11 +9,14 @@ class DocumentController extends GetxController {
|
|||||||
var isLoading = false.obs;
|
var isLoading = false.obs;
|
||||||
var documents = <DocumentItem>[].obs;
|
var documents = <DocumentItem>[].obs;
|
||||||
var filters = Rxn<DocumentFiltersData>();
|
var filters = Rxn<DocumentFiltersData>();
|
||||||
|
|
||||||
|
// Selected filters
|
||||||
var selectedFilter = "".obs;
|
var selectedFilter = "".obs;
|
||||||
var selectedUploadedBy = "".obs;
|
var selectedUploadedBy = "".obs;
|
||||||
var selectedCategory = "".obs;
|
var selectedCategory = "".obs;
|
||||||
var selectedType = "".obs;
|
var selectedType = "".obs;
|
||||||
var selectedTag = "".obs;
|
var selectedTag = "".obs;
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
var pageNumber = 1.obs;
|
var pageNumber = 1.obs;
|
||||||
final int pageSize = 20;
|
final int pageSize = 20;
|
||||||
@ -21,6 +25,13 @@ class DocumentController extends GetxController {
|
|||||||
// Error message
|
// Error message
|
||||||
var errorMessage = "".obs;
|
var errorMessage = "".obs;
|
||||||
|
|
||||||
|
// NEW: show inactive toggle
|
||||||
|
var showInactive = false.obs;
|
||||||
|
|
||||||
|
// NEW: search
|
||||||
|
var searchQuery = ''.obs;
|
||||||
|
var searchController = TextEditingController();
|
||||||
|
|
||||||
// ------------------ API Calls -----------------------
|
// ------------------ API Calls -----------------------
|
||||||
|
|
||||||
/// Fetch Document Filters for an Entity
|
/// Fetch Document Filters for an Entity
|
||||||
@ -41,12 +52,67 @@ class DocumentController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle document active/inactive state
|
||||||
|
Future<bool> toggleDocumentActive(
|
||||||
|
String id, {
|
||||||
|
required bool isActive,
|
||||||
|
required String entityTypeId,
|
||||||
|
required String entityId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
final success =
|
||||||
|
await ApiService.deleteDocumentApi(id: id, isActive: isActive);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// 🔥 Always fetch fresh list after toggle
|
||||||
|
await fetchDocuments(
|
||||||
|
entityTypeId: entityTypeId,
|
||||||
|
entityId: entityId,
|
||||||
|
reset: true,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
errorMessage.value = "Failed to update document state";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
errorMessage.value = "Error updating document: $e";
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Permanently delete a document (or deactivate depending on API)
|
||||||
|
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
|
/// Fetch Documents for an entity
|
||||||
Future<void> fetchDocuments({
|
Future<void> fetchDocuments({
|
||||||
required String entityTypeId,
|
required String entityTypeId,
|
||||||
required String entityId,
|
required String entityId,
|
||||||
String filter = "",
|
String? filter,
|
||||||
String searchString = "",
|
String? searchString,
|
||||||
bool reset = false,
|
bool reset = false,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
@ -63,10 +129,11 @@ class DocumentController extends GetxController {
|
|||||||
final response = await ApiService.getDocumentListApi(
|
final response = await ApiService.getDocumentListApi(
|
||||||
entityTypeId: entityTypeId,
|
entityTypeId: entityTypeId,
|
||||||
entityId: entityId,
|
entityId: entityId,
|
||||||
filter: filter,
|
filter: filter ?? "",
|
||||||
searchString: searchString,
|
searchString: searchString ?? searchQuery.value,
|
||||||
pageNumber: pageNumber.value,
|
pageNumber: pageNumber.value,
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
|
isActive: !showInactive.value, // 👈 active or inactive
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response != null && response.success) {
|
if (response != null && response.success) {
|
||||||
@ -95,4 +162,12 @@ class DocumentController extends GetxController {
|
|||||||
selectedType.value = "";
|
selectedType.value = "";
|
||||||
selectedTag.value = "";
|
selectedTag.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if any filters are active (for red dot indicator)
|
||||||
|
bool hasActiveFilters() {
|
||||||
|
return selectedUploadedBy.value.isNotEmpty ||
|
||||||
|
selectedCategory.value.isNotEmpty ||
|
||||||
|
selectedType.value.isNotEmpty ||
|
||||||
|
selectedTag.value.isNotEmpty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,4 +84,5 @@ class ApiEndpoints {
|
|||||||
static const String getDocumentTypesByCategory = "/master/document-type/list";
|
static const String getDocumentTypesByCategory = "/master/document-type/list";
|
||||||
static const String getDocumentVersion = "/document/get/version";
|
static const String getDocumentVersion = "/document/get/version";
|
||||||
static const String getDocumentVersions = "/document/list/versions";
|
static const String getDocumentVersions = "/document/list/versions";
|
||||||
|
static const String editDocument = "/document/edit";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import 'package:marco/model/document/master_document_type_model.dart';
|
|||||||
import 'package:marco/model/document/document_details_model.dart';
|
import 'package:marco/model/document/document_details_model.dart';
|
||||||
import 'package:marco/model/document/document_version_model.dart';
|
import 'package:marco/model/document/document_version_model.dart';
|
||||||
|
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
static const Duration timeout = Duration(seconds: 30);
|
static const Duration timeout = Duration(seconds: 30);
|
||||||
static const bool enableLogs = true;
|
static const bool enableLogs = true;
|
||||||
@ -247,6 +246,7 @@ class ApiService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get Pre-Signed URL for Old Version
|
/// Get Pre-Signed URL for Old Version
|
||||||
static Future<String?> getPresignedUrlApi(String versionId) async {
|
static Future<String?> getPresignedUrlApi(String versionId) async {
|
||||||
final endpoint = "${ApiEndpoints.getDocumentVersion}/$versionId";
|
final endpoint = "${ApiEndpoints.getDocumentVersion}/$versionId";
|
||||||
@ -268,22 +268,138 @@ class ApiService {
|
|||||||
return jsonResponse['data'] as String?;
|
return jsonResponse['data'] as String?;
|
||||||
}
|
}
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
logSafe("Exception during getPresignedUrlApi: $e",
|
logSafe("Exception during getPresignedUrlApi: $e", level: LogLevel.error);
|
||||||
level: LogLevel.error);
|
|
||||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete (Soft Delete / Deactivate) Document API
|
||||||
|
static Future<bool> deleteDocumentApi({
|
||||||
|
required String id,
|
||||||
|
bool isActive = false, // default false = delete
|
||||||
|
}) async {
|
||||||
|
final endpoint = "${ApiEndpoints.deleteDocument}/$id";
|
||||||
|
final queryParams = {"isActive": isActive.toString()};
|
||||||
|
logSafe("Deleting document with id: $id | isActive: $isActive");
|
||||||
|
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint")
|
||||||
|
.replace(queryParameters: queryParams);
|
||||||
|
|
||||||
|
String? token = await _getToken();
|
||||||
|
if (token == null) return false;
|
||||||
|
|
||||||
|
final headers = _headers(token);
|
||||||
|
logSafe("DELETE (PUT/POST style) $uri\nHeaders: $headers");
|
||||||
|
|
||||||
|
// some backends use PUT instead of DELETE for soft deletes
|
||||||
|
final response =
|
||||||
|
await http.delete(uri, headers: headers).timeout(extendedTimeout);
|
||||||
|
|
||||||
|
if (response.statusCode == 401) {
|
||||||
|
logSafe("Unauthorized DELETE. Attempting token refresh...");
|
||||||
|
if (await AuthService.refreshToken()) {
|
||||||
|
return await deleteDocumentApi(id: id, isActive: isActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logSafe("Delete document response status: ${response.statusCode}");
|
||||||
|
logSafe("Delete document response body: ${response.body}");
|
||||||
|
|
||||||
|
final json = jsonDecode(response.body);
|
||||||
|
if (json['success'] == true) {
|
||||||
|
logSafe("Document delete/update success: ${json['data']}");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
logSafe(
|
||||||
|
"Failed to delete document: ${json['message'] ?? 'Unknown error'}",
|
||||||
|
level: LogLevel.warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
logSafe("Exception during deleteDocumentApi: $e", level: LogLevel.error);
|
||||||
|
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Edit Document API
|
||||||
|
static Future<bool> editDocumentApi({
|
||||||
|
required String id,
|
||||||
|
required String name,
|
||||||
|
required String documentId,
|
||||||
|
String? description,
|
||||||
|
required String fileName,
|
||||||
|
required String base64Data,
|
||||||
|
required String contentType,
|
||||||
|
required int fileSize,
|
||||||
|
String? fileDescription,
|
||||||
|
bool isActive = true,
|
||||||
|
List<Map<String, dynamic>> tags = const [],
|
||||||
|
}) async {
|
||||||
|
final endpoint = "${ApiEndpoints.editDocument}/$id";
|
||||||
|
logSafe("Editing document with id: $id");
|
||||||
|
|
||||||
|
final Map<String, dynamic> payload = {
|
||||||
|
"id": id,
|
||||||
|
"name": name,
|
||||||
|
"documentId": documentId,
|
||||||
|
"description": description ?? "",
|
||||||
|
"attachment": {
|
||||||
|
"fileName": fileName,
|
||||||
|
"base64Data": base64Data,
|
||||||
|
"contentType": contentType,
|
||||||
|
"fileSize": fileSize,
|
||||||
|
"description": fileDescription ?? "",
|
||||||
|
"isActive": isActive,
|
||||||
|
},
|
||||||
|
"tags": tags.isNotEmpty
|
||||||
|
? tags
|
||||||
|
: [
|
||||||
|
{"name": "default", "isActive": true}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response =
|
||||||
|
await _putRequest(endpoint, payload, customTimeout: extendedTimeout);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
logSafe("Edit document failed: null response", level: LogLevel.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logSafe("Edit document response status: ${response.statusCode}");
|
||||||
|
logSafe("Edit document response body: ${response.body}");
|
||||||
|
|
||||||
|
final json = jsonDecode(response.body);
|
||||||
|
if (json['success'] == true) {
|
||||||
|
logSafe("Document edited successfully: ${json['data']}");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
logSafe(
|
||||||
|
"Failed to edit document: ${json['message'] ?? 'Unknown error'}",
|
||||||
|
level: LogLevel.warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
logSafe("Exception during editDocumentApi: $e", level: LogLevel.error);
|
||||||
|
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// Get List of Versions by ParentAttachmentId
|
/// Get List of Versions by ParentAttachmentId
|
||||||
static Future<DocumentVersionsResponse?> getDocumentVersionsApi({
|
static Future<DocumentVersionsResponse?> getDocumentVersionsApi({
|
||||||
required String parentAttachmentId,
|
required String parentAttachmentId,
|
||||||
int pageNumber = 1,
|
int pageNumber = 1,
|
||||||
int pageSize = 20,
|
int pageSize = 20,
|
||||||
}) async {
|
}) async {
|
||||||
final endpoint =
|
final endpoint = "${ApiEndpoints.getDocumentVersions}/$parentAttachmentId";
|
||||||
"${ApiEndpoints.getDocumentVersions}/$parentAttachmentId";
|
|
||||||
final queryParams = {
|
final queryParams = {
|
||||||
"pageNumber": pageNumber.toString(),
|
"pageNumber": pageNumber.toString(),
|
||||||
"pageSize": pageSize.toString(),
|
"pageSize": pageSize.toString(),
|
||||||
@ -293,8 +409,7 @@ class ApiService {
|
|||||||
"Fetching document versions for parentAttachmentId: $parentAttachmentId");
|
"Fetching document versions for parentAttachmentId: $parentAttachmentId");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response =
|
final response = await _getRequest(endpoint, queryParams: queryParams);
|
||||||
await _getRequest(endpoint, queryParams: queryParams);
|
|
||||||
|
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
logSafe("Document versions request failed: null response",
|
logSafe("Document versions request failed: null response",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
|
|
||||||
|
|
||||||
class ConfirmDialog extends StatelessWidget {
|
class ConfirmDialog extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final String message;
|
final String message;
|
||||||
|
|||||||
@ -142,6 +142,27 @@ class DocumentItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension DocumentItemCopy on DocumentItem {
|
||||||
|
DocumentItem copyWith({
|
||||||
|
bool? isActive,
|
||||||
|
}) {
|
||||||
|
return DocumentItem(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
documentId: documentId,
|
||||||
|
description: description,
|
||||||
|
uploadedAt: uploadedAt,
|
||||||
|
parentAttachmentId: parentAttachmentId,
|
||||||
|
isCurrentVersion: isCurrentVersion,
|
||||||
|
version: version,
|
||||||
|
isActive: isActive ?? this.isActive,
|
||||||
|
isVerified: isVerified,
|
||||||
|
uploadedBy: uploadedBy,
|
||||||
|
documentType: documentType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class UploadedBy {
|
class UploadedBy {
|
||||||
final String id;
|
final String id;
|
||||||
final String firstName;
|
final String firstName;
|
||||||
|
|||||||
@ -73,7 +73,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
onRefresh: _onRefresh,
|
onRefresh: _onRefresh,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
physics:
|
physics:
|
||||||
const AlwaysScrollableScrollPhysics(), // ensures pull works
|
const AlwaysScrollableScrollPhysics(),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -117,30 +117,43 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Header
|
// Header with Edit button
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
|
||||||
backgroundColor: Colors.blue.shade50,
|
|
||||||
radius: 28,
|
|
||||||
child:
|
|
||||||
const Icon(Icons.description, color: Colors.blue, size: 28),
|
|
||||||
),
|
|
||||||
MySpacing.width(16),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
MyText.titleLarge(doc.name,
|
CircleAvatar(
|
||||||
fontWeight: 700, color: Colors.black),
|
backgroundColor: Colors.blue.shade50,
|
||||||
MyText.bodySmall(
|
radius: 28,
|
||||||
doc.documentType.name,
|
child: const Icon(Icons.description,
|
||||||
color: Colors.blueGrey,
|
color: Colors.blue, size: 28),
|
||||||
fontWeight: 600,
|
),
|
||||||
|
MySpacing.width(16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
MyText.titleLarge(doc.name,
|
||||||
|
fontWeight: 700, color: Colors.black),
|
||||||
|
MyText.bodySmall(
|
||||||
|
doc.documentType.name,
|
||||||
|
color: Colors.blueGrey,
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit, color: Colors.red),
|
||||||
|
onPressed: () {
|
||||||
|
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
@ -272,16 +285,15 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openDocument(String url) async {
|
Future<void> _openDocument(String url) async {
|
||||||
final uri = Uri.parse(url);
|
final uri = Uri.parse(url);
|
||||||
if (await canLaunchUrl(uri)) {
|
if (await canLaunchUrl(uri)) {
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
} else {
|
} else {
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Could not open document",
|
message: "Could not open document",
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import 'package:marco/model/document/document_upload_bottom_sheet.dart';
|
|||||||
import 'package:marco/controller/document/document_upload_controller.dart';
|
import 'package:marco/controller/document/document_upload_controller.dart';
|
||||||
import 'package:marco/view/document/document_details_page.dart';
|
import 'package:marco/view/document/document_details_page.dart';
|
||||||
import 'package:marco/helpers/widgets/custom_app_bar.dart';
|
import 'package:marco/helpers/widgets/custom_app_bar.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_confirmation_dialog.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
|
|
||||||
class UserDocumentsPage extends StatefulWidget {
|
class UserDocumentsPage extends StatefulWidget {
|
||||||
final String? entityId;
|
final String? entityId;
|
||||||
@ -137,9 +139,85 @@ class _UserDocumentsPageState extends State<UserDocumentsPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
PopupMenuButton<String>(
|
||||||
icon: const Icon(Icons.arrow_forward_ios, color: Colors.black54),
|
icon: const Icon(Icons.more_vert, color: Colors.black54),
|
||||||
onPressed: () {},
|
onSelected: (value) async {
|
||||||
|
if (value == "delete") {
|
||||||
|
final result = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => ConfirmDialog(
|
||||||
|
title: "Delete Document",
|
||||||
|
message:
|
||||||
|
"Are you sure you want to delete \"${doc.name}\"?\nThis action cannot be undone.",
|
||||||
|
confirmText: "Delete",
|
||||||
|
cancelText: "Cancel",
|
||||||
|
icon: Icons.delete_forever,
|
||||||
|
confirmColor: Colors.redAccent,
|
||||||
|
onConfirm: () async {
|
||||||
|
final success =
|
||||||
|
await docController.toggleDocumentActive(
|
||||||
|
doc.id,
|
||||||
|
isActive: false,
|
||||||
|
entityTypeId: entityTypeId,
|
||||||
|
entityId: resolvedEntityId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Deleted",
|
||||||
|
message: "Document deleted successfully",
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Error",
|
||||||
|
message: "Failed to delete document",
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
|
throw Exception(
|
||||||
|
"Failed to delete"); // keep dialog open
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (result == true) {
|
||||||
|
debugPrint("✅ Document deleted and removed from list");
|
||||||
|
}
|
||||||
|
} else if (value == "activate") {
|
||||||
|
final success = await docController.toggleDocumentActive(
|
||||||
|
doc.id,
|
||||||
|
isActive: true,
|
||||||
|
entityTypeId: entityTypeId,
|
||||||
|
entityId: resolvedEntityId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Reactivated",
|
||||||
|
message: "Document reactivated successfully",
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Error",
|
||||||
|
message: "Failed to reactivate document",
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
if (doc.isActive)
|
||||||
|
const PopupMenuItem(
|
||||||
|
value: "delete",
|
||||||
|
child: Text("Delete"),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const PopupMenuItem(
|
||||||
|
value: "activate",
|
||||||
|
child: Text("Activate"),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -172,26 +250,210 @@ class _UserDocumentsPageState extends State<UserDocumentsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFilterRow(BuildContext context) {
|
Widget _buildFilterRow(BuildContext context) {
|
||||||
return Container(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
padding: MySpacing.xy(8, 8),
|
||||||
alignment: Alignment.centerRight,
|
child: Row(
|
||||||
child: IconButton(
|
children: [
|
||||||
icon: const Icon(Icons.tune, color: Colors.black),
|
// 🔍 Search Bar
|
||||||
onPressed: () {
|
Expanded(
|
||||||
showModalBottomSheet(
|
child: SizedBox(
|
||||||
context: context,
|
height: 35,
|
||||||
isScrollControlled: true,
|
child: TextField(
|
||||||
backgroundColor: Colors.transparent,
|
controller: docController.searchController,
|
||||||
builder: (_) => UserDocumentFilterBottomSheet(
|
onChanged: (value) {
|
||||||
entityId: resolvedEntityId,
|
docController.searchQuery.value = value;
|
||||||
entityTypeId: entityTypeId,
|
docController.fetchDocuments(
|
||||||
|
entityTypeId: entityTypeId,
|
||||||
|
entityId: resolvedEntityId,
|
||||||
|
reset: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
prefixIcon:
|
||||||
|
const Icon(Icons.search, size: 20, color: Colors.grey),
|
||||||
|
suffixIcon: ValueListenableBuilder<TextEditingValue>(
|
||||||
|
valueListenable: docController.searchController,
|
||||||
|
builder: (context, value, _) {
|
||||||
|
if (value.text.isEmpty) return const SizedBox.shrink();
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.clear,
|
||||||
|
size: 20, color: Colors.grey),
|
||||||
|
onPressed: () {
|
||||||
|
docController.searchController.clear();
|
||||||
|
docController.searchQuery.value = '';
|
||||||
|
docController.fetchDocuments(
|
||||||
|
entityTypeId: entityTypeId,
|
||||||
|
entityId: resolvedEntityId,
|
||||||
|
reset: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
hintText: 'Search documents...',
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
MySpacing.width(8),
|
||||||
|
|
||||||
|
// 🛠️ Filter Icon with indicator
|
||||||
|
Obx(() {
|
||||||
|
final isFilterActive = docController.hasActiveFilters();
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 35,
|
||||||
|
width: 35,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: BoxConstraints(),
|
||||||
|
icon: Icon(
|
||||||
|
Icons.tune,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
builder: (_) => UserDocumentFilterBottomSheet(
|
||||||
|
entityId: resolvedEntityId,
|
||||||
|
entityTypeId: entityTypeId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isFilterActive)
|
||||||
|
Positioned(
|
||||||
|
top: 6,
|
||||||
|
right: 6,
|
||||||
|
child: Container(
|
||||||
|
height: 8,
|
||||||
|
width: 8,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.red,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
MySpacing.width(10),
|
||||||
|
|
||||||
|
// ⋮ Menu (Show Inactive toggle)
|
||||||
|
Container(
|
||||||
|
height: 35,
|
||||||
|
width: 35,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: PopupMenuButton<int>(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon:
|
||||||
|
const Icon(Icons.more_vert, size: 20, color: Colors.black87),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
const PopupMenuItem<int>(
|
||||||
|
enabled: false,
|
||||||
|
height: 30,
|
||||||
|
child: Text(
|
||||||
|
"Preferences",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem<int>(
|
||||||
|
value: 0,
|
||||||
|
enabled: false,
|
||||||
|
child: Obx(() => Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.visibility_off_outlined,
|
||||||
|
size: 20, color: Colors.black87),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
const Expanded(child: Text('Show Inactive')),
|
||||||
|
Switch.adaptive(
|
||||||
|
value: docController.showInactive.value,
|
||||||
|
activeColor: Colors.indigo,
|
||||||
|
onChanged: (val) {
|
||||||
|
docController.showInactive.value = val;
|
||||||
|
docController.fetchDocuments(
|
||||||
|
entityTypeId: entityTypeId,
|
||||||
|
entityId: resolvedEntityId,
|
||||||
|
reset: true,
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusHeader() {
|
||||||
|
return Obx(() {
|
||||||
|
final isInactive = docController.showInactive.value;
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
|
color: isInactive ? Colors.red.shade50 : Colors.green.shade50,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isInactive ? Icons.visibility_off : Icons.check_circle,
|
||||||
|
color: isInactive ? Colors.red : Colors.green,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
isInactive
|
||||||
|
? "Showing Inactive Documents"
|
||||||
|
: "Showing Active Documents",
|
||||||
|
style: TextStyle(
|
||||||
|
color: isInactive ? Colors.red : Colors.green,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildBody(BuildContext context) {
|
Widget _buildBody(BuildContext context) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
if (docController.isLoading.value && docController.documents.isEmpty) {
|
if (docController.isLoading.value && docController.documents.isEmpty) {
|
||||||
@ -206,6 +468,10 @@ class _UserDocumentsPageState extends State<UserDocumentsPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildFilterRow(context),
|
_buildFilterRow(context),
|
||||||
|
|
||||||
|
// 👇 Add this
|
||||||
|
_buildStatusHeader(),
|
||||||
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: MyRefreshIndicator(
|
child: MyRefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user