- Updated import paths across multiple files to reflect the new package name. - Changed application name and identifiers in CMakeLists.txt, Runner.rc, and other configuration files. - Modified web index.html and manifest.json to update the app title and name. - Adjusted macOS and Windows project settings to align with the new application name. - Ensured consistency in naming across all relevant files and directories.
1024 lines
34 KiB
Dart
1024 lines
34 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:on_field_work/controller/document/document_details_controller.dart';
|
|
import 'package:on_field_work/controller/document/document_upload_controller.dart';
|
|
import 'package:on_field_work/controller/document/user_document_controller.dart';
|
|
import 'package:on_field_work/controller/permission_controller.dart';
|
|
import 'package:on_field_work/controller/project_controller.dart';
|
|
import 'package:on_field_work/helpers/utils/permission_constants.dart';
|
|
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
|
import 'package:on_field_work/helpers/widgets/my_confirmation_dialog.dart';
|
|
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
|
|
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
|
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
|
|
import 'package:on_field_work/model/document/document_upload_bottom_sheet.dart';
|
|
import 'package:on_field_work/model/document/documents_list_model.dart';
|
|
import 'package:on_field_work/model/document/user_document_filter_bottom_sheet.dart';
|
|
import 'package:on_field_work/view/document/document_details_page.dart';
|
|
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.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>
|
|
with UIMixin, SingleTickerProviderStateMixin {
|
|
late ScrollController _scrollController;
|
|
late AnimationController _fabAnimationController;
|
|
late Animation<double> _fabScaleAnimation;
|
|
|
|
DocumentController get docController => Get.find<DocumentController>();
|
|
PermissionController get permissionController =>
|
|
Get.find<PermissionController>();
|
|
DocumentDetailsController get detailsController =>
|
|
Get.find<DocumentDetailsController>();
|
|
|
|
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();
|
|
|
|
if (!Get.isRegistered<DocumentController>()) Get.put(DocumentController());
|
|
if (!Get.isRegistered<PermissionController>())
|
|
Get.put(PermissionController());
|
|
if (!Get.isRegistered<DocumentDetailsController>())
|
|
Get.put(DocumentDetailsController());
|
|
|
|
_scrollController = ScrollController()..addListener(_onScroll);
|
|
|
|
_fabAnimationController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 200),
|
|
);
|
|
_fabScaleAnimation = CurvedAnimation(
|
|
parent: _fabAnimationController,
|
|
curve: Curves.easeInOut,
|
|
);
|
|
_fabAnimationController.forward();
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => _initializeData());
|
|
}
|
|
|
|
void _initializeData() {
|
|
docController.fetchFilters(entityTypeId);
|
|
docController.fetchDocuments(
|
|
entityTypeId: entityTypeId,
|
|
entityId: resolvedEntityId,
|
|
reset: true,
|
|
);
|
|
}
|
|
|
|
void _onScroll() {
|
|
if (_scrollController.position.pixels >=
|
|
_scrollController.position.maxScrollExtent * 0.8) {
|
|
if (!docController.isLoading.value && docController.hasMore.value) {
|
|
docController.fetchDocuments(
|
|
entityTypeId: entityTypeId,
|
|
entityId: resolvedEntityId,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (_scrollController.position.userScrollDirection ==
|
|
ScrollDirection.reverse) {
|
|
if (_fabAnimationController.isCompleted)
|
|
_fabAnimationController.reverse();
|
|
} else if (_scrollController.position.userScrollDirection ==
|
|
ScrollDirection.forward) {
|
|
if (_fabAnimationController.isDismissed)
|
|
_fabAnimationController.forward();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scrollController.dispose();
|
|
_fabAnimationController.dispose();
|
|
docController.searchController.dispose();
|
|
docController.documents.clear();
|
|
super.dispose();
|
|
}
|
|
|
|
// ==================== UI BUILDERS ====================
|
|
|
|
Widget _buildSearchBar() {
|
|
return Container(
|
|
margin: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(5),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: TextField(
|
|
controller: docController.searchController,
|
|
onChanged: (value) {
|
|
docController.searchQuery.value = value;
|
|
docController.fetchDocuments(
|
|
entityTypeId: entityTypeId,
|
|
entityId: resolvedEntityId,
|
|
reset: true,
|
|
);
|
|
},
|
|
style: const TextStyle(fontSize: 15),
|
|
decoration: InputDecoration(
|
|
contentPadding:
|
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
prefixIcon:
|
|
const Icon(Icons.search_rounded, size: 22, 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_rounded, size: 20),
|
|
color: Colors.grey.shade600,
|
|
onPressed: () {
|
|
docController.searchController.clear();
|
|
docController.searchQuery.value = '';
|
|
docController.fetchDocuments(
|
|
entityTypeId: entityTypeId,
|
|
entityId: resolvedEntityId,
|
|
reset: true,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
hintText: 'Search by document name or type...',
|
|
hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 15),
|
|
filled: true,
|
|
fillColor: Colors.white,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(5),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(5),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(5),
|
|
borderSide: BorderSide(color: contentTheme.primary, width: 2),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFilterChips() {
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Obx(() {
|
|
final hasFilters = docController.hasActiveFilters();
|
|
return Row(
|
|
children: [
|
|
if (hasFilters) ...[
|
|
_buildChip(
|
|
'Clear Filters',
|
|
icon: Icons.close_rounded,
|
|
isSelected: false,
|
|
onTap: () {
|
|
docController.clearFilters();
|
|
docController.fetchDocuments(
|
|
entityTypeId: entityTypeId,
|
|
entityId: resolvedEntityId,
|
|
reset: true,
|
|
);
|
|
},
|
|
backgroundColor: Colors.red.shade50,
|
|
textColor: Colors.red.shade700,
|
|
),
|
|
const SizedBox(width: 8),
|
|
],
|
|
_buildFilterButton(),
|
|
const SizedBox(width: 8),
|
|
_buildMoreOptionsButton(),
|
|
],
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildChip(
|
|
String label, {
|
|
IconData? icon,
|
|
bool isSelected = false,
|
|
VoidCallback? onTap,
|
|
Color? backgroundColor,
|
|
Color? textColor,
|
|
}) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(5),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: backgroundColor ??
|
|
(isSelected
|
|
? contentTheme.primary.withOpacity(0.1)
|
|
: Colors.white),
|
|
borderRadius: BorderRadius.circular(5),
|
|
border: Border.all(
|
|
color: isSelected ? contentTheme.primary : Colors.grey.shade300,
|
|
width: 1.5,
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (icon != null) ...[
|
|
Icon(
|
|
icon,
|
|
size: 16,
|
|
color: textColor ??
|
|
(isSelected ? contentTheme.primary : Colors.grey.shade700),
|
|
),
|
|
const SizedBox(width: 6),
|
|
],
|
|
MyText.labelSmall(
|
|
label,
|
|
fontWeight: 600,
|
|
color: textColor ??
|
|
(isSelected ? contentTheme.primary : Colors.grey.shade700),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFilterButton() {
|
|
return Obx(() {
|
|
final isFilterActive = docController.hasActiveFilters();
|
|
return Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
_buildChip(
|
|
'Filters',
|
|
icon: Icons.tune_rounded,
|
|
isSelected: isFilterActive,
|
|
onTap: () {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (_) => UserDocumentFilterBottomSheet(
|
|
entityId: resolvedEntityId,
|
|
entityTypeId: entityTypeId,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
if (isFilterActive)
|
|
Positioned(
|
|
top: -4,
|
|
right: -4,
|
|
child: Container(
|
|
height: 10,
|
|
width: 10,
|
|
decoration: BoxDecoration(
|
|
color: Colors.red,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: const Color(0xFFF1F1F1), width: 2),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
});
|
|
}
|
|
|
|
Widget _buildMoreOptionsButton() {
|
|
return PopupMenuButton<String>(
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
|
offset: const Offset(0, 40),
|
|
child: _buildChip(
|
|
'Options',
|
|
icon: Icons.more_horiz_rounded,
|
|
),
|
|
itemBuilder: (context) => [
|
|
PopupMenuItem<String>(
|
|
enabled: false,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: MyText.bodySmall(
|
|
'Preferences',
|
|
fontWeight: 700,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
const PopupMenuDivider(height: 1),
|
|
PopupMenuItem<String>(
|
|
value: 'show_deleted',
|
|
enabled: false,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
|
child: Obx(() => Row(
|
|
children: [
|
|
Icon(Icons.visibility_off_outlined,
|
|
size: 20, color: Colors.grey.shade700),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: MyText.bodyMedium('Show Deleted', fontSize: 14),
|
|
),
|
|
Switch.adaptive(
|
|
value: docController.showInactive.value,
|
|
activeColor: contentTheme.primary,
|
|
onChanged: (val) {
|
|
docController.showInactive.value = val;
|
|
docController.fetchDocuments(
|
|
entityTypeId: entityTypeId,
|
|
entityId: resolvedEntityId,
|
|
reset: true,
|
|
);
|
|
Navigator.pop(context);
|
|
},
|
|
),
|
|
],
|
|
)),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildStatusBanner() {
|
|
return Obx(() {
|
|
if (!docController.showInactive.value) return const SizedBox.shrink();
|
|
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 300),
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red.shade50,
|
|
border:
|
|
Border(bottom: BorderSide(color: Colors.red.shade100, width: 1)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.info_outline_rounded,
|
|
color: Colors.red.shade700, size: 18),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: MyText.bodySmall(
|
|
'Showing deleted documents',
|
|
fontWeight: 600,
|
|
color: Colors.red.shade700,
|
|
),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
docController.showInactive.value = false;
|
|
docController.fetchDocuments(
|
|
entityTypeId: entityTypeId,
|
|
entityId: resolvedEntityId,
|
|
reset: true,
|
|
);
|
|
},
|
|
style: TextButton.styleFrom(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
minimumSize: Size.zero,
|
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
),
|
|
child: MyText.labelMedium(
|
|
'Hide',
|
|
fontWeight: 700,
|
|
color: Colors.red.shade700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
Widget _buildDocumentCard(DocumentItem doc, bool showDateHeader) {
|
|
final uploadDate =
|
|
DateFormat("dd MMM yyyy").format(doc.uploadedAt.toLocal());
|
|
final uploadTime = DateFormat("hh:mm a").format(doc.uploadedAt.toLocal());
|
|
final uploader = doc.uploadedBy.firstName.isNotEmpty
|
|
? "${doc.uploadedBy.firstName} ${doc.uploadedBy.lastName}".trim()
|
|
: "You";
|
|
|
|
final iconColor = _getDocumentTypeColor(doc.documentType.name);
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (showDateHeader) _buildDateHeader(uploadDate),
|
|
Hero(
|
|
tag: 'document_${doc.id}',
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: () {
|
|
Get.to(
|
|
() => DocumentDetailsPage(documentId: doc.id),
|
|
transition: Transition.rightToLeft,
|
|
duration: const Duration(milliseconds: 300),
|
|
);
|
|
},
|
|
borderRadius: BorderRadius.circular(5),
|
|
child: Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(5),
|
|
border: Border.all(color: Colors.grey.shade200, width: 1),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.03),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: iconColor.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Icon(
|
|
_getDocumentIcon(doc.documentType.name),
|
|
color: iconColor,
|
|
size: 24,
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: iconColor.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: MyText.labelSmall(
|
|
doc.documentType.name,
|
|
fontWeight: 600,
|
|
color: iconColor,
|
|
letterSpacing: 0.3,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
MyText.bodyMedium(
|
|
doc.name,
|
|
fontWeight: 600,
|
|
color: Colors.black87,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 6),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.person_outline_rounded,
|
|
size: 14, color: Colors.grey.shade600),
|
|
const SizedBox(width: 4),
|
|
Expanded(
|
|
child: MyText.bodySmall(
|
|
'Added by $uploader',
|
|
color: Colors.grey.shade600,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
MyText.bodySmall(
|
|
uploadTime,
|
|
color: Colors.grey.shade500,
|
|
fontWeight: 500,
|
|
fontSize: 11,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
_buildDocumentMenu(doc),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildDateHeader(String date) {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
|
child: MyText.bodySmall(
|
|
date,
|
|
fontWeight: 700,
|
|
color: Colors.grey.shade700,
|
|
letterSpacing: 0.5,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDocumentMenu(DocumentItem doc) {
|
|
return Obx(() {
|
|
final canDelete =
|
|
permissionController.hasPermission(Permissions.deleteDocument);
|
|
final canModify =
|
|
permissionController.hasPermission(Permissions.modifyDocument);
|
|
|
|
// Build menu items list
|
|
final List<PopupMenuEntry<String>> menuItems = [];
|
|
|
|
if (doc.isActive && canDelete) {
|
|
menuItems.add(
|
|
PopupMenuItem(
|
|
value: "delete",
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.delete_outline_rounded,
|
|
size: 20, color: Colors.red.shade700),
|
|
const SizedBox(width: 12),
|
|
MyText.bodyMedium(
|
|
'Delete',
|
|
color: Colors.red.shade700,
|
|
)
|
|
],
|
|
),
|
|
),
|
|
);
|
|
} else if (!doc.isActive && canModify) {
|
|
menuItems.add(
|
|
PopupMenuItem(
|
|
value: "restore",
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.restore_rounded,
|
|
size: 20, color: contentTheme.primary),
|
|
const SizedBox(width: 12),
|
|
MyText.bodyMedium(
|
|
'Restore',
|
|
color: contentTheme.primary,
|
|
)
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// If no menu items, return empty widget
|
|
if (menuItems.isEmpty) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
return PopupMenuButton<String>(
|
|
icon: Icon(Icons.more_vert_rounded,
|
|
color: Colors.grey.shade600, size: 20),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
|
offset: const Offset(-10, 30),
|
|
onSelected: (value) => _handleMenuAction(value, doc),
|
|
itemBuilder: (context) => menuItems,
|
|
);
|
|
});
|
|
}
|
|
|
|
Future<void> _handleMenuAction(String action, DocumentItem doc) async {
|
|
if (action == "delete") {
|
|
await showDialog<bool>(
|
|
context: context,
|
|
builder: (_) => ConfirmDialog(
|
|
title: "Delete Document",
|
|
message:
|
|
"Are you sure you want to delete \"${doc.name}\"?\n\nThis action cannot be undone.",
|
|
confirmText: "Delete",
|
|
cancelText: "Cancel",
|
|
icon: Icons.delete_forever_rounded,
|
|
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");
|
|
}
|
|
},
|
|
),
|
|
);
|
|
} else if (action == "restore") {
|
|
final success = await docController.toggleDocumentActive(
|
|
doc.id,
|
|
isActive: true,
|
|
entityTypeId: entityTypeId,
|
|
entityId: resolvedEntityId,
|
|
);
|
|
|
|
if (success) {
|
|
showAppSnackbar(
|
|
title: "Restored",
|
|
message: "Document restored successfully",
|
|
type: SnackbarType.success,
|
|
);
|
|
} else {
|
|
showAppSnackbar(
|
|
title: "Error",
|
|
message: "Failed to restore document",
|
|
type: SnackbarType.error,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Widget _buildEmptyState() {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.folder_open_rounded,
|
|
size: 64,
|
|
color: Colors.grey.shade400,
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
MyText.bodyLarge(
|
|
'No documents found',
|
|
fontWeight: 600,
|
|
color: Colors.grey.shade700,
|
|
),
|
|
const SizedBox(height: 8),
|
|
MyText.bodySmall(
|
|
'Try adjusting your filters or\nadd a new document to get started',
|
|
color: Colors.grey.shade600,
|
|
height: 1.5,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLoadingIndicator() {
|
|
return const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(16),
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildNoMoreIndicator() {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Center(
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
height: 1,
|
|
width: 40,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
child: MyText.bodySmall(
|
|
'No more documents',
|
|
fontWeight: 500,
|
|
)),
|
|
Container(
|
|
height: 1,
|
|
width: 40,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPermissionDenied() {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red.shade50,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.lock_outline_rounded,
|
|
size: 64,
|
|
color: Colors.red.shade300,
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
MyText.bodyLarge(
|
|
'Access Denied',
|
|
fontWeight: 600,
|
|
color: Colors.grey.shade700,
|
|
),
|
|
const SizedBox(height: 8),
|
|
MyText.bodySmall(
|
|
'You don\'t have permission\nto view documents',
|
|
color: Colors.grey.shade600,
|
|
height: 1.5,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBody() {
|
|
return Obx(() {
|
|
// Check permissions
|
|
if (permissionController.permissions.isEmpty) {
|
|
return _buildLoadingIndicator();
|
|
}
|
|
|
|
if (!permissionController.hasPermission(Permissions.viewDocument)) {
|
|
return _buildPermissionDenied();
|
|
}
|
|
|
|
// Show skeleton loader
|
|
if (docController.isLoading.value && docController.documents.isEmpty) {
|
|
return SingleChildScrollView(
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
child: SkeletonLoaders.documentSkeletonLoader(),
|
|
);
|
|
}
|
|
|
|
final docs = docController.documents;
|
|
|
|
return Column(
|
|
children: [
|
|
_buildSearchBar(),
|
|
_buildFilterChips(),
|
|
_buildStatusBanner(),
|
|
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: docs.isEmpty
|
|
? ListView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
children: [
|
|
SizedBox(
|
|
height: MediaQuery.of(context).size.height * 0.5,
|
|
child: _buildEmptyState(),
|
|
),
|
|
],
|
|
)
|
|
: ListView.builder(
|
|
controller: _scrollController,
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
padding: const EdgeInsets.only(bottom: 100, top: 8),
|
|
itemCount: docs.length + 1,
|
|
itemBuilder: (context, index) {
|
|
if (index == docs.length) {
|
|
return Obx(() {
|
|
if (docController.isLoading.value) {
|
|
return _buildLoadingIndicator();
|
|
}
|
|
if (!docController.hasMore.value &&
|
|
docs.isNotEmpty) {
|
|
return _buildNoMoreIndicator();
|
|
}
|
|
return const SizedBox.shrink();
|
|
});
|
|
}
|
|
|
|
final doc = docs[index];
|
|
final currentDate = DateFormat("dd MMM yyyy")
|
|
.format(doc.uploadedAt.toLocal());
|
|
final prevDate = index > 0
|
|
? DateFormat("dd MMM yyyy")
|
|
.format(docs[index - 1].uploadedAt.toLocal())
|
|
: null;
|
|
final showDateHeader = currentDate != prevDate;
|
|
|
|
return _buildDocumentCard(doc, showDateHeader);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
});
|
|
}
|
|
|
|
Widget _buildFAB() {
|
|
return Obx(() {
|
|
if (permissionController.permissions.isEmpty) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
if (!permissionController.hasPermission(Permissions.uploadDocument)) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
return ScaleTransition(
|
|
scale: _fabScaleAnimation,
|
|
child: FloatingActionButton.extended(
|
|
onPressed: _showUploadBottomSheet,
|
|
elevation: 4,
|
|
highlightElevation: 8,
|
|
backgroundColor: contentTheme.primary,
|
|
foregroundColor: Colors.white,
|
|
icon: const Icon(Icons.add_rounded, size: 24),
|
|
label: const Text(
|
|
'Add Document',
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.3,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
void _showUploadBottomSheet() {
|
|
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,
|
|
);
|
|
showAppSnackbar(
|
|
title: "Success",
|
|
message: "Document uploaded successfully",
|
|
type: SnackbarType.success,
|
|
);
|
|
} else {
|
|
showAppSnackbar(
|
|
title: "Error",
|
|
message: "Upload failed, please try again",
|
|
type: SnackbarType.error,
|
|
);
|
|
}
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// Helper methods for document type styling
|
|
Color _getDocumentTypeColor(String type) {
|
|
final lowerType = type.toLowerCase();
|
|
if (lowerType.contains('contract') || lowerType.contains('agreement')) {
|
|
return Colors.purple;
|
|
} else if (lowerType.contains('invoice') || lowerType.contains('receipt')) {
|
|
return Colors.green;
|
|
} else if (lowerType.contains('report')) {
|
|
return Colors.orange;
|
|
} else if (lowerType.contains('certificate')) {
|
|
return Colors.blue;
|
|
} else if (lowerType.contains('id') || lowerType.contains('identity')) {
|
|
return Colors.red;
|
|
} else {
|
|
return Colors.blueGrey;
|
|
}
|
|
}
|
|
|
|
IconData _getDocumentIcon(String type) {
|
|
final lowerType = type.toLowerCase();
|
|
if (lowerType.contains('contract') || lowerType.contains('agreement')) {
|
|
return Icons.article_rounded;
|
|
} else if (lowerType.contains('invoice') || lowerType.contains('receipt')) {
|
|
return Icons.receipt_long_rounded;
|
|
} else if (lowerType.contains('report')) {
|
|
return Icons.assessment_rounded;
|
|
} else if (lowerType.contains('certificate')) {
|
|
return Icons.workspace_premium_rounded;
|
|
} else if (lowerType.contains('id') || lowerType.contains('identity')) {
|
|
return Icons.badge_rounded;
|
|
} else {
|
|
return Icons.description_rounded;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF1F1F1),
|
|
appBar: !widget.isEmployee
|
|
? CustomAppBar(
|
|
title: 'Documents',
|
|
onBackPressed: () => Get.back(),
|
|
)
|
|
: null,
|
|
body: SafeArea(
|
|
child: _buildBody(),
|
|
),
|
|
floatingActionButton: _buildFAB(),
|
|
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
|
);
|
|
}
|
|
}
|