added team member list

This commit is contained in:
Vaibhav Surve 2025-12-15 10:53:16 +05:30
parent 6cd9fbe57b
commit 03082aeea9
6 changed files with 418 additions and 4113 deletions

View File

@ -1,6 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/infra_project/infra_project_details.dart'; import 'package:on_field_work/model/infra_project/infra_project_details.dart';
import 'package:on_field_work/model/infra_project/infra_team_list_model.dart';
class InfraProjectDetailsController extends GetxController { class InfraProjectDetailsController extends GetxController {
final String projectId; final String projectId;
@ -9,25 +10,39 @@ class InfraProjectDetailsController extends GetxController {
var isLoading = true.obs; var isLoading = true.obs;
var projectDetails = Rxn<ProjectData>(); var projectDetails = Rxn<ProjectData>();
var teamList = <ProjectAllocation>[].obs;
var teamLoading = true.obs;
var errorMessage = ''.obs; var errorMessage = ''.obs;
var teamErrorMessage = ''.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
fetchProjectDetails(); fetchProjectDetails();
fetchProjectTeamList();
}
Map<String, List<ProjectAllocation>> get groupedTeamByRole {
final Map<String, List<ProjectAllocation>> map = {};
for (final member in teamList) {
map.putIfAbsent(member.jobRoleId, () => []).add(member);
}
return map;
} }
Future<void> fetchProjectDetails() async { Future<void> fetchProjectDetails() async {
try { try {
isLoading.value = true; isLoading.value = true;
final response = await ApiService.getInfraProjectDetails(projectId: projectId); final response =
await ApiService.getInfraProjectDetails(projectId: projectId);
if (response != null && response.success == true && response.data != null) { if (response != null &&
response.success == true &&
response.data != null) {
projectDetails.value = response.data; projectDetails.value = response.data;
isLoading.value = false; errorMessage.value = '';
} else { } else {
errorMessage.value = response?.message ?? "Failed to load project details"; errorMessage.value =
response?.message ?? "Failed to load project details";
} }
} catch (e) { } catch (e) {
errorMessage.value = "Error fetching project details: $e"; errorMessage.value = "Error fetching project details: $e";
@ -35,4 +50,28 @@ class InfraProjectDetailsController extends GetxController {
isLoading.value = false; isLoading.value = false;
} }
} }
Future<void> fetchProjectTeamList() async {
try {
teamLoading.value = true;
teamErrorMessage.value = '';
final response = await ApiService.getInfraProjectTeamListApi(
projectId: projectId,
includeInactive: false,
);
if (response?.success == true && response!.data.isNotEmpty) {
teamList.assignAll(response.data);
} else {
teamList.clear();
teamErrorMessage.value = response?.message ?? "No team members found.";
}
} catch (e) {
teamList.clear();
teamErrorMessage.value = "Failed to load team members";
} finally {
teamLoading.value = false;
}
}
} }

View File

@ -167,4 +167,6 @@ class ApiEndpoints {
// Infra Project Module API Endpoints // Infra Project Module API Endpoints
static const String getInfraProjectsList = "/project/list"; static const String getInfraProjectsList = "/project/list";
static const String getInfraProjectDetail = "/project/details"; static const String getInfraProjectDetail = "/project/details";
static const String getInfraProjectTeamList = "/project/allocation";
} }

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,7 @@ import 'package:on_field_work/model/infra_project/infra_project_list.dart';
import 'package:on_field_work/model/infra_project/infra_project_details.dart'; import 'package:on_field_work/model/infra_project/infra_project_details.dart';
import 'package:on_field_work/model/dashboard/collection_overview_model.dart'; import 'package:on_field_work/model/dashboard/collection_overview_model.dart';
import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart'; import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart';
import 'package:on_field_work/model/infra_project/infra_team_list_model.dart';
class ApiService { class ApiService {
static const bool enableLogs = true; static const bool enableLogs = true;
@ -2007,6 +2008,43 @@ class ApiService {
label: "Comment Task", returnFullResponse: true); label: "Comment Task", returnFullResponse: true);
return parsed != null && parsed['success'] == true; return parsed != null && parsed['success'] == true;
} }
static Future<ProjectAllocationResponse?> getInfraProjectTeamListApi({
required String projectId,
String? serviceId,
bool includeInactive = false,
}) async {
if (projectId.isEmpty) {
_log("projectId must not be empty for getInfraProjectTeamListApi",
level: LogLevel.error);
return null;
}
final endpoint = "${ApiEndpoints.getInfraProjectTeamList}/$projectId";
final queryParams = <String, String>{
'includeInactive': includeInactive.toString(),
};
if (serviceId != null && serviceId.isNotEmpty) {
queryParams['serviceId'] = serviceId;
}
final response = await _safeApiCall(
endpoint,
method: 'GET',
queryParams: queryParams,
);
if (response == null) return null;
final parsedJson = _parseAndDecryptResponse(
response,
label: "InfraProjectTeamList",
returnFullResponse: true,
);
if (parsedJson == null || parsedJson is! Map<String, dynamic>) {
return null;
}
return ProjectAllocationResponse.fromJson(parsedJson);
}
static Future<Map<String, dynamic>?> getInfraDetails(String projectId, static Future<Map<String, dynamic>?> getInfraDetails(String projectId,
{String? serviceId}) async { {String? serviceId}) async {

View File

@ -0,0 +1,119 @@
class ProjectAllocationResponse {
final bool success;
final String message;
final List<ProjectAllocation> data;
final int statusCode;
final String timestamp;
ProjectAllocationResponse({
required this.success,
required this.message,
required this.data,
required this.statusCode,
required this.timestamp,
});
factory ProjectAllocationResponse.fromJson(Map<String, dynamic> json) {
final List<dynamic>? rawData = json['data'] as List<dynamic>?;
final List<ProjectAllocation> allocations = rawData
?.map((item) => ProjectAllocation.fromJson(item as Map<String, dynamic>))
.toList()
?? [];
return ProjectAllocationResponse(
success: json['success'] as bool? ?? false,
message: json['message'] as String? ?? 'An unknown API error occurred.',
data: allocations,
statusCode: json['statusCode'] as int? ?? 0,
timestamp: json['timestamp'] as String? ?? '',
);
}
/// Converts the [ProjectAllocationResponse] object back to a JSON map.
Map<String, dynamic> toJson() {
return {
'success': success,
'message': message,
'data': data.map((e) => e.toJson()).toList(),
'statusCode': statusCode,
'timestamp': timestamp,
};
}
}
// --- Allocation Detail Class ---
class ProjectAllocation {
final String id;
final String employeeId;
final String projectId;
final String allocationDate;
final bool isActive;
final String firstName;
final String lastName;
final String middleName;
final String organizationId;
final String organizationName;
final String serviceId;
final String serviceName;
final String jobRoleId;
final String jobRoleName;
ProjectAllocation({
required this.id,
required this.employeeId,
required this.projectId,
required this.allocationDate,
required this.isActive,
required this.firstName,
required this.lastName,
required this.middleName,
required this.organizationId,
required this.organizationName,
required this.serviceId,
required this.serviceName,
required this.jobRoleId,
required this.jobRoleName
});
factory ProjectAllocation.fromJson(Map<String, dynamic> json) {
return ProjectAllocation(
id: json['id'] as String? ?? '',
employeeId: json['employeeId'] as String? ?? '',
projectId: json['projectId'] as String? ?? '',
allocationDate: json['allocationDate'] as String? ?? '',
isActive: json['isActive'] as bool? ?? false,
firstName: json['firstName'] as String? ?? '',
lastName: json['lastName'] as String? ?? '',
middleName: json['middleName'] as String? ?? '',
organizationId: json['organizationId'] as String? ?? '',
organizationName: json['organizationName'] as String? ?? '',
serviceId: json['serviceId'] as String? ?? '',
serviceName: json['serviceName'] as String? ?? '',
jobRoleId: json['jobRoleId'] as String? ?? '',
jobRoleName: json['jobRoleName'] as String? ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'employeeId': employeeId,
'projectId': projectId,
'allocationDate': allocationDate,
'isActive': isActive,
'firstName': firstName,
'lastName': lastName,
'middleName': middleName,
'organizationId': organizationId,
'organizationName': organizationName,
'serviceId': serviceId,
'serviceName': serviceName,
'jobRoleId': jobRoleId,
'jobRoleName': jobRoleName
};
}
}

View File

@ -10,11 +10,14 @@ 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_spacing.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart'; import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
import 'package:on_field_work/helpers/widgets/avatar.dart';
import 'package:on_field_work/view/employees/employee_profile_screen.dart';
import 'package:on_field_work/controller/dynamicMenu/dynamic_menu_controller.dart'; import 'package:on_field_work/controller/dynamicMenu/dynamic_menu_controller.dart';
import 'package:on_field_work/controller/infra_project/infra_project_screen_details_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_progress_report.dart';
import 'package:on_field_work/view/taskPlanning/daily_task_planning.dart'; import 'package:on_field_work/view/taskPlanning/daily_task_planning.dart';
import 'package:on_field_work/model/infra_project/infra_team_list_model.dart';
class InfraProjectDetailsScreen extends StatefulWidget { class InfraProjectDetailsScreen extends StatefulWidget {
final String projectId; final String projectId;
@ -36,17 +39,20 @@ class _InfraProjectDetailsScreenState extends State<InfraProjectDetailsScreen>
late final TabController _tabController; late final TabController _tabController;
final DynamicMenuController menuController = final DynamicMenuController menuController =
Get.find<DynamicMenuController>(); Get.find<DynamicMenuController>();
late final InfraProjectDetailsController controller;
final List<_InfraTab> _tabs = []; final List<_InfraTab> _tabs = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
controller =
Get.put(InfraProjectDetailsController(projectId: widget.projectId));
_prepareTabs(); _prepareTabs();
} }
void _prepareTabs() { void _prepareTabs() {
// Profile tab is always added
_tabs.add(_InfraTab(name: "Profile", view: _buildProfileTab())); _tabs.add(_InfraTab(name: "Profile", view: _buildProfileTab()));
_tabs.add(_InfraTab(name: "Team", view: _buildTeamTab()));
final allowedMenu = menuController.menuItems.where((m) => m.available); final allowedMenu = menuController.menuItems.where((m) => m.available);
@ -77,10 +83,148 @@ class _InfraProjectDetailsScreenState extends State<InfraProjectDetailsScreen>
super.dispose(); super.dispose();
} }
Widget _buildProfileTab() { Widget _buildTeamTab() {
final controller = return Obx(() {
Get.put(InfraProjectDetailsController(projectId: widget.projectId)); if (controller.teamLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (controller.teamErrorMessage.isNotEmpty) {
return Center(
child: MyText.bodyMedium(controller.teamErrorMessage.value),
);
}
if (controller.teamList.isEmpty) {
return const Center(
child: Text("No team members allocated to this project."),
);
}
final roleGroups = controller.groupedTeamByRole;
final sortedRoleEntries = roleGroups.entries.toList()
..sort((a, b) {
final aName = (a.value.isNotEmpty ? a.value.first.jobRoleName : '')
.toLowerCase();
final bName = (b.value.isNotEmpty ? b.value.first.jobRoleName : '')
.toLowerCase();
return aName.compareTo(bName);
});
return MyRefreshIndicator(
onRefresh: controller.fetchProjectTeamList,
backgroundColor: Colors.indigo,
color: Colors.white,
child: ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: sortedRoleEntries.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final teamMembers = sortedRoleEntries[index].value;
return _buildRoleCard(teamMembers);
},
),
);
});
}
Widget _buildRoleCard(List<ProjectAllocation> teamMembers) {
teamMembers.sort((a, b) {
final aName = ("${a.firstName} ${a.lastName}").trim().toLowerCase();
final bName = ("${b.firstName} ${b.lastName}").trim().toLowerCase();
return aName.compareTo(bName);
});
final String roleName =
(teamMembers.isNotEmpty ? (teamMembers.first.jobRoleName) : '').trim();
return Card(
elevation: 3,
shadowColor: Colors.black26,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// TOP: Job Role name
if (roleName.isNotEmpty) ...[
MyText.bodyLarge(
roleName,
fontWeight: 700,
),
const Divider(height: 20),
] else
const Divider(height: 20),
// Team members list
...teamMembers.map((allocation) {
return InkWell(
onTap: () {
Get.to(
() => EmployeeProfilePage(
employeeId: allocation.employeeId,
),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Avatar(
firstName: allocation.firstName,
lastName: allocation.lastName,
size: 32,
),
MySpacing.width(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleMedium(
"${allocation.firstName} ${allocation.lastName}",
fontWeight: 600,
),
MyText.bodySmall(
allocation.serviceName.isNotEmpty
? "Service: ${allocation.serviceName}"
: "No Service Assigned",
color: Colors.grey[700],
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
MyText.bodySmall(
"Allocated",
color: Colors.grey.shade500,
),
MyText.bodySmall(
DateFormat('d MMM yyyy').format(
DateTime.parse(allocation.allocationDate),
),
fontWeight: 600,
),
],
),
],
),
),
);
}).toList(),
],
),
),
);
}
Widget _buildProfileTab() {
return Obx(() { return Obx(() {
if (controller.isLoading.value) { if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
@ -155,23 +299,27 @@ class _InfraProjectDetailsScreenState extends State<InfraProjectDetailsScreen>
_buildDetailRow( _buildDetailRow(
icon: Icons.location_on_outlined, icon: Icons.location_on_outlined,
label: 'Address', label: 'Address',
value: data.projectAddress ?? "-"), value: data.projectAddress ?? "-",
),
_buildDetailRow( _buildDetailRow(
icon: Icons.calendar_today_outlined, icon: Icons.calendar_today_outlined,
label: 'Start Date', label: 'Start Date',
value: data.startDate != null value: data.startDate != null
? DateFormat('d/M/yyyy').format(data.startDate!) ? DateFormat('d/M/yyyy').format(data.startDate!)
: "-"), : "-",
),
_buildDetailRow( _buildDetailRow(
icon: Icons.calendar_today_outlined, icon: Icons.calendar_today_outlined,
label: 'End Date', label: 'End Date',
value: data.endDate != null value: data.endDate != null
? DateFormat('d/M/yyyy').format(data.endDate!) ? DateFormat('d/M/yyyy').format(data.endDate!)
: "-"), : "-",
),
_buildDetailRow( _buildDetailRow(
icon: Icons.flag_outlined, icon: Icons.flag_outlined,
label: 'Status', label: 'Status',
value: data.projectStatus?.status ?? "-"), value: data.projectStatus?.status ?? "-",
),
_buildDetailRow( _buildDetailRow(
icon: Icons.person_outline, icon: Icons.person_outline,
label: 'Contact Person', label: 'Contact Person',
@ -181,7 +329,8 @@ class _InfraProjectDetailsScreenState extends State<InfraProjectDetailsScreen>
if (data.contactPerson != null) { if (data.contactPerson != null) {
LauncherUtils.launchPhone(data.contactPerson!); LauncherUtils.launchPhone(data.contactPerson!);
} }
}), },
),
], ],
); );
} }
@ -194,20 +343,22 @@ class _InfraProjectDetailsScreenState extends State<InfraProjectDetailsScreen>
_buildDetailRow( _buildDetailRow(
icon: Icons.person_outline, icon: Icons.person_outline,
label: 'Name', label: 'Name',
value: promoter.name ?? "-"), value: promoter.name ?? "-",
),
_buildDetailRow( _buildDetailRow(
icon: Icons.phone_outlined, icon: Icons.phone_outlined,
label: 'Contact', label: 'Contact',
value: promoter.contactNumber ?? "-", value: promoter.contactNumber ?? "-",
isActionable: true, isActionable: true,
onTap: () => onTap: () => LauncherUtils.launchPhone(promoter.contactNumber ?? ""),
LauncherUtils.launchPhone(promoter.contactNumber ?? "")), ),
_buildDetailRow( _buildDetailRow(
icon: Icons.email_outlined, icon: Icons.email_outlined,
label: 'Email', label: 'Email',
value: promoter.email ?? "-", value: promoter.email ?? "-",
isActionable: true, isActionable: true,
onTap: () => LauncherUtils.launchEmail(promoter.email ?? "")), onTap: () => LauncherUtils.launchEmail(promoter.email ?? ""),
),
], ],
); );
} }
@ -218,19 +369,24 @@ class _InfraProjectDetailsScreenState extends State<InfraProjectDetailsScreen>
titleIcon: Icons.engineering_outlined, titleIcon: Icons.engineering_outlined,
children: [ children: [
_buildDetailRow( _buildDetailRow(
icon: Icons.person_outline, label: 'Name', value: pmc.name ?? "-"), icon: Icons.person_outline,
label: 'Name',
value: pmc.name ?? "-",
),
_buildDetailRow( _buildDetailRow(
icon: Icons.phone_outlined, icon: Icons.phone_outlined,
label: 'Contact', label: 'Contact',
value: pmc.contactNumber ?? "-", value: pmc.contactNumber ?? "-",
isActionable: true, isActionable: true,
onTap: () => LauncherUtils.launchPhone(pmc.contactNumber ?? "")), onTap: () => LauncherUtils.launchPhone(pmc.contactNumber ?? ""),
),
_buildDetailRow( _buildDetailRow(
icon: Icons.email_outlined, icon: Icons.email_outlined,
label: 'Email', label: 'Email',
value: pmc.email ?? "-", value: pmc.email ?? "-",
isActionable: true, isActionable: true,
onTap: () => LauncherUtils.launchEmail(pmc.email ?? "")), onTap: () => LauncherUtils.launchEmail(pmc.email ?? ""),
),
], ],
); );
} }
@ -251,7 +407,9 @@ class _InfraProjectDetailsScreenState extends State<InfraProjectDetailsScreen>
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(8), child: Icon(icon, size: 20)), padding: const EdgeInsets.all(8),
child: Icon(icon, size: 20),
),
MySpacing.width(16), MySpacing.width(16),
Expanded( Expanded(
child: Column( child: Column(