marco.pms.mobileapp/lib/view/infraProject/infra_project_details_screen.dart
Vaibhav Surve 3dfa6e5877 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.
2025-12-03 16:49:46 +05:30

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