marco.pms.mobileapp/lib/view/service_project/service_project_screen.dart

322 lines
11 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/service_project/service_project_screen_controller.dart';
import 'package:on_field_work/model/service_project/service_projects_list_model.dart';
import 'package:on_field_work/helpers/utils/date_time_utils.dart';
import 'package:on_field_work/view/service_project/service_project_details_screen.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
class ServiceProjectScreen extends StatefulWidget {
const ServiceProjectScreen({super.key});
@override
State<ServiceProjectScreen> createState() => _ServiceProjectScreenState();
}
class _ServiceProjectScreenState extends State<ServiceProjectScreen>
with UIMixin {
final TextEditingController searchController = TextEditingController();
final ServiceProjectController controller =
Get.put(ServiceProjectController());
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
controller.fetchProjects();
});
searchController.addListener(() {
controller.updateSearch(searchController.text);
});
}
Future<void> _refreshProjects() async {
await controller.fetchProjects();
}
Widget _buildProjectCard(ProjectItem project) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
shadowColor: Colors.indigo.withOpacity(0.10),
color: Colors.white,
child: InkWell(
borderRadius: BorderRadius.circular(5),
onTap: () {
Get.to(() => ServiceProjectDetailsScreen(projectId: project.id));
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: MyText.titleMedium(
project.name,
fontWeight: 700,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
if (project.status?.status.isNotEmpty ?? false)
Container(
decoration: BoxDecoration(
color: Colors.indigo.withOpacity(0.08),
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
child: MyText.labelSmall(
project.status!.status,
color: Colors.indigo[700],
fontWeight: 600,
),
),
],
),
MySpacing.height(8),
_buildDetailRow(
Icons.date_range_outlined,
Colors.teal,
"Assigned: ${DateTimeUtils.convertUtcToLocal(project.assignedDate.toIso8601String(), format: DateTimeUtils.defaultFormat)}",
),
MySpacing.height(6),
if (project.client != null)
_buildDetailRow(
Icons.account_circle_outlined,
Colors.indigo,
"Client: ${project.client!.name} (${project.client!.contactPerson})",
),
MySpacing.height(6),
_buildDetailRow(
Icons.phone,
Colors.green,
"Contact: ${project.contactName} (${project.contactPhone})",
),
MySpacing.height(10),
if (project.services.isNotEmpty)
Wrap(
spacing: 6,
runSpacing: 4,
children: project.services
.map((e) => _buildServiceChip(e.name))
.toList(),
),
],
),
),
),
);
}
Widget _buildServiceChip(String name) {
return Container(
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: MyText.labelSmall(
name,
color: Colors.orange[800],
fontWeight: 500,
),
);
}
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,
maxLines: 2,
overflow: TextOverflow.ellipsis,
fontWeight: 500,
color: Colors.grey[900],
),
),
],
);
}
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.',
color: Colors.grey, fontWeight: 600),
MySpacing.height(10),
MyText.bodySmall('Try adjusting your filters or refresh.',
color: Colors.grey),
],
),
);
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: CustomAppBar(
title: "Service Projects",
onBackPressed: () => Get.toNamed('/dashboard'),
),
body: LayoutBuilder(
builder: (context, constraints) {
return Column(
children: [
/// SEARCH BAR AREA
Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
Expanded(
child: SizedBox(
height: 38,
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, _) {
return value.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear,
size: 20, color: Colors.grey),
onPressed: () {
searchController.clear();
controller.updateSearch('');
},
)
: const SizedBox.shrink();
},
),
hintText: 'Search projects...',
fillColor: Colors.white,
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide:
BorderSide(color: Colors.grey.shade300),
),
),
),
),
),
MySpacing.width(8),
/// FILTER BUTTON
_roundIconButton(Icons.tune),
MySpacing.width(8),
/// ACTION MENU
_roundMenuButton(),
],
),
),
/// LIST AREA
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
final projects = controller.filteredProjects;
return MyRefreshIndicator(
onRefresh: _refreshProjects,
color: Colors.white,
backgroundColor: Colors.indigo,
child: projects.isEmpty
? _buildEmptyState()
: ListView.separated(
padding: const EdgeInsets.only(
left: 8, right: 8, top: 4, bottom: 20),
itemCount: projects.length,
separatorBuilder: (_, __) => MySpacing.height(12),
itemBuilder: (_, index) =>
_buildProjectCard(projects[index]),
),
);
}),
),
],
);
},
),
),
);
}
Widget _roundIconButton(IconData icon) {
return Container(
height: 38,
width: 38,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
border: Border.all(color: Colors.grey.shade300),
),
child: Icon(icon, size: 20, color: Colors.black87),
);
}
Widget _roundMenuButton() {
return Container(
height: 38,
width: 38,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(5),
),
child: PopupMenuButton<int>(
padding: EdgeInsets.zero,
icon: const Icon(Icons.more_vert, size: 20, color: Colors.black87),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
itemBuilder: (context) => [
const PopupMenuItem<int>(
enabled: false,
height: 30,
child: Text("Actions",
style:
TextStyle(fontWeight: FontWeight.bold, color: Colors.grey)),
),
const PopupMenuItem<int>(
value: 1,
child: Row(
children: [
Expanded(child: Text("Manage Projects")),
Icon(Icons.chevron_right, size: 20, color: Colors.indigo),
],
),
),
],
),
);
}
}