diff --git a/lib/controller/document/user_document_controller.dart b/lib/controller/document/user_document_controller.dart index a424dce..420a322 100644 --- a/lib/controller/document/user_document_controller.dart +++ b/lib/controller/document/user_document_controller.dart @@ -142,8 +142,8 @@ class DocumentController extends GetxController { ); if (response != null && response.success) { - if (response.data.data.isNotEmpty) { - documents.addAll(response.data.data); + if (response.data?.data.isNotEmpty ?? false) { + documents.addAll(response.data!.data); pageNumber.value++; } else { hasMore.value = false; diff --git a/lib/helpers/widgets/custom_app_bar.dart b/lib/helpers/widgets/custom_app_bar.dart index 6d3159f..5128802 100644 --- a/lib/helpers/widgets/custom_app_bar.dart +++ b/lib/helpers/widgets/custom_app_bar.dart @@ -3,95 +3,108 @@ import 'package:get/get.dart'; import 'package:on_field_work/controller/project_controller.dart'; import 'package:on_field_work/helpers/widgets/my_spacing.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart'; +import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; -class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { +class CustomAppBar extends StatelessWidget + with UIMixin + implements PreferredSizeWidget { final String title; - final String? projectName; + final String? projectName; final VoidCallback? onBackPressed; + final Color? backgroundColor; - const CustomAppBar({ + CustomAppBar({ super.key, required this.title, this.projectName, this.onBackPressed, + this.backgroundColor, }); @override - Widget build(BuildContext context) { - return PreferredSize( - preferredSize: const Size.fromHeight(72), - child: AppBar( - backgroundColor: const Color(0xFFF5F5F5), - elevation: 0.5, - automaticallyImplyLeading: false, - titleSpacing: 0, - title: Padding( - padding: MySpacing.xy(16, 0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - icon: const Icon( - Icons.arrow_back_ios_new, - color: Colors.black, - size: 20, - ), - onPressed: onBackPressed ?? () => Get.back(), - ), - MySpacing.width(5), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // TITLE - MyText.titleLarge( - title, - fontWeight: 700, - color: Colors.black, - ), - - MySpacing.height(2), - - // PROJECT NAME ROW - GetBuilder( - builder: (projectController) { - // NEW LOGIC — simple and safe - final displayProjectName = - projectName ?? - projectController.selectedProject?.name ?? - 'Select Project'; - - return Row( - children: [ - const Icon( - Icons.work_outline, - size: 14, - color: Colors.grey, - ), - MySpacing.width(4), - Expanded( - child: MyText.bodySmall( - displayProjectName, - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: Colors.grey[700], - ), - ), - ], - ); - }, - ), - ], - ), - ), - ], - ), - ), - ), - ); - } + Size get preferredSize => const Size.fromHeight(72); @override - Size get preferredSize => const Size.fromHeight(72); + Widget build(BuildContext context) { + final Color effectiveBackgroundColor = + backgroundColor ?? contentTheme.primary; + const Color onPrimaryColor = Colors.white; + const double horizontalPadding = 16.0; + + return AppBar( + backgroundColor: effectiveBackgroundColor, + elevation: 0, + automaticallyImplyLeading: false, + titleSpacing: 0, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(bottom: Radius.circular(0)), + ), + leading: Padding( + padding: MySpacing.only(left: horizontalPadding), + child: IconButton( + icon: const Icon( + Icons.arrow_back_ios_new, + color: onPrimaryColor, + size: 20, + ), + onPressed: onBackPressed ?? () => Get.back(), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ), + title: Padding( + padding: MySpacing.only(right: horizontalPadding, left: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + MyText.titleLarge( + title, + fontWeight: 800, + color: onPrimaryColor, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + MySpacing.height(3), + GetBuilder( + builder: (projectController) { + final displayProjectName = projectName ?? + projectController.selectedProject?.name ?? + 'Select Project'; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.folder_open, + size: 14, color: onPrimaryColor), + MySpacing.width(4), + Flexible( + child: MyText.bodySmall( + displayProjectName, + fontWeight: 500, + color: onPrimaryColor.withOpacity(0.8), + overflow: TextOverflow.ellipsis, + ), + ), + MySpacing.width(2), + const Icon(Icons.keyboard_arrow_down, + size: 18, color: onPrimaryColor), + ], + ); + }, + ), + ], + ), + ), + actions: [ + Padding( + padding: MySpacing.only(right: horizontalPadding), + child: IconButton( + icon: const Icon(Icons.home, color: onPrimaryColor, size: 24), + onPressed: () => Get.offAllNamed('/dashboard'), + ), + ), + ], + ); + } } diff --git a/lib/model/document/document_details_model.dart b/lib/model/document/document_details_model.dart index 06e52eb..14bef8b 100644 --- a/lib/model/document/document_details_model.dart +++ b/lib/model/document/document_details_model.dart @@ -148,7 +148,7 @@ class DocumentType { final String name; final String? regexExpression; final String allowedContentType; - final int maxSizeAllowedInMB; + final double maxSizeAllowedInMB; final bool isValidationRequired; final bool isMandatory; final bool isSystem; diff --git a/lib/model/document/documents_list_model.dart b/lib/model/document/documents_list_model.dart index 5bbf820..ddf8fe0 100644 --- a/lib/model/document/documents_list_model.dart +++ b/lib/model/document/documents_list_model.dart @@ -1,7 +1,7 @@ class DocumentsResponse { final bool success; final String message; - final DocumentDataWrapper data; + final DocumentDataWrapper? data; final dynamic errors; final int statusCode; final DateTime timestamp; @@ -9,7 +9,7 @@ class DocumentsResponse { DocumentsResponse({ required this.success, required this.message, - required this.data, + this.data, this.errors, required this.statusCode, required this.timestamp, @@ -19,11 +19,13 @@ class DocumentsResponse { return DocumentsResponse( success: json['success'] ?? false, message: json['message'] ?? '', - data: DocumentDataWrapper.fromJson(json['data']), + data: json['data'] != null + ? DocumentDataWrapper.fromJson(json['data']) + : null, errors: json['errors'], statusCode: json['statusCode'] ?? 0, timestamp: json['timestamp'] != null - ? DateTime.parse(json['timestamp']) + ? DateTime.tryParse(json['timestamp']) ?? DateTime.now() : DateTime.now(), ); } @@ -32,7 +34,7 @@ class DocumentsResponse { return { 'success': success, 'message': message, - 'data': data.toJson(), + 'data': data?.toJson(), 'errors': errors, 'statusCode': statusCode, 'timestamp': timestamp.toIso8601String(), @@ -61,9 +63,10 @@ class DocumentDataWrapper { currentPage: json['currentPage'] ?? 0, totalPages: json['totalPages'] ?? 0, totalEntites: json['totalEntites'] ?? 0, - data: (json['data'] as List? ?? []) - .map((e) => DocumentItem.fromJson(e)) - .toList(), + data: (json['data'] as List?) + ?.map((e) => DocumentItem.fromJson(e)) + .toList() ?? + [], ); } @@ -83,28 +86,28 @@ class DocumentItem { final String name; final String documentId; final String description; - final DateTime uploadedAt; + final DateTime? uploadedAt; final String? parentAttachmentId; final bool isCurrentVersion; final int version; final bool isActive; final bool? isVerified; - final UploadedBy uploadedBy; - final DocumentType documentType; + final UploadedBy? uploadedBy; + final DocumentType? documentType; DocumentItem({ required this.id, required this.name, required this.documentId, required this.description, - required this.uploadedAt, + this.uploadedAt, this.parentAttachmentId, required this.isCurrentVersion, required this.version, required this.isActive, this.isVerified, - required this.uploadedBy, - required this.documentType, + this.uploadedBy, + this.documentType, }); factory DocumentItem.fromJson(Map json) { @@ -113,14 +116,20 @@ class DocumentItem { name: json['name'] ?? '', documentId: json['documentId'] ?? '', description: json['description'] ?? '', - uploadedAt: DateTime.parse(json['uploadedAt']), + uploadedAt: json['uploadedAt'] != null + ? DateTime.tryParse(json['uploadedAt']) + : null, parentAttachmentId: json['parentAttachmentId'], isCurrentVersion: json['isCurrentVersion'] ?? false, version: json['version'] ?? 0, isActive: json['isActive'] ?? false, isVerified: json['isVerified'], - uploadedBy: UploadedBy.fromJson(json['uploadedBy']), - documentType: DocumentType.fromJson(json['documentType']), + uploadedBy: json['uploadedBy'] != null + ? UploadedBy.fromJson(json['uploadedBy']) + : null, + documentType: json['documentType'] != null + ? DocumentType.fromJson(json['documentType']) + : null, ); } @@ -130,14 +139,14 @@ class DocumentItem { 'name': name, 'documentId': documentId, 'description': description, - 'uploadedAt': uploadedAt.toIso8601String(), + 'uploadedAt': uploadedAt?.toIso8601String(), 'parentAttachmentId': parentAttachmentId, 'isCurrentVersion': isCurrentVersion, 'version': version, 'isActive': isActive, 'isVerified': isVerified, - 'uploadedBy': uploadedBy.toJson(), - 'documentType': documentType.toJson(), + 'uploadedBy': uploadedBy?.toJson(), + 'documentType': documentType?.toJson(), }; } } @@ -208,7 +217,7 @@ class DocumentType { final String name; final String? regexExpression; final String? allowedContentType; - final int? maxSizeAllowedInMB; + final double? maxSizeAllowedInMB; final bool isValidationRequired; final bool isMandatory; final bool isSystem; @@ -232,7 +241,7 @@ class DocumentType { return DocumentType( id: json['id'] ?? '', name: json['name'] ?? '', - regexExpression: json['regexExpression'], // nullable + regexExpression: json['regexExpression'], allowedContentType: json['allowedContentType'], maxSizeAllowedInMB: json['maxSizeAllowedInMB'], isValidationRequired: json['isValidationRequired'] ?? false, diff --git a/lib/view/Attendence/attendance_screen.dart b/lib/view/Attendence/attendance_screen.dart index d617654..fca026e 100644 --- a/lib/view/Attendence/attendance_screen.dart +++ b/lib/view/Attendence/attendance_screen.dart @@ -14,6 +14,7 @@ import 'package:on_field_work/view/Attendence/regularization_requests_tab.dart'; import 'package:on_field_work/view/Attendence/attendance_logs_tab.dart'; import 'package:on_field_work/view/Attendence/todays_attendance_tab.dart'; import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart'; +import 'package:on_field_work/helpers/widgets/custom_app_bar.dart'; class AttendanceScreen extends StatefulWidget { const AttendanceScreen({super.key}); @@ -75,61 +76,6 @@ class _AttendanceScreenState extends State with UIMixin { } } - Widget _buildAppBar() { - return AppBar( - backgroundColor: const Color(0xFFF5F5F5), - elevation: 0.5, - automaticallyImplyLeading: false, - titleSpacing: 0, - title: Padding( - padding: MySpacing.xy(16, 0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios_new, - color: Colors.black, size: 20), - onPressed: () => Get.offNamed('/dashboard'), - ), - MySpacing.width(8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.titleLarge('Attendance', - fontWeight: 700, color: Colors.black), - MySpacing.height(2), - GetBuilder( - builder: (projectController) { - final projectName = - projectController.selectedProject?.name ?? - 'Select Project'; - return Row( - children: [ - const Icon(Icons.work_outline, - size: 14, color: Colors.grey), - MySpacing.width(4), - Expanded( - child: MyText.bodySmall( - projectName, - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: Colors.grey[700], - ), - ), - ], - ); - }, - ), - ], - ), - ), - ], - ), - ), - ); - } - Widget _buildFilterSearchRow() { return Padding( padding: MySpacing.xy(8, 8), @@ -358,24 +304,44 @@ class _AttendanceScreenState extends State with UIMixin { @override Widget build(BuildContext context) { - return Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(72), - child: _buildAppBar(), - ), - body: SafeArea( - child: GetBuilder( - init: attendanceController, - tag: 'attendance_dashboard_controller', - builder: (controller) { - final selectedProjectId = projectController.selectedProjectId.value; - final noProjectSelected = selectedProjectId.isEmpty; + final Color appBarColor = contentTheme.primary; - return MyRefreshIndicator( - onRefresh: _refreshData, - child: Builder( - builder: (context) { - return SingleChildScrollView( + return Scaffold( + appBar: CustomAppBar( + title: "Attendance", + backgroundColor: appBarColor, + onBackPressed: () => Get.toNamed('/dashboard'), + ), + body: Stack( + children: [ + // Gradient container at top + Container( + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), + ], + ), + ), + ), + + // Main content + SafeArea( + child: GetBuilder( + init: attendanceController, + tag: 'attendance_dashboard_controller', + builder: (controller) { + final selectedProjectId = + projectController.selectedProjectId.value; + final noProjectSelected = selectedProjectId.isEmpty; + + return MyRefreshIndicator( + onRefresh: _refreshData, + child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: MySpacing.zero, child: Column( @@ -394,12 +360,12 @@ class _AttendanceScreenState extends State with UIMixin { ), ], ), - ); - }, - ), - ); - }, - ), + ), + ); + }, + ), + ), + ], ), ); } diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index f475dc4..d25ba2e 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -6,17 +6,12 @@ import 'package:on_field_work/controller/project_controller.dart'; import 'package:on_field_work/controller/dynamicMenu/dynamic_menu_controller.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; -import 'package:on_field_work/helpers/widgets/my_card.dart'; import 'package:on_field_work/helpers/widgets/my_spacing.dart'; -import 'package:on_field_work/helpers/widgets/dashbaord/attendance_overview_chart.dart'; -import 'package:on_field_work/helpers/widgets/dashbaord/project_progress_chart.dart'; import 'package:on_field_work/helpers/widgets/dashbaord/expense_breakdown_chart.dart'; import 'package:on_field_work/helpers/widgets/dashbaord/expense_by_status_widget.dart'; import 'package:on_field_work/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart'; -import 'package:on_field_work/helpers/widgets/dashbaord/dashboard_overview_widgets.dart'; import 'package:on_field_work/view/layouts/layout.dart'; import 'package:on_field_work/helpers/utils/permission_constants.dart'; -import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart'; class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); @@ -44,64 +39,289 @@ class _DashboardScreenState extends State with UIMixin { if (mounted) setState(() {}); } - @override - Widget build(BuildContext context) { - return Layout( - child: SingleChildScrollView( - padding: const EdgeInsets.all(10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildDashboardCards(), - MySpacing.height(24), - _buildProjectSelector(), - MySpacing.height(24), - _buildAttendanceChartSection(), - MySpacing.height(12), - MySpacing.height(24), - _buildProjectProgressChartSection(), - MySpacing.height(24), - SizedBox( - width: double.infinity, - child: DashboardOverviewWidgets.teamsOverview(), - ), - MySpacing.height(24), - SizedBox( - width: double.infinity, - child: DashboardOverviewWidgets.tasksOverview(), - ), - MySpacing.height(24), - ExpenseByStatusWidget(controller: dashboardController), - MySpacing.height(24), - ExpenseTypeReportChart(), - MySpacing.height(24), - MonthlyExpenseDashboardChart(), - ], + //--------------------------------------------------------------------------- + // REUSABLE CARD (smaller, minimal) + //--------------------------------------------------------------------------- + + Widget _cardWrapper({required Widget child}) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + border: Border.all(color: Colors.black12.withOpacity(.04)), + boxShadow: [ + BoxShadow( + color: Colors.black12.withOpacity(.05), + blurRadius: 12, + offset: const Offset(0, 4), + ) + ], + ), + child: child, + ); + } + + //--------------------------------------------------------------------------- + // SECTION TITLE + //--------------------------------------------------------------------------- + + Widget _sectionTitle(String title) { + return Padding( + padding: const EdgeInsets.only(left: 4, bottom: 8), + child: Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Colors.black87, ), ), ); } - /// ---------------- Dynamic Dashboard Cards ---------------- - Widget _buildDashboardCards() { +//--------------------------------------------------------------------------- +// CONDITIONAL QUICK ACTION CARD +//--------------------------------------------------------------------------- + Widget _conditionalQuickActionCard() { + // STATIC CONDITION + String status = "O"; // <-- change if needed + bool isCheckedIn = status == "O"; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + gradient: LinearGradient( + colors: isCheckedIn + ? [Colors.red.shade200, Colors.red.shade400] + : [Colors.green.shade200, Colors.green.shade400], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: Colors.black12.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title & Status + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isCheckedIn ? "Checked-In" : "Not Checked-In", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + Icon( + isCheckedIn ? LucideIcons.log_out : LucideIcons.log_in, + color: Colors.white, + size: 24, + ), + ], + ), + const SizedBox(height: 8), + // Description + Text( + isCheckedIn + ? "You are currently checked-in. Don't forget to check-out after your work." + : "You are not checked-in yet. Please check-in to start your work.", + style: const TextStyle(color: Colors.white70, fontSize: 13), + ), + const SizedBox(height: 12), + // Action Buttons + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (!isCheckedIn) + ElevatedButton.icon( + onPressed: () { + // Check-In action + }, + label: const Text("Check-In"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade700, + foregroundColor: Colors.white, + ), + ), + if (isCheckedIn) + ElevatedButton.icon( + onPressed: () { + // Check-Out action + }, + label: const Text("Check-Out"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red.shade700, + foregroundColor: Colors.white, + ), + ), + ], + ), + ], + ), + ); + } + +//--------------------------------------------------------------------------- +// QUICK ACTIONS (updated to use the single card) +//--------------------------------------------------------------------------- + + Widget _quickActions() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionTitle("Quick Action"), // Change title to singular + _conditionalQuickActionCard(), // Use the new conditional card + ], + ); + } + //--------------------------------------------------------------------------- + // PROJECT DROPDOWN (clean compact) + //--------------------------------------------------------------------------- + + Widget _projectSelector() { return Obx(() { - if (menuController.isLoading.value) { - return SkeletonLoaders.dashboardCardsSkeleton(); + final isLoading = projectController.isLoading.value; + final expanded = projectController.isProjectSelectionExpanded.value; + final projects = projectController.projects; + final selectedId = projectController.selectedProjectId.value; + + if (isLoading) { + return const Center(child: CircularProgressIndicator()); } - if (menuController.hasError.value || menuController.menuItems.isEmpty) { - return const Center( - child: Text( - "Failed to load menus. Please try again later.", - style: TextStyle(color: Colors.red), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionTitle("Project"), + + // Compact Selector + GestureDetector( + onTap: () => projectController.isProjectSelectionExpanded.toggle(), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + border: Border.all(color: Colors.black12.withOpacity(.15)), + boxShadow: [ + BoxShadow( + color: Colors.black12.withOpacity(.04), + blurRadius: 6, + offset: const Offset(0, 2), + ) + ], + ), + child: Row( + children: [ + const Icon(Icons.work_outline, color: Colors.blue, size: 20), + const SizedBox(width: 12), + Expanded( + child: Text( + projects + .firstWhereOrNull((p) => p.id == selectedId) + ?.name ?? + "Select Project", + style: const TextStyle( + fontSize: 15, fontWeight: FontWeight.w600), + ), + ), + Icon( + expanded + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down, + size: 26, + color: Colors.black54, + ) + ], + ), + ), ), - ); + + if (expanded) _projectDropdownList(projects, selectedId), + ], + ); + }); + } + + Widget _projectDropdownList(projects, selectedId) { + return Container( + margin: const EdgeInsets.only(top: 10), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + border: Border.all(color: Colors.black12.withOpacity(.2)), + boxShadow: [ + BoxShadow( + color: Colors.black12.withOpacity(.07), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + constraints: + BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.33), + child: Column( + children: [ + TextField( + decoration: InputDecoration( + hintText: "Search project...", + isDense: true, + prefixIcon: const Icon(Icons.search), + border: + OutlineInputBorder(borderRadius: BorderRadius.circular(5)), + ), + ), + const SizedBox(height: 10), + Expanded( + child: ListView.builder( + itemCount: projects.length, + itemBuilder: (_, index) { + final project = projects[index]; + return RadioListTile( + dense: true, + value: project.id, + groupValue: selectedId, + onChanged: (v) { + if (v != null) { + projectController.updateSelectedProject(v); + projectController.isProjectSelectionExpanded.value = + false; + } + }, + title: Text(project.name), + ); + }, + ), + ), + ], + ), + ); + } +//--------------------------------------------------------------------------- + // DASHBOARD MODULE CARDS (UPDATED FOR MINIMAL PADDING / SLL SIZE) + //--------------------------------------------------------------------------- + + Widget _dashboardCards() { + return Obx(() { + if (menuController.isLoading.value) { + return const Center(child: CircularProgressIndicator()); } final projectSelected = projectController.selectedProject != null; - // Define dashboard card meta with order - final List cardOrder = [ + final cardOrder = [ MenuItems.attendance, MenuItems.employees, MenuItems.dailyTaskPlanning, @@ -109,10 +329,10 @@ class _DashboardScreenState extends State with UIMixin { MenuItems.directory, MenuItems.finance, MenuItems.documents, - MenuItems.serviceProjects + MenuItems.serviceProjects, ]; - final Map cardMeta = { + final meta = { MenuItems.attendance: _DashboardCardMeta(LucideIcons.scan_face, contentTheme.success), MenuItems.employees: @@ -131,373 +351,136 @@ class _DashboardScreenState extends State with UIMixin { _DashboardCardMeta(LucideIcons.package, contentTheme.info), }; - // Filter only available menus that exist in cardMeta - final allowedMenusMap = { - for (var menu in menuController.menuItems) - if (menu.available && cardMeta.containsKey(menu.id)) menu.id: menu + final allowed = { + for (var m in menuController.menuItems) + if (m.available && meta.containsKey(m.id)) m.id: m }; - if (allowedMenusMap.isEmpty) { - return const Center( - child: Text( - "No accessible modules found.", - style: TextStyle(color: Colors.grey), + final filtered = cardOrder.where(allowed.containsKey).toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionTitle("Modules"), + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + + // **More compact grid** + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + crossAxisSpacing: 6, + mainAxisSpacing: 6, + childAspectRatio: 1.2, // smaller & tighter + ), + + itemCount: filtered.length, + itemBuilder: (context, index) { + final id = filtered[index]; + final item = allowed[id]!; + final cardMeta = meta[id]!; + + final isEnabled = + item.name == "Attendance" ? true : projectSelected; + + return GestureDetector( + onTap: () { + if (!isEnabled) { + Get.defaultDialog( + title: "No Project Selected", + middleText: "Please select a project first.", + ); + } else { + Get.toNamed(item.mobileLink); + } + }, + child: Container( + // **Reduced padding** + padding: const EdgeInsets.all(4), + + decoration: BoxDecoration( + color: isEnabled ? Colors.white : Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.black12.withOpacity(.1), + width: 0.7, + ), + boxShadow: [ + BoxShadow( + color: Colors.black12.withOpacity(.05), + blurRadius: 4, + offset: const Offset(0, 2), + ) + ], + ), + + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + cardMeta.icon, + size: 20, // **smaller icon** + color: + isEnabled ? cardMeta.color : Colors.grey.shade400, + ), + const SizedBox(height: 3), + Text( + item.name, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 9.5, // **reduced text size** + fontWeight: FontWeight.w600, + color: + isEnabled ? Colors.black87 : Colors.grey.shade600, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + }, ), - ); - } - - // Create list of cards in fixed order - final stats = - cardOrder.where((id) => allowedMenusMap.containsKey(id)).map((id) { - final menu = allowedMenusMap[id]!; - final meta = cardMeta[id]!; - return _DashboardStatItem( - meta.icon, menu.name, meta.color, menu.mobileLink); - }).toList(); - - return LayoutBuilder(builder: (context, constraints) { - int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4); - double cardWidth = - (constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount; - - return Wrap( - spacing: 6, - runSpacing: 6, - alignment: WrapAlignment.start, - children: stats - .map((stat) => - _buildDashboardCard(stat, projectSelected, cardWidth)) - .toList(), - ); - }); + ], + ); }); } - Widget _buildDashboardCard( - _DashboardStatItem stat, bool isProjectSelected, double width) { - final isEnabled = stat.title == "Attendance" ? true : isProjectSelected; + //--------------------------------------------------------------------------- + // MAIN UI + //--------------------------------------------------------------------------- - return Opacity( - opacity: isEnabled ? 1.0 : 0.4, - child: IgnorePointer( - ignoring: !isEnabled, - child: InkWell( - onTap: () => _onDashboardCardTap(stat, isEnabled), - borderRadius: BorderRadius.circular(5), - child: MyCard.bordered( - width: width, - height: 60, - paddingAll: 4, - borderRadiusAll: 5, - border: Border.all(color: Colors.grey.withOpacity(0.15)), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: stat.color.withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Icon( - stat.icon, - size: 16, - color: stat.color, - ), - ), - MySpacing.height(4), - Flexible( - child: Text( - stat.title, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 10, - overflow: TextOverflow.ellipsis, - ), - maxLines: 2, - ), - ), - ], - ), + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xfff5f6fa), + body: Layout( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _projectSelector(), + MySpacing.height(20), + _quickActions(), + MySpacing.height(20), + // The updated module cards + _dashboardCards(), + MySpacing.height(20), + _sectionTitle("Reports & Analytics"), + _cardWrapper(child: ExpenseTypeReportChart()), + _cardWrapper( + child: + ExpenseByStatusWidget(controller: dashboardController)), + _cardWrapper(child: MonthlyExpenseDashboardChart()), + MySpacing.height(20), + ], ), ), ), ); } - - void _onDashboardCardTap(_DashboardStatItem statItem, bool isEnabled) { - if (!isEnabled) { - Get.defaultDialog( - title: "No Project Selected", - middleText: "Please select a project before accessing this module.", - confirm: ElevatedButton( - onPressed: () => Get.back(), - child: const Text("OK"), - ), - ); - } else { - Get.toNamed(statItem.route); - } - } - - /// ---------------- Project Progress Chart ---------------- - Widget _buildProjectProgressChartSection() { - return Obx(() { - if (dashboardController.projectChartData.isEmpty) { - return const Padding( - padding: EdgeInsets.all(16), - child: Center( - child: Text("No project progress data available."), - ), - ); - } - - return ClipRRect( - borderRadius: BorderRadius.circular(5), - child: SizedBox( - height: 400, - child: ProjectProgressChart( - data: dashboardController.projectChartData, - ), - ), - ); - }); - } - - /// ---------------- Attendance Chart ---------------- - Widget _buildAttendanceChartSection() { - return Obx(() { - final attendanceMenu = menuController.menuItems - .firstWhereOrNull((m) => m.id == MenuItems.attendance); - if (attendanceMenu == null || !attendanceMenu.available) - return const SizedBox.shrink(); - - final isProjectSelected = projectController.selectedProject != null; - - return Opacity( - opacity: isProjectSelected ? 1.0 : 0.4, - child: IgnorePointer( - ignoring: !isProjectSelected, - child: ClipRRect( - borderRadius: BorderRadius.circular(5), - child: SizedBox( - height: 400, - child: AttendanceDashboardChart(), - ), - ), - ), - ); - }); - } - - /// ---------------- Project Selector (Inserted between Attendance & Project Progress) - Widget _buildProjectSelector() { - return Obx(() { - final isLoading = projectController.isLoading.value; - final isExpanded = projectController.isProjectSelectionExpanded.value; - final projects = projectController.projects; - final selectedProjectId = projectController.selectedProjectId.value; - final hasProjects = projects.isNotEmpty; - - if (isLoading) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 12), - child: Center(child: CircularProgressIndicator(strokeWidth: 2)), - ); - } - - if (!hasProjects) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - children: const [ - Icon(Icons.warning_amber_outlined, color: Colors.redAccent), - SizedBox(width: 8), - Text( - "No Project Assigned", - style: TextStyle( - color: Colors.redAccent, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ); - } - - final selectedProject = - projects.firstWhereOrNull((p) => p.id == selectedProjectId); - - final searchNotifier = ValueNotifier(""); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () => projectController.isProjectSelectionExpanded.toggle(), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - border: Border.all(color: Colors.grey.withOpacity(0.15)), - color: Colors.white, - boxShadow: const [ - BoxShadow( - color: Colors.black12, - blurRadius: 1, - offset: Offset(0, 1)) - ], - ), - child: Row( - children: [ - const Icon(Icons.work_outline, - size: 18, color: Colors.blueAccent), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - selectedProject?.name ?? "Select Project", - style: const TextStyle( - fontSize: 14, fontWeight: FontWeight.w700), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 2), - Text( - "Tap to switch project (${projects.length})", - style: const TextStyle( - fontSize: 12, color: Colors.black54), - ), - ], - ), - ), - Icon( - isExpanded - ? Icons.arrow_drop_up_outlined - : Icons.arrow_drop_down_outlined, - color: Colors.black, - ), - ], - ), - ), - ), - if (isExpanded) - ValueListenableBuilder( - valueListenable: searchNotifier, - builder: (context, query, _) { - final lowerQuery = query.toLowerCase(); - final filteredProjects = lowerQuery.isEmpty - ? projects - : projects - .where((p) => p.name.toLowerCase().contains(lowerQuery)) - .toList(); - - return Container( - margin: const EdgeInsets.only(top: 8), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(5), - border: Border.all(color: Colors.grey.withOpacity(0.12)), - boxShadow: const [ - BoxShadow( - color: Colors.black12, - blurRadius: 4, - offset: Offset(0, 2)) - ], - ), - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.35), - child: Column( - children: [ - TextField( - decoration: InputDecoration( - isDense: true, - prefixIcon: const Icon(Icons.search, size: 18), - hintText: "Search project", - hintStyle: const TextStyle(fontSize: 13), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(5)), - contentPadding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 2), - ), - onChanged: (value) => searchNotifier.value = value, - ), - const SizedBox(height: 8), - if (filteredProjects.isEmpty) - const Expanded( - child: Center( - child: Text("No projects found", - style: TextStyle( - fontSize: 13, color: Colors.black54)), - ), - ) - else - Expanded( - child: ListView.builder( - shrinkWrap: true, - itemCount: filteredProjects.length, - itemBuilder: (context, index) { - final project = filteredProjects[index]; - final isSelected = - project.id == selectedProjectId; - return RadioListTile( - value: project.id, - groupValue: selectedProjectId, - onChanged: (value) { - if (value != null) { - projectController - .updateSelectedProject(value); - projectController.isProjectSelectionExpanded - .value = false; - } - }, - title: Text( - project.name, - style: TextStyle( - fontWeight: isSelected - ? FontWeight.bold - : FontWeight.normal, - color: isSelected - ? Colors.blueAccent - : Colors.black87, - ), - ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 0), - activeColor: Colors.blueAccent, - tileColor: isSelected - ? Colors.blueAccent.withOpacity(0.06) - : Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5)), - visualDensity: - const VisualDensity(vertical: -4), - ); - }, - ), - ), - ], - ), - ); - }, - ), - ], - ); - }); - } -} - -/// ---------------- Dashboard Card Models ---------------- -class _DashboardStatItem { - final IconData icon; - final String title; - final Color color; - final String route; - - _DashboardStatItem(this.icon, this.title, this.color, this.route); } class _DashboardCardMeta { diff --git a/lib/view/directory/contact_detail_screen.dart b/lib/view/directory/contact_detail_screen.dart index b031fe0..47872e4 100644 --- a/lib/view/directory/contact_detail_screen.dart +++ b/lib/view/directory/contact_detail_screen.dart @@ -13,6 +13,7 @@ import 'package:on_field_work/helpers/utils/date_time_utils.dart'; import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart'; import 'package:on_field_work/helpers/widgets/my_confirmation_dialog.dart'; import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; +import 'package:on_field_work/helpers/widgets/custom_app_bar.dart'; class ContactDetailScreen extends StatefulWidget { final ContactModel contact; @@ -23,18 +24,21 @@ class ContactDetailScreen extends StatefulWidget { } class _ContactDetailScreenState extends State - with UIMixin { + with SingleTickerProviderStateMixin, UIMixin { late final DirectoryController directoryController; late final ProjectController projectController; late Rx contactRx; + late TabController _tabController; @override void initState() { super.initState(); directoryController = Get.find(); - projectController = Get.find(); + projectController = Get.put(ProjectController()); contactRx = widget.contact.obs; + _tabController = TabController(length: 2, vsync: this); + WidgetsBinding.instance.addPostFrameCallback((_) async { await directoryController.fetchCommentsForContact(contactRx.value.id, active: true); @@ -50,65 +54,72 @@ class _ContactDetailScreenState extends State } @override - Widget build(BuildContext context) { - return DefaultTabController( - length: 2, - child: Scaffold( - backgroundColor: const Color(0xFFF5F5F5), - appBar: _buildMainAppBar(), - body: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Obx(() => _buildSubHeader(contactRx.value)), - const Divider(height: 1, thickness: 0.5, color: Colors.grey), - Expanded( - child: TabBarView(children: [ - Obx(() => _buildDetailsTab(contactRx.value)), - _buildCommentsTab(), - ]), - ), - ], - ), - ), - ), - ); + void dispose() { + _tabController.dispose(); + super.dispose(); } - PreferredSizeWidget _buildMainAppBar() { - return AppBar( + @override + Widget build(BuildContext context) { + final Color appBarColor = contentTheme.primary; + + return Scaffold( backgroundColor: const Color(0xFFF5F5F5), - elevation: 0.2, - automaticallyImplyLeading: false, - titleSpacing: 0, - title: Padding( - padding: MySpacing.xy(16, 0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios_new, - color: Colors.black, size: 20), - onPressed: () => - Get.offAllNamed('/dashboard/directory-main-page'), + body: Stack( + children: [ + // GRADIENT BEHIND APPBAR & TABBAR + Positioned.fill( + child: Column( + children: [ + Container( + height: 120, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), + ], + ), + ), + ), + Expanded(child: Container(color: Colors.grey[100])), + ], ), - MySpacing.width(8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - MyText.titleLarge('Contact Profile', - fontWeight: 700, color: Colors.black), - MySpacing.height(2), - GetBuilder(builder: (p) { - return ProjectLabel(p.selectedProject?.name); - }), - ], - ), + ), + + // MAIN CONTENT + SafeArea( + top: true, + bottom: true, + child: Column( + children: [ + // APPBAR + CustomAppBar( + title: 'Contact Profile', + backgroundColor: Colors.transparent, + onBackPressed: () => + Get.offAllNamed('/dashboard/directory-main-page'), + ), + + // SUBHEADER + TABBAR + Obx(() => _buildSubHeader(contactRx.value)), + + // TABBAR VIEW + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + Obx(() => _buildDetailsTab(contactRx.value)), + _buildCommentsTab(), + ], + ), + ), + ], ), - ], - ), + ), + ], ), ); } @@ -118,39 +129,44 @@ class _ContactDetailScreenState extends State final lastName = contact.name.split(" ").length > 1 ? contact.name.split(" ").last : ""; - return Padding( - padding: MySpacing.xy(16, 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row(children: [ - Avatar(firstName: firstName, lastName: lastName, size: 35), - MySpacing.width(12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.titleSmall(contact.name, - fontWeight: 600, color: Colors.black), - MySpacing.height(2), - MyText.bodySmall(contact.organization, - fontWeight: 500, color: Colors.grey[700]), + return Container( + color: Colors.transparent, + child: Padding( + padding: MySpacing.xy(16, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Avatar(firstName: firstName, lastName: lastName, size: 35), + MySpacing.width(12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleSmall(contact.name, + fontWeight: 600, color: Colors.black), + MySpacing.height(2), + MyText.bodySmall(contact.organization, + fontWeight: 500, color: Colors.grey[700]), + ], + ), + ]), + TabBar( + controller: _tabController, + labelColor: Colors.black, + unselectedLabelColor: Colors.grey, + indicatorColor: contentTheme.primary, + tabs: const [ + Tab(text: "Details"), + Tab(text: "Notes"), ], ), - ]), - TabBar( - labelColor: Colors.black, - unselectedLabelColor: Colors.grey, - indicatorColor: contentTheme.primary, - tabs: const [ - Tab(text: "Details"), - Tab(text: "Notes"), - ], - ), - ], + ], + ), ), ); } + // --- DETAILS TAB --- Widget _buildDetailsTab(ContactModel contact) { final tags = contact.tags.map((e) => e.name).join(", "); final bucketNames = contact.bucketIds @@ -228,7 +244,8 @@ class _ContactDetailScreenState extends State _iconInfoRow(Icons.location_on, "Address", contact.address), ]), _infoCard("Organization", [ - _iconInfoRow(Icons.business, "Organization", contact.organization), + _iconInfoRow( + Icons.business, "Organization", contact.organization), _iconInfoRow(Icons.category, "Category", category), ]), _infoCard("Meta Info", [ @@ -281,6 +298,7 @@ class _ContactDetailScreenState extends State ); } + // --- COMMENTS TAB --- Widget _buildCommentsTab() { return Obx(() { final contactId = contactRx.value.id; @@ -622,25 +640,3 @@ class _ContactDetailScreenState extends State ); } } - -class ProjectLabel extends StatelessWidget { - final String? projectName; - const ProjectLabel(this.projectName, {super.key}); - @override - Widget build(BuildContext context) { - return Row( - children: [ - const Icon(Icons.work_outline, size: 14, color: Colors.grey), - MySpacing.width(4), - Expanded( - child: MyText.bodySmall( - projectName ?? 'Select Project', - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: Colors.grey[700], - ), - ), - ], - ); - } -} diff --git a/lib/view/document/document_details_page.dart b/lib/view/document/document_details_page.dart index e645cc5..4112851 100644 --- a/lib/view/document/document_details_page.dart +++ b/lib/view/document/document_details_page.dart @@ -13,6 +13,7 @@ import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/model/document/document_edit_bottom_sheet.dart'; import 'package:on_field_work/controller/permission_controller.dart'; import 'package:on_field_work/helpers/utils/permission_constants.dart'; +import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; class DocumentDetailsPage extends StatefulWidget { final String documentId; @@ -23,7 +24,7 @@ class DocumentDetailsPage extends StatefulWidget { State createState() => _DocumentDetailsPageState(); } -class _DocumentDetailsPageState extends State { +class _DocumentDetailsPageState extends State with UIMixin { final DocumentDetailsController controller = Get.find(); @@ -49,50 +50,78 @@ class _DocumentDetailsPageState extends State { @override Widget build(BuildContext context) { + final Color appBarColor = contentTheme.primary; + return Scaffold( backgroundColor: const Color(0xFFF1F1F1), appBar: CustomAppBar( title: 'Document Details', + backgroundColor: appBarColor, 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(), - ], + body: Stack( + children: [ + // Gradient behind content + Container( + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), + ], + ), ), ), - ); - }), + + // Main content + SafeArea( + child: 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(), + ], + ), + ), + ); + }), + ), + ], + ), ); } diff --git a/lib/view/document/user_document_screen.dart b/lib/view/document/user_document_screen.dart index 8a11f63..4810d63 100644 --- a/lib/view/document/user_document_screen.dart +++ b/lib/view/document/user_document_screen.dart @@ -428,14 +428,21 @@ class _UserDocumentsPageState extends State } 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 uploadDate = doc.uploadedAt != null + ? DateFormat("dd MMM yyyy").format(doc.uploadedAt!.toLocal()) + : '-'; + final uploadTime = doc.uploadedAt != null + ? DateFormat("hh:mm a").format(doc.uploadedAt!.toLocal()) + : ''; - final iconColor = _getDocumentTypeColor(doc.documentType.name); + final uploader = + (doc.uploadedBy != null && doc.uploadedBy!.firstName.isNotEmpty) + ? "${doc.uploadedBy!.firstName} ${doc.uploadedBy!.lastName ?? ''}" + .trim() + : "You"; + + final iconColor = + _getDocumentTypeColor(doc.documentType?.name ?? 'unknown'); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -473,17 +480,16 @@ class _UserDocumentsPageState extends State 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, - ), - ), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: iconColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + _getDocumentIcon(doc.documentType?.name ?? 'unknown'), + color: iconColor, + size: 24, + )), const SizedBox(width: 14), Expanded( child: Column( @@ -497,7 +503,7 @@ class _UserDocumentsPageState extends State borderRadius: BorderRadius.circular(6), ), child: MyText.labelSmall( - doc.documentType.name, + doc.documentType?.name ?? 'Unknown', fontWeight: 600, color: iconColor, letterSpacing: 0.3, @@ -872,12 +878,18 @@ class _UserDocumentsPageState extends State } final doc = docs[index]; - final currentDate = DateFormat("dd MMM yyyy") - .format(doc.uploadedAt.toLocal()); - final prevDate = index > 0 + final currentDate = doc.uploadedAt != null ? DateFormat("dd MMM yyyy") - .format(docs[index - 1].uploadedAt.toLocal()) + .format(doc.uploadedAt!.toLocal()) + : ''; + + final prevDate = index > 0 + ? (docs[index - 1].uploadedAt != null + ? DateFormat("dd MMM yyyy").format( + docs[index - 1].uploadedAt!.toLocal()) + : '') : null; + final showDateHeader = currentDate != prevDate; return _buildDocumentCard(doc, showDateHeader); diff --git a/lib/view/employees/employee_profile_screen.dart b/lib/view/employees/employee_profile_screen.dart index 5dbcfbd..e22f57e 100644 --- a/lib/view/employees/employee_profile_screen.dart +++ b/lib/view/employees/employee_profile_screen.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:on_field_work/view/employees/employee_detail_screen.dart'; import 'package:on_field_work/view/document/user_document_screen.dart'; import 'package:on_field_work/helpers/widgets/custom_app_bar.dart'; +import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; class EmployeeProfilePage extends StatefulWidget { final String employeeId; @@ -14,7 +15,7 @@ class EmployeeProfilePage extends StatefulWidget { } class _EmployeeProfilePageState extends State - with SingleTickerProviderStateMixin { + with SingleTickerProviderStateMixin, UIMixin { late TabController _tabController; @override @@ -31,44 +32,64 @@ class _EmployeeProfilePageState extends State @override Widget build(BuildContext context) { + final Color appBarColor = contentTheme.primary; + return Scaffold( backgroundColor: const Color(0xFFF1F1F1), appBar: CustomAppBar( title: "Employee Profile", onBackPressed: () => Get.back(), + backgroundColor: appBarColor, ), - body: Column( + body: Stack( children: [ - // ---------------- TabBar outside AppBar ---------------- + // === Gradient at the top behind AppBar + TabBar === Container( - color: Colors.white, - child: TabBar( - controller: _tabController, - labelColor: Colors.black, - unselectedLabelColor: Colors.grey, - indicatorColor: Colors.red, - tabs: const [ - Tab(text: "Details"), - Tab(text: "Documents"), - ], + height: 50, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), + ], + ), ), ), - - // ---------------- TabBarView ---------------- - Expanded( - child: TabBarView( - controller: _tabController, + SafeArea( + top: false, + bottom: true, + child: Column( children: [ - // Details Tab - EmployeeDetailPage( - employeeId: widget.employeeId, - fromProfile: true, + Container( + decoration: const BoxDecoration(color: Colors.transparent), + child: TabBar( + controller: _tabController, + labelColor: Colors.white, + unselectedLabelColor: Colors.white70, + indicatorColor: Colors.white, + indicatorWeight: 3, + tabs: const [ + Tab(text: "Details"), + Tab(text: "Documents"), + ], + ), ), - - // Documents Tab - UserDocumentsPage( - entityId: widget.employeeId, - isEmployee: true, + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + EmployeeDetailPage( + employeeId: widget.employeeId, + fromProfile: true, + ), + UserDocumentsPage( + entityId: widget.employeeId, + isEmployee: true, + ), + ], + ), ), ], ), diff --git a/lib/view/employees/employees_screen.dart b/lib/view/employees/employees_screen.dart index da60eca..c0bd785 100644 --- a/lib/view/employees/employees_screen.dart +++ b/lib/view/employees/employees_screen.dart @@ -17,6 +17,7 @@ import 'package:on_field_work/helpers/utils/permission_constants.dart'; import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart'; import 'package:on_field_work/view/employees/employee_profile_screen.dart'; import 'package:on_field_work/view/employees/manage_reporting_bottom_sheet.dart'; +import 'package:on_field_work/helpers/widgets/custom_app_bar.dart'; class EmployeesScreen extends StatefulWidget { const EmployeesScreen({super.key}); @@ -104,7 +105,7 @@ class _EmployeesScreenState extends State with UIMixin { backgroundColor: Colors.transparent, builder: (_) => AssignProjectBottomSheet( employeeId: employeeId, - jobRoleId: employeeData['jobRoleId'] as String, + jobRoleId: employeeData['jobRoleId'] as String, ), ); @@ -113,98 +114,69 @@ class _EmployeesScreenState extends State with UIMixin { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - appBar: _buildAppBar(), - floatingActionButton: _buildFloatingActionButton(), - body: SafeArea( - child: GetBuilder( - init: _employeeController, - tag: 'employee_screen_controller', - builder: (_) { - _filterEmployees(_searchController.text); - return MyRefreshIndicator( - onRefresh: _refreshEmployees, - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.only(bottom: 40), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MySpacing.height(flexSpacing), - Padding( - padding: MySpacing.x(15), - child: _buildSearchField(), - ), - MySpacing.height(flexSpacing), - Padding( - padding: MySpacing.x(flexSpacing), - child: _buildEmployeeList(), - ), - ], - ), - ), - ); - }, - ), - ), - ); - } + final Color appBarColor = contentTheme.primary; - PreferredSizeWidget _buildAppBar() { - return PreferredSize( - preferredSize: const Size.fromHeight(72), - child: AppBar( - backgroundColor: const Color(0xFFF5F5F5), - elevation: 0.5, - automaticallyImplyLeading: false, - titleSpacing: 0, - title: Padding( - padding: MySpacing.xy(16, 0), - child: Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios_new, - color: Colors.black, size: 20), - onPressed: () => Get.offNamed('/dashboard'), - ), - MySpacing.width(8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.titleLarge('Employees', - fontWeight: 700, color: Colors.black), - MySpacing.height(2), - GetBuilder( - builder: (projectController) { - final projectName = - projectController.selectedProject?.name ?? - 'Select Project'; - return Row( - children: [ - const Icon(Icons.work_outline, - size: 14, color: Colors.grey), - MySpacing.width(4), - Expanded( - child: MyText.bodySmall( - projectName, - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: Colors.grey[700], - ), - ), - ], - ); - }, - ), - ], - ), - ), - ], - ), - ), + return Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + appBar: CustomAppBar( + title: "Employees", + backgroundColor: appBarColor, + projectName: Get.find().selectedProject?.name ?? + 'Select Project', + onBackPressed: () => Get.offNamed('/dashboard'), ), + body: Stack( + children: [ + // Gradient behind content + Container( + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), + ], + ), + ), + ), + + // Main content + SafeArea( + child: GetBuilder( + init: _employeeController, + tag: 'employee_screen_controller', + builder: (_) { + _filterEmployees(_searchController.text); + return MyRefreshIndicator( + onRefresh: _refreshEmployees, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only(bottom: 40), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MySpacing.height(flexSpacing), + Padding( + padding: MySpacing.x(15), + child: _buildSearchField(), + ), + MySpacing.height(flexSpacing), + Padding( + padding: MySpacing.x(flexSpacing), + child: _buildEmployeeList(), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + floatingActionButton: _buildFloatingActionButton(), ); } diff --git a/lib/view/expense/expense_detail_screen.dart b/lib/view/expense/expense_detail_screen.dart index 6d0c4be..3c459c6 100644 --- a/lib/view/expense/expense_detail_screen.dart +++ b/lib/view/expense/expense_detail_screen.dart @@ -14,7 +14,7 @@ import 'package:on_field_work/controller/expense/add_expense_controller.dart'; import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart'; - +import 'package:on_field_work/helpers/widgets/custom_app_bar.dart'; import 'package:on_field_work/helpers/widgets/expense/expense_detail_helpers.dart'; import 'package:on_field_work/helpers/widgets/my_spacing.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart'; @@ -81,34 +81,62 @@ class _ExpenseDetailScreenState extends State canSubmit.value = result; } - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF7F7F7), - appBar: _AppBar(projectController: projectController), - body: SafeArea( - child: Obx(() { - if (controller.isLoading.value) return buildLoadingSkeleton(); - final expense = controller.expense.value; - if (controller.errorMessage.isNotEmpty || expense == null) { - return Center(child: MyText.bodyMedium("No data to display.")); - } + @override +Widget build(BuildContext context) { + final Color appBarColor = contentTheme.primary; - WidgetsBinding.instance.addPostFrameCallback((_) { - _checkPermissionToSubmit(expense); - }); + return Scaffold( + backgroundColor: const Color(0xFFF7F7F7), + appBar: CustomAppBar( + title: "Expense Details", + backgroundColor: appBarColor, + onBackPressed: () => Get.toNamed('/dashboard/expense-main-page'), + ), + body: Stack( + children: [ + // Gradient behind content + Container( + height: kToolbarHeight + MediaQuery.of(context).padding.top, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), + ], + ), + ), + ), - final statusColor = getExpenseStatusColor(expense.status.name, - colorCode: expense.status.color); - final formattedAmount = formatExpenseAmount(expense.amount); + // Main content + SafeArea( + child: Obx(() { + if (controller.isLoading.value) return buildLoadingSkeleton(); - return MyRefreshIndicator( + final expense = controller.expense.value; + if (controller.errorMessage.isNotEmpty || expense == null) { + return Center(child: MyText.bodyMedium("No data to display.")); + } + + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkPermissionToSubmit(expense); + }); + + final statusColor = getExpenseStatusColor( + expense.status.name, + colorCode: expense.status.color, + ); + final formattedAmount = formatExpenseAmount(expense.amount); + + return MyRefreshIndicator( onRefresh: () async { await controller.fetchExpenseDetails(); }, child: SingleChildScrollView( padding: EdgeInsets.fromLTRB( - 12, 12, 12, 30 + MediaQuery.of(context).padding.bottom), + 12, 12, 12, 30 + MediaQuery.of(context).padding.bottom + ), child: Center( child: Container( constraints: const BoxConstraints(maxWidth: 520), @@ -122,21 +150,21 @@ class _ExpenseDetailScreenState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // ---------------- Header & Status ---------------- + // Header & Status _InvoiceHeader(expense: expense), const Divider(height: 30, thickness: 1.2), - // ---------------- Activity Logs ---------------- + // Activity Logs InvoiceLogs(logs: expense.expenseLogs), const Divider(height: 30, thickness: 1.2), - // ---------------- Amount & Summary ---------------- + + // Amount & Summary Row( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - MyText.bodyMedium('Amount', - fontWeight: 600), + MyText.bodyMedium('Amount', fontWeight: 600), const SizedBox(height: 4), MyText.bodyLarge( formattedAmount, @@ -146,7 +174,6 @@ class _ExpenseDetailScreenState extends State ], ), const Spacer(), - // Optional: Pre-approved badge if (expense.preApproved) Container( padding: const EdgeInsets.symmetric( @@ -165,19 +192,19 @@ class _ExpenseDetailScreenState extends State ), const Divider(height: 30, thickness: 1.2), - // ---------------- Parties ---------------- + // Parties _InvoicePartiesTable(expense: expense), const Divider(height: 30, thickness: 1.2), - // ---------------- Expense Details ---------------- + // Expense Details _InvoiceDetailsTable(expense: expense), const Divider(height: 30, thickness: 1.2), - // ---------------- Documents ---------------- + // Documents _InvoiceDocuments(documents: expense.documents), const Divider(height: 30, thickness: 1.2), - // ---------------- Totals ---------------- + // Totals _InvoiceTotals( expense: expense, formattedAmount: formattedAmount, @@ -189,122 +216,109 @@ class _ExpenseDetailScreenState extends State ), ), ), - )); - }), - ), - floatingActionButton: Obx(() { - if (controller.isLoading.value) return buildLoadingSkeleton(); + ), + ); + }), + ), + ], + ), + floatingActionButton: Obx(() { + if (controller.isLoading.value) return buildLoadingSkeleton(); - final expense = controller.expense.value; - if (controller.errorMessage.isNotEmpty || expense == null) { - return Center(child: MyText.bodyMedium("No data to display.")); - } + final expense = controller.expense.value; + if (controller.errorMessage.isNotEmpty || expense == null) { + return const SizedBox.shrink(); + } - if (!_checkedPermission) { - _checkedPermission = true; - WidgetsBinding.instance.addPostFrameCallback((_) { - _checkPermissionToSubmit(expense); - }); - } + if (!_checkedPermission) { + _checkedPermission = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkPermissionToSubmit(expense); + }); + } - if (!ExpensePermissionHelper.canEditExpense(employeeInfo, expense)) { - return const SizedBox.shrink(); - } + if (!ExpensePermissionHelper.canEditExpense(employeeInfo, expense)) { + return const SizedBox.shrink(); + } - return FloatingActionButton.extended( - onPressed: () async { - final editData = { - 'id': expense.id, - 'projectName': expense.project.name, - 'amount': expense.amount, - 'supplerName': expense.supplerName, - 'description': expense.description, - 'transactionId': expense.transactionId, - 'location': expense.location, - 'transactionDate': expense.transactionDate, - 'noOfPersons': expense.noOfPersons, - 'expensesTypeId': expense.expensesType.id, - 'paymentModeId': expense.paymentMode.id, - 'paidById': expense.paidBy.id, - 'paidByFirstName': expense.paidBy.firstName, - 'paidByLastName': expense.paidBy.lastName, - 'attachments': expense.documents - .map((doc) => { - 'url': doc.preSignedUrl, - 'fileName': doc.fileName, - 'documentId': doc.documentId, - 'contentType': doc.contentType, - }) - .toList(), - }; - logSafe('editData: $editData', level: LogLevel.info); + return FloatingActionButton.extended( + onPressed: () async { + final editData = { + 'id': expense.id, + 'projectName': expense.project.name, + 'amount': expense.amount, + 'supplerName': expense.supplerName, + 'description': expense.description, + 'transactionId': expense.transactionId, + 'location': expense.location, + 'transactionDate': expense.transactionDate, + 'noOfPersons': expense.noOfPersons, + 'expensesTypeId': expense.expensesType.id, + 'paymentModeId': expense.paymentMode.id, + 'paidById': expense.paidBy.id, + 'paidByFirstName': expense.paidBy.firstName, + 'paidByLastName': expense.paidBy.lastName, + 'attachments': expense.documents + .map((doc) => { + 'url': doc.preSignedUrl, + 'fileName': doc.fileName, + 'documentId': doc.documentId, + 'contentType': doc.contentType, + }) + .toList(), + }; - final addCtrl = Get.put(AddExpenseController()); + final addCtrl = Get.put(AddExpenseController()); + await addCtrl.loadMasterData(); + addCtrl.populateFieldsForEdit(editData); - await addCtrl.loadMasterData(); - addCtrl.populateFieldsForEdit(editData); + await showAddExpenseBottomSheet(isEdit: true); + await controller.fetchExpenseDetails(); + }, + backgroundColor: contentTheme.primary, + icon: const Icon(Icons.edit), + label: MyText.bodyMedium("Edit Expense", + fontWeight: 600, color: Colors.white), + ); + }), + bottomNavigationBar: Obx(() { + final expense = controller.expense.value; + if (expense == null) return const SizedBox(); - await showAddExpenseBottomSheet(isEdit: true); - await controller.fetchExpenseDetails(); - }, - backgroundColor: contentTheme.primary, - icon: const Icon(Icons.edit), - label: MyText.bodyMedium("Edit Expense", - fontWeight: 600, color: Colors.white), - ); - }), - bottomNavigationBar: Obx(() { - final expense = controller.expense.value; - if (expense == null) return const SizedBox(); - - return SafeArea( - child: Container( - decoration: const BoxDecoration( - color: Colors.white, - border: Border(top: BorderSide(color: Color(0x11000000))), - ), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - child: Wrap( - alignment: WrapAlignment.center, - spacing: 10, - runSpacing: 10, - children: expense.nextStatus.where((next) { - const submitStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7'; - - final rawPermissions = next.permissionIds; - final parsedPermissions = - controller.parsePermissionIds(rawPermissions); - - final isSubmitStatus = next.id == submitStatusId; - final isCreatedByCurrentUser = - employeeInfo?.id == expense.createdBy.id; - - logSafe( - '🔐 Permission Logic:\n' - '🔸 Status: ${next.name}\n' - '🔸 Status ID: ${next.id}\n' - '🔸 Parsed Permissions: $parsedPermissions\n' - '🔸 Is Submit: $isSubmitStatus\n' - '🔸 Created By Current User: $isCreatedByCurrentUser', - level: LogLevel.debug, - ); - - if (isSubmitStatus) { - // Submit can be done ONLY by the creator - return isCreatedByCurrentUser; - } - - // All other statuses - check permission normally - return permissionController.hasAnyPermission(parsedPermissions); - }).map((next) { - return _statusButton(context, controller, expense, next); - }).toList(), - ), + return SafeArea( + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Color(0x11000000))), ), - ); - }), - ); - } + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + child: Wrap( + alignment: WrapAlignment.center, + spacing: 10, + runSpacing: 10, + children: expense.nextStatus.where((next) { + const submitStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7'; + + final rawPermissions = next.permissionIds; + final parsedPermissions = + controller.parsePermissionIds(rawPermissions); + + final isSubmitStatus = next.id == submitStatusId; + final isCreatedByCurrentUser = + employeeInfo?.id == expense.createdBy.id; + + if (isSubmitStatus) return isCreatedByCurrentUser; + return permissionController.hasAnyPermission(parsedPermissions); + }).map((next) { + return _statusButton(context, controller, expense, next); + }).toList(), + ), + ), + ); + }), + ); +} + Widget _statusButton(BuildContext context, ExpenseDetailController controller, ExpenseDetailModel expense, dynamic next) { @@ -449,64 +463,6 @@ class _ExpenseDetailScreenState extends State } } -class _AppBar extends StatelessWidget implements PreferredSizeWidget { - final ProjectController projectController; - const _AppBar({required this.projectController}); - @override - Widget build(BuildContext context) { - return AppBar( - automaticallyImplyLeading: false, - elevation: 1, - backgroundColor: Colors.white, - title: Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios_new, - color: Colors.black, size: 20), - onPressed: () => Get.toNamed('/dashboard/expense-main-page'), - ), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.titleLarge('Expense Details', - fontWeight: 700, color: Colors.black), - MySpacing.height(2), - GetBuilder( - builder: (_) { - final projectName = - projectController.selectedProject?.name ?? - 'Select Project'; - return Row( - children: [ - const Icon(Icons.work_outline, - size: 14, color: Colors.grey), - MySpacing.width(4), - Expanded( - child: MyText.bodySmall( - projectName, - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: Colors.grey[700], - ), - ), - ], - ); - }, - ), - ], - ), - ), - ], - ), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} - class _InvoiceHeader extends StatelessWidget { final ExpenseDetailModel expense; const _InvoiceHeader({required this.expense}); diff --git a/lib/view/expense/expense_screen.dart b/lib/view/expense/expense_screen.dart index d0f2c9f..821c6a0 100644 --- a/lib/view/expense/expense_screen.dart +++ b/lib/view/expense/expense_screen.dart @@ -12,6 +12,7 @@ import 'package:on_field_work/helpers/widgets/expense/expense_main_components.da import 'package:on_field_work/helpers/utils/permission_constants.dart'; import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart'; import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; +import 'package:on_field_work/helpers/widgets/custom_app_bar.dart'; class ExpenseMainScreen extends StatefulWidget { const ExpenseMainScreen({super.key}); @@ -87,65 +88,103 @@ class _ExpenseMainScreenState extends State @override Widget build(BuildContext context) { + final Color appBarColor = contentTheme.primary; + return Scaffold( backgroundColor: Colors.white, - appBar: ExpenseAppBar(projectController: projectController), - body: Column( + appBar: CustomAppBar( + title: "Expense & Reimbursement", + backgroundColor: appBarColor, + onBackPressed: () => Get.toNamed('/dashboard/finance'), + ), + body: Stack( children: [ - // ---------------- TabBar ---------------- - Container( - color: Colors.white, - child: TabBar( - controller: _tabController, - labelColor: Colors.black, - unselectedLabelColor: Colors.grey, - indicatorColor: Colors.red, - tabs: const [ - Tab(text: "Current Month"), - Tab(text: "History"), + // === FULL GRADIENT BEHIND APPBAR & TABBAR === + Positioned.fill( + child: Column( + children: [ + Container( + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), + ], + ), + ), + ), + Expanded( + child: + Container(color: Colors.grey[100]), + ), ], ), ), - // ---------------- Gray background for rest ---------------- - Expanded( - child: Container( - color: Colors.grey[100], - child: Column( - children: [ - // ---------------- Search ---------------- - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 0, vertical: 0), - child: SearchAndFilter( - controller: searchController, - onChanged: (_) => setState(() {}), - onFilterTap: _openFilterBottomSheet, - expenseController: expenseController, - ), + // === MAIN CONTENT === + SafeArea( + top: false, + bottom: true, + child: Column( + children: [ + // TAB BAR WITH TRANSPARENT BACKGROUND + Container( + decoration: const BoxDecoration(color: Colors.transparent), + child: TabBar( + controller: _tabController, + labelColor: Colors.white, + unselectedLabelColor: Colors.white70, + indicatorColor: Colors.white, + indicatorWeight: 3, + tabs: const [ + Tab(text: "Current Month"), + Tab(text: "History"), + ], ), + ), - // ---------------- TabBarView ---------------- - Expanded( - child: TabBarView( - controller: _tabController, + // CONTENT AREA + Expanded( + child: Container( + color: Colors.transparent, + child: Column( children: [ - _buildExpenseList(isHistory: false), - _buildExpenseList(isHistory: true), + // SEARCH & FILTER + Padding( + padding: const EdgeInsets.symmetric(horizontal: 0), + child: SearchAndFilter( + controller: searchController, + onChanged: (_) => setState(() {}), + onFilterTap: _openFilterBottomSheet, + expenseController: expenseController, + ), + ), + + // TABBAR VIEW + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildExpenseList(isHistory: false), + _buildExpenseList(isHistory: true), + ], + ), + ), ], ), ), - ], - ), + ), + ], ), ), ], ), floatingActionButton: Obx(() { - // Show loader or hide FAB while permissions are loading - if (permissionController.permissions.isEmpty) { + if (permissionController.permissions.isEmpty) return const SizedBox.shrink(); - } final canUpload = permissionController.hasPermission(Permissions.expenseUpload); diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart index 0334c45..7fa2795 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -3,10 +3,9 @@ import 'package:get/get.dart'; import 'package:on_field_work/controller/finance/advance_payment_controller.dart'; import 'package:on_field_work/controller/project_controller.dart'; import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; -import 'package:on_field_work/helpers/widgets/my_text.dart'; -import 'package:on_field_work/helpers/widgets/my_spacing.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; +import 'package:on_field_work/helpers/widgets/custom_app_bar.dart'; class AdvancePaymentScreen extends StatefulWidget { const AdvancePaymentScreen({super.key}); @@ -49,148 +48,106 @@ class _AdvancePaymentScreenState extends State @override Widget build(BuildContext context) { + final Color appBarColor = contentTheme.primary; + return Scaffold( backgroundColor: const Color(0xFFF5F5F5), - appBar: _buildAppBar(), - - // ✅ SafeArea added so nothing hides under system navigation buttons - body: SafeArea( - bottom: true, - child: GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), - child: RefreshIndicator( - onRefresh: () async { - final emp = controller.selectedEmployee.value; - if (emp != null) { - await controller.fetchAdvancePayments(emp.id.toString()); - } - }, - color: Colors.white, - backgroundColor: contentTheme.primary, - strokeWidth: 2.5, - displacement: 60, - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: Padding( - // ✅ Extra bottom padding so content does NOT go under 3-button navbar - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom + 20, - ), - - child: Column( - children: [ - _buildSearchBar(), - _buildEmployeeDropdown(context), - _buildTopBalance(), - _buildPaymentList(), - ], - ), + appBar: CustomAppBar( + title: "Advance Payments", + onBackPressed: () => Get.offNamed('/dashboard/finance'), + backgroundColor: appBarColor, + ), + body: Stack( + children: [ + // ===== TOP GRADIENT ===== + Container( + height: 100, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), + ], ), ), ), - ), - ), - ); - } - // ---------------- AppBar ---------------- - PreferredSizeWidget _buildAppBar() { - return PreferredSize( - preferredSize: const Size.fromHeight(72), - child: AppBar( - backgroundColor: const Color(0xFFF5F5F5), - elevation: 0.5, - automaticallyImplyLeading: false, - titleSpacing: 0, - title: Padding( - padding: MySpacing.xy(16, 0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios_new, - color: Colors.black, size: 20), - onPressed: () => Get.offNamed('/dashboard/finance'), - ), - MySpacing.width(8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - MyText.titleLarge( - 'Advance Payments', - fontWeight: 700, - color: Colors.black, + // ===== MAIN CONTENT ===== + SafeArea( + top: false, + bottom: true, + child: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: RefreshIndicator( + onRefresh: () async { + final emp = controller.selectedEmployee.value; + if (emp != null) { + await controller.fetchAdvancePayments(emp.id.toString()); + } + }, + color: Colors.white, + backgroundColor: appBarColor, + strokeWidth: 2.5, + displacement: 60, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom + 20, ), - MySpacing.height(2), - GetBuilder( - builder: (_) { - final name = projectController.selectedProject?.name ?? - 'Select Project'; - return Row( - children: [ - const Icon(Icons.work_outline, - size: 14, color: Colors.grey), - MySpacing.width(4), - Expanded( - child: MyText.bodySmall( - name, - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: Colors.grey[700], + child: Column( + children: [ + // ===== SEARCH BAR FLOATING OVER GRADIENT ===== + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 8), + child: SizedBox( + height: 38, + child: TextField( + controller: _searchCtrl, + focusNode: _searchFocus, + onChanged: (v) => + controller.searchQuery.value = v.trim(), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 0), + prefixIcon: const Icon(Icons.search, + size: 20, color: Colors.grey), + hintText: 'Search Employee...', + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: Colors.grey.shade300, width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: Colors.grey.shade300, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: appBarColor, width: 1.5), + ), ), ), - ], - ); - }, - ), - ], - ), - ), - ], - ), - ), - ), - ); - } + ), + ), - // ---------------- Search ---------------- - Widget _buildSearchBar() { - return Container( - color: Colors.grey[100], - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: Row( - children: [ - Expanded( - child: SizedBox( - height: 38, - child: TextField( - controller: _searchCtrl, - focusNode: _searchFocus, - onChanged: (v) => controller.searchQuery.value = v.trim(), - decoration: InputDecoration( - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 0), - prefixIcon: - const Icon(Icons.search, size: 20, color: Colors.grey), - hintText: 'Search Employee...', - filled: true, - fillColor: Colors.white, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: - BorderSide(color: Colors.grey.shade300, width: 1), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: - BorderSide(color: Colors.grey.shade300, width: 1), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: - BorderSide(color: contentTheme.primary, width: 1.5), + // ===== EMPLOYEE DROPDOWN ===== + _buildEmployeeDropdown(context), + + // ===== TOP BALANCE ===== + _buildTopBalance(), + + // ===== PAYMENTS LIST ===== + _buildPaymentList(), + ], + ), ), ), ), diff --git a/lib/view/finance/finance_screen.dart b/lib/view/finance/finance_screen.dart index 55e7716..4f50dde 100644 --- a/lib/view/finance/finance_screen.dart +++ b/lib/view/finance/finance_screen.dart @@ -6,13 +6,14 @@ import 'package:on_field_work/controller/dynamicMenu/dynamic_menu_controller.dar import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; import 'package:on_field_work/helpers/widgets/my_card.dart'; import 'package:on_field_work/helpers/widgets/my_spacing.dart'; -import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:on_field_work/helpers/widgets/dashbaord/expense_breakdown_chart.dart'; import 'package:on_field_work/helpers/widgets/dashbaord/expense_by_status_widget.dart'; import 'package:on_field_work/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart'; import 'package:on_field_work/controller/dashboard/dashboard_controller.dart'; import 'package:on_field_work/helpers/utils/permission_constants.dart'; import 'package:on_field_work/model/dynamicMenu/dynamic_menu_model.dart'; +import 'package:on_field_work/helpers/widgets/custom_app_bar.dart'; + class FinanceScreen extends StatefulWidget { const FinanceScreen({super.key}); @@ -52,132 +53,116 @@ class _FinanceScreenState extends State @override Widget build(BuildContext context) { + final Color appBarColor = contentTheme.primary; + return Scaffold( backgroundColor: const Color(0xFFF8F9FA), - appBar: PreferredSize( - preferredSize: const Size.fromHeight(72), - child: AppBar( - backgroundColor: const Color(0xFFF5F5F5), - elevation: 0.5, - automaticallyImplyLeading: false, - titleSpacing: 0, - title: Padding( - padding: MySpacing.xy(16, 0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios_new, - color: Colors.black, size: 20), - onPressed: () => Get.offNamed('/dashboard'), - ), - MySpacing.width(8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - MyText.titleLarge( - 'Finance', - fontWeight: 700, - color: Colors.black, - ), - MySpacing.height(2), - GetBuilder( - builder: (projectController) { - final projectName = - projectController.selectedProject?.name ?? - 'Select Project'; - return Row( - children: [ - const Icon(Icons.work_outline, - size: 14, color: Colors.grey), - MySpacing.width(4), - Expanded( - child: MyText.bodySmall( - projectName, - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: Colors.grey[700], - ), - ), - ], - ); - }, - ), - ], - ), - ), - ], - ), - ), - ), + appBar: CustomAppBar( + title: "Finance", + onBackPressed: () => Get.offAllNamed( '/dashboard' ), + backgroundColor: appBarColor, ), - body: SafeArea( - top: false, // keep appbar area same - bottom: true, // avoid system bottom buttons - child: FadeTransition( - opacity: _fadeAnimation, - child: Obx(() { - if (menuController.isLoading.value) { - return const Center(child: CircularProgressIndicator()); - } - - if (menuController.hasError.value || - menuController.menuItems.isEmpty) { - return const Center( - child: Text( - "Failed to load menus. Please try again later.", - style: TextStyle(color: Colors.red), - ), - ); - } - - final financeMenuIds = [ - MenuItems.expenseReimbursement, - MenuItems.paymentRequests, - MenuItems.advancePaymentStatements, - ]; - - final financeMenus = menuController.menuItems - .where((m) => financeMenuIds.contains(m.id) && m.available) - .toList(); - - if (financeMenus.isEmpty) { - return const Center( - child: Text( - "You don’t have access to the Finance section.", - style: TextStyle(color: Colors.grey), - ), - ); - } - - // ---- IMPORTANT FIX: Add bottom safe padding ---- - final double bottomInset = - MediaQuery.of(context).viewPadding.bottom; - - return SingleChildScrollView( - padding: EdgeInsets.fromLTRB( - 16, - 16, - 16, - bottomInset + - 24, // ensures charts never go under system buttons - ), - child: Column( - children: [ - _buildFinanceModulesCompact(financeMenus), - MySpacing.height(24), - ExpenseByStatusWidget(controller: dashboardController), - MySpacing.height(24), - ExpenseTypeReportChart(), - MySpacing.height(24), - MonthlyExpenseDashboardChart(), + body: Stack( + children: [ + // Top fade under AppBar + Container( + height: 40, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), ], ), - ); - }), - ), + ), + ), + + // Bottom fade (above system buttons or FAB) + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: 60, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + appBarColor.withOpacity(0.05), + Colors.transparent, + ], + ), + ), + ), + ), + + // Main scrollable content + SafeArea( + top: false, + bottom: true, + child: FadeTransition( + opacity: _fadeAnimation, + child: Obx(() { + if (menuController.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (menuController.hasError.value || + menuController.menuItems.isEmpty) { + return const Center( + child: Text( + "Failed to load menus. Please try again later.", + style: TextStyle(color: Colors.red), + ), + ); + } + + final financeMenuIds = [ + MenuItems.expenseReimbursement, + MenuItems.paymentRequests, + MenuItems.advancePaymentStatements, + ]; + + final financeMenus = menuController.menuItems + .where((m) => financeMenuIds.contains(m.id) && m.available) + .toList(); + + if (financeMenus.isEmpty) { + return const Center( + child: Text( + "You don’t have access to the Finance section.", + style: TextStyle(color: Colors.grey), + ), + ); + } + + final double bottomInset = + MediaQuery.of(context).viewPadding.bottom; + + return SingleChildScrollView( + padding: EdgeInsets.fromLTRB( + 16, + 16, + 16, + bottomInset + 24, + ), + child: Column( + children: [ + _buildFinanceModulesCompact(financeMenus), + MySpacing.height(24), + ExpenseByStatusWidget(controller: dashboardController), + MySpacing.height(24), + ExpenseTypeReportChart(), + MySpacing.height(24), + MonthlyExpenseDashboardChart(), + ], + ), + ); + }), + ), + ), + ], ), ); } diff --git a/lib/view/finance/payment_request_detail_screen.dart b/lib/view/finance/payment_request_detail_screen.dart index 84fc219..e53ac85 100644 --- a/lib/view/finance/payment_request_detail_screen.dart +++ b/lib/view/finance/payment_request_detail_screen.dart @@ -21,6 +21,7 @@ import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/model/finance/payment_request_rembursement_bottom_sheet.dart'; import 'package:on_field_work/model/finance/make_expense_bottom_sheet.dart'; import 'package:on_field_work/model/finance/add_payment_request_bottom_sheet.dart'; +import 'package:on_field_work/helpers/widgets/custom_app_bar.dart'; class PaymentRequestDetailScreen extends StatefulWidget { final String paymentRequestId; @@ -107,76 +108,101 @@ class _PaymentRequestDetailScreenState extends State @override Widget build(BuildContext context) { + final Color appBarColor = contentTheme.primary; + return Scaffold( backgroundColor: Colors.white, - appBar: _buildAppBar(), - body: SafeArea( - child: Obx(() { - if (controller.isLoading.value && - controller.paymentRequest.value == null) { - return SkeletonLoaders.paymentRequestDetailSkeletonLoader(); - } - - final request = controller.paymentRequest.value; - - if ((controller.errorMessage.value).isNotEmpty) { - return Center( - child: MyText.bodyMedium(controller.errorMessage.value)); - } - - if (request == null) { - return Center(child: MyText.bodyMedium("No data to display.")); - } - - return MyRefreshIndicator( - onRefresh: controller.fetchPaymentRequestDetail, - child: SingleChildScrollView( - padding: EdgeInsets.fromLTRB( - 12, - 12, - 12, - 60 + MediaQuery.of(context).padding.bottom, + appBar: CustomAppBar( + title: "Payment Request Details", + backgroundColor: appBarColor, + ), + body: Stack( + children: [ + // ===== TOP GRADIENT ===== + Container( + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), + ], ), - child: Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 520), - child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5)), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 12, horizontal: 14), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _Header( - request: request, - colorParser: _parseColor, - employeeInfo: employeeInfo, - onEdit: () => - _openEditPaymentRequestBottomSheet(request), + ), + ), + + // ===== MAIN CONTENT ===== + SafeArea( + child: Obx(() { + if (controller.isLoading.value && + controller.paymentRequest.value == null) { + return SkeletonLoaders.paymentRequestDetailSkeletonLoader(); + } + + final request = controller.paymentRequest.value; + + if ((controller.errorMessage.value).isNotEmpty) { + return Center( + child: MyText.bodyMedium(controller.errorMessage.value)); + } + + if (request == null) { + return Center(child: MyText.bodyMedium("No data to display.")); + } + + return MyRefreshIndicator( + onRefresh: controller.fetchPaymentRequestDetail, + child: SingleChildScrollView( + padding: EdgeInsets.fromLTRB( + 12, + 12, + 12, + 60 + MediaQuery.of(context).padding.bottom, + ), + child: Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 520), + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5)), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, horizontal: 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Header( + request: request, + colorParser: _parseColor, + employeeInfo: employeeInfo, + onEdit: () => + _openEditPaymentRequestBottomSheet(request), + ), + const Divider(height: 30, thickness: 1.2), + _Logs( + logs: request.updateLogs ?? [], + colorParser: _parseColor, + ), + const Divider(height: 30, thickness: 1.2), + _Parties(request: request), + const Divider(height: 30, thickness: 1.2), + _DetailsTable(request: request), + const Divider(height: 30, thickness: 1.2), + _Documents(documents: request.attachments ?? []), + MySpacing.height(24), + ], ), - const Divider(height: 30, thickness: 1.2), - _Logs( - logs: request.updateLogs ?? [], - colorParser: _parseColor, - ), - const Divider(height: 30, thickness: 1.2), - _Parties(request: request), - const Divider(height: 30, thickness: 1.2), - _DetailsTable(request: request), - const Divider(height: 30, thickness: 1.2), - _Documents(documents: request.attachments ?? []), - MySpacing.height(24), - ], + ), ), ), ), ), - ), - ), - ); - }), + ); + }), + ), + ], ), bottomNavigationBar: _buildBottomActionBar(), ); @@ -275,7 +301,7 @@ class _PaymentRequestDetailScreenState extends State ); return; } - + showAppSnackbar( title: 'Success', message: 'Status updated successfully', @@ -295,65 +321,6 @@ class _PaymentRequestDetailScreenState extends State ); }); } - - PreferredSizeWidget _buildAppBar() { - return PreferredSize( - preferredSize: const Size.fromHeight(72), - child: AppBar( - backgroundColor: const Color(0xFFF5F5F5), - elevation: 0.5, - automaticallyImplyLeading: false, - titleSpacing: 0, - title: Padding( - padding: MySpacing.xy(16, 0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios_new, - color: Colors.black, size: 20), - onPressed: () => Get.back(), - ), - MySpacing.width(8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - MyText.titleLarge( - 'Payment Request Details', - fontWeight: 700, - color: Colors.black, - ), - MySpacing.height(2), - GetBuilder(builder: (_) { - final name = projectController.selectedProject?.name ?? - 'Select Project'; - return Row( - children: [ - const Icon(Icons.work_outline, - size: 14, color: Colors.grey), - MySpacing.width(4), - Expanded( - child: MyText.bodySmall( - name, - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: Colors.grey[700], - ), - ), - ], - ); - }), - ], - ), - ), - ], - ), - ), - ), - ); - } } class PaymentRequestPermissionHelper { diff --git a/lib/view/finance/payment_request_screen.dart b/lib/view/finance/payment_request_screen.dart index 5c46a37..0564d3f 100644 --- a/lib/view/finance/payment_request_screen.dart +++ b/lib/view/finance/payment_request_screen.dart @@ -13,6 +13,7 @@ import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart'; import 'package:on_field_work/controller/permission_controller.dart'; import 'package:on_field_work/helpers/utils/permission_constants.dart'; import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart'; +import 'package:on_field_work/helpers/widgets/custom_app_bar.dart'; class PaymentRequestMainScreen extends StatefulWidget { const PaymentRequestMainScreen({super.key}); @@ -96,53 +97,88 @@ class _PaymentRequestMainScreenState extends State @override Widget build(BuildContext context) { + final Color appBarColor = contentTheme.primary; + return Scaffold( backgroundColor: Colors.white, - appBar: _buildAppBar(), - - // ------------------------ - // FIX: SafeArea prevents content from going under 3-button navbar - // ------------------------ - body: SafeArea( - bottom: true, - child: Column( - children: [ - Container( - color: Colors.white, - child: TabBar( - controller: _tabController, - labelColor: Colors.black, - unselectedLabelColor: Colors.grey, - indicatorColor: Colors.red, - tabs: const [ - Tab(text: "Current Month"), - Tab(text: "History"), - ], - ), - ), - Expanded( - child: Container( - color: Colors.grey[100], - child: Column( - children: [ - _buildSearchBar(), - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - _buildPaymentRequestList(isHistory: false), - _buildPaymentRequestList(isHistory: true), - ], - ), - ), - ], - ), - ), - ), - ], - ), + appBar: CustomAppBar( + title: "Payment Requests", + onBackPressed: () => Get.offNamed('/dashboard/finance'), + backgroundColor: appBarColor, ), + body: Stack( + children: [ + // === FULL GRADIENT BEHIND APPBAR & TABBAR === + Positioned.fill( + child: Column( + children: [ + Container( + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), + ], + ), + ), + ), + Expanded( + child: + Container(color: Colors.grey[100]), + ), + ], + ), + ), + // === MAIN CONTENT === + SafeArea( + top: false, + bottom: true, + child: Column( + children: [ + // TAB BAR WITH TRANSPARENT BACKGROUND + Container( + decoration: const BoxDecoration(color: Colors.transparent), + child: TabBar( + controller: _tabController, + labelColor: Colors.white, + unselectedLabelColor: Colors.white70, + indicatorColor: Colors.white, + tabs: const [ + Tab(text: "Current Month"), + Tab(text: "History"), + ], + ), + ), + + // CONTENT AREA + Expanded( + child: Container( + color: Colors.transparent, + child: Column( + children: [ + _buildSearchBar(), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildPaymentRequestList(isHistory: false), + _buildPaymentRequestList(isHistory: true), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), floatingActionButton: Obx(() { if (permissionController.permissions.isEmpty) { return const SizedBox.shrink(); @@ -166,67 +202,6 @@ class _PaymentRequestMainScreenState extends State ); } - PreferredSizeWidget _buildAppBar() { - return PreferredSize( - preferredSize: const Size.fromHeight(72), - child: AppBar( - backgroundColor: const Color(0xFFF5F5F5), - elevation: 0.5, - automaticallyImplyLeading: false, - titleSpacing: 0, - title: Padding( - padding: MySpacing.xy(16, 0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios_new, - color: Colors.black, size: 20), - onPressed: () => Get.offNamed('/dashboard/finance'), - ), - MySpacing.width(8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - MyText.titleLarge( - 'Payment Requests', - fontWeight: 700, - color: Colors.black, - ), - MySpacing.height(2), - GetBuilder( - builder: (_) { - final name = projectController.selectedProject?.name ?? - 'Select Project'; - return Row( - children: [ - const Icon(Icons.work_outline, - size: 14, color: Colors.grey), - MySpacing.width(4), - Expanded( - child: MyText.bodySmall( - name, - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: Colors.grey[700], - ), - ), - ], - ); - }, - ), - ], - ), - ), - ], - ), - ), - ), - ); - } - Widget _buildSearchBar() { return Padding( padding: MySpacing.fromLTRB(12, 10, 12, 0), diff --git a/lib/view/layouts/layout.dart b/lib/view/layouts/layout.dart index d266ef3..2d20bfa 100644 --- a/lib/view/layouts/layout.dart +++ b/lib/view/layouts/layout.dart @@ -64,15 +64,16 @@ class _LayoutState extends State { body: SafeArea( child: GestureDetector( behavior: HitTestBehavior.translucent, - onTap: () {}, // project selection removed → nothing to close + onTap: () {}, child: Column( children: [ _buildHeader(context, isMobile), Expanded( child: SingleChildScrollView( key: controller.scrollKey, - padding: EdgeInsets.symmetric( - horizontal: 0, vertical: isMobile ? 16 : 32), + // Removed redundant vertical padding here. DashboardScreen's + // SingleChildScrollView now handles all internal padding. + padding: EdgeInsets.symmetric(horizontal: 0, vertical: 0), child: widget.child, ), ), diff --git a/lib/view/service_project/service_project_details_screen.dart b/lib/view/service_project/service_project_details_screen.dart index 9a3d6d5..b98347e 100644 --- a/lib/view/service_project/service_project_details_screen.dart +++ b/lib/view/service_project/service_project_details_screen.dart @@ -429,6 +429,8 @@ class _ServiceProjectDetailsScreenState @override Widget build(BuildContext context) { + final Color appBarColor = contentTheme.primary; + return Scaffold( backgroundColor: const Color(0xFFF5F5F5), appBar: CustomAppBar( @@ -436,55 +438,82 @@ class _ServiceProjectDetailsScreenState projectName: widget.projectName, onBackPressed: () => Get.toNamed('/dashboard/service-projects'), ), - body: SafeArea( - child: Column( - children: [ - // TabBar - Container( - color: Colors.white, - child: TabBar( - controller: _tabController, - labelColor: Colors.black, - unselectedLabelColor: Colors.grey, - indicatorColor: Colors.red, - indicatorWeight: 3, - isScrollable: false, - tabs: [ - Tab(child: MyText.bodyMedium("Profile")), - Tab(child: MyText.bodyMedium("Jobs")), - Tab(child: MyText.bodyMedium("Teams")), + body: Stack( + children: [ + // === TOP FADE BELOW APPBAR === + Container( + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), ], ), ), + ), - // TabBarView - Expanded( - child: Obx(() { - if (controller.isLoading.value && - controller.projectDetail.value == null) { - return const Center(child: CircularProgressIndicator()); - } - if (controller.errorMessage.value.isNotEmpty && - controller.projectDetail.value == null) { - return Center( - child: MyText.bodyMedium(controller.errorMessage.value)); - } + SafeArea( + top: false, + bottom: true, + child: Column( + children: [ + // === TAB BAR WITH TRANSPARENT BACKGROUND === + Container( + decoration: const BoxDecoration(color: Colors.transparent), + child: TabBar( + controller: _tabController, + labelColor: Colors.white, + unselectedLabelColor: Colors.white70, + indicatorColor: Colors.white, + indicatorWeight: 3, + tabs: [ + Tab( + child: MyText.bodyMedium("Profile", + color: Colors.white)), + Tab( + child: + MyText.bodyMedium("Jobs", color: Colors.white)), + Tab( + child: + MyText.bodyMedium("Teams", color: Colors.white)), + ], + ), + ), - return TabBarView( - controller: _tabController, - children: [ - _buildProfileTab(), - JobsTab( - scrollController: _jobScrollController, - projectName: widget.projectName ?? '', - ), - _buildTeamsTab(), - ], - ); - }), + // === TABBAR VIEW === + Expanded( + child: Obx(() { + if (controller.isLoading.value && + controller.projectDetail.value == null) { + return const Center(child: CircularProgressIndicator()); + } + if (controller.errorMessage.value.isNotEmpty && + controller.projectDetail.value == null) { + return Center( + child: + MyText.bodyMedium(controller.errorMessage.value)); + } + + return TabBarView( + controller: _tabController, + children: [ + _buildProfileTab(), + JobsTab( + scrollController: _jobScrollController, + projectName: widget.projectName ?? '', + ), + _buildTeamsTab(), + ], + ); + }), + ), + ], ), - ], - ), + ), + ], ), floatingActionButton: _tabController.index == 1 ? FloatingActionButton.extended( diff --git a/lib/view/service_project/service_project_job_detail_screen.dart b/lib/view/service_project/service_project_job_detail_screen.dart index ad1c019..814e68d 100644 --- a/lib/view/service_project/service_project_job_detail_screen.dart +++ b/lib/view/service_project/service_project_job_detail_screen.dart @@ -760,17 +760,20 @@ class _JobDetailsScreenState extends State with UIMixin { @override Widget build(BuildContext context) { final projectName = widget.projectName; + final Color appBarColor = contentTheme.primary; + return Scaffold( backgroundColor: const Color(0xFFF5F5F5), appBar: CustomAppBar( title: "Job Details Screen", onBackPressed: () => Get.back(), projectName: projectName, + backgroundColor: appBarColor, ), floatingActionButton: Obx(() => FloatingActionButton.extended( onPressed: isEditing.value ? _editJob : () => isEditing.value = true, - backgroundColor: contentTheme.primary, + backgroundColor: appBarColor, label: MyText.bodyMedium( isEditing.value ? "Save" : "Edit", color: Colors.white, @@ -778,63 +781,101 @@ class _JobDetailsScreenState extends State with UIMixin { ), icon: Icon(isEditing.value ? Icons.save : Icons.edit), )), - body: Obx(() { - if (controller.isJobDetailLoading.value) { - return const Center(child: CircularProgressIndicator()); - } - - if (controller.jobDetailErrorMessage.value.isNotEmpty) { - return Center( - child: MyText.bodyMedium(controller.jobDetailErrorMessage.value)); - } - - final job = controller.jobDetail.value?.data; - if (job == null) { - return Center(child: MyText.bodyMedium("No details available")); - } - - return SingleChildScrollView( - padding: MySpacing.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildAttendanceCard(), - _buildSectionCard( - title: "Job Info", - titleIcon: Icons.task_outlined, - children: [ - _editableRow("Title", _titleController), - _editableRow("Description", _descriptionController), - _dateRangePicker(), + body: Stack( + children: [ + Container( + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), ], ), - MySpacing.height(12), - _buildSectionCard( - title: "Project Branch", - titleIcon: Icons.account_tree_outlined, - children: [_branchDisplay()], - ), - MySpacing.height(16), - _buildSectionCard( - title: "Assignees", - titleIcon: Icons.person_outline, - children: [_assigneeInputWithChips()]), - MySpacing.height(16), - _buildSectionCard( - title: "Tags", - titleIcon: Icons.label_outline, - children: [_tagEditor()]), - MySpacing.height(16), - if ((job.updateLogs?.isNotEmpty ?? false)) - _buildSectionCard( - title: "Update Logs", - titleIcon: Icons.history, - children: [JobTimeline(logs: job.updateLogs ?? [])]), - MySpacing.height(80), - ], + ), ), - ); - }), + + // Bottom fade (for smooth transition above FAB) + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: 60, // adjust based on FAB height + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + appBarColor.withOpacity(0.05), + Colors.transparent, + ], + ), + ), + ), + ), + + // Main scrollable content + Obx(() { + if (controller.isJobDetailLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (controller.jobDetailErrorMessage.value.isNotEmpty) { + return Center( + child: MyText.bodyMedium( + controller.jobDetailErrorMessage.value)); + } + + final job = controller.jobDetail.value?.data; + if (job == null) { + return Center(child: MyText.bodyMedium("No details available")); + } + + return SingleChildScrollView( + padding: MySpacing.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildAttendanceCard(), + _buildSectionCard( + title: "Job Info", + titleIcon: Icons.task_outlined, + children: [ + _editableRow("Title", _titleController), + _editableRow("Description", _descriptionController), + _dateRangePicker(), + ], + ), + MySpacing.height(12), + _buildSectionCard( + title: "Project Branch", + titleIcon: Icons.account_tree_outlined, + children: [_branchDisplay()], + ), + MySpacing.height(16), + _buildSectionCard( + title: "Assignees", + titleIcon: Icons.person_outline, + children: [_assigneeInputWithChips()]), + MySpacing.height(16), + _buildSectionCard( + title: "Tags", + titleIcon: Icons.label_outline, + children: [_tagEditor()]), + MySpacing.height(16), + if ((job.updateLogs?.isNotEmpty ?? false)) + _buildSectionCard( + title: "Update Logs", + titleIcon: Icons.history, + children: [JobTimeline(logs: job.updateLogs ?? [])]), + MySpacing.height(80), + ], + ), + ); + }), + ], + ), ); } } diff --git a/lib/view/service_project/service_project_screen.dart b/lib/view/service_project/service_project_screen.dart index 7b28aff..24f3b69 100644 --- a/lib/view/service_project/service_project_screen.dart +++ b/lib/view/service_project/service_project_screen.dart @@ -181,99 +181,115 @@ class _ServiceProjectScreenState extends State @override Widget build(BuildContext context) { + final Color appBarColor = contentTheme.primary; return Scaffold( backgroundColor: const Color(0xFFF5F5F5), - appBar: CustomAppBar( title: "Service Projects", projectName: 'All Service Projects', + backgroundColor: appBarColor, onBackPressed: () => Get.toNamed('/dashboard'), ), - - // FIX 1: Entire body wrapped in SafeArea - body: SafeArea( - bottom: true, - child: Column( - children: [ - Padding( - padding: MySpacing.xy(8, 8), - child: Row( - children: [ - Expanded( - child: SizedBox( - height: 35, - child: TextField( - controller: searchController, - decoration: InputDecoration( - contentPadding: - const EdgeInsets.symmetric(horizontal: 12), - prefixIcon: const Icon(Icons.search, - size: 20, color: Colors.grey), - suffixIcon: ValueListenableBuilder( - valueListenable: 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: () { - searchController.clear(); - controller.updateSearch(''); - }, - ); - }, - ), - hintText: 'Search projects...', - 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), - ), - ), - ), - ), - ), + body: Stack( + children: [ + Container( + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), ], ), ), - Expanded( - child: Obx(() { - if (controller.isLoading.value) { - return const Center(child: CircularProgressIndicator()); - } + ), - final projects = controller.filteredProjects; - - return MyRefreshIndicator( - onRefresh: _refreshProjects, - backgroundColor: Colors.indigo, - color: Colors.white, - child: projects.isEmpty - ? _buildEmptyState() - : ListView.separated( - physics: const AlwaysScrollableScrollPhysics(), - - // FIX 2: Increased bottom padding for landscape - padding: MySpacing.only( - left: 8, right: 8, top: 4, bottom: 120), - - itemCount: projects.length, - separatorBuilder: (_, __) => MySpacing.height(12), - itemBuilder: (_, index) => - _buildProjectCard(projects[index]), + // Main content + SafeArea( + bottom: true, + child: Column( + children: [ + Padding( + padding: MySpacing.xy(8, 8), + child: Row( + children: [ + Expanded( + child: SizedBox( + height: 35, + child: TextField( + controller: searchController, + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(horizontal: 12), + prefixIcon: const Icon(Icons.search, + size: 20, color: Colors.grey), + suffixIcon: + ValueListenableBuilder( + valueListenable: 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: () { + searchController.clear(); + controller.updateSearch(''); + }, + ); + }, + ), + hintText: 'Search projects...', + 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), + ), + ), + ), ), - ); - }), + ), + ], + ), + ), + Expanded( + child: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + final projects = controller.filteredProjects; + + return MyRefreshIndicator( + onRefresh: _refreshProjects, + backgroundColor: Colors.indigo, + color: Colors.white, + child: projects.isEmpty + ? _buildEmptyState() + : ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + padding: MySpacing.only( + left: 8, right: 8, top: 4, bottom: 120), + itemCount: projects.length, + separatorBuilder: (_, __) => MySpacing.height(12), + itemBuilder: (_, index) => + _buildProjectCard(projects[index]), + ), + ); + }), + ), + ], ), - ], - ), + ), + ], ), ); } diff --git a/lib/view/taskPlanning/daily_progress_report.dart b/lib/view/taskPlanning/daily_progress_report.dart index 74d6cc0..60dd191 100644 --- a/lib/view/taskPlanning/daily_progress_report.dart +++ b/lib/view/taskPlanning/daily_progress_report.dart @@ -17,6 +17,8 @@ import 'package:on_field_work/model/dailyTaskPlanning/task_action_buttons.dart'; import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart'; import 'package:on_field_work/helpers/utils/permission_constants.dart'; import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart'; +import 'package:on_field_work/helpers/widgets/custom_app_bar.dart'; + class DailyProgressReportScreen extends StatefulWidget { const DailyProgressReportScreen({super.key}); @@ -87,124 +89,94 @@ class _DailyProgressReportScreenState extends State @override Widget build(BuildContext context) { + final Color appBarColor = contentTheme.primary; + return Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(72), - child: AppBar( - backgroundColor: const Color(0xFFF5F5F5), - elevation: 0.5, - automaticallyImplyLeading: false, - titleSpacing: 0, - title: Padding( - padding: MySpacing.xy(16, 0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios_new, - color: Colors.black, size: 20), - onPressed: () => Get.offNamed('/dashboard'), - ), - MySpacing.width(8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - MyText.titleLarge( - 'Daily Progress Report', - fontWeight: 700, - color: Colors.black, - ), - MySpacing.height(2), - GetBuilder( - builder: (projectController) { - final projectName = - projectController.selectedProject?.name ?? - 'Select Project'; - return Row( - children: [ - const Icon(Icons.work_outline, - size: 14, color: Colors.grey), - MySpacing.width(4), - Expanded( - child: MyText.bodySmall( - projectName, - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: Colors.grey[700], - ), - ), - ], - ); - }, - ), - ], - ), - ), - ], + backgroundColor: const Color(0xFFF5F5F5), + appBar: CustomAppBar( + title: 'Daily Progress Report', + backgroundColor: appBarColor, + projectName: + projectController.selectedProject?.name ?? 'Select Project', + onBackPressed: () => Get.offNamed('/dashboard'), + ), + body: Stack( + children: [ + // Gradient behind content (like EmployeesScreen) + Container( + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), + ], + ), ), ), - ), - ), - body: SafeArea( - child: MyRefreshIndicator( - onRefresh: _refreshData, - child: CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverToBoxAdapter( - child: GetBuilder( - init: dailyTaskController, - tag: 'daily_progress_report_controller', - builder: (controller) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MySpacing.height(flexSpacing), - Padding( - padding: MySpacing.x(15), - child: Row( - mainAxisAlignment: - MainAxisAlignment.end, - children: [ - InkWell( - borderRadius: BorderRadius.circular(22), - onTap: _openFilterSheet, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - child: Row( - children: [ - MyText.bodySmall( - "Filter", - fontWeight: 600, - color: Colors.black, + + // Main content + SafeArea( + child: MyRefreshIndicator( + onRefresh: _refreshData, + child: CustomScrollView( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: GetBuilder( + init: dailyTaskController, + tag: 'daily_progress_report_controller', + builder: (controller) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MySpacing.height(flexSpacing), + Padding( + padding: MySpacing.x(15), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + InkWell( + borderRadius: BorderRadius.circular(22), + onTap: _openFilterSheet, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + child: Row( + children: [ + MyText.bodySmall( + "Filter", + fontWeight: 600, + color: Colors.black, + ), + const SizedBox(width: 4), + const Icon(Icons.tune, + size: 20, color: Colors.black), + ], ), - const SizedBox(width: 4), - Icon(Icons.tune, - size: 20, color: Colors.black), - - ], + ), ), - ), + ], ), - ], - ), - ), - MySpacing.height(8), - Padding( - padding: MySpacing.x(8), - child: _buildDailyProgressReportTab(), - ), - ], - ); - }, - ), + ), + MySpacing.height(8), + Padding( + padding: MySpacing.x(8), + child: _buildDailyProgressReportTab(), + ), + ], + ); + }, + ), + ), + ], ), - ], + ), ), - ), + ], ), ); } diff --git a/lib/view/taskPlanning/daily_task_planning.dart b/lib/view/taskPlanning/daily_task_planning.dart index 7e35ef5..cb6091b 100644 --- a/lib/view/taskPlanning/daily_task_planning.dart +++ b/lib/view/taskPlanning/daily_task_planning.dart @@ -15,6 +15,8 @@ import 'package:on_field_work/helpers/utils/permission_constants.dart'; import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart'; import 'package:on_field_work/controller/tenant/service_controller.dart'; import 'package:on_field_work/helpers/widgets/tenant/service_selector.dart'; +import 'package:on_field_work/helpers/widgets/custom_app_bar.dart'; + class DailyTaskPlanningScreen extends StatefulWidget { DailyTaskPlanningScreen({super.key}); @@ -58,128 +60,99 @@ class _DailyTaskPlanningScreenState extends State @override Widget build(BuildContext context) { + final Color appBarColor = contentTheme.primary; + return Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(72), - child: AppBar( - backgroundColor: const Color(0xFFF5F5F5), - elevation: 0.5, - automaticallyImplyLeading: false, - titleSpacing: 0, - title: Padding( - padding: MySpacing.xy(16, 0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios_new, - color: Colors.black, size: 20), - onPressed: () => Get.offNamed('/dashboard'), - ), - MySpacing.width(8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - MyText.titleLarge( - 'Daily Task Planning', - fontWeight: 700, - color: Colors.black, - ), - MySpacing.height(2), - GetBuilder( - builder: (projectController) { - final projectName = - projectController.selectedProject?.name ?? - 'Select Project'; - return Row( - children: [ - const Icon(Icons.work_outline, - size: 14, color: Colors.grey), - MySpacing.width(4), - Expanded( - child: MyText.bodySmall( - projectName, - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: Colors.grey[700], - ), + backgroundColor: const Color(0xFFF5F5F5), + appBar: CustomAppBar( + title: 'Daily Task Planning', + backgroundColor: appBarColor, + projectName: + projectController.selectedProject?.name ?? 'Select Project', + onBackPressed: () => Get.offNamed('/dashboard'), + ), + body: Stack( + children: [ + // Gradient behind content + Container( + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), + ], + ), + ), + ), + + // Main content + SafeArea( + child: MyRefreshIndicator( + onRefresh: () async { + final projectId = projectController.selectedProjectId.value; + if (projectId.isNotEmpty) { + try { + await dailyTaskPlanningController.fetchTaskData( + projectId, + serviceId: serviceController.selectedService?.id, + ); + } catch (e) { + debugPrint('Error refreshing task data: ${e.toString()}'); + } + } + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: MySpacing.x(0), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height - + kToolbarHeight - + MediaQuery.of(context).padding.top, + ), + child: GetBuilder( + init: dailyTaskPlanningController, + tag: 'daily_task_Planning_controller', + builder: (controller) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MySpacing.height(flexSpacing), + Padding( + padding: MySpacing.x(10), + child: ServiceSelector( + controller: serviceController, + height: 40, + onSelectionChanged: (service) async { + final projectId = + projectController.selectedProjectId.value; + if (projectId.isNotEmpty) { + await dailyTaskPlanningController + .fetchTaskData( + projectId, + serviceId: service?.id, + ); + } + }, ), - ], - ); - }), - ], + ), + MySpacing.height(flexSpacing), + Padding( + padding: MySpacing.x(8), + child: dailyProgressReportTab(), + ), + ], + ); + }, ), ), - ], - ), - ), - ), - ), - body: SafeArea( - child: MyRefreshIndicator( - onRefresh: () async { - final projectId = projectController.selectedProjectId.value; - if (projectId.isNotEmpty) { - try { - // keep previous behavior but now fetchTaskData is lighter (buildings only) - await dailyTaskPlanningController.fetchTaskData( - projectId, - serviceId: serviceController.selectedService?.id, - ); - } catch (e) { - debugPrint('Error refreshing task data: ${e.toString()}'); - } - } - }, - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - padding: MySpacing.x(0), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: MediaQuery.of(context).size.height - - kToolbarHeight - - MediaQuery.of(context).padding.top, - ), - child: GetBuilder( - init: dailyTaskPlanningController, - tag: 'daily_task_Planning_controller', - builder: (controller) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MySpacing.height(flexSpacing), - Padding( - padding: MySpacing.x(10), - child: ServiceSelector( - controller: serviceController, - height: 40, - onSelectionChanged: (service) async { - final projectId = - projectController.selectedProjectId.value; - if (projectId.isNotEmpty) { - await dailyTaskPlanningController.fetchTaskData( - projectId, - serviceId: - service?.id, // <-- pass selected service - ); - } - }, - ), - ), - MySpacing.height(flexSpacing), - Padding( - padding: MySpacing.x(8), - child: dailyProgressReportTab(), - ), - ], - ); - }, ), ), ), - ), + ], ), ); } @@ -227,8 +200,7 @@ class _DailyTaskPlanningScreenState extends State final buildings = dailyTasks .expand((task) => task.buildings) .where((building) => - (building.plannedWork ) > 0 || - (building.completedWork ) > 0) + (building.plannedWork) > 0 || (building.completedWork) > 0) .toList(); if (buildings.isEmpty) {