536 lines
16 KiB
Dart
536 lines
16 KiB
Dart
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/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/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';
|
|
import 'package:on_field_work/model/infra_project/infra_team_list_model.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>();
|
|
late final InfraProjectDetailsController controller;
|
|
final List<_InfraTab> _tabs = [];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
controller =
|
|
Get.put(InfraProjectDetailsController(projectId: widget.projectId));
|
|
_prepareTabs();
|
|
}
|
|
|
|
void _prepareTabs() {
|
|
_tabs.add(_InfraTab(name: "Profile", view: _buildProfileTab()));
|
|
_tabs.add(_InfraTab(name: "Team", view: _buildTeamTab()));
|
|
|
|
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 _buildTeamTab() {
|
|
return Obx(() {
|
|
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(() {
|
|
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});
|
|
}
|