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:
parent
84156167ea
commit
259f2aa928
@ -142,8 +142,8 @@ class DocumentController extends GetxController {
|
||||
);
|
||||
|
||||
if (response != null && response.success) {
|
||||
if (response.data.data.isNotEmpty) {
|
||||
documents.addAll(response.data.data);
|
||||
if (response.data?.data.isNotEmpty ?? false) {
|
||||
documents.addAll(response.data!.data);
|
||||
pageNumber.value++;
|
||||
} else {
|
||||
hasMore.value = false;
|
||||
|
||||
@ -3,95 +3,108 @@ import 'package:get/get.dart';
|
||||
import 'package:on_field_work/controller/project_controller.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||
|
||||
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
class CustomAppBar extends StatelessWidget
|
||||
with UIMixin
|
||||
implements PreferredSizeWidget {
|
||||
final String title;
|
||||
final String? projectName;
|
||||
final VoidCallback? onBackPressed;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const CustomAppBar({
|
||||
CustomAppBar({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.projectName,
|
||||
this.onBackPressed,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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: onBackPressed ?? () => Get.back(),
|
||||
),
|
||||
MySpacing.width(5),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// TITLE
|
||||
MyText.titleLarge(
|
||||
title,
|
||||
fontWeight: 700,
|
||||
color: Colors.black,
|
||||
),
|
||||
|
||||
MySpacing.height(2),
|
||||
|
||||
// PROJECT NAME ROW
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
// NEW LOGIC — simple and safe
|
||||
final displayProjectName =
|
||||
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(
|
||||
displayProjectName,
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Size get preferredSize => const Size.fromHeight(72);
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(72);
|
||||
Widget build(BuildContext context) {
|
||||
final Color effectiveBackgroundColor =
|
||||
backgroundColor ?? contentTheme.primary;
|
||||
const Color onPrimaryColor = Colors.white;
|
||||
const double horizontalPadding = 16.0;
|
||||
|
||||
return AppBar(
|
||||
backgroundColor: effectiveBackgroundColor,
|
||||
elevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 0,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(bottom: Radius.circular(0)),
|
||||
),
|
||||
leading: Padding(
|
||||
padding: MySpacing.only(left: horizontalPadding),
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios_new,
|
||||
color: onPrimaryColor,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: onBackPressed ?? () => Get.back(),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
),
|
||||
title: Padding(
|
||||
padding: MySpacing.only(right: horizontalPadding, left: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MyText.titleLarge(
|
||||
title,
|
||||
fontWeight: 800,
|
||||
color: onPrimaryColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
MySpacing.height(3),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
final displayProjectName = projectName ??
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.folder_open,
|
||||
size: 14, color: onPrimaryColor),
|
||||
MySpacing.width(4),
|
||||
Flexible(
|
||||
child: MyText.bodySmall(
|
||||
displayProjectName,
|
||||
fontWeight: 500,
|
||||
color: onPrimaryColor.withOpacity(0.8),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
MySpacing.width(2),
|
||||
const Icon(Icons.keyboard_arrow_down,
|
||||
size: 18, color: onPrimaryColor),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: MySpacing.only(right: horizontalPadding),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.home, color: onPrimaryColor, size: 24),
|
||||
onPressed: () => Get.offAllNamed('/dashboard'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,7 +148,7 @@ class DocumentType {
|
||||
final String name;
|
||||
final String? regexExpression;
|
||||
final String allowedContentType;
|
||||
final int maxSizeAllowedInMB;
|
||||
final double maxSizeAllowedInMB;
|
||||
final bool isValidationRequired;
|
||||
final bool isMandatory;
|
||||
final bool isSystem;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
class DocumentsResponse {
|
||||
final bool success;
|
||||
final String message;
|
||||
final DocumentDataWrapper data;
|
||||
final DocumentDataWrapper? data;
|
||||
final dynamic errors;
|
||||
final int statusCode;
|
||||
final DateTime timestamp;
|
||||
@ -9,7 +9,7 @@ class DocumentsResponse {
|
||||
DocumentsResponse({
|
||||
required this.success,
|
||||
required this.message,
|
||||
required this.data,
|
||||
this.data,
|
||||
this.errors,
|
||||
required this.statusCode,
|
||||
required this.timestamp,
|
||||
@ -19,11 +19,13 @@ class DocumentsResponse {
|
||||
return DocumentsResponse(
|
||||
success: json['success'] ?? false,
|
||||
message: json['message'] ?? '',
|
||||
data: DocumentDataWrapper.fromJson(json['data']),
|
||||
data: json['data'] != null
|
||||
? DocumentDataWrapper.fromJson(json['data'])
|
||||
: null,
|
||||
errors: json['errors'],
|
||||
statusCode: json['statusCode'] ?? 0,
|
||||
timestamp: json['timestamp'] != null
|
||||
? DateTime.parse(json['timestamp'])
|
||||
? DateTime.tryParse(json['timestamp']) ?? DateTime.now()
|
||||
: DateTime.now(),
|
||||
);
|
||||
}
|
||||
@ -32,7 +34,7 @@ class DocumentsResponse {
|
||||
return {
|
||||
'success': success,
|
||||
'message': message,
|
||||
'data': data.toJson(),
|
||||
'data': data?.toJson(),
|
||||
'errors': errors,
|
||||
'statusCode': statusCode,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
@ -61,9 +63,10 @@ class DocumentDataWrapper {
|
||||
currentPage: json['currentPage'] ?? 0,
|
||||
totalPages: json['totalPages'] ?? 0,
|
||||
totalEntites: json['totalEntites'] ?? 0,
|
||||
data: (json['data'] as List<dynamic>? ?? [])
|
||||
.map((e) => DocumentItem.fromJson(e))
|
||||
.toList(),
|
||||
data: (json['data'] as List<dynamic>?)
|
||||
?.map((e) => DocumentItem.fromJson(e))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
@ -83,28 +86,28 @@ class DocumentItem {
|
||||
final String name;
|
||||
final String documentId;
|
||||
final String description;
|
||||
final DateTime uploadedAt;
|
||||
final DateTime? uploadedAt;
|
||||
final String? parentAttachmentId;
|
||||
final bool isCurrentVersion;
|
||||
final int version;
|
||||
final bool isActive;
|
||||
final bool? isVerified;
|
||||
final UploadedBy uploadedBy;
|
||||
final DocumentType documentType;
|
||||
final UploadedBy? uploadedBy;
|
||||
final DocumentType? documentType;
|
||||
|
||||
DocumentItem({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.documentId,
|
||||
required this.description,
|
||||
required this.uploadedAt,
|
||||
this.uploadedAt,
|
||||
this.parentAttachmentId,
|
||||
required this.isCurrentVersion,
|
||||
required this.version,
|
||||
required this.isActive,
|
||||
this.isVerified,
|
||||
required this.uploadedBy,
|
||||
required this.documentType,
|
||||
this.uploadedBy,
|
||||
this.documentType,
|
||||
});
|
||||
|
||||
factory DocumentItem.fromJson(Map<String, dynamic> json) {
|
||||
@ -113,14 +116,20 @@ class DocumentItem {
|
||||
name: json['name'] ?? '',
|
||||
documentId: json['documentId'] ?? '',
|
||||
description: json['description'] ?? '',
|
||||
uploadedAt: DateTime.parse(json['uploadedAt']),
|
||||
uploadedAt: json['uploadedAt'] != null
|
||||
? DateTime.tryParse(json['uploadedAt'])
|
||||
: null,
|
||||
parentAttachmentId: json['parentAttachmentId'],
|
||||
isCurrentVersion: json['isCurrentVersion'] ?? false,
|
||||
version: json['version'] ?? 0,
|
||||
isActive: json['isActive'] ?? false,
|
||||
isVerified: json['isVerified'],
|
||||
uploadedBy: UploadedBy.fromJson(json['uploadedBy']),
|
||||
documentType: DocumentType.fromJson(json['documentType']),
|
||||
uploadedBy: json['uploadedBy'] != null
|
||||
? UploadedBy.fromJson(json['uploadedBy'])
|
||||
: null,
|
||||
documentType: json['documentType'] != null
|
||||
? DocumentType.fromJson(json['documentType'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@ -130,14 +139,14 @@ class DocumentItem {
|
||||
'name': name,
|
||||
'documentId': documentId,
|
||||
'description': description,
|
||||
'uploadedAt': uploadedAt.toIso8601String(),
|
||||
'uploadedAt': uploadedAt?.toIso8601String(),
|
||||
'parentAttachmentId': parentAttachmentId,
|
||||
'isCurrentVersion': isCurrentVersion,
|
||||
'version': version,
|
||||
'isActive': isActive,
|
||||
'isVerified': isVerified,
|
||||
'uploadedBy': uploadedBy.toJson(),
|
||||
'documentType': documentType.toJson(),
|
||||
'uploadedBy': uploadedBy?.toJson(),
|
||||
'documentType': documentType?.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -208,7 +217,7 @@ class DocumentType {
|
||||
final String name;
|
||||
final String? regexExpression;
|
||||
final String? allowedContentType;
|
||||
final int? maxSizeAllowedInMB;
|
||||
final double? maxSizeAllowedInMB;
|
||||
final bool isValidationRequired;
|
||||
final bool isMandatory;
|
||||
final bool isSystem;
|
||||
@ -232,7 +241,7 @@ class DocumentType {
|
||||
return DocumentType(
|
||||
id: json['id'] ?? '',
|
||||
name: json['name'] ?? '',
|
||||
regexExpression: json['regexExpression'], // nullable
|
||||
regexExpression: json['regexExpression'],
|
||||
allowedContentType: json['allowedContentType'],
|
||||
maxSizeAllowedInMB: json['maxSizeAllowedInMB'],
|
||||
isValidationRequired: json['isValidationRequired'] ?? false,
|
||||
|
||||
@ -14,6 +14,7 @@ import 'package:on_field_work/view/Attendence/regularization_requests_tab.dart';
|
||||
import 'package:on_field_work/view/Attendence/attendance_logs_tab.dart';
|
||||
import 'package:on_field_work/view/Attendence/todays_attendance_tab.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
|
||||
class AttendanceScreen extends StatefulWidget {
|
||||
const AttendanceScreen({super.key});
|
||||
@ -75,61 +76,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildAppBar() {
|
||||
return AppBar(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
elevation: 0.5,
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 0,
|
||||
title: Padding(
|
||||
padding: MySpacing.xy(16, 0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () => Get.offNamed('/dashboard'),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleLarge('Attendance',
|
||||
fontWeight: 700, color: Colors.black),
|
||||
MySpacing.height(2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
final projectName =
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline,
|
||||
size: 14, color: Colors.grey),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
projectName,
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterSearchRow() {
|
||||
return Padding(
|
||||
padding: MySpacing.xy(8, 8),
|
||||
@ -358,24 +304,44 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(72),
|
||||
child: _buildAppBar(),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: GetBuilder<AttendanceController>(
|
||||
init: attendanceController,
|
||||
tag: 'attendance_dashboard_controller',
|
||||
builder: (controller) {
|
||||
final selectedProjectId = projectController.selectedProjectId.value;
|
||||
final noProjectSelected = selectedProjectId.isEmpty;
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return SingleChildScrollView(
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: "Attendance",
|
||||
backgroundColor: appBarColor,
|
||||
onBackPressed: () => Get.toNamed('/dashboard'),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
// Gradient container at top
|
||||
Container(
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
appBarColor,
|
||||
appBarColor.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Main content
|
||||
SafeArea(
|
||||
child: GetBuilder<AttendanceController>(
|
||||
init: attendanceController,
|
||||
tag: 'attendance_dashboard_controller',
|
||||
builder: (controller) {
|
||||
final selectedProjectId =
|
||||
projectController.selectedProjectId.value;
|
||||
final noProjectSelected = selectedProjectId.isEmpty;
|
||||
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: MySpacing.zero,
|
||||
child: Column(
|
||||
@ -394,12 +360,12 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,17 +6,12 @@ import 'package:on_field_work/controller/project_controller.dart';
|
||||
import 'package:on_field_work/controller/dynamicMenu/dynamic_menu_controller.dart';
|
||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_card.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
||||
import 'package:on_field_work/helpers/widgets/dashbaord/attendance_overview_chart.dart';
|
||||
import 'package:on_field_work/helpers/widgets/dashbaord/project_progress_chart.dart';
|
||||
import 'package:on_field_work/helpers/widgets/dashbaord/expense_breakdown_chart.dart';
|
||||
import 'package:on_field_work/helpers/widgets/dashbaord/expense_by_status_widget.dart';
|
||||
import 'package:on_field_work/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart';
|
||||
import 'package:on_field_work/helpers/widgets/dashbaord/dashboard_overview_widgets.dart';
|
||||
import 'package:on_field_work/view/layouts/layout.dart';
|
||||
import 'package:on_field_work/helpers/utils/permission_constants.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
|
||||
|
||||
class DashboardScreen extends StatefulWidget {
|
||||
const DashboardScreen({super.key});
|
||||
@ -44,64 +39,289 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Layout(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDashboardCards(),
|
||||
MySpacing.height(24),
|
||||
_buildProjectSelector(),
|
||||
MySpacing.height(24),
|
||||
_buildAttendanceChartSection(),
|
||||
MySpacing.height(12),
|
||||
MySpacing.height(24),
|
||||
_buildProjectProgressChartSection(),
|
||||
MySpacing.height(24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: DashboardOverviewWidgets.teamsOverview(),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: DashboardOverviewWidgets.tasksOverview(),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
ExpenseByStatusWidget(controller: dashboardController),
|
||||
MySpacing.height(24),
|
||||
ExpenseTypeReportChart(),
|
||||
MySpacing.height(24),
|
||||
MonthlyExpenseDashboardChart(),
|
||||
],
|
||||
//---------------------------------------------------------------------------
|
||||
// REUSABLE CARD (smaller, minimal)
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
Widget _cardWrapper({required Widget child}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
border: Border.all(color: Colors.black12.withOpacity(.04)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12.withOpacity(.05),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// SECTION TITLE
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
Widget _sectionTitle(String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// ---------------- Dynamic Dashboard Cards ----------------
|
||||
Widget _buildDashboardCards() {
|
||||
//---------------------------------------------------------------------------
|
||||
// CONDITIONAL QUICK ACTION CARD
|
||||
//---------------------------------------------------------------------------
|
||||
Widget _conditionalQuickActionCard() {
|
||||
// STATIC CONDITION
|
||||
String status = "O"; // <-- change if needed
|
||||
bool isCheckedIn = status == "O";
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
gradient: LinearGradient(
|
||||
colors: isCheckedIn
|
||||
? [Colors.red.shade200, Colors.red.shade400]
|
||||
: [Colors.green.shade200, Colors.green.shade400],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title & Status
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
isCheckedIn ? "Checked-In" : "Not Checked-In",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isCheckedIn ? LucideIcons.log_out : LucideIcons.log_in,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Description
|
||||
Text(
|
||||
isCheckedIn
|
||||
? "You are currently checked-in. Don't forget to check-out after your work."
|
||||
: "You are not checked-in yet. Please check-in to start your work.",
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Action Buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (!isCheckedIn)
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// Check-In action
|
||||
},
|
||||
label: const Text("Check-In"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
if (isCheckedIn)
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// Check-Out action
|
||||
},
|
||||
label: const Text("Check-Out"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// QUICK ACTIONS (updated to use the single card)
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
Widget _quickActions() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_sectionTitle("Quick Action"), // Change title to singular
|
||||
_conditionalQuickActionCard(), // Use the new conditional card
|
||||
],
|
||||
);
|
||||
}
|
||||
//---------------------------------------------------------------------------
|
||||
// PROJECT DROPDOWN (clean compact)
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
Widget _projectSelector() {
|
||||
return Obx(() {
|
||||
if (menuController.isLoading.value) {
|
||||
return SkeletonLoaders.dashboardCardsSkeleton();
|
||||
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());
|
||||
}
|
||||
|
||||
if (menuController.hasError.value || menuController.menuItems.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
"Failed to load menus. Please try again later.",
|
||||
style: TextStyle(color: Colors.red),
|
||||
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),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _projectDropdownList(projects, selectedId) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
border: Border.all(color: Colors.black12.withOpacity(.2)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12.withOpacity(.07),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
constraints:
|
||||
BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.33),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search project...",
|
||||
isDense: true,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border:
|
||||
OutlineInputBorder(borderRadius: BorderRadius.circular(5)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: projects.length,
|
||||
itemBuilder: (_, index) {
|
||||
final project = projects[index];
|
||||
return RadioListTile<String>(
|
||||
dense: true,
|
||||
value: project.id,
|
||||
groupValue: selectedId,
|
||||
onChanged: (v) {
|
||||
if (v != null) {
|
||||
projectController.updateSelectedProject(v);
|
||||
projectController.isProjectSelectionExpanded.value =
|
||||
false;
|
||||
}
|
||||
},
|
||||
title: Text(project.name),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
//---------------------------------------------------------------------------
|
||||
// DASHBOARD MODULE CARDS (UPDATED FOR MINIMAL PADDING / SLL SIZE)
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
Widget _dashboardCards() {
|
||||
return Obx(() {
|
||||
if (menuController.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final projectSelected = projectController.selectedProject != null;
|
||||
|
||||
// Define dashboard card meta with order
|
||||
final List<String> cardOrder = [
|
||||
final cardOrder = [
|
||||
MenuItems.attendance,
|
||||
MenuItems.employees,
|
||||
MenuItems.dailyTaskPlanning,
|
||||
@ -109,10 +329,10 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
MenuItems.directory,
|
||||
MenuItems.finance,
|
||||
MenuItems.documents,
|
||||
MenuItems.serviceProjects
|
||||
MenuItems.serviceProjects,
|
||||
];
|
||||
|
||||
final Map<String, _DashboardCardMeta> cardMeta = {
|
||||
final meta = {
|
||||
MenuItems.attendance:
|
||||
_DashboardCardMeta(LucideIcons.scan_face, contentTheme.success),
|
||||
MenuItems.employees:
|
||||
@ -131,373 +351,136 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
_DashboardCardMeta(LucideIcons.package, contentTheme.info),
|
||||
};
|
||||
|
||||
// Filter only available menus that exist in cardMeta
|
||||
final allowedMenusMap = {
|
||||
for (var menu in menuController.menuItems)
|
||||
if (menu.available && cardMeta.containsKey(menu.id)) menu.id: menu
|
||||
final allowed = {
|
||||
for (var m in menuController.menuItems)
|
||||
if (m.available && meta.containsKey(m.id)) m.id: m
|
||||
};
|
||||
|
||||
if (allowedMenusMap.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
"No accessible modules found.",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
final filtered = cardOrder.where(allowed.containsKey).toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_sectionTitle("Modules"),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
|
||||
// **More compact grid**
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
crossAxisSpacing: 6,
|
||||
mainAxisSpacing: 6,
|
||||
childAspectRatio: 1.2, // smaller & tighter
|
||||
),
|
||||
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (context, index) {
|
||||
final id = filtered[index];
|
||||
final item = allowed[id]!;
|
||||
final cardMeta = meta[id]!;
|
||||
|
||||
final isEnabled =
|
||||
item.name == "Attendance" ? true : projectSelected;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (!isEnabled) {
|
||||
Get.defaultDialog(
|
||||
title: "No Project Selected",
|
||||
middleText: "Please select a project first.",
|
||||
);
|
||||
} else {
|
||||
Get.toNamed(item.mobileLink);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
// **Reduced padding**
|
||||
padding: const EdgeInsets.all(4),
|
||||
|
||||
decoration: BoxDecoration(
|
||||
color: isEnabled ? Colors.white : Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.black12.withOpacity(.1),
|
||||
width: 0.7,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12.withOpacity(.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
cardMeta.icon,
|
||||
size: 20, // **smaller icon**
|
||||
color:
|
||||
isEnabled ? cardMeta.color : Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
item.name,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 9.5, // **reduced text size**
|
||||
fontWeight: FontWeight.w600,
|
||||
color:
|
||||
isEnabled ? Colors.black87 : Colors.grey.shade600,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
||||
//---------------------------------------------------------------------------
|
||||
// MAIN UI
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => projectController.isProjectSelectionExpanded.toggle(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
||||
color: Colors.white,
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 1,
|
||||
offset: Offset(0, 1))
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline,
|
||||
size: 18, color: Colors.blueAccent),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
selectedProject?.name ?? "Select Project",
|
||||
style: const TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w700),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
"Tap to switch project (${projects.length})",
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: Colors.black54),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isExpanded
|
||||
? Icons.arrow_drop_up_outlined
|
||||
: Icons.arrow_drop_down_outlined,
|
||||
color: Colors.black,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isExpanded)
|
||||
ValueListenableBuilder<String>(
|
||||
valueListenable: searchNotifier,
|
||||
builder: (context, query, _) {
|
||||
final lowerQuery = query.toLowerCase();
|
||||
final filteredProjects = lowerQuery.isEmpty
|
||||
? projects
|
||||
: projects
|
||||
.where((p) => p.name.toLowerCase().contains(lowerQuery))
|
||||
.toList();
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.12)),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2))
|
||||
],
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.35),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
prefixIcon: const Icon(Icons.search, size: 18),
|
||||
hintText: "Search project",
|
||||
hintStyle: const TextStyle(fontSize: 13),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 2),
|
||||
),
|
||||
onChanged: (value) => searchNotifier.value = value,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (filteredProjects.isEmpty)
|
||||
const Expanded(
|
||||
child: Center(
|
||||
child: Text("No projects found",
|
||||
style: TextStyle(
|
||||
fontSize: 13, color: Colors.black54)),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: filteredProjects.length,
|
||||
itemBuilder: (context, index) {
|
||||
final project = filteredProjects[index];
|
||||
final isSelected =
|
||||
project.id == selectedProjectId;
|
||||
return RadioListTile<String>(
|
||||
value: project.id,
|
||||
groupValue: selectedProjectId,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
projectController
|
||||
.updateSelectedProject(value);
|
||||
projectController.isProjectSelectionExpanded
|
||||
.value = false;
|
||||
}
|
||||
},
|
||||
title: Text(
|
||||
project.name,
|
||||
style: TextStyle(
|
||||
fontWeight: isSelected
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
color: isSelected
|
||||
? Colors.blueAccent
|
||||
: Colors.black87,
|
||||
),
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 0),
|
||||
activeColor: Colors.blueAccent,
|
||||
tileColor: isSelected
|
||||
? Colors.blueAccent.withOpacity(0.06)
|
||||
: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
visualDensity:
|
||||
const VisualDensity(vertical: -4),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// ---------------- 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 {
|
||||
|
||||
@ -13,6 +13,7 @@ import 'package:on_field_work/helpers/utils/date_time_utils.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_confirmation_dialog.dart';
|
||||
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
|
||||
class ContactDetailScreen extends StatefulWidget {
|
||||
final ContactModel contact;
|
||||
@ -23,18 +24,21 @@ class ContactDetailScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ContactDetailScreenState extends State<ContactDetailScreen>
|
||||
with UIMixin {
|
||||
with SingleTickerProviderStateMixin, UIMixin {
|
||||
late final DirectoryController directoryController;
|
||||
late final ProjectController projectController;
|
||||
late Rx<ContactModel> contactRx;
|
||||
late TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
directoryController = Get.find<DirectoryController>();
|
||||
projectController = Get.find<ProjectController>();
|
||||
projectController = Get.put(ProjectController());
|
||||
contactRx = widget.contact.obs;
|
||||
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await directoryController.fetchCommentsForContact(contactRx.value.id,
|
||||
active: true);
|
||||
@ -50,65 +54,72 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
child: Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: _buildMainAppBar(),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Obx(() => _buildSubHeader(contactRx.value)),
|
||||
const Divider(height: 1, thickness: 0.5, color: Colors.grey),
|
||||
Expanded(
|
||||
child: TabBarView(children: [
|
||||
Obx(() => _buildDetailsTab(contactRx.value)),
|
||||
_buildCommentsTab(),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildMainAppBar() {
|
||||
return AppBar(
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
elevation: 0.2,
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 0,
|
||||
title: Padding(
|
||||
padding: MySpacing.xy(16, 0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () =>
|
||||
Get.offAllNamed('/dashboard/directory-main-page'),
|
||||
body: Stack(
|
||||
children: [
|
||||
// GRADIENT BEHIND APPBAR & TABBAR
|
||||
Positioned.fill(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
appBarColor,
|
||||
appBarColor.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: Container(color: Colors.grey[100])),
|
||||
],
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MyText.titleLarge('Contact Profile',
|
||||
fontWeight: 700, color: Colors.black),
|
||||
MySpacing.height(2),
|
||||
GetBuilder<ProjectController>(builder: (p) {
|
||||
return ProjectLabel(p.selectedProject?.name);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// MAIN CONTENT
|
||||
SafeArea(
|
||||
top: true,
|
||||
bottom: true,
|
||||
child: Column(
|
||||
children: [
|
||||
// APPBAR
|
||||
CustomAppBar(
|
||||
title: 'Contact Profile',
|
||||
backgroundColor: Colors.transparent,
|
||||
onBackPressed: () =>
|
||||
Get.offAllNamed('/dashboard/directory-main-page'),
|
||||
),
|
||||
|
||||
// SUBHEADER + TABBAR
|
||||
Obx(() => _buildSubHeader(contactRx.value)),
|
||||
|
||||
// TABBAR VIEW
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
Obx(() => _buildDetailsTab(contactRx.value)),
|
||||
_buildCommentsTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -118,39 +129,44 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
|
||||
final lastName =
|
||||
contact.name.split(" ").length > 1 ? contact.name.split(" ").last : "";
|
||||
|
||||
return Padding(
|
||||
padding: MySpacing.xy(16, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(children: [
|
||||
Avatar(firstName: firstName, lastName: lastName, size: 35),
|
||||
MySpacing.width(12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleSmall(contact.name,
|
||||
fontWeight: 600, color: Colors.black),
|
||||
MySpacing.height(2),
|
||||
MyText.bodySmall(contact.organization,
|
||||
fontWeight: 500, color: Colors.grey[700]),
|
||||
return Container(
|
||||
color: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: MySpacing.xy(16, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(children: [
|
||||
Avatar(firstName: firstName, lastName: lastName, size: 35),
|
||||
MySpacing.width(12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleSmall(contact.name,
|
||||
fontWeight: 600, color: Colors.black),
|
||||
MySpacing.height(2),
|
||||
MyText.bodySmall(contact.organization,
|
||||
fontWeight: 500, color: Colors.grey[700]),
|
||||
],
|
||||
),
|
||||
]),
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.black,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
indicatorColor: contentTheme.primary,
|
||||
tabs: const [
|
||||
Tab(text: "Details"),
|
||||
Tab(text: "Notes"),
|
||||
],
|
||||
),
|
||||
]),
|
||||
TabBar(
|
||||
labelColor: Colors.black,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
indicatorColor: contentTheme.primary,
|
||||
tabs: const [
|
||||
Tab(text: "Details"),
|
||||
Tab(text: "Notes"),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- DETAILS TAB ---
|
||||
Widget _buildDetailsTab(ContactModel contact) {
|
||||
final tags = contact.tags.map((e) => e.name).join(", ");
|
||||
final bucketNames = contact.bucketIds
|
||||
@ -228,7 +244,8 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
|
||||
_iconInfoRow(Icons.location_on, "Address", contact.address),
|
||||
]),
|
||||
_infoCard("Organization", [
|
||||
_iconInfoRow(Icons.business, "Organization", contact.organization),
|
||||
_iconInfoRow(
|
||||
Icons.business, "Organization", contact.organization),
|
||||
_iconInfoRow(Icons.category, "Category", category),
|
||||
]),
|
||||
_infoCard("Meta Info", [
|
||||
@ -281,6 +298,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
|
||||
);
|
||||
}
|
||||
|
||||
// --- COMMENTS TAB ---
|
||||
Widget _buildCommentsTab() {
|
||||
return Obx(() {
|
||||
final contactId = contactRx.value.id;
|
||||
@ -622,25 +640,3 @@ class _ContactDetailScreenState extends State<ContactDetailScreen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProjectLabel extends StatelessWidget {
|
||||
final String? projectName;
|
||||
const ProjectLabel(this.projectName, {super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline, size: 14, color: Colors.grey),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
projectName ?? 'Select Project',
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/model/document/document_edit_bottom_sheet.dart';
|
||||
import 'package:on_field_work/controller/permission_controller.dart';
|
||||
import 'package:on_field_work/helpers/utils/permission_constants.dart';
|
||||
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||
|
||||
class DocumentDetailsPage extends StatefulWidget {
|
||||
final String documentId;
|
||||
@ -23,7 +24,7 @@ class DocumentDetailsPage extends StatefulWidget {
|
||||
State<DocumentDetailsPage> createState() => _DocumentDetailsPageState();
|
||||
}
|
||||
|
||||
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
class _DocumentDetailsPageState extends State<DocumentDetailsPage> with UIMixin {
|
||||
final DocumentDetailsController controller =
|
||||
Get.find<DocumentDetailsController>();
|
||||
|
||||
@ -49,50 +50,78 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF1F1F1),
|
||||
appBar: CustomAppBar(
|
||||
title: 'Document Details',
|
||||
backgroundColor: appBarColor,
|
||||
onBackPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return SkeletonLoaders.documentDetailsSkeletonLoader();
|
||||
}
|
||||
|
||||
final docResponse = controller.documentDetails.value;
|
||||
if (docResponse == null || docResponse.data == null) {
|
||||
return Center(
|
||||
child: MyText.bodyMedium(
|
||||
"Failed to load document details.",
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final doc = docResponse.data!;
|
||||
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: _onRefresh,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDetailsCard(doc),
|
||||
const SizedBox(height: 20),
|
||||
MyText.titleMedium("Versions",
|
||||
fontWeight: 700, color: Colors.black),
|
||||
const SizedBox(height: 10),
|
||||
_buildVersionsSection(),
|
||||
],
|
||||
body: Stack(
|
||||
children: [
|
||||
// Gradient behind content
|
||||
Container(
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
appBarColor,
|
||||
appBarColor.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// Main content
|
||||
SafeArea(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return SkeletonLoaders.documentDetailsSkeletonLoader();
|
||||
}
|
||||
|
||||
final docResponse = controller.documentDetails.value;
|
||||
if (docResponse == null || docResponse.data == null) {
|
||||
return Center(
|
||||
child: MyText.bodyMedium(
|
||||
"Failed to load document details.",
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final doc = docResponse.data!;
|
||||
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: _onRefresh,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDetailsCard(doc),
|
||||
const SizedBox(height: 20),
|
||||
MyText.titleMedium(
|
||||
"Versions",
|
||||
fontWeight: 700,
|
||||
color: Colors.black,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_buildVersionsSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -428,14 +428,21 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
|
||||
}
|
||||
|
||||
Widget _buildDocumentCard(DocumentItem doc, bool showDateHeader) {
|
||||
final uploadDate =
|
||||
DateFormat("dd MMM yyyy").format(doc.uploadedAt.toLocal());
|
||||
final uploadTime = DateFormat("hh:mm a").format(doc.uploadedAt.toLocal());
|
||||
final uploader = doc.uploadedBy.firstName.isNotEmpty
|
||||
? "${doc.uploadedBy.firstName} ${doc.uploadedBy.lastName}".trim()
|
||||
: "You";
|
||||
final uploadDate = doc.uploadedAt != null
|
||||
? DateFormat("dd MMM yyyy").format(doc.uploadedAt!.toLocal())
|
||||
: '-';
|
||||
final uploadTime = doc.uploadedAt != null
|
||||
? DateFormat("hh:mm a").format(doc.uploadedAt!.toLocal())
|
||||
: '';
|
||||
|
||||
final iconColor = _getDocumentTypeColor(doc.documentType.name);
|
||||
final uploader =
|
||||
(doc.uploadedBy != null && doc.uploadedBy!.firstName.isNotEmpty)
|
||||
? "${doc.uploadedBy!.firstName} ${doc.uploadedBy!.lastName ?? ''}"
|
||||
.trim()
|
||||
: "You";
|
||||
|
||||
final iconColor =
|
||||
_getDocumentTypeColor(doc.documentType?.name ?? 'unknown');
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -473,17 +480,16 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
_getDocumentIcon(doc.documentType.name),
|
||||
color: iconColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
_getDocumentIcon(doc.documentType?.name ?? 'unknown'),
|
||||
color: iconColor,
|
||||
size: 24,
|
||||
)),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@ -497,7 +503,7 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: MyText.labelSmall(
|
||||
doc.documentType.name,
|
||||
doc.documentType?.name ?? 'Unknown',
|
||||
fontWeight: 600,
|
||||
color: iconColor,
|
||||
letterSpacing: 0.3,
|
||||
@ -872,12 +878,18 @@ class _UserDocumentsPageState extends State<UserDocumentsPage>
|
||||
}
|
||||
|
||||
final doc = docs[index];
|
||||
final currentDate = DateFormat("dd MMM yyyy")
|
||||
.format(doc.uploadedAt.toLocal());
|
||||
final prevDate = index > 0
|
||||
final currentDate = doc.uploadedAt != null
|
||||
? DateFormat("dd MMM yyyy")
|
||||
.format(docs[index - 1].uploadedAt.toLocal())
|
||||
.format(doc.uploadedAt!.toLocal())
|
||||
: '';
|
||||
|
||||
final prevDate = index > 0
|
||||
? (docs[index - 1].uploadedAt != null
|
||||
? DateFormat("dd MMM yyyy").format(
|
||||
docs[index - 1].uploadedAt!.toLocal())
|
||||
: '')
|
||||
: null;
|
||||
|
||||
final showDateHeader = currentDate != prevDate;
|
||||
|
||||
return _buildDocumentCard(doc, showDateHeader);
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
||||
import 'package:on_field_work/view/employees/employee_detail_screen.dart';
|
||||
import 'package:on_field_work/view/document/user_document_screen.dart';
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||
|
||||
class EmployeeProfilePage extends StatefulWidget {
|
||||
final String employeeId;
|
||||
@ -14,7 +15,7 @@ class EmployeeProfilePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _EmployeeProfilePageState extends State<EmployeeProfilePage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
with SingleTickerProviderStateMixin, UIMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
@override
|
||||
@ -31,44 +32,64 @@ class _EmployeeProfilePageState extends State<EmployeeProfilePage>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF1F1F1),
|
||||
appBar: CustomAppBar(
|
||||
title: "Employee Profile",
|
||||
onBackPressed: () => Get.back(),
|
||||
backgroundColor: appBarColor,
|
||||
),
|
||||
body: Column(
|
||||
body: Stack(
|
||||
children: [
|
||||
// ---------------- TabBar outside AppBar ----------------
|
||||
// === Gradient at the top behind AppBar + TabBar ===
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.black,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
indicatorColor: Colors.red,
|
||||
tabs: const [
|
||||
Tab(text: "Details"),
|
||||
Tab(text: "Documents"),
|
||||
],
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
appBarColor,
|
||||
appBarColor.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ---------------- TabBarView ----------------
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: Column(
|
||||
children: [
|
||||
// Details Tab
|
||||
EmployeeDetailPage(
|
||||
employeeId: widget.employeeId,
|
||||
fromProfile: true,
|
||||
Container(
|
||||
decoration: const BoxDecoration(color: Colors.transparent),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: Colors.white70,
|
||||
indicatorColor: Colors.white,
|
||||
indicatorWeight: 3,
|
||||
tabs: const [
|
||||
Tab(text: "Details"),
|
||||
Tab(text: "Documents"),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Documents Tab
|
||||
UserDocumentsPage(
|
||||
entityId: widget.employeeId,
|
||||
isEmployee: true,
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
EmployeeDetailPage(
|
||||
employeeId: widget.employeeId,
|
||||
fromProfile: true,
|
||||
),
|
||||
UserDocumentsPage(
|
||||
entityId: widget.employeeId,
|
||||
isEmployee: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -17,6 +17,7 @@ import 'package:on_field_work/helpers/utils/permission_constants.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
|
||||
import 'package:on_field_work/view/employees/employee_profile_screen.dart';
|
||||
import 'package:on_field_work/view/employees/manage_reporting_bottom_sheet.dart';
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
|
||||
class EmployeesScreen extends StatefulWidget {
|
||||
const EmployeesScreen({super.key});
|
||||
@ -113,98 +114,69 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: _buildAppBar(),
|
||||
floatingActionButton: _buildFloatingActionButton(),
|
||||
body: SafeArea(
|
||||
child: GetBuilder<EmployeesScreenController>(
|
||||
init: _employeeController,
|
||||
tag: 'employee_screen_controller',
|
||||
builder: (_) {
|
||||
_filterEmployees(_searchController.text);
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: _refreshEmployees,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.only(bottom: 40),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(15),
|
||||
child: _buildSearchField(),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: _buildEmployeeList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
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],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: CustomAppBar(
|
||||
title: "Employees",
|
||||
backgroundColor: appBarColor,
|
||||
projectName: Get.find<ProjectController>().selectedProject?.name ??
|
||||
'Select Project',
|
||||
onBackPressed: () => Get.offNamed('/dashboard'),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
// Gradient behind content
|
||||
Container(
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
appBarColor,
|
||||
appBarColor.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Main content
|
||||
SafeArea(
|
||||
child: GetBuilder<EmployeesScreenController>(
|
||||
init: _employeeController,
|
||||
tag: 'employee_screen_controller',
|
||||
builder: (_) {
|
||||
_filterEmployees(_searchController.text);
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: _refreshEmployees,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.only(bottom: 40),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(15),
|
||||
child: _buildSearchField(),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: _buildEmployeeList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: _buildFloatingActionButton(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import 'package:on_field_work/controller/expense/add_expense_controller.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
|
||||
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
import 'package:on_field_work/helpers/widgets/expense/expense_detail_helpers.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||
@ -81,34 +81,62 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
|
||||
canSubmit.value = result;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF7F7F7),
|
||||
appBar: _AppBar(projectController: projectController),
|
||||
body: SafeArea(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value) return buildLoadingSkeleton();
|
||||
final expense = controller.expense.value;
|
||||
if (controller.errorMessage.isNotEmpty || expense == null) {
|
||||
return Center(child: MyText.bodyMedium("No data to display."));
|
||||
}
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkPermissionToSubmit(expense);
|
||||
});
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF7F7F7),
|
||||
appBar: CustomAppBar(
|
||||
title: "Expense Details",
|
||||
backgroundColor: appBarColor,
|
||||
onBackPressed: () => Get.toNamed('/dashboard/expense-main-page'),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
// Gradient behind content
|
||||
Container(
|
||||
height: kToolbarHeight + MediaQuery.of(context).padding.top,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
appBarColor,
|
||||
appBarColor.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
final statusColor = getExpenseStatusColor(expense.status.name,
|
||||
colorCode: expense.status.color);
|
||||
final formattedAmount = formatExpenseAmount(expense.amount);
|
||||
// Main content
|
||||
SafeArea(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value) return buildLoadingSkeleton();
|
||||
|
||||
return MyRefreshIndicator(
|
||||
final expense = controller.expense.value;
|
||||
if (controller.errorMessage.isNotEmpty || expense == null) {
|
||||
return Center(child: MyText.bodyMedium("No data to display."));
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkPermissionToSubmit(expense);
|
||||
});
|
||||
|
||||
final statusColor = getExpenseStatusColor(
|
||||
expense.status.name,
|
||||
colorCode: expense.status.color,
|
||||
);
|
||||
final formattedAmount = formatExpenseAmount(expense.amount);
|
||||
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.fetchExpenseDetails();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
12, 12, 12, 30 + MediaQuery.of(context).padding.bottom),
|
||||
12, 12, 12, 30 + MediaQuery.of(context).padding.bottom
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 520),
|
||||
@ -122,21 +150,21 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ---------------- Header & Status ----------------
|
||||
// Header & Status
|
||||
_InvoiceHeader(expense: expense),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
|
||||
// ---------------- Activity Logs ----------------
|
||||
// Activity Logs
|
||||
InvoiceLogs(logs: expense.expenseLogs),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
// ---------------- Amount & Summary ----------------
|
||||
|
||||
// Amount & Summary
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium('Amount',
|
||||
fontWeight: 600),
|
||||
MyText.bodyMedium('Amount', fontWeight: 600),
|
||||
const SizedBox(height: 4),
|
||||
MyText.bodyLarge(
|
||||
formattedAmount,
|
||||
@ -146,7 +174,6 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
// Optional: Pre-approved badge
|
||||
if (expense.preApproved)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@ -165,19 +192,19 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
|
||||
),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
|
||||
// ---------------- Parties ----------------
|
||||
// Parties
|
||||
_InvoicePartiesTable(expense: expense),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
|
||||
// ---------------- Expense Details ----------------
|
||||
// Expense Details
|
||||
_InvoiceDetailsTable(expense: expense),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
|
||||
// ---------------- Documents ----------------
|
||||
// Documents
|
||||
_InvoiceDocuments(documents: expense.documents),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
|
||||
// ---------------- Totals ----------------
|
||||
// Totals
|
||||
_InvoiceTotals(
|
||||
expense: expense,
|
||||
formattedAmount: formattedAmount,
|
||||
@ -189,122 +216,109 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
}),
|
||||
),
|
||||
floatingActionButton: Obx(() {
|
||||
if (controller.isLoading.value) return buildLoadingSkeleton();
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: Obx(() {
|
||||
if (controller.isLoading.value) return buildLoadingSkeleton();
|
||||
|
||||
final expense = controller.expense.value;
|
||||
if (controller.errorMessage.isNotEmpty || expense == null) {
|
||||
return Center(child: MyText.bodyMedium("No data to display."));
|
||||
}
|
||||
final expense = controller.expense.value;
|
||||
if (controller.errorMessage.isNotEmpty || expense == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
if (!_checkedPermission) {
|
||||
_checkedPermission = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkPermissionToSubmit(expense);
|
||||
});
|
||||
}
|
||||
if (!_checkedPermission) {
|
||||
_checkedPermission = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkPermissionToSubmit(expense);
|
||||
});
|
||||
}
|
||||
|
||||
if (!ExpensePermissionHelper.canEditExpense(employeeInfo, expense)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (!ExpensePermissionHelper.canEditExpense(employeeInfo, expense)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: () async {
|
||||
final editData = {
|
||||
'id': expense.id,
|
||||
'projectName': expense.project.name,
|
||||
'amount': expense.amount,
|
||||
'supplerName': expense.supplerName,
|
||||
'description': expense.description,
|
||||
'transactionId': expense.transactionId,
|
||||
'location': expense.location,
|
||||
'transactionDate': expense.transactionDate,
|
||||
'noOfPersons': expense.noOfPersons,
|
||||
'expensesTypeId': expense.expensesType.id,
|
||||
'paymentModeId': expense.paymentMode.id,
|
||||
'paidById': expense.paidBy.id,
|
||||
'paidByFirstName': expense.paidBy.firstName,
|
||||
'paidByLastName': expense.paidBy.lastName,
|
||||
'attachments': expense.documents
|
||||
.map((doc) => {
|
||||
'url': doc.preSignedUrl,
|
||||
'fileName': doc.fileName,
|
||||
'documentId': doc.documentId,
|
||||
'contentType': doc.contentType,
|
||||
})
|
||||
.toList(),
|
||||
};
|
||||
logSafe('editData: $editData', level: LogLevel.info);
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: () async {
|
||||
final editData = {
|
||||
'id': expense.id,
|
||||
'projectName': expense.project.name,
|
||||
'amount': expense.amount,
|
||||
'supplerName': expense.supplerName,
|
||||
'description': expense.description,
|
||||
'transactionId': expense.transactionId,
|
||||
'location': expense.location,
|
||||
'transactionDate': expense.transactionDate,
|
||||
'noOfPersons': expense.noOfPersons,
|
||||
'expensesTypeId': expense.expensesType.id,
|
||||
'paymentModeId': expense.paymentMode.id,
|
||||
'paidById': expense.paidBy.id,
|
||||
'paidByFirstName': expense.paidBy.firstName,
|
||||
'paidByLastName': expense.paidBy.lastName,
|
||||
'attachments': expense.documents
|
||||
.map((doc) => {
|
||||
'url': doc.preSignedUrl,
|
||||
'fileName': doc.fileName,
|
||||
'documentId': doc.documentId,
|
||||
'contentType': doc.contentType,
|
||||
})
|
||||
.toList(),
|
||||
};
|
||||
|
||||
final addCtrl = Get.put(AddExpenseController());
|
||||
final addCtrl = Get.put(AddExpenseController());
|
||||
await addCtrl.loadMasterData();
|
||||
addCtrl.populateFieldsForEdit(editData);
|
||||
|
||||
await addCtrl.loadMasterData();
|
||||
addCtrl.populateFieldsForEdit(editData);
|
||||
await showAddExpenseBottomSheet(isEdit: true);
|
||||
await controller.fetchExpenseDetails();
|
||||
},
|
||||
backgroundColor: contentTheme.primary,
|
||||
icon: const Icon(Icons.edit),
|
||||
label: MyText.bodyMedium("Edit Expense",
|
||||
fontWeight: 600, color: Colors.white),
|
||||
);
|
||||
}),
|
||||
bottomNavigationBar: Obx(() {
|
||||
final expense = controller.expense.value;
|
||||
if (expense == null) return const SizedBox();
|
||||
|
||||
await showAddExpenseBottomSheet(isEdit: true);
|
||||
await controller.fetchExpenseDetails();
|
||||
},
|
||||
backgroundColor: contentTheme.primary,
|
||||
icon: const Icon(Icons.edit),
|
||||
label: MyText.bodyMedium("Edit Expense",
|
||||
fontWeight: 600, color: Colors.white),
|
||||
);
|
||||
}),
|
||||
bottomNavigationBar: Obx(() {
|
||||
final expense = controller.expense.value;
|
||||
if (expense == null) return const SizedBox();
|
||||
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Color(0x11000000))),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: expense.nextStatus.where((next) {
|
||||
const submitStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7';
|
||||
|
||||
final rawPermissions = next.permissionIds;
|
||||
final parsedPermissions =
|
||||
controller.parsePermissionIds(rawPermissions);
|
||||
|
||||
final isSubmitStatus = next.id == submitStatusId;
|
||||
final isCreatedByCurrentUser =
|
||||
employeeInfo?.id == expense.createdBy.id;
|
||||
|
||||
logSafe(
|
||||
'🔐 Permission Logic:\n'
|
||||
'🔸 Status: ${next.name}\n'
|
||||
'🔸 Status ID: ${next.id}\n'
|
||||
'🔸 Parsed Permissions: $parsedPermissions\n'
|
||||
'🔸 Is Submit: $isSubmitStatus\n'
|
||||
'🔸 Created By Current User: $isCreatedByCurrentUser',
|
||||
level: LogLevel.debug,
|
||||
);
|
||||
|
||||
if (isSubmitStatus) {
|
||||
// Submit can be done ONLY by the creator
|
||||
return isCreatedByCurrentUser;
|
||||
}
|
||||
|
||||
// All other statuses - check permission normally
|
||||
return permissionController.hasAnyPermission(parsedPermissions);
|
||||
}).map((next) {
|
||||
return _statusButton(context, controller, expense, next);
|
||||
}).toList(),
|
||||
),
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Color(0x11000000))),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: expense.nextStatus.where((next) {
|
||||
const submitStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7';
|
||||
|
||||
final rawPermissions = next.permissionIds;
|
||||
final parsedPermissions =
|
||||
controller.parsePermissionIds(rawPermissions);
|
||||
|
||||
final isSubmitStatus = next.id == submitStatusId;
|
||||
final isCreatedByCurrentUser =
|
||||
employeeInfo?.id == expense.createdBy.id;
|
||||
|
||||
if (isSubmitStatus) return isCreatedByCurrentUser;
|
||||
return permissionController.hasAnyPermission(parsedPermissions);
|
||||
}).map((next) {
|
||||
return _statusButton(context, controller, expense, next);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _statusButton(BuildContext context, ExpenseDetailController controller,
|
||||
ExpenseDetailModel expense, dynamic next) {
|
||||
@ -449,64 +463,6 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
|
||||
}
|
||||
}
|
||||
|
||||
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final ProjectController projectController;
|
||||
const _AppBar({required this.projectController});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
elevation: 1,
|
||||
backgroundColor: Colors.white,
|
||||
title: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () => Get.toNamed('/dashboard/expense-main-page'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleLarge('Expense Details',
|
||||
fontWeight: 700, color: Colors.black),
|
||||
MySpacing.height(2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (_) {
|
||||
final projectName =
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline,
|
||||
size: 14, color: Colors.grey),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
projectName,
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
|
||||
class _InvoiceHeader extends StatelessWidget {
|
||||
final ExpenseDetailModel expense;
|
||||
const _InvoiceHeader({required this.expense});
|
||||
|
||||
@ -12,6 +12,7 @@ import 'package:on_field_work/helpers/widgets/expense/expense_main_components.da
|
||||
import 'package:on_field_work/helpers/utils/permission_constants.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
|
||||
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
|
||||
class ExpenseMainScreen extends StatefulWidget {
|
||||
const ExpenseMainScreen({super.key});
|
||||
@ -87,65 +88,103 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: ExpenseAppBar(projectController: projectController),
|
||||
body: Column(
|
||||
appBar: CustomAppBar(
|
||||
title: "Expense & Reimbursement",
|
||||
backgroundColor: appBarColor,
|
||||
onBackPressed: () => Get.toNamed('/dashboard/finance'),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
// ---------------- TabBar ----------------
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.black,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
indicatorColor: Colors.red,
|
||||
tabs: const [
|
||||
Tab(text: "Current Month"),
|
||||
Tab(text: "History"),
|
||||
// === FULL GRADIENT BEHIND APPBAR & TABBAR ===
|
||||
Positioned.fill(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
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]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ---------------- Gray background for rest ----------------
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Colors.grey[100],
|
||||
child: Column(
|
||||
children: [
|
||||
// ---------------- Search ----------------
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||
child: SearchAndFilter(
|
||||
controller: searchController,
|
||||
onChanged: (_) => setState(() {}),
|
||||
onFilterTap: _openFilterBottomSheet,
|
||||
expenseController: expenseController,
|
||||
),
|
||||
// === MAIN CONTENT ===
|
||||
SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: Column(
|
||||
children: [
|
||||
// TAB BAR WITH TRANSPARENT BACKGROUND
|
||||
Container(
|
||||
decoration: const BoxDecoration(color: Colors.transparent),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: Colors.white70,
|
||||
indicatorColor: Colors.white,
|
||||
indicatorWeight: 3,
|
||||
tabs: const [
|
||||
Tab(text: "Current Month"),
|
||||
Tab(text: "History"),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ---------------- TabBarView ----------------
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
// CONTENT AREA
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildExpenseList(isHistory: false),
|
||||
_buildExpenseList(isHistory: true),
|
||||
// SEARCH & FILTER
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 0),
|
||||
child: SearchAndFilter(
|
||||
controller: searchController,
|
||||
onChanged: (_) => setState(() {}),
|
||||
onFilterTap: _openFilterBottomSheet,
|
||||
expenseController: expenseController,
|
||||
),
|
||||
),
|
||||
|
||||
// TABBAR VIEW
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildExpenseList(isHistory: false),
|
||||
_buildExpenseList(isHistory: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: Obx(() {
|
||||
// Show loader or hide FAB while permissions are loading
|
||||
if (permissionController.permissions.isEmpty) {
|
||||
if (permissionController.permissions.isEmpty)
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final canUpload =
|
||||
permissionController.hasPermission(Permissions.expenseUpload);
|
||||
|
||||
@ -3,10 +3,9 @@ import 'package:get/get.dart';
|
||||
import 'package:on_field_work/controller/finance/advance_payment_controller.dart';
|
||||
import 'package:on_field_work/controller/project_controller.dart';
|
||||
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
|
||||
class AdvancePaymentScreen extends StatefulWidget {
|
||||
const AdvancePaymentScreen({super.key});
|
||||
@ -49,148 +48,106 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: _buildAppBar(),
|
||||
|
||||
// ✅ SafeArea added so nothing hides under system navigation buttons
|
||||
body: SafeArea(
|
||||
bottom: true,
|
||||
child: GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
final emp = controller.selectedEmployee.value;
|
||||
if (emp != null) {
|
||||
await controller.fetchAdvancePayments(emp.id.toString());
|
||||
}
|
||||
},
|
||||
color: Colors.white,
|
||||
backgroundColor: contentTheme.primary,
|
||||
strokeWidth: 2.5,
|
||||
displacement: 60,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
// ✅ Extra bottom padding so content does NOT go under 3-button navbar
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 20,
|
||||
),
|
||||
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSearchBar(),
|
||||
_buildEmployeeDropdown(context),
|
||||
_buildTopBalance(),
|
||||
_buildPaymentList(),
|
||||
],
|
||||
),
|
||||
appBar: 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- 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,
|
||||
// ===== MAIN CONTENT =====
|
||||
SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
final emp = controller.selectedEmployee.value;
|
||||
if (emp != null) {
|
||||
await controller.fetchAdvancePayments(emp.id.toString());
|
||||
}
|
||||
},
|
||||
color: Colors.white,
|
||||
backgroundColor: appBarColor,
|
||||
strokeWidth: 2.5,
|
||||
displacement: 60,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 20,
|
||||
),
|
||||
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],
|
||||
child: Column(
|
||||
children: [
|
||||
// ===== SEARCH BAR FLOATING OVER GRADIENT =====
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
child: SizedBox(
|
||||
height: 38,
|
||||
child: TextField(
|
||||
controller: _searchCtrl,
|
||||
focusNode: _searchFocus,
|
||||
onChanged: (v) =>
|
||||
controller.searchQuery.value = v.trim(),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 0),
|
||||
prefixIcon: const Icon(Icons.search,
|
||||
size: 20, color: Colors.grey),
|
||||
hintText: 'Search Employee...',
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300, width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300, width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: appBarColor, width: 1.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
|
||||
// ---------------- Search ----------------
|
||||
Widget _buildSearchBar() {
|
||||
return Container(
|
||||
color: Colors.grey[100],
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 38,
|
||||
child: TextField(
|
||||
controller: _searchCtrl,
|
||||
focusNode: _searchFocus,
|
||||
onChanged: (v) => controller.searchQuery.value = v.trim(),
|
||||
decoration: InputDecoration(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 0),
|
||||
prefixIcon:
|
||||
const Icon(Icons.search, size: 20, color: Colors.grey),
|
||||
hintText: 'Search Employee...',
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide:
|
||||
BorderSide(color: Colors.grey.shade300, width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide:
|
||||
BorderSide(color: Colors.grey.shade300, width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide:
|
||||
BorderSide(color: contentTheme.primary, width: 1.5),
|
||||
// ===== EMPLOYEE DROPDOWN =====
|
||||
_buildEmployeeDropdown(context),
|
||||
|
||||
// ===== TOP BALANCE =====
|
||||
_buildTopBalance(),
|
||||
|
||||
// ===== PAYMENTS LIST =====
|
||||
_buildPaymentList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -6,13 +6,14 @@ import 'package:on_field_work/controller/dynamicMenu/dynamic_menu_controller.dar
|
||||
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_card.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||
import 'package:on_field_work/helpers/widgets/dashbaord/expense_breakdown_chart.dart';
|
||||
import 'package:on_field_work/helpers/widgets/dashbaord/expense_by_status_widget.dart';
|
||||
import 'package:on_field_work/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart';
|
||||
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
|
||||
import 'package:on_field_work/helpers/utils/permission_constants.dart';
|
||||
import 'package:on_field_work/model/dynamicMenu/dynamic_menu_model.dart';
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
|
||||
|
||||
class FinanceScreen extends StatefulWidget {
|
||||
const FinanceScreen({super.key});
|
||||
@ -52,132 +53,116 @@ class _FinanceScreenState extends State<FinanceScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FA),
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(72),
|
||||
child: AppBar(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
elevation: 0.5,
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 0,
|
||||
title: Padding(
|
||||
padding: MySpacing.xy(16, 0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () => Get.offNamed('/dashboard'),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MyText.titleLarge(
|
||||
'Finance',
|
||||
fontWeight: 700,
|
||||
color: Colors.black,
|
||||
),
|
||||
MySpacing.height(2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
final projectName =
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline,
|
||||
size: 14, color: Colors.grey),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
projectName,
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
appBar: CustomAppBar(
|
||||
title: "Finance",
|
||||
onBackPressed: () => Get.offAllNamed( '/dashboard' ),
|
||||
backgroundColor: appBarColor,
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false, // keep appbar area same
|
||||
bottom: true, // avoid system bottom buttons
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Obx(() {
|
||||
if (menuController.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (menuController.hasError.value ||
|
||||
menuController.menuItems.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
"Failed to load menus. Please try again later.",
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final financeMenuIds = [
|
||||
MenuItems.expenseReimbursement,
|
||||
MenuItems.paymentRequests,
|
||||
MenuItems.advancePaymentStatements,
|
||||
];
|
||||
|
||||
final financeMenus = menuController.menuItems
|
||||
.where((m) => financeMenuIds.contains(m.id) && m.available)
|
||||
.toList();
|
||||
|
||||
if (financeMenus.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
"You don’t have access to the Finance section.",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---- IMPORTANT FIX: Add bottom safe padding ----
|
||||
final double bottomInset =
|
||||
MediaQuery.of(context).viewPadding.bottom;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
bottomInset +
|
||||
24, // ensures charts never go under system buttons
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildFinanceModulesCompact(financeMenus),
|
||||
MySpacing.height(24),
|
||||
ExpenseByStatusWidget(controller: dashboardController),
|
||||
MySpacing.height(24),
|
||||
ExpenseTypeReportChart(),
|
||||
MySpacing.height(24),
|
||||
MonthlyExpenseDashboardChart(),
|
||||
body: Stack(
|
||||
children: [
|
||||
// Top fade under AppBar
|
||||
Container(
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
appBarColor,
|
||||
appBarColor.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom fade (above system buttons or FAB)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
appBarColor.withOpacity(0.05),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Main scrollable content
|
||||
SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Obx(() {
|
||||
if (menuController.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (menuController.hasError.value ||
|
||||
menuController.menuItems.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
"Failed to load menus. Please try again later.",
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final financeMenuIds = [
|
||||
MenuItems.expenseReimbursement,
|
||||
MenuItems.paymentRequests,
|
||||
MenuItems.advancePaymentStatements,
|
||||
];
|
||||
|
||||
final financeMenus = menuController.menuItems
|
||||
.where((m) => financeMenuIds.contains(m.id) && m.available)
|
||||
.toList();
|
||||
|
||||
if (financeMenus.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
"You don’t have access to the Finance section.",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final double bottomInset =
|
||||
MediaQuery.of(context).viewPadding.bottom;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
bottomInset + 24,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildFinanceModulesCompact(financeMenus),
|
||||
MySpacing.height(24),
|
||||
ExpenseByStatusWidget(controller: dashboardController),
|
||||
MySpacing.height(24),
|
||||
ExpenseTypeReportChart(),
|
||||
MySpacing.height(24),
|
||||
MonthlyExpenseDashboardChart(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/model/finance/payment_request_rembursement_bottom_sheet.dart';
|
||||
import 'package:on_field_work/model/finance/make_expense_bottom_sheet.dart';
|
||||
import 'package:on_field_work/model/finance/add_payment_request_bottom_sheet.dart';
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
|
||||
class PaymentRequestDetailScreen extends StatefulWidget {
|
||||
final String paymentRequestId;
|
||||
@ -107,76 +108,101 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: _buildAppBar(),
|
||||
body: SafeArea(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value &&
|
||||
controller.paymentRequest.value == null) {
|
||||
return SkeletonLoaders.paymentRequestDetailSkeletonLoader();
|
||||
}
|
||||
|
||||
final request = controller.paymentRequest.value;
|
||||
|
||||
if ((controller.errorMessage.value).isNotEmpty) {
|
||||
return Center(
|
||||
child: MyText.bodyMedium(controller.errorMessage.value));
|
||||
}
|
||||
|
||||
if (request == null) {
|
||||
return Center(child: MyText.bodyMedium("No data to display."));
|
||||
}
|
||||
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: controller.fetchPaymentRequestDetail,
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
60 + MediaQuery.of(context).padding.bottom,
|
||||
appBar: CustomAppBar(
|
||||
title: "Payment Request Details",
|
||||
backgroundColor: appBarColor,
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
// ===== TOP GRADIENT =====
|
||||
Container(
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
appBarColor,
|
||||
appBarColor.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 520),
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12, horizontal: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_Header(
|
||||
request: request,
|
||||
colorParser: _parseColor,
|
||||
employeeInfo: employeeInfo,
|
||||
onEdit: () =>
|
||||
_openEditPaymentRequestBottomSheet(request),
|
||||
),
|
||||
),
|
||||
|
||||
// ===== MAIN CONTENT =====
|
||||
SafeArea(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value &&
|
||||
controller.paymentRequest.value == null) {
|
||||
return SkeletonLoaders.paymentRequestDetailSkeletonLoader();
|
||||
}
|
||||
|
||||
final request = controller.paymentRequest.value;
|
||||
|
||||
if ((controller.errorMessage.value).isNotEmpty) {
|
||||
return Center(
|
||||
child: MyText.bodyMedium(controller.errorMessage.value));
|
||||
}
|
||||
|
||||
if (request == null) {
|
||||
return Center(child: MyText.bodyMedium("No data to display."));
|
||||
}
|
||||
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: controller.fetchPaymentRequestDetail,
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
60 + MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 520),
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12, horizontal: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_Header(
|
||||
request: request,
|
||||
colorParser: _parseColor,
|
||||
employeeInfo: employeeInfo,
|
||||
onEdit: () =>
|
||||
_openEditPaymentRequestBottomSheet(request),
|
||||
),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
_Logs(
|
||||
logs: request.updateLogs ?? [],
|
||||
colorParser: _parseColor,
|
||||
),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
_Parties(request: request),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
_DetailsTable(request: request),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
_Documents(documents: request.attachments ?? []),
|
||||
MySpacing.height(24),
|
||||
],
|
||||
),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
_Logs(
|
||||
logs: request.updateLogs ?? [],
|
||||
colorParser: _parseColor,
|
||||
),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
_Parties(request: request),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
_DetailsTable(request: request),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
_Documents(documents: request.attachments ?? []),
|
||||
MySpacing.height(24),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
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 {
|
||||
|
||||
@ -13,6 +13,7 @@ import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
|
||||
import 'package:on_field_work/controller/permission_controller.dart';
|
||||
import 'package:on_field_work/helpers/utils/permission_constants.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
|
||||
class PaymentRequestMainScreen extends StatefulWidget {
|
||||
const PaymentRequestMainScreen({super.key});
|
||||
@ -96,53 +97,88 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: _buildAppBar(),
|
||||
|
||||
// ------------------------
|
||||
// FIX: SafeArea prevents content from going under 3-button navbar
|
||||
// ------------------------
|
||||
body: SafeArea(
|
||||
bottom: true,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.black,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
indicatorColor: Colors.red,
|
||||
tabs: const [
|
||||
Tab(text: "Current Month"),
|
||||
Tab(text: "History"),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Colors.grey[100],
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSearchBar(),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildPaymentRequestList(isHistory: false),
|
||||
_buildPaymentRequestList(isHistory: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
appBar: CustomAppBar(
|
||||
title: "Payment Requests",
|
||||
onBackPressed: () => Get.offNamed('/dashboard/finance'),
|
||||
backgroundColor: appBarColor,
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
// === FULL GRADIENT BEHIND APPBAR & TABBAR ===
|
||||
Positioned.fill(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
appBarColor,
|
||||
appBarColor.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child:
|
||||
Container(color: Colors.grey[100]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// === MAIN CONTENT ===
|
||||
SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: Column(
|
||||
children: [
|
||||
// TAB BAR WITH TRANSPARENT BACKGROUND
|
||||
Container(
|
||||
decoration: const BoxDecoration(color: Colors.transparent),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: Colors.white70,
|
||||
indicatorColor: Colors.white,
|
||||
tabs: const [
|
||||
Tab(text: "Current Month"),
|
||||
Tab(text: "History"),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// CONTENT AREA
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSearchBar(),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildPaymentRequestList(isHistory: false),
|
||||
_buildPaymentRequestList(isHistory: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: Obx(() {
|
||||
if (permissionController.permissions.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
@ -166,67 +202,6 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
return PreferredSize(
|
||||
preferredSize: const Size.fromHeight(72),
|
||||
child: AppBar(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
elevation: 0.5,
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 0,
|
||||
title: Padding(
|
||||
padding: MySpacing.xy(16, 0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () => Get.offNamed('/dashboard/finance'),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MyText.titleLarge(
|
||||
'Payment Requests',
|
||||
fontWeight: 700,
|
||||
color: Colors.black,
|
||||
),
|
||||
MySpacing.height(2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (_) {
|
||||
final name = projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline,
|
||||
size: 14, color: Colors.grey),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
name,
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBar() {
|
||||
return Padding(
|
||||
padding: MySpacing.fromLTRB(12, 10, 12, 0),
|
||||
|
||||
@ -64,15 +64,16 @@ class _LayoutState extends State<Layout> {
|
||||
body: SafeArea(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {}, // project selection removed → nothing to close
|
||||
onTap: () {},
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(context, isMobile),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
key: controller.scrollKey,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 0, vertical: isMobile ? 16 : 32),
|
||||
// Removed redundant vertical padding here. DashboardScreen's
|
||||
// SingleChildScrollView now handles all internal padding.
|
||||
padding: EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
|
||||
@ -429,6 +429,8 @@ class _ServiceProjectDetailsScreenState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: CustomAppBar(
|
||||
@ -436,55 +438,82 @@ class _ServiceProjectDetailsScreenState
|
||||
projectName: widget.projectName,
|
||||
onBackPressed: () => Get.toNamed('/dashboard/service-projects'),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// TabBar
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.black,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
indicatorColor: Colors.red,
|
||||
indicatorWeight: 3,
|
||||
isScrollable: false,
|
||||
tabs: [
|
||||
Tab(child: MyText.bodyMedium("Profile")),
|
||||
Tab(child: MyText.bodyMedium("Jobs")),
|
||||
Tab(child: MyText.bodyMedium("Teams")),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// TabBarView
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value &&
|
||||
controller.projectDetail.value == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (controller.errorMessage.value.isNotEmpty &&
|
||||
controller.projectDetail.value == null) {
|
||||
return Center(
|
||||
child: MyText.bodyMedium(controller.errorMessage.value));
|
||||
}
|
||||
SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: Column(
|
||||
children: [
|
||||
// === TAB BAR WITH TRANSPARENT BACKGROUND ===
|
||||
Container(
|
||||
decoration: const BoxDecoration(color: Colors.transparent),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: Colors.white70,
|
||||
indicatorColor: Colors.white,
|
||||
indicatorWeight: 3,
|
||||
tabs: [
|
||||
Tab(
|
||||
child: MyText.bodyMedium("Profile",
|
||||
color: Colors.white)),
|
||||
Tab(
|
||||
child:
|
||||
MyText.bodyMedium("Jobs", color: Colors.white)),
|
||||
Tab(
|
||||
child:
|
||||
MyText.bodyMedium("Teams", color: Colors.white)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
return TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildProfileTab(),
|
||||
JobsTab(
|
||||
scrollController: _jobScrollController,
|
||||
projectName: widget.projectName ?? '',
|
||||
),
|
||||
_buildTeamsTab(),
|
||||
],
|
||||
);
|
||||
}),
|
||||
// === TABBAR VIEW ===
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value &&
|
||||
controller.projectDetail.value == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (controller.errorMessage.value.isNotEmpty &&
|
||||
controller.projectDetail.value == null) {
|
||||
return Center(
|
||||
child:
|
||||
MyText.bodyMedium(controller.errorMessage.value));
|
||||
}
|
||||
|
||||
return TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildProfileTab(),
|
||||
JobsTab(
|
||||
scrollController: _jobScrollController,
|
||||
projectName: widget.projectName ?? '',
|
||||
),
|
||||
_buildTeamsTab(),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: _tabController.index == 1
|
||||
? FloatingActionButton.extended(
|
||||
|
||||
@ -760,17 +760,20 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final projectName = widget.projectName;
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: CustomAppBar(
|
||||
title: "Job Details Screen",
|
||||
onBackPressed: () => Get.back(),
|
||||
projectName: projectName,
|
||||
backgroundColor: appBarColor,
|
||||
),
|
||||
floatingActionButton: Obx(() => FloatingActionButton.extended(
|
||||
onPressed:
|
||||
isEditing.value ? _editJob : () => isEditing.value = true,
|
||||
backgroundColor: contentTheme.primary,
|
||||
backgroundColor: appBarColor,
|
||||
label: MyText.bodyMedium(
|
||||
isEditing.value ? "Save" : "Edit",
|
||||
color: Colors.white,
|
||||
@ -778,63 +781,101 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
),
|
||||
icon: Icon(isEditing.value ? Icons.save : Icons.edit),
|
||||
)),
|
||||
body: Obx(() {
|
||||
if (controller.isJobDetailLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.jobDetailErrorMessage.value.isNotEmpty) {
|
||||
return Center(
|
||||
child: MyText.bodyMedium(controller.jobDetailErrorMessage.value));
|
||||
}
|
||||
|
||||
final job = controller.jobDetail.value?.data;
|
||||
if (job == null) {
|
||||
return Center(child: MyText.bodyMedium("No details available"));
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: MySpacing.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildAttendanceCard(),
|
||||
_buildSectionCard(
|
||||
title: "Job Info",
|
||||
titleIcon: Icons.task_outlined,
|
||||
children: [
|
||||
_editableRow("Title", _titleController),
|
||||
_editableRow("Description", _descriptionController),
|
||||
_dateRangePicker(),
|
||||
body: Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
appBarColor,
|
||||
appBarColor.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
MySpacing.height(12),
|
||||
_buildSectionCard(
|
||||
title: "Project Branch",
|
||||
titleIcon: Icons.account_tree_outlined,
|
||||
children: [_branchDisplay()],
|
||||
),
|
||||
MySpacing.height(16),
|
||||
_buildSectionCard(
|
||||
title: "Assignees",
|
||||
titleIcon: Icons.person_outline,
|
||||
children: [_assigneeInputWithChips()]),
|
||||
MySpacing.height(16),
|
||||
_buildSectionCard(
|
||||
title: "Tags",
|
||||
titleIcon: Icons.label_outline,
|
||||
children: [_tagEditor()]),
|
||||
MySpacing.height(16),
|
||||
if ((job.updateLogs?.isNotEmpty ?? false))
|
||||
_buildSectionCard(
|
||||
title: "Update Logs",
|
||||
titleIcon: Icons.history,
|
||||
children: [JobTimeline(logs: job.updateLogs ?? [])]),
|
||||
MySpacing.height(80),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// Bottom fade (for smooth transition above FAB)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
height: 60, // adjust based on FAB height
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
appBarColor.withOpacity(0.05),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Main scrollable content
|
||||
Obx(() {
|
||||
if (controller.isJobDetailLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.jobDetailErrorMessage.value.isNotEmpty) {
|
||||
return Center(
|
||||
child: MyText.bodyMedium(
|
||||
controller.jobDetailErrorMessage.value));
|
||||
}
|
||||
|
||||
final job = controller.jobDetail.value?.data;
|
||||
if (job == null) {
|
||||
return Center(child: MyText.bodyMedium("No details available"));
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: MySpacing.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildAttendanceCard(),
|
||||
_buildSectionCard(
|
||||
title: "Job Info",
|
||||
titleIcon: Icons.task_outlined,
|
||||
children: [
|
||||
_editableRow("Title", _titleController),
|
||||
_editableRow("Description", _descriptionController),
|
||||
_dateRangePicker(),
|
||||
],
|
||||
),
|
||||
MySpacing.height(12),
|
||||
_buildSectionCard(
|
||||
title: "Project Branch",
|
||||
titleIcon: Icons.account_tree_outlined,
|
||||
children: [_branchDisplay()],
|
||||
),
|
||||
MySpacing.height(16),
|
||||
_buildSectionCard(
|
||||
title: "Assignees",
|
||||
titleIcon: Icons.person_outline,
|
||||
children: [_assigneeInputWithChips()]),
|
||||
MySpacing.height(16),
|
||||
_buildSectionCard(
|
||||
title: "Tags",
|
||||
titleIcon: Icons.label_outline,
|
||||
children: [_tagEditor()]),
|
||||
MySpacing.height(16),
|
||||
if ((job.updateLogs?.isNotEmpty ?? false))
|
||||
_buildSectionCard(
|
||||
title: "Update Logs",
|
||||
titleIcon: Icons.history,
|
||||
children: [JobTimeline(logs: job.updateLogs ?? [])]),
|
||||
MySpacing.height(80),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,99 +181,115 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
|
||||
appBar: CustomAppBar(
|
||||
title: "Service Projects",
|
||||
projectName: 'All Service Projects',
|
||||
backgroundColor: appBarColor,
|
||||
onBackPressed: () => Get.toNamed('/dashboard'),
|
||||
),
|
||||
|
||||
// FIX 1: Entire body wrapped in SafeArea
|
||||
body: SafeArea(
|
||||
bottom: true,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.xy(8, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 35,
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12),
|
||||
prefixIcon: const Icon(Icons.search,
|
||||
size: 20, color: Colors.grey),
|
||||
suffixIcon: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: searchController,
|
||||
builder: (context, value, _) {
|
||||
if (value.text.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.clear,
|
||||
size: 20, color: Colors.grey),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
controller.updateSearch('');
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
hintText: 'Search projects...',
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
appBarColor,
|
||||
appBarColor.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
),
|
||||
|
||||
final projects = controller.filteredProjects;
|
||||
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: _refreshProjects,
|
||||
backgroundColor: Colors.indigo,
|
||||
color: Colors.white,
|
||||
child: projects.isEmpty
|
||||
? _buildEmptyState()
|
||||
: ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
|
||||
// FIX 2: Increased bottom padding for landscape
|
||||
padding: MySpacing.only(
|
||||
left: 8, right: 8, top: 4, bottom: 120),
|
||||
|
||||
itemCount: projects.length,
|
||||
separatorBuilder: (_, __) => MySpacing.height(12),
|
||||
itemBuilder: (_, index) =>
|
||||
_buildProjectCard(projects[index]),
|
||||
// Main content
|
||||
SafeArea(
|
||||
bottom: true,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.xy(8, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 35,
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12),
|
||||
prefixIcon: const Icon(Icons.search,
|
||||
size: 20, color: Colors.grey),
|
||||
suffixIcon:
|
||||
ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: searchController,
|
||||
builder: (context, value, _) {
|
||||
if (value.text.isEmpty)
|
||||
return const SizedBox.shrink();
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.clear,
|
||||
size: 20, color: Colors.grey),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
controller.updateSearch('');
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
hintText: 'Search projects...',
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderSide:
|
||||
BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderSide:
|
||||
BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final projects = controller.filteredProjects;
|
||||
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: _refreshProjects,
|
||||
backgroundColor: Colors.indigo,
|
||||
color: Colors.white,
|
||||
child: projects.isEmpty
|
||||
? _buildEmptyState()
|
||||
: ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: MySpacing.only(
|
||||
left: 8, right: 8, top: 4, bottom: 120),
|
||||
itemCount: projects.length,
|
||||
separatorBuilder: (_, __) => MySpacing.height(12),
|
||||
itemBuilder: (_, index) =>
|
||||
_buildProjectCard(projects[index]),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@ import 'package:on_field_work/model/dailyTaskPlanning/task_action_buttons.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
|
||||
import 'package:on_field_work/helpers/utils/permission_constants.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
|
||||
|
||||
class DailyProgressReportScreen extends StatefulWidget {
|
||||
const DailyProgressReportScreen({super.key});
|
||||
@ -87,124 +89,94 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(72),
|
||||
child: AppBar(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
elevation: 0.5,
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 0,
|
||||
title: Padding(
|
||||
padding: MySpacing.xy(16, 0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () => Get.offNamed('/dashboard'),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MyText.titleLarge(
|
||||
'Daily Progress Report',
|
||||
fontWeight: 700,
|
||||
color: Colors.black,
|
||||
),
|
||||
MySpacing.height(2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
final projectName =
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline,
|
||||
size: 14, color: Colors.grey),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
projectName,
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: CustomAppBar(
|
||||
title: 'Daily Progress Report',
|
||||
backgroundColor: appBarColor,
|
||||
projectName:
|
||||
projectController.selectedProject?.name ?? 'Select Project',
|
||||
onBackPressed: () => Get.offNamed('/dashboard'),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
// Gradient behind content (like EmployeesScreen)
|
||||
Container(
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
appBarColor,
|
||||
appBarColor.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: MyRefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: GetBuilder<DailyTaskController>(
|
||||
init: dailyTaskController,
|
||||
tag: 'daily_progress_report_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(15),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.end,
|
||||
children: [
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
onTap: _openFilterSheet,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
MyText.bodySmall(
|
||||
"Filter",
|
||||
fontWeight: 600,
|
||||
color: Colors.black,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(Icons.tune,
|
||||
size: 20, color: Colors.black),
|
||||
|
||||
],
|
||||
// Main content
|
||||
SafeArea(
|
||||
child: MyRefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: GetBuilder<DailyTaskController>(
|
||||
init: dailyTaskController,
|
||||
tag: 'daily_progress_report_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(15),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
onTap: _openFilterSheet,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
MyText.bodySmall(
|
||||
"Filter",
|
||||
fontWeight: 600,
|
||||
color: Colors.black,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.tune,
|
||||
size: 20, color: Colors.black),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.height(8),
|
||||
Padding(
|
||||
padding: MySpacing.x(8),
|
||||
child: _buildDailyProgressReportTab(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
MySpacing.height(8),
|
||||
Padding(
|
||||
padding: MySpacing.x(8),
|
||||
child: _buildDailyProgressReportTab(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -15,6 +15,8 @@ import 'package:on_field_work/helpers/utils/permission_constants.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
|
||||
import 'package:on_field_work/controller/tenant/service_controller.dart';
|
||||
import 'package:on_field_work/helpers/widgets/tenant/service_selector.dart';
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
|
||||
|
||||
class DailyTaskPlanningScreen extends StatefulWidget {
|
||||
DailyTaskPlanningScreen({super.key});
|
||||
@ -58,128 +60,99 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(72),
|
||||
child: AppBar(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
elevation: 0.5,
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 0,
|
||||
title: Padding(
|
||||
padding: MySpacing.xy(16, 0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () => Get.offNamed('/dashboard'),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MyText.titleLarge(
|
||||
'Daily Task Planning',
|
||||
fontWeight: 700,
|
||||
color: Colors.black,
|
||||
),
|
||||
MySpacing.height(2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
final projectName =
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline,
|
||||
size: 14, color: Colors.grey),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
projectName,
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: CustomAppBar(
|
||||
title: 'Daily Task Planning',
|
||||
backgroundColor: appBarColor,
|
||||
projectName:
|
||||
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: MyRefreshIndicator(
|
||||
onRefresh: () async {
|
||||
final projectId = projectController.selectedProjectId.value;
|
||||
if (projectId.isNotEmpty) {
|
||||
try {
|
||||
await dailyTaskPlanningController.fetchTaskData(
|
||||
projectId,
|
||||
serviceId: serviceController.selectedService?.id,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error refreshing task data: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: MySpacing.x(0),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: MediaQuery.of(context).size.height -
|
||||
kToolbarHeight -
|
||||
MediaQuery.of(context).padding.top,
|
||||
),
|
||||
child: GetBuilder<DailyTaskPlanningController>(
|
||||
init: dailyTaskPlanningController,
|
||||
tag: 'daily_task_Planning_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(10),
|
||||
child: ServiceSelector(
|
||||
controller: serviceController,
|
||||
height: 40,
|
||||
onSelectionChanged: (service) async {
|
||||
final projectId =
|
||||
projectController.selectedProjectId.value;
|
||||
if (projectId.isNotEmpty) {
|
||||
await dailyTaskPlanningController
|
||||
.fetchTaskData(
|
||||
projectId,
|
||||
serviceId: service?.id,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(8),
|
||||
child: dailyProgressReportTab(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: MyRefreshIndicator(
|
||||
onRefresh: () async {
|
||||
final projectId = projectController.selectedProjectId.value;
|
||||
if (projectId.isNotEmpty) {
|
||||
try {
|
||||
// keep previous behavior but now fetchTaskData is lighter (buildings only)
|
||||
await dailyTaskPlanningController.fetchTaskData(
|
||||
projectId,
|
||||
serviceId: serviceController.selectedService?.id,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error refreshing task data: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: MySpacing.x(0),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: MediaQuery.of(context).size.height -
|
||||
kToolbarHeight -
|
||||
MediaQuery.of(context).padding.top,
|
||||
),
|
||||
child: GetBuilder<DailyTaskPlanningController>(
|
||||
init: dailyTaskPlanningController,
|
||||
tag: 'daily_task_Planning_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(10),
|
||||
child: ServiceSelector(
|
||||
controller: serviceController,
|
||||
height: 40,
|
||||
onSelectionChanged: (service) async {
|
||||
final projectId =
|
||||
projectController.selectedProjectId.value;
|
||||
if (projectId.isNotEmpty) {
|
||||
await dailyTaskPlanningController.fetchTaskData(
|
||||
projectId,
|
||||
serviceId:
|
||||
service?.id, // <-- pass selected service
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(8),
|
||||
child: dailyProgressReportTab(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -227,8 +200,7 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
final buildings = dailyTasks
|
||||
.expand((task) => task.buildings)
|
||||
.where((building) =>
|
||||
(building.plannedWork ) > 0 ||
|
||||
(building.completedWork ) > 0)
|
||||
(building.plannedWork) > 0 || (building.completedWork) > 0)
|
||||
.toList();
|
||||
|
||||
if (buildings.isEmpty) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user