From 65fbef3441b265907e02380658ef1e25f72126c1 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Fri, 28 Nov 2025 14:48:39 +0530 Subject: [PATCH] Enhance UI and Navigation - Added navigation to the dashboard after applying the theme in ThemeController. - Introduced a new PillTabBar widget for a modern tab design across multiple screens. - Updated dashboard screen to improve button actions and UI consistency. - Refactored contact detail screen to streamline layout and enhance gradient effects. - Implemented PillTabBar in directory main screen, expense screen, and payment request screen for consistent tab navigation. - Improved layout structure in user document screen and employee profile screen for better user experience. - Enhanced service project details screen with a modern tab bar implementation. --- lib/helpers/theme/theme_editor_widget.dart | 3 + lib/helpers/widgets/pill_tab_bar.dart | 61 ++++ lib/view/dashboard/dashboard_screen.dart | 67 ++--- lib/view/directory/contact_detail_screen.dart | 169 ++++++----- lib/view/directory/directory_main_screen.dart | 126 +++----- lib/view/document/user_document_screen.dart | 173 ++++++----- .../employees/employee_profile_screen.dart | 74 ++++- lib/view/expense/expense_screen.dart | 28 +- lib/view/finance/payment_request_screen.dart | 29 +- lib/view/layouts/layout.dart | 268 +++++++++--------- .../service_project_details_screen.dart | 29 +- 11 files changed, 536 insertions(+), 491 deletions(-) create mode 100644 lib/helpers/widgets/pill_tab_bar.dart diff --git a/lib/helpers/theme/theme_editor_widget.dart b/lib/helpers/theme/theme_editor_widget.dart index 0a49c45..2e12a95 100644 --- a/lib/helpers/theme/theme_editor_widget.dart +++ b/lib/helpers/theme/theme_editor_widget.dart @@ -63,6 +63,9 @@ class ThemeController extends GetxController { await Future.delayed(const Duration(milliseconds: 600)); showApplied.value = false; + + // Navigate to dashboard after applying theme + Get.offAllNamed('/dashboard'); } } diff --git a/lib/helpers/widgets/pill_tab_bar.dart b/lib/helpers/widgets/pill_tab_bar.dart new file mode 100644 index 0000000..204cab4 --- /dev/null +++ b/lib/helpers/widgets/pill_tab_bar.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +class PillTabBar extends StatelessWidget { + final TabController controller; + final List tabs; + final Color selectedColor; + final Color unselectedColor; + final Color indicatorColor; + final double height; + + const PillTabBar({ + Key? key, + required this.controller, + required this.tabs, + this.selectedColor = Colors.blue, + this.unselectedColor = Colors.grey, + this.indicatorColor = Colors.blueAccent, + this.height = 48, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Container( + height: height, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(height / 2), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.15), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: TabBar( + controller: controller, + indicator: BoxDecoration( + color: indicatorColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(height / 2), + ), + indicatorSize: TabBarIndicatorSize.tab, + indicatorPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), + labelColor: selectedColor, + unselectedLabelColor: unselectedColor, + labelStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + ), + unselectedLabelStyle: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 15, + ), + tabs: tabs.map((text) => Tab(text: text)).toList(), + ), + ), + ); + } +} diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index d25ba2e..25041ae 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -81,30 +81,31 @@ class _DashboardScreenState extends State with UIMixin { ); } -//--------------------------------------------------------------------------- -// CONDITIONAL QUICK ACTION CARD -//--------------------------------------------------------------------------- Widget _conditionalQuickActionCard() { - // STATIC CONDITION - String status = "O"; // <-- change if needed + String status = "1"; // <-- change as needed bool isCheckedIn = status == "O"; + // Button color remains the same + Color buttonColor = + isCheckedIn ? Colors.red.shade700 : Colors.green.shade700; + 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], + colors: [ + contentTheme.primary.withOpacity(0.3), // lighter/faded + contentTheme.primary.withOpacity(0.6), // slightly stronger + ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), boxShadow: [ BoxShadow( - color: Colors.black12.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 4), + color: Colors.black12.withOpacity(0.05), + blurRadius: 6, + offset: const Offset(0, 3), ), ], ), @@ -139,32 +140,24 @@ class _DashboardScreenState extends State with UIMixin { style: const TextStyle(color: Colors.white70, fontSize: 13), ), const SizedBox(height: 12), - // Action Buttons + // Action Button (solid color) 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, - ), + ElevatedButton.icon( + onPressed: () { + // Check-In / Check-Out action + }, + icon: Icon( + isCheckedIn ? LucideIcons.log_out : LucideIcons.log_in, + size: 16, ), - if (isCheckedIn) - ElevatedButton.icon( - onPressed: () { - // Check-Out action - }, - label: const Text("Check-Out"), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red.shade700, - foregroundColor: Colors.white, - ), + label: Text(isCheckedIn ? "Check-Out" : "Check-In"), + style: ElevatedButton.styleFrom( + backgroundColor: buttonColor, + foregroundColor: Colors.white, ), + ), ], ), ], @@ -180,8 +173,8 @@ class _DashboardScreenState extends State with UIMixin { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _sectionTitle("Quick Action"), // Change title to singular - _conditionalQuickActionCard(), // Use the new conditional card + _sectionTitle("Quick Action"), + _conditionalQuickActionCard(), ], ); } @@ -419,7 +412,7 @@ class _DashboardScreenState extends State with UIMixin { children: [ Icon( cardMeta.icon, - size: 20, // **smaller icon** + size: 20, color: isEnabled ? cardMeta.color : Colors.grey.shade400, ), @@ -428,7 +421,7 @@ class _DashboardScreenState extends State with UIMixin { item.name, textAlign: TextAlign.center, style: TextStyle( - fontSize: 9.5, // **reduced text size** + fontSize: 9.5, fontWeight: FontWeight.w600, color: isEnabled ? Colors.black87 : Colors.grey.shade600, @@ -457,7 +450,7 @@ class _DashboardScreenState extends State with UIMixin { backgroundColor: const Color(0xfff5f6fa), body: Layout( child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 14), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/view/directory/contact_detail_screen.dart b/lib/view/directory/contact_detail_screen.dart index 47872e4..b906cdd 100644 --- a/lib/view/directory/contact_detail_screen.dart +++ b/lib/view/directory/contact_detail_screen.dart @@ -65,61 +65,47 @@ class _ContactDetailScreenState extends State return Scaffold( backgroundColor: const Color(0xFFF5F5F5), - 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), - ], - ), - ), + + // ✔ AppBar is outside SafeArea (correct) + appBar: CustomAppBar( + title: 'Contact Profile', + backgroundColor: appBarColor, + onBackPressed: () => Get.offAllNamed('/dashboard/directory-main-page'), + ), + + // ✔ Only the content is wrapped inside SafeArea + body: SafeArea( + child: Column( + children: [ + // ************ GRADIENT + SUBHEADER + TABBAR ************ + Container( + width: double.infinity, + padding: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + contentTheme.primary, + contentTheme.primary.withOpacity(0), + ], ), - Expanded(child: Container(color: Colors.grey[100])), - ], + ), + child: Obx(() => _buildSubHeader(contactRx.value)), ), - ), - // 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(), - ], - ), - ), - ], + // ************ TAB CONTENT ************ + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + Obx(() => _buildDetailsTab(contactRx.value)), + _buildCommentsTab(), + ], + ), ), - ), - ], + ], + ), ), ); } @@ -129,39 +115,70 @@ class _ContactDetailScreenState extends State final lastName = contact.name.split(" ").length > 1 ? contact.name.split(" ").last : ""; + final Color primaryColor = contentTheme.primary; + 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( + 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]), + ], + ), + ]), + MySpacing.height(12), + // === MODERN PILL-SHAPED TABBAR === + Container( + height: 48, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.15), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: TabBar( controller: _tabController, - labelColor: Colors.black, - unselectedLabelColor: Colors.grey, - indicatorColor: contentTheme.primary, + indicator: BoxDecoration( + color: primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(24), + ), + indicatorSize: TabBarIndicatorSize.tab, + indicatorPadding: + const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0), + labelColor: primaryColor, + unselectedLabelColor: Colors.grey.shade600, + labelStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + ), + unselectedLabelStyle: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 15, + ), tabs: const [ Tab(text: "Details"), Tab(text: "Notes"), ], + dividerColor: Colors.transparent, ), - ], - ), + ), + ], ), ); } diff --git a/lib/view/directory/directory_main_screen.dart b/lib/view/directory/directory_main_screen.dart index 0295dcd..55be534 100644 --- a/lib/view/directory/directory_main_screen.dart +++ b/lib/view/directory/directory_main_screen.dart @@ -3,12 +3,13 @@ import 'package:get/get.dart'; import 'package:on_field_work/controller/directory/directory_controller.dart'; import 'package:on_field_work/controller/directory/notes_controller.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/widgets/custom_app_bar.dart'; import 'package:on_field_work/view/directory/directory_view.dart'; import 'package:on_field_work/view/directory/notes_view.dart'; +import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; +import 'package:on_field_work/helpers/widgets/pill_tab_bar.dart'; class DirectoryMainScreen extends StatefulWidget { const DirectoryMainScreen({super.key}); @@ -18,7 +19,7 @@ class DirectoryMainScreen extends StatefulWidget { } class _DirectoryMainScreenState extends State - with SingleTickerProviderStateMixin { + with SingleTickerProviderStateMixin, UIMixin { late TabController _tabController; final DirectoryController controller = Get.put(DirectoryController()); @@ -38,97 +39,46 @@ class _DirectoryMainScreenState extends State @override Widget build(BuildContext context) { - return OrientationBuilder( - builder: (context, orientation) { - final bool isLandscape = orientation == Orientation.landscape; + final Color appBarColor = contentTheme.primary; - return Scaffold( - backgroundColor: const Color(0xFFF5F5F5), - appBar: PreferredSize( - preferredSize: Size.fromHeight( - isLandscape ? 55 : 72, // Responsive height - ), - child: SafeArea( - bottom: false, - 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), - - /// FIX: Flexible to prevent overflow in landscape - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - MyText.titleLarge( - 'Directory', - 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(0xFFF1F1F1), + appBar: CustomAppBar( + title: "Directory", + onBackPressed: () => Get.offNamed('/dashboard'), + backgroundColor: appBarColor, + ), + body: Stack( + children: [ + // === TOP GRADIENT === + Container( + height: 50, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + appBarColor, + appBarColor.withOpacity(0.0), + ], ), ), ), - /// MAIN CONTENT - body: SafeArea( + SafeArea( + top: false, 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: "Directory"), - Tab(text: "Notes"), - ], - ), + PillTabBar( + controller: _tabController, + tabs: const ["Directory", "Notes"], + selectedColor: contentTheme.primary, + unselectedColor: Colors.grey.shade600, + indicatorColor: contentTheme.primary, ), + + // === TABBAR VIEW === Expanded( child: TabBarView( controller: _tabController, @@ -141,8 +91,8 @@ class _DirectoryMainScreenState extends State ], ), ), - ); - }, + ], + ), ); } } diff --git a/lib/view/document/user_document_screen.dart b/lib/view/document/user_document_screen.dart index 4810d63..84484a2 100644 --- a/lib/view/document/user_document_screen.dart +++ b/lib/view/document/user_document_screen.dart @@ -115,7 +115,6 @@ class _UserDocumentsPageState extends State void dispose() { _scrollController.dispose(); _fabAnimationController.dispose(); - docController.searchController.dispose(); docController.documents.clear(); super.dispose(); } @@ -137,7 +136,7 @@ class _UserDocumentsPageState extends State ], ), child: TextField( - controller: docController.searchController, + controller: docController.searchController, // keep GetX controller onChanged: (value) { docController.searchQuery.value = value; docController.fetchDocuments( @@ -804,103 +803,93 @@ class _UserDocumentsPageState extends State ); } - Widget _buildBody() { - return Obx(() { - // Check permissions - if (permissionController.permissions.isEmpty) { - return _buildLoadingIndicator(); - } + Widget _buildBody() { + // Non-reactive widgets + final searchBar = _buildSearchBar(); + final filterChips = _buildFilterChips(); + final statusBanner = _buildStatusBanner(); - if (!permissionController.hasPermission(Permissions.viewDocument)) { - return _buildPermissionDenied(); - } + return Column( + children: [ + searchBar, + filterChips, + statusBanner, - // Show skeleton loader - if (docController.isLoading.value && docController.documents.isEmpty) { - return SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - child: SkeletonLoaders.documentSkeletonLoader(), - ); - } + // Only the list is reactive + Expanded( + child: Obx(() { + if (!permissionController.hasPermission(Permissions.viewDocument)) { + return _buildPermissionDenied(); + } - final docs = docController.documents; + final docs = docController.documents; - return Column( - children: [ - _buildSearchBar(), - _buildFilterChips(), - _buildStatusBanner(), - Expanded( - child: MyRefreshIndicator( - onRefresh: () async { - final combinedFilter = { - 'uploadedByIds': docController.selectedUploadedBy.toList(), - 'documentCategoryIds': - docController.selectedCategory.toList(), - 'documentTypeIds': docController.selectedType.toList(), - 'documentTagIds': docController.selectedTag.toList(), - }; + // Skeleton loader + if (docController.isLoading.value && docs.isEmpty) { + return SkeletonLoaders.documentSkeletonLoader(); + } - await docController.fetchDocuments( - entityTypeId: entityTypeId, - entityId: resolvedEntityId, - filter: jsonEncode(combinedFilter), - reset: true, - ); + // Empty state + if (!docController.isLoading.value && docs.isEmpty) { + return _buildEmptyState(); + } + + // List of documents + return MyRefreshIndicator( + onRefresh: () async { + final combinedFilter = { + 'uploadedByIds': docController.selectedUploadedBy.toList(), + 'documentCategoryIds': docController.selectedCategory.toList(), + 'documentTypeIds': docController.selectedType.toList(), + 'documentTagIds': docController.selectedTag.toList(), + }; + + await docController.fetchDocuments( + entityTypeId: entityTypeId, + entityId: resolvedEntityId, + filter: jsonEncode(combinedFilter), + reset: true, + ); + }, + child: ListView.builder( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only(bottom: 100, top: 8), + itemCount: docs.length + 1, + itemBuilder: (context, index) { + if (index == docs.length) { + return Obx(() { + if (docController.isLoading.value) { + return _buildLoadingIndicator(); + } + if (!docController.hasMore.value && docs.isNotEmpty) { + return _buildNoMoreIndicator(); + } + return const SizedBox.shrink(); + }); + } + + final doc = docs[index]; + final currentDate = doc.uploadedAt != null + ? DateFormat("dd MMM yyyy").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); }, - child: docs.isEmpty - ? ListView( - physics: const AlwaysScrollableScrollPhysics(), - children: [ - SizedBox( - height: MediaQuery.of(context).size.height * 0.5, - child: _buildEmptyState(), - ), - ], - ) - : ListView.builder( - controller: _scrollController, - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.only(bottom: 100, top: 8), - itemCount: docs.length + 1, - itemBuilder: (context, index) { - if (index == docs.length) { - return Obx(() { - if (docController.isLoading.value) { - return _buildLoadingIndicator(); - } - if (!docController.hasMore.value && - docs.isNotEmpty) { - return _buildNoMoreIndicator(); - } - return const SizedBox.shrink(); - }); - } - - final doc = docs[index]; - final currentDate = doc.uploadedAt != null - ? DateFormat("dd MMM yyyy") - .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); - }, - ), ), - ), - ], - ); - }); - } + ); + }), + ), + ], + ); +} Widget _buildFAB() { return Obx(() { diff --git a/lib/view/employees/employee_profile_screen.dart b/lib/view/employees/employee_profile_screen.dart index e22f57e..93f1c37 100644 --- a/lib/view/employees/employee_profile_screen.dart +++ b/lib/view/employees/employee_profile_screen.dart @@ -16,11 +16,14 @@ class EmployeeProfilePage extends StatefulWidget { class _EmployeeProfilePageState extends State with SingleTickerProviderStateMixin, UIMixin { + // We no longer need to listen to the TabController for setState, + // as the TabBar handles its own state updates via the controller. late TabController _tabController; @override void initState() { super.initState(); + // Initialize TabController with 2 tabs _tabController = TabController(length: 2, vsync: this); } @@ -30,9 +33,13 @@ class _EmployeeProfilePageState extends State super.dispose(); } + // --- No need for _buildSegmentedButton function anymore --- + @override Widget build(BuildContext context) { + // Accessing theme colors for consistency final Color appBarColor = contentTheme.primary; + final Color primaryColor = contentTheme.primary; return Scaffold( backgroundColor: const Color(0xFFF1F1F1), @@ -43,7 +50,8 @@ class _EmployeeProfilePageState extends State ), body: Stack( children: [ - // === Gradient at the top behind AppBar + TabBar === + // === Gradient at the top behind AppBar + Toggle === + // This container ensures the background color transitions nicely Container( height: 50, decoration: BoxDecoration( @@ -57,25 +65,63 @@ class _EmployeeProfilePageState extends State ), ), ), + // === Main Content Area === SafeArea( top: false, bottom: true, child: Column( children: [ - 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"), - ], + // 🛑 NEW: The Modern TabBar Implementation 🛑 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Container( + height: 48, // Define a specific height for the TabBar container + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24.0), // Rounded corners for a chip-like look + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.15), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: TabBar( + controller: _tabController, + // Style the indicator as a subtle pill/chip + indicator: BoxDecoration( + color: primaryColor.withOpacity(0.1), // Light background color for the selection + borderRadius: BorderRadius.circular(24.0), + ), + indicatorSize: TabBarIndicatorSize.tab, + // The padding is used to slightly shrink the indicator area + indicatorPadding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0), + + // Text styling + labelColor: primaryColor, // Selected text color is primary + unselectedLabelColor: Colors.grey.shade600, // Unselected text color is darker grey + labelStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + ), + unselectedLabelStyle: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 15, + ), + + // Tabs (No custom widget needed, just use the built-in Tab) + tabs: const [ + Tab(text: "Details"), + Tab(text: "Documents"), + ], + // Setting this to zero removes the default underline + dividerColor: Colors.transparent, + ), ), ), + + // 🛑 TabBarView (The Content) 🛑 Expanded( child: TabBarView( controller: _tabController, @@ -98,4 +144,4 @@ class _EmployeeProfilePageState extends State ), ); } -} +} \ No newline at end of file diff --git a/lib/view/expense/expense_screen.dart b/lib/view/expense/expense_screen.dart index 821c6a0..d1d4687 100644 --- a/lib/view/expense/expense_screen.dart +++ b/lib/view/expense/expense_screen.dart @@ -13,6 +13,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/helpers/utils/mixins/ui_mixin.dart'; import 'package:on_field_work/helpers/widgets/custom_app_bar.dart'; +import 'package:on_field_work/helpers/widgets/pill_tab_bar.dart'; class ExpenseMainScreen extends StatefulWidget { const ExpenseMainScreen({super.key}); @@ -117,8 +118,7 @@ class _ExpenseMainScreenState extends State ), ), Expanded( - child: - Container(color: Colors.grey[100]), + child: Container(color: Colors.grey[100]), ), ], ), @@ -126,30 +126,22 @@ class _ExpenseMainScreenState extends State // === MAIN CONTENT === SafeArea( - top: false, + 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"), - ], - ), + PillTabBar( + controller: _tabController, + tabs: const ["Current Month", "History"], + selectedColor: contentTheme.primary, + unselectedColor: Colors.grey.shade600, + indicatorColor: contentTheme.primary, ), // CONTENT AREA Expanded( child: Container( - color: Colors.transparent, + color: Colors.transparent, child: Column( children: [ // SEARCH & FILTER diff --git a/lib/view/finance/payment_request_screen.dart b/lib/view/finance/payment_request_screen.dart index 0564d3f..da452a9 100644 --- a/lib/view/finance/payment_request_screen.dart +++ b/lib/view/finance/payment_request_screen.dart @@ -14,6 +14,7 @@ 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'; +import 'package:on_field_work/helpers/widgets/pill_tab_bar.dart'; class PaymentRequestMainScreen extends StatefulWidget { const PaymentRequestMainScreen({super.key}); @@ -113,7 +114,7 @@ class _PaymentRequestMainScreenState extends State child: Column( children: [ Container( - height: 80, + height: 80, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, @@ -126,8 +127,7 @@ class _PaymentRequestMainScreenState extends State ), ), Expanded( - child: - Container(color: Colors.grey[100]), + child: Container(color: Colors.grey[100]), ), ], ), @@ -135,29 +135,22 @@ class _PaymentRequestMainScreenState extends State // === MAIN CONTENT === SafeArea( - top: false, + 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"), - ], - ), + PillTabBar( + controller: _tabController, + tabs: const ["Current Month", "History"], + selectedColor: contentTheme.primary, + unselectedColor: Colors.grey.shade600, + indicatorColor: contentTheme.primary, ), // CONTENT AREA Expanded( child: Container( - color: Colors.transparent, + color: Colors.transparent, child: Column( children: [ _buildSearchBar(), diff --git a/lib/view/layouts/layout.dart b/lib/view/layouts/layout.dart index 2d20bfa..c54d2ad 100644 --- a/lib/view/layouts/layout.dart +++ b/lib/view/layouts/layout.dart @@ -9,6 +9,7 @@ import 'package:on_field_work/helpers/services/api_endpoints.dart'; import 'package:on_field_work/images.dart'; import 'package:on_field_work/view/layouts/user_profile_right_bar.dart'; import 'package:on_field_work/helpers/services/tenant_service.dart'; +import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; class Layout extends StatefulWidget { final Widget? child; @@ -20,7 +21,7 @@ class Layout extends StatefulWidget { State createState() => _LayoutState(); } -class _LayoutState extends State { +class _LayoutState extends State with UIMixin { final LayoutController controller = LayoutController(); final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo(); final bool isBetaEnvironment = ApiEndpoints.baseUrl.contains("stage"); @@ -57,142 +58,155 @@ class _LayoutState extends State { } Widget _buildScaffold(BuildContext context, {bool isMobile = false}) { + final primaryColor = contentTheme.primary; + return Scaffold( key: controller.scaffoldKey, endDrawer: const UserProfileBar(), floatingActionButton: widget.floatingActionButton, - body: SafeArea( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () {}, - child: Column( - children: [ - _buildHeader(context, isMobile), - Expanded( - child: SingleChildScrollView( - key: controller.scrollKey, - // Removed redundant vertical padding here. DashboardScreen's - // SingleChildScrollView now handles all internal padding. - padding: EdgeInsets.symmetric(horizontal: 0, vertical: 0), - child: widget.child, - ), - ), + body: Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + primaryColor, + primaryColor.withOpacity(0.7), + primaryColor.withOpacity(0.0), ], + stops: const [0.0, 0.1, 0.3], ), ), - ), - ); - } - - /// Header Section (Project selection removed) - Widget _buildHeader(BuildContext context, bool isMobile) { - final selectedTenant = TenantService.currentTenant; - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0), - child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - ), - margin: EdgeInsets.zero, - clipBehavior: Clip.antiAlias, - child: Padding( - padding: const EdgeInsets.all(10), - child: Row( - children: [ - ClipRRect( - child: Stack( - clipBehavior: Clip.none, - children: [ - Image.asset( - Images.logoDark, - height: 50, - width: 50, - fit: BoxFit.contain, - ), - if (isBetaEnvironment) - Positioned( - bottom: 0, - left: 0, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 4, vertical: 2), - decoration: BoxDecoration( - color: Colors.deepPurple, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.white, width: 1.2), - ), - child: const Text( - 'B', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ), - ], - ), - ), - const SizedBox(width: 12), - - /// Dashboard title + current organization - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodyLarge( - "Dashboard", - fontWeight: 700, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - // MyText.bodyMedium( - // "Hi, ${employeeInfo?.firstName ?? ''}", - // color: Colors.black54, - // ), - if (selectedTenant != null) - MyText.bodySmall( - "Organization: ${selectedTenant.name}", - color: Colors.black54, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - - /// Menu Button - Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - IconButton( - icon: const Icon(Icons.menu), - onPressed: () => - controller.scaffoldKey.currentState?.openEndDrawer(), + child: Column( + children: [ + _buildHeaderContent(isMobile), + Expanded( + child: SafeArea( + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () {}, + child: SingleChildScrollView( + key: controller.scrollKey, + padding: EdgeInsets.zero, + child: widget.child, ), - if (!hasMpin) - Positioned( - right: 10, - top: 10, - child: Container( - width: 14, - height: 14, - decoration: BoxDecoration( - color: Colors.redAccent, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - ), - ), - ), - ], - ) - ], - ), + ), + ), + ), + ], ), ), ); } + + Widget _buildHeaderContent(bool isMobile) { + final selectedTenant = TenantService.currentTenant; + + return Padding( + padding: const EdgeInsets.fromLTRB(10, 45, 10, 0), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 6, + offset: const Offset(0, 3), + ), + ], + ), + child: Row( + children: [ + // Logo inside white background card + Stack( + clipBehavior: Clip.none, + children: [ + Image.asset( + Images.logoDark, + height: 50, + width: 50, + fit: BoxFit.contain, + ), + if (ApiEndpoints.baseUrl.contains("stage")) + Positioned( + bottom: 0, + left: 0, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 2), + decoration: BoxDecoration( + color: Colors.deepPurple, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.white, width: 1.2), + ), + child: const Text( + 'B', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ], + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyLarge( + "Dashboard", + fontWeight: 700, + maxLines: 1, + overflow: TextOverflow.ellipsis, + color: Colors.black87, + ), + if (selectedTenant != null) + MyText.bodySmall( + "Organization: ${selectedTenant.name}", + color: Colors.black54, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + IconButton( + icon: const Icon(Icons.menu, color: Colors.black87), + onPressed: () => + controller.scaffoldKey.currentState?.openEndDrawer(), + ), + if (!hasMpin) + Positioned( + right: 10, + top: 10, + child: Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: Colors.redAccent, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + ), + ), + ), + ], + ), + ], + ), + ), + ); +} + + } diff --git a/lib/view/service_project/service_project_details_screen.dart b/lib/view/service_project/service_project_details_screen.dart index b98347e..4fe6064 100644 --- a/lib/view/service_project/service_project_details_screen.dart +++ b/lib/view/service_project/service_project_details_screen.dart @@ -13,6 +13,7 @@ import 'package:on_field_work/model/service_project/service_project_allocation_b import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:on_field_work/view/service_project/jobs_tab.dart'; import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart'; +import 'package:on_field_work/helpers/widgets/pill_tab_bar.dart'; class ServiceProjectDetailsScreen extends StatefulWidget { final String projectId; @@ -460,27 +461,13 @@ class _ServiceProjectDetailsScreenState 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)), - ], - ), + PillTabBar( + controller: _tabController, + tabs: const ["Profile", "Jobs", "Teams"], + selectedColor: contentTheme.primary, + unselectedColor: Colors.grey.shade600, + indicatorColor: contentTheme.primary.withOpacity(0.1), + height: 48, ), // === TABBAR VIEW ===