feat: Add infrastructure project details and list models
- Implemented ProjectDetailsResponse and ProjectData models for handling project details. - Created ProjectsResponse and ProjectsPageData models for listing infrastructure projects. - Added InfraProjectScreen and InfraProjectDetailsScreen for displaying project information. - Integrated search functionality in InfraProjectScreen to filter projects. - Updated DailyTaskPlanningScreen and DailyProgressReportScreen to accept projectId as a parameter. - Removed unnecessary dependencies and cleaned up code for better maintainability.
This commit is contained in:
parent
03e3e7b5db
commit
3dfa6e5877
@ -0,0 +1,48 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/infra_project/infra_project_list.dart';
|
||||
|
||||
class InfraProjectController extends GetxController {
|
||||
final projects = <ProjectData>[].obs;
|
||||
final isLoading = false.obs;
|
||||
final searchQuery = ''.obs;
|
||||
|
||||
// Filtered list
|
||||
List<ProjectData> get filteredProjects {
|
||||
final q = searchQuery.value.trim().toLowerCase();
|
||||
if (q.isEmpty) return projects;
|
||||
|
||||
return projects.where((p) {
|
||||
return (p.name?.toLowerCase().contains(q) ?? false) ||
|
||||
(p.shortName?.toLowerCase().contains(q) ?? false) ||
|
||||
(p.projectAddress?.toLowerCase().contains(q) ?? false) ||
|
||||
(p.contactPerson?.toLowerCase().contains(q) ?? false);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Fetch Projects
|
||||
Future<void> fetchProjects({int pageNumber = 1, int pageSize = 20}) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
final response = await ApiService.getInfraProjectsList(
|
||||
pageNumber: pageNumber,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
|
||||
if (response != null && response.data != null) {
|
||||
projects.assignAll(response.data!.data ?? []);
|
||||
} else {
|
||||
projects.clear();
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void updateSearch(String query) {
|
||||
searchQuery.value = query;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/infra_project/infra_project_details.dart';
|
||||
|
||||
class InfraProjectDetailsController extends GetxController {
|
||||
final String projectId;
|
||||
|
||||
InfraProjectDetailsController({required this.projectId});
|
||||
|
||||
var isLoading = true.obs;
|
||||
var projectDetails = Rxn<ProjectData>();
|
||||
var errorMessage = ''.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchProjectDetails();
|
||||
}
|
||||
|
||||
Future<void> fetchProjectDetails() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
final response = await ApiService.getInfraProjectDetails(projectId: projectId);
|
||||
|
||||
if (response != null && response.success == true && response.data != null) {
|
||||
projectDetails.value = response.data;
|
||||
isLoading.value = false;
|
||||
|
||||
} else {
|
||||
errorMessage.value = response?.message ?? "Failed to load project details";
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = "Error fetching project details: $e";
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -159,4 +159,8 @@ class ApiEndpoints {
|
||||
static const String addJobComment = "/ServiceProject/job/add/comment";
|
||||
|
||||
static const String getJobCommentList = "/ServiceProject/job/comment/list";
|
||||
|
||||
// Infra Project Module API Endpoints
|
||||
static const String getInfraProjectsList = "/project/list";
|
||||
static const String getInfraProjectDetail = "/project/details";
|
||||
}
|
||||
|
||||
@ -43,6 +43,8 @@ import 'package:on_field_work/model/service_project/service_project_branches_mod
|
||||
import 'package:on_field_work/model/service_project/job_status_response.dart';
|
||||
import 'package:on_field_work/model/service_project/job_comments.dart';
|
||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||
import 'package:on_field_work/model/infra_project/infra_project_list.dart';
|
||||
import 'package:on_field_work/model/infra_project/infra_project_details.dart';
|
||||
|
||||
class ApiService {
|
||||
static const bool enableLogs = true;
|
||||
@ -314,6 +316,77 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
// Infra Project Module APIs
|
||||
|
||||
/// ================================
|
||||
/// GET INFRA PROJECT DETAILS
|
||||
/// ================================
|
||||
static Future<ProjectDetailsResponse?> getInfraProjectDetails({
|
||||
required String projectId,
|
||||
}) async {
|
||||
final endpoint = "${ApiEndpoints.getInfraProjectDetail}/$projectId";
|
||||
|
||||
try {
|
||||
final response = await _getRequest(endpoint);
|
||||
|
||||
if (response == null) {
|
||||
_log("getInfraProjectDetails: No response from server",
|
||||
level: LogLevel.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
final parsedJson =
|
||||
_parseResponseForAllData(response, label: "InfraProjectDetails");
|
||||
|
||||
if (parsedJson == null) return null;
|
||||
|
||||
return ProjectDetailsResponse.fromJson(parsedJson);
|
||||
} catch (e, stack) {
|
||||
_log("Exception in getInfraProjectDetails: $e\n$stack",
|
||||
level: LogLevel.error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// ================================
|
||||
/// GET INFRA PROJECTS LIST
|
||||
/// ================================
|
||||
static Future<ProjectsResponse?> getInfraProjectsList({
|
||||
int pageSize = 20,
|
||||
int pageNumber = 1,
|
||||
String searchString = "",
|
||||
}) async {
|
||||
final queryParams = {
|
||||
"pageSize": pageSize.toString(),
|
||||
"pageNumber": pageNumber.toString(),
|
||||
"searchString": searchString,
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await _getRequest(
|
||||
ApiEndpoints.getInfraProjectsList,
|
||||
queryParams: queryParams,
|
||||
);
|
||||
|
||||
if (response == null) {
|
||||
_log("getInfraProjectsList: No response from server",
|
||||
level: LogLevel.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
final parsedJson =
|
||||
_parseResponseForAllData(response, label: "InfraProjectsList");
|
||||
|
||||
if (parsedJson == null) return null;
|
||||
|
||||
return ProjectsResponse.fromJson(parsedJson);
|
||||
} catch (e, stack) {
|
||||
_log("Exception in getInfraProjectsList: $e\n$stack",
|
||||
level: LogLevel.error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<JobCommentResponse?> getJobCommentList({
|
||||
required String jobTicketId,
|
||||
int pageNumber = 1,
|
||||
|
||||
@ -165,7 +165,7 @@ class MenuItems {
|
||||
static const String serviceProjects = "7faddfe7-994b-4712-91c2-32ba44129d9b";
|
||||
|
||||
/// Infrastructure Projects
|
||||
static const String infraProjects = "d3b5f3e3-3f7c-4f2b-99f1-1c9e4b8e6c2a";
|
||||
static const String infraProjects = "5fab4b88-c9a0-417b-aca2-130980fdb0cf";
|
||||
}
|
||||
|
||||
/// Contains all job status IDs used across the application.
|
||||
|
||||
@ -8,6 +8,7 @@ class PillTabBar extends StatelessWidget {
|
||||
final Color indicatorColor;
|
||||
final double height;
|
||||
final ValueChanged<int>? onTap;
|
||||
|
||||
const PillTabBar({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
@ -21,6 +22,10 @@ class PillTabBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Dynamic horizontal padding between tabs
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final tabSpacing = (screenWidth / (tabs.length * 12)).clamp(8.0, 24.0);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Container(
|
||||
@ -43,19 +48,35 @@ class PillTabBar extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(height / 2),
|
||||
),
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicatorPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||
indicatorPadding: EdgeInsets.symmetric(
|
||||
horizontal: tabSpacing / 2,
|
||||
vertical: 4,
|
||||
),
|
||||
labelColor: selectedColor,
|
||||
unselectedLabelColor: unselectedColor,
|
||||
labelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
fontSize: 13,
|
||||
),
|
||||
unselectedLabelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 15,
|
||||
fontSize: 13,
|
||||
),
|
||||
tabs: tabs.map((text) => Tab(text: text)).toList(),
|
||||
tabs: tabs
|
||||
.map(
|
||||
(text) => Tab(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: tabSpacing),
|
||||
child: Text(
|
||||
text,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onTap: onTap,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
222
lib/model/infra_project/infra_project_details.dart
Normal file
222
lib/model/infra_project/infra_project_details.dart
Normal file
@ -0,0 +1,222 @@
|
||||
class ProjectDetailsResponse {
|
||||
final bool? success;
|
||||
final String? message;
|
||||
final ProjectData? data;
|
||||
final dynamic errors;
|
||||
final int? statusCode;
|
||||
final DateTime? timestamp;
|
||||
|
||||
ProjectDetailsResponse({
|
||||
this.success,
|
||||
this.message,
|
||||
this.data,
|
||||
this.errors,
|
||||
this.statusCode,
|
||||
this.timestamp,
|
||||
});
|
||||
|
||||
factory ProjectDetailsResponse.fromJson(Map<String, dynamic> json) {
|
||||
return ProjectDetailsResponse(
|
||||
success: json['success'] as bool?,
|
||||
message: json['message'] as String?,
|
||||
data: json['data'] != null ? ProjectData.fromJson(json['data']) : null,
|
||||
errors: json['errors'],
|
||||
statusCode: json['statusCode'] as int?,
|
||||
timestamp: json['timestamp'] != null
|
||||
? DateTime.tryParse(json['timestamp'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'success': success,
|
||||
'message': message,
|
||||
'data': data?.toJson(),
|
||||
'errors': errors,
|
||||
'statusCode': statusCode,
|
||||
'timestamp': timestamp?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ProjectData {
|
||||
final String? id;
|
||||
final String? name;
|
||||
final String? shortName;
|
||||
final String? projectAddress;
|
||||
final String? contactPerson;
|
||||
final DateTime? startDate;
|
||||
final DateTime? endDate;
|
||||
final ProjectStatus? projectStatus;
|
||||
final Promoter? promoter;
|
||||
final Pmc? pmc;
|
||||
|
||||
ProjectData({
|
||||
this.id,
|
||||
this.name,
|
||||
this.shortName,
|
||||
this.projectAddress,
|
||||
this.contactPerson,
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.projectStatus,
|
||||
this.promoter,
|
||||
this.pmc,
|
||||
});
|
||||
|
||||
factory ProjectData.fromJson(Map<String, dynamic> json) {
|
||||
return ProjectData(
|
||||
id: json['id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
shortName: json['shortName'] as String?,
|
||||
projectAddress: json['projectAddress'] as String?,
|
||||
contactPerson: json['contactPerson'] as String?,
|
||||
startDate: json['startDate'] != null
|
||||
? DateTime.tryParse(json['startDate'])
|
||||
: null,
|
||||
endDate: json['endDate'] != null
|
||||
? DateTime.tryParse(json['endDate'])
|
||||
: null,
|
||||
projectStatus: json['projectStatus'] != null
|
||||
? ProjectStatus.fromJson(json['projectStatus'])
|
||||
: null,
|
||||
promoter: json['promoter'] != null
|
||||
? Promoter.fromJson(json['promoter'])
|
||||
: null,
|
||||
pmc: json['pmc'] != null ? Pmc.fromJson(json['pmc']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'shortName': shortName,
|
||||
'projectAddress': projectAddress,
|
||||
'contactPerson': contactPerson,
|
||||
'startDate': startDate?.toIso8601String(),
|
||||
'endDate': endDate?.toIso8601String(),
|
||||
'projectStatus': projectStatus?.toJson(),
|
||||
'promoter': promoter?.toJson(),
|
||||
'pmc': pmc?.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ProjectStatus {
|
||||
final String? id;
|
||||
final String? status;
|
||||
|
||||
ProjectStatus({this.id, this.status});
|
||||
|
||||
factory ProjectStatus.fromJson(Map<String, dynamic> json) {
|
||||
return ProjectStatus(
|
||||
id: json['id'] as String?,
|
||||
status: json['status'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'status': status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Promoter {
|
||||
final String? id;
|
||||
final String? name;
|
||||
final String? email;
|
||||
final String? contactPerson;
|
||||
final String? address;
|
||||
final String? gstNumber;
|
||||
final String? contactNumber;
|
||||
final int? sprid;
|
||||
|
||||
Promoter({
|
||||
this.id,
|
||||
this.name,
|
||||
this.email,
|
||||
this.contactPerson,
|
||||
this.address,
|
||||
this.gstNumber,
|
||||
this.contactNumber,
|
||||
this.sprid,
|
||||
});
|
||||
|
||||
factory Promoter.fromJson(Map<String, dynamic> json) {
|
||||
return Promoter(
|
||||
id: json['id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
email: json['email'] as String?,
|
||||
contactPerson: json['contactPerson'] as String?,
|
||||
address: json['address'] as String?,
|
||||
gstNumber: json['gstNumber'] as String?,
|
||||
contactNumber: json['contactNumber'] as String?,
|
||||
sprid: json['sprid'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'email': email,
|
||||
'contactPerson': contactPerson,
|
||||
'address': address,
|
||||
'gstNumber': gstNumber,
|
||||
'contactNumber': contactNumber,
|
||||
'sprid': sprid,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Pmc {
|
||||
final String? id;
|
||||
final String? name;
|
||||
final String? email;
|
||||
final String? contactPerson;
|
||||
final String? address;
|
||||
final String? gstNumber;
|
||||
final String? contactNumber;
|
||||
final int? sprid;
|
||||
|
||||
Pmc({
|
||||
this.id,
|
||||
this.name,
|
||||
this.email,
|
||||
this.contactPerson,
|
||||
this.address,
|
||||
this.gstNumber,
|
||||
this.contactNumber,
|
||||
this.sprid,
|
||||
});
|
||||
|
||||
factory Pmc.fromJson(Map<String, dynamic> json) {
|
||||
return Pmc(
|
||||
id: json['id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
email: json['email'] as String?,
|
||||
contactPerson: json['contactPerson'] as String?,
|
||||
address: json['address'] as String?,
|
||||
gstNumber: json['gstNumber'] as String?,
|
||||
contactNumber: json['contactNumber'] as String?,
|
||||
sprid: json['sprid'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'email': email,
|
||||
'contactPerson': contactPerson,
|
||||
'address': address,
|
||||
'gstNumber': gstNumber,
|
||||
'contactNumber': contactNumber,
|
||||
'sprid': sprid,
|
||||
};
|
||||
}
|
||||
}
|
||||
138
lib/model/infra_project/infra_project_list.dart
Normal file
138
lib/model/infra_project/infra_project_list.dart
Normal file
@ -0,0 +1,138 @@
|
||||
// Root Response Model
|
||||
class ProjectsResponse {
|
||||
final bool? success;
|
||||
final String? message;
|
||||
final ProjectsPageData? data;
|
||||
final dynamic errors;
|
||||
final int? statusCode;
|
||||
final String? timestamp;
|
||||
|
||||
ProjectsResponse({
|
||||
this.success,
|
||||
this.message,
|
||||
this.data,
|
||||
this.errors,
|
||||
this.statusCode,
|
||||
this.timestamp,
|
||||
});
|
||||
|
||||
factory ProjectsResponse.fromJson(Map<String, dynamic> json) {
|
||||
return ProjectsResponse(
|
||||
success: json['success'],
|
||||
message: json['message'],
|
||||
data: json['data'] != null
|
||||
? ProjectsPageData.fromJson(json['data'])
|
||||
: null,
|
||||
errors: json['errors'],
|
||||
statusCode: json['statusCode'],
|
||||
timestamp: json['timestamp'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'success': success,
|
||||
'message': message,
|
||||
'data': data?.toJson(),
|
||||
'errors': errors,
|
||||
'statusCode': statusCode,
|
||||
'timestamp': timestamp,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination + Data List
|
||||
class ProjectsPageData {
|
||||
final int? currentPage;
|
||||
final int? totalPages;
|
||||
final int? totalEntites;
|
||||
final List<ProjectData>? data;
|
||||
|
||||
ProjectsPageData({
|
||||
this.currentPage,
|
||||
this.totalPages,
|
||||
this.totalEntites,
|
||||
this.data,
|
||||
});
|
||||
|
||||
factory ProjectsPageData.fromJson(Map<String, dynamic> json) {
|
||||
return ProjectsPageData(
|
||||
currentPage: json['currentPage'],
|
||||
totalPages: json['totalPages'],
|
||||
totalEntites: json['totalEntites'],
|
||||
data: (json['data'] as List<dynamic>?)
|
||||
?.map((e) => ProjectData.fromJson(e))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'currentPage': currentPage,
|
||||
'totalPages': totalPages,
|
||||
'totalEntites': totalEntites,
|
||||
'data': data?.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Individual Project Model
|
||||
class ProjectData {
|
||||
final String? id;
|
||||
final String? name;
|
||||
final String? shortName;
|
||||
final String? projectAddress;
|
||||
final String? contactPerson;
|
||||
final String? startDate;
|
||||
final String? endDate;
|
||||
final String? projectStatusId;
|
||||
final int? teamSize;
|
||||
final double? completedWork;
|
||||
final double? plannedWork;
|
||||
|
||||
ProjectData({
|
||||
this.id,
|
||||
this.name,
|
||||
this.shortName,
|
||||
this.projectAddress,
|
||||
this.contactPerson,
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.projectStatusId,
|
||||
this.teamSize,
|
||||
this.completedWork,
|
||||
this.plannedWork,
|
||||
});
|
||||
|
||||
factory ProjectData.fromJson(Map<String, dynamic> json) {
|
||||
return ProjectData(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
shortName: json['shortName'],
|
||||
projectAddress: json['projectAddress'],
|
||||
contactPerson: json['contactPerson'],
|
||||
startDate: json['startDate'],
|
||||
endDate: json['endDate'],
|
||||
projectStatusId: json['projectStatusId'],
|
||||
teamSize: json['teamSize'],
|
||||
completedWork: (json['completedWork'] as num?)?.toDouble(),
|
||||
plannedWork: (json['plannedWork'] as num?)?.toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'shortName': shortName,
|
||||
'projectAddress': projectAddress,
|
||||
'contactPerson': contactPerson,
|
||||
'startDate': startDate,
|
||||
'endDate': endDate,
|
||||
'projectStatusId': projectStatusId,
|
||||
'teamSize': teamSize,
|
||||
'completedWork': completedWork,
|
||||
'plannedWork': plannedWork,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -11,8 +11,6 @@ import 'package:on_field_work/view/error_pages/error_404_screen.dart';
|
||||
import 'package:on_field_work/view/error_pages/error_500_screen.dart';
|
||||
import 'package:on_field_work/view/dashboard/dashboard_screen.dart';
|
||||
import 'package:on_field_work/view/Attendence/attendance_screen.dart';
|
||||
import 'package:on_field_work/view/taskPlanning/daily_task_planning.dart';
|
||||
import 'package:on_field_work/view/taskPlanning/daily_progress_report.dart';
|
||||
import 'package:on_field_work/view/employees/employees_screen.dart';
|
||||
import 'package:on_field_work/view/auth/login_option_screen.dart';
|
||||
import 'package:on_field_work/view/auth/mpin_screen.dart';
|
||||
@ -25,6 +23,8 @@ import 'package:on_field_work/view/finance/finance_screen.dart';
|
||||
import 'package:on_field_work/view/finance/advance_payment_screen.dart';
|
||||
import 'package:on_field_work/view/finance/payment_request_screen.dart';
|
||||
import 'package:on_field_work/view/service_project/service_project_screen.dart';
|
||||
import 'package:on_field_work/view/infraProject/infra_project_screen.dart';
|
||||
|
||||
class AuthMiddleware extends GetMiddleware {
|
||||
@override
|
||||
RouteSettings? redirect(String? route) {
|
||||
@ -70,15 +70,6 @@ getPageRoute() {
|
||||
name: '/dashboard/employees',
|
||||
page: () => EmployeesScreen(),
|
||||
middlewares: [AuthMiddleware()]),
|
||||
// Daily Task Planning
|
||||
GetPage(
|
||||
name: '/dashboard/daily-task-Planning',
|
||||
page: () => DailyTaskPlanningScreen(),
|
||||
middlewares: [AuthMiddleware()]),
|
||||
GetPage(
|
||||
name: '/dashboard/daily-task-progress',
|
||||
page: () => DailyProgressReportScreen(),
|
||||
middlewares: [AuthMiddleware()]),
|
||||
GetPage(
|
||||
name: '/dashboard/directory-main-page',
|
||||
page: () => DirectoryMainScreen(),
|
||||
@ -102,6 +93,12 @@ getPageRoute() {
|
||||
name: '/dashboard/payment-request',
|
||||
page: () => PaymentRequestMainScreen(),
|
||||
middlewares: [AuthMiddleware()]),
|
||||
// Infrastructure Projects
|
||||
GetPage(
|
||||
name: '/dashboard/infra-projects',
|
||||
page: () => InfraProjectScreen(),
|
||||
middlewares: [AuthMiddleware()]),
|
||||
|
||||
// Authentication
|
||||
GetPage(name: '/auth/login', page: () => LoginScreen()),
|
||||
GetPage(name: '/auth/login-option', page: () => LoginOptionScreen()),
|
||||
|
||||
377
lib/view/infraProject/infra_project_details_screen.dart
Normal file
377
lib/view/infraProject/infra_project_details_screen.dart
Normal file
@ -0,0 +1,377 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
import 'package:on_field_work/helpers/widgets/pill_tab_bar.dart';
|
||||
import 'package:on_field_work/helpers/utils/permission_constants.dart';
|
||||
import 'package:on_field_work/helpers/utils/launcher_utils.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
|
||||
|
||||
import 'package:on_field_work/controller/dynamicMenu/dynamic_menu_controller.dart';
|
||||
import 'package:on_field_work/controller/infra_project/infra_project_screen_details_controller.dart';
|
||||
import 'package:on_field_work/view/taskPlanning/daily_progress_report.dart';
|
||||
import 'package:on_field_work/view/taskPlanning/daily_task_planning.dart';
|
||||
|
||||
class InfraProjectDetailsScreen extends StatefulWidget {
|
||||
final String projectId;
|
||||
final String? projectName;
|
||||
|
||||
const InfraProjectDetailsScreen({
|
||||
super.key,
|
||||
required this.projectId,
|
||||
this.projectName,
|
||||
});
|
||||
|
||||
@override
|
||||
State<InfraProjectDetailsScreen> createState() =>
|
||||
_InfraProjectDetailsScreenState();
|
||||
}
|
||||
|
||||
class _InfraProjectDetailsScreenState extends State<InfraProjectDetailsScreen>
|
||||
with SingleTickerProviderStateMixin, UIMixin {
|
||||
late final TabController _tabController;
|
||||
final DynamicMenuController menuController =
|
||||
Get.find<DynamicMenuController>();
|
||||
final List<_InfraTab> _tabs = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_prepareTabs();
|
||||
}
|
||||
|
||||
void _prepareTabs() {
|
||||
// Profile tab is always added
|
||||
_tabs.add(_InfraTab(name: "Profile", view: _buildProfileTab()));
|
||||
|
||||
final allowedMenu = menuController.menuItems.where((m) => m.available);
|
||||
|
||||
if (allowedMenu.any((m) => m.id == MenuItems.dailyTaskPlanning)) {
|
||||
_tabs.add(
|
||||
_InfraTab(
|
||||
name: "Task Planning",
|
||||
view: DailyTaskPlanningScreen(projectId: widget.projectId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (allowedMenu.any((m) => m.id == MenuItems.dailyProgressReport)) {
|
||||
_tabs.add(
|
||||
_InfraTab(
|
||||
name: "Task Progress",
|
||||
view: DailyProgressReportScreen(projectId: widget.projectId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_tabController = TabController(length: _tabs.length, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildProfileTab() {
|
||||
final controller =
|
||||
Get.put(InfraProjectDetailsController(projectId: widget.projectId));
|
||||
|
||||
return Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.errorMessage.isNotEmpty) {
|
||||
return Center(child: Text(controller.errorMessage.value));
|
||||
}
|
||||
|
||||
final data = controller.projectDetails.value;
|
||||
if (data == null) {
|
||||
return const Center(child: Text("No project data available"));
|
||||
}
|
||||
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: controller.fetchProjectDetails,
|
||||
backgroundColor: Colors.indigo,
|
||||
color: Colors.white,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildHeaderCard(data),
|
||||
MySpacing.height(16),
|
||||
_buildProjectInfoSection(data),
|
||||
if (data.promoter != null) MySpacing.height(12),
|
||||
if (data.promoter != null) _buildPromoterInfo(data.promoter!),
|
||||
if (data.pmc != null) MySpacing.height(12),
|
||||
if (data.pmc != null) _buildPMCInfo(data.pmc!),
|
||||
MySpacing.height(40),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildHeaderCard(dynamic data) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black12,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline, size: 35),
|
||||
MySpacing.width(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleMedium(data.name ?? "-", fontWeight: 700),
|
||||
MySpacing.height(6),
|
||||
MyText.bodySmall(data.shortName ?? "-", fontWeight: 500),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProjectInfoSection(dynamic data) {
|
||||
return _buildSectionCard(
|
||||
title: 'Project Information',
|
||||
titleIcon: Icons.info_outline,
|
||||
children: [
|
||||
_buildDetailRow(
|
||||
icon: Icons.location_on_outlined,
|
||||
label: 'Address',
|
||||
value: data.projectAddress ?? "-"),
|
||||
_buildDetailRow(
|
||||
icon: Icons.calendar_today_outlined,
|
||||
label: 'Start Date',
|
||||
value: data.startDate != null
|
||||
? DateFormat('d/M/yyyy').format(data.startDate!)
|
||||
: "-"),
|
||||
_buildDetailRow(
|
||||
icon: Icons.calendar_today_outlined,
|
||||
label: 'End Date',
|
||||
value: data.endDate != null
|
||||
? DateFormat('d/M/yyyy').format(data.endDate!)
|
||||
: "-"),
|
||||
_buildDetailRow(
|
||||
icon: Icons.flag_outlined,
|
||||
label: 'Status',
|
||||
value: data.projectStatus?.status ?? "-"),
|
||||
_buildDetailRow(
|
||||
icon: Icons.person_outline,
|
||||
label: 'Contact Person',
|
||||
value: data.contactPerson ?? "-",
|
||||
isActionable: true,
|
||||
onTap: () {
|
||||
if (data.contactPerson != null) {
|
||||
LauncherUtils.launchPhone(data.contactPerson!);
|
||||
}
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPromoterInfo(dynamic promoter) {
|
||||
return _buildSectionCard(
|
||||
title: 'Promoter Information',
|
||||
titleIcon: Icons.business_outlined,
|
||||
children: [
|
||||
_buildDetailRow(
|
||||
icon: Icons.person_outline,
|
||||
label: 'Name',
|
||||
value: promoter.name ?? "-"),
|
||||
_buildDetailRow(
|
||||
icon: Icons.phone_outlined,
|
||||
label: 'Contact',
|
||||
value: promoter.contactNumber ?? "-",
|
||||
isActionable: true,
|
||||
onTap: () =>
|
||||
LauncherUtils.launchPhone(promoter.contactNumber ?? "")),
|
||||
_buildDetailRow(
|
||||
icon: Icons.email_outlined,
|
||||
label: 'Email',
|
||||
value: promoter.email ?? "-",
|
||||
isActionable: true,
|
||||
onTap: () => LauncherUtils.launchEmail(promoter.email ?? "")),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPMCInfo(dynamic pmc) {
|
||||
return _buildSectionCard(
|
||||
title: 'PMC Information',
|
||||
titleIcon: Icons.engineering_outlined,
|
||||
children: [
|
||||
_buildDetailRow(
|
||||
icon: Icons.person_outline, label: 'Name', value: pmc.name ?? "-"),
|
||||
_buildDetailRow(
|
||||
icon: Icons.phone_outlined,
|
||||
label: 'Contact',
|
||||
value: pmc.contactNumber ?? "-",
|
||||
isActionable: true,
|
||||
onTap: () => LauncherUtils.launchPhone(pmc.contactNumber ?? "")),
|
||||
_buildDetailRow(
|
||||
icon: Icons.email_outlined,
|
||||
label: 'Email',
|
||||
value: pmc.email ?? "-",
|
||||
isActionable: true,
|
||||
onTap: () => LauncherUtils.launchEmail(pmc.email ?? "")),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String value,
|
||||
VoidCallback? onTap,
|
||||
bool isActionable = false,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: InkWell(
|
||||
onTap: isActionable ? onTap : null,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8), child: Icon(icon, size: 20)),
|
||||
MySpacing.width(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodySmall(
|
||||
label,
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: 500,
|
||||
),
|
||||
MySpacing.height(4),
|
||||
MyText.bodyMedium(
|
||||
value,
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
color: isActionable ? Colors.blueAccent : Colors.black87,
|
||||
decoration: isActionable
|
||||
? TextDecoration.underline
|
||||
: TextDecoration.none,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionCard({
|
||||
required String title,
|
||||
required IconData titleIcon,
|
||||
required List<Widget> children,
|
||||
}) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black12,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(titleIcon, size: 20),
|
||||
MySpacing.width(8),
|
||||
MyText.bodyLarge(
|
||||
title,
|
||||
fontSize: 16,
|
||||
fontWeight: 700,
|
||||
color: Colors.black87,
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(8),
|
||||
const Divider(),
|
||||
...children,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF1F1F1),
|
||||
appBar: CustomAppBar(
|
||||
title: "Infra Projects",
|
||||
onBackPressed: () => Get.back(),
|
||||
projectName: widget.projectName,
|
||||
backgroundColor: appBarColor,
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [appBarColor, appBarColor.withOpacity(0)],
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: Column(
|
||||
children: [
|
||||
PillTabBar(
|
||||
controller: _tabController,
|
||||
tabs: _tabs.map((e) => e.name).toList(),
|
||||
selectedColor: contentTheme.primary,
|
||||
unselectedColor: Colors.grey.shade600,
|
||||
indicatorColor: contentTheme.primary,
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: _tabs.map((e) => e.view).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// INTERNAL MODEL
|
||||
class _InfraTab {
|
||||
final String name;
|
||||
final Widget view;
|
||||
|
||||
_InfraTab({required this.name, required this.view});
|
||||
}
|
||||
@ -1,120 +1,272 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
|
||||
|
||||
import 'package:on_field_work/controller/infra_project/infra_project_screen_controller.dart';
|
||||
import 'package:on_field_work/model/infra_project/infra_project_list.dart';
|
||||
|
||||
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
|
||||
import 'package:on_field_work/helpers/widgets/pill_tab_bar.dart';
|
||||
|
||||
import 'package:on_field_work/controller/dynamicMenu/dynamic_menu_controller.dart';
|
||||
import 'package:on_field_work/helpers/utils/permission_constants.dart';
|
||||
import 'package:on_field_work/view/infraProject/infra_project_details_screen.dart';
|
||||
|
||||
// === Your 3 Screens ===
|
||||
import 'package:on_field_work/view/taskPlanning/daily_progress_report.dart';
|
||||
import 'package:on_field_work/view/taskPlanning/daily_task_planning.dart';
|
||||
|
||||
class InfraProjectsMainScreen extends StatefulWidget {
|
||||
const InfraProjectsMainScreen({super.key});
|
||||
class InfraProjectScreen extends StatefulWidget {
|
||||
const InfraProjectScreen({super.key});
|
||||
|
||||
@override
|
||||
State<InfraProjectsMainScreen> createState() =>
|
||||
_InfraProjectsMainScreenState();
|
||||
State<InfraProjectScreen> createState() => _InfraProjectScreenState();
|
||||
}
|
||||
|
||||
class _InfraProjectsMainScreenState extends State<InfraProjectsMainScreen>
|
||||
with SingleTickerProviderStateMixin, UIMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
final DynamicMenuController menuController = Get.find<DynamicMenuController>();
|
||||
|
||||
// Final tab list after filtering
|
||||
final List<_InfraTab> _tabs = [];
|
||||
class _InfraProjectScreenState extends State<InfraProjectScreen> with UIMixin {
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
final InfraProjectController controller = Get.put(InfraProjectController());
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_prepareTabs();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
controller.fetchProjects();
|
||||
});
|
||||
|
||||
searchController.addListener(() {
|
||||
controller.updateSearch(searchController.text);
|
||||
});
|
||||
}
|
||||
|
||||
void _prepareTabs() {
|
||||
// Use the same permission logic used in your dashboard_cards
|
||||
final allowedMenu = menuController.menuItems.where((m) => m.available);
|
||||
Future<void> _refreshProjects() async {
|
||||
await controller.fetchProjects();
|
||||
}
|
||||
|
||||
if (allowedMenu.any((m) => m.id == MenuItems.dailyTaskPlanning)) {
|
||||
_tabs.add(
|
||||
_InfraTab(
|
||||
name: "Task Planning",
|
||||
view: DailyTaskPlanningScreen(),
|
||||
// ---------------------------------------------------------------------------
|
||||
// PROJECT CARD
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildProjectCard(ProjectData project) {
|
||||
return Card(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
shadowColor: Colors.indigo.withOpacity(0.10),
|
||||
color: Colors.white,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
onTap: () {
|
||||
Get.to(() => InfraProjectDetailsScreen(projectId: project.id!, projectName: project.name));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// TOP: Name + Status
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyText.titleMedium(
|
||||
project.name ?? "-",
|
||||
fontWeight: 700,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
],
|
||||
),
|
||||
|
||||
MySpacing.height(10),
|
||||
|
||||
if (project.shortName != null)
|
||||
_buildDetailRow(
|
||||
Icons.badge_outlined,
|
||||
Colors.teal,
|
||||
"Short Name: ${project.shortName}",
|
||||
),
|
||||
|
||||
MySpacing.height(8),
|
||||
|
||||
if (project.projectAddress != null)
|
||||
_buildDetailRow(
|
||||
Icons.location_on_outlined,
|
||||
Colors.orange,
|
||||
"Address: ${project.projectAddress}",
|
||||
),
|
||||
|
||||
MySpacing.height(8),
|
||||
|
||||
if (project.contactPerson != null)
|
||||
_buildDetailRow(
|
||||
Icons.phone,
|
||||
Colors.green,
|
||||
"Contact: ${project.contactPerson}",
|
||||
),
|
||||
|
||||
MySpacing.height(12),
|
||||
|
||||
if (project.teamSize != null)
|
||||
_buildDetailRow(
|
||||
Icons.group,
|
||||
Colors.indigo,
|
||||
"Team Size: ${project.teamSize}",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (allowedMenu.any((m) => m.id == MenuItems.dailyProgressReport)) {
|
||||
_tabs.add(
|
||||
_InfraTab(
|
||||
name: "Task Progress",
|
||||
view: DailyProgressReportScreen(),
|
||||
Widget _buildDetailRow(IconData icon, Color color, String value) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: color),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
value,
|
||||
color: Colors.grey[900],
|
||||
fontWeight: 500,
|
||||
fontSize: 13,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EMPTY STATE
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.work_outline, size: 60, color: Colors.grey),
|
||||
MySpacing.height(18),
|
||||
MyText.titleMedium(
|
||||
'No matching projects found.',
|
||||
fontWeight: 600,
|
||||
color: Colors.grey,
|
||||
),
|
||||
MySpacing.height(10),
|
||||
MyText.bodySmall(
|
||||
'Try adjusting your filters or refresh.',
|
||||
color: Colors.grey,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_tabController = TabController(length: _tabs.length, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MAIN BUILD
|
||||
// ---------------------------------------------------------------------------
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF1F1F1),
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: CustomAppBar(
|
||||
title: "Infra Projects",
|
||||
onBackPressed: () => Get.back(),
|
||||
projectName: 'All Infra Projects',
|
||||
backgroundColor: appBarColor,
|
||||
onBackPressed: () => Get.toNamed('/dashboard'),
|
||||
),
|
||||
|
||||
body: Stack(
|
||||
children: [
|
||||
// Top faded gradient
|
||||
// GRADIENT BACKDROP
|
||||
Container(
|
||||
height: 50,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
appBarColor,
|
||||
appBarColor.withOpacity(0.0),
|
||||
appBarColor.withOpacity(0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: Column(
|
||||
children: [
|
||||
// PILL TABS
|
||||
PillTabBar(
|
||||
controller: _tabController,
|
||||
tabs: _tabs.map((e) => e.name).toList(),
|
||||
selectedColor: contentTheme.primary,
|
||||
unselectedColor: Colors.grey.shade600,
|
||||
indicatorColor: contentTheme.primary,
|
||||
// SEARCH BAR
|
||||
Padding(
|
||||
padding: MySpacing.xy(8, 8),
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// TAB CONTENT
|
||||
// LIST
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: _tabs.map((e) => e.view).toList(),
|
||||
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: 100),
|
||||
itemCount: projects.length,
|
||||
separatorBuilder: (_, __) => MySpacing.height(12),
|
||||
itemBuilder: (_, index) =>
|
||||
_buildProjectCard(projects[index]),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -124,11 +276,3 @@ class _InfraProjectsMainScreenState extends State<InfraProjectsMainScreen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// INTERNAL MODEL
|
||||
class _InfraTab {
|
||||
final String name;
|
||||
final Widget view;
|
||||
|
||||
_InfraTab({required this.name, required this.view});
|
||||
}
|
||||
|
||||
@ -17,11 +17,10 @@ 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});
|
||||
final String projectId;
|
||||
const DailyProgressReportScreen({super.key, required this.projectId});
|
||||
|
||||
@override
|
||||
State<DailyProgressReportScreen> createState() =>
|
||||
@ -64,21 +63,15 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
}
|
||||
}
|
||||
});
|
||||
final initialProjectId = projectController.selectedProjectId.value;
|
||||
|
||||
// ✅ Use projectId passed from parent instead of global selectedProjectId
|
||||
final initialProjectId = widget.projectId;
|
||||
if (initialProjectId.isNotEmpty) {
|
||||
dailyTaskController.selectedProjectId = initialProjectId;
|
||||
dailyTaskController.fetchTaskData(initialProjectId);
|
||||
}
|
||||
|
||||
// Update when project changes
|
||||
ever<String>(projectController.selectedProjectId, (newProjectId) async {
|
||||
if (newProjectId.isNotEmpty &&
|
||||
newProjectId != dailyTaskController.selectedProjectId) {
|
||||
dailyTaskController.selectedProjectId = newProjectId;
|
||||
await dailyTaskController.fetchTaskData(newProjectId);
|
||||
dailyTaskController.update(['daily_progress_report_controller']);
|
||||
}
|
||||
});
|
||||
// ❌ Removed the ever<ProjectController> block to keep it independent
|
||||
}
|
||||
|
||||
@override
|
||||
@ -89,35 +82,9 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Main content
|
||||
SafeArea(
|
||||
child: MyRefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
@ -182,7 +149,6 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
}
|
||||
|
||||
Future<void> _openFilterSheet() async {
|
||||
// ✅ Fetch filter data first
|
||||
if (dailyTaskController.taskFilterData == null) {
|
||||
await dailyTaskController
|
||||
.fetchTaskFilter(dailyTaskController.selectedProjectId ?? '');
|
||||
@ -279,32 +245,27 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
final isLoading = dailyTaskController.isLoading.value;
|
||||
final groupedTasks = dailyTaskController.groupedDailyTasks;
|
||||
|
||||
// 🟡 Show loading skeleton on first load
|
||||
if (isLoading && dailyTaskController.currentPage == 1) {
|
||||
return SkeletonLoaders.dailyProgressReportSkeletonLoader();
|
||||
}
|
||||
|
||||
// ⚪ No data available
|
||||
if (groupedTasks.isEmpty) {
|
||||
return Center(
|
||||
child: MyText.bodySmall(
|
||||
"No Progress Report Found",
|
||||
"No Progress Report Found for selected filters.",
|
||||
fontWeight: 600,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 🔽 Sort all date keys by descending (latest first)
|
||||
final sortedDates = groupedTasks.keys.toList()
|
||||
..sort((a, b) => b.compareTo(a));
|
||||
|
||||
// 🔹 Auto expand if only one date present
|
||||
if (sortedDates.length == 1 &&
|
||||
!dailyTaskController.expandedDates.contains(sortedDates[0])) {
|
||||
dailyTaskController.expandedDates.add(sortedDates[0]);
|
||||
}
|
||||
|
||||
// 🧱 Return a scrollable column of cards
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -323,7 +284,6 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 🗓️ Date Header
|
||||
GestureDetector(
|
||||
onTap: () => dailyTaskController.toggleDate(dateKey),
|
||||
child: Padding(
|
||||
@ -348,8 +308,6 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 🔽 Task List (expandable)
|
||||
Obx(() {
|
||||
if (!dailyTaskController.expandedDates
|
||||
.contains(dateKey)) {
|
||||
@ -387,15 +345,12 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 🏗️ Activity name & location
|
||||
MyText.bodyMedium(activityName,
|
||||
fontWeight: 600),
|
||||
const SizedBox(height: 2),
|
||||
MyText.bodySmall(location,
|
||||
color: Colors.grey),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 👥 Team Members
|
||||
GestureDetector(
|
||||
onTap: () => _showTeamMembersBottomSheet(
|
||||
task.teamMembers),
|
||||
@ -413,8 +368,6 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 📊 Progress info
|
||||
MyText.bodySmall(
|
||||
"Completed: $completed / $planned",
|
||||
fontWeight: 600,
|
||||
@ -459,8 +412,6 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
: Colors.red[700],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 🎯 Action Buttons
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
@ -519,8 +470,6 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// 🔻 Loading More Indicator
|
||||
Obx(() => dailyTaskController.isLoadingMore.value
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
|
||||
@ -7,7 +7,6 @@ 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/controller/permission_controller.dart';
|
||||
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart';
|
||||
import 'package:on_field_work/controller/project_controller.dart';
|
||||
import 'package:percent_indicator/percent_indicator.dart';
|
||||
import 'package:on_field_work/model/dailyTaskPlanning/assign_task_bottom_sheet .dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_custom_skeleton.dart';
|
||||
@ -15,11 +14,11 @@ 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});
|
||||
final String projectId; // ✅ Optional projectId from parent
|
||||
|
||||
DailyTaskPlanningScreen({super.key, required this.projectId});
|
||||
|
||||
@override
|
||||
State<DailyTaskPlanningScreen> createState() =>
|
||||
@ -32,67 +31,29 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
Get.put(DailyTaskPlanningController());
|
||||
final PermissionController permissionController =
|
||||
Get.put(PermissionController());
|
||||
final ProjectController projectController = Get.find<ProjectController>();
|
||||
final ServiceController serviceController = Get.put(ServiceController());
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final projectId = projectController.selectedProjectId.value;
|
||||
// ✅ Use widget.projectId if passed; otherwise fallback to selectedProjectId
|
||||
final projectId = widget.projectId;
|
||||
if (projectId.isNotEmpty) {
|
||||
// Now this will fetch only services + building list (no deep infra)
|
||||
dailyTaskPlanningController.fetchTaskData(projectId);
|
||||
serviceController.fetchServices(projectId);
|
||||
}
|
||||
|
||||
// Whenever project changes, fetch buildings & services (still lazy load infra per building)
|
||||
ever<String>(
|
||||
projectController.selectedProjectId,
|
||||
(newProjectId) {
|
||||
if (newProjectId.isNotEmpty) {
|
||||
dailyTaskPlanningController.fetchTaskData(newProjectId);
|
||||
serviceController.fetchServices(newProjectId);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color appBarColor = contentTheme.primary;
|
||||
|
||||
return Scaffold(
|
||||
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;
|
||||
final projectId = widget.projectId;
|
||||
if (projectId.isNotEmpty) {
|
||||
try {
|
||||
await dailyTaskPlanningController.fetchTaskData(
|
||||
@ -127,8 +88,7 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
controller: serviceController,
|
||||
height: 40,
|
||||
onSelectionChanged: (service) async {
|
||||
final projectId =
|
||||
projectController.selectedProjectId.value;
|
||||
final projectId = widget.projectId;
|
||||
if (projectId.isNotEmpty) {
|
||||
await dailyTaskPlanningController
|
||||
.fetchTaskData(
|
||||
@ -239,16 +199,14 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
});
|
||||
|
||||
if (expanded && !buildingLoaded && !buildingLoading) {
|
||||
// fetch infra details for this building lazily
|
||||
final projectId =
|
||||
projectController.selectedProjectId.value;
|
||||
final projectId = widget.projectId;
|
||||
if (projectId.isNotEmpty) {
|
||||
await dailyTaskPlanningController.fetchBuildingInfra(
|
||||
building.id.toString(),
|
||||
projectId,
|
||||
serviceController.selectedService?.id,
|
||||
);
|
||||
setMainState(() {}); // rebuild to reflect loaded data
|
||||
setMainState(() {});
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -292,7 +250,7 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: MyText.bodySmall(
|
||||
"No Progress Report Found",
|
||||
"No Progress Report Found for this Project",
|
||||
fontWeight: 600,
|
||||
),
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user