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));
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() {
// 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<DashboardScreen> 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<DashboardScreen> 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<DashboardScreen> 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<DashboardScreen> 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<DashboardScreen> 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: [

View File

@ -65,61 +65,47 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
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<ContactDetailScreen>
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,
),
],
),
),
],
),
);
}

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/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<DirectoryMainScreen>
with SingleTickerProviderStateMixin {
with SingleTickerProviderStateMixin, UIMixin {
late TabController _tabController;
final DirectoryController controller = Get.put(DirectoryController());
@ -38,97 +39,46 @@ class _DirectoryMainScreenState extends State<DirectoryMainScreen>
@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<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],
),
),
],
);
},
),
],
),
),
],
),
),
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<DirectoryMainScreen>
],
),
),
);
},
],
),
);
}
}

View File

@ -115,7 +115,6 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
void dispose() {
_scrollController.dispose();
_fabAnimationController.dispose();
docController.searchController.dispose();
docController.documents.clear();
super.dispose();
}
@ -137,7 +136,7 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
],
),
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<UserDocumentsPage>
);
}
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(() {

View File

@ -16,11 +16,14 @@ class EmployeeProfilePage extends StatefulWidget {
class _EmployeeProfilePageState extends State<EmployeeProfilePage>
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<EmployeeProfilePage>
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<EmployeeProfilePage>
),
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<EmployeeProfilePage>
),
),
),
// === 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<EmployeeProfilePage>
),
);
}
}
}

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/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<ExpenseMainScreen>
),
),
Expanded(
child:
Container(color: Colors.grey[100]),
child: Container(color: Colors.grey[100]),
),
],
),
@ -126,30 +126,22 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen>
// === 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

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/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<PaymentRequestMainScreen>
child: Column(
children: [
Container(
height: 80,
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
@ -126,8 +127,7 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
),
),
Expanded(
child:
Container(color: Colors.grey[100]),
child: Container(color: Colors.grey[100]),
),
],
),
@ -135,29 +135,22 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
// === 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(),

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/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<Layout> createState() => _LayoutState();
}
class _LayoutState extends State<Layout> {
class _LayoutState extends State<Layout> 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<Layout> {
}
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),
),
),
),
],
),
],
),
),
);
}
}

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/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 ===