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.
This commit is contained in:
Vaibhav Surve 2025-11-28 14:48:39 +05:30
parent 259f2aa928
commit 65fbef3441
11 changed files with 536 additions and 491 deletions

View File

@ -63,6 +63,9 @@ class ThemeController extends GetxController {
await Future.delayed(const Duration(milliseconds: 600)); await Future.delayed(const Duration(milliseconds: 600));
showApplied.value = false; showApplied.value = false;
// Navigate to dashboard after applying theme
Get.offAllNamed('/dashboard');
} }
} }

View File

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
class PillTabBar extends StatelessWidget {
final TabController controller;
final List<String> 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(),
),
),
);
}
}

View File

@ -81,30 +81,31 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
); );
} }
//---------------------------------------------------------------------------
// CONDITIONAL QUICK ACTION CARD
//---------------------------------------------------------------------------
Widget _conditionalQuickActionCard() { Widget _conditionalQuickActionCard() {
// STATIC CONDITION String status = "1"; // <-- change as needed
String status = "O"; // <-- change if needed
bool isCheckedIn = status == "O"; bool isCheckedIn = status == "O";
// Button color remains the same
Color buttonColor =
isCheckedIn ? Colors.red.shade700 : Colors.green.shade700;
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
gradient: LinearGradient( gradient: LinearGradient(
colors: isCheckedIn colors: [
? [Colors.red.shade200, Colors.red.shade400] contentTheme.primary.withOpacity(0.3), // lighter/faded
: [Colors.green.shade200, Colors.green.shade400], contentTheme.primary.withOpacity(0.6), // slightly stronger
],
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black12.withOpacity(0.1), color: Colors.black12.withOpacity(0.05),
blurRadius: 8, blurRadius: 6,
offset: const Offset(0, 4), offset: const Offset(0, 3),
), ),
], ],
), ),
@ -139,29 +140,21 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
style: const TextStyle(color: Colors.white70, fontSize: 13), style: const TextStyle(color: Colors.white70, fontSize: 13),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Action Buttons // Action Button (solid color)
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
if (!isCheckedIn)
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () { onPressed: () {
// Check-In action // Check-In / Check-Out action
}, },
label: const Text("Check-In"), icon: Icon(
style: ElevatedButton.styleFrom( isCheckedIn ? LucideIcons.log_out : LucideIcons.log_in,
backgroundColor: Colors.green.shade700, size: 16,
foregroundColor: Colors.white,
), ),
), label: Text(isCheckedIn ? "Check-Out" : "Check-In"),
if (isCheckedIn)
ElevatedButton.icon(
onPressed: () {
// Check-Out action
},
label: const Text("Check-Out"),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade700, backgroundColor: buttonColor,
foregroundColor: Colors.white, foregroundColor: Colors.white,
), ),
), ),
@ -180,8 +173,8 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_sectionTitle("Quick Action"), // Change title to singular _sectionTitle("Quick Action"),
_conditionalQuickActionCard(), // Use the new conditional card _conditionalQuickActionCard(),
], ],
); );
} }
@ -419,7 +412,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
children: [ children: [
Icon( Icon(
cardMeta.icon, cardMeta.icon,
size: 20, // **smaller icon** size: 20,
color: color:
isEnabled ? cardMeta.color : Colors.grey.shade400, isEnabled ? cardMeta.color : Colors.grey.shade400,
), ),
@ -428,7 +421,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
item.name, item.name,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 9.5, // **reduced text size** fontSize: 9.5,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: color:
isEnabled ? Colors.black87 : Colors.grey.shade600, isEnabled ? Colors.black87 : Colors.grey.shade600,
@ -457,7 +450,7 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
backgroundColor: const Color(0xfff5f6fa), backgroundColor: const Color(0xfff5f6fa),
body: Layout( body: Layout(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 14), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View File

@ -65,48 +65,36 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: const Color(0xFFF5F5F5),
body: Stack(
children: [ // AppBar is outside SafeArea (correct)
// GRADIENT BEHIND APPBAR & TABBAR appBar: CustomAppBar(
Positioned.fill( title: 'Contact Profile',
backgroundColor: appBarColor,
onBackPressed: () => Get.offAllNamed('/dashboard/directory-main-page'),
),
// Only the content is wrapped inside SafeArea
body: SafeArea(
child: Column( child: Column(
children: [ children: [
// ************ GRADIENT + SUBHEADER + TABBAR ************
Container( Container(
height: 120, width: double.infinity,
padding: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [ colors: [
appBarColor, contentTheme.primary,
appBarColor.withOpacity(0.0), contentTheme.primary.withOpacity(0),
], ],
), ),
), ),
), child: Obx(() => _buildSubHeader(contactRx.value)),
Expanded(child: Container(color: Colors.grey[100])),
],
),
), ),
// MAIN CONTENT // ************ TAB 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( Expanded(
child: TabBarView( child: TabBarView(
controller: _tabController, controller: _tabController,
@ -119,8 +107,6 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
], ],
), ),
), ),
],
),
); );
} }
@ -129,9 +115,10 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
final lastName = final lastName =
contact.name.split(" ").length > 1 ? contact.name.split(" ").last : ""; contact.name.split(" ").length > 1 ? contact.name.split(" ").last : "";
final Color primaryColor = contentTheme.primary;
return Container( return Container(
color: Colors.transparent, color: Colors.transparent,
child: Padding(
padding: MySpacing.xy(16, 12), padding: MySpacing.xy(16, 12),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -150,19 +137,49 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
], ],
), ),
]), ]),
TabBar( 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, controller: _tabController,
labelColor: Colors.black, indicator: BoxDecoration(
unselectedLabelColor: Colors.grey, color: primaryColor.withOpacity(0.1),
indicatorColor: contentTheme.primary, 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 [ tabs: const [
Tab(text: "Details"), Tab(text: "Details"),
Tab(text: "Notes"), Tab(text: "Notes"),
], ],
dividerColor: Colors.transparent,
),
), ),
], ],
), ),
),
); );
} }

View File

@ -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/directory_controller.dart';
import 'package:on_field_work/controller/directory/notes_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/custom_app_bar.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:on_field_work/view/directory/directory_view.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/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 { class DirectoryMainScreen extends StatefulWidget {
const DirectoryMainScreen({super.key}); const DirectoryMainScreen({super.key});
@ -18,7 +19,7 @@ class DirectoryMainScreen extends StatefulWidget {
} }
class _DirectoryMainScreenState extends State<DirectoryMainScreen> class _DirectoryMainScreenState extends State<DirectoryMainScreen>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin, UIMixin {
late TabController _tabController; late TabController _tabController;
final DirectoryController controller = Get.put(DirectoryController()); final DirectoryController controller = Get.put(DirectoryController());
@ -38,97 +39,46 @@ class _DirectoryMainScreenState extends State<DirectoryMainScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return OrientationBuilder( final Color appBarColor = contentTheme.primary;
builder: (context, orientation) {
final bool isLandscape = orientation == Orientation.landscape;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: const Color(0xFFF1F1F1),
appBar: PreferredSize( appBar: CustomAppBar(
preferredSize: Size.fromHeight( title: "Directory",
isLandscape ? 55 : 72, // Responsive height onBackPressed: () => Get.offNamed('/dashboard'),
backgroundColor: appBarColor,
), ),
child: SafeArea( body: Stack(
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: [ children: [
IconButton( // === TOP GRADIENT ===
icon: const Icon(Icons.arrow_back_ios_new, Container(
color: Colors.black, size: 20), height: 50,
onPressed: () => Get.offNamed('/dashboard'), decoration: BoxDecoration(
), gradient: LinearGradient(
MySpacing.width(8), begin: Alignment.topCenter,
end: Alignment.bottomCenter,
/// FIX: Flexible to prevent overflow in landscape colors: [
Flexible( appBarColor,
child: Column( appBarColor.withOpacity(0.0),
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Directory',
fontWeight: 700,
color: Colors.black,
),
MySpacing.height(2),
GetBuilder<ProjectController>(
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],
),
),
],
);
},
),
],
),
),
], ],
), ),
), ),
), ),
),
),
/// MAIN CONTENT SafeArea(
body: SafeArea( top: false,
bottom: true, bottom: true,
child: Column( child: Column(
children: [ children: [
Container( PillTabBar(
color: Colors.white,
child: TabBar(
controller: _tabController, controller: _tabController,
labelColor: Colors.black, tabs: const ["Directory", "Notes"],
unselectedLabelColor: Colors.grey, selectedColor: contentTheme.primary,
indicatorColor: Colors.red, unselectedColor: Colors.grey.shade600,
tabs: const [ indicatorColor: contentTheme.primary,
Tab(text: "Directory"),
Tab(text: "Notes"),
],
),
), ),
// === TABBAR VIEW ===
Expanded( Expanded(
child: TabBarView( child: TabBarView(
controller: _tabController, controller: _tabController,
@ -141,8 +91,8 @@ class _DirectoryMainScreenState extends State<DirectoryMainScreen>
], ],
), ),
), ),
); ],
}, ),
); );
} }
} }

View File

@ -115,7 +115,6 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
void dispose() { void dispose() {
_scrollController.dispose(); _scrollController.dispose();
_fabAnimationController.dispose(); _fabAnimationController.dispose();
docController.searchController.dispose();
docController.documents.clear(); docController.documents.clear();
super.dispose(); super.dispose();
} }
@ -137,7 +136,7 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
], ],
), ),
child: TextField( child: TextField(
controller: docController.searchController, controller: docController.searchController, // keep GetX controller
onChanged: (value) { onChanged: (value) {
docController.searchQuery.value = value; docController.searchQuery.value = value;
docController.fetchDocuments( docController.fetchDocuments(
@ -805,38 +804,42 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
} }
Widget _buildBody() { Widget _buildBody() {
return Obx(() { // Non-reactive widgets
// Check permissions final searchBar = _buildSearchBar();
if (permissionController.permissions.isEmpty) { final filterChips = _buildFilterChips();
return _buildLoadingIndicator(); final statusBanner = _buildStatusBanner();
}
return Column(
children: [
searchBar,
filterChips,
statusBanner,
// Only the list is reactive
Expanded(
child: Obx(() {
if (!permissionController.hasPermission(Permissions.viewDocument)) { if (!permissionController.hasPermission(Permissions.viewDocument)) {
return _buildPermissionDenied(); return _buildPermissionDenied();
} }
// Show skeleton loader
if (docController.isLoading.value && docController.documents.isEmpty) {
return SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: SkeletonLoaders.documentSkeletonLoader(),
);
}
final docs = docController.documents; final docs = docController.documents;
return Column( // Skeleton loader
children: [ if (docController.isLoading.value && docs.isEmpty) {
_buildSearchBar(), return SkeletonLoaders.documentSkeletonLoader();
_buildFilterChips(), }
_buildStatusBanner(),
Expanded( // Empty state
child: MyRefreshIndicator( if (!docController.isLoading.value && docs.isEmpty) {
return _buildEmptyState();
}
// List of documents
return MyRefreshIndicator(
onRefresh: () async { onRefresh: () async {
final combinedFilter = { final combinedFilter = {
'uploadedByIds': docController.selectedUploadedBy.toList(), 'uploadedByIds': docController.selectedUploadedBy.toList(),
'documentCategoryIds': 'documentCategoryIds': docController.selectedCategory.toList(),
docController.selectedCategory.toList(),
'documentTypeIds': docController.selectedType.toList(), 'documentTypeIds': docController.selectedType.toList(),
'documentTagIds': docController.selectedTag.toList(), 'documentTagIds': docController.selectedTag.toList(),
}; };
@ -848,17 +851,7 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
reset: true, reset: true,
); );
}, },
child: docs.isEmpty child: ListView.builder(
? ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.5,
child: _buildEmptyState(),
),
],
)
: ListView.builder(
controller: _scrollController, controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.only(bottom: 100, top: 8), padding: const EdgeInsets.only(bottom: 100, top: 8),
@ -869,8 +862,7 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
if (docController.isLoading.value) { if (docController.isLoading.value) {
return _buildLoadingIndicator(); return _buildLoadingIndicator();
} }
if (!docController.hasMore.value && if (!docController.hasMore.value && docs.isNotEmpty) {
docs.isNotEmpty) {
return _buildNoMoreIndicator(); return _buildNoMoreIndicator();
} }
return const SizedBox.shrink(); return const SizedBox.shrink();
@ -879,27 +871,24 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
final doc = docs[index]; final doc = docs[index];
final currentDate = doc.uploadedAt != null final currentDate = doc.uploadedAt != null
? DateFormat("dd MMM yyyy") ? DateFormat("dd MMM yyyy").format(doc.uploadedAt!.toLocal())
.format(doc.uploadedAt!.toLocal())
: ''; : '';
final prevDate = index > 0 final prevDate = index > 0
? (docs[index - 1].uploadedAt != null ? (docs[index - 1].uploadedAt != null
? DateFormat("dd MMM yyyy").format( ? DateFormat("dd MMM yyyy")
docs[index - 1].uploadedAt!.toLocal()) .format(docs[index - 1].uploadedAt!.toLocal())
: '') : '')
: null; : null;
final showDateHeader = currentDate != prevDate; final showDateHeader = currentDate != prevDate;
return _buildDocumentCard(doc, showDateHeader); return _buildDocumentCard(doc, showDateHeader);
}, },
), ),
), );
}),
), ),
], ],
); );
});
} }
Widget _buildFAB() { Widget _buildFAB() {

View File

@ -16,11 +16,14 @@ class EmployeeProfilePage extends StatefulWidget {
class _EmployeeProfilePageState extends State<EmployeeProfilePage> class _EmployeeProfilePageState extends State<EmployeeProfilePage>
with SingleTickerProviderStateMixin, UIMixin { 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; late TabController _tabController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Initialize TabController with 2 tabs
_tabController = TabController(length: 2, vsync: this); _tabController = TabController(length: 2, vsync: this);
} }
@ -30,9 +33,13 @@ class _EmployeeProfilePageState extends State<EmployeeProfilePage>
super.dispose(); super.dispose();
} }
// --- No need for _buildSegmentedButton function anymore ---
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Accessing theme colors for consistency
final Color appBarColor = contentTheme.primary; final Color appBarColor = contentTheme.primary;
final Color primaryColor = contentTheme.primary;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF1F1F1), backgroundColor: const Color(0xFFF1F1F1),
@ -43,7 +50,8 @@ class _EmployeeProfilePageState extends State<EmployeeProfilePage>
), ),
body: Stack( body: Stack(
children: [ children: [
// === Gradient at the top behind AppBar + TabBar === // === Gradient at the top behind AppBar + Toggle ===
// This container ensures the background color transitions nicely
Container( Container(
height: 50, height: 50,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -57,25 +65,63 @@ class _EmployeeProfilePageState extends State<EmployeeProfilePage>
), ),
), ),
), ),
// === Main Content Area ===
SafeArea( SafeArea(
top: false, top: false,
bottom: true, bottom: true,
child: Column( child: Column(
children: [ children: [
Container( // 🛑 NEW: The Modern TabBar Implementation 🛑
decoration: const BoxDecoration(color: Colors.transparent), 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( child: TabBar(
controller: _tabController, controller: _tabController,
labelColor: Colors.white, // Style the indicator as a subtle pill/chip
unselectedLabelColor: Colors.white70, indicator: BoxDecoration(
indicatorColor: Colors.white, color: primaryColor.withOpacity(0.1), // Light background color for the selection
indicatorWeight: 3, 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 [ tabs: const [
Tab(text: "Details"), Tab(text: "Details"),
Tab(text: "Documents"), Tab(text: "Documents"),
], ],
// Setting this to zero removes the default underline
dividerColor: Colors.transparent,
), ),
), ),
),
// 🛑 TabBarView (The Content) 🛑
Expanded( Expanded(
child: TabBarView( child: TabBarView(
controller: _tabController, controller: _tabController,

View File

@ -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/widgets/my_refresh_indicator.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.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/custom_app_bar.dart';
import 'package:on_field_work/helpers/widgets/pill_tab_bar.dart';
class ExpenseMainScreen extends StatefulWidget { class ExpenseMainScreen extends StatefulWidget {
const ExpenseMainScreen({super.key}); const ExpenseMainScreen({super.key});
@ -117,8 +118,7 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen>
), ),
), ),
Expanded( Expanded(
child: child: Container(color: Colors.grey[100]),
Container(color: Colors.grey[100]),
), ),
], ],
), ),
@ -130,20 +130,12 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen>
bottom: true, bottom: true,
child: Column( child: Column(
children: [ children: [
// TAB BAR WITH TRANSPARENT BACKGROUND PillTabBar(
Container(
decoration: const BoxDecoration(color: Colors.transparent),
child: TabBar(
controller: _tabController, controller: _tabController,
labelColor: Colors.white, tabs: const ["Current Month", "History"],
unselectedLabelColor: Colors.white70, selectedColor: contentTheme.primary,
indicatorColor: Colors.white, unselectedColor: Colors.grey.shade600,
indicatorWeight: 3, indicatorColor: contentTheme.primary,
tabs: const [
Tab(text: "Current Month"),
Tab(text: "History"),
],
),
), ),
// CONTENT AREA // CONTENT AREA

View File

@ -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/utils/permission_constants.dart';
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.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/custom_app_bar.dart';
import 'package:on_field_work/helpers/widgets/pill_tab_bar.dart';
class PaymentRequestMainScreen extends StatefulWidget { class PaymentRequestMainScreen extends StatefulWidget {
const PaymentRequestMainScreen({super.key}); const PaymentRequestMainScreen({super.key});
@ -126,8 +127,7 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
), ),
), ),
Expanded( Expanded(
child: child: Container(color: Colors.grey[100]),
Container(color: Colors.grey[100]),
), ),
], ],
), ),
@ -139,19 +139,12 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
bottom: true, bottom: true,
child: Column( child: Column(
children: [ children: [
// TAB BAR WITH TRANSPARENT BACKGROUND PillTabBar(
Container(
decoration: const BoxDecoration(color: Colors.transparent),
child: TabBar(
controller: _tabController, controller: _tabController,
labelColor: Colors.white, tabs: const ["Current Month", "History"],
unselectedLabelColor: Colors.white70, selectedColor: contentTheme.primary,
indicatorColor: Colors.white, unselectedColor: Colors.grey.shade600,
tabs: const [ indicatorColor: contentTheme.primary,
Tab(text: "Current Month"),
Tab(text: "History"),
],
),
), ),
// CONTENT AREA // CONTENT AREA

View File

@ -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/images.dart';
import 'package:on_field_work/view/layouts/user_profile_right_bar.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/services/tenant_service.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
class Layout extends StatefulWidget { class Layout extends StatefulWidget {
final Widget? child; final Widget? child;
@ -20,7 +21,7 @@ class Layout extends StatefulWidget {
State<Layout> createState() => _LayoutState(); State<Layout> createState() => _LayoutState();
} }
class _LayoutState extends State<Layout> { class _LayoutState extends State<Layout> with UIMixin {
final LayoutController controller = LayoutController(); final LayoutController controller = LayoutController();
final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo(); final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo();
final bool isBetaEnvironment = ApiEndpoints.baseUrl.contains("stage"); final bool isBetaEnvironment = ApiEndpoints.baseUrl.contains("stage");
@ -57,51 +58,71 @@ class _LayoutState extends State<Layout> {
} }
Widget _buildScaffold(BuildContext context, {bool isMobile = false}) { Widget _buildScaffold(BuildContext context, {bool isMobile = false}) {
final primaryColor = contentTheme.primary;
return Scaffold( return Scaffold(
key: controller.scaffoldKey, key: controller.scaffoldKey,
endDrawer: const UserProfileBar(), endDrawer: const UserProfileBar(),
floatingActionButton: widget.floatingActionButton, floatingActionButton: widget.floatingActionButton,
body: SafeArea( 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],
),
),
child: Column(
children: [
_buildHeaderContent(isMobile),
Expanded(
child: SafeArea(
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () {}, onTap: () {},
child: Column(
children: [
_buildHeader(context, isMobile),
Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
key: controller.scrollKey, key: controller.scrollKey,
// Removed redundant vertical padding here. DashboardScreen's padding: EdgeInsets.zero,
// SingleChildScrollView now handles all internal padding.
padding: EdgeInsets.symmetric(horizontal: 0, vertical: 0),
child: widget.child, child: widget.child,
), ),
), ),
),
),
], ],
), ),
), ),
),
); );
} }
/// Header Section (Project selection removed) Widget _buildHeaderContent(bool isMobile) {
Widget _buildHeader(BuildContext context, bool isMobile) {
final selectedTenant = TenantService.currentTenant; final selectedTenant = TenantService.currentTenant;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0), padding: const EdgeInsets.fromLTRB(10, 45, 10, 0),
child: Card( child: Container(
shape: RoundedRectangleBorder( width: double.infinity,
borderRadius: BorderRadius.circular(5), 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),
),
],
), ),
margin: EdgeInsets.zero,
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.all(10),
child: Row( child: Row(
children: [ children: [
ClipRRect( // Logo inside white background card
child: Stack( Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Image.asset( Image.asset(
@ -110,7 +131,7 @@ class _LayoutState extends State<Layout> {
width: 50, width: 50,
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
if (isBetaEnvironment) if (ApiEndpoints.baseUrl.contains("stage"))
Positioned( Positioned(
bottom: 0, bottom: 0,
left: 0, left: 0,
@ -134,10 +155,7 @@ class _LayoutState extends State<Layout> {
), ),
], ],
), ),
),
const SizedBox(width: 12), const SizedBox(width: 12),
/// Dashboard title + current organization
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -147,11 +165,8 @@ class _LayoutState extends State<Layout> {
fontWeight: 700, fontWeight: 700,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: Colors.black87,
), ),
// MyText.bodyMedium(
// "Hi, ${employeeInfo?.firstName ?? ''}",
// color: Colors.black54,
// ),
if (selectedTenant != null) if (selectedTenant != null)
MyText.bodySmall( MyText.bodySmall(
"Organization: ${selectedTenant.name}", "Organization: ${selectedTenant.name}",
@ -162,14 +177,12 @@ class _LayoutState extends State<Layout> {
], ],
), ),
), ),
/// Menu Button
Stack( Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.menu), icon: const Icon(Icons.menu, color: Colors.black87),
onPressed: () => onPressed: () =>
controller.scaffoldKey.currentState?.openEndDrawer(), controller.scaffoldKey.currentState?.openEndDrawer(),
), ),
@ -188,11 +201,12 @@ class _LayoutState extends State<Layout> {
), ),
), ),
], ],
)
],
), ),
],
), ),
), ),
); );
} }
} }

View File

@ -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/model/employees/employee_model.dart';
import 'package:on_field_work/view/service_project/jobs_tab.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/my_refresh_indicator.dart';
import 'package:on_field_work/helpers/widgets/pill_tab_bar.dart';
class ServiceProjectDetailsScreen extends StatefulWidget { class ServiceProjectDetailsScreen extends StatefulWidget {
final String projectId; final String projectId;
@ -460,27 +461,13 @@ class _ServiceProjectDetailsScreenState
bottom: true, bottom: true,
child: Column( child: Column(
children: [ children: [
// === TAB BAR WITH TRANSPARENT BACKGROUND === PillTabBar(
Container(
decoration: const BoxDecoration(color: Colors.transparent),
child: TabBar(
controller: _tabController, controller: _tabController,
labelColor: Colors.white, tabs: const ["Profile", "Jobs", "Teams"],
unselectedLabelColor: Colors.white70, selectedColor: contentTheme.primary,
indicatorColor: Colors.white, unselectedColor: Colors.grey.shade600,
indicatorWeight: 3, indicatorColor: contentTheme.primary.withOpacity(0.1),
tabs: [ height: 48,
Tab(
child: MyText.bodyMedium("Profile",
color: Colors.white)),
Tab(
child:
MyText.bodyMedium("Jobs", color: Colors.white)),
Tab(
child:
MyText.bodyMedium("Teams", color: Colors.white)),
],
),
), ),
// === TABBAR VIEW === // === TABBAR VIEW ===