marco.pms.mobileapp/lib/view/document/user_document_screen.dart
Vaibhav Surve 334023bf1b feat: Add document management features including document listing, details, and filtering
- Implemented DocumentsResponse and related models for handling document data.
- Created UserDocumentsPage for displaying user-specific documents with filtering options.
- Developed DocumentDetailsPage to show detailed information about a selected document.
- Added functionality for uploading documents with DocumentUploadBottomSheet.
- Integrated document filtering through UserDocumentFilterBottomSheet.
- Enhanced dashboard to include navigation to the document management section.
- Updated user profile right bar to provide quick access to user documents.
2025-09-04 16:56:49 +05:30

318 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
class UserDocumentsPage extends StatefulWidget {
final String? entityId;
final bool isEmployee;
const UserDocumentsPage({
super.key,
this.entityId,
this.isEmployee = false,
});
@override
State<UserDocumentsPage> createState() => _UserDocumentsPageState();
}
class _UserDocumentsPageState extends State<UserDocumentsPage> {
final DocumentController docController = Get.put(DocumentController());
String get entityTypeId => widget.isEmployee
? Permissions.employeeEntity
: Permissions.projectEntity;
String get resolvedEntityId => widget.isEmployee
? widget.entityId ?? ""
: Get.find<ProjectController>().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(12),
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(8),
),
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,
),
],
),
),
IconButton(
icon: const Icon(Icons.arrow_forward_ios, color: Colors.black54),
onPressed: () {/* future actions */},
),
],
),
),
),
],
);
}
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 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,
),
);
},
),
);
}
Widget _buildBody(BuildContext context) {
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),
Expanded(
child: MyRefreshIndicator(
onRefresh: () async {
await docController.fetchDocuments(
entityTypeId: entityTypeId,
entityId: resolvedEntityId,
filter: docController.selectedFilter.value,
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) {
return Scaffold(
backgroundColor: const Color(0xFFF1F1F1),
appBar: CustomAppBar(
title: 'Documents',
onBackPressed: () {
Get.back();
},
),
body: _buildBody(context),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
final uploadController = Get.put(DocumentUploadController());
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => DocumentUploadBottomSheet(
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) {
// ✅ Only close on success
Navigator.pop(context);
// Refresh list
docController.fetchDocuments(
entityTypeId: entityTypeId,
entityId: resolvedEntityId,
reset: true,
);
} else {
// ❌ Dont close, show error
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,
),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
);
}
}