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:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/model/document/document_filter_model.dart';
|
||||
@ -8,11 +9,14 @@ class DocumentController extends GetxController {
|
||||
var isLoading = false.obs;
|
||||
var documents = <DocumentItem>[].obs;
|
||||
var filters = Rxn<DocumentFiltersData>();
|
||||
|
||||
// Selected filters
|
||||
var selectedFilter = "".obs;
|
||||
var selectedUploadedBy = "".obs;
|
||||
var selectedCategory = "".obs;
|
||||
var selectedType = "".obs;
|
||||
var selectedTag = "".obs;
|
||||
|
||||
// Pagination state
|
||||
var pageNumber = 1.obs;
|
||||
final int pageSize = 20;
|
||||
@ -21,6 +25,13 @@ class DocumentController extends GetxController {
|
||||
// Error message
|
||||
var errorMessage = "".obs;
|
||||
|
||||
// NEW: show inactive toggle
|
||||
var showInactive = false.obs;
|
||||
|
||||
// NEW: search
|
||||
var searchQuery = ''.obs;
|
||||
var searchController = TextEditingController();
|
||||
|
||||
// ------------------ API Calls -----------------------
|
||||
|
||||
/// 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
|
||||
Future<void> fetchDocuments({
|
||||
required String entityTypeId,
|
||||
required String entityId,
|
||||
String filter = "",
|
||||
String searchString = "",
|
||||
String? filter,
|
||||
String? searchString,
|
||||
bool reset = false,
|
||||
}) async {
|
||||
try {
|
||||
@ -63,10 +129,11 @@ class DocumentController extends GetxController {
|
||||
final response = await ApiService.getDocumentListApi(
|
||||
entityTypeId: entityTypeId,
|
||||
entityId: entityId,
|
||||
filter: filter,
|
||||
searchString: searchString,
|
||||
filter: filter ?? "",
|
||||
searchString: searchString ?? searchQuery.value,
|
||||
pageNumber: pageNumber.value,
|
||||
pageSize: pageSize,
|
||||
isActive: !showInactive.value, // 👈 active or inactive
|
||||
);
|
||||
|
||||
if (response != null && response.success) {
|
||||
@ -95,4 +162,12 @@ class DocumentController extends GetxController {
|
||||
selectedType.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 getDocumentVersion = "/document/get/version";
|
||||
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_version_model.dart';
|
||||
|
||||
|
||||
class ApiService {
|
||||
static const Duration timeout = Duration(seconds: 30);
|
||||
static const bool enableLogs = true;
|
||||
@ -247,6 +246,7 @@ class ApiService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get Pre-Signed URL for Old Version
|
||||
static Future<String?> getPresignedUrlApi(String versionId) async {
|
||||
final endpoint = "${ApiEndpoints.getDocumentVersion}/$versionId";
|
||||
@ -268,22 +268,138 @@ class ApiService {
|
||||
return jsonResponse['data'] as String?;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
logSafe("Exception during getPresignedUrlApi: $e",
|
||||
level: LogLevel.error);
|
||||
logSafe("Exception during getPresignedUrlApi: $e", level: LogLevel.error);
|
||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||
}
|
||||
|
||||
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
|
||||
static Future<DocumentVersionsResponse?> getDocumentVersionsApi({
|
||||
required String parentAttachmentId,
|
||||
int pageNumber = 1,
|
||||
int pageSize = 20,
|
||||
}) async {
|
||||
final endpoint =
|
||||
"${ApiEndpoints.getDocumentVersions}/$parentAttachmentId";
|
||||
final endpoint = "${ApiEndpoints.getDocumentVersions}/$parentAttachmentId";
|
||||
final queryParams = {
|
||||
"pageNumber": pageNumber.toString(),
|
||||
"pageSize": pageSize.toString(),
|
||||
@ -293,8 +409,7 @@ class ApiService {
|
||||
"Fetching document versions for parentAttachmentId: $parentAttachmentId");
|
||||
|
||||
try {
|
||||
final response =
|
||||
await _getRequest(endpoint, queryParams: queryParams);
|
||||
final response = await _getRequest(endpoint, queryParams: queryParams);
|
||||
|
||||
if (response == null) {
|
||||
logSafe("Document versions request failed: null response",
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
|
||||
|
||||
class ConfirmDialog extends StatelessWidget {
|
||||
final String title;
|
||||
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 {
|
||||
final String id;
|
||||
final String firstName;
|
||||
|
@ -73,7 +73,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
onRefresh: _onRefresh,
|
||||
child: SingleChildScrollView(
|
||||
physics:
|
||||
const AlwaysScrollableScrollPhysics(), // ensures pull works
|
||||
const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -117,30 +117,43 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
// Header with Edit button
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: Colors.blue.shade50,
|
||||
radius: 28,
|
||||
child:
|
||||
const Icon(Icons.description, color: Colors.blue, size: 28),
|
||||
),
|
||||
MySpacing.width(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: Row(
|
||||
children: [
|
||||
MyText.titleLarge(doc.name,
|
||||
fontWeight: 700, color: Colors.black),
|
||||
MyText.bodySmall(
|
||||
doc.documentType.name,
|
||||
color: Colors.blueGrey,
|
||||
fontWeight: 600,
|
||||
CircleAvatar(
|
||||
backgroundColor: Colors.blue.shade50,
|
||||
radius: 28,
|
||||
child: const Icon(Icons.description,
|
||||
color: Colors.blue, size: 28),
|
||||
),
|
||||
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),
|
||||
@ -272,16 +285,15 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
}
|
||||
|
||||
Future<void> _openDocument(String url) async {
|
||||
final uri = Uri.parse(url);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Could not open document",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
final uri = Uri.parse(url);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Could not open document",
|
||||
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/view/document/document_details_page.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 {
|
||||
final String? entityId;
|
||||
@ -137,9 +139,85 @@ class _UserDocumentsPageState extends State<UserDocumentsPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_forward_ios, color: Colors.black54),
|
||||
onPressed: () {},
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert, color: Colors.black54),
|
||||
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) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
alignment: Alignment.centerRight,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.tune, color: Colors.black),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => UserDocumentFilterBottomSheet(
|
||||
entityId: resolvedEntityId,
|
||||
entityTypeId: entityTypeId,
|
||||
return Padding(
|
||||
padding: MySpacing.xy(8, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
// 🔍 Search Bar
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 35,
|
||||
child: TextField(
|
||||
controller: docController.searchController,
|
||||
onChanged: (value) {
|
||||
docController.searchQuery.value = value;
|
||||
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) {
|
||||
return Obx(() {
|
||||
if (docController.isLoading.value && docController.documents.isEmpty) {
|
||||
@ -206,6 +468,10 @@ class _UserDocumentsPageState extends State<UserDocumentsPage> {
|
||||
child: Column(
|
||||
children: [
|
||||
_buildFilterRow(context),
|
||||
|
||||
// 👇 Add this
|
||||
_buildStatusHeader(),
|
||||
|
||||
Expanded(
|
||||
child: MyRefreshIndicator(
|
||||
onRefresh: () async {
|
||||
|
Loading…
x
Reference in New Issue
Block a user