- 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.
279 lines
9.3 KiB
Dart
279 lines
9.3 KiB
Dart
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/view/infraProject/infra_project_details_screen.dart';
|
|
|
|
class InfraProjectScreen extends StatefulWidget {
|
|
const InfraProjectScreen({super.key});
|
|
|
|
@override
|
|
State<InfraProjectScreen> createState() => _InfraProjectScreenState();
|
|
}
|
|
|
|
class _InfraProjectScreenState extends State<InfraProjectScreen> with UIMixin {
|
|
final TextEditingController searchController = TextEditingController();
|
|
final InfraProjectController controller = Get.put(InfraProjectController());
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
controller.fetchProjects();
|
|
});
|
|
|
|
searchController.addListener(() {
|
|
controller.updateSearch(searchController.text);
|
|
});
|
|
}
|
|
|
|
Future<void> _refreshProjects() async {
|
|
await controller.fetchProjects();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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}",
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MAIN BUILD
|
|
// ---------------------------------------------------------------------------
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final Color appBarColor = contentTheme.primary;
|
|
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF5F5F5),
|
|
appBar: CustomAppBar(
|
|
title: "Infra Projects",
|
|
projectName: 'All Infra Projects',
|
|
backgroundColor: appBarColor,
|
|
onBackPressed: () => Get.toNamed('/dashboard'),
|
|
),
|
|
body: Stack(
|
|
children: [
|
|
// GRADIENT BACKDROP
|
|
Container(
|
|
height: 80,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
appBarColor,
|
|
appBarColor.withOpacity(0),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
SafeArea(
|
|
bottom: true,
|
|
child: Column(
|
|
children: [
|
|
// 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),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// LIST
|
|
Expanded(
|
|
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]),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|