added service projet screen

This commit is contained in:
Vaibhav Surve 2025-11-12 15:36:10 +05:30
parent 8fb65d31e2
commit cb00911983
10 changed files with 991 additions and 245 deletions

View File

@ -0,0 +1,53 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/service_project/service_projects_details_model.dart';
class ServiceProjectDetailsController extends GetxController {
// Selected project id
var projectId = ''.obs;
// Project details
var projectDetail = Rxn<ProjectDetail>();
// Loading state
var isLoading = false.obs;
// Error message
var errorMessage = ''.obs;
/// Set project id and fetch its details
void setProjectId(String id) {
projectId.value = id;
fetchProjectDetail();
}
/// Fetch project detail from API
Future<void> fetchProjectDetail() async {
if (projectId.value.isEmpty) {
errorMessage.value = "Invalid project ID";
return;
}
isLoading.value = true;
errorMessage.value = '';
try {
final result = await ApiService.getServiceProjectDetailApi(projectId.value);
if (result != null && result.data != null) {
projectDetail.value = result.data!;
} else {
errorMessage.value = result?.message ?? "Failed to fetch project details";
}
} catch (e) {
errorMessage.value = "Error: $e";
} finally {
isLoading.value = false;
}
}
/// Refresh project details manually
Future<void> refresh() async {
await fetchProjectDetail();
}
}

View File

@ -0,0 +1,36 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/service_project/service_projects_list_model.dart';
class ServiceProjectController extends GetxController {
var projects = <ProjectItem>[].obs;
var isLoading = false.obs;
var searchQuery = ''.obs;
RxList<ProjectItem> get filteredProjects {
if (searchQuery.value.isEmpty) return projects;
return projects
.where((p) =>
p.name.toLowerCase().contains(searchQuery.value.toLowerCase()) ||
p.contactPerson.toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList()
.obs;
}
Future<void> fetchProjects({int pageNumber = 1, int pageSize = 20}) async {
try {
isLoading.value = true;
final result = await ApiService.getServiceProjectsListApi(
pageNumber: pageNumber, pageSize: pageSize);
if (result != null && result.data != null) {
projects.assignAll(result.data!.data ?? []);
}
} finally {
isLoading.value = false;
}
}
void updateSearch(String query) {
searchQuery.value = query;
}
}

View File

@ -1,7 +1,9 @@
class ApiEndpoints {
static const String baseUrl = "https://stageapi.marcoaiot.com/api";
// static const String baseUrl = "https://stageapi.marcoaiot.com/api";
// static const String baseUrl = "https://api.marcoaiot.com/api";
// static const String baseUrl = "https://devapi.marcoaiot.com/api";
static const String baseUrl = "https://mapi.marcoaiot.com/api";
static const String getMasterCurrencies = "/Master/currencies/list";
static const String getMasterExpensesCategories =
@ -127,4 +129,8 @@ class ApiEndpoints {
static const String getAssignedServices = "/Project/get/assigned/services";
static const String getAdvancePayments = '/Expense/get/transactions';
// Service Project Module API Endpoints
static const String getServiceProjectsList = "/serviceproject/list";
static const String getServiceProjectDetail = "/serviceproject/details";
}

View File

@ -33,6 +33,8 @@ import 'package:marco/model/finance/payment_request_list_model.dart';
import 'package:marco/model/finance/payment_request_filter.dart';
import 'package:marco/model/finance/payment_request_details_model.dart';
import 'package:marco/model/finance/advance_payment_model.dart';
import 'package:marco/model/service_project/service_projects_list_model.dart';
import 'package:marco/model/service_project/service_projects_details_model.dart';
class ApiService {
static const bool enableLogs = true;
@ -302,6 +304,76 @@ class ApiService {
}
}
// Service Project Module APIs
/// Get details of a single service project
static Future<ServiceProjectDetailModel?> getServiceProjectDetailApi(String projectId) async {
final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId";
logSafe("Fetching details for Service Project ID: $projectId");
try {
final response = await _getRequest(endpoint);
if (response == null) {
logSafe("Service Project Detail request failed: null response", level: LogLevel.error);
return null;
}
final jsonResponse = _parseResponseForAllData(
response,
label: "Service Project Detail",
);
if (jsonResponse != null) {
return ServiceProjectDetailModel.fromJson(jsonResponse);
}
} catch (e, stack) {
logSafe("Exception during getServiceProjectDetailApi: $e", level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
}
return null;
}
/// Get Service Project List
static Future<ServiceProjectListModel?> getServiceProjectsListApi({
int pageNumber = 1,
int pageSize = 20,
}) async {
const endpoint = ApiEndpoints.getServiceProjectsList;
logSafe("Fetching Service Project List");
try {
final queryParams = {
'pageNumber': pageNumber.toString(),
'pageSize': pageSize.toString(),
};
final response = await _getRequest(endpoint, queryParams: queryParams);
if (response == null) {
logSafe("Service Project List request failed: null response",
level: LogLevel.error);
return null;
}
final jsonResponse = _parseResponseForAllData(
response,
label: "Service Project List",
);
if (jsonResponse != null) {
return ServiceProjectListModel.fromJson(jsonResponse);
}
} catch (e, stack) {
logSafe("Exception during getServiceProjectsListApi: $e",
level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
}
return null;
}
/// Edit Expense Payment Request
static Future<bool> editExpensePaymentRequestApi({
required String id,
@ -1707,8 +1779,8 @@ class ApiService {
String? reimbursedById,
double? baseAmount,
double? taxAmount,
double? tdsPercent,
double? netPayable,
double? tdsPercent,
double? netPayable,
}) async {
final Map<String, dynamic> payload = {
"expenseId": expenseId,

View File

@ -0,0 +1,241 @@
class ServiceProjectDetailModel {
final bool success;
final String message;
final ProjectDetail? data;
final dynamic errors;
final int statusCode;
final DateTime timestamp;
ServiceProjectDetailModel({
required this.success,
required this.message,
this.data,
this.errors,
required this.statusCode,
required this.timestamp,
});
factory ServiceProjectDetailModel.fromJson(Map<String, dynamic> json) {
return ServiceProjectDetailModel(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: json['data'] != null ? ProjectDetail.fromJson(json['data']) : null,
errors: json['errors'],
statusCode: json['statusCode'] ?? 0,
timestamp: DateTime.parse(json['timestamp'] ?? DateTime.now().toIso8601String()),
);
}
Map<String, dynamic> toJson() => {
'success': success,
'message': message,
'data': data?.toJson(),
'errors': errors,
'statusCode': statusCode,
'timestamp': timestamp.toIso8601String(),
};
}
class ProjectDetail {
final String id;
final String name;
final String shortName;
final String address;
final DateTime assignedDate;
final Status? status;
final Client? client;
final List<Service>? services;
final int numberOfJobs;
final String contactName;
final String contactPhone;
final String contactEmail;
final DateTime createdAt;
final User? createdBy;
final DateTime updatedAt;
final User? updatedBy;
ProjectDetail({
required this.id,
required this.name,
required this.shortName,
required this.address,
required this.assignedDate,
this.status,
this.client,
this.services,
required this.numberOfJobs,
required this.contactName,
required this.contactPhone,
required this.contactEmail,
required this.createdAt,
this.createdBy,
required this.updatedAt,
this.updatedBy,
});
factory ProjectDetail.fromJson(Map<String, dynamic> json) {
return ProjectDetail(
id: json['id'] ?? '',
name: json['name'] ?? '',
shortName: json['shortName'] ?? '',
address: json['address'] ?? '',
assignedDate: DateTime.parse(json['assignedDate'] ?? DateTime.now().toIso8601String()),
status: json['status'] != null ? Status.fromJson(json['status']) : null,
client: json['client'] != null ? Client.fromJson(json['client']) : null,
services: json['services'] != null
? List<Service>.from(json['services'].map((x) => Service.fromJson(x)))
: [],
numberOfJobs: json['numberOfJobs'] ?? 0,
contactName: json['contactName'] ?? '',
contactPhone: json['contactPhone'] ?? '',
contactEmail: json['contactEmail'] ?? '',
createdAt: DateTime.parse(json['createdAt'] ?? DateTime.now().toIso8601String()),
createdBy: json['createdBy'] != null ? User.fromJson(json['createdBy']) : null,
updatedAt: DateTime.parse(json['updatedAt'] ?? DateTime.now().toIso8601String()),
updatedBy: json['updatedBy'] != null ? User.fromJson(json['updatedBy']) : null,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'shortName': shortName,
'address': address,
'assignedDate': assignedDate.toIso8601String(),
'status': status?.toJson(),
'client': client?.toJson(),
'services': services?.map((x) => x.toJson()).toList(),
'numberOfJobs': numberOfJobs,
'contactName': contactName,
'contactPhone': contactPhone,
'contactEmail': contactEmail,
'createdAt': createdAt.toIso8601String(),
'createdBy': createdBy?.toJson(),
'updatedAt': updatedAt.toIso8601String(),
'updatedBy': updatedBy?.toJson(),
};
}
class Status {
final String id;
final String status;
Status({required this.id, required this.status});
factory Status.fromJson(Map<String, dynamic> json) =>
Status(id: json['id'] ?? '', status: json['status'] ?? '');
Map<String, dynamic> toJson() => {'id': id, 'status': status};
}
class Client {
final String id;
final String name;
final String? email;
final String? contactPerson;
final String? address;
final String? contactNumber;
final int? sprid;
Client({
required this.id,
required this.name,
this.email,
this.contactPerson,
this.address,
this.contactNumber,
this.sprid,
});
factory Client.fromJson(Map<String, dynamic> json) => Client(
id: json['id'] ?? '',
name: json['name'] ?? '',
email: json['email'],
contactPerson: json['contactPerson'],
address: json['address'],
contactNumber: json['contactNumber'],
sprid: json['sprid'],
);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
'contactPerson': contactPerson,
'address': address,
'contactNumber': contactNumber,
'sprid': sprid,
};
}
class Service {
final String id;
final String name;
final String? description;
final bool isSystem;
final bool isActive;
Service({
required this.id,
required this.name,
this.description,
required this.isSystem,
required this.isActive,
});
factory Service.fromJson(Map<String, dynamic> json) => Service(
id: json['id'] ?? '',
name: json['name'] ?? '',
description: json['description'],
isSystem: json['isSystem'] ?? false,
isActive: json['isActive'] ?? false,
);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'description': description,
'isSystem': isSystem,
'isActive': isActive,
};
}
class User {
final String id;
final String firstName;
final String lastName;
final String email;
final String? photo;
final String? jobRoleId;
final String? jobRoleName;
User({
required this.id,
required this.firstName,
required this.lastName,
required this.email,
this.photo,
this.jobRoleId,
this.jobRoleName,
});
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] ?? '',
firstName: json['firstName'] ?? '',
lastName: json['lastName'] ?? '',
email: json['email'] ?? '',
photo: json['photo'],
jobRoleId: json['jobRoleId'],
jobRoleName: json['jobRoleName'],
);
Map<String, dynamic> toJson() => {
'id': id,
'firstName': firstName,
'lastName': lastName,
'email': email,
'photo': photo,
'jobRoleId': jobRoleId,
'jobRoleName': jobRoleName,
};
}

View File

@ -0,0 +1,127 @@
class ServiceProjectListModel {
final bool success;
final String message;
final ProjectData? data;
final dynamic errors;
final int statusCode;
final DateTime timestamp;
ServiceProjectListModel({
required this.success,
required this.message,
this.data,
this.errors,
required this.statusCode,
required this.timestamp,
});
factory ServiceProjectListModel.fromJson(Map<String, dynamic> json) {
return ServiceProjectListModel(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: json['data'] != null ? ProjectData.fromJson(json['data']) : null,
errors: json['errors'],
statusCode: json['statusCode'] ?? 0,
timestamp: DateTime.parse(json['timestamp'] ?? DateTime.now().toIso8601String()),
);
}
Map<String, dynamic> toJson() => {
'success': success,
'message': message,
'data': data?.toJson(),
'errors': errors,
'statusCode': statusCode,
'timestamp': timestamp.toIso8601String(),
};
}
class ProjectData {
final int currentPage;
final int totalPages;
final int totalEntities;
final List<ProjectItem>? data;
ProjectData({
required this.currentPage,
required this.totalPages,
required this.totalEntities,
this.data,
});
factory ProjectData.fromJson(Map<String, dynamic> json) {
return ProjectData(
currentPage: json['currentPage'] ?? 1,
totalPages: json['totalPages'] ?? 1,
totalEntities: json['totalEntites'] ?? 0,
data: json['data'] != null
? List<ProjectItem>.from(json['data'].map((x) => ProjectItem.fromJson(x)))
: [],
);
}
Map<String, dynamic> toJson() => {
'currentPage': currentPage,
'totalPages': totalPages,
'totalEntites': totalEntities,
'data': data?.map((x) => x.toJson()).toList(),
};
}
class ProjectItem {
final String id;
final String name;
final String shortName;
final String projectAddress;
final String contactPerson;
final DateTime startDate;
final DateTime endDate;
final String projectStatusId;
final int teamSize;
final double completedWork;
final double plannedWork;
ProjectItem({
required this.id,
required this.name,
required this.shortName,
required this.projectAddress,
required this.contactPerson,
required this.startDate,
required this.endDate,
required this.projectStatusId,
required this.teamSize,
required this.completedWork,
required this.plannedWork,
});
factory ProjectItem.fromJson(Map<String, dynamic> json) {
return ProjectItem(
id: json['id'] ?? '',
name: json['name'] ?? '',
shortName: json['shortName'] ?? '',
projectAddress: json['projectAddress'] ?? '',
contactPerson: json['contactPerson'] ?? '',
startDate: DateTime.parse(json['startDate'] ?? DateTime.now().toIso8601String()),
endDate: DateTime.parse(json['endDate'] ?? DateTime.now().toIso8601String()),
projectStatusId: json['projectStatusId'] ?? '',
teamSize: json['teamSize'] ?? 0,
completedWork: (json['completedWork']?.toDouble() ?? 0.0),
plannedWork: (json['plannedWork']?.toDouble() ?? 0.0),
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'shortName': shortName,
'projectAddress': projectAddress,
'contactPerson': contactPerson,
'startDate': startDate.toIso8601String(),
'endDate': endDate.toIso8601String(),
'projectStatusId': projectStatusId,
'teamSize': teamSize,
'completedWork': completedWork,
'plannedWork': plannedWork,
};
}

View File

@ -24,7 +24,6 @@ import 'package:marco/view/tenant/tenant_selection_screen.dart';
import 'package:marco/view/finance/finance_screen.dart';
import 'package:marco/view/finance/advance_payment_screen.dart';
import 'package:marco/view/finance/payment_request_screen.dart';
import 'package:marco/view/service_project/service_project_details_screen.dart';
import 'package:marco/view/service_project/service_project_screen.dart';
class AuthMiddleware extends GetMiddleware {
@override
@ -136,11 +135,6 @@ getPageRoute() {
),
// Service Projects
GetPage(
name: '/dashboard/service-project-details',
page: () => ServiceProjectDetailsScreen(),
middlewares: [AuthMiddleware()],
),
GetPage(
name: '/dashboard/service-projects',
page: () => ServiceProjectScreen(),

View File

@ -1,11 +1,16 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:marco/controller/service_project/service_project_details_screen_controller.dart';
import 'package:marco/helpers/utils/launcher_utils.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
class ServiceProjectDetailsScreen extends StatefulWidget {
const ServiceProjectDetailsScreen({super.key});
final String projectId;
const ServiceProjectDetailsScreen({super.key, required this.projectId});
@override
State<ServiceProjectDetailsScreen> createState() =>
@ -13,13 +18,17 @@ class ServiceProjectDetailsScreen extends StatefulWidget {
}
class _ServiceProjectDetailsScreenState
extends State<ServiceProjectDetailsScreen> with SingleTickerProviderStateMixin {
extends State<ServiceProjectDetailsScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final ServiceProjectDetailsController controller =
Get.put(ServiceProjectDetailsController());
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
controller.setProjectId(widget.projectId);
}
@override
@ -28,6 +37,263 @@ class _ServiceProjectDetailsScreenState
super.dispose();
}
// ---------------- Helper Widgets ----------------
Widget _buildDetailRow({
required IconData icon,
required String label,
required String value,
VoidCallback? onTap,
VoidCallback? onLongPress,
bool isActionable = false,
}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: InkWell(
onTap: isActionable && value != 'NA' ? onTap : null,
onLongPress: isActionable && value != 'NA' ? onLongPress : null,
borderRadius: BorderRadius.circular(5),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.redAccent.withOpacity(0.1),
borderRadius: BorderRadius.circular(5),
),
child: Icon(icon, size: 20, color: Colors.redAccent),
),
MySpacing.width(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
MySpacing.height(4),
Text(
value,
style: TextStyle(
fontSize: 15,
color: isActionable && value != 'NA'
? Colors.redAccent
: Colors.black87,
fontWeight: FontWeight.w500,
decoration: isActionable && value != 'NA'
? TextDecoration.underline
: TextDecoration.none,
),
),
],
),
),
if (isActionable && value != 'NA')
Icon(Icons.chevron_right, color: Colors.grey[400], size: 20),
],
),
),
);
}
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, color: Colors.redAccent),
MySpacing.width(8),
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black87),
),
],
),
MySpacing.height(8),
const Divider(),
...children,
],
),
),
);
}
String _formatDate(DateTime? date) {
if (date == null) return 'NA';
try {
return DateFormat('d/M/yyyy').format(date);
} catch (_) {
return 'NA';
}
}
Widget _buildProfileTab() {
final project = controller.projectDetail.value;
if (project == null) return const Center(child: Text("No project data"));
return Padding(
padding: MySpacing.all(12),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Header
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: 45, color: Colors.redAccent),
MySpacing.width(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleMedium(project.name, fontWeight: 700),
MySpacing.height(6),
MyText.bodySmall(
project.client?.name ?? 'N/A', fontWeight: 500),
],
),
),
],
),
),
),
MySpacing.height(16),
// Project Information
_buildSectionCard(
title: 'Project Information',
titleIcon: Icons.info_outline,
children: [
_buildDetailRow(
icon: Icons.calendar_today_outlined,
label: 'Assigned Date',
value: _formatDate(project.assignedDate),
),
_buildDetailRow(
icon: Icons.location_on_outlined,
label: 'Address',
value: project.address,
),
_buildDetailRow(
icon: Icons.people_outline,
label: 'Contact Name',
value: project.contactName,
),
_buildDetailRow(
icon: Icons.phone_outlined,
label: 'Contact Phone',
value: project.contactPhone,
isActionable: true,
onTap: () =>
LauncherUtils.launchPhone(project.contactPhone),
onLongPress: () => LauncherUtils.copyToClipboard(
project.contactPhone,
typeLabel: 'Phone'),
),
_buildDetailRow(
icon: Icons.email_outlined,
label: 'Contact Email',
value: project.contactEmail,
isActionable: true,
onTap: () =>
LauncherUtils.launchEmail(project.contactEmail),
onLongPress: () => LauncherUtils.copyToClipboard(
project.contactEmail,
typeLabel: 'Email'),
),
],
),
MySpacing.height(12),
// Status
if (project.status != null)
_buildSectionCard(
title: 'Status',
titleIcon: Icons.flag_outlined,
children: [
_buildDetailRow(
icon: Icons.info_outline,
label: 'Status',
value: project.status!.status,
),
],
),
// Services
if (project.services != null && project.services!.isNotEmpty)
_buildSectionCard(
title: 'Services',
titleIcon: Icons.miscellaneous_services_outlined,
children: project.services!.map((service) {
return _buildDetailRow(
icon: Icons.build_outlined,
label: service.name,
value: service.description ?? '-',
);
}).toList(),
),
MySpacing.height(12),
// Client Section
if (project.client != null)
_buildSectionCard(
title: 'Client Information',
titleIcon: Icons.business_outlined,
children: [
_buildDetailRow(
icon: Icons.person_outline,
label: 'Client Name',
value: project.client!.name,
),
_buildDetailRow(
icon: Icons.phone_outlined,
label: 'Client Phone',
value: project.client!.contactNumber ?? 'NA',
isActionable: true,
onTap: () => LauncherUtils.launchPhone(
project.client!.contactNumber ?? ''),
onLongPress: () => LauncherUtils.copyToClipboard(
project.client!.contactNumber ?? '',
typeLabel: 'Phone'),
),
],
),
MySpacing.height(40),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -47,7 +313,7 @@ class _ServiceProjectDetailsScreenState
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard'),
onPressed: () => Get.toNamed('/dashboard/service-projects'),
),
MySpacing.width(8),
Expanded(
@ -101,6 +367,8 @@ class _ServiceProjectDetailsScreenState
labelColor: Colors.black,
unselectedLabelColor: Colors.grey,
indicatorColor: Colors.red,
indicatorWeight: 3,
isScrollable: false,
tabs: const [
Tab(text: "Profile"),
Tab(text: "Jobs"),
@ -110,12 +378,25 @@ class _ServiceProjectDetailsScreenState
// ---------------- TabBarView ----------------
Expanded(
child: TabBarView(
controller: _tabController,
children: const [
// Add your tab content here later
],
),
child: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (controller.errorMessage.value.isNotEmpty) {
return Center(child: Text(controller.errorMessage.value));
}
return TabBarView(
controller: _tabController,
children: [
// Profile Tab
_buildProfileTab(),
// Jobs Tab - empty
Container(color: Colors.white),
],
);
}),
),
],
),

View File

@ -1,12 +1,13 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/helpers/widgets/avatar.dart';
import 'package:marco/controller/project_controller.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});
@ -18,190 +19,104 @@ class ServiceProjectScreen extends StatefulWidget {
class _ServiceProjectScreenState extends State<ServiceProjectScreen>
with UIMixin {
final TextEditingController searchController = TextEditingController();
final RxList<Map<String, dynamic>> allProjects = <Map<String, dynamic>>[].obs;
final RxList<Map<String, dynamic>> filteredProjects =
<Map<String, dynamic>>[].obs;
final ServiceProjectController controller =
Get.put(ServiceProjectController());
@override
void initState() {
super.initState();
_loadProjects();
}
void _loadProjects() {
final staticProjects = [
{
"name": "Website Redesign",
"description": "Revamping the corporate website UI/UX",
"status": "In Progress",
"manager": "John Doe",
"email": "john@company.com",
"phone": "+91 9876543210",
"tags": ["UI", "Frontend", "High Priority"]
},
{
"name": "Mobile App Development",
"description": "Cross-platform mobile app for customers",
"status": "Completed",
"manager": "Priya Sharma",
"email": "priya@company.com",
"phone": "+91 9812345678",
"tags": ["Flutter", "Backend"]
},
{
"name": "Data Migration",
"description": "Migrating legacy data to AWS",
"status": "Pending",
"manager": "Arun Mehta",
"email": "arun@company.com",
"phone": "+91 9999988888",
"tags": ["Database", "Cloud"]
},
];
allProjects.assignAll(staticProjects);
filteredProjects.assignAll(staticProjects);
}
void _filterProjects(String query) {
if (query.isEmpty) {
filteredProjects.assignAll(allProjects);
} else {
filteredProjects.assignAll(allProjects
.where((p) =>
p["name"].toLowerCase().contains(query.toLowerCase()) ||
p["manager"].toLowerCase().contains(query.toLowerCase()))
.toList());
}
controller.fetchProjects();
searchController.addListener(() {
controller.updateSearch(searchController.text);
});
}
Future<void> _refreshProjects() async {
await Future.delayed(const Duration(seconds: 1));
await controller.fetchProjects();
}
Widget _buildProjectCard(Map<String, dynamic> project) {
Widget _buildProjectCard(ProjectItem project) {
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
elevation: 3,
shadowColor: Colors.grey.withOpacity(0.3),
margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
shadowColor: Colors.indigo.withOpacity(0.10),
color: Colors.white,
child: InkWell(
borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(14),
onTap: () {
// TODO: Navigate to Project Details screen
// Navigate to ServiceProjectDetailsScreen
Get.to(
() => ServiceProjectDetailsScreen(projectId: project.id),
);
},
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Avatar(
firstName: project["name"].split(" ").first,
lastName: project["name"].split(" ").length > 1
? project["name"].split(" ").last
: "",
size: 40,
),
MySpacing.width(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleSmall(project["name"],
fontWeight: 600, overflow: TextOverflow.ellipsis),
MyText.bodySmall(project["description"],
color: Colors.grey[700],
overflow: TextOverflow.ellipsis),
MySpacing.height(6),
Row(
children: [
Icon(Icons.person_outline,
size: 16, color: Colors.indigo),
MySpacing.width(4),
MyText.labelSmall(project["manager"],
color: Colors.indigo),
],
),
MySpacing.height(4),
Row(
children: [
Icon(Icons.email_outlined,
size: 16, color: Colors.indigo),
MySpacing.width(4),
Expanded(
child: MyText.labelSmall(
project["email"],
color: Colors.indigo,
decoration: TextDecoration.underline,
overflow: TextOverflow.ellipsis,
),
),
],
),
MySpacing.height(4),
Row(
children: [
Icon(Icons.phone_outlined,
size: 16, color: Colors.indigo),
MySpacing.width(4),
Expanded(
child: MyText.labelSmall(
project["phone"],
color: Colors.indigo,
decoration: TextDecoration.underline,
overflow: TextOverflow.ellipsis,
),
),
MySpacing.width(8),
const FaIcon(FontAwesomeIcons.whatsapp,
color: Colors.green, size: 20),
],
),
MySpacing.height(6),
Wrap(
spacing: 6,
runSpacing: 2,
children: (project["tags"] as List<String>)
.map((tag) => Chip(
label: Text(tag),
backgroundColor: Colors.indigo.shade50,
labelStyle: const TextStyle(
color: Colors.indigo, fontSize: 12),
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
))
.toList(),
),
],
),
),
Column(
/// Header Row: Avatar | Name & Tags | Status
Row(
children: [
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: project["status"] == "Completed"
? Colors.green.shade100
: project["status"] == "In Progress"
? Colors.orange.shade100
: Colors.red.shade100,
borderRadius: BorderRadius.circular(12),
),
child: MyText.labelSmall(
project["status"],
fontWeight: 600,
color: project["status"] == "Completed"
? Colors.green
: project["status"] == "In Progress"
? Colors.orange
: Colors.red,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleMedium(
project.name,
fontWeight: 800,
),
MySpacing.height(2),
Row(
children: [
if (project.shortName.isNotEmpty)
_buildTag(project.shortName),
if (project.shortName.isNotEmpty)
MySpacing.width(6),
Icon(Icons.location_on,
size: 15, color: Colors.deepOrange.shade400),
MySpacing.width(2),
Flexible(
child: MyText.bodySmall(
project.projectAddress,
color: Colors.grey[700],
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
const SizedBox(height: 10),
const Icon(Icons.arrow_forward_ios,
color: Colors.grey, size: 20),
],
),
MySpacing.height(12),
_buildDetailRow(
Icons.date_range_outlined,
Colors.teal,
"${DateTimeUtils.convertUtcToLocal(project.startDate.toIso8601String(), format: DateTimeUtils.defaultFormat)} To "
"${DateTimeUtils.convertUtcToLocal(project.endDate.toIso8601String(), format: DateTimeUtils.defaultFormat)}",
fontSize: 13,
),
MySpacing.height(12),
/// Stats
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildStatColumn(Icons.people_alt_rounded, "Team",
"${project.teamSize}", Colors.blue[700]),
_buildStatColumn(
Icons.check_circle,
"Completed",
"${project.completedWork.toStringAsFixed(1)}%",
Colors.green[600]),
_buildStatColumn(
Icons.pending,
"Planned",
"${project.plannedWork.toStringAsFixed(1)}%",
Colors.orange[800]),
],
),
],
@ -211,6 +126,53 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
);
}
// Helper to build colored tags
Widget _buildTag(String label) {
return Container(
decoration: BoxDecoration(
color: Colors.indigo.withOpacity(0.08),
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child:
MyText.labelSmall(label, color: Colors.indigo[700], fontWeight: 500),
);
}
// Helper for detail row with icon and text
Widget _buildDetailRow(IconData icon, Color iconColor, String value,
{double fontSize = 12}) {
return Row(
children: [
Icon(icon, size: 19, color: iconColor),
MySpacing.width(8),
Flexible(
child: MyText.bodySmall(
value,
color: Colors.grey[900],
fontWeight: 500,
fontSize: fontSize,
overflow: TextOverflow.ellipsis,
),
),
],
);
}
// Helper for stats column (icon + label + value)
Widget _buildStatColumn(
IconData icon, String label, String value, Color? color) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(icon, color: color, size: 19),
SizedBox(height: 3),
MyText.labelSmall(value, color: color, fontWeight: 700),
MyText.bodySmall(label, color: Colors.grey[500], fontSize: 11),
],
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
@ -233,7 +195,7 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
/// --- SAME APPBAR AS DETAILS SCREEN ---
/// APPBAR
appBar: PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
@ -249,44 +211,13 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard'),
onPressed: () => Get.back(),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Service Projects',
fontWeight: 700,
color: Colors.black,
),
MySpacing.height(2),
GetBuilder<ProjectController>(
builder: (projectController) {
final projectName =
projectController.selectedProject?.name ??
'All Projects';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
projectName,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
);
},
),
],
),
MyText.titleLarge(
'Service Projects',
fontWeight: 700,
color: Colors.black,
),
],
),
@ -296,7 +227,7 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
body: Column(
children: [
/// --- SEARCH + FILTER BAR ---
/// SEARCH + FILTER BAR
Padding(
padding: MySpacing.xy(8, 8),
child: Row(
@ -306,7 +237,6 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
height: 35,
child: TextField(
controller: searchController,
onChanged: _filterProjects,
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(horizontal: 12),
@ -315,15 +245,14 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
suffixIcon: ValueListenableBuilder<TextEditingValue>(
valueListenable: searchController,
builder: (context, value, _) {
if (value.text.isEmpty) {
if (value.text.isEmpty)
return const SizedBox.shrink();
}
return IconButton(
icon: const Icon(Icons.clear,
size: 20, color: Colors.grey),
onPressed: () {
searchController.clear();
_filterProjects('');
controller.updateSearch('');
},
);
},
@ -402,24 +331,31 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
),
),
/// --- PROJECT LIST ---
/// PROJECT LIST
Expanded(
child: Obx(() => MyRefreshIndicator(
onRefresh: _refreshProjects,
backgroundColor: Colors.indigo,
color: Colors.white,
child: filteredProjects.isEmpty
? _buildEmptyState()
: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
padding: MySpacing.only(
left: 8, right: 8, top: 4, bottom: 80),
itemCount: filteredProjects.length,
separatorBuilder: (_, __) => MySpacing.height(12),
itemBuilder: (_, index) =>
_buildProjectCard(filteredProjects[index]),
),
)),
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]),
),
);
}),
),
],
),

View File

@ -227,8 +227,8 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
final buildings = dailyTasks
.expand((task) => task.buildings)
.where((building) =>
(building.plannedWork ?? 0) > 0 ||
(building.completedWork ?? 0) > 0)
(building.plannedWork ) > 0 ||
(building.completedWork ) > 0)
.toList();
if (buildings.isEmpty) {