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:
Vaibhav Surve 2025-12-03 16:49:46 +05:30
parent 03e3e7b5db
commit 3dfa6e5877
13 changed files with 1168 additions and 199 deletions

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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";
}

View File

@ -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,

View File

@ -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.

View File

@ -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,
),
),
);

View 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,
};
}
}

View 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,
};
}
}

View File

@ -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()),

View 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});
}

View File

@ -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});
}

View File

@ -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),

View File

@ -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,
),
)