- 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.
378 lines
11 KiB
Dart
378 lines
11 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/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});
|
|
}
|