import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/model/document/document_details_model.dart'; import 'package:marco/controller/document/document_details_controller.dart'; import 'package:marco/helpers/widgets/custom_app_bar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/model/document/document_edit_bottom_sheet.dart'; import 'package:marco/controller/permission_controller.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; class DocumentDetailsPage extends StatefulWidget { final String documentId; const DocumentDetailsPage({super.key, required this.documentId}); @override State createState() => _DocumentDetailsPageState(); } class _DocumentDetailsPageState extends State with UIMixin { final DocumentDetailsController controller = Get.find(); final PermissionController permissionController = Get.find(); @override void initState() { super.initState(); _fetchDetails(); } Future _fetchDetails() async { await controller.fetchDocumentDetails(widget.documentId); final parentId = controller.documentDetails.value?.data?.parentAttachmentId; if (parentId != null && parentId.isNotEmpty) { await controller.fetchDocumentVersions(parentId); } } Future _onRefresh() async { await _fetchDetails(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF1F1F1), appBar: CustomAppBar( title: 'Document Details', onBackPressed: () { Get.back(); }, ), body: Obx(() { if (controller.isLoading.value) { return SkeletonLoaders.documentDetailsSkeletonLoader(); } final docResponse = controller.documentDetails.value; if (docResponse == null || docResponse.data == null) { return Center( child: MyText.bodyMedium( "Failed to load document details.", color: Colors.grey, ), ); } final doc = docResponse.data!; return MyRefreshIndicator( onRefresh: _onRefresh, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildDetailsCard(doc), const SizedBox(height: 20), MyText.titleMedium("Versions", fontWeight: 700, color: Colors.black), const SizedBox(height: 10), _buildVersionsSection(), ], ), ), ); }), ); } /// ---------------- DOCUMENT DETAILS CARD ---------------- Widget _buildDetailsCard(DocumentDetails doc) { final uploadDate = DateFormat("dd MMM yyyy, hh:mm a").format(doc.uploadedAt.toLocal()); final updateDate = doc.updatedAt != null ? DateFormat("dd MMM yyyy, hh:mm a").format(doc.updatedAt!.toLocal()) : "-"; return Container( constraints: const BoxConstraints(maxWidth: 460), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(5), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.06), blurRadius: 16, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header with Edit button Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Row( 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, children: [ MyText.titleLarge(doc.name, fontWeight: 700, color: Colors.black), MyText.bodySmall( doc.documentType.name, color: Colors.blueGrey, fontWeight: 600, ), ], ), ), ], ), ), if (permissionController .hasPermission(Permissions.modifyDocument)) IconButton( icon: Icon(Icons.edit, color: contentTheme.primary), onPressed: () async { // existing bottom sheet flow await controller .fetchDocumentVersions(doc.parentAttachmentId); final latestVersion = controller.versions.isNotEmpty ? controller.versions.reduce((a, b) => a.uploadedAt.isAfter(b.uploadedAt) ? a : b) : null; final documentData = { "id": doc.id, "documentId": doc.documentId, "name": doc.name, "description": doc.description, "tags": doc.tags .map((t) => {"name": t.name, "isActive": t.isActive}) .toList(), "category": doc.documentType.documentCategory?.toJson(), "type": doc.documentType.toJson(), "attachment": latestVersion != null ? { "id": latestVersion.id, "fileName": latestVersion.name, "contentType": latestVersion.contentType, "fileSize": latestVersion.fileSize, } : null, }; showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(5)), ), builder: (_) { return DocumentEditBottomSheet( documentData: documentData, onSubmit: (updatedData) async { await _fetchDetails(); }, ); }, ); }, ) ], ), MySpacing.height(12), // Tags if (doc.tags.isNotEmpty) Wrap( children: doc.tags.map((t) => _buildTagChip(t.name)).toList(), ), MySpacing.height(16), // Info rows _buildDetailRow("Document ID", doc.documentId), _buildDetailRow("Description", doc.description ?? "-"), _buildDetailRow( "Category", doc.documentType.documentCategory?.name ?? "-"), _buildDetailRow("Version", "v${doc.version}"), _buildDetailRow( "Current Version", doc.isCurrentVersion ? "Yes" : "No"), _buildDetailRow("Verified", doc.isVerified == null ? '-' : (doc.isVerified! ? "Yes" : "No")), _buildDetailRow("Uploaded By", "${doc.uploadedBy.firstName} ${doc.uploadedBy.lastName}"), _buildDetailRow("Uploaded On", uploadDate), if (doc.updatedAt != null) _buildDetailRow("Last Updated On", updateDate), MySpacing.height(12), // Show buttons only if user has permission AND document is not verified yet if (permissionController.hasPermission(Permissions.verifyDocument) && doc.isVerified == null) ...[ Row( children: [ Expanded( child: ElevatedButton.icon( icon: const Icon(Icons.check, color: Colors.white), label: MyText.bodyMedium( "Verify", color: Colors.white, fontWeight: 600, ), style: ElevatedButton.styleFrom( backgroundColor: Colors.green, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), padding: const EdgeInsets.symmetric(vertical: 8), ), onPressed: () async { final success = await controller.verifyDocument(doc.id); if (success) { showAppSnackbar( title: "Success", message: "Document verified successfully", type: SnackbarType.success, ); } else { showAppSnackbar( title: "Error", message: "Failed to verify document", type: SnackbarType.error, ); } }, ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( icon: const Icon(Icons.close, color: Colors.white), label: MyText.bodyMedium( "Reject", color: Colors.white, fontWeight: 600, ), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), padding: const EdgeInsets.symmetric(vertical: 8), ), onPressed: () async { final success = await controller.rejectDocument(doc.id); if (success) { showAppSnackbar( title: "Rejected", message: "Document rejected successfully", type: SnackbarType.warning, ); } else { showAppSnackbar( title: "Error", message: "Failed to reject document", type: SnackbarType.error, ); } }, ), ), ], ), ], ], ), ); } /// ---------------- VERSIONS SECTION ---------------- Widget _buildVersionsSection() { return Obx(() { if (controller.isVersionsLoading.value) { return const Center(child: CircularProgressIndicator()); } if (controller.versions.isEmpty) { return MyText.bodySmall("No versions found", color: Colors.grey); } final sorted = [...controller.versions]; sorted.sort((a, b) => b.uploadedAt.compareTo(a.uploadedAt)); return ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: sorted.length, separatorBuilder: (_, __) => Divider(height: 1), itemBuilder: (context, index) { final version = sorted[index]; final uploadDate = DateFormat("dd MMM yyyy, hh:mm a").format(version.uploadedAt); return ListTile( leading: const Icon(Icons.description, color: Colors.blue), title: MyText.bodyMedium( "${version.name} (v${version.version})", fontWeight: 600, color: Colors.black, ), subtitle: MyText.bodySmall( "Uploaded by ${version.uploadedBy.firstName} ${version.uploadedBy.lastName} • $uploadDate", color: Colors.grey.shade600, ), trailing: permissionController.hasPermission(Permissions.viewDocument) ? IconButton( icon: const Icon(Icons.open_in_new, color: Colors.blue), onPressed: () async { final url = await controller.fetchPresignedUrl(version.id); if (url != null) { _openDocument(url); } else { showAppSnackbar( title: "Error", message: "Failed to fetch document link", type: SnackbarType.error, ); } }, ) : null, ); }, ); }); } /// ---------------- HELPERS ---------------- Widget _buildTagChip(String label) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), margin: const EdgeInsets.only(right: 6, bottom: 6), decoration: BoxDecoration( color: Colors.blue.shade100, borderRadius: BorderRadius.circular(5), ), child: MyText.bodySmall( label, color: Colors.blue.shade900, fontWeight: 600, ), ); } Widget _buildDetailRow(String title, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ SizedBox( width: 120, child: MyText.bodySmall( "$title:", fontWeight: 600, color: Colors.grey.shade800, overflow: TextOverflow.ellipsis, ), ), Expanded( child: MyText.bodySmall( value, color: Colors.grey.shade600, overflow: TextOverflow.ellipsis, ), ), ], ), ); } Future _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, ); } } }