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 != null && response.success) {
if (response.data.data.isNotEmpty) { if (response.data?.data.isNotEmpty ?? false) {
documents.addAll(response.data.data); documents.addAll(response.data!.data);
pageNumber.value++; pageNumber.value++;
} else { } else {
hasMore.value = false; 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/controller/project_controller.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.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/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 title;
final String? projectName; final String? projectName;
final VoidCallback? onBackPressed; final VoidCallback? onBackPressed;
final Color? backgroundColor;
const CustomAppBar({ CustomAppBar({
super.key, super.key,
required this.title, required this.title,
this.projectName, this.projectName,
this.onBackPressed, this.onBackPressed,
this.backgroundColor,
}); });
@override
Size get preferredSize => const Size.fromHeight(72);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PreferredSize( final Color effectiveBackgroundColor =
preferredSize: const Size.fromHeight(72), backgroundColor ?? contentTheme.primary;
child: AppBar( const Color onPrimaryColor = Colors.white;
backgroundColor: const Color(0xFFF5F5F5), const double horizontalPadding = 16.0;
elevation: 0.5,
return AppBar(
backgroundColor: effectiveBackgroundColor,
elevation: 0,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
titleSpacing: 0, titleSpacing: 0,
title: Padding( shape: const RoundedRectangleBorder(
padding: MySpacing.xy(16, 0), borderRadius: BorderRadius.vertical(bottom: Radius.circular(0)),
child: Row( ),
crossAxisAlignment: CrossAxisAlignment.center, leading: Padding(
children: [ padding: MySpacing.only(left: horizontalPadding),
IconButton( child: IconButton(
icon: const Icon( icon: const Icon(
Icons.arrow_back_ios_new, Icons.arrow_back_ios_new,
color: Colors.black, color: onPrimaryColor,
size: 20, size: 20,
), ),
onPressed: onBackPressed ?? () => Get.back(), 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// TITLE
MyText.titleLarge( MyText.titleLarge(
title, title,
fontWeight: 700, fontWeight: 800,
color: Colors.black, color: onPrimaryColor,
overflow: TextOverflow.ellipsis,
maxLines: 1,
), ),
MySpacing.height(3),
MySpacing.height(2),
// PROJECT NAME ROW
GetBuilder<ProjectController>( GetBuilder<ProjectController>(
builder: (projectController) { builder: (projectController) {
// NEW LOGIC simple and safe final displayProjectName = projectName ??
final displayProjectName =
projectName ??
projectController.selectedProject?.name ?? projectController.selectedProject?.name ??
'Select Project'; 'Select Project';
return Row( return Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon( const Icon(Icons.folder_open,
Icons.work_outline, size: 14, color: onPrimaryColor),
size: 14,
color: Colors.grey,
),
MySpacing.width(4), MySpacing.width(4),
Expanded( Flexible(
child: MyText.bodySmall( child: MyText.bodySmall(
displayProjectName, displayProjectName,
fontWeight: 600, fontWeight: 500,
color: onPrimaryColor.withOpacity(0.8),
overflow: TextOverflow.ellipsis, 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 name;
final String? regexExpression; final String? regexExpression;
final String allowedContentType; final String allowedContentType;
final int maxSizeAllowedInMB; final double maxSizeAllowedInMB;
final bool isValidationRequired; final bool isValidationRequired;
final bool isMandatory; final bool isMandatory;
final bool isSystem; final bool isSystem;

View File

@ -1,7 +1,7 @@
class DocumentsResponse { class DocumentsResponse {
final bool success; final bool success;
final String message; final String message;
final DocumentDataWrapper data; final DocumentDataWrapper? data;
final dynamic errors; final dynamic errors;
final int statusCode; final int statusCode;
final DateTime timestamp; final DateTime timestamp;
@ -9,7 +9,7 @@ class DocumentsResponse {
DocumentsResponse({ DocumentsResponse({
required this.success, required this.success,
required this.message, required this.message,
required this.data, this.data,
this.errors, this.errors,
required this.statusCode, required this.statusCode,
required this.timestamp, required this.timestamp,
@ -19,11 +19,13 @@ class DocumentsResponse {
return DocumentsResponse( return DocumentsResponse(
success: json['success'] ?? false, success: json['success'] ?? false,
message: json['message'] ?? '', message: json['message'] ?? '',
data: DocumentDataWrapper.fromJson(json['data']), data: json['data'] != null
? DocumentDataWrapper.fromJson(json['data'])
: null,
errors: json['errors'], errors: json['errors'],
statusCode: json['statusCode'] ?? 0, statusCode: json['statusCode'] ?? 0,
timestamp: json['timestamp'] != null timestamp: json['timestamp'] != null
? DateTime.parse(json['timestamp']) ? DateTime.tryParse(json['timestamp']) ?? DateTime.now()
: DateTime.now(), : DateTime.now(),
); );
} }
@ -32,7 +34,7 @@ class DocumentsResponse {
return { return {
'success': success, 'success': success,
'message': message, 'message': message,
'data': data.toJson(), 'data': data?.toJson(),
'errors': errors, 'errors': errors,
'statusCode': statusCode, 'statusCode': statusCode,
'timestamp': timestamp.toIso8601String(), 'timestamp': timestamp.toIso8601String(),
@ -61,9 +63,10 @@ class DocumentDataWrapper {
currentPage: json['currentPage'] ?? 0, currentPage: json['currentPage'] ?? 0,
totalPages: json['totalPages'] ?? 0, totalPages: json['totalPages'] ?? 0,
totalEntites: json['totalEntites'] ?? 0, totalEntites: json['totalEntites'] ?? 0,
data: (json['data'] as List<dynamic>? ?? []) data: (json['data'] as List<dynamic>?)
.map((e) => DocumentItem.fromJson(e)) ?.map((e) => DocumentItem.fromJson(e))
.toList(), .toList() ??
[],
); );
} }
@ -83,28 +86,28 @@ class DocumentItem {
final String name; final String name;
final String documentId; final String documentId;
final String description; final String description;
final DateTime uploadedAt; final DateTime? uploadedAt;
final String? parentAttachmentId; final String? parentAttachmentId;
final bool isCurrentVersion; final bool isCurrentVersion;
final int version; final int version;
final bool isActive; final bool isActive;
final bool? isVerified; final bool? isVerified;
final UploadedBy uploadedBy; final UploadedBy? uploadedBy;
final DocumentType documentType; final DocumentType? documentType;
DocumentItem({ DocumentItem({
required this.id, required this.id,
required this.name, required this.name,
required this.documentId, required this.documentId,
required this.description, required this.description,
required this.uploadedAt, this.uploadedAt,
this.parentAttachmentId, this.parentAttachmentId,
required this.isCurrentVersion, required this.isCurrentVersion,
required this.version, required this.version,
required this.isActive, required this.isActive,
this.isVerified, this.isVerified,
required this.uploadedBy, this.uploadedBy,
required this.documentType, this.documentType,
}); });
factory DocumentItem.fromJson(Map<String, dynamic> json) { factory DocumentItem.fromJson(Map<String, dynamic> json) {
@ -113,14 +116,20 @@ class DocumentItem {
name: json['name'] ?? '', name: json['name'] ?? '',
documentId: json['documentId'] ?? '', documentId: json['documentId'] ?? '',
description: json['description'] ?? '', description: json['description'] ?? '',
uploadedAt: DateTime.parse(json['uploadedAt']), uploadedAt: json['uploadedAt'] != null
? DateTime.tryParse(json['uploadedAt'])
: null,
parentAttachmentId: json['parentAttachmentId'], parentAttachmentId: json['parentAttachmentId'],
isCurrentVersion: json['isCurrentVersion'] ?? false, isCurrentVersion: json['isCurrentVersion'] ?? false,
version: json['version'] ?? 0, version: json['version'] ?? 0,
isActive: json['isActive'] ?? false, isActive: json['isActive'] ?? false,
isVerified: json['isVerified'], isVerified: json['isVerified'],
uploadedBy: UploadedBy.fromJson(json['uploadedBy']), uploadedBy: json['uploadedBy'] != null
documentType: DocumentType.fromJson(json['documentType']), ? UploadedBy.fromJson(json['uploadedBy'])
: null,
documentType: json['documentType'] != null
? DocumentType.fromJson(json['documentType'])
: null,
); );
} }
@ -130,14 +139,14 @@ class DocumentItem {
'name': name, 'name': name,
'documentId': documentId, 'documentId': documentId,
'description': description, 'description': description,
'uploadedAt': uploadedAt.toIso8601String(), 'uploadedAt': uploadedAt?.toIso8601String(),
'parentAttachmentId': parentAttachmentId, 'parentAttachmentId': parentAttachmentId,
'isCurrentVersion': isCurrentVersion, 'isCurrentVersion': isCurrentVersion,
'version': version, 'version': version,
'isActive': isActive, 'isActive': isActive,
'isVerified': isVerified, 'isVerified': isVerified,
'uploadedBy': uploadedBy.toJson(), 'uploadedBy': uploadedBy?.toJson(),
'documentType': documentType.toJson(), 'documentType': documentType?.toJson(),
}; };
} }
} }
@ -208,7 +217,7 @@ class DocumentType {
final String name; final String name;
final String? regexExpression; final String? regexExpression;
final String? allowedContentType; final String? allowedContentType;
final int? maxSizeAllowedInMB; final double? maxSizeAllowedInMB;
final bool isValidationRequired; final bool isValidationRequired;
final bool isMandatory; final bool isMandatory;
final bool isSystem; final bool isSystem;
@ -232,7 +241,7 @@ class DocumentType {
return DocumentType( return DocumentType(
id: json['id'] ?? '', id: json['id'] ?? '',
name: json['name'] ?? '', name: json['name'] ?? '',
regexExpression: json['regexExpression'], // nullable regexExpression: json['regexExpression'],
allowedContentType: json['allowedContentType'], allowedContentType: json['allowedContentType'],
maxSizeAllowedInMB: json['maxSizeAllowedInMB'], maxSizeAllowedInMB: json['maxSizeAllowedInMB'],
isValidationRequired: json['isValidationRequired'] ?? false, 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/attendance_logs_tab.dart';
import 'package:on_field_work/view/Attendence/todays_attendance_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/my_refresh_indicator.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
class AttendanceScreen extends StatefulWidget { class AttendanceScreen extends StatefulWidget {
const AttendanceScreen({super.key}); 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() { Widget _buildFilterSearchRow() {
return Padding( return Padding(
padding: MySpacing.xy(8, 8), padding: MySpacing.xy(8, 8),
@ -358,24 +304,44 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold( return Scaffold(
appBar: PreferredSize( appBar: CustomAppBar(
preferredSize: const Size.fromHeight(72), title: "Attendance",
child: _buildAppBar(), 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>( child: GetBuilder<AttendanceController>(
init: attendanceController, init: attendanceController,
tag: 'attendance_dashboard_controller', tag: 'attendance_dashboard_controller',
builder: (controller) { builder: (controller) {
final selectedProjectId = projectController.selectedProjectId.value; final selectedProjectId =
projectController.selectedProjectId.value;
final noProjectSelected = selectedProjectId.isEmpty; final noProjectSelected = selectedProjectId.isEmpty;
return MyRefreshIndicator( return MyRefreshIndicator(
onRefresh: _refreshData, onRefresh: _refreshData,
child: Builder( child: SingleChildScrollView(
builder: (context) {
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
padding: MySpacing.zero, padding: MySpacing.zero,
child: Column( 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/controller/dynamicMenu/dynamic_menu_controller.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.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/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_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_breakdown_chart.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/expense_by_status_widget.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/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/view/layouts/layout.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_custom_skeleton.dart';
class DashboardScreen extends StatefulWidget { class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key}); const DashboardScreen({super.key});
@ -44,64 +39,289 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
if (mounted) setState(() {}); if (mounted) setState(() {});
} }
@override //---------------------------------------------------------------------------
Widget build(BuildContext context) { // REUSABLE CARD (smaller, minimal)
return Layout( //---------------------------------------------------------------------------
child: SingleChildScrollView(
padding: const EdgeInsets.all(10), 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildDashboardCards(), // Title & Status
MySpacing.height(24), Row(
_buildProjectSelector(), mainAxisAlignment: MainAxisAlignment.spaceBetween,
MySpacing.height(24), children: [
_buildAttendanceChartSection(), Text(
MySpacing.height(12), isCheckedIn ? "Checked-In" : "Not Checked-In",
MySpacing.height(24), style: const TextStyle(
_buildProjectProgressChartSection(), fontSize: 16,
MySpacing.height(24), fontWeight: FontWeight.bold,
SizedBox( color: Colors.white,
width: double.infinity,
child: DashboardOverviewWidgets.teamsOverview(),
), ),
MySpacing.height(24),
SizedBox(
width: double.infinity,
child: DashboardOverviewWidgets.tasksOverview(),
), ),
MySpacing.height(24), Icon(
ExpenseByStatusWidget(controller: dashboardController), isCheckedIn ? LucideIcons.log_out : LucideIcons.log_in,
MySpacing.height(24), color: Colors.white,
ExpenseTypeReportChart(), size: 24,
MySpacing.height(24), ),
MonthlyExpenseDashboardChart(), ],
),
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 _projectDropdownList(projects, selectedId) {
Widget _buildDashboardCards() { return Container(
return Obx(() { margin: const EdgeInsets.only(top: 10),
if (menuController.isLoading.value) { padding: const EdgeInsets.all(12),
return SkeletonLoaders.dashboardCardsSkeleton(); 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) { title: Text(project.name),
return const Center( );
child: Text( },
"Failed to load menus. Please try again later.", ),
style: TextStyle(color: Colors.red), ),
],
), ),
); );
} }
//---------------------------------------------------------------------------
// 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; final projectSelected = projectController.selectedProject != null;
// Define dashboard card meta with order final cardOrder = [
final List<String> cardOrder = [
MenuItems.attendance, MenuItems.attendance,
MenuItems.employees, MenuItems.employees,
MenuItems.dailyTaskPlanning, MenuItems.dailyTaskPlanning,
@ -109,10 +329,10 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
MenuItems.directory, MenuItems.directory,
MenuItems.finance, MenuItems.finance,
MenuItems.documents, MenuItems.documents,
MenuItems.serviceProjects MenuItems.serviceProjects,
]; ];
final Map<String, _DashboardCardMeta> cardMeta = { final meta = {
MenuItems.attendance: MenuItems.attendance:
_DashboardCardMeta(LucideIcons.scan_face, contentTheme.success), _DashboardCardMeta(LucideIcons.scan_face, contentTheme.success),
MenuItems.employees: MenuItems.employees:
@ -131,356 +351,94 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
_DashboardCardMeta(LucideIcons.package, contentTheme.info), _DashboardCardMeta(LucideIcons.package, contentTheme.info),
}; };
// Filter only available menus that exist in cardMeta final allowed = {
final allowedMenusMap = { for (var m in menuController.menuItems)
for (var menu in menuController.menuItems) if (m.available && meta.containsKey(m.id)) m.id: m
if (menu.available && cardMeta.containsKey(menu.id)) menu.id: menu
}; };
if (allowedMenusMap.isEmpty) { final filtered = cardOrder.where(allowed.containsKey).toList();
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>("");
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
GestureDetector( _sectionTitle("Modules"),
onTap: () => projectController.isProjectSelectionExpanded.toggle(), GridView.builder(
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(
shrinkWrap: true, 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) { itemBuilder: (context, index) {
final project = filteredProjects[index]; final id = filtered[index];
final isSelected = final item = allowed[id]!;
project.id == selectedProjectId; final cardMeta = meta[id]!;
return RadioListTile<String>(
value: project.id, final isEnabled =
groupValue: selectedProjectId, item.name == "Attendance" ? true : projectSelected;
onChanged: (value) {
if (value != null) { return GestureDetector(
projectController onTap: () {
.updateSelectedProject(value); if (!isEnabled) {
projectController.isProjectSelectionExpanded Get.defaultDialog(
.value = false; title: "No Project Selected",
middleText: "Please select a project first.",
);
} else {
Get.toNamed(item.mobileLink);
} }
}, },
title: Text( child: Container(
project.name, // **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( style: TextStyle(
fontWeight: isSelected fontSize: 9.5, // **reduced text size**
? FontWeight.bold fontWeight: FontWeight.w600,
: FontWeight.normal, color:
color: isSelected isEnabled ? Colors.black87 : Colors.grey.shade600,
? 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),
);
},
), ),
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 { 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_refresh_indicator.dart';
import 'package:on_field_work/helpers/widgets/my_confirmation_dialog.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/utils/mixins/ui_mixin.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
class ContactDetailScreen extends StatefulWidget { class ContactDetailScreen extends StatefulWidget {
final ContactModel contact; final ContactModel contact;
@ -23,18 +24,21 @@ class ContactDetailScreen extends StatefulWidget {
} }
class _ContactDetailScreenState extends State<ContactDetailScreen> class _ContactDetailScreenState extends State<ContactDetailScreen>
with UIMixin { with SingleTickerProviderStateMixin, UIMixin {
late final DirectoryController directoryController; late final DirectoryController directoryController;
late final ProjectController projectController; late final ProjectController projectController;
late Rx<ContactModel> contactRx; late Rx<ContactModel> contactRx;
late TabController _tabController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
directoryController = Get.find<DirectoryController>(); directoryController = Get.find<DirectoryController>();
projectController = Get.find<ProjectController>(); projectController = Get.put(ProjectController());
contactRx = widget.contact.obs; contactRx = widget.contact.obs;
_tabController = TabController(length: 2, vsync: this);
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
await directoryController.fetchCommentsForContact(contactRx.value.id, await directoryController.fetchCommentsForContact(contactRx.value.id,
active: true); active: true);
@ -50,66 +54,73 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
} }
@override @override
Widget build(BuildContext context) { void dispose() {
return DefaultTabController( _tabController.dispose();
length: 2, super.dispose();
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(),
]),
),
],
),
),
),
);
} }
PreferredSizeWidget _buildMainAppBar() { @override
return AppBar( Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.2, body: Stack(
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
IconButton( // GRADIENT BEHIND APPBAR & TABBAR
icon: const Icon(Icons.arrow_back_ios_new, Positioned.fill(
color: Colors.black, size: 20), child: Column(
onPressed: () => 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'), Get.offAllNamed('/dashboard/directory-main-page'),
), ),
MySpacing.width(8),
// SUBHEADER + TABBAR
Obx(() => _buildSubHeader(contactRx.value)),
// TABBAR VIEW
Expanded( Expanded(
child: Column( child: TabBarView(
crossAxisAlignment: CrossAxisAlignment.start, controller: _tabController,
mainAxisSize: MainAxisSize.min,
children: [ children: [
MyText.titleLarge('Contact Profile', Obx(() => _buildDetailsTab(contactRx.value)),
fontWeight: 700, color: Colors.black), _buildCommentsTab(),
MySpacing.height(2),
GetBuilder<ProjectController>(builder: (p) {
return ProjectLabel(p.selectedProject?.name);
}),
], ],
), ),
), ),
], ],
), ),
), ),
],
),
); );
} }
@ -118,7 +129,9 @@ 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 : "";
return Padding( return Container(
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,
@ -138,6 +151,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
), ),
]), ]),
TabBar( TabBar(
controller: _tabController,
labelColor: Colors.black, labelColor: Colors.black,
unselectedLabelColor: Colors.grey, unselectedLabelColor: Colors.grey,
indicatorColor: contentTheme.primary, indicatorColor: contentTheme.primary,
@ -148,9 +162,11 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
), ),
], ],
), ),
),
); );
} }
// --- DETAILS TAB ---
Widget _buildDetailsTab(ContactModel contact) { Widget _buildDetailsTab(ContactModel contact) {
final tags = contact.tags.map((e) => e.name).join(", "); final tags = contact.tags.map((e) => e.name).join(", ");
final bucketNames = contact.bucketIds final bucketNames = contact.bucketIds
@ -228,7 +244,8 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
_iconInfoRow(Icons.location_on, "Address", contact.address), _iconInfoRow(Icons.location_on, "Address", contact.address),
]), ]),
_infoCard("Organization", [ _infoCard("Organization", [
_iconInfoRow(Icons.business, "Organization", contact.organization), _iconInfoRow(
Icons.business, "Organization", contact.organization),
_iconInfoRow(Icons.category, "Category", category), _iconInfoRow(Icons.category, "Category", category),
]), ]),
_infoCard("Meta Info", [ _infoCard("Meta Info", [
@ -281,6 +298,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
); );
} }
// --- COMMENTS TAB ---
Widget _buildCommentsTab() { Widget _buildCommentsTab() {
return Obx(() { return Obx(() {
final contactId = contactRx.value.id; 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/model/document/document_edit_bottom_sheet.dart';
import 'package:on_field_work/controller/permission_controller.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/permission_constants.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
class DocumentDetailsPage extends StatefulWidget { class DocumentDetailsPage extends StatefulWidget {
final String documentId; final String documentId;
@ -23,7 +24,7 @@ class DocumentDetailsPage extends StatefulWidget {
State<DocumentDetailsPage> createState() => _DocumentDetailsPageState(); State<DocumentDetailsPage> createState() => _DocumentDetailsPageState();
} }
class _DocumentDetailsPageState extends State<DocumentDetailsPage> { class _DocumentDetailsPageState extends State<DocumentDetailsPage> with UIMixin {
final DocumentDetailsController controller = final DocumentDetailsController controller =
Get.find<DocumentDetailsController>(); Get.find<DocumentDetailsController>();
@ -49,15 +50,37 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF1F1F1), backgroundColor: const Color(0xFFF1F1F1),
appBar: CustomAppBar( appBar: CustomAppBar(
title: 'Document Details', title: 'Document Details',
backgroundColor: appBarColor,
onBackPressed: () { onBackPressed: () {
Get.back(); 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) { if (controller.isLoading.value) {
return SkeletonLoaders.documentDetailsSkeletonLoader(); return SkeletonLoaders.documentDetailsSkeletonLoader();
} }
@ -84,8 +107,11 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
children: [ children: [
_buildDetailsCard(doc), _buildDetailsCard(doc),
const SizedBox(height: 20), const SizedBox(height: 20),
MyText.titleMedium("Versions", MyText.titleMedium(
fontWeight: 700, color: Colors.black), "Versions",
fontWeight: 700,
color: Colors.black,
),
const SizedBox(height: 10), const SizedBox(height: 10),
_buildVersionsSection(), _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) { Widget _buildDocumentCard(DocumentItem doc, bool showDateHeader) {
final uploadDate = final uploadDate = doc.uploadedAt != null
DateFormat("dd MMM yyyy").format(doc.uploadedAt.toLocal()); ? DateFormat("dd MMM yyyy").format(doc.uploadedAt!.toLocal())
final uploadTime = DateFormat("hh:mm a").format(doc.uploadedAt.toLocal()); : '-';
final uploader = doc.uploadedBy.firstName.isNotEmpty final uploadTime = doc.uploadedAt != null
? "${doc.uploadedBy.firstName} ${doc.uploadedBy.lastName}".trim() ? 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"; : "You";
final iconColor = _getDocumentTypeColor(doc.documentType.name); final iconColor =
_getDocumentTypeColor(doc.documentType?.name ?? 'unknown');
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -479,11 +486,10 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Icon( child: Icon(
_getDocumentIcon(doc.documentType.name), _getDocumentIcon(doc.documentType?.name ?? 'unknown'),
color: iconColor, color: iconColor,
size: 24, size: 24,
), )),
),
const SizedBox(width: 14), const SizedBox(width: 14),
Expanded( Expanded(
child: Column( child: Column(
@ -497,7 +503,7 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: MyText.labelSmall( child: MyText.labelSmall(
doc.documentType.name, doc.documentType?.name ?? 'Unknown',
fontWeight: 600, fontWeight: 600,
color: iconColor, color: iconColor,
letterSpacing: 0.3, letterSpacing: 0.3,
@ -872,12 +878,18 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
} }
final doc = docs[index]; final doc = docs[index];
final currentDate = DateFormat("dd MMM yyyy") final currentDate = doc.uploadedAt != null
.format(doc.uploadedAt.toLocal());
final prevDate = index > 0
? DateFormat("dd MMM yyyy") ? 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; : null;
final showDateHeader = currentDate != prevDate; final showDateHeader = currentDate != prevDate;
return _buildDocumentCard(doc, showDateHeader); 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/employees/employee_detail_screen.dart';
import 'package:on_field_work/view/document/user_document_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/widgets/custom_app_bar.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
class EmployeeProfilePage extends StatefulWidget { class EmployeeProfilePage extends StatefulWidget {
final String employeeId; final String employeeId;
@ -14,7 +15,7 @@ class EmployeeProfilePage extends StatefulWidget {
} }
class _EmployeeProfilePageState extends State<EmployeeProfilePage> class _EmployeeProfilePageState extends State<EmployeeProfilePage>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin, UIMixin {
late TabController _tabController; late TabController _tabController;
@override @override
@ -31,41 +32,58 @@ class _EmployeeProfilePageState extends State<EmployeeProfilePage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF1F1F1), backgroundColor: const Color(0xFFF1F1F1),
appBar: CustomAppBar( appBar: CustomAppBar(
title: "Employee Profile", title: "Employee Profile",
onBackPressed: () => Get.back(), onBackPressed: () => Get.back(),
backgroundColor: appBarColor,
), ),
body: Column( body: Stack(
children: [ children: [
// ---------------- TabBar outside AppBar ---------------- // === Gradient at the top behind AppBar + TabBar ===
Container( 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( child: TabBar(
controller: _tabController, controller: _tabController,
labelColor: Colors.black, labelColor: Colors.white,
unselectedLabelColor: Colors.grey, unselectedLabelColor: Colors.white70,
indicatorColor: Colors.red, indicatorColor: Colors.white,
indicatorWeight: 3,
tabs: const [ tabs: const [
Tab(text: "Details"), Tab(text: "Details"),
Tab(text: "Documents"), Tab(text: "Documents"),
], ],
), ),
), ),
// ---------------- TabBarView ----------------
Expanded( Expanded(
child: TabBarView( child: TabBarView(
controller: _tabController, controller: _tabController,
children: [ children: [
// Details Tab
EmployeeDetailPage( EmployeeDetailPage(
employeeId: widget.employeeId, employeeId: widget.employeeId,
fromProfile: true, fromProfile: true,
), ),
// Documents Tab
UserDocumentsPage( UserDocumentsPage(
entityId: widget.employeeId, entityId: widget.employeeId,
isEmployee: true, 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/helpers/widgets/my_refresh_indicator.dart';
import 'package:on_field_work/view/employees/employee_profile_screen.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/view/employees/manage_reporting_bottom_sheet.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
class EmployeesScreen extends StatefulWidget { class EmployeesScreen extends StatefulWidget {
const EmployeesScreen({super.key}); const EmployeesScreen({super.key});
@ -113,11 +114,36 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: const Color(0xFFF5F5F5),
appBar: _buildAppBar(), appBar: CustomAppBar(
floatingActionButton: _buildFloatingActionButton(), title: "Employees",
body: SafeArea( 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>( child: GetBuilder<EmployeesScreenController>(
init: _employeeController, init: _employeeController,
tag: 'employee_screen_controller', 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:on_field_work/helpers/services/app_logger.dart';
import 'package:url_launcher/url_launcher.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/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/expense/expense_detail_helpers.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.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/my_text.dart';
@ -83,12 +83,37 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF7F7F7), backgroundColor: const Color(0xFFF7F7F7),
appBar: _AppBar(projectController: projectController), appBar: CustomAppBar(
body: SafeArea( 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(() { child: Obx(() {
if (controller.isLoading.value) return buildLoadingSkeleton(); if (controller.isLoading.value) return buildLoadingSkeleton();
final expense = controller.expense.value; final expense = controller.expense.value;
if (controller.errorMessage.isNotEmpty || expense == null) { if (controller.errorMessage.isNotEmpty || expense == null) {
return Center(child: MyText.bodyMedium("No data to display.")); return Center(child: MyText.bodyMedium("No data to display."));
@ -98,8 +123,10 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
_checkPermissionToSubmit(expense); _checkPermissionToSubmit(expense);
}); });
final statusColor = getExpenseStatusColor(expense.status.name, final statusColor = getExpenseStatusColor(
colorCode: expense.status.color); expense.status.name,
colorCode: expense.status.color,
);
final formattedAmount = formatExpenseAmount(expense.amount); final formattedAmount = formatExpenseAmount(expense.amount);
return MyRefreshIndicator( return MyRefreshIndicator(
@ -108,7 +135,8 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
12, 12, 12, 30 + MediaQuery.of(context).padding.bottom), 12, 12, 12, 30 + MediaQuery.of(context).padding.bottom
),
child: Center( child: Center(
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 520), constraints: const BoxConstraints(maxWidth: 520),
@ -122,21 +150,21 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// ---------------- Header & Status ---------------- // Header & Status
_InvoiceHeader(expense: expense), _InvoiceHeader(expense: expense),
const Divider(height: 30, thickness: 1.2), const Divider(height: 30, thickness: 1.2),
// ---------------- Activity Logs ---------------- // Activity Logs
InvoiceLogs(logs: expense.expenseLogs), InvoiceLogs(logs: expense.expenseLogs),
const Divider(height: 30, thickness: 1.2), const Divider(height: 30, thickness: 1.2),
// ---------------- Amount & Summary ----------------
// Amount & Summary
Row( Row(
children: [ children: [
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MyText.bodyMedium('Amount', MyText.bodyMedium('Amount', fontWeight: 600),
fontWeight: 600),
const SizedBox(height: 4), const SizedBox(height: 4),
MyText.bodyLarge( MyText.bodyLarge(
formattedAmount, formattedAmount,
@ -146,7 +174,6 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
], ],
), ),
const Spacer(), const Spacer(),
// Optional: Pre-approved badge
if (expense.preApproved) if (expense.preApproved)
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -165,19 +192,19 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
), ),
const Divider(height: 30, thickness: 1.2), const Divider(height: 30, thickness: 1.2),
// ---------------- Parties ---------------- // Parties
_InvoicePartiesTable(expense: expense), _InvoicePartiesTable(expense: expense),
const Divider(height: 30, thickness: 1.2), const Divider(height: 30, thickness: 1.2),
// ---------------- Expense Details ---------------- // Expense Details
_InvoiceDetailsTable(expense: expense), _InvoiceDetailsTable(expense: expense),
const Divider(height: 30, thickness: 1.2), const Divider(height: 30, thickness: 1.2),
// ---------------- Documents ---------------- // Documents
_InvoiceDocuments(documents: expense.documents), _InvoiceDocuments(documents: expense.documents),
const Divider(height: 30, thickness: 1.2), const Divider(height: 30, thickness: 1.2),
// ---------------- Totals ---------------- // Totals
_InvoiceTotals( _InvoiceTotals(
expense: expense, expense: expense,
formattedAmount: formattedAmount, formattedAmount: formattedAmount,
@ -189,15 +216,18 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
), ),
), ),
), ),
)); ),
);
}), }),
), ),
],
),
floatingActionButton: Obx(() { floatingActionButton: Obx(() {
if (controller.isLoading.value) return buildLoadingSkeleton(); if (controller.isLoading.value) return buildLoadingSkeleton();
final expense = controller.expense.value; final expense = controller.expense.value;
if (controller.errorMessage.isNotEmpty || expense == null) { if (controller.errorMessage.isNotEmpty || expense == null) {
return Center(child: MyText.bodyMedium("No data to display.")); return const SizedBox.shrink();
} }
if (!_checkedPermission) { if (!_checkedPermission) {
@ -237,10 +267,8 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
}) })
.toList(), .toList(),
}; };
logSafe('editData: $editData', level: LogLevel.info);
final addCtrl = Get.put(AddExpenseController()); final addCtrl = Get.put(AddExpenseController());
await addCtrl.loadMasterData(); await addCtrl.loadMasterData();
addCtrl.populateFieldsForEdit(editData); addCtrl.populateFieldsForEdit(editData);
@ -279,22 +307,7 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
final isCreatedByCurrentUser = final isCreatedByCurrentUser =
employeeInfo?.id == expense.createdBy.id; employeeInfo?.id == expense.createdBy.id;
logSafe( if (isSubmitStatus) return isCreatedByCurrentUser;
'🔐 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
return permissionController.hasAnyPermission(parsedPermissions); return permissionController.hasAnyPermission(parsedPermissions);
}).map((next) { }).map((next) {
return _statusButton(context, controller, expense, next); return _statusButton(context, controller, expense, next);
@ -306,6 +319,7 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
); );
} }
Widget _statusButton(BuildContext context, ExpenseDetailController controller, Widget _statusButton(BuildContext context, ExpenseDetailController controller,
ExpenseDetailModel expense, dynamic next) { ExpenseDetailModel expense, dynamic next) {
Color primary = Colors.red; 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 { class _InvoiceHeader extends StatelessWidget {
final ExpenseDetailModel expense; final ExpenseDetailModel expense;
const _InvoiceHeader({required this.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/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';
class ExpenseMainScreen extends StatefulWidget { class ExpenseMainScreen extends StatefulWidget {
const ExpenseMainScreen({super.key}); const ExpenseMainScreen({super.key});
@ -87,19 +88,57 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
appBar: ExpenseAppBar(projectController: projectController), appBar: CustomAppBar(
body: Column( title: "Expense & Reimbursement",
backgroundColor: appBarColor,
onBackPressed: () => Get.toNamed('/dashboard/finance'),
),
body: Stack(
children: [
// === FULL GRADIENT BEHIND APPBAR & TABBAR ===
Positioned.fill(
child: Column(
children: [ children: [
// ---------------- TabBar ----------------
Container( 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( child: TabBar(
controller: _tabController, controller: _tabController,
labelColor: Colors.black, labelColor: Colors.white,
unselectedLabelColor: Colors.grey, unselectedLabelColor: Colors.white70,
indicatorColor: Colors.red, indicatorColor: Colors.white,
indicatorWeight: 3,
tabs: const [ tabs: const [
Tab(text: "Current Month"), Tab(text: "Current Month"),
Tab(text: "History"), Tab(text: "History"),
@ -107,16 +146,15 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen>
), ),
), ),
// ---------------- Gray background for rest ---------------- // CONTENT AREA
Expanded( Expanded(
child: Container( child: Container(
color: Colors.grey[100], color: Colors.transparent,
child: Column( child: Column(
children: [ children: [
// ---------------- Search ---------------- // SEARCH & FILTER
Padding( Padding(
padding: padding: const EdgeInsets.symmetric(horizontal: 0),
const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
child: SearchAndFilter( child: SearchAndFilter(
controller: searchController, controller: searchController,
onChanged: (_) => setState(() {}), onChanged: (_) => setState(() {}),
@ -125,7 +163,7 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen>
), ),
), ),
// ---------------- TabBarView ---------------- // TABBAR VIEW
Expanded( Expanded(
child: TabBarView( child: TabBarView(
controller: _tabController, controller: _tabController,
@ -141,11 +179,12 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen>
), ),
], ],
), ),
),
],
),
floatingActionButton: Obx(() { floatingActionButton: Obx(() {
// Show loader or hide FAB while permissions are loading if (permissionController.permissions.isEmpty)
if (permissionController.permissions.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
}
final canUpload = final canUpload =
permissionController.hasPermission(Permissions.expenseUpload); 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/finance/advance_payment_controller.dart';
import 'package:on_field_work/controller/project_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/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:flutter/services.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
class AdvancePaymentScreen extends StatefulWidget { class AdvancePaymentScreen extends StatefulWidget {
const AdvancePaymentScreen({super.key}); const AdvancePaymentScreen({super.key});
@ -49,12 +48,35 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F5F5), 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 // ===== MAIN CONTENT =====
body: SafeArea( SafeArea(
top: false,
bottom: true, bottom: true,
child: GestureDetector( child: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(), onTap: () => FocusScope.of(context).unfocus(),
@ -66,131 +88,66 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
} }
}, },
color: Colors.white, color: Colors.white,
backgroundColor: contentTheme.primary, backgroundColor: appBarColor,
strokeWidth: 2.5, strokeWidth: 2.5,
displacement: 60, displacement: 60,
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
child: Padding( child: Padding(
// Extra bottom padding so content does NOT go under 3-button navbar
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 20, bottom: MediaQuery.of(context).padding.bottom + 20,
), ),
child: Column( child: Column(
children: [ children: [
_buildSearchBar(), // ===== SEARCH BAR FLOATING OVER GRADIENT =====
_buildEmployeeDropdown(context), Padding(
_buildTopBalance(), padding: const EdgeInsets.symmetric(
_buildPaymentList(), horizontal: 12, vertical: 8),
],
),
),
),
),
),
),
);
}
// ---------------- 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(
child: SizedBox( child: SizedBox(
height: 38, height: 38,
child: TextField( child: TextField(
controller: _searchCtrl, controller: _searchCtrl,
focusNode: _searchFocus, focusNode: _searchFocus,
onChanged: (v) => controller.searchQuery.value = v.trim(), onChanged: (v) =>
controller.searchQuery.value = v.trim(),
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: contentPadding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 12, vertical: 0), horizontal: 12, vertical: 0),
prefixIcon: prefixIcon: const Icon(Icons.search,
const Icon(Icons.search, size: 20, color: Colors.grey), size: 20, color: Colors.grey),
hintText: 'Search Employee...', hintText: 'Search Employee...',
filled: true, filled: true,
fillColor: Colors.white, fillColor: Colors.white,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: borderSide: BorderSide(
BorderSide(color: Colors.grey.shade300, width: 1), color: Colors.grey.shade300, width: 1),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: borderSide: BorderSide(
BorderSide(color: Colors.grey.shade300, width: 1), color: Colors.grey.shade300, width: 1),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: borderSide: BorderSide(
BorderSide(color: contentTheme.primary, width: 1.5), 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/utils/mixins/ui_mixin.dart';
import 'package:on_field_work/helpers/widgets/my_card.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_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_breakdown_chart.dart';
import 'package:on_field_work/helpers/widgets/dashbaord/expense_by_status_widget.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/monthly_expense_dashboard_chart.dart';
import 'package:on_field_work/controller/dashboard/dashboard_controller.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/helpers/utils/permission_constants.dart';
import 'package:on_field_work/model/dynamicMenu/dynamic_menu_model.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 { class FinanceScreen extends StatefulWidget {
const FinanceScreen({super.key}); const FinanceScreen({super.key});
@ -52,70 +53,54 @@ class _FinanceScreenState extends State<FinanceScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FA), backgroundColor: const Color(0xFFF8F9FA),
appBar: PreferredSize( appBar: CustomAppBar(
preferredSize: const Size.fromHeight(72), title: "Finance",
child: AppBar( onBackPressed: () => Get.offAllNamed( '/dashboard' ),
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: appBarColor,
elevation: 0.5, ),
automaticallyImplyLeading: false, body: Stack(
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
IconButton( // Top fade under AppBar
icon: const Icon(Icons.arrow_back_ios_new, Container(
color: Colors.black, size: 20), height: 40,
onPressed: () => Get.offNamed('/dashboard'), decoration: BoxDecoration(
), gradient: LinearGradient(
MySpacing.width(8), begin: Alignment.topCenter,
Expanded( end: Alignment.bottomCenter,
child: Column( colors: [
crossAxisAlignment: CrossAxisAlignment.start, appBarColor,
mainAxisSize: MainAxisSize.min, appBarColor.withOpacity(0.0),
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],
),
),
],
);
},
),
], ],
), ),
), ),
),
// 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 // Main scrollable content
bottom: true, // avoid system bottom buttons SafeArea(
top: false,
bottom: true,
child: FadeTransition( child: FadeTransition(
opacity: _fadeAnimation, opacity: _fadeAnimation,
child: Obx(() { child: Obx(() {
@ -152,7 +137,6 @@ class _FinanceScreenState extends State<FinanceScreen>
); );
} }
// ---- IMPORTANT FIX: Add bottom safe padding ----
final double bottomInset = final double bottomInset =
MediaQuery.of(context).viewPadding.bottom; MediaQuery.of(context).viewPadding.bottom;
@ -161,8 +145,7 @@ class _FinanceScreenState extends State<FinanceScreen>
16, 16,
16, 16,
16, 16,
bottomInset + bottomInset + 24,
24, // ensures charts never go under system buttons
), ),
child: Column( child: Column(
children: [ 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/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/make_expense_bottom_sheet.dart';
import 'package:on_field_work/model/finance/add_payment_request_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 { class PaymentRequestDetailScreen extends StatefulWidget {
final String paymentRequestId; final String paymentRequestId;
@ -107,10 +108,33 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
appBar: _buildAppBar(), appBar: CustomAppBar(
body: SafeArea( 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(() { child: Obx(() {
if (controller.isLoading.value && if (controller.isLoading.value &&
controller.paymentRequest.value == null) { controller.paymentRequest.value == null) {
@ -178,6 +202,8 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
); );
}), }),
), ),
],
),
bottomNavigationBar: _buildBottomActionBar(), 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 { 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/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';
class PaymentRequestMainScreen extends StatefulWidget { class PaymentRequestMainScreen extends StatefulWidget {
const PaymentRequestMainScreen({super.key}); const PaymentRequestMainScreen({super.key});
@ -96,33 +97,67 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
appBar: _buildAppBar(), appBar: CustomAppBar(
title: "Payment Requests",
// ------------------------ onBackPressed: () => Get.offNamed('/dashboard/finance'),
// FIX: SafeArea prevents content from going under 3-button navbar backgroundColor: appBarColor,
// ------------------------ ),
body: SafeArea( body: Stack(
bottom: true, children: [
// === FULL GRADIENT BEHIND APPBAR & TABBAR ===
Positioned.fill(
child: Column( child: Column(
children: [ children: [
Container( 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( child: TabBar(
controller: _tabController, controller: _tabController,
labelColor: Colors.black, labelColor: Colors.white,
unselectedLabelColor: Colors.grey, unselectedLabelColor: Colors.white70,
indicatorColor: Colors.red, indicatorColor: Colors.white,
tabs: const [ tabs: const [
Tab(text: "Current Month"), Tab(text: "Current Month"),
Tab(text: "History"), Tab(text: "History"),
], ],
), ),
), ),
// CONTENT AREA
Expanded( Expanded(
child: Container( child: Container(
color: Colors.grey[100], color: Colors.transparent,
child: Column( child: Column(
children: [ children: [
_buildSearchBar(), _buildSearchBar(),
@ -142,7 +177,8 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
], ],
), ),
), ),
],
),
floatingActionButton: Obx(() { floatingActionButton: Obx(() {
if (permissionController.permissions.isEmpty) { if (permissionController.permissions.isEmpty) {
return const SizedBox.shrink(); 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() { Widget _buildSearchBar() {
return Padding( return Padding(
padding: MySpacing.fromLTRB(12, 10, 12, 0), padding: MySpacing.fromLTRB(12, 10, 12, 0),

View File

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

View File

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

View File

@ -760,17 +760,20 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final projectName = widget.projectName; final projectName = widget.projectName;
final Color appBarColor = contentTheme.primary;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: const Color(0xFFF5F5F5),
appBar: CustomAppBar( appBar: CustomAppBar(
title: "Job Details Screen", title: "Job Details Screen",
onBackPressed: () => Get.back(), onBackPressed: () => Get.back(),
projectName: projectName, projectName: projectName,
backgroundColor: appBarColor,
), ),
floatingActionButton: Obx(() => FloatingActionButton.extended( floatingActionButton: Obx(() => FloatingActionButton.extended(
onPressed: onPressed:
isEditing.value ? _editJob : () => isEditing.value = true, isEditing.value ? _editJob : () => isEditing.value = true,
backgroundColor: contentTheme.primary, backgroundColor: appBarColor,
label: MyText.bodyMedium( label: MyText.bodyMedium(
isEditing.value ? "Save" : "Edit", isEditing.value ? "Save" : "Edit",
color: Colors.white, color: Colors.white,
@ -778,14 +781,50 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
), ),
icon: Icon(isEditing.value ? Icons.save : Icons.edit), 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) { if (controller.isJobDetailLoading.value) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
if (controller.jobDetailErrorMessage.value.isNotEmpty) { if (controller.jobDetailErrorMessage.value.isNotEmpty) {
return Center( return Center(
child: MyText.bodyMedium(controller.jobDetailErrorMessage.value)); child: MyText.bodyMedium(
controller.jobDetailErrorMessage.value));
} }
final job = controller.jobDetail.value?.data; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: const Color(0xFFF5F5F5),
appBar: CustomAppBar( appBar: CustomAppBar(
title: "Service Projects", title: "Service Projects",
projectName: 'All Service Projects', projectName: 'All Service Projects',
backgroundColor: appBarColor,
onBackPressed: () => Get.toNamed('/dashboard'), 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 // Main content
body: SafeArea( SafeArea(
bottom: true, bottom: true,
child: Column( child: Column(
children: [ children: [
@ -209,12 +225,12 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
const EdgeInsets.symmetric(horizontal: 12), const EdgeInsets.symmetric(horizontal: 12),
prefixIcon: const Icon(Icons.search, prefixIcon: const Icon(Icons.search,
size: 20, color: Colors.grey), size: 20, color: Colors.grey),
suffixIcon: ValueListenableBuilder<TextEditingValue>( suffixIcon:
ValueListenableBuilder<TextEditingValue>(
valueListenable: searchController, valueListenable: searchController,
builder: (context, value, _) { builder: (context, value, _) {
if (value.text.isEmpty) { if (value.text.isEmpty)
return const SizedBox.shrink(); return const SizedBox.shrink();
}
return IconButton( return IconButton(
icon: const Icon(Icons.clear, icon: const Icon(Icons.clear,
size: 20, color: Colors.grey), size: 20, color: Colors.grey),
@ -230,11 +246,13 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
fillColor: Colors.white, fillColor: Colors.white,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300), borderSide:
BorderSide(color: Colors.grey.shade300),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5), 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) { if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
final projects = controller.filteredProjects; final projects = controller.filteredProjects;
return MyRefreshIndicator( return MyRefreshIndicator(
@ -259,11 +276,8 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
? _buildEmptyState() ? _buildEmptyState()
: ListView.separated( : ListView.separated(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
// FIX 2: Increased bottom padding for landscape
padding: MySpacing.only( padding: MySpacing.only(
left: 8, right: 8, top: 4, bottom: 120), left: 8, right: 8, top: 4, bottom: 120),
itemCount: projects.length, itemCount: projects.length,
separatorBuilder: (_, __) => MySpacing.height(12), separatorBuilder: (_, __) => MySpacing.height(12),
itemBuilder: (_, index) => 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/widgets/my_custom_skeleton.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';
class DailyProgressReportScreen extends StatefulWidget { class DailyProgressReportScreen extends StatefulWidget {
const DailyProgressReportScreen({super.key}); const DailyProgressReportScreen({super.key});
@ -87,70 +89,40 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold( return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5, appBar: CustomAppBar(
automaticallyImplyLeading: false, title: 'Daily Progress Report',
titleSpacing: 0, backgroundColor: appBarColor,
title: Padding( projectName:
padding: MySpacing.xy(16, 0), projectController.selectedProject?.name ?? 'Select Project',
child: Row( onBackPressed: () => Get.offNamed('/dashboard'),
crossAxisAlignment: CrossAxisAlignment.center, ),
body: Stack(
children: [ children: [
IconButton( // Gradient behind content (like EmployeesScreen)
icon: const Icon(Icons.arrow_back_ios_new, Container(
color: Colors.black, size: 20), height: 80,
onPressed: () => Get.offNamed('/dashboard'), decoration: BoxDecoration(
), gradient: LinearGradient(
MySpacing.width(8), begin: Alignment.topCenter,
Expanded( end: Alignment.bottomCenter,
child: Column( colors: [
crossAxisAlignment: CrossAxisAlignment.start, appBarColor,
mainAxisSize: MainAxisSize.min, appBarColor.withOpacity(0.0),
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],
),
),
],
);
},
),
],
),
),
], ],
), ),
), ),
), ),
),
body: SafeArea( // Main content
SafeArea(
child: MyRefreshIndicator( child: MyRefreshIndicator(
onRefresh: _refreshData, onRefresh: _refreshData,
child: CustomScrollView( child: CustomScrollView(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
@ -165,8 +137,7 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
Padding( Padding(
padding: MySpacing.x(15), padding: MySpacing.x(15),
child: Row( child: Row(
mainAxisAlignment: mainAxisAlignment: MainAxisAlignment.end,
MainAxisAlignment.end,
children: [ children: [
InkWell( InkWell(
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(22),
@ -182,9 +153,8 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
color: Colors.black, color: Colors.black,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Icon(Icons.tune, const Icon(Icons.tune,
size: 20, color: Colors.black), 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/helpers/widgets/my_refresh_indicator.dart';
import 'package:on_field_work/controller/tenant/service_controller.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/tenant/service_selector.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
class DailyTaskPlanningScreen extends StatefulWidget { class DailyTaskPlanningScreen extends StatefulWidget {
DailyTaskPlanningScreen({super.key}); DailyTaskPlanningScreen({super.key});
@ -58,72 +60,41 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
return Scaffold( return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5, appBar: CustomAppBar(
automaticallyImplyLeading: false, title: 'Daily Task Planning',
titleSpacing: 0, backgroundColor: appBarColor,
title: Padding( projectName:
padding: MySpacing.xy(16, 0), projectController.selectedProject?.name ?? 'Select Project',
child: Row( onBackPressed: () => Get.offNamed('/dashboard'),
crossAxisAlignment: CrossAxisAlignment.center, ),
body: Stack(
children: [ children: [
IconButton( // Gradient behind content
icon: const Icon(Icons.arrow_back_ios_new, Container(
color: Colors.black, size: 20), height: 80,
onPressed: () => Get.offNamed('/dashboard'), decoration: BoxDecoration(
), gradient: LinearGradient(
MySpacing.width(8), begin: Alignment.topCenter,
Expanded( end: Alignment.bottomCenter,
child: Column( colors: [
crossAxisAlignment: CrossAxisAlignment.start, appBarColor,
mainAxisSize: MainAxisSize.min, appBarColor.withOpacity(0.0),
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],
),
),
],
);
}),
],
),
),
], ],
), ),
), ),
), ),
),
body: SafeArea( // Main content
SafeArea(
child: MyRefreshIndicator( child: MyRefreshIndicator(
onRefresh: () async { onRefresh: () async {
final projectId = projectController.selectedProjectId.value; final projectId = projectController.selectedProjectId.value;
if (projectId.isNotEmpty) { if (projectId.isNotEmpty) {
try { try {
// keep previous behavior but now fetchTaskData is lighter (buildings only)
await dailyTaskPlanningController.fetchTaskData( await dailyTaskPlanningController.fetchTaskData(
projectId, projectId,
serviceId: serviceController.selectedService?.id, serviceId: serviceController.selectedService?.id,
@ -159,10 +130,10 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
final projectId = final projectId =
projectController.selectedProjectId.value; projectController.selectedProjectId.value;
if (projectId.isNotEmpty) { if (projectId.isNotEmpty) {
await dailyTaskPlanningController.fetchTaskData( await dailyTaskPlanningController
.fetchTaskData(
projectId, projectId,
serviceId: serviceId: service?.id,
service?.id, // <-- pass selected service
); );
} }
}, },
@ -181,6 +152,8 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
), ),
), ),
), ),
],
),
); );
} }
@ -227,8 +200,7 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
final buildings = dailyTasks final buildings = dailyTasks
.expand((task) => task.buildings) .expand((task) => task.buildings)
.where((building) => .where((building) =>
(building.plannedWork ) > 0 || (building.plannedWork) > 0 || (building.completedWork) > 0)
(building.completedWork ) > 0)
.toList(); .toList();
if (buildings.isEmpty) { if (buildings.isEmpty) {