Refactor UI components to use CustomAppBar and improve layout consistency

- Replaced existing AppBar implementations with CustomAppBar in multiple screens including PaymentRequestDetailScreen, PaymentRequestMainScreen, ServiceProjectDetailsScreen, JobDetailsScreen, DailyProgressReportScreen, DailyTaskPlanningScreen, and ServiceProjectScreen.
- Enhanced visual hierarchy by adding gradient backgrounds behind app bars for better aesthetics.
- Streamlined SafeArea usage to ensure proper content display across different devices.
- Improved code readability and maintainability by removing redundant code and consolidating UI elements.
This commit is contained in:
Vaibhav Surve 2025-11-27 19:07:24 +05:30
parent 84156167ea
commit 259f2aa928
23 changed files with 1908 additions and 1997 deletions

View File

@ -142,8 +142,8 @@ class DocumentController extends GetxController {
);
if (response != null && response.success) {
if (response.data.data.isNotEmpty) {
documents.addAll(response.data.data);
if (response.data?.data.isNotEmpty ?? false) {
documents.addAll(response.data!.data);
pageNumber.value++;
} else {
hasMore.value = false;

View File

@ -3,81 +3,92 @@ import 'package:get/get.dart';
import 'package:on_field_work/controller/project_controller.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
class CustomAppBar extends StatelessWidget
with UIMixin
implements PreferredSizeWidget {
final String title;
final String? projectName;
final VoidCallback? onBackPressed;
final Color? backgroundColor;
const CustomAppBar({
CustomAppBar({
super.key,
required this.title,
this.projectName,
this.onBackPressed,
this.backgroundColor,
});
@override
Size get preferredSize => const Size.fromHeight(72);
@override
Widget build(BuildContext context) {
return PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
final Color effectiveBackgroundColor =
backgroundColor ?? contentTheme.primary;
const Color onPrimaryColor = Colors.white;
const double horizontalPadding = 16.0;
return AppBar(
backgroundColor: effectiveBackgroundColor,
elevation: 0,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(0)),
),
leading: Padding(
padding: MySpacing.only(left: horizontalPadding),
child: IconButton(
icon: const Icon(
Icons.arrow_back_ios_new,
color: Colors.black,
color: onPrimaryColor,
size: 20,
),
onPressed: onBackPressed ?? () => Get.back(),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
MySpacing.width(5),
Expanded(
),
title: Padding(
padding: MySpacing.only(right: horizontalPadding, left: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
// TITLE
MyText.titleLarge(
title,
fontWeight: 700,
color: Colors.black,
fontWeight: 800,
color: onPrimaryColor,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
MySpacing.height(2),
// PROJECT NAME ROW
MySpacing.height(3),
GetBuilder<ProjectController>(
builder: (projectController) {
// NEW LOGIC simple and safe
final displayProjectName =
projectName ??
final displayProjectName = projectName ??
projectController.selectedProject?.name ??
'Select Project';
return Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.work_outline,
size: 14,
color: Colors.grey,
),
const Icon(Icons.folder_open,
size: 14, color: onPrimaryColor),
MySpacing.width(4),
Expanded(
Flexible(
child: MyText.bodySmall(
displayProjectName,
fontWeight: 600,
fontWeight: 500,
color: onPrimaryColor.withOpacity(0.8),
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
MySpacing.width(2),
const Icon(Icons.keyboard_arrow_down,
size: 18, color: onPrimaryColor),
],
);
},
@ -85,13 +96,15 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
],
),
),
actions: [
Padding(
padding: MySpacing.only(right: horizontalPadding),
child: IconButton(
icon: const Icon(Icons.home, color: onPrimaryColor, size: 24),
onPressed: () => Get.offAllNamed('/dashboard'),
),
),
],
),
),
),
);
}
@override
Size get preferredSize => const Size.fromHeight(72);
}

View File

@ -148,7 +148,7 @@ class DocumentType {
final String name;
final String? regexExpression;
final String allowedContentType;
final int maxSizeAllowedInMB;
final double maxSizeAllowedInMB;
final bool isValidationRequired;
final bool isMandatory;
final bool isSystem;

View File

@ -1,7 +1,7 @@
class DocumentsResponse {
final bool success;
final String message;
final DocumentDataWrapper data;
final DocumentDataWrapper? data;
final dynamic errors;
final int statusCode;
final DateTime timestamp;
@ -9,7 +9,7 @@ class DocumentsResponse {
DocumentsResponse({
required this.success,
required this.message,
required this.data,
this.data,
this.errors,
required this.statusCode,
required this.timestamp,
@ -19,11 +19,13 @@ class DocumentsResponse {
return DocumentsResponse(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: DocumentDataWrapper.fromJson(json['data']),
data: json['data'] != null
? DocumentDataWrapper.fromJson(json['data'])
: null,
errors: json['errors'],
statusCode: json['statusCode'] ?? 0,
timestamp: json['timestamp'] != null
? DateTime.parse(json['timestamp'])
? DateTime.tryParse(json['timestamp']) ?? DateTime.now()
: DateTime.now(),
);
}
@ -32,7 +34,7 @@ class DocumentsResponse {
return {
'success': success,
'message': message,
'data': data.toJson(),
'data': data?.toJson(),
'errors': errors,
'statusCode': statusCode,
'timestamp': timestamp.toIso8601String(),
@ -61,9 +63,10 @@ class DocumentDataWrapper {
currentPage: json['currentPage'] ?? 0,
totalPages: json['totalPages'] ?? 0,
totalEntites: json['totalEntites'] ?? 0,
data: (json['data'] as List<dynamic>? ?? [])
.map((e) => DocumentItem.fromJson(e))
.toList(),
data: (json['data'] as List<dynamic>?)
?.map((e) => DocumentItem.fromJson(e))
.toList() ??
[],
);
}
@ -83,28 +86,28 @@ class DocumentItem {
final String name;
final String documentId;
final String description;
final DateTime uploadedAt;
final DateTime? uploadedAt;
final String? parentAttachmentId;
final bool isCurrentVersion;
final int version;
final bool isActive;
final bool? isVerified;
final UploadedBy uploadedBy;
final DocumentType documentType;
final UploadedBy? uploadedBy;
final DocumentType? documentType;
DocumentItem({
required this.id,
required this.name,
required this.documentId,
required this.description,
required this.uploadedAt,
this.uploadedAt,
this.parentAttachmentId,
required this.isCurrentVersion,
required this.version,
required this.isActive,
this.isVerified,
required this.uploadedBy,
required this.documentType,
this.uploadedBy,
this.documentType,
});
factory DocumentItem.fromJson(Map<String, dynamic> json) {
@ -113,14 +116,20 @@ class DocumentItem {
name: json['name'] ?? '',
documentId: json['documentId'] ?? '',
description: json['description'] ?? '',
uploadedAt: DateTime.parse(json['uploadedAt']),
uploadedAt: json['uploadedAt'] != null
? DateTime.tryParse(json['uploadedAt'])
: null,
parentAttachmentId: json['parentAttachmentId'],
isCurrentVersion: json['isCurrentVersion'] ?? false,
version: json['version'] ?? 0,
isActive: json['isActive'] ?? false,
isVerified: json['isVerified'],
uploadedBy: UploadedBy.fromJson(json['uploadedBy']),
documentType: DocumentType.fromJson(json['documentType']),
uploadedBy: json['uploadedBy'] != null
? UploadedBy.fromJson(json['uploadedBy'])
: null,
documentType: json['documentType'] != null
? DocumentType.fromJson(json['documentType'])
: null,
);
}
@ -130,14 +139,14 @@ class DocumentItem {
'name': name,
'documentId': documentId,
'description': description,
'uploadedAt': uploadedAt.toIso8601String(),
'uploadedAt': uploadedAt?.toIso8601String(),
'parentAttachmentId': parentAttachmentId,
'isCurrentVersion': isCurrentVersion,
'version': version,
'isActive': isActive,
'isVerified': isVerified,
'uploadedBy': uploadedBy.toJson(),
'documentType': documentType.toJson(),
'uploadedBy': uploadedBy?.toJson(),
'documentType': documentType?.toJson(),
};
}
}
@ -208,7 +217,7 @@ class DocumentType {
final String name;
final String? regexExpression;
final String? allowedContentType;
final int? maxSizeAllowedInMB;
final double? maxSizeAllowedInMB;
final bool isValidationRequired;
final bool isMandatory;
final bool isSystem;
@ -232,7 +241,7 @@ class DocumentType {
return DocumentType(
id: json['id'] ?? '',
name: json['name'] ?? '',
regexExpression: json['regexExpression'], // nullable
regexExpression: json['regexExpression'],
allowedContentType: json['allowedContentType'],
maxSizeAllowedInMB: json['maxSizeAllowedInMB'],
isValidationRequired: json['isValidationRequired'] ?? false,

View File

@ -14,6 +14,7 @@ import 'package:on_field_work/view/Attendence/regularization_requests_tab.dart';
import 'package:on_field_work/view/Attendence/attendance_logs_tab.dart';
import 'package:on_field_work/view/Attendence/todays_attendance_tab.dart';
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
class AttendanceScreen extends StatefulWidget {
const AttendanceScreen({super.key});
@ -75,61 +76,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
}
}
Widget _buildAppBar() {
return AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard'),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleLarge('Attendance',
fontWeight: 700, color: Colors.black),
MySpacing.height(2),
GetBuilder<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],
),
),
],
);
},
),
],
),
),
],
),
),
);
}
Widget _buildFilterSearchRow() {
return Padding(
padding: MySpacing.xy(8, 8),
@ -358,24 +304,44 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(72),
child: _buildAppBar(),
appBar: CustomAppBar(
title: "Attendance",
backgroundColor: appBarColor,
onBackPressed: () => Get.toNamed('/dashboard'),
),
body: SafeArea(
body: Stack(
children: [
// Gradient container at top
Container(
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
// Main content
SafeArea(
child: GetBuilder<AttendanceController>(
init: attendanceController,
tag: 'attendance_dashboard_controller',
builder: (controller) {
final selectedProjectId = projectController.selectedProjectId.value;
final selectedProjectId =
projectController.selectedProjectId.value;
final noProjectSelected = selectedProjectId.isEmpty;
return MyRefreshIndicator(
onRefresh: _refreshData,
child: Builder(
builder: (context) {
return SingleChildScrollView(
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: MySpacing.zero,
child: Column(
@ -394,13 +360,13 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
),
],
),
);
},
),
);
},
),
),
],
),
);
}

View File

@ -6,17 +6,12 @@ import 'package:on_field_work/controller/project_controller.dart';
import 'package:on_field_work/controller/dynamicMenu/dynamic_menu_controller.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
import 'package:on_field_work/helpers/widgets/my_card.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/attendance_overview_chart.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/project_progress_chart.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/expense_breakdown_chart.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/expense_by_status_widget.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/dashboard_overview_widgets.dart';
import 'package:on_field_work/view/layouts/layout.dart';
import 'package:on_field_work/helpers/utils/permission_constants.dart';
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
@ -44,64 +39,289 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
return Layout(
child: SingleChildScrollView(
padding: const EdgeInsets.all(10),
//---------------------------------------------------------------------------
// REUSABLE CARD (smaller, minimal)
//---------------------------------------------------------------------------
Widget _cardWrapper({required Widget child}) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
border: Border.all(color: Colors.black12.withOpacity(.04)),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(.05),
blurRadius: 12,
offset: const Offset(0, 4),
)
],
),
child: child,
);
}
//---------------------------------------------------------------------------
// SECTION TITLE
//---------------------------------------------------------------------------
Widget _sectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8),
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Colors.black87,
),
),
);
}
//---------------------------------------------------------------------------
// CONDITIONAL QUICK ACTION CARD
//---------------------------------------------------------------------------
Widget _conditionalQuickActionCard() {
// STATIC CONDITION
String status = "O"; // <-- change if needed
bool isCheckedIn = status == "O";
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
colors: isCheckedIn
? [Colors.red.shade200, Colors.red.shade400]
: [Colors.green.shade200, Colors.green.shade400],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDashboardCards(),
MySpacing.height(24),
_buildProjectSelector(),
MySpacing.height(24),
_buildAttendanceChartSection(),
MySpacing.height(12),
MySpacing.height(24),
_buildProjectProgressChartSection(),
MySpacing.height(24),
SizedBox(
width: double.infinity,
child: DashboardOverviewWidgets.teamsOverview(),
// Title & Status
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
isCheckedIn ? "Checked-In" : "Not Checked-In",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
MySpacing.height(24),
SizedBox(
width: double.infinity,
child: DashboardOverviewWidgets.tasksOverview(),
),
MySpacing.height(24),
ExpenseByStatusWidget(controller: dashboardController),
MySpacing.height(24),
ExpenseTypeReportChart(),
MySpacing.height(24),
MonthlyExpenseDashboardChart(),
Icon(
isCheckedIn ? LucideIcons.log_out : LucideIcons.log_in,
color: Colors.white,
size: 24,
),
],
),
const SizedBox(height: 8),
// Description
Text(
isCheckedIn
? "You are currently checked-in. Don't forget to check-out after your work."
: "You are not checked-in yet. Please check-in to start your work.",
style: const TextStyle(color: Colors.white70, fontSize: 13),
),
const SizedBox(height: 12),
// Action Buttons
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (!isCheckedIn)
ElevatedButton.icon(
onPressed: () {
// Check-In action
},
label: const Text("Check-In"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade700,
foregroundColor: Colors.white,
),
),
if (isCheckedIn)
ElevatedButton.icon(
onPressed: () {
// Check-Out action
},
label: const Text("Check-Out"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white,
),
),
],
),
],
),
);
}
//---------------------------------------------------------------------------
// QUICK ACTIONS (updated to use the single card)
//---------------------------------------------------------------------------
Widget _quickActions() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle("Quick Action"), // Change title to singular
_conditionalQuickActionCard(), // Use the new conditional card
],
);
}
//---------------------------------------------------------------------------
// PROJECT DROPDOWN (clean compact)
//---------------------------------------------------------------------------
Widget _projectSelector() {
return Obx(() {
final isLoading = projectController.isLoading.value;
final expanded = projectController.isProjectSelectionExpanded.value;
final projects = projectController.projects;
final selectedId = projectController.selectedProjectId.value;
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle("Project"),
// Compact Selector
GestureDetector(
onTap: () => projectController.isProjectSelectionExpanded.toggle(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
border: Border.all(color: Colors.black12.withOpacity(.15)),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(.04),
blurRadius: 6,
offset: const Offset(0, 2),
)
],
),
child: Row(
children: [
const Icon(Icons.work_outline, color: Colors.blue, size: 20),
const SizedBox(width: 12),
Expanded(
child: Text(
projects
.firstWhereOrNull((p) => p.id == selectedId)
?.name ??
"Select Project",
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w600),
),
),
Icon(
expanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
size: 26,
color: Colors.black54,
)
],
),
),
),
if (expanded) _projectDropdownList(projects, selectedId),
],
);
});
}
/// ---------------- Dynamic Dashboard Cards ----------------
Widget _buildDashboardCards() {
return Obx(() {
if (menuController.isLoading.value) {
return SkeletonLoaders.dashboardCardsSkeleton();
Widget _projectDropdownList(projects, selectedId) {
return Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
border: Border.all(color: Colors.black12.withOpacity(.2)),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(.07),
blurRadius: 10,
offset: const Offset(0, 3),
),
],
),
constraints:
BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.33),
child: Column(
children: [
TextField(
decoration: InputDecoration(
hintText: "Search project...",
isDense: true,
prefixIcon: const Icon(Icons.search),
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(5)),
),
),
const SizedBox(height: 10),
Expanded(
child: ListView.builder(
itemCount: projects.length,
itemBuilder: (_, index) {
final project = projects[index];
return RadioListTile<String>(
dense: true,
value: project.id,
groupValue: selectedId,
onChanged: (v) {
if (v != null) {
projectController.updateSelectedProject(v);
projectController.isProjectSelectionExpanded.value =
false;
}
if (menuController.hasError.value || menuController.menuItems.isEmpty) {
return const Center(
child: Text(
"Failed to load menus. Please try again later.",
style: TextStyle(color: Colors.red),
},
title: Text(project.name),
);
},
),
),
],
),
);
}
//---------------------------------------------------------------------------
// DASHBOARD MODULE CARDS (UPDATED FOR MINIMAL PADDING / SLL SIZE)
//---------------------------------------------------------------------------
Widget _dashboardCards() {
return Obx(() {
if (menuController.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
final projectSelected = projectController.selectedProject != null;
// Define dashboard card meta with order
final List<String> cardOrder = [
final cardOrder = [
MenuItems.attendance,
MenuItems.employees,
MenuItems.dailyTaskPlanning,
@ -109,10 +329,10 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
MenuItems.directory,
MenuItems.finance,
MenuItems.documents,
MenuItems.serviceProjects
MenuItems.serviceProjects,
];
final Map<String, _DashboardCardMeta> cardMeta = {
final meta = {
MenuItems.attendance:
_DashboardCardMeta(LucideIcons.scan_face, contentTheme.success),
MenuItems.employees:
@ -131,356 +351,94 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
_DashboardCardMeta(LucideIcons.package, contentTheme.info),
};
// Filter only available menus that exist in cardMeta
final allowedMenusMap = {
for (var menu in menuController.menuItems)
if (menu.available && cardMeta.containsKey(menu.id)) menu.id: menu
final allowed = {
for (var m in menuController.menuItems)
if (m.available && meta.containsKey(m.id)) m.id: m
};
if (allowedMenusMap.isEmpty) {
return const Center(
child: Text(
"No accessible modules found.",
style: TextStyle(color: Colors.grey),
),
);
}
// Create list of cards in fixed order
final stats =
cardOrder.where((id) => allowedMenusMap.containsKey(id)).map((id) {
final menu = allowedMenusMap[id]!;
final meta = cardMeta[id]!;
return _DashboardStatItem(
meta.icon, menu.name, meta.color, menu.mobileLink);
}).toList();
return LayoutBuilder(builder: (context, constraints) {
int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4);
double cardWidth =
(constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount;
return Wrap(
spacing: 6,
runSpacing: 6,
alignment: WrapAlignment.start,
children: stats
.map((stat) =>
_buildDashboardCard(stat, projectSelected, cardWidth))
.toList(),
);
});
});
}
Widget _buildDashboardCard(
_DashboardStatItem stat, bool isProjectSelected, double width) {
final isEnabled = stat.title == "Attendance" ? true : isProjectSelected;
return Opacity(
opacity: isEnabled ? 1.0 : 0.4,
child: IgnorePointer(
ignoring: !isEnabled,
child: InkWell(
onTap: () => _onDashboardCardTap(stat, isEnabled),
borderRadius: BorderRadius.circular(5),
child: MyCard.bordered(
width: width,
height: 60,
paddingAll: 4,
borderRadiusAll: 5,
border: Border.all(color: Colors.grey.withOpacity(0.15)),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: stat.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
stat.icon,
size: 16,
color: stat.color,
),
),
MySpacing.height(4),
Flexible(
child: Text(
stat.title,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 10,
overflow: TextOverflow.ellipsis,
),
maxLines: 2,
),
),
],
),
),
),
),
);
}
void _onDashboardCardTap(_DashboardStatItem statItem, bool isEnabled) {
if (!isEnabled) {
Get.defaultDialog(
title: "No Project Selected",
middleText: "Please select a project before accessing this module.",
confirm: ElevatedButton(
onPressed: () => Get.back(),
child: const Text("OK"),
),
);
} else {
Get.toNamed(statItem.route);
}
}
/// ---------------- Project Progress Chart ----------------
Widget _buildProjectProgressChartSection() {
return Obx(() {
if (dashboardController.projectChartData.isEmpty) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(
child: Text("No project progress data available."),
),
);
}
return ClipRRect(
borderRadius: BorderRadius.circular(5),
child: SizedBox(
height: 400,
child: ProjectProgressChart(
data: dashboardController.projectChartData,
),
),
);
});
}
/// ---------------- Attendance Chart ----------------
Widget _buildAttendanceChartSection() {
return Obx(() {
final attendanceMenu = menuController.menuItems
.firstWhereOrNull((m) => m.id == MenuItems.attendance);
if (attendanceMenu == null || !attendanceMenu.available)
return const SizedBox.shrink();
final isProjectSelected = projectController.selectedProject != null;
return Opacity(
opacity: isProjectSelected ? 1.0 : 0.4,
child: IgnorePointer(
ignoring: !isProjectSelected,
child: ClipRRect(
borderRadius: BorderRadius.circular(5),
child: SizedBox(
height: 400,
child: AttendanceDashboardChart(),
),
),
),
);
});
}
/// ---------------- Project Selector (Inserted between Attendance & Project Progress)
Widget _buildProjectSelector() {
return Obx(() {
final isLoading = projectController.isLoading.value;
final isExpanded = projectController.isProjectSelectionExpanded.value;
final projects = projectController.projects;
final selectedProjectId = projectController.selectedProjectId.value;
final hasProjects = projects.isNotEmpty;
if (isLoading) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
);
}
if (!hasProjects) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: const [
Icon(Icons.warning_amber_outlined, color: Colors.redAccent),
SizedBox(width: 8),
Text(
"No Project Assigned",
style: TextStyle(
color: Colors.redAccent,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
final selectedProject =
projects.firstWhereOrNull((p) => p.id == selectedProjectId);
final searchNotifier = ValueNotifier<String>("");
final filtered = cardOrder.where(allowed.containsKey).toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () => projectController.isProjectSelectionExpanded.toggle(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
border: Border.all(color: Colors.grey.withOpacity(0.15)),
color: Colors.white,
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 1,
offset: Offset(0, 1))
],
),
child: Row(
children: [
const Icon(Icons.work_outline,
size: 18, color: Colors.blueAccent),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
selectedProject?.name ?? "Select Project",
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.w700),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
"Tap to switch project (${projects.length})",
style: const TextStyle(
fontSize: 12, color: Colors.black54),
),
],
),
),
Icon(
isExpanded
? Icons.arrow_drop_up_outlined
: Icons.arrow_drop_down_outlined,
color: Colors.black,
),
],
),
),
),
if (isExpanded)
ValueListenableBuilder<String>(
valueListenable: searchNotifier,
builder: (context, query, _) {
final lowerQuery = query.toLowerCase();
final filteredProjects = lowerQuery.isEmpty
? projects
: projects
.where((p) => p.name.toLowerCase().contains(lowerQuery))
.toList();
return Container(
margin: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
border: Border.all(color: Colors.grey.withOpacity(0.12)),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: Offset(0, 2))
],
),
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.35),
child: Column(
children: [
TextField(
decoration: InputDecoration(
isDense: true,
prefixIcon: const Icon(Icons.search, size: 18),
hintText: "Search project",
hintStyle: const TextStyle(fontSize: 13),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 2),
),
onChanged: (value) => searchNotifier.value = value,
),
const SizedBox(height: 8),
if (filteredProjects.isEmpty)
const Expanded(
child: Center(
child: Text("No projects found",
style: TextStyle(
fontSize: 13, color: Colors.black54)),
),
)
else
Expanded(
child: ListView.builder(
_sectionTitle("Modules"),
GridView.builder(
shrinkWrap: true,
itemCount: filteredProjects.length,
physics: const NeverScrollableScrollPhysics(),
// **More compact grid**
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 6,
mainAxisSpacing: 6,
childAspectRatio: 1.2, // smaller & tighter
),
itemCount: filtered.length,
itemBuilder: (context, index) {
final project = filteredProjects[index];
final isSelected =
project.id == selectedProjectId;
return RadioListTile<String>(
value: project.id,
groupValue: selectedProjectId,
onChanged: (value) {
if (value != null) {
projectController
.updateSelectedProject(value);
projectController.isProjectSelectionExpanded
.value = false;
final id = filtered[index];
final item = allowed[id]!;
final cardMeta = meta[id]!;
final isEnabled =
item.name == "Attendance" ? true : projectSelected;
return GestureDetector(
onTap: () {
if (!isEnabled) {
Get.defaultDialog(
title: "No Project Selected",
middleText: "Please select a project first.",
);
} else {
Get.toNamed(item.mobileLink);
}
},
title: Text(
project.name,
child: Container(
// **Reduced padding**
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: isEnabled ? Colors.white : Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.black12.withOpacity(.1),
width: 0.7,
),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(.05),
blurRadius: 4,
offset: const Offset(0, 2),
)
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
cardMeta.icon,
size: 20, // **smaller icon**
color:
isEnabled ? cardMeta.color : Colors.grey.shade400,
),
const SizedBox(height: 3),
Text(
item.name,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
color: isSelected
? Colors.blueAccent
: Colors.black87,
),
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 0),
activeColor: Colors.blueAccent,
tileColor: isSelected
? Colors.blueAccent.withOpacity(0.06)
: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)),
visualDensity:
const VisualDensity(vertical: -4),
);
},
fontSize: 9.5, // **reduced text size**
fontWeight: FontWeight.w600,
color:
isEnabled ? Colors.black87 : Colors.grey.shade600,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
},
),
@ -488,16 +446,41 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
);
});
}
//---------------------------------------------------------------------------
// MAIN UI
//---------------------------------------------------------------------------
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xfff5f6fa),
body: Layout(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_projectSelector(),
MySpacing.height(20),
_quickActions(),
MySpacing.height(20),
// The updated module cards
_dashboardCards(),
MySpacing.height(20),
_sectionTitle("Reports & Analytics"),
_cardWrapper(child: ExpenseTypeReportChart()),
_cardWrapper(
child:
ExpenseByStatusWidget(controller: dashboardController)),
_cardWrapper(child: MonthlyExpenseDashboardChart()),
MySpacing.height(20),
],
),
),
),
);
}
/// ---------------- Dashboard Card Models ----------------
class _DashboardStatItem {
final IconData icon;
final String title;
final Color color;
final String route;
_DashboardStatItem(this.icon, this.title, this.color, this.route);
}
class _DashboardCardMeta {

View File

@ -13,6 +13,7 @@ import 'package:on_field_work/helpers/utils/date_time_utils.dart';
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
import 'package:on_field_work/helpers/widgets/my_confirmation_dialog.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
class ContactDetailScreen extends StatefulWidget {
final ContactModel contact;
@ -23,18 +24,21 @@ class ContactDetailScreen extends StatefulWidget {
}
class _ContactDetailScreenState extends State<ContactDetailScreen>
with UIMixin {
with SingleTickerProviderStateMixin, UIMixin {
late final DirectoryController directoryController;
late final ProjectController projectController;
late Rx<ContactModel> contactRx;
late TabController _tabController;
@override
void initState() {
super.initState();
directoryController = Get.find<DirectoryController>();
projectController = Get.find<ProjectController>();
projectController = Get.put(ProjectController());
contactRx = widget.contact.obs;
_tabController = TabController(length: 2, vsync: this);
WidgetsBinding.instance.addPostFrameCallback((_) async {
await directoryController.fetchCommentsForContact(contactRx.value.id,
active: true);
@ -50,66 +54,73 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: _buildMainAppBar(),
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Obx(() => _buildSubHeader(contactRx.value)),
const Divider(height: 1, thickness: 0.5, color: Colors.grey),
Expanded(
child: TabBarView(children: [
Obx(() => _buildDetailsTab(contactRx.value)),
_buildCommentsTab(),
]),
),
],
),
),
),
);
void dispose() {
_tabController.dispose();
super.dispose();
}
PreferredSizeWidget _buildMainAppBar() {
return AppBar(
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.2,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
body: Stack(
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () =>
// GRADIENT BEHIND APPBAR & TABBAR
Positioned.fill(
child: Column(
children: [
Container(
height: 120,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
Expanded(child: Container(color: Colors.grey[100])),
],
),
),
// 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'),
),
MySpacing.width(8),
// SUBHEADER + TABBAR
Obx(() => _buildSubHeader(contactRx.value)),
// TABBAR VIEW
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
child: TabBarView(
controller: _tabController,
children: [
MyText.titleLarge('Contact Profile',
fontWeight: 700, color: Colors.black),
MySpacing.height(2),
GetBuilder<ProjectController>(builder: (p) {
return ProjectLabel(p.selectedProject?.name);
}),
Obx(() => _buildDetailsTab(contactRx.value)),
_buildCommentsTab(),
],
),
),
],
),
),
],
),
);
}
@ -118,7 +129,9 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
final lastName =
contact.name.split(" ").length > 1 ? contact.name.split(" ").last : "";
return Padding(
return Container(
color: Colors.transparent,
child: Padding(
padding: MySpacing.xy(16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -138,6 +151,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
),
]),
TabBar(
controller: _tabController,
labelColor: Colors.black,
unselectedLabelColor: Colors.grey,
indicatorColor: contentTheme.primary,
@ -148,9 +162,11 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
),
],
),
),
);
}
// --- DETAILS TAB ---
Widget _buildDetailsTab(ContactModel contact) {
final tags = contact.tags.map((e) => e.name).join(", ");
final bucketNames = contact.bucketIds
@ -228,7 +244,8 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
_iconInfoRow(Icons.location_on, "Address", contact.address),
]),
_infoCard("Organization", [
_iconInfoRow(Icons.business, "Organization", contact.organization),
_iconInfoRow(
Icons.business, "Organization", contact.organization),
_iconInfoRow(Icons.category, "Category", category),
]),
_infoCard("Meta Info", [
@ -281,6 +298,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
);
}
// --- COMMENTS TAB ---
Widget _buildCommentsTab() {
return Obx(() {
final contactId = contactRx.value.id;
@ -622,25 +640,3 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
);
}
}
class ProjectLabel extends StatelessWidget {
final String? projectName;
const ProjectLabel(this.projectName, {super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
const Icon(Icons.work_outline, size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
projectName ?? 'Select Project',
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
);
}
}

View File

@ -13,6 +13,7 @@ import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/model/document/document_edit_bottom_sheet.dart';
import 'package:on_field_work/controller/permission_controller.dart';
import 'package:on_field_work/helpers/utils/permission_constants.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
class DocumentDetailsPage extends StatefulWidget {
final String documentId;
@ -23,7 +24,7 @@ class DocumentDetailsPage extends StatefulWidget {
State<DocumentDetailsPage> createState() => _DocumentDetailsPageState();
}
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
class _DocumentDetailsPageState extends State<DocumentDetailsPage> with UIMixin {
final DocumentDetailsController controller =
Get.find<DocumentDetailsController>();
@ -49,15 +50,37 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: const Color(0xFFF1F1F1),
appBar: CustomAppBar(
title: 'Document Details',
backgroundColor: appBarColor,
onBackPressed: () {
Get.back();
},
),
body: Obx(() {
body: Stack(
children: [
// Gradient behind content
Container(
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
// Main content
SafeArea(
child: Obx(() {
if (controller.isLoading.value) {
return SkeletonLoaders.documentDetailsSkeletonLoader();
}
@ -84,8 +107,11 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
children: [
_buildDetailsCard(doc),
const SizedBox(height: 20),
MyText.titleMedium("Versions",
fontWeight: 700, color: Colors.black),
MyText.titleMedium(
"Versions",
fontWeight: 700,
color: Colors.black,
),
const SizedBox(height: 10),
_buildVersionsSection(),
],
@ -93,6 +119,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
),
);
}),
),
],
),
);
}

View File

@ -428,14 +428,21 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
}
Widget _buildDocumentCard(DocumentItem doc, bool showDateHeader) {
final uploadDate =
DateFormat("dd MMM yyyy").format(doc.uploadedAt.toLocal());
final uploadTime = DateFormat("hh:mm a").format(doc.uploadedAt.toLocal());
final uploader = doc.uploadedBy.firstName.isNotEmpty
? "${doc.uploadedBy.firstName} ${doc.uploadedBy.lastName}".trim()
final uploadDate = doc.uploadedAt != null
? DateFormat("dd MMM yyyy").format(doc.uploadedAt!.toLocal())
: '-';
final uploadTime = doc.uploadedAt != null
? DateFormat("hh:mm a").format(doc.uploadedAt!.toLocal())
: '';
final uploader =
(doc.uploadedBy != null && doc.uploadedBy!.firstName.isNotEmpty)
? "${doc.uploadedBy!.firstName} ${doc.uploadedBy!.lastName ?? ''}"
.trim()
: "You";
final iconColor = _getDocumentTypeColor(doc.documentType.name);
final iconColor =
_getDocumentTypeColor(doc.documentType?.name ?? 'unknown');
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -479,11 +486,10 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
borderRadius: BorderRadius.circular(10),
),
child: Icon(
_getDocumentIcon(doc.documentType.name),
_getDocumentIcon(doc.documentType?.name ?? 'unknown'),
color: iconColor,
size: 24,
),
),
)),
const SizedBox(width: 14),
Expanded(
child: Column(
@ -497,7 +503,7 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
borderRadius: BorderRadius.circular(6),
),
child: MyText.labelSmall(
doc.documentType.name,
doc.documentType?.name ?? 'Unknown',
fontWeight: 600,
color: iconColor,
letterSpacing: 0.3,
@ -872,12 +878,18 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
}
final doc = docs[index];
final currentDate = DateFormat("dd MMM yyyy")
.format(doc.uploadedAt.toLocal());
final prevDate = index > 0
final currentDate = doc.uploadedAt != null
? DateFormat("dd MMM yyyy")
.format(docs[index - 1].uploadedAt.toLocal())
.format(doc.uploadedAt!.toLocal())
: '';
final prevDate = index > 0
? (docs[index - 1].uploadedAt != null
? DateFormat("dd MMM yyyy").format(
docs[index - 1].uploadedAt!.toLocal())
: '')
: null;
final showDateHeader = currentDate != prevDate;
return _buildDocumentCard(doc, showDateHeader);

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:on_field_work/view/employees/employee_detail_screen.dart';
import 'package:on_field_work/view/document/user_document_screen.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
class EmployeeProfilePage extends StatefulWidget {
final String employeeId;
@ -14,7 +15,7 @@ class EmployeeProfilePage extends StatefulWidget {
}
class _EmployeeProfilePageState extends State<EmployeeProfilePage>
with SingleTickerProviderStateMixin {
with SingleTickerProviderStateMixin, UIMixin {
late TabController _tabController;
@override
@ -31,41 +32,58 @@ class _EmployeeProfilePageState extends State<EmployeeProfilePage>
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: const Color(0xFFF1F1F1),
appBar: CustomAppBar(
title: "Employee Profile",
onBackPressed: () => Get.back(),
backgroundColor: appBarColor,
),
body: Column(
body: Stack(
children: [
// ---------------- TabBar outside AppBar ----------------
// === Gradient at the top behind AppBar + TabBar ===
Container(
color: Colors.white,
height: 50,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
SafeArea(
top: false,
bottom: true,
child: Column(
children: [
Container(
decoration: const BoxDecoration(color: Colors.transparent),
child: TabBar(
controller: _tabController,
labelColor: Colors.black,
unselectedLabelColor: Colors.grey,
indicatorColor: Colors.red,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
indicatorColor: Colors.white,
indicatorWeight: 3,
tabs: const [
Tab(text: "Details"),
Tab(text: "Documents"),
],
),
),
// ---------------- TabBarView ----------------
Expanded(
child: TabBarView(
controller: _tabController,
children: [
// Details Tab
EmployeeDetailPage(
employeeId: widget.employeeId,
fromProfile: true,
),
// Documents Tab
UserDocumentsPage(
entityId: widget.employeeId,
isEmployee: true,
@ -75,6 +93,9 @@ class _EmployeeProfilePageState extends State<EmployeeProfilePage>
),
],
),
),
],
),
);
}
}

View File

@ -17,6 +17,7 @@ import 'package:on_field_work/helpers/utils/permission_constants.dart';
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
import 'package:on_field_work/view/employees/employee_profile_screen.dart';
import 'package:on_field_work/view/employees/manage_reporting_bottom_sheet.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
class EmployeesScreen extends StatefulWidget {
const EmployeesScreen({super.key});
@ -113,11 +114,36 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(),
floatingActionButton: _buildFloatingActionButton(),
body: SafeArea(
backgroundColor: const Color(0xFFF5F5F5),
appBar: CustomAppBar(
title: "Employees",
backgroundColor: appBarColor,
projectName: Get.find<ProjectController>().selectedProject?.name ??
'Select Project',
onBackPressed: () => Get.offNamed('/dashboard'),
),
body: Stack(
children: [
// Gradient behind content
Container(
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
// Main content
SafeArea(
child: GetBuilder<EmployeesScreenController>(
init: _employeeController,
tag: 'employee_screen_controller',
@ -148,63 +174,9 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
},
),
),
);
}
PreferredSizeWidget _buildAppBar() {
return PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard'),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleLarge('Employees',
fontWeight: 700, color: Colors.black),
MySpacing.height(2),
GetBuilder<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],
),
),
],
);
},
),
],
),
),
],
),
),
),
floatingActionButton: _buildFloatingActionButton(),
);
}

View File

@ -14,7 +14,7 @@ import 'package:on_field_work/controller/expense/add_expense_controller.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
import 'package:on_field_work/helpers/widgets/expense/expense_detail_helpers.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart';
@ -83,12 +83,37 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: const Color(0xFFF7F7F7),
appBar: _AppBar(projectController: projectController),
body: SafeArea(
appBar: CustomAppBar(
title: "Expense Details",
backgroundColor: appBarColor,
onBackPressed: () => Get.toNamed('/dashboard/expense-main-page'),
),
body: Stack(
children: [
// Gradient behind content
Container(
height: kToolbarHeight + MediaQuery.of(context).padding.top,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
// Main content
SafeArea(
child: Obx(() {
if (controller.isLoading.value) return buildLoadingSkeleton();
final expense = controller.expense.value;
if (controller.errorMessage.isNotEmpty || expense == null) {
return Center(child: MyText.bodyMedium("No data to display."));
@ -98,8 +123,10 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
_checkPermissionToSubmit(expense);
});
final statusColor = getExpenseStatusColor(expense.status.name,
colorCode: expense.status.color);
final statusColor = getExpenseStatusColor(
expense.status.name,
colorCode: expense.status.color,
);
final formattedAmount = formatExpenseAmount(expense.amount);
return MyRefreshIndicator(
@ -108,7 +135,8 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
},
child: SingleChildScrollView(
padding: EdgeInsets.fromLTRB(
12, 12, 12, 30 + MediaQuery.of(context).padding.bottom),
12, 12, 12, 30 + MediaQuery.of(context).padding.bottom
),
child: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 520),
@ -122,21 +150,21 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ---------------- Header & Status ----------------
// Header & Status
_InvoiceHeader(expense: expense),
const Divider(height: 30, thickness: 1.2),
// ---------------- Activity Logs ----------------
// Activity Logs
InvoiceLogs(logs: expense.expenseLogs),
const Divider(height: 30, thickness: 1.2),
// ---------------- Amount & Summary ----------------
// Amount & Summary
Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium('Amount',
fontWeight: 600),
MyText.bodyMedium('Amount', fontWeight: 600),
const SizedBox(height: 4),
MyText.bodyLarge(
formattedAmount,
@ -146,7 +174,6 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
],
),
const Spacer(),
// Optional: Pre-approved badge
if (expense.preApproved)
Container(
padding: const EdgeInsets.symmetric(
@ -165,19 +192,19 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
),
const Divider(height: 30, thickness: 1.2),
// ---------------- Parties ----------------
// Parties
_InvoicePartiesTable(expense: expense),
const Divider(height: 30, thickness: 1.2),
// ---------------- Expense Details ----------------
// Expense Details
_InvoiceDetailsTable(expense: expense),
const Divider(height: 30, thickness: 1.2),
// ---------------- Documents ----------------
// Documents
_InvoiceDocuments(documents: expense.documents),
const Divider(height: 30, thickness: 1.2),
// ---------------- Totals ----------------
// Totals
_InvoiceTotals(
expense: expense,
formattedAmount: formattedAmount,
@ -189,15 +216,18 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
),
),
),
));
),
);
}),
),
],
),
floatingActionButton: Obx(() {
if (controller.isLoading.value) return buildLoadingSkeleton();
final expense = controller.expense.value;
if (controller.errorMessage.isNotEmpty || expense == null) {
return Center(child: MyText.bodyMedium("No data to display."));
return const SizedBox.shrink();
}
if (!_checkedPermission) {
@ -237,10 +267,8 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
})
.toList(),
};
logSafe('editData: $editData', level: LogLevel.info);
final addCtrl = Get.put(AddExpenseController());
await addCtrl.loadMasterData();
addCtrl.populateFieldsForEdit(editData);
@ -279,22 +307,7 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
final isCreatedByCurrentUser =
employeeInfo?.id == expense.createdBy.id;
logSafe(
'🔐 Permission Logic:\n'
'🔸 Status: ${next.name}\n'
'🔸 Status ID: ${next.id}\n'
'🔸 Parsed Permissions: $parsedPermissions\n'
'🔸 Is Submit: $isSubmitStatus\n'
'🔸 Created By Current User: $isCreatedByCurrentUser',
level: LogLevel.debug,
);
if (isSubmitStatus) {
// Submit can be done ONLY by the creator
return isCreatedByCurrentUser;
}
// All other statuses - check permission normally
if (isSubmitStatus) return isCreatedByCurrentUser;
return permissionController.hasAnyPermission(parsedPermissions);
}).map((next) {
return _statusButton(context, controller, expense, next);
@ -306,6 +319,7 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
);
}
Widget _statusButton(BuildContext context, ExpenseDetailController controller,
ExpenseDetailModel expense, dynamic next) {
Color primary = Colors.red;
@ -449,64 +463,6 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
}
}
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
final ProjectController projectController;
const _AppBar({required this.projectController});
@override
Widget build(BuildContext context) {
return AppBar(
automaticallyImplyLeading: false,
elevation: 1,
backgroundColor: Colors.white,
title: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.toNamed('/dashboard/expense-main-page'),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleLarge('Expense Details',
fontWeight: 700, color: Colors.black),
MySpacing.height(2),
GetBuilder<ProjectController>(
builder: (_) {
final projectName =
projectController.selectedProject?.name ??
'Select Project';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
projectName,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
);
},
),
],
),
),
],
),
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _InvoiceHeader extends StatelessWidget {
final ExpenseDetailModel expense;
const _InvoiceHeader({required this.expense});

View File

@ -12,6 +12,7 @@ import 'package:on_field_work/helpers/widgets/expense/expense_main_components.da
import 'package:on_field_work/helpers/utils/permission_constants.dart';
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
class ExpenseMainScreen extends StatefulWidget {
const ExpenseMainScreen({super.key});
@ -87,19 +88,57 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen>
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: Colors.white,
appBar: ExpenseAppBar(projectController: projectController),
body: Column(
appBar: CustomAppBar(
title: "Expense & Reimbursement",
backgroundColor: appBarColor,
onBackPressed: () => Get.toNamed('/dashboard/finance'),
),
body: Stack(
children: [
// === FULL GRADIENT BEHIND APPBAR & TABBAR ===
Positioned.fill(
child: Column(
children: [
// ---------------- TabBar ----------------
Container(
color: Colors.white,
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
Expanded(
child:
Container(color: Colors.grey[100]),
),
],
),
),
// === MAIN CONTENT ===
SafeArea(
top: false,
bottom: true,
child: Column(
children: [
// TAB BAR WITH TRANSPARENT BACKGROUND
Container(
decoration: const BoxDecoration(color: Colors.transparent),
child: TabBar(
controller: _tabController,
labelColor: Colors.black,
unselectedLabelColor: Colors.grey,
indicatorColor: Colors.red,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
indicatorColor: Colors.white,
indicatorWeight: 3,
tabs: const [
Tab(text: "Current Month"),
Tab(text: "History"),
@ -107,16 +146,15 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen>
),
),
// ---------------- Gray background for rest ----------------
// CONTENT AREA
Expanded(
child: Container(
color: Colors.grey[100],
color: Colors.transparent,
child: Column(
children: [
// ---------------- Search ----------------
// SEARCH & FILTER
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
padding: const EdgeInsets.symmetric(horizontal: 0),
child: SearchAndFilter(
controller: searchController,
onChanged: (_) => setState(() {}),
@ -125,7 +163,7 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen>
),
),
// ---------------- TabBarView ----------------
// TABBAR VIEW
Expanded(
child: TabBarView(
controller: _tabController,
@ -141,11 +179,12 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen>
),
],
),
),
],
),
floatingActionButton: Obx(() {
// Show loader or hide FAB while permissions are loading
if (permissionController.permissions.isEmpty) {
if (permissionController.permissions.isEmpty)
return const SizedBox.shrink();
}
final canUpload =
permissionController.hasPermission(Permissions.expenseUpload);

View File

@ -3,10 +3,9 @@ import 'package:get/get.dart';
import 'package:on_field_work/controller/finance/advance_payment_controller.dart';
import 'package:on_field_work/controller/project_controller.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
class AdvancePaymentScreen extends StatefulWidget {
const AdvancePaymentScreen({super.key});
@ -49,12 +48,35 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: _buildAppBar(),
appBar: CustomAppBar(
title: "Advance Payments",
onBackPressed: () => Get.offNamed('/dashboard/finance'),
backgroundColor: appBarColor,
),
body: Stack(
children: [
// ===== TOP GRADIENT =====
Container(
height: 100,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
// SafeArea added so nothing hides under system navigation buttons
body: SafeArea(
// ===== MAIN CONTENT =====
SafeArea(
top: false,
bottom: true,
child: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
@ -66,131 +88,66 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
}
},
color: Colors.white,
backgroundColor: contentTheme.primary,
backgroundColor: appBarColor,
strokeWidth: 2.5,
displacement: 60,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
// Extra bottom padding so content does NOT go under 3-button navbar
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 20,
),
child: Column(
children: [
_buildSearchBar(),
_buildEmployeeDropdown(context),
_buildTopBalance(),
_buildPaymentList(),
],
),
),
),
),
),
),
);
}
// ---------------- AppBar ----------------
PreferredSizeWidget _buildAppBar() {
return PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard/finance'),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Advance Payments',
fontWeight: 700,
color: Colors.black,
),
MySpacing.height(2),
GetBuilder<ProjectController>(
builder: (_) {
final name = projectController.selectedProject?.name ??
'Select Project';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
name,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
);
},
),
],
),
),
],
),
),
),
);
}
// ---------------- Search ----------------
Widget _buildSearchBar() {
return Container(
color: Colors.grey[100],
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
Expanded(
// ===== SEARCH BAR FLOATING OVER GRADIENT =====
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
child: SizedBox(
height: 38,
child: TextField(
controller: _searchCtrl,
focusNode: _searchFocus,
onChanged: (v) => controller.searchQuery.value = v.trim(),
onChanged: (v) =>
controller.searchQuery.value = v.trim(),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 0),
prefixIcon:
const Icon(Icons.search, size: 20, color: Colors.grey),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 0),
prefixIcon: const Icon(Icons.search,
size: 20, color: Colors.grey),
hintText: 'Search Employee...',
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide:
BorderSide(color: Colors.grey.shade300, width: 1),
borderSide: BorderSide(
color: Colors.grey.shade300, width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide:
BorderSide(color: Colors.grey.shade300, width: 1),
borderSide: BorderSide(
color: Colors.grey.shade300, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide:
BorderSide(color: contentTheme.primary, width: 1.5),
borderSide: BorderSide(
color: appBarColor, width: 1.5),
),
),
),
),
),
// ===== EMPLOYEE DROPDOWN =====
_buildEmployeeDropdown(context),
// ===== TOP BALANCE =====
_buildTopBalance(),
// ===== PAYMENTS LIST =====
_buildPaymentList(),
],
),
),
),
),

View File

@ -6,13 +6,14 @@ import 'package:on_field_work/controller/dynamicMenu/dynamic_menu_controller.dar
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
import 'package:on_field_work/helpers/widgets/my_card.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/expense_breakdown_chart.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/expense_by_status_widget.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart';
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
import 'package:on_field_work/helpers/utils/permission_constants.dart';
import 'package:on_field_work/model/dynamicMenu/dynamic_menu_model.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
class FinanceScreen extends StatefulWidget {
const FinanceScreen({super.key});
@ -52,70 +53,54 @@ class _FinanceScreenState extends State<FinanceScreen>
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
appBar: PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
appBar: CustomAppBar(
title: "Finance",
onBackPressed: () => Get.offAllNamed( '/dashboard' ),
backgroundColor: appBarColor,
),
body: Stack(
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard'),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Finance',
fontWeight: 700,
color: Colors.black,
),
MySpacing.height(2),
GetBuilder<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],
),
),
],
);
},
),
// Top fade under AppBar
Container(
height: 40,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
// Bottom fade (above system buttons or FAB)
Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 60,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
appBarColor.withOpacity(0.05),
Colors.transparent,
],
),
),
),
),
body: SafeArea(
top: false, // keep appbar area same
bottom: true, // avoid system bottom buttons
// Main scrollable content
SafeArea(
top: false,
bottom: true,
child: FadeTransition(
opacity: _fadeAnimation,
child: Obx(() {
@ -152,7 +137,6 @@ class _FinanceScreenState extends State<FinanceScreen>
);
}
// ---- IMPORTANT FIX: Add bottom safe padding ----
final double bottomInset =
MediaQuery.of(context).viewPadding.bottom;
@ -161,8 +145,7 @@ class _FinanceScreenState extends State<FinanceScreen>
16,
16,
16,
bottomInset +
24, // ensures charts never go under system buttons
bottomInset + 24,
),
child: Column(
children: [
@ -179,6 +162,8 @@ class _FinanceScreenState extends State<FinanceScreen>
}),
),
),
],
),
);
}

View File

@ -21,6 +21,7 @@ import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/model/finance/payment_request_rembursement_bottom_sheet.dart';
import 'package:on_field_work/model/finance/make_expense_bottom_sheet.dart';
import 'package:on_field_work/model/finance/add_payment_request_bottom_sheet.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
class PaymentRequestDetailScreen extends StatefulWidget {
final String paymentRequestId;
@ -107,10 +108,33 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(),
body: SafeArea(
appBar: CustomAppBar(
title: "Payment Request Details",
backgroundColor: appBarColor,
),
body: Stack(
children: [
// ===== TOP GRADIENT =====
Container(
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
// ===== MAIN CONTENT =====
SafeArea(
child: Obx(() {
if (controller.isLoading.value &&
controller.paymentRequest.value == null) {
@ -178,6 +202,8 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
);
}),
),
],
),
bottomNavigationBar: _buildBottomActionBar(),
);
}
@ -295,65 +321,6 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
);
});
}
PreferredSizeWidget _buildAppBar() {
return PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.back(),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Payment Request Details',
fontWeight: 700,
color: Colors.black,
),
MySpacing.height(2),
GetBuilder<ProjectController>(builder: (_) {
final name = projectController.selectedProject?.name ??
'Select Project';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
name,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
);
}),
],
),
),
],
),
),
),
);
}
}
class PaymentRequestPermissionHelper {

View File

@ -13,6 +13,7 @@ import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
import 'package:on_field_work/controller/permission_controller.dart';
import 'package:on_field_work/helpers/utils/permission_constants.dart';
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
class PaymentRequestMainScreen extends StatefulWidget {
const PaymentRequestMainScreen({super.key});
@ -96,33 +97,67 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(),
// ------------------------
// FIX: SafeArea prevents content from going under 3-button navbar
// ------------------------
body: SafeArea(
bottom: true,
appBar: CustomAppBar(
title: "Payment Requests",
onBackPressed: () => Get.offNamed('/dashboard/finance'),
backgroundColor: appBarColor,
),
body: Stack(
children: [
// === FULL GRADIENT BEHIND APPBAR & TABBAR ===
Positioned.fill(
child: Column(
children: [
Container(
color: Colors.white,
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
Expanded(
child:
Container(color: Colors.grey[100]),
),
],
),
),
// === MAIN CONTENT ===
SafeArea(
top: false,
bottom: true,
child: Column(
children: [
// TAB BAR WITH TRANSPARENT BACKGROUND
Container(
decoration: const BoxDecoration(color: Colors.transparent),
child: TabBar(
controller: _tabController,
labelColor: Colors.black,
unselectedLabelColor: Colors.grey,
indicatorColor: Colors.red,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
indicatorColor: Colors.white,
tabs: const [
Tab(text: "Current Month"),
Tab(text: "History"),
],
),
),
// CONTENT AREA
Expanded(
child: Container(
color: Colors.grey[100],
color: Colors.transparent,
child: Column(
children: [
_buildSearchBar(),
@ -142,7 +177,8 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
],
),
),
],
),
floatingActionButton: Obx(() {
if (permissionController.permissions.isEmpty) {
return const SizedBox.shrink();
@ -166,67 +202,6 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
);
}
PreferredSizeWidget _buildAppBar() {
return PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard/finance'),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Payment Requests',
fontWeight: 700,
color: Colors.black,
),
MySpacing.height(2),
GetBuilder<ProjectController>(
builder: (_) {
final name = projectController.selectedProject?.name ??
'Select Project';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
name,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
);
},
),
],
),
),
],
),
),
),
);
}
Widget _buildSearchBar() {
return Padding(
padding: MySpacing.fromLTRB(12, 10, 12, 0),

View File

@ -64,15 +64,16 @@ class _LayoutState extends State<Layout> {
body: SafeArea(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {}, // project selection removed nothing to close
onTap: () {},
child: Column(
children: [
_buildHeader(context, isMobile),
Expanded(
child: SingleChildScrollView(
key: controller.scrollKey,
padding: EdgeInsets.symmetric(
horizontal: 0, vertical: isMobile ? 16 : 32),
// Removed redundant vertical padding here. DashboardScreen's
// SingleChildScrollView now handles all internal padding.
padding: EdgeInsets.symmetric(horizontal: 0, vertical: 0),
child: widget.child,
),
),

View File

@ -429,6 +429,8 @@ class _ServiceProjectDetailsScreenState
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: CustomAppBar(
@ -436,28 +438,52 @@ class _ServiceProjectDetailsScreenState
projectName: widget.projectName,
onBackPressed: () => Get.toNamed('/dashboard/service-projects'),
),
body: SafeArea(
body: Stack(
children: [
// === TOP FADE BELOW APPBAR ===
Container(
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
SafeArea(
top: false,
bottom: true,
child: Column(
children: [
// TabBar
// === TAB BAR WITH TRANSPARENT BACKGROUND ===
Container(
color: Colors.white,
decoration: const BoxDecoration(color: Colors.transparent),
child: TabBar(
controller: _tabController,
labelColor: Colors.black,
unselectedLabelColor: Colors.grey,
indicatorColor: Colors.red,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
indicatorColor: Colors.white,
indicatorWeight: 3,
isScrollable: false,
tabs: [
Tab(child: MyText.bodyMedium("Profile")),
Tab(child: MyText.bodyMedium("Jobs")),
Tab(child: MyText.bodyMedium("Teams")),
Tab(
child: MyText.bodyMedium("Profile",
color: Colors.white)),
Tab(
child:
MyText.bodyMedium("Jobs", color: Colors.white)),
Tab(
child:
MyText.bodyMedium("Teams", color: Colors.white)),
],
),
),
// TabBarView
// === TABBAR VIEW ===
Expanded(
child: Obx(() {
if (controller.isLoading.value &&
@ -467,7 +493,8 @@ class _ServiceProjectDetailsScreenState
if (controller.errorMessage.value.isNotEmpty &&
controller.projectDetail.value == null) {
return Center(
child: MyText.bodyMedium(controller.errorMessage.value));
child:
MyText.bodyMedium(controller.errorMessage.value));
}
return TabBarView(
@ -486,6 +513,8 @@ class _ServiceProjectDetailsScreenState
],
),
),
],
),
floatingActionButton: _tabController.index == 1
? FloatingActionButton.extended(
onPressed: () {

View File

@ -760,17 +760,20 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
@override
Widget build(BuildContext context) {
final projectName = widget.projectName;
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: CustomAppBar(
title: "Job Details Screen",
onBackPressed: () => Get.back(),
projectName: projectName,
backgroundColor: appBarColor,
),
floatingActionButton: Obx(() => FloatingActionButton.extended(
onPressed:
isEditing.value ? _editJob : () => isEditing.value = true,
backgroundColor: contentTheme.primary,
backgroundColor: appBarColor,
label: MyText.bodyMedium(
isEditing.value ? "Save" : "Edit",
color: Colors.white,
@ -778,14 +781,50 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
),
icon: Icon(isEditing.value ? Icons.save : Icons.edit),
)),
body: Obx(() {
body: Stack(
children: [
Container(
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
// Bottom fade (for smooth transition above FAB)
Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 60, // adjust based on FAB height
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
appBarColor.withOpacity(0.05),
Colors.transparent,
],
),
),
),
),
// Main scrollable content
Obx(() {
if (controller.isJobDetailLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (controller.jobDetailErrorMessage.value.isNotEmpty) {
return Center(
child: MyText.bodyMedium(controller.jobDetailErrorMessage.value));
child: MyText.bodyMedium(
controller.jobDetailErrorMessage.value));
}
final job = controller.jobDetail.value?.data;
@ -835,6 +874,8 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
),
);
}),
],
),
);
}
}

View File

@ -181,17 +181,33 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: CustomAppBar(
title: "Service Projects",
projectName: 'All Service Projects',
backgroundColor: appBarColor,
onBackPressed: () => Get.toNamed('/dashboard'),
),
body: Stack(
children: [
Container(
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
// FIX 1: Entire body wrapped in SafeArea
body: SafeArea(
// Main content
SafeArea(
bottom: true,
child: Column(
children: [
@ -209,12 +225,12 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
const EdgeInsets.symmetric(horizontal: 12),
prefixIcon: const Icon(Icons.search,
size: 20, color: Colors.grey),
suffixIcon: ValueListenableBuilder<TextEditingValue>(
suffixIcon:
ValueListenableBuilder<TextEditingValue>(
valueListenable: searchController,
builder: (context, value, _) {
if (value.text.isEmpty) {
if (value.text.isEmpty)
return const SizedBox.shrink();
}
return IconButton(
icon: const Icon(Icons.clear,
size: 20, color: Colors.grey),
@ -230,11 +246,13 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300),
borderSide:
BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300),
borderSide:
BorderSide(color: Colors.grey.shade300),
),
),
),
@ -248,7 +266,6 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
final projects = controller.filteredProjects;
return MyRefreshIndicator(
@ -259,11 +276,8 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
? _buildEmptyState()
: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
// FIX 2: Increased bottom padding for landscape
padding: MySpacing.only(
left: 8, right: 8, top: 4, bottom: 120),
itemCount: projects.length,
separatorBuilder: (_, __) => MySpacing.height(12),
itemBuilder: (_, index) =>
@ -275,6 +289,8 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
],
),
),
],
),
);
}
}

View File

@ -17,6 +17,8 @@ import 'package:on_field_work/model/dailyTaskPlanning/task_action_buttons.dart';
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
import 'package:on_field_work/helpers/utils/permission_constants.dart';
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
class DailyProgressReportScreen extends StatefulWidget {
const DailyProgressReportScreen({super.key});
@ -87,70 +89,40 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
appBar: CustomAppBar(
title: 'Daily Progress Report',
backgroundColor: appBarColor,
projectName:
projectController.selectedProject?.name ?? 'Select Project',
onBackPressed: () => Get.offNamed('/dashboard'),
),
body: Stack(
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard'),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Daily Progress Report',
fontWeight: 700,
color: Colors.black,
),
MySpacing.height(2),
GetBuilder<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],
),
),
],
);
},
),
],
),
),
// Gradient behind content (like EmployeesScreen)
Container(
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
),
body: SafeArea(
// Main content
SafeArea(
child: MyRefreshIndicator(
onRefresh: _refreshData,
child: CustomScrollView(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
@ -165,8 +137,7 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
Padding(
padding: MySpacing.x(15),
child: Row(
mainAxisAlignment:
MainAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
InkWell(
borderRadius: BorderRadius.circular(22),
@ -182,9 +153,8 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
color: Colors.black,
),
const SizedBox(width: 4),
Icon(Icons.tune,
const Icon(Icons.tune,
size: 20, color: Colors.black),
],
),
),
@ -206,6 +176,8 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
),
),
),
],
),
);
}

View File

@ -15,6 +15,8 @@ import 'package:on_field_work/helpers/utils/permission_constants.dart';
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
import 'package:on_field_work/controller/tenant/service_controller.dart';
import 'package:on_field_work/helpers/widgets/tenant/service_selector.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
class DailyTaskPlanningScreen extends StatefulWidget {
DailyTaskPlanningScreen({super.key});
@ -58,72 +60,41 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
@override
Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
appBar: CustomAppBar(
title: 'Daily Task Planning',
backgroundColor: appBarColor,
projectName:
projectController.selectedProject?.name ?? 'Select Project',
onBackPressed: () => Get.offNamed('/dashboard'),
),
body: Stack(
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard'),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Daily Task Planning',
fontWeight: 700,
color: Colors.black,
),
MySpacing.height(2),
GetBuilder<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],
),
),
],
);
}),
],
),
),
// Gradient behind content
Container(
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
),
),
),
),
body: SafeArea(
// Main content
SafeArea(
child: MyRefreshIndicator(
onRefresh: () async {
final projectId = projectController.selectedProjectId.value;
if (projectId.isNotEmpty) {
try {
// keep previous behavior but now fetchTaskData is lighter (buildings only)
await dailyTaskPlanningController.fetchTaskData(
projectId,
serviceId: serviceController.selectedService?.id,
@ -159,10 +130,10 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
final projectId =
projectController.selectedProjectId.value;
if (projectId.isNotEmpty) {
await dailyTaskPlanningController.fetchTaskData(
await dailyTaskPlanningController
.fetchTaskData(
projectId,
serviceId:
service?.id, // <-- pass selected service
serviceId: service?.id,
);
}
},
@ -181,6 +152,8 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
),
),
),
],
),
);
}
@ -227,8 +200,7 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
final buildings = dailyTasks
.expand((task) => task.buildings)
.where((building) =>
(building.plannedWork ) > 0 ||
(building.completedWork ) > 0)
(building.plannedWork) > 0 || (building.completedWork) > 0)
.toList();
if (buildings.isEmpty) {