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:
parent
259f2aa928
commit
65fbef3441
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
61
lib/helpers/widgets/pill_tab_bar.dart
Normal file
61
lib/helpers/widgets/pill_tab_bar.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,32 +140,24 @@ 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 / Check-Out action
|
||||||
// Check-In action
|
},
|
||||||
},
|
icon: Icon(
|
||||||
label: const Text("Check-In"),
|
isCheckedIn ? LucideIcons.log_out : LucideIcons.log_in,
|
||||||
style: ElevatedButton.styleFrom(
|
size: 16,
|
||||||
backgroundColor: Colors.green.shade700,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (isCheckedIn)
|
label: Text(isCheckedIn ? "Check-Out" : "Check-In"),
|
||||||
ElevatedButton.icon(
|
style: ElevatedButton.styleFrom(
|
||||||
onPressed: () {
|
backgroundColor: buttonColor,
|
||||||
// Check-Out action
|
foregroundColor: Colors.white,
|
||||||
},
|
|
||||||
label: const Text("Check-Out"),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.red.shade700,
|
|
||||||
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: [
|
||||||
|
|||||||
@ -65,61 +65,47 @@ 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',
|
||||||
child: Column(
|
backgroundColor: appBarColor,
|
||||||
children: [
|
onBackPressed: () => Get.offAllNamed('/dashboard/directory-main-page'),
|
||||||
Container(
|
),
|
||||||
height: 120,
|
|
||||||
decoration: BoxDecoration(
|
// ✔ Only the content is wrapped inside SafeArea
|
||||||
gradient: LinearGradient(
|
body: SafeArea(
|
||||||
begin: Alignment.topCenter,
|
child: Column(
|
||||||
end: Alignment.bottomCenter,
|
children: [
|
||||||
colors: [
|
// ************ GRADIENT + SUBHEADER + TABBAR ************
|
||||||
appBarColor,
|
Container(
|
||||||
appBarColor.withOpacity(0.0),
|
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
|
// ************ TAB CONTENT ************
|
||||||
SafeArea(
|
Expanded(
|
||||||
top: true,
|
child: TabBarView(
|
||||||
bottom: true,
|
controller: _tabController,
|
||||||
child: Column(
|
children: [
|
||||||
children: [
|
Obx(() => _buildDetailsTab(contactRx.value)),
|
||||||
// APPBAR
|
_buildCommentsTab(),
|
||||||
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(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -129,39 +115,70 @@ 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,
|
children: [
|
||||||
children: [
|
Row(children: [
|
||||||
Row(children: [
|
Avatar(firstName: firstName, lastName: lastName, size: 35),
|
||||||
Avatar(firstName: firstName, lastName: lastName, size: 35),
|
MySpacing.width(12),
|
||||||
MySpacing.width(12),
|
Column(
|
||||||
Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
MyText.titleSmall(contact.name,
|
||||||
MyText.titleSmall(contact.name,
|
fontWeight: 600, color: Colors.black),
|
||||||
fontWeight: 600, color: Colors.black),
|
MySpacing.height(2),
|
||||||
MySpacing.height(2),
|
MyText.bodySmall(contact.organization,
|
||||||
MyText.bodySmall(contact.organization,
|
fontWeight: 500, color: Colors.grey[700]),
|
||||||
fontWeight: 500, color: Colors.grey[700]),
|
],
|
||||||
],
|
),
|
||||||
),
|
]),
|
||||||
]),
|
MySpacing.height(12),
|
||||||
TabBar(
|
// === 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,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
),
|
||||||
bottom: false,
|
body: Stack(
|
||||||
child: AppBar(
|
children: [
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
// === TOP GRADIENT ===
|
||||||
elevation: 0.5,
|
Container(
|
||||||
automaticallyImplyLeading: false,
|
height: 50,
|
||||||
titleSpacing: 0,
|
decoration: BoxDecoration(
|
||||||
title: Padding(
|
gradient: LinearGradient(
|
||||||
padding: MySpacing.xy(16, 0),
|
begin: Alignment.topCenter,
|
||||||
child: Row(
|
end: Alignment.bottomCenter,
|
||||||
children: [
|
colors: [
|
||||||
IconButton(
|
appBarColor,
|
||||||
icon: const Icon(Icons.arrow_back_ios_new,
|
appBarColor.withOpacity(0.0),
|
||||||
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],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
/// MAIN CONTENT
|
SafeArea(
|
||||||
body: SafeArea(
|
top: false,
|
||||||
bottom: true,
|
bottom: true,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
PillTabBar(
|
||||||
color: Colors.white,
|
controller: _tabController,
|
||||||
child: TabBar(
|
tabs: const ["Directory", "Notes"],
|
||||||
controller: _tabController,
|
selectedColor: contentTheme.primary,
|
||||||
labelColor: Colors.black,
|
unselectedColor: Colors.grey.shade600,
|
||||||
unselectedLabelColor: Colors.grey,
|
indicatorColor: contentTheme.primary,
|
||||||
indicatorColor: Colors.red,
|
|
||||||
tabs: const [
|
|
||||||
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>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
],
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
@ -804,103 +803,93 @@ 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();
|
||||||
}
|
|
||||||
|
|
||||||
if (!permissionController.hasPermission(Permissions.viewDocument)) {
|
return Column(
|
||||||
return _buildPermissionDenied();
|
children: [
|
||||||
}
|
searchBar,
|
||||||
|
filterChips,
|
||||||
|
statusBanner,
|
||||||
|
|
||||||
// Show skeleton loader
|
// Only the list is reactive
|
||||||
if (docController.isLoading.value && docController.documents.isEmpty) {
|
Expanded(
|
||||||
return SingleChildScrollView(
|
child: Obx(() {
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
if (!permissionController.hasPermission(Permissions.viewDocument)) {
|
||||||
child: SkeletonLoaders.documentSkeletonLoader(),
|
return _buildPermissionDenied();
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
child: MyRefreshIndicator(
|
|
||||||
onRefresh: () async {
|
|
||||||
final combinedFilter = {
|
|
||||||
'uploadedByIds': docController.selectedUploadedBy.toList(),
|
|
||||||
'documentCategoryIds':
|
|
||||||
docController.selectedCategory.toList(),
|
|
||||||
'documentTypeIds': docController.selectedType.toList(),
|
|
||||||
'documentTagIds': docController.selectedTag.toList(),
|
|
||||||
};
|
|
||||||
|
|
||||||
await docController.fetchDocuments(
|
// Empty state
|
||||||
entityTypeId: entityTypeId,
|
if (!docController.isLoading.value && docs.isEmpty) {
|
||||||
entityId: resolvedEntityId,
|
return _buildEmptyState();
|
||||||
filter: jsonEncode(combinedFilter),
|
}
|
||||||
reset: true,
|
|
||||||
);
|
// 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() {
|
Widget _buildFAB() {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
|
|||||||
@ -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(
|
||||||
child: TabBar(
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
controller: _tabController,
|
child: Container(
|
||||||
labelColor: Colors.white,
|
height: 48, // Define a specific height for the TabBar container
|
||||||
unselectedLabelColor: Colors.white70,
|
decoration: BoxDecoration(
|
||||||
indicatorColor: Colors.white,
|
color: Colors.white,
|
||||||
indicatorWeight: 3,
|
borderRadius: BorderRadius.circular(24.0), // Rounded corners for a chip-like look
|
||||||
tabs: const [
|
boxShadow: [
|
||||||
Tab(text: "Details"),
|
BoxShadow(
|
||||||
Tab(text: "Documents"),
|
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(
|
Expanded(
|
||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
@ -98,4 +144,4 @@ class _EmployeeProfilePageState extends State<EmployeeProfilePage>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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]),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -126,30 +126,22 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen>
|
|||||||
|
|
||||||
// === MAIN CONTENT ===
|
// === MAIN CONTENT ===
|
||||||
SafeArea(
|
SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
bottom: true,
|
bottom: true,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// TAB BAR WITH TRANSPARENT BACKGROUND
|
PillTabBar(
|
||||||
Container(
|
controller: _tabController,
|
||||||
decoration: const BoxDecoration(color: Colors.transparent),
|
tabs: const ["Current Month", "History"],
|
||||||
child: TabBar(
|
selectedColor: contentTheme.primary,
|
||||||
controller: _tabController,
|
unselectedColor: Colors.grey.shade600,
|
||||||
labelColor: Colors.white,
|
indicatorColor: contentTheme.primary,
|
||||||
unselectedLabelColor: Colors.white70,
|
|
||||||
indicatorColor: Colors.white,
|
|
||||||
indicatorWeight: 3,
|
|
||||||
tabs: const [
|
|
||||||
Tab(text: "Current Month"),
|
|
||||||
Tab(text: "History"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// CONTENT AREA
|
// CONTENT AREA
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// SEARCH & FILTER
|
// SEARCH & FILTER
|
||||||
|
|||||||
@ -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});
|
||||||
@ -113,7 +114,7 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
height: 80,
|
height: 80,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
@ -126,8 +127,7 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child:
|
child: Container(color: Colors.grey[100]),
|
||||||
Container(color: Colors.grey[100]),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -135,29 +135,22 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
|
|||||||
|
|
||||||
// === MAIN CONTENT ===
|
// === MAIN CONTENT ===
|
||||||
SafeArea(
|
SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
bottom: true,
|
bottom: true,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// TAB BAR WITH TRANSPARENT BACKGROUND
|
PillTabBar(
|
||||||
Container(
|
controller: _tabController,
|
||||||
decoration: const BoxDecoration(color: Colors.transparent),
|
tabs: const ["Current Month", "History"],
|
||||||
child: TabBar(
|
selectedColor: contentTheme.primary,
|
||||||
controller: _tabController,
|
unselectedColor: Colors.grey.shade600,
|
||||||
labelColor: Colors.white,
|
indicatorColor: contentTheme.primary,
|
||||||
unselectedLabelColor: Colors.white70,
|
|
||||||
indicatorColor: Colors.white,
|
|
||||||
tabs: const [
|
|
||||||
Tab(text: "Current Month"),
|
|
||||||
Tab(text: "History"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// CONTENT AREA
|
// CONTENT AREA
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildSearchBar(),
|
_buildSearchBar(),
|
||||||
|
|||||||
@ -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,142 +58,155 @@ 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(
|
||||||
child: GestureDetector(
|
width: double.infinity,
|
||||||
behavior: HitTestBehavior.translucent,
|
decoration: BoxDecoration(
|
||||||
onTap: () {},
|
gradient: LinearGradient(
|
||||||
child: Column(
|
begin: Alignment.topCenter,
|
||||||
children: [
|
end: Alignment.bottomCenter,
|
||||||
_buildHeader(context, isMobile),
|
colors: [
|
||||||
Expanded(
|
primaryColor,
|
||||||
child: SingleChildScrollView(
|
primaryColor.withOpacity(0.7),
|
||||||
key: controller.scrollKey,
|
primaryColor.withOpacity(0.0),
|
||||||
// Removed redundant vertical padding here. DashboardScreen's
|
|
||||||
// SingleChildScrollView now handles all internal padding.
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
|
||||||
child: widget.child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
|
stops: const [0.0, 0.1, 0.3],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
child: Column(
|
||||||
);
|
children: [
|
||||||
}
|
_buildHeaderContent(isMobile),
|
||||||
|
Expanded(
|
||||||
/// Header Section (Project selection removed)
|
child: SafeArea(
|
||||||
Widget _buildHeader(BuildContext context, bool isMobile) {
|
child: GestureDetector(
|
||||||
final selectedTenant = TenantService.currentTenant;
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onTap: () {},
|
||||||
return Padding(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0),
|
key: controller.scrollKey,
|
||||||
child: Card(
|
padding: EdgeInsets.zero,
|
||||||
shape: RoundedRectangleBorder(
|
child: widget.child,
|
||||||
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(),
|
|
||||||
),
|
),
|
||||||
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
controller: _tabController,
|
||||||
decoration: const BoxDecoration(color: Colors.transparent),
|
tabs: const ["Profile", "Jobs", "Teams"],
|
||||||
child: TabBar(
|
selectedColor: contentTheme.primary,
|
||||||
controller: _tabController,
|
unselectedColor: Colors.grey.shade600,
|
||||||
labelColor: Colors.white,
|
indicatorColor: contentTheme.primary.withOpacity(0.1),
|
||||||
unselectedLabelColor: Colors.white70,
|
height: 48,
|
||||||
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)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// === TABBAR VIEW ===
|
// === TABBAR VIEW ===
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user