363 lines
13 KiB
Dart
363 lines
13 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
|
import 'package:marco/helpers/widgets/my_text.dart';
|
|
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
|
import 'package:marco/controller/service_project/service_project_screen_controller.dart';
|
|
import 'package:marco/model/service_project/service_projects_list_model.dart';
|
|
import 'package:marco/helpers/utils/date_time_utils.dart';
|
|
import 'package:marco/view/service_project/service_project_details_screen.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();
|
|
|
|
// Fetch projects safely after first frame
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
controller.fetchProjects();
|
|
});
|
|
|
|
searchController.addListener(() {
|
|
controller.updateSearch(searchController.text);
|
|
});
|
|
}
|
|
|
|
Future<void> _refreshProjects() async {
|
|
await controller.fetchProjects();
|
|
}
|
|
|
|
Widget _buildProjectCard(ProjectItem project) {
|
|
return Card(
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
|
margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
|
shadowColor: Colors.indigo.withOpacity(0.10),
|
|
color: Colors.white,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(14),
|
|
onTap: () {
|
|
// Navigate to ServiceProjectDetailsScreen
|
|
Get.to(() => ServiceProjectDetailsScreen(projectId: project.id));
|
|
},
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
/// Project Header
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
MyText.titleMedium(
|
|
project.name,
|
|
fontWeight: 700,
|
|
),
|
|
MySpacing.height(4),
|
|
],
|
|
),
|
|
),
|
|
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(10),
|
|
|
|
/// Assigned Date
|
|
_buildDetailRow(
|
|
Icons.date_range_outlined,
|
|
Colors.teal,
|
|
"Assigned: ${DateTimeUtils.convertUtcToLocal(project.assignedDate.toIso8601String(), format: DateTimeUtils.defaultFormat)}",
|
|
fontSize: 13,
|
|
),
|
|
|
|
MySpacing.height(8),
|
|
|
|
/// Client Info
|
|
if (project.client != null)
|
|
_buildDetailRow(
|
|
Icons.account_circle_outlined,
|
|
Colors.indigo,
|
|
"Client: ${project.client!.name} (${project.client!.contactPerson})",
|
|
fontSize: 13,
|
|
),
|
|
|
|
MySpacing.height(8),
|
|
|
|
/// Contact Info
|
|
_buildDetailRow(
|
|
Icons.phone,
|
|
Colors.green,
|
|
"Contact: ${project.contactName} (${project.contactPhone})",
|
|
fontSize: 13,
|
|
),
|
|
|
|
MySpacing.height(12),
|
|
|
|
/// Services List
|
|
if (project.services.isNotEmpty)
|
|
Wrap(
|
|
spacing: 6,
|
|
runSpacing: 4,
|
|
children: project.services
|
|
.map((service) => _buildServiceChip(service.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: 3),
|
|
child: MyText.labelSmall(
|
|
name,
|
|
color: Colors.orange[800],
|
|
fontWeight: 500,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDetailRow(IconData icon, Color iconColor, String value,
|
|
{double fontSize = 12}) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Icon(icon, size: 18, color: iconColor),
|
|
MySpacing.width(8),
|
|
Flexible(
|
|
child: MyText.bodySmall(
|
|
value,
|
|
color: Colors.grey[900],
|
|
fontWeight: 500,
|
|
fontSize: fontSize,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
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),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF5F5F5),
|
|
appBar: PreferredSize(
|
|
preferredSize: const Size.fromHeight(72),
|
|
child: AppBar(
|
|
backgroundColor: const Color(0xFFF5F5F5),
|
|
elevation: 0.5,
|
|
automaticallyImplyLeading: false,
|
|
titleSpacing: 0,
|
|
title: Padding(
|
|
padding: MySpacing.xy(16, 0),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(Icons.arrow_back_ios_new,
|
|
color: Colors.black, size: 20),
|
|
onPressed: () => Get.toNamed('/dashboard'),
|
|
),
|
|
MySpacing.width(8),
|
|
MyText.titleLarge(
|
|
'Service Projects',
|
|
fontWeight: 700,
|
|
color: Colors.black,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
body: Column(
|
|
children: [
|
|
/// Search bar and actions
|
|
Padding(
|
|
padding: MySpacing.xy(8, 8),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
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),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
MySpacing.width(8),
|
|
Container(
|
|
height: 35,
|
|
width: 35,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(5),
|
|
),
|
|
child: IconButton(
|
|
icon:
|
|
const Icon(Icons.tune, size: 20, color: Colors.black87),
|
|
onPressed: () {
|
|
// TODO: Open filter bottom sheet
|
|
},
|
|
),
|
|
),
|
|
MySpacing.width(10),
|
|
Container(
|
|
height: 35,
|
|
width: 35,
|
|
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: [
|
|
SizedBox(width: 10),
|
|
Expanded(child: Text("Manage Projects")),
|
|
Icon(Icons.chevron_right,
|
|
size: 20, color: Colors.indigo),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
/// Project 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: 80),
|
|
itemCount: projects.length,
|
|
separatorBuilder: (_, __) => MySpacing.height(12),
|
|
itemBuilder: (_, index) =>
|
|
_buildProjectCard(projects[index]),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|