import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/document/user_document_controller.dart'; import 'package:marco/controller/project_controller.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/model/document/user_document_filter_bottom_sheet.dart'; import 'package:marco/model/document/documents_list_model.dart'; import 'package:intl/intl.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; 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'; import 'package:marco/controller/permission_controller.dart'; import 'package:marco/controller/document/document_details_controller.dart'; import 'dart:convert'; class UserDocumentsPage extends StatefulWidget { final String? entityId; final bool isEmployee; const UserDocumentsPage({ super.key, this.entityId, this.isEmployee = false, }); @override State createState() => _UserDocumentsPageState(); } class _UserDocumentsPageState extends State { final DocumentController docController = Get.put(DocumentController()); final PermissionController permissionController = Get.find(); final DocumentDetailsController controller = Get.put(DocumentDetailsController()); String get entityTypeId => widget.isEmployee ? Permissions.employeeEntity : Permissions.projectEntity; String get resolvedEntityId => widget.isEmployee ? widget.entityId ?? "" : Get.find().selectedProject?.id ?? ""; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { docController.fetchFilters(entityTypeId); docController.fetchDocuments( entityTypeId: entityTypeId, entityId: resolvedEntityId, reset: true, ); }); } @override void dispose() { docController.documents.clear(); super.dispose(); } Widget _buildDocumentTile(DocumentItem doc) { final uploadDate = DateFormat("dd MMM yyyy").format(doc.uploadedAt.toLocal()); final uploader = doc.uploadedBy.firstName.isNotEmpty ? "Added by ${doc.uploadedBy.firstName} ${doc.uploadedBy.lastName}" .trim() : "Added by you"; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: MyText.bodySmall( uploadDate, fontSize: 13, fontWeight: 500, color: Colors.grey, ), ), InkWell( onTap: () { // 👉 Navigate to details page Get.to(() => DocumentDetailsPage(documentId: doc.id)); }, child: Container( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(5), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(5), ), child: const Icon(Icons.description, color: Colors.blue), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodySmall( doc.documentType.name, fontSize: 13, fontWeight: 600, color: Colors.grey, ), MySpacing.height(2), MyText.bodyMedium( doc.name, fontSize: 15, fontWeight: 600, color: Colors.black, ), MySpacing.height(2), MyText.bodySmall( uploader, fontSize: 13, color: Colors.grey, ), ], ), ), PopupMenuButton( icon: const Icon(Icons.more_vert, color: Colors.black54), onSelected: (value) async { if (value == "delete") { // existing delete flow (unchanged) final result = await showDialog( 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 == "restore") { // existing activate flow (unchanged) final success = await docController.toggleDocumentActive( doc.id, isActive: true, entityTypeId: entityTypeId, entityId: resolvedEntityId, ); if (success) { showAppSnackbar( title: "Restored", message: "Document reastored successfully", type: SnackbarType.success, ); } else { showAppSnackbar( title: "Error", message: "Failed to restore document", type: SnackbarType.error, ); } } }, itemBuilder: (context) => [ if (doc.isActive && permissionController .hasPermission(Permissions.deleteDocument)) const PopupMenuItem( value: "delete", child: Text("Delete"), ) else if (!doc.isActive && permissionController .hasPermission(Permissions.modifyDocument)) const PopupMenuItem( value: "restore", child: Text("Restore"), ), ], ), ], ), ), ), ], ); } Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.inbox_outlined, size: 60, color: Colors.grey), MySpacing.height(18), MyText.titleMedium( 'No documents found.', fontWeight: 600, color: Colors.grey, ), MySpacing.height(10), MyText.bodySmall( 'Try adjusting your filters or refresh to reload.', color: Colors.grey, ), ], ), ); } Widget _buildFilterRow(BuildContext context) { 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( 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(5), borderSide: BorderSide(color: Colors.grey.shade300), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(5), 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(5), ), 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(5)), ), 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(5), ), child: PopupMenuButton( padding: EdgeInsets.zero, icon: const Icon(Icons.more_vert, size: 20, color: Colors.black87), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), itemBuilder: (context) => [ const PopupMenuItem( enabled: false, height: 30, child: Text( "Preferences", style: TextStyle( fontWeight: FontWeight.bold, color: Colors.grey, ), ), ), PopupMenuItem( 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 Deleted Documents')), 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; if (!isInactive) return const SizedBox.shrink(); // hide when active return Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), color: Colors.red.shade50, child: Row( children: [ Icon( Icons.visibility_off, color: Colors.red, size: 18, ), const SizedBox(width: 8), Text( "Showing Deleted Documents", style: TextStyle( color: Colors.red, fontWeight: FontWeight.w600, ), ), ], ), ); }); } Widget _buildBody(BuildContext context) { // 🔒 Check for viewDocument permission if (!permissionController.hasPermission(Permissions.viewDocument)) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.lock_outline, size: 60, color: Colors.grey), MySpacing.height(18), MyText.titleMedium( 'Access Denied', fontWeight: 600, color: Colors.grey, ), MySpacing.height(10), MyText.bodySmall( 'You do not have permission to view documents.', color: Colors.grey, ), ], ), ); } return Obx(() { if (docController.isLoading.value && docController.documents.isEmpty) { return SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), child: SkeletonLoaders.documentSkeletonLoader(), ); } final docs = docController.documents; return SafeArea( child: Column( children: [ _buildFilterRow(context), _buildStatusHeader(), Expanded( child: MyRefreshIndicator( onRefresh: () async { final combinedFilter = { 'uploadedByIds': docController.selectedUploadedBy.toList(), 'documentCategoryIds': docController.selectedCategory.toList(), 'documentTypeIds': docController.selectedType.toList(), 'documentTagIds': docController.selectedTag.toList(), }; await docController.fetchDocuments( entityTypeId: entityTypeId, entityId: resolvedEntityId, filter: jsonEncode(combinedFilter), reset: true, ); }, child: ListView( physics: const AlwaysScrollableScrollPhysics(), padding: docs.isEmpty ? null : const EdgeInsets.fromLTRB(0, 0, 0, 80), children: docs.isEmpty ? [ SizedBox( height: MediaQuery.of(context).size.height * 0.6, child: _buildEmptyState(), ), ] : [ ...docs.map(_buildDocumentTile), if (docController.isLoading.value) const Padding( padding: EdgeInsets.all(12), child: Center(child: CircularProgressIndicator()), ), if (!docController.hasMore.value) Padding( padding: const EdgeInsets.all(12), child: Center( child: MyText.bodySmall( "No more documents", color: Colors.grey, ), ), ), ], ), ), ), ], ), ); }); } @override Widget build(BuildContext context) { final bool showAppBar = !widget.isEmployee; return Scaffold( backgroundColor: const Color(0xFFF1F1F1), appBar: showAppBar ? CustomAppBar( title: 'Documents', onBackPressed: () { Get.back(); }, ) : null, body: _buildBody(context), floatingActionButton: permissionController .hasPermission(Permissions.uploadDocument) ? FloatingActionButton.extended( onPressed: () { final uploadController = Get.put(DocumentUploadController()); showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => DocumentUploadBottomSheet( isEmployee: widget.isEmployee, onSubmit: (data) async { final success = await uploadController.uploadDocument( name: data["name"], description: data["description"], documentId: data["documentId"], entityId: resolvedEntityId, documentTypeId: data["documentTypeId"], fileName: data["attachment"]["fileName"], base64Data: data["attachment"]["base64Data"], contentType: data["attachment"]["contentType"], fileSize: data["attachment"]["fileSize"], ); if (success) { Navigator.pop(context); docController.fetchDocuments( entityTypeId: entityTypeId, entityId: resolvedEntityId, reset: true, ); } else { Get.snackbar( "Error", "Upload failed, please try again"); } }, ), ); }, icon: const Icon(Icons.add, color: Colors.white), label: MyText.bodyMedium( "Add Document", color: Colors.white, fontWeight: 600, ), backgroundColor: Colors.red, ) : null, floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, ); } }