diff --git a/lib/controller/infra_project/infra_project_screen_controller.dart b/lib/controller/infra_project/infra_project_screen_controller.dart new file mode 100644 index 0000000..cc1c2bc --- /dev/null +++ b/lib/controller/infra_project/infra_project_screen_controller.dart @@ -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 = [].obs; + final isLoading = false.obs; + final searchQuery = ''.obs; + + // Filtered list + List 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 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; + } +} diff --git a/lib/controller/infra_project/infra_project_screen_details_controller.dart b/lib/controller/infra_project/infra_project_screen_details_controller.dart new file mode 100644 index 0000000..a877236 --- /dev/null +++ b/lib/controller/infra_project/infra_project_screen_details_controller.dart @@ -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(); + var errorMessage = ''.obs; + + @override + void onInit() { + super.onInit(); + fetchProjectDetails(); + } + + Future 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; + } + } +} diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 328d7fe..293fd2a 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -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"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 6a70f6f..5adb89e 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -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 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 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 getJobCommentList({ required String jobTicketId, int pageNumber = 1, diff --git a/lib/helpers/utils/permission_constants.dart b/lib/helpers/utils/permission_constants.dart index 51867b8..79a750c 100644 --- a/lib/helpers/utils/permission_constants.dart +++ b/lib/helpers/utils/permission_constants.dart @@ -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. diff --git a/lib/helpers/widgets/pill_tab_bar.dart b/lib/helpers/widgets/pill_tab_bar.dart index c1b8f62..87950c8 100644 --- a/lib/helpers/widgets/pill_tab_bar.dart +++ b/lib/helpers/widgets/pill_tab_bar.dart @@ -8,6 +8,7 @@ class PillTabBar extends StatelessWidget { final Color indicatorColor; final double height; final ValueChanged? 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, ), ), ); diff --git a/lib/model/infra_project/infra_project_details.dart b/lib/model/infra_project/infra_project_details.dart new file mode 100644 index 0000000..aa180fd --- /dev/null +++ b/lib/model/infra_project/infra_project_details.dart @@ -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 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 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 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 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 json) { + return ProjectStatus( + id: json['id'] as String?, + status: json['status'] as String?, + ); + } + + Map 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 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 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 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 toJson() { + return { + 'id': id, + 'name': name, + 'email': email, + 'contactPerson': contactPerson, + 'address': address, + 'gstNumber': gstNumber, + 'contactNumber': contactNumber, + 'sprid': sprid, + }; + } +} diff --git a/lib/model/infra_project/infra_project_list.dart b/lib/model/infra_project/infra_project_list.dart new file mode 100644 index 0000000..8f93174 --- /dev/null +++ b/lib/model/infra_project/infra_project_list.dart @@ -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 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 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? data; + + ProjectsPageData({ + this.currentPage, + this.totalPages, + this.totalEntites, + this.data, + }); + + factory ProjectsPageData.fromJson(Map json) { + return ProjectsPageData( + currentPage: json['currentPage'], + totalPages: json['totalPages'], + totalEntites: json['totalEntites'], + data: (json['data'] as List?) + ?.map((e) => ProjectData.fromJson(e)) + .toList(), + ); + } + + Map 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 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 toJson() { + return { + 'id': id, + 'name': name, + 'shortName': shortName, + 'projectAddress': projectAddress, + 'contactPerson': contactPerson, + 'startDate': startDate, + 'endDate': endDate, + 'projectStatusId': projectStatusId, + 'teamSize': teamSize, + 'completedWork': completedWork, + 'plannedWork': plannedWork, + }; + } +} diff --git a/lib/routes.dart b/lib/routes.dart index bbd8931..44f8287 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -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(), @@ -93,7 +84,7 @@ getPageRoute() { name: '/dashboard/document-main-page', page: () => UserDocumentsPage(), middlewares: [AuthMiddleware()]), - // Finance + // Finance GetPage( name: '/dashboard/finance', page: () => FinanceScreen(), @@ -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()), diff --git a/lib/view/infraProject/infra_project_details_screen.dart b/lib/view/infraProject/infra_project_details_screen.dart new file mode 100644 index 0000000..ef7b4b2 --- /dev/null +++ b/lib/view/infraProject/infra_project_details_screen.dart @@ -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 createState() => + _InfraProjectDetailsScreenState(); +} + +class _InfraProjectDetailsScreenState extends State + with SingleTickerProviderStateMixin, UIMixin { + late final TabController _tabController; + final DynamicMenuController menuController = + Get.find(); + 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 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}); +} diff --git a/lib/view/infraProject/infra_project_screen.dart b/lib/view/infraProject/infra_project_screen.dart index 6a5bdc5..2ee0495 100644 --- a/lib/view/infraProject/infra_project_screen.dart +++ b/lib/view/infraProject/infra_project_screen.dart @@ -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 createState() => - _InfraProjectsMainScreenState(); + State createState() => _InfraProjectScreenState(); } -class _InfraProjectsMainScreenState extends State - with SingleTickerProviderStateMixin, UIMixin { - late TabController _tabController; - - final DynamicMenuController menuController = Get.find(); - - // Final tab list after filtering - final List<_InfraTab> _tabs = []; +class _InfraProjectScreenState extends State 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 _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, + ), ), - ); - } - - _tabController = TabController(length: _tabs.length, vsync: this); + ], + ); } - @override - void dispose() { - _tabController.dispose(); - super.dispose(); + // --------------------------------------------------------------------------- + // 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, + ), + ], + ), + ); } + // --------------------------------------------------------------------------- + // 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( + 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 ); } } - -/// INTERNAL MODEL -class _InfraTab { - final String name; - final Widget view; - - _InfraTab({required this.name, required this.view}); -} diff --git a/lib/view/taskPlanning/daily_progress_report.dart b/lib/view/taskPlanning/daily_progress_report.dart index 60dd191..c1f2b55 100644 --- a/lib/view/taskPlanning/daily_progress_report.dart +++ b/lib/view/taskPlanning/daily_progress_report.dart @@ -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 createState() => @@ -64,21 +63,15 @@ class _DailyProgressReportScreenState extends State } } }); - 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(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 block to keep it independent } @override @@ -89,35 +82,9 @@ class _DailyProgressReportScreenState extends State @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 } Future _openFilterSheet() async { - // ✅ Fetch filter data first if (dailyTaskController.taskFilterData == null) { await dailyTaskController .fetchTaskFilter(dailyTaskController.selectedProjectId ?? ''); @@ -279,32 +245,27 @@ class _DailyProgressReportScreenState extends State 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 child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 🗓️ Date Header GestureDetector( onTap: () => dailyTaskController.toggleDate(dateKey), child: Padding( @@ -348,8 +308,6 @@ class _DailyProgressReportScreenState extends State ), ), ), - - // 🔽 Task List (expandable) Obx(() { if (!dailyTaskController.expandedDates .contains(dateKey)) { @@ -387,15 +345,12 @@ class _DailyProgressReportScreenState extends State 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 ), ), const SizedBox(height: 8), - - // 📊 Progress info MyText.bodySmall( "Completed: $completed / $planned", fontWeight: 600, @@ -459,8 +412,6 @@ class _DailyProgressReportScreenState extends State : Colors.red[700], ), const SizedBox(height: 12), - - // 🎯 Action Buttons SingleChildScrollView( scrollDirection: Axis.horizontal, physics: const ClampingScrollPhysics(), @@ -519,8 +470,6 @@ class _DailyProgressReportScreenState extends State ), ); }), - - // 🔻 Loading More Indicator Obx(() => dailyTaskController.isLoadingMore.value ? const Padding( padding: EdgeInsets.symmetric(vertical: 16), diff --git a/lib/view/taskPlanning/daily_task_planning.dart b/lib/view/taskPlanning/daily_task_planning.dart index cb6091b..4660537 100644 --- a/lib/view/taskPlanning/daily_task_planning.dart +++ b/lib/view/taskPlanning/daily_task_planning.dart @@ -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 createState() => @@ -32,67 +31,29 @@ class _DailyTaskPlanningScreenState extends State Get.put(DailyTaskPlanningController()); final PermissionController permissionController = Get.put(PermissionController()); - final ProjectController projectController = Get.find(); 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( - 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 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 }); 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 Padding( padding: const EdgeInsets.all(16.0), child: MyText.bodySmall( - "No Progress Report Found", + "No Progress Report Found for this Project", fontWeight: 600, ), )